diff --git a/include/meson.build b/include/meson.build
index e9b434f4a632d7b234cccd86f815a63128230eb1..a534bcd119435fc58562019d935b2b060e89c5c5 100644
--- a/include/meson.build
+++ b/include/meson.build
@@ -31,6 +31,7 @@ install_headers(
     'vlc_block_helper.h',
     'vlc_boxes.h',
     'vlc_charset.h',
+    'vlc_chroma_probe.h',
     'vlc_codec.h',
     'vlc_codecs.h',
     'vlc_common.h',
diff --git a/include/vlc_chroma_probe.h b/include/vlc_chroma_probe.h
new file mode 100644
index 0000000000000000000000000000000000000000..9f3a70733412dd380623d7361aa131c4eccd79c6
--- /dev/null
+++ b/include/vlc_chroma_probe.h
@@ -0,0 +1,259 @@
+/*****************************************************************************
+ * vlc_chroma_probe.h: chroma conversion probing
+ *****************************************************************************
+ * Copyright (C) 2025 VLC authors and VideoLAN
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef VLC_CHROMA_PROBE_H
+#define VLC_CHROMA_PROBE_H 1
+
+#include <vlc_common.h>
+#include <vlc_vector.h>
+
+/**
+ * \defgroup chroma_probe Chroma conversion probing
+ * \ingroup filter
+ * @{
+ * \file
+ * Chroma conversion probing
+ *
+ * \defgroup chroma_probe_api Chroma probing API
+ * \ingroup chroma_probe
+ *
+ * @{
+ */
+
+#define VLC_CHROMA_CONV_MAX_INDIRECT_STEPS 1
+#define VLC_CHROMA_CONV_CHAIN_COUNT_MAX (2 /* in + out */ + VLC_CHROMA_CONV_MAX_INDIRECT_STEPS)
+
+/**
+ * Chroma conversion result structure
+ */
+struct vlc_chroma_conv_result
+{
+    /**
+     * Array of chromas used to achieve the conversion
+     *
+     * 'chain[0]' is always equals to the 'in' argument of the
+     * vlc_chroma_conv_Probe() function.
+     *
+     * if the out argument of the vlc_chroma_conv_Probe() is valid,
+     * chain[chain_count - 1] is equals to 'out'
+     */
+    vlc_fourcc_t chain[VLC_CHROMA_CONV_CHAIN_COUNT_MAX];
+
+    /** Number of chromas in the chain */
+    size_t chain_count;
+
+    /**
+     * Cost of the full conversion, lower is better.
+     */
+    unsigned cost;
+
+    /**
+     * Quality of the conversion, higher is better.
+     *
+     * A quality of 100 means there are no quality loss: same color size and
+     * same vlc_chroma_subtype (or same YUV subsampling for video).
+     */
+    unsigned quality;
+};
+
+/** Only accept YUV output chromas (the input chroma can be RGB) */
+#define VLC_CHROMA_CONV_FLAG_ONLY_YUV 0x1
+/** Only accept RGB output chromas (the input chroma can be YUV) */
+#define VLC_CHROMA_CONV_FLAG_ONLY_RGB 0x2
+/** Sort results by cost instead of quality */
+#define VLC_CHROMA_CONV_FLAG_SORT_COST  0x4
+
+/**
+ * Probe possible chroma conversions
+
+ * Results are sorted by quality, unless VLC_CHROMA_CONV_FLAG_SORT_COST is
+ * specified in flags.
+
+ * @param in the input chroma to convert from, must be valid
+ * @param out the output chroma to convert to, if 0, the function will find all
+ * possible conversion from in to x
+ * @param width video width, used for finer cost calculation, can be 0
+ * @param height video height, used for finer cost calculation, can be 0
+ * @param max_indirect_steps maximum number of indirect conversion steps, must
+ * be lower or equal to @ref VLC_CHROMA_CONV_MAX_INDIRECT_STEPS, if in and out
+ * chromas are CPU chromas, the steps will be automatically lowered to 0
+ * @param flags bitwise flags, cf. VLC_CHROMA_CONV_FLAG_*
+ * @param count pointer to the number of results, must be valid
+ * @return a pointer to an array of results, must be released with free(), can
+ * be NULL
+ */
+VLC_API struct vlc_chroma_conv_result *
+vlc_chroma_conv_Probe(vlc_fourcc_t in, vlc_fourcc_t out,
+                      unsigned width, unsigned height,
+                      unsigned max_indirect_steps, int flags, size_t *count);
+
+/**
+ * Get a string representing the result
+ *
+ * @param res pointer to a valid result
+ * @return a string or NULL, must be released with free()
+ */
+VLC_API char *
+vlc_chroma_conv_result_ToString(const struct vlc_chroma_conv_result *res);
+
+/**
+ * @}
+ *
+ * \defgroup chroma_probe_module Chroma probing module implementation
+ * \ingroup chroma_probe
+ *
+ * @{
+ */
+
+/**
+ * Chroma conversion entry structure
+ */
+struct vlc_chroma_conv_entry
+{
+    /** Cost factor, 0.25 for GPU<->GPU conversions, 0.75 for SIMD, 1 for CPU */
+    float cost_factor;
+    /** input chroma */
+    vlc_fourcc_t in;
+    /** output chroma */
+    vlc_fourcc_t out;
+};
+typedef struct VLC_VECTOR(struct vlc_chroma_conv_entry) vlc_chroma_conv_vec;
+
+/**
+ * Module probe function signature
+ *
+ * @param vec pointer to an allocated vector
+ * @return a VLC error code
+ */
+typedef void (*vlc_chroma_conv_probe)(vlc_chroma_conv_vec *vec);
+
+#define set_callback_chroma_conv_probe(activate) \
+    { \
+        vlc_chroma_conv_probe activate__ = activate; \
+        (void) activate__; \
+        set_callback(activate) \
+    } \
+    set_capability("chroma probe", 100)
+
+/**
+ * Helper that add a chroma conversion
+ *
+ * Must be called inside vlc_chroma_conv_probe()
+ *
+ * @param vec pointer to the vector of chromas
+ * @param cost_factor cf. vlc_chroma_conv_entry.cost_factor
+ * @param in cf. vlc_chroma_conv_entry.in
+ * @param out cf. vlc_chroma_conv_entry.out
+ * @param twoway if true, 'out' can also be converted to 'in'
+ */
+static inline void
+vlc_chroma_conv_add(vlc_chroma_conv_vec *vec, float cost_factor,
+                    vlc_fourcc_t in, vlc_fourcc_t out, bool twoway)
+{
+    {
+        const struct vlc_chroma_conv_entry entry = {
+            cost_factor, in, out
+        };
+        vlc_vector_push(vec, entry);
+    }
+
+    if (twoway)
+    {
+        const struct vlc_chroma_conv_entry entry = {
+            cost_factor, out, in
+        };
+        vlc_vector_push(vec, entry);
+    }
+}
+
+/**
+ * Helper that add an array of out chroma conversions
+ *
+ * Must be called inside vlc_chroma_conv_probe()
+ *
+ * @param vec pointer to the vector of chromas
+ * @param cost_factor cf. vlc_chroma_conv_entry.cost_factor
+ * @param in cf. vlc_chroma_conv_entry.in
+ * @param out_array a list of out chromas
+ * @param out_count number of elements in the out_array
+ */
+static inline void
+vlc_chroma_conv_add_in_outarray(vlc_chroma_conv_vec *vec, float cost_factor,
+                                vlc_fourcc_t in,
+                                const vlc_fourcc_t *out_array, size_t out_count)
+{
+    for (size_t i = 0; i < out_count; i++)
+    {
+        const struct vlc_chroma_conv_entry entry = {
+            cost_factor, in, out_array[i],
+        };
+        vlc_vector_push(vec, entry);
+    }
+}
+
+/**
+ * Helper that add a list of out chroma conversions
+ */
+#define vlc_chroma_conv_add_in_outlist(vec, cost_factor, in, ...) do { \
+    static const vlc_fourcc_t out_array[] = { __VA_ARGS__ }; \
+    size_t count = ARRAY_SIZE(out_array); \
+    vlc_chroma_conv_add_in_outarray(vec, cost_factor, in, out_array, count); \
+} while(0)
+
+/**
+ * Helper that add an array of in chroma conversions
+ *
+ * Must be called inside vlc_chroma_conv_probe()
+ *
+ * @param vec pointer to the vector of chromas
+ * @param cost_factor cf. vlc_chroma_conv_entry.cost_factor
+ * @param out cf. vlc_chroma_conv_entry.out
+ * @param in_array a list of out chromas
+ * @param in_count number of elements in the in_array
+ */
+static inline void
+vlc_chroma_conv_add_out_inarray(vlc_chroma_conv_vec *vec, float cost_factor,
+                                vlc_fourcc_t out,
+                                const vlc_fourcc_t *in_array, size_t in_count)
+{
+    for (size_t i = 0; i < in_count; i++)
+    {
+        const struct vlc_chroma_conv_entry entry = {
+            cost_factor, in_array[i], out,
+        };
+        vlc_vector_push(vec, entry);
+    }
+}
+
+/**
+ * Helper that add a list of in chroma conversions
+ */
+#define vlc_chroma_conv_add_out_inlist(vec, cost_factor, out, ...) do { \
+    static const vlc_fourcc_t in_array[] = { __VA_ARGS__ }; \
+    size_t count = ARRAY_SIZE(in_array); \
+    vlc_chroma_conv_add_out_inarray(vec, cost_factor, out, in_array, count); \
+} while(0)
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* VLC_CHROMA_PROBE_H */
diff --git a/include/vlc_fourcc.h b/include/vlc_fourcc.h
index 045e5e24c6ad7067b36f2262260b742b8e2dd322..e42c2c9233be0fcdcb2e2e42870aac1218758a5b 100644
--- a/include/vlc_fourcc.h
+++ b/include/vlc_fourcc.h
@@ -777,37 +777,50 @@ VLC_API const char * vlc_fourcc_GetDescription( int i_cat, vlc_fourcc_t i_fourcc
  * It returns a list (terminated with the value 0) of YUV fourccs in
  * decreasing priority order for the given chroma.
  *
- * It will always return a non NULL pointer that must not be freed.
+ * It can return a NULL pointer, it must be freed.
  */
-VLC_API const vlc_fourcc_t * vlc_fourcc_GetYUVFallback( vlc_fourcc_t );
+VLC_API vlc_fourcc_t * vlc_fourcc_GetYUVFallback( vlc_fourcc_t );
 
 /**
  * It returns a list (terminated with the value 0) of RGB fourccs in
  * decreasing priority order for the given chroma.
  *
- * It will always return a non NULL pointer that must not be freed.
+ * It can return a NULL pointer, it must be freed.
  */
-VLC_API const vlc_fourcc_t * vlc_fourcc_GetRGBFallback( vlc_fourcc_t );
+VLC_API vlc_fourcc_t * vlc_fourcc_GetRGBFallback( vlc_fourcc_t );
 
 /**
  * It returns a list (terminated with the value 0) of fourccs in decreasing
  * priority order for the given chroma. It will return either YUV or RGB
  * fallbacks depending on whether or not the fourcc given is YUV.
  *
- * It will always return a non NULL pointer that must not be freed.
+ * It can return a NULL pointer, it must be freed.
  */
-VLC_API const vlc_fourcc_t * vlc_fourcc_GetFallback( vlc_fourcc_t );
+VLC_API vlc_fourcc_t * vlc_fourcc_GetFallback( vlc_fourcc_t );
 
 /**
- * It returns true if the given fourcc is YUV and false otherwise.
+ * Chroma subtype
  */
-VLC_API bool vlc_fourcc_IsYUV( vlc_fourcc_t );
+enum vlc_chroma_subtype
+{
+    VLC_CHROMA_SUBTYPE_OTHER,
+    VLC_CHROMA_SUBTYPE_YUV444,
+    VLC_CHROMA_SUBTYPE_YUV440,
+    VLC_CHROMA_SUBTYPE_YUV422,
+    VLC_CHROMA_SUBTYPE_YUV420,
+    VLC_CHROMA_SUBTYPE_YUV411,
+    VLC_CHROMA_SUBTYPE_YUV410,
+    VLC_CHROMA_SUBTYPE_YUV211,
+    VLC_CHROMA_SUBTYPE_RGB,
+    VLC_CHROMA_SUBTYPE_GREY,
+};
 
 /**
  * Chroma related information.
  */
 typedef struct {
     vlc_fourcc_t fcc;
+    enum vlc_chroma_subtype subtype;
     unsigned plane_count;
     struct {
         vlc_rational_t w;
@@ -815,6 +828,7 @@ typedef struct {
     } p[4];
     unsigned pixel_size;        /* Number of bytes per pixel for a plane */
     unsigned pixel_bits;        /* Number of bits actually used bits per pixel for a plane */
+    float color_bits;           /* Average number of bits used by one color */
 } vlc_chroma_description_t;
 
 /**
@@ -823,6 +837,36 @@ typedef struct {
  */
 VLC_API const vlc_chroma_description_t * vlc_fourcc_GetChromaDescription( vlc_fourcc_t fourcc ) VLC_USED;
 
+/**
+ * Returns true if the chroma description is YUV
+ */
+static inline bool
+vlc_chroma_description_IsYUV(const vlc_chroma_description_t *desc)
+{
+    switch (desc->subtype)
+    {
+        case VLC_CHROMA_SUBTYPE_YUV444:
+        case VLC_CHROMA_SUBTYPE_YUV440:
+        case VLC_CHROMA_SUBTYPE_YUV422:
+        case VLC_CHROMA_SUBTYPE_YUV420:
+        case VLC_CHROMA_SUBTYPE_YUV410:
+        case VLC_CHROMA_SUBTYPE_YUV411:
+        case VLC_CHROMA_SUBTYPE_YUV211:
+            return true;
+        default:
+            return false;
+    }
+}
+
+/**
+ * It returns true if the given fourcc is YUV and false otherwise.
+ */
+static inline bool vlc_fourcc_IsYUV(vlc_fourcc_t fcc)
+{
+    const vlc_chroma_description_t *desc = vlc_fourcc_GetChromaDescription(fcc);
+    return desc == NULL ? false : vlc_chroma_description_IsYUV(desc);
+}
+
 /**
  * Get the average usable bits per pixel for a chroma.
  * \note it may return 0 for opaque or compressed vlc_fourcc_t
diff --git a/modules/codec/avcodec/chroma.c b/modules/codec/avcodec/chroma.c
index cfd61a311dd91e55777dc4dff4f2c650e49226e8..520f36d8349d486be7443a9bc2fd894df8541dbb 100644
--- a/modules/codec/avcodec/chroma.c
+++ b/modules/codec/avcodec/chroma.c
@@ -38,13 +38,7 @@
 /*****************************************************************************
  * Chroma fourcc -> libavutil pixfmt mapping
  *****************************************************************************/
-static const struct
-{
-    vlc_fourcc_t  i_chroma;
-    enum AVPixelFormat i_chroma_id;
-    video_color_range_t range;
-
-} chroma_table[] =
+static const struct vlc_chroma_ffmpeg chroma_table[] =
 {
     /* Planar YUV formats */
     {VLC_CODEC_I444, AV_PIX_FMT_YUV444P,  COLOR_RANGE_UNDEF },
@@ -208,6 +202,13 @@ static const struct
     {VLC_CODEC_XYZ_12B, AV_PIX_FMT_XYZ12BE,  COLOR_RANGE_UNDEF },
 };
 
+
+const struct vlc_chroma_ffmpeg *GetVlcChromaFfmpegTable( size_t *count )
+{
+    *count = ARRAY_SIZE(chroma_table);
+    return chroma_table;
+}
+
 int GetVlcChroma( video_format_t *fmt, enum AVPixelFormat i_ffmpeg_chroma )
 {
     for( size_t i = 0; i < ARRAY_SIZE(chroma_table); i++ )
diff --git a/modules/codec/avcodec/chroma.h b/modules/codec/avcodec/chroma.h
index 7edbaa5209e727ef386ff7ed47f0e2d4ca7159f7..4372d4a0aeb51fe4b1079447832b42bb1c5a034a 100644
--- a/modules/codec/avcodec/chroma.h
+++ b/modules/codec/avcodec/chroma.h
@@ -27,6 +27,15 @@
 
 #include <libavutil/pixfmt.h>
 
+struct vlc_chroma_ffmpeg
+{
+    vlc_fourcc_t  i_chroma;
+    enum AVPixelFormat i_chroma_id;
+    video_color_range_t range;
+};
+
+const struct vlc_chroma_ffmpeg *GetVlcChromaFfmpegTable( size_t *count );
+
 enum AVPixelFormat FindFfmpegChroma( vlc_fourcc_t, bool *uv_flipped );
 
 int GetVlcChroma( video_format_t *fmt, enum AVPixelFormat i_ffmpeg_chroma );
diff --git a/modules/hw/d3d11/d3d11_filters.c b/modules/hw/d3d11/d3d11_filters.c
index 27a52cb35d8b245bac90eec05c662901ac9fe67a..905a6d4b20701e2985e653a4954ad7909833902f 100644
--- a/modules/hw/d3d11/d3d11_filters.c
+++ b/modules/hw/d3d11/d3d11_filters.c
@@ -34,6 +34,7 @@
 #include <vlc_filter.h>
 #include <vlc_picture.h>
 #include <vlc_codec.h>
+#include <vlc_chroma_probe.h>
 
 #define COBJMACROS
 #include <d3d11.h>
@@ -543,6 +544,26 @@ error:
     return VLC_EGENERIC;
 }
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define OUT_GPU_CHROMAS VLC_CODEC_D3D11_OPAQUE, VLC_CODEC_D3D11_OPAQUE_10B
+
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D11_OPAQUE, VLC_CODEC_I420,
+        VLC_CODEC_YV12, VLC_CODEC_NV12);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D11_OPAQUE_10B,
+        VLC_CODEC_I420_10L, VLC_CODEC_P010);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D11_OPAQUE_RGBA,
+        VLC_CODEC_RGBA);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D11_OPAQUE_BGRA,
+        VLC_CODEC_BGRA);
+
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420_10L, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_YV12, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_NV12, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_P010, OUT_GPU_CHROMAS);
+}
+
 vlc_module_begin()
     set_description(N_("Direct3D11 adjust filter"))
     set_subcategory( SUBCAT_VIDEO_VFILTER )
@@ -589,4 +610,6 @@ vlc_module_begin()
     set_callbacks( D3D11OpenBlockDecoder, D3D11CloseBlockDecoder )
     set_capability( "video decoder", 90 )
 
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
diff --git a/modules/hw/d3d9/d3d9_filters.c b/modules/hw/d3d9/d3d9_filters.c
index f7ba764ca670c7852db4d7591e23d4a125eeb805..309001359d4e64e0b1fabb5091e8246bb9747b0b 100644
--- a/modules/hw/d3d9/d3d9_filters.c
+++ b/modules/hw/d3d9/d3d9_filters.c
@@ -34,6 +34,7 @@
 #include <vlc_filter.h>
 #include <vlc_picture.h>
 #include <vlc_codec.h>
+#include <vlc_chroma_probe.h>
 
 #define COBJMACROS
 #include <initguid.h>
@@ -457,6 +458,20 @@ error:
     return VLC_EGENERIC;
 }
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define OUT_GPU_CHROMAS VLC_CODEC_D3D9_OPAQUE, VLC_CODEC_D3D9_OPAQUE_10B
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D9_OPAQUE, VLC_CODEC_I420,
+        VLC_CODEC_YV12, VLC_CODEC_NV12);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_D3D9_OPAQUE_10B,
+        VLC_CODEC_I420_10L, VLC_CODEC_P010);
+
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_YV12, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420_10L, OUT_GPU_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_P010, OUT_GPU_CHROMAS);
+}
+
 vlc_module_begin()
     set_description(N_("Direct3D9 adjust filter"))
     set_subcategory(SUBCAT_VIDEO_VFILTER)
@@ -493,4 +508,6 @@ vlc_module_begin()
     set_description(N_("Direct3D9"))
     set_callback_dec_device( D3D9OpenDecoderDevice, 10 )
     add_shortcut ("dxva2")
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
diff --git a/modules/hw/mmal/converter.c b/modules/hw/mmal/converter.c
index cbdc2b4b70cdafb8ebbae5cdc4f87da188c058f0..5dda06e4b6e1204c9bcd1aab7192afe7ca339b37 100644
--- a/modules/hw/mmal/converter.c
+++ b/modules/hw/mmal/converter.c
@@ -32,6 +32,7 @@
 #include <vlc_codec.h>
 #include <vlc_filter.h>
 #include <vlc_plugin.h>
+#include <vlc_chroma_probe.h>
 
 #include <interface/mmal/mmal.h>
 #include <interface/mmal/util/mmal_util.h>
@@ -65,6 +66,20 @@ static const char * const  ppsz_converter_text[] = {
 
 static int OpenConverter(filter_t *);
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define OUT_CHROMAS \
+    VLC_CODEC_BGRX, VLC_CODEC_RGBX, VLC_CODEC_XBGR, \
+    VLC_CODEC_XRGB, VLC_CODEC_RGB565BE, VLC_CODEC_RGBA, \
+    VLC_CODEC_BGRA, VLC_CODEC_ARGB, VLC_CODEC_ABGR
+
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_I420_10L, OUT_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_I420, OUT_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_MMAL_OPAQUE, OUT_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420_10L, VLC_CODEC_MMAL_OPAQUE);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_I420, VLC_CODEC_MMAL_OPAQUE);
+}
+
 vlc_module_begin()
     set_subcategory( SUBCAT_VIDEO_VFILTER )
     set_shortname(N_("MMAL resizer"))
@@ -75,6 +90,8 @@ vlc_module_begin()
         change_integer_list( pi_converter_modes, ppsz_converter_text )
 #endif
     set_callback_video_converter(OpenConverter, 900)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
 
 #define MMAL_SLICE_HEIGHT 16
diff --git a/modules/hw/nvdec/chroma.c b/modules/hw/nvdec/chroma.c
index 5739f5886b4b932f869de717a194e74dc6d8a3dc..d8bc4fac61c0407517900e815f92de3e970c8fd0 100644
--- a/modules/hw/nvdec/chroma.c
+++ b/modules/hw/nvdec/chroma.c
@@ -28,17 +28,34 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_codec.h>
+#include <vlc_chroma_probe.h>
 
 #include "nvdec_fmt.h"
 #include "nvdec_priv.h"
 
 static int OpenCUDAToCPU( filter_t * );
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_NVDEC_OPAQUE, VLC_CODEC_NV12,
+                        false);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_NVDEC_OPAQUE_10B, VLC_CODEC_P010,
+                        false);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_NVDEC_OPAQUE_16B, VLC_CODEC_P016,
+                        false);
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_NVDEC_OPAQUE_444,
+        VLC_CODEC_I444, VLC_CODEC_YUVA);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_NVDEC_OPAQUE_444_16B, VLC_CODEC_I444_16L,
+                        false);
+}
+
 vlc_module_begin()
     set_shortname(N_("CUDA converter"))
     set_description(N_("CUDA/NVDEC Chroma Converter filter"))
     set_subcategory(SUBCAT_VIDEO_VFILTER)
     set_callback_video_converter(OpenCUDAToCPU, 10)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
 
 #define CALL_CUDA(func, ...) CudaCheckErr(VLC_OBJECT(p_filter), devsys->cudaFunctions, devsys->cudaFunctions->func(__VA_ARGS__), #func)
diff --git a/modules/hw/vaapi/filters.c b/modules/hw/vaapi/filters.c
index a0b4fc3f833d7477782d3beb3625ca9d854e5e05..40369b8929c4091a1c8ec29a8840a70d930a98e4 100644
--- a/modules/hw/vaapi/filters.c
+++ b/modules/hw/vaapi/filters.c
@@ -30,6 +30,7 @@
 #include <vlc_common.h>
 #include <vlc_filter.h>
 #include <vlc_plugin.h>
+#include <vlc_chroma_probe.h>
 #include "filters.h"
 
 /********************************
@@ -1118,6 +1119,15 @@ error:
  * Module descriptor *
  *********************/
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420, VLC_CODEC_I420, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_I420_10L, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_12BPP, VLC_CODEC_P012, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_12BPP, VLC_CODEC_I420_12L, true);
+}
+
 vlc_module_begin()
     set_shortname(N_("VAAPI filters"))
     set_description(N_("Video Accelerated API filters"))
@@ -1144,4 +1154,7 @@ vlc_module_begin()
 
     add_submodule()
     set_callback_video_converter(vlc_vaapi_OpenChroma, 10)
+
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
diff --git a/modules/hw/vdpau/chroma.c b/modules/hw/vdpau/chroma.c
index b492be1ace8b5a637871adfa3a83e4c03f4acbc2..83fb18a01fe4878831edc1622a631abe42f23027 100644
--- a/modules/hw/vdpau/chroma.c
+++ b/modules/hw/vdpau/chroma.c
@@ -31,6 +31,7 @@
 #include <vlc_filter.h>
 #include <vlc_picture.h>
 #include <vlc_picture_pool.h>
+#include <vlc_chroma_probe.h>
 #include "vlc_vdpau.h"
 
 /* Picture history as recommended by VDPAU documentation */
@@ -787,6 +788,16 @@ static const char *const algo_names[] = {
     N_("Bob"), N_("Temporal"), N_("Temporal-spatial"),
 };
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_VDPAU_VIDEO,
+        VLC_CODEC_VDPAU_OUTPUT, false);
+
+    vlc_chroma_conv_add_in_outlist(vec, 1.1, VLC_CODEC_VDPAU_VIDEO, VLC_CODEC_I420,
+        VLC_CODEC_YV12, VLC_CODEC_NV12, VLC_CODEC_I422, VLC_CODEC_NV16,
+        VLC_CODEC_YUYV, VLC_CODEC_UYVY, VLC_CODEC_I444, VLC_CODEC_NV24);
+}
+
 vlc_module_begin()
     set_shortname(N_("VDPAU"))
     set_description(N_("VDPAU surface conversions"))
@@ -809,4 +820,6 @@ vlc_module_begin()
 
     add_submodule()
     set_callback_video_converter(YCbCrOpen, 10)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
diff --git a/modules/isa/arm/neon/chroma_yuv.c b/modules/isa/arm/neon/chroma_yuv.c
index 9b26fdfb6560ae3bbdea96972b6e9b53110c6493..b2e744b45e3867da4a17151cf8bb8d4641c46692 100644
--- a/modules/isa/arm/neon/chroma_yuv.c
+++ b/modules/isa/arm/neon/chroma_yuv.c
@@ -26,14 +26,35 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_cpu.h>
 #include "chroma_neon.h"
 
 static int Open (filter_t *);
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define PACKED_CHROMAS VLC_CODEC_YUYV, VLC_CODEC_UYVY, VLC_CODEC_YVYU, VLC_CODEC_VYUY
+
+    vlc_chroma_conv_add_in_outlist(vec, 0.75, VLC_CODEC_I420, PACKED_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 0.75, VLC_CODEC_YV12, PACKED_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, 0.75, VLC_CODEC_I422, PACKED_CHROMAS);
+
+    vlc_chroma_conv_add_in_outlist(vec, 0.75, VLC_CODEC_NV12, VLC_CODEC_I420,
+        VLC_CODEC_YV12);
+    vlc_chroma_conv_add_in_outlist(vec, 0.75, VLC_CODEC_NV21, VLC_CODEC_I420,
+        VLC_CODEC_YV12);
+
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_NV24, VLC_CODEC_I444, false);
+
+    vlc_chroma_conv_add_out_inlist(vec, 0.75, VLC_CODEC_I422, VLC_CODEC_NV16,
+        PACKED_CHROMAS);
+}
 vlc_module_begin ()
     set_description (N_("ARM NEON video chroma conversions"))
     set_callback_video_converter(Open, 250)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 #define DEFINE_PACK(pack, pict) \
diff --git a/modules/isa/arm/neon/yuv_rgb.c b/modules/isa/arm/neon/yuv_rgb.c
index 67a8e559685251eb7122e80814a29ea5b2d688f7..251c6e6760b173cff3175ed6a438b18795c13599 100644
--- a/modules/isa/arm/neon/yuv_rgb.c
+++ b/modules/isa/arm/neon/yuv_rgb.c
@@ -27,14 +27,26 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_cpu.h>
 #include "chroma_neon.h"
 
 static int Open (filter_t *);
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_I420, VLC_CODEC_RGB565LE, false);
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_I420, VLC_CODEC_XBGR, false);
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_YV12, VLC_CODEC_XBGR, false);
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_NV21, VLC_CODEC_XBGR, false);
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_NV12, VLC_CODEC_XBGR, false);
+}
+
 vlc_module_begin ()
     set_description (N_("ARM NEON video chroma YUV->RGBA"))
     set_callback_video_converter(Open, 250)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 /*
diff --git a/modules/video_chroma/chain.c b/modules/video_chroma/chain.c
index 34957d9a8212deaad88b24e07c35fe8294bbadcc..43de130f571e97ba62dee1c1eb333ce9c7bd14d0 100644
--- a/modules/video_chroma/chain.c
+++ b/modules/video_chroma/chain.c
@@ -33,6 +33,7 @@
 #include <vlc_filter.h>
 #include <vlc_mouse.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 /*****************************************************************************
  * Module descriptor
@@ -66,80 +67,6 @@ static void EsFormatMergeSize( es_format_t *p_dst,
                                const es_format_t *p_base,
                                const es_format_t *p_size );
 
-#define ALLOWED_CHROMAS_YUV10 \
-    VLC_CODEC_I420_10L, \
-    VLC_CODEC_I420_10B, \
-    VLC_CODEC_I420_12B, \
-    VLC_CODEC_I420_12L, \
-    VLC_CODEC_I420_16L \
-
-static const vlc_fourcc_t pi_allowed_chromas_yuv[] = {
-    VLC_CODEC_I420,
-    VLC_CODEC_I422,
-    ALLOWED_CHROMAS_YUV10,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_BGRA,
-    0
-};
-
-static const vlc_fourcc_t pi_allowed_chromas_yuv10[] = {
-    ALLOWED_CHROMAS_YUV10,
-    VLC_CODEC_I420,
-    VLC_CODEC_I422,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_BGRA,
-    0
-};
-
-static const vlc_fourcc_t pi_allowed_chromas_yuv444[] = {
-    VLC_CODEC_RGBA,
-    VLC_CODEC_BGRA,
-    VLC_CODEC_I422,
-    VLC_CODEC_I420,
-    0
-};
-
-static const vlc_fourcc_t pi_allowed_chromas_yuv444_10[] = {
-    VLC_CODEC_RGBA10LE,
-    VLC_CODEC_RGBA64,
-    ALLOWED_CHROMAS_YUV10,
-    VLC_CODEC_I422,
-    VLC_CODEC_I420,
-    0
-};
-
-static const vlc_fourcc_t *get_allowed_chromas( filter_t *p_filter )
-{
-    switch (p_filter->fmt_out.video.i_chroma)
-    {
-        case VLC_CODEC_I420_10L:
-        case VLC_CODEC_I420_10B:
-        case VLC_CODEC_I420_12L:
-        case VLC_CODEC_I420_12B:
-        case VLC_CODEC_I420_16L:
-        case VLC_CODEC_CVPX_P010:
-        case VLC_CODEC_D3D9_OPAQUE_10B:
-        case VLC_CODEC_D3D11_OPAQUE_10B:
-        case VLC_CODEC_VAAPI_420_10BPP:
-        case VLC_CODEC_VAAPI_420_12BPP:
-            return pi_allowed_chromas_yuv10;
-        case VLC_CODEC_I444:
-            return pi_allowed_chromas_yuv444;
-        case VLC_CODEC_I444_10L:
-        case VLC_CODEC_I444_12L:
-        case VLC_CODEC_I444_16L:
-            return pi_allowed_chromas_yuv444_10;
-        default:
-            return pi_allowed_chromas_yuv;
-    }
-}
-
 typedef struct
 {
     filter_chain_t *p_chain;
@@ -212,21 +139,7 @@ static int Activate( filter_t *p_filter, int (*pf_build)(filter_t *) )
         return VLC_EGENERIC;
     }
 
-    int type = VLC_VAR_INTEGER;
-    if( var_Type( vlc_object_parent(p_filter), "chain-level" ) != 0 )
-        type |= VLC_VAR_DOINHERIT;
-
-    var_Create( p_filter, "chain-level", type );
-    /* Note: atomicity is not actually needed here. */
-    var_IncInteger( p_filter, "chain-level" );
-
-    int level = var_GetInteger( p_filter, "chain-level" );
-    if( level < 0 || level > CHAIN_LEVEL_MAX )
-        msg_Err( p_filter, "Too high level of recursion (%d)", level );
-    else
-        i_ret = pf_build( p_filter );
-
-    var_Destroy( p_filter, "chain-level" );
+    i_ret = pf_build( p_filter );
 
     if( i_ret )
     {
@@ -263,9 +176,16 @@ static int ActivateConverter( filter_t *p_filter )
     if( !b_chroma && !b_chroma_resize && !b_transform)
         return VLC_EGENERIC;
 
-    return Activate( p_filter, b_transform ? BuildTransformChain :
-                               b_chroma_resize ? BuildChromaResize :
-                               BuildChromaChain );
+    if( var_Type( vlc_object_parent(p_filter), "chain-level" ) != 0 )
+        return VLC_EGENERIC;
+    var_Create( p_filter, "chain-level", VLC_VAR_INTEGER );
+
+    int ret = Activate( p_filter, b_transform ? BuildTransformChain :
+                        b_chroma_resize ? BuildChromaResize :
+                        BuildChromaChain );
+
+    var_Destroy( p_filter, "chain-level" );
+    return ret;
 }
 
 static int ActivateFilter( filter_t *p_filter )
@@ -273,14 +193,14 @@ static int ActivateFilter( filter_t *p_filter )
     if( !p_filter->b_allow_fmt_out_change || p_filter->psz_name == NULL )
         return VLC_EGENERIC;
 
-    if( var_Type( vlc_object_parent(p_filter), "chain-filter-level" ) != 0 )
+    if( var_Type( vlc_object_parent(p_filter), "chain-level" ) != 0 )
         return VLC_EGENERIC;
+    var_Create( p_filter, "chain-level", VLC_VAR_INTEGER );
 
-    var_Create( p_filter, "chain-filter-level", VLC_VAR_INTEGER );
-    int i_ret = Activate( p_filter, BuildFilterChain );
-    var_Destroy( p_filter, "chain-filter-level" );
+    int ret = Activate( p_filter, BuildFilterChain );
 
-    return i_ret;
+    var_Destroy( p_filter, "chain-level" );
+    return ret;
 }
 
 static void Destroy( filter_t *p_filter )
@@ -361,33 +281,66 @@ static int BuildChromaResize( filter_t *p_filter )
     return VLC_EGENERIC;
 }
 
-static int BuildChromaChain( filter_t *p_filter )
+static int AppendChromaChain( filter_t *p_filter, const vlc_fourcc_t *chromas,
+                              size_t chroma_count )
 {
+    filter_sys_t *p_sys = p_filter->p_sys;
     es_format_t fmt_mid;
-    int i_ret = VLC_EGENERIC;
 
-    /* Now try chroma format list */
-    const vlc_fourcc_t *pi_allowed_chromas = get_allowed_chromas( p_filter );
-    for( int i = 0; pi_allowed_chromas[i]; i++ )
+    for( size_t i = 0; i < chroma_count; ++i )
     {
-        const vlc_fourcc_t i_chroma = pi_allowed_chromas[i];
-        if( i_chroma == p_filter->fmt_in.i_codec ||
-            i_chroma == p_filter->fmt_out.i_codec )
-            continue;
-
-        msg_Dbg( p_filter, "Trying to use chroma %4.4s as middle man",
-                 (char*)&i_chroma );
-
         es_format_Copy( &fmt_mid, &p_filter->fmt_in );
-        fmt_mid.i_codec        =
-        fmt_mid.video.i_chroma = i_chroma;
+        fmt_mid.i_codec = fmt_mid.video.i_chroma = chromas[i];
 
-        i_ret = CreateChain( p_filter, &fmt_mid );
+        int i_ret = filter_chain_AppendConverter( p_sys->p_chain, &fmt_mid );
         es_format_Clean( &fmt_mid );
+        if ( i_ret != VLC_SUCCESS )
+            return i_ret;
+    }
+
+    return VLC_SUCCESS;
+}
+
+static int BuildChromaChain( filter_t *p_filter )
+{
+    filter_sys_t *p_sys = p_filter->p_sys;
+    int i_ret = VLC_EGENERIC;
 
+    size_t res_count;
+    struct vlc_chroma_conv_result *results =
+        vlc_chroma_conv_Probe( p_filter->fmt_in.video.i_chroma,
+                               p_filter->fmt_out.video.i_chroma,
+                               p_filter->fmt_in.video.i_width,
+                               p_filter->fmt_in.video.i_height, 1,
+                               0, &res_count );
+    if( results == NULL )
+        return i_ret;
+
+    /* Now try chroma format list */
+    for( size_t i = 0; i < res_count; ++i )
+    {
+        const struct vlc_chroma_conv_result *res = &results[i];
+        char *res_str = vlc_chroma_conv_result_ToString( res );
+        if( res_str == NULL )
+        {
+            i_ret = VLC_ENOMEM;
+            break;
+        }
+        msg_Info( p_filter, "Trying to use chroma_chain: %s...", res_str);
+        free(res_str);
+
+        filter_chain_Reset( p_sys->p_chain, &p_filter->fmt_in, p_filter->vctx_in,
+                            &p_filter->fmt_out );
+
+        i_ret = AppendChromaChain( p_filter, &res->chain[1], res->chain_count - 1);
         if( i_ret == VLC_SUCCESS )
+        {
+            p_filter->vctx_out = filter_chain_GetVideoCtxOut( p_sys->p_chain );
+            msg_Info( p_filter, "success");
             break;
+        }
     }
+    free( results );
 
     return i_ret;
 }
@@ -399,54 +352,91 @@ static int ChainMouse( filter_t *p_filter, vlc_mouse_t *p_mouse,
     return filter_chain_MouseFilter( p_sys->p_chain, p_mouse, p_old );
 }
 
+static bool
+CheckFilterChroma( filter_t *p_filter, vlc_fourcc_t chroma, const char *name )
+{
+    filter_t *test = vlc_object_create( p_filter, sizeof(filter_t) );
+    if (test == NULL)
+        return false;
+
+    es_format_t fmt = p_filter->fmt_out;
+    fmt.i_codec = fmt.video.i_chroma = chroma;
+    test->fmt_in = fmt;
+    test->fmt_out = fmt;
+
+    test->p_module = vlc_filter_LoadModule( test, "video filter", name, true );
+    bool success = test->p_module != NULL;
+    vlc_filter_Delete( test );
+    return success;
+}
+
 static int BuildFilterChain( filter_t *p_filter )
 {
-    es_format_t fmt_mid;
-    es_format_Init(&fmt_mid, p_filter->fmt_in.i_cat, p_filter->fmt_in.i_codec);
+    filter_sys_t *p_sys = p_filter->p_sys;
     int i_ret = VLC_EGENERIC;
 
-    filter_sys_t *p_sys = p_filter->p_sys;
+    assert( p_filter->b_allow_fmt_out_change );
 
-    /* Now try chroma format list */
-    const vlc_fourcc_t *pi_allowed_chromas = get_allowed_chromas( p_filter );
-    for( int i = 0; pi_allowed_chromas[i]; i++ )
-    {
-        filter_chain_Reset( p_sys->p_chain, &p_filter->fmt_in, p_filter->vctx_in, &p_filter->fmt_out );
+    /* Search for in -> x conversion */
+    size_t res_count;
+    struct vlc_chroma_conv_result *results =
+        vlc_chroma_conv_Probe( p_filter->fmt_in.video.i_chroma, 0,
+                               p_filter->fmt_in.video.i_width,
+                               p_filter->fmt_in.video.i_height, 1,
+                               0, &res_count );
+    if( results == NULL )
+        return i_ret;
 
-        const vlc_fourcc_t i_chroma = pi_allowed_chromas[i];
-        if( i_chroma == p_filter->fmt_in.i_codec ||
-            i_chroma == p_filter->fmt_out.i_codec )
+    for( size_t i = 0; i < res_count; ++i )
+    {
+        const struct vlc_chroma_conv_result *res = &results[i];
+        assert(res->chain_count >= 2);
+
+        /* Check first if the filter could accept the new output format. This
+         * might be faster to fail now than failing after creating the whole
+         * chroma chain */
+        if( !CheckFilterChroma( p_filter, res->chain[res->chain_count - 1],
+                                p_filter->psz_name ) )
             continue;
 
-        msg_Dbg( p_filter, "Trying to use chroma %4.4s as middle man in chain (%p)",
-                 (char*)&i_chroma, (void*)p_sys->p_chain );
+        char *res_str = vlc_chroma_conv_result_ToString( res );
+        if( res_str == NULL )
+        {
+            i_ret = VLC_ENOMEM;
+            break;
+        }
 
-        es_format_Clean( &fmt_mid );
-        es_format_Copy( &fmt_mid, &p_filter->fmt_in );
-        fmt_mid.i_codec        =
-        fmt_mid.video.i_chroma = i_chroma;
+        msg_Info( p_filter, "Trying to use chain: %s -> %s",
+                  res_str, p_filter->psz_name );
 
-        if( filter_chain_AppendConverter( p_sys->p_chain,
-                                          &fmt_mid ) != VLC_SUCCESS )
+        free( res_str );
+
+        filter_chain_Reset( p_sys->p_chain, &p_filter->fmt_in, p_filter->vctx_in,
+                            &p_filter->fmt_out );
+
+        i_ret = AppendChromaChain( p_filter, &res->chain[1],
+                                   res->chain_count - 1 );
+        if( i_ret != VLC_SUCCESS )
             continue;
 
         p_sys->p_video_filter =
             filter_chain_AppendFilter( p_sys->p_chain,
                                        p_filter->psz_name, p_filter->p_cfg,
-                                       &fmt_mid );
+                                       filter_chain_GetFmtOut( p_sys->p_chain ) );
         if( p_sys->p_video_filter == NULL)
+        {
+            i_ret = VLC_EGENERIC;
             continue;
+        }
 
         filter_AddProxyCallbacks( p_filter,
                                   p_sys->p_video_filter,
                                   RestartFilterCallback );
 
-        i_ret = VLC_SUCCESS;
         p_filter->vctx_out = filter_chain_GetVideoCtxOut( p_sys->p_chain );
         break;
     }
-
-    es_format_Clean( &fmt_mid );
+    free( results );
 
     if( i_ret != VLC_SUCCESS )
         filter_chain_Reset( p_sys->p_chain, &p_filter->fmt_in, p_filter->vctx_in, &p_filter->fmt_out );
diff --git a/modules/video_chroma/copy.h b/modules/video_chroma/copy.h
index 7158a703e73dec16669acafb2e465e81e53980af..08136f27d4f016960ab455065c4d6ae02ca38ff1 100644
--- a/modules/video_chroma/copy.h
+++ b/modules/video_chroma/copy.h
@@ -29,6 +29,13 @@
 extern "C" {
 #endif
 
+#ifdef CAN_COMPILE_SSE2
+#define COPY_COST 0.75
+#else
+#define COPY_COST 1
+#endif
+
+
 typedef struct {
 # ifdef CAN_COMPILE_SSE2
     uint8_t *buffer;
diff --git a/modules/video_chroma/cvpx.c b/modules/video_chroma/cvpx.c
index 9ecf5b050734807529373fd17b31e7557981a684..1d16158f5a04c314fe2d8049c5d17f3269955bd8 100644
--- a/modules/video_chroma/cvpx.c
+++ b/modules/video_chroma/cvpx.c
@@ -33,6 +33,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_modules.h>
 #include "../codec/vt_utils.h"
 #include "../video_chroma/copy.h"
@@ -64,6 +65,31 @@ typedef struct
     };
 } filter_sys_t;
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_BGRA, VLC_CODEC_CVPX_I420, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_BGRA, VLC_CODEC_CVPX_NV12, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_BGRA, VLC_CODEC_CVPX_P010, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_BGRA, VLC_CODEC_CVPX_UYVY, true);
+
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_I420, VLC_CODEC_CVPX_NV12, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_I420, VLC_CODEC_CVPX_P010, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_I420, VLC_CODEC_CVPX_UYVY, true);
+
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_NV12, VLC_CODEC_CVPX_P010, true);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_NV12, VLC_CODEC_CVPX_UYVY, true);
+
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_P010, VLC_CODEC_CVPX_UYVY, true);
+
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_NV12, VLC_CODEC_NV12, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_NV12, VLC_CODEC_I420, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_P010, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_P010, VLC_CODEC_I420_10L, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_UYVY, VLC_CODEC_UYVY, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_I420, VLC_CODEC_I420, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_BGRA, VLC_CODEC_BGRA, true);
+}
+
 vlc_module_begin ()
     set_description("Conversions from/to CoreVideo buffers")
     set_callback_video_converter(Open, 10)
@@ -76,6 +102,8 @@ vlc_module_begin ()
     set_description("Fast CoreVideo resize+conversion")
     set_callback_video_converter(Open_chain_CVPX, 11)
 #endif
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 
diff --git a/modules/video_chroma/grey_yuv.c b/modules/video_chroma/grey_yuv.c
index 1339362ae89e94e18981cef6a3e5bbcdc56e72f5..907d53023eb1f5b9331ff7a0300f2ee01eaadbf4 100644
--- a/modules/video_chroma/grey_yuv.c
+++ b/modules/video_chroma/grey_yuv.c
@@ -32,6 +32,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 #define SRC_FOURCC  "GREY"
 #define DEST_FOURCC "I420,YUY2"
@@ -44,9 +45,17 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor.
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_GREY, VLC_CODEC_I420,
+        VLC_CODEC_YUYV);
+}
+
 vlc_module_begin ()
     set_description( N_("Conversions from " SRC_FOURCC " to " DEST_FOURCC) )
     set_callback_video_converter( Activate, 80 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 VIDEO_FILTER_WRAPPER( GREY_I420 )
diff --git a/modules/video_chroma/gst_mem.c b/modules/video_chroma/gst_mem.c
index 775ad7307faa63c51169abe4bd73913dcf0cb1fc..e1c13654b6e50ba269b730da7f28fee9d0ca809e 100644
--- a/modules/video_chroma/gst_mem.c
+++ b/modules/video_chroma/gst_mem.c
@@ -32,6 +32,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 #include "../codec/gstreamer/gstcopypicture.h"
 #include "../codec/gstreamer/gst_mem.h"
@@ -110,9 +111,16 @@ static int Open(filter_t *p_filter)
     return VLC_SUCCESS;
 }
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_GST_MEM_OPAQUE, VLC_CODEC_NV12, false);
+}
+
 vlc_module_begin()
     set_shortname(N_("GST_MEM converter"))
     set_description(N_("GST_MEM Chroma Converter filter"))
     set_subcategory(SUBCAT_VIDEO_VFILTER)
     set_callback_video_converter(Open, 10)
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end()
diff --git a/modules/video_chroma/i420_nv12.c b/modules/video_chroma/i420_nv12.c
index 1f0758c548050cc2691c908984577d9634a80e73..b89231492bddd69758937e605f494640c52d94de 100644
--- a/modules/video_chroma/i420_nv12.c
+++ b/modules/video_chroma/i420_nv12.c
@@ -32,6 +32,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include "copy.h"
 
 typedef struct
@@ -227,7 +228,16 @@ static int Create( filter_t *p_filter )
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, COPY_COST, VLC_CODEC_I420, VLC_CODEC_NV12, true);
+    vlc_chroma_conv_add(vec, COPY_COST, VLC_CODEC_YV12, VLC_CODEC_NV12, true);
+    vlc_chroma_conv_add(vec, COPY_COST, VLC_CODEC_I420_10L, VLC_CODEC_P010, true);
+}
+
 vlc_module_begin ()
     set_description( N_("YUV planar to semiplanar conversions") )
     set_callback_video_converter( Create, 160 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
diff --git a/modules/video_chroma/i420_rgb.c b/modules/video_chroma/i420_rgb.c
index 3c7e617712865fbe2471cc1a61df6e503563bbab..aba01915763daabb3c2b661e5e2add207e516591 100644
--- a/modules/video_chroma/i420_rgb.c
+++ b/modules/video_chroma/i420_rgb.c
@@ -34,6 +34,7 @@
 #include <vlc_filter.h>
 #include <vlc_picture.h>
 #include <vlc_cpu.h>
+#include <vlc_chroma_probe.h>
 
 #include "i420_rgb.h"
 #include "../video_filter/filter_picture.h"
@@ -58,6 +59,28 @@ static void Set8bppPalette( filter_t *, uint8_t * );
 static int  Activate   ( filter_t * );
 static void Deactivate ( filter_t * );
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define OUT_CHROMAS_COMMON VLC_CODEC_RGB565, VLC_CODEC_RGB555, VLC_CODEC_XRGB, \
+    VLC_CODEC_RGBX, VLC_CODEC_BGRX, VLC_CODEC_XBGR
+
+#ifndef PLUGIN_PLAIN
+#define OUT_CHROMAS OUT_CHROMAS_COMMON
+#else
+#define OUT_CHROMAS OUT_CHROMAS_COMMON, VLC_CODEC_RGB233, VLC_CODEC_RGB332, \
+    VLC_CODEC_BGR233, VLC_CODEC_BGR565, VLC_CODEC_BGR555
+#endif
+
+#if defined (PLUGIN_SSE2)
+#define COST 0.75
+#else
+#define COST 1
+#endif
+
+    vlc_chroma_conv_add_in_outlist(vec, COST, VLC_CODEC_YV12, OUT_CHROMAS);
+    vlc_chroma_conv_add_in_outlist(vec, COST, VLC_CODEC_I420, OUT_CHROMAS);
+}
+
 vlc_module_begin ()
 #if defined (PLUGIN_SSE2)
     set_description( N_( "SSE2 I420,IYUV,YV12 to "
@@ -70,6 +93,8 @@ vlc_module_begin ()
     set_callback_video_converter( Activate, 80 )
 # define vlc_CPU_capable() (true)
 #endif
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 #ifndef PLUGIN_PLAIN
diff --git a/modules/video_chroma/i420_yuy2.c b/modules/video_chroma/i420_yuy2.c
index 982043238496cc6a57797c525e3aa6b716efef0c..8af14a3ef0568676a458003bea1a57f9d9f3a65e 100644
--- a/modules/video_chroma/i420_yuy2.c
+++ b/modules/video_chroma/i420_yuy2.c
@@ -33,6 +33,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_cpu.h>
 
 #if defined (PLUGIN_ALTIVEC) && defined(HAVE_ALTIVEC_H)
@@ -46,13 +47,16 @@
 #if defined (PLUGIN_SSE2)
 #    define DEST_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422"
 #    define VLC_TARGET VLC_SSE
+#    define COST 0.75
 #elif defined (PLUGIN_ALTIVEC)
 #    define DEST_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422"
 #    define VLC_TARGET VLC_ALTIVEC
+#    define COST 0.75
 #else
 #    define PLUGIN_PLAIN
 #    define DEST_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422,Y211"
 #    define VLC_TARGET
+#    define COST 1
 #endif
 
 /*****************************************************************************
@@ -63,6 +67,15 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor.
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add_in_outlist(vec, COST, VLC_CODEC_I420, VLC_CODEC_YUYV,
+        VLC_CODEC_YVYU, VLC_CODEC_UYVY);
+#ifdef PLUGIN_PLAIN
+    vlc_chroma_conv_add(vec, COST, VLC_CODEC_I420, VLC_CODEC_Y211, false);
+#endif
+}
+
 vlc_module_begin ()
 #if defined (PLUGIN_PLAIN)
     set_description( N_("Conversions from " SRC_FOURCC " to " DEST_FOURCC) )
@@ -77,6 +90,8 @@ vlc_module_begin ()
     set_callback_video_converter( Activate, 250 )
 # define vlc_CPU_capable() vlc_CPU_ALTIVEC()
 #endif
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 VIDEO_FILTER_WRAPPER( I420_YUY2 )
diff --git a/modules/video_chroma/i422_i420.c b/modules/video_chroma/i422_i420.c
index 8d09c5c5d8ece031bd6a3f7303dc7dff4ba69c5d..258e977c585e82b4a0296bc40e6b4e0c97f3fbdb 100644
--- a/modules/video_chroma/i422_i420.c
+++ b/modules/video_chroma/i422_i420.c
@@ -33,6 +33,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 #define SRC_FOURCC  "I422,J422"
 #define DEST_FOURCC "I420,IYUV,J420,YV12,YUVA"
@@ -45,9 +46,17 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_I422, VLC_CODEC_I420,
+        VLC_CODEC_YV12, VLC_CODEC_YUV420A);
+}
+
 vlc_module_begin ()
     set_description( N_("Conversions from " SRC_FOURCC " to " DEST_FOURCC) )
     set_callback_video_converter( Activate, 60 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 VIDEO_FILTER_WRAPPER( I422_I420 )
diff --git a/modules/video_chroma/i422_yuy2.c b/modules/video_chroma/i422_yuy2.c
index 8077897652221e9043e851220fbbb8496b7e0aff..a047bd2cf9dfb0a30000a4d588483d0785bb1c04 100644
--- a/modules/video_chroma/i422_yuy2.c
+++ b/modules/video_chroma/i422_yuy2.c
@@ -33,6 +33,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_cpu.h>
 
 #include "i422_yuy2.h"
@@ -40,8 +41,10 @@
 #define SRC_FOURCC  "I422"
 #if !defined (PLUGIN_SSE2)
 #    define DEST_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422,Y211"
+#    define COST 0.75
 #else
 #    define DEST_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422"
+#    define COST 1
 #endif
 
 /*****************************************************************************
@@ -52,6 +55,15 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add_in_outlist(vec, COST, VLC_CODEC_I422, VLC_CODEC_YUYV,
+        VLC_CODEC_YVYU, VLC_CODEC_UYVY);
+#ifdef PLUGIN_PLAIN
+    vlc_chroma_conv_add(vec, COST, VLC_CODEC_I422, VLC_CODEC_Y211, false);
+#endif
+}
+
 vlc_module_begin ()
 #if defined (PLUGIN_SSE2)
     set_description( N_("SSE2 conversions from " SRC_FOURCC " to " DEST_FOURCC) )
@@ -65,6 +77,8 @@ vlc_module_begin ()
 # define vlc_CPU_capable() (true)
 # define VLC_TARGET
 #endif
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 
diff --git a/modules/video_chroma/rv32.c b/modules/video_chroma/rv32.c
index 5c8a8172f713b3cce742f902677762bb917735b3..9be8ab10e5e3b86d376d9b8613bd37cbd372e95e 100644
--- a/modules/video_chroma/rv32.c
+++ b/modules/video_chroma/rv32.c
@@ -31,6 +31,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 /****************************************************************************
  * Local prototypes
@@ -41,9 +42,16 @@ static picture_t *Filter( filter_t *, picture_t * );
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_BGR24, VLC_CODEC_RGBA, false);
+}
+
 vlc_module_begin ()
     set_description( N_("RV32 conversion filter") )
     set_callback_video_converter( OpenFilter, 1 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 static const struct vlc_filter_operations filter_ops = {
diff --git a/modules/video_chroma/swscale.c b/modules/video_chroma/swscale.c
index 17f66d5325dcf3e5e6c6cf09de801abb2d743573..8bbb0edafab59a9c407af605c26bfe51a4e84ac0 100644
--- a/modules/video_chroma/swscale.c
+++ b/modules/video_chroma/swscale.c
@@ -33,6 +33,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <vlc_cpu.h>
 
 #include <libswscale/swscale.h>
@@ -60,6 +61,57 @@ static const char *const ppsz_mode_descriptions[] =
   N_("Area"), N_("Luma bicubic / chroma bilinear"), N_("Gauss"),
   N_("SincR"), N_("Lanczos"), N_("Bicubic spline") };
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+#define COST_FACTOR 1
+    size_t count;
+    const struct vlc_chroma_ffmpeg *table = GetVlcChromaFfmpegTable(&count);
+    assert(table != NULL && count > 0);
+
+    for (size_t i = 0; i < count; i++)
+    {
+        vlc_fourcc_t cur_chroma = table[i].i_chroma;
+        enum AVPixelFormat cur_avpf = table[i].i_chroma_id;
+        bool cur_supported_input = sws_isSupportedInput(cur_avpf);
+        bool cur_supported_output = sws_isSupportedOutput(cur_avpf);
+
+        if (!cur_supported_input && !cur_supported_output)
+            continue;
+
+        for (size_t j = i + 1; j < count; j++)
+        {
+            vlc_fourcc_t next_chroma = table[j].i_chroma;
+
+            if (next_chroma == cur_chroma)
+                continue;
+
+            enum AVPixelFormat next_avpf = table[j].i_chroma_id;
+            bool next_supported_input = sws_isSupportedInput(next_avpf);
+            bool next_supported_output = sws_isSupportedOutput(next_avpf);
+
+            if (!next_supported_input && !next_supported_output)
+                continue;
+
+            if (cur_supported_input && cur_supported_output
+             && next_supported_input && next_supported_output)
+            {
+                /* Likely case: add in <-> out two way conversion */
+                vlc_chroma_conv_add(vec, COST_FACTOR, cur_chroma, next_chroma,
+                                    true);
+            }
+            else
+            {
+                if (cur_supported_input && next_supported_output)
+                    vlc_chroma_conv_add(vec, COST_FACTOR, cur_chroma, next_chroma,
+                                        false);
+                else if (cur_supported_output && next_supported_input)
+                    vlc_chroma_conv_add(vec, COST_FACTOR, next_chroma, cur_chroma,
+                                        false);
+            }
+        }
+    }
+}
+
 vlc_module_begin ()
     set_description( N_("Video scaling filter") )
     set_shortname( N_("Swscale" ) )
@@ -67,6 +119,8 @@ vlc_module_begin ()
     set_callback_video_converter( OpenScaler, 150 )
     add_integer( "swscale-mode", 2, SCALEMODE_TEXT, SCALEMODE_LONGTEXT )
         change_integer_list( pi_mode_values, ppsz_mode_descriptions )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 /* Version checking */
diff --git a/modules/video_chroma/yuvp.c b/modules/video_chroma/yuvp.c
index eedc932165e28651ede522505fec8ff14b1cba59..32fa798f6f924c4943b9765387485b2c92a49ae8 100644
--- a/modules/video_chroma/yuvp.c
+++ b/modules/video_chroma/yuvp.c
@@ -31,6 +31,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 #include <assert.h>
 #include "../video_filter/filter_picture.h"
 
@@ -43,9 +44,17 @@
  *****************************************************************************/
 static int  Open ( filter_t * );
 
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_YUVP, VLC_CODEC_RGBA,
+        VLC_CODEC_ARGB, VLC_CODEC_BGRA, VLC_CODEC_ABGR, VLC_CODEC_YUVA);
+}
+
 vlc_module_begin ()
     set_description( N_("YUVP converter") )
     set_callback_video_converter( Open, 10 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 /****************************************************************************
diff --git a/modules/video_chroma/yuy2_i420.c b/modules/video_chroma/yuy2_i420.c
index 2ed4d660611dc933149d8ce9c988b7a59dc848f3..c69866ed7fc5d25f75e1bd9d6ca5c5ce2d7cbc42 100644
--- a/modules/video_chroma/yuy2_i420.c
+++ b/modules/video_chroma/yuy2_i420.c
@@ -32,6 +32,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 #define SRC_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422"
 #define DEST_FOURCC  "I420"
@@ -44,9 +45,19 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUYV, VLC_CODEC_I420, false);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YVYU, VLC_CODEC_I420, false);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_UYVY, VLC_CODEC_I420, false);
+}
+
 vlc_module_begin ()
     set_description( N_("Conversions from " SRC_FOURCC " to " DEST_FOURCC) )
     set_callback_video_converter( Activate, 80 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 VIDEO_FILTER_WRAPPER( YUY2_I420 )
diff --git a/modules/video_chroma/yuy2_i422.c b/modules/video_chroma/yuy2_i422.c
index 9b9b97bb16cc3b59bb29955c29a368205d644740..feb289187ffbd866b904d526e54e7dbbb3dc11a1 100644
--- a/modules/video_chroma/yuy2_i422.c
+++ b/modules/video_chroma/yuy2_i422.c
@@ -32,6 +32,7 @@
 #include <vlc_plugin.h>
 #include <vlc_filter.h>
 #include <vlc_picture.h>
+#include <vlc_chroma_probe.h>
 
 #define SRC_FOURCC "YUY2,YUNV,YVYU,UYVY,UYNV,Y422"
 #define DEST_FOURCC  "I422"
@@ -44,9 +45,19 @@ static int  Activate ( filter_t * );
 /*****************************************************************************
  * Module descriptor
  *****************************************************************************/
+
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUYV, VLC_CODEC_I422, false);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YVYU, VLC_CODEC_I422, false);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_UYVY, VLC_CODEC_I422, false);
+}
+
 vlc_module_begin ()
     set_description( N_("Conversions from " SRC_FOURCC " to " DEST_FOURCC) )
     set_callback_video_converter( Activate, 80 )
+    add_submodule()
+        set_callback_chroma_conv_probe(ProbeChroma)
 vlc_module_end ()
 
 VIDEO_FILTER_WRAPPER( YUY2_I422 )
diff --git a/modules/video_output/drm/planes.c b/modules/video_output/drm/planes.c
index d96232a184bba9016b00e9cb1c14b6186acfffdd..1277d25fb30a4740a4e133fe7edde260d1656e1b 100644
--- a/modules/video_output/drm/planes.c
+++ b/modules/video_output/drm/planes.c
@@ -243,14 +243,16 @@ uint_fast32_t vlc_drm_find_best_format(int fd, uint_fast32_t plane_id,
     if (nfmt > plane.count_format_types)
         nfmt = plane.count_format_types;
 
+    vlc_fourcc_t *list = NULL;
     /* Look for an exact match first */
     uint_fast32_t drm_fourcc = vlc_drm_find_format(chroma, nfmt, fmts);
     if (drm_fourcc != 0)
         goto out;
 
     /* Fallback to decreasingly optimal formats */
-    const vlc_fourcc_t *list = vlc_fourcc_GetFallback(chroma);
-    assert(list != NULL);
+    list = vlc_fourcc_GetFallback(chroma);
+    if (list == NULL)
+        goto out;
 
     for (size_t i = 0; list[i] != 0; i++) {
         drm_fourcc = vlc_drm_find_format(list[i], nfmt, fmts);
@@ -259,6 +261,7 @@ uint_fast32_t vlc_drm_find_best_format(int fd, uint_fast32_t plane_id,
     }
     errno = ENOTSUP;
 out:
+    free(list);
     free(fmts);
     return drm_fourcc;
 }
diff --git a/modules/video_output/kva.c b/modules/video_output/kva.c
index 532daf43360679411230de83ca1b1765bcb0e288..ca3ee4d11a7d55ddb801fbdd01eb57f5bb4445c1 100644
--- a/modules/video_output/kva.c
+++ b/modules/video_output/kva.c
@@ -446,7 +446,7 @@ static int Control( vout_display_t *vd, int query )
 static int OpenDisplay( vout_display_t *vd, video_format_t *fmt )
 {
     vout_display_sys_t * sys = vd->sys;
-    const vlc_fourcc_t *fallback;
+    vlc_fourcc_t *fallback;
     bool b_hw_accel = 0;
     FOURCC i_kva_fourcc;
     int i_chroma_shift = 0;
@@ -458,6 +458,8 @@ static int OpenDisplay( vout_display_t *vd, video_format_t *fmt )
     {
         fallback = ( pass == 0 ) ? vlc_fourcc_GetYUVFallback( fmt->i_chroma ) :
                                    vlc_fourcc_GetRGBFallback( fmt->i_chroma );
+        if( fallback == NULL )
+            continue;
 
         for( int i = 0; fallback[ i ]; i++ )
         {
@@ -512,6 +514,7 @@ static int OpenDisplay( vout_display_t *vd, video_format_t *fmt )
                 fmt->i_chroma = fallback[ i ];
                 break;
             }
+            free( fallback );
         }
     }
 
diff --git a/modules/video_output/libplacebo/display.c b/modules/video_output/libplacebo/display.c
index fd84160a4a697633f0e11e8be1f896ef2fb85ddc..f2f12ed298869267024669d68bed1ee895a81043 100644
--- a/modules/video_output/libplacebo/display.c
+++ b/modules/video_output/libplacebo/display.c
@@ -136,12 +136,16 @@ static int Open(vout_display_t *vd,
         fmt->i_chroma = vd->source->i_chroma;
     } else {
         fmt->i_chroma = 0;
-        const vlc_fourcc_t *fcc;
-        for (fcc = vlc_fourcc_GetFallback(vd->source->i_chroma); *fcc; fcc++) {
-            if (vlc_placebo_FormatSupported(gpu, *fcc)) {
-                fmt->i_chroma = *fcc;
-                break;
+        vlc_fourcc_t *list = vlc_fourcc_GetFallback(vd->source->i_chroma);
+        if (list != NULL)
+        {
+            for (const vlc_fourcc_t *fcc = list; *fcc; fcc++) {
+                if (vlc_placebo_FormatSupported(gpu, *fcc)) {
+                    fmt->i_chroma = *fcc;
+                    break;
+                }
             }
+            free(list);
         }
 
         if (fmt->i_chroma == 0) {
diff --git a/modules/video_output/opengl/interop_sw.c b/modules/video_output/opengl/interop_sw.c
index e039e34861bcb34cda05ac1e41133a887f32f004..b6867b9a6a65e31173e42d0a804d48b6afe00335 100644
--- a/modules/video_output/opengl/interop_sw.c
+++ b/modules/video_output/opengl/interop_sw.c
@@ -792,7 +792,6 @@ opengl_interop_generic_init(struct vlc_gl_interop *interop, bool allow_dr)
         || vlc_gl_HasExtension(&extension_vt, "GL_EXT_texture_integer"));
 
     video_color_space_t space;
-    const vlc_fourcc_t *list;
     const bool is_yup = vlc_fourcc_IsYUV(interop->fmt_in.i_chroma);
 
     if (is_yup)
@@ -802,19 +801,10 @@ opengl_interop_generic_init(struct vlc_gl_interop *interop, bool allow_dr)
         if (max_texture_units < 3)
             goto error;
 
-        list = vlc_fourcc_GetYUVFallback(interop->fmt_in.i_chroma);
         space = interop->fmt_in.space;
     }
-    else if (interop->fmt_in.i_chroma == VLC_CODEC_XYZ_12B)
-    {
-        list = NULL;
-        space = COLOR_SPACE_UNDEF;
-    }
     else
-    {
-        list = vlc_fourcc_GetRGBFallback(interop->fmt_in.i_chroma);
         space = COLOR_SPACE_UNDEF;
-    }
 
     /* The pictures are uploaded upside-down */
     video_format_TransformBy(&interop->fmt_out, TRANSFORM_VFLIP);
@@ -833,24 +823,30 @@ opengl_interop_generic_init(struct vlc_gl_interop *interop, bool allow_dr)
             goto interop_init;
     }
 
+    vlc_fourcc_t *list = NULL;
+    if (is_yup)
+        list = vlc_fourcc_GetYUVFallback(interop->fmt_in.i_chroma);
+    else if (interop->fmt_in.i_chroma != VLC_CODEC_XYZ_12B)
+        list = vlc_fourcc_GetRGBFallback(interop->fmt_in.i_chroma);
+
     if (list == NULL)
         goto error;
 
     /* Check whether any fallback for the chroma is translatable to OpenGL. */
-    while (*list)
+    for (const vlc_fourcc_t *fcc = list; *fcc; fcc++)
     {
-        ret = opengl_interop_init(interop, *list, space);
+        ret = opengl_interop_init(interop, *fcc, space);
         if (ret == VLC_SUCCESS)
         {
-            i_chroma = *list;
+            i_chroma = *fcc;
             msg_Warn(interop->gl, "direct rendering failed for %4.4s, "
                      "fallback to %4.4s",
                      (const char *)&interop->fmt_in.i_chroma,
                      (const char *)&i_chroma);
             goto interop_init;
         }
-        list++;
     }
+    free(list);
 
     goto error;
 
diff --git a/modules/video_output/win32/direct3d11.cpp b/modules/video_output/win32/direct3d11.cpp
index 309ae47a42f7d2fec050e5c2922f80c7657401c0..336c3d4e88c1796cdbad14ef22b5a34729557ecc 100644
--- a/modules/video_output/win32/direct3d11.cpp
+++ b/modules/video_output/win32/direct3d11.cpp
@@ -1083,14 +1083,18 @@ static int Direct3D11Open(vout_display_t *vd, video_format_t *fmtp, vlc_video_co
 #endif
                 )
         {
-            const vlc_fourcc_t *list = vlc_fourcc_GetFallback(vd->source->i_chroma);
-            for (unsigned i = 0; list[i] != 0; i++) {
-                if (list[i] == vd->source->i_chroma)
-                    continue;
-                fmt.i_chroma = list[i];
-                err = SetupOutputFormat(vd, &fmt, nullptr, &sys->picQuad.quad_fmt);
-                if (err == VLC_SUCCESS)
-                    break;
+            vlc_fourcc_t *list = vlc_fourcc_GetFallback(vd->source->i_chroma);
+            if (list != NULL)
+            {
+                for (unsigned i = 0; list[i] != 0; i++) {
+                    if (list[i] == vd->source->i_chroma)
+                        continue;
+                    fmt.i_chroma = list[i];
+                    err = SetupOutputFormat(vd, &fmt, nullptr, &sys->picQuad.quad_fmt);
+                    if (err == VLC_SUCCESS)
+                        break;
+                }
+                free(list);
             }
         }
         if (err != VLC_SUCCESS)
diff --git a/modules/video_output/win32/direct3d9.c b/modules/video_output/win32/direct3d9.c
index 597030691758b3b6c4c0cebc868ea39d2bc2bda9..ac49bdaf1d90efbbcad0addde169fd45a6f112ba 100644
--- a/modules/video_output/win32/direct3d9.c
+++ b/modules/video_output/win32/direct3d9.c
@@ -1351,7 +1351,8 @@ static const d3d9_format_t *Direct3DFindFormat(vout_display_t *vd, const video_f
         msg_Warn( vd, "Disabling hardware chroma conversion due to odd dimensions" );
 
     for (unsigned pass = 0; pass < 2; pass++) {
-        const vlc_fourcc_t *list;
+        const vlc_fourcc_t *list = NULL;
+        vlc_fourcc_t *fallback_list = NULL;
         const vlc_fourcc_t dxva_chroma[] = {fmt->i_chroma, 0};
         D3DFORMAT decoder_format = D3DFMT_UNKNOWN;
 
@@ -1364,10 +1365,11 @@ static const d3d9_format_t *Direct3DFindFormat(vout_display_t *vd, const video_f
             msg_Dbg(vd, "favor decoder format: %4.4s (%d)", (const char*)&decoder_format, decoder_format);
         }
         else if (pass == 0 && hardware_scale_ok && sys->allow_hw_yuv && vlc_fourcc_IsYUV(fmt->i_chroma))
-            list = vlc_fourcc_GetYUVFallback(fmt->i_chroma);
+            list = fallback_list = vlc_fourcc_GetYUVFallback(fmt->i_chroma);
         else if (pass == 1)
-            list = vlc_fourcc_GetRGBFallback(fmt->i_chroma);
-        else
+            list = fallback_list = vlc_fourcc_GetRGBFallback(fmt->i_chroma);
+
+        if (list == NULL)
             continue;
 
         for (unsigned i = 0; list[i] != 0; i++) {
@@ -1381,9 +1383,13 @@ static const d3d9_format_t *Direct3DFindFormat(vout_display_t *vd, const video_f
 
                 msg_Dbg(vd, "trying surface pixel format: %s", format->name);
                 if (!Direct3D9CheckConversion(vd, format->format))
+                {
+                    free(fallback_list);
                     return format;
+                }
             }
         }
+        free(fallback_list);
     }
     return NULL;
 }
diff --git a/src/Makefile.am b/src/Makefile.am
index c2742fddca3b64011f37ca7c92bb99edfda7d644..e9721d7404b84903e8978f48c862a7aa1942495f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -43,6 +43,7 @@ pluginsinclude_HEADERS.h = \
 	../include/vlc_configuration.h \
 	../include/vlc_cpu.h \
 	../include/vlc_clock.h \
+	../include/vlc_chroma_probe.h \
 	../include/vlc_decoder.h \
 	../include/vlc_demux.h \
 	../include/vlc_dialog.h \
@@ -380,6 +381,7 @@ libvlccore_la_SOURCES = \
 	misc/actions.c \
 	misc/ancillary.h \
 	misc/ancillary.c \
+	misc/chroma_probe.c \
 	misc/executor.c \
 	misc/md5.c \
 	misc/probe.c \
diff --git a/src/libvlccore.sym b/src/libvlccore.sym
index ff8bb77ffcb7a4fb48c48ae051a8d24ac3ed215b..b8c9c3cca7a0accd3eaadc1de2d7ef9d9ea5c057 100644
--- a/src/libvlccore.sym
+++ b/src/libvlccore.sym
@@ -58,6 +58,8 @@ vlc_frame_shm_Alloc
 vlc_frame_Realloc
 vlc_frame_Release
 vlc_frame_TryRealloc
+vlc_chroma_conv_Probe
+vlc_chroma_conv_result_ToString
 config_AddIntf
 config_ChainCreate
 config_ChainDestroy
@@ -590,7 +592,6 @@ vlc_fourcc_GetCodecAudio
 vlc_fourcc_GetCodecFromString
 vlc_fourcc_GetDescription
 vlc_fourcc_GetChromaDescription
-vlc_fourcc_IsYUV
 vlc_fourcc_GetRGBFallback
 vlc_fourcc_GetYUVFallback
 vlc_fourcc_GetFallback
diff --git a/src/meson.build b/src/meson.build
index 8e420f121956c0889d59e27aa9b7730d87e45027..18f680b7ef6998a2705f0f96573cd88584c559ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -231,6 +231,7 @@ libvlccore_sources_base = files(
     'text/iso-639_def.h',
     'misc/actions.c',
     'misc/ancillary.c',
+    'misc/chroma_probe.c',
     'misc/executor.c',
     'misc/md5.c',
     'misc/probe.c',
diff --git a/src/misc/chroma_probe.c b/src/misc/chroma_probe.c
new file mode 100644
index 0000000000000000000000000000000000000000..fff486c3d81e4cb38ea554386acc81468a3b52ad
--- /dev/null
+++ b/src/misc/chroma_probe.c
@@ -0,0 +1,475 @@
+/*****************************************************************************
+ * chroma_probe.c: chroma conversion probing
+ *****************************************************************************
+ * Copyright (C) 2025 VLC authors and VideoLAN
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_chroma_probe.h>
+#include <vlc_fourcc.h>
+#include <vlc_threads.h>
+#include <vlc_modules.h>
+#include <vlc_sort.h>
+#include <vlc_memstream.h>
+
+static int
+modules_Probe(vlc_chroma_conv_vec *chroma_table)
+{
+    module_t **mods;
+    ssize_t total = vlc_module_match("chroma probe", NULL, false, &mods, NULL);
+    if (total == -1)
+        return -ENOENT;
+
+    for (ssize_t i = 0; i < total; ++i)
+    {
+        vlc_chroma_conv_probe fn = vlc_module_map(NULL, mods[i]);
+        if (fn == NULL)
+            continue;
+        fn(chroma_table);
+    }
+    free(mods);
+    return 0;
+}
+
+/* Breadth First Search (BFS) node */
+struct bfs_node
+{
+    vlc_fourcc_t chain[VLC_CHROMA_CONV_CHAIN_COUNT_MAX];
+    unsigned depth; /* Max deep is VLC_CHROMA_CONV_CHAIN_COUNT_MAX -1 */
+    float cost_factor;
+};
+typedef struct VLC_VECTOR(struct bfs_node) bfs_queue_vec;
+
+static int
+bfs_Run(vlc_fourcc_t chroma_from, vlc_fourcc_t chroma_to, unsigned max_depth,
+        const vlc_chroma_conv_vec *chroma_table, int flags,
+        bfs_queue_vec *queue)
+{
+    struct bfs_node start = {
+        .chain[0] = chroma_from,
+        .cost_factor = 1,
+        .depth = 0,
+    };
+    bool success = vlc_vector_push(queue, start);
+    if (!success)
+        return -ENOMEM;
+
+    for (size_t queue_idx = 0; queue_idx < queue->size; queue_idx++)
+    {
+        const struct bfs_node current = queue->data[queue_idx];
+        vlc_fourcc_t current_chroma = current.chain[current.depth];
+
+        if (chroma_to != 0 && current_chroma == chroma_to)
+            continue; /* Found a path to 'chroma_to' */
+
+        if (current.depth == max_depth)
+            continue;
+
+        /* Enqueue neighbors */
+        for (size_t chroma_idx = 0; chroma_idx < chroma_table->size; chroma_idx++)
+        {
+            struct vlc_chroma_conv_entry *entry = &chroma_table->data[chroma_idx];
+            vlc_fourcc_t from = entry->in;
+            vlc_fourcc_t to = entry->out;
+            float cost_factor = entry->cost_factor;
+
+            if (from == current_chroma)
+            {
+                vlc_fourcc_t next_chroma = to;
+
+                /* Apply filters from flags */
+                if (flags & VLC_CHROMA_CONV_FLAG_ONLY_YUV)
+                {
+                    if (!vlc_fourcc_IsYUV(next_chroma))
+                        continue;
+                }
+                else if (flags & VLC_CHROMA_CONV_FLAG_ONLY_RGB)
+                {
+                    const vlc_chroma_description_t *desc =
+                        vlc_fourcc_GetChromaDescription(next_chroma);
+                    if (desc == NULL || desc->subtype != VLC_CHROMA_SUBTYPE_RGB)
+                        continue;
+                }
+
+                /* If next_chroma is already in the chain at any previous step,
+                 * we've encountered a cycle or a duplicate. */
+                bool already_visited = false;
+                for (size_t i = 0; i < current.depth; ++i)
+                    if (current.chain[i] == next_chroma)
+                    {
+                        already_visited = true;
+                        break;
+                    }
+                if (already_visited)
+                    continue;
+
+                struct bfs_node next = current;
+                next.depth = current.depth + 1;
+                next.cost_factor = current.cost_factor * cost_factor;
+
+                next.chain[next.depth] = next_chroma;
+                success = vlc_vector_push(queue, next);
+                if (!success)
+                    return -ENOMEM;
+            }
+        }
+    }
+    return 0;
+}
+
+static uint64_t
+GetChromaBits(const vlc_chroma_description_t *desc,
+              unsigned width, unsigned height)
+{
+    if (desc->plane_count == 0)
+    {
+        /* Fallback to the size of the subtype */
+        switch (desc->subtype)
+        {
+            case VLC_CHROMA_SUBTYPE_OTHER:
+                return 0;
+            case VLC_CHROMA_SUBTYPE_YUV444:
+                return width * height * 3 * desc->color_bits;
+            case VLC_CHROMA_SUBTYPE_YUV440:
+            case VLC_CHROMA_SUBTYPE_YUV422:
+                return width * height * 2 * desc->color_bits;
+            case VLC_CHROMA_SUBTYPE_YUV420:
+            case VLC_CHROMA_SUBTYPE_YUV411:
+                return width * height * 1.5 * desc->color_bits;
+            case VLC_CHROMA_SUBTYPE_YUV410:
+                return width * height * 1.125 * desc->color_bits;
+            case VLC_CHROMA_SUBTYPE_YUV211:
+            case VLC_CHROMA_SUBTYPE_GREY:
+                return width * height * desc->color_bits;
+            case VLC_CHROMA_SUBTYPE_RGB:
+                return width * height * 4 * desc->color_bits;
+            default:
+                vlc_assert_unreachable();
+        }
+    }
+
+    uint64_t total_bits = 0;
+    for (unsigned i = 0; i < desc->plane_count; i++)
+    {
+        const vlc_rational_t rw = desc->p[i].w;
+        const vlc_rational_t rh = desc->p[i].h;
+
+        unsigned plane_width = width * rw.num / rw.den;
+        unsigned plane_height = height * rh.num / rh.den;
+
+        uint64_t plane_pixels = plane_width * plane_height;
+        uint64_t plane_bits = plane_pixels * desc->pixel_bits;
+
+        total_bits += plane_bits;
+    }
+
+    return total_bits;
+}
+
+static float
+GetColorRatio(enum vlc_chroma_subtype subtype)
+{
+    switch (subtype)
+    {
+        case VLC_CHROMA_SUBTYPE_YUV444:
+        case VLC_CHROMA_SUBTYPE_RGB:
+            return 1.f;
+        case VLC_CHROMA_SUBTYPE_YUV422:
+            return 0.67;
+        case VLC_CHROMA_SUBTYPE_YUV440:
+            return 0.5; /* should be like YUV422, but it is less common */
+        case VLC_CHROMA_SUBTYPE_YUV420:
+            return 0.5;
+        case VLC_CHROMA_SUBTYPE_YUV411:
+            return 0.33;
+        case VLC_CHROMA_SUBTYPE_YUV410:
+            return 0.25;
+        case VLC_CHROMA_SUBTYPE_YUV211:
+        case VLC_CHROMA_SUBTYPE_OTHER:
+            return 0.2;
+        case VLC_CHROMA_SUBTYPE_GREY:
+            return 0.1;
+        default:
+            vlc_assert_unreachable();
+    }
+}
+
+static float
+CompareDescs(const vlc_chroma_description_t *in_desc,
+             const vlc_chroma_description_t *out_desc)
+{
+    /* Compare color bits */
+    float bits_ratio;
+    if (in_desc->color_bits == 0 || out_desc->color_bits == 0)
+        bits_ratio = 1.f;
+    else
+    {
+        bits_ratio = out_desc->color_bits / in_desc->color_bits;
+        if (bits_ratio > 1.f)
+            bits_ratio = 1.f;
+    }
+
+    /* Compare color ratios, favor same or near subtype */
+    if (in_desc->subtype == out_desc->subtype)
+        return bits_ratio;
+
+    float color_ratio = GetColorRatio(out_desc->subtype)
+                      / GetColorRatio(in_desc->subtype);
+    if (color_ratio > 1.f)
+        color_ratio = 1.f;
+
+    /* Malus for CPU YUV <-> Other. Favor staying in the same color model. */
+    bool in_is_yuv = vlc_chroma_description_IsYUV(in_desc);
+    bool out_is_yuv = vlc_chroma_description_IsYUV(out_desc);
+    if ((in_desc->plane_count != 0 && out_desc->plane_count != 0)
+     && (in_is_yuv || out_is_yuv) && (in_is_yuv != out_is_yuv))
+        color_ratio *= 0.9;
+
+    return color_ratio * bits_ratio;
+}
+
+static void
+vlc_chroma_conv_result_FromNode(struct vlc_chroma_conv_result *res,
+                                const struct bfs_node *node,
+                                unsigned width, unsigned height)
+{
+    res->chain_count = node->depth + 1;
+    res->cost = 0;
+
+    uint64_t total_cost = 0;
+    float total_quality = 1.f;
+    for (size_t i = 0; i < res->chain_count; ++i)
+    {
+        res->chain[i] = node->chain[i];
+
+        if (i > 0)
+        {
+            const vlc_chroma_description_t *from_desc =
+                vlc_fourcc_GetChromaDescription(res->chain[i - 1]);
+            const vlc_chroma_description_t *to_desc =
+                vlc_fourcc_GetChromaDescription(res->chain[i]);
+
+            if (from_desc == NULL || to_desc == NULL)
+            {
+                /* Unlikely, fallback for a big cost */
+                total_cost += width * height * 4 * 8 * node->cost_factor;
+                continue;
+            }
+
+            uint64_t from_bits = GetChromaBits(from_desc, width, height);
+            uint64_t to_bits = GetChromaBits(to_desc, width, height);
+
+            /* Unlikely case */
+            if (from_bits == 0) /* OTHER -> ANY */
+                from_bits = to_bits;
+            else if (to_bits == 0) /* ANY -> OTHER */
+                to_bits = from_bits;
+
+            total_cost += (from_bits + to_bits) * node->cost_factor;
+
+            float quality = CompareDescs(from_desc, to_desc);
+            assert(quality > 0.f && quality <= 1.f);
+
+            total_quality *= quality;
+        }
+    }
+    res->cost = total_cost / width / height;
+    res->quality = 100 * total_quality;
+}
+
+static int
+SortResults(const void *a, const void *b, void *arg)
+{
+    const struct vlc_chroma_conv_result *ra = a;
+    const struct vlc_chroma_conv_result *rb = b;
+    bool *sort_by_quality = arg;
+
+    int cost_score = 0, quality_score = 0;
+
+    /* Lower cost is better */
+    if (ra->cost < rb->cost)
+        cost_score = -1;
+    else if (ra->cost > rb->cost)
+        cost_score = 1;
+
+    /* Higher Quality is better */
+    if (ra->quality > rb->quality)
+        quality_score = -1;
+    else if (ra->quality < rb->quality)
+        quality_score = 1;
+
+    /* Fallback to secondary score in same score */
+    if (*sort_by_quality)
+        return quality_score != 0 ? quality_score : cost_score;
+    else
+        return cost_score != 0 ? cost_score : quality_score;
+}
+
+static bool
+bfs_node_IsResult(const struct bfs_node *node, vlc_fourcc_t to)
+{
+    vlc_fourcc_t current_chroma = node->chain[node->depth];
+    return to == 0 || current_chroma == to;
+}
+
+static bool
+vlc_chroma_conv_result_Equals(struct vlc_chroma_conv_result *a,
+                              struct vlc_chroma_conv_result *b)
+{
+    if (a->chain_count != b->chain_count)
+        return false;
+    if (a->quality != b->quality)
+        return false;
+    /* Don't check cost since we want to merge results with different costs */
+    for (size_t i = 0; i < a->chain_count; ++i)
+        if (a->chain[i] != b->chain[i])
+            return false;
+    return true;
+}
+
+struct vlc_chroma_conv_result *
+vlc_chroma_conv_Probe(vlc_fourcc_t from, vlc_fourcc_t to,
+                      unsigned width, unsigned height,
+                      unsigned max_indirect_steps, int flags, size_t *count)
+{
+    assert(from != 0);
+    assert(max_indirect_steps <= VLC_CHROMA_CONV_MAX_INDIRECT_STEPS);
+    vlc_chroma_conv_vec chroma_table;
+    vlc_vector_init(&chroma_table);
+
+    if (width == 0 || height == 0)
+    {
+        width = 3840;
+        height = 2160;
+    }
+
+    if (max_indirect_steps > 0)
+    {
+        /* Allow indirect steps only when converting from/to a GPU chroma */
+        bool from_cpu = vlc_fourcc_GetChromaBPP(from) != 0;
+        bool to_cpu = to == 0 ? true : vlc_fourcc_GetChromaBPP(to) != 0;
+        if (from_cpu && to_cpu)
+            max_indirect_steps--;
+    }
+
+    /* Probe modules */
+    int ret = modules_Probe(&chroma_table);
+    if (ret != 0 || chroma_table.size == 0)
+    {
+        vlc_vector_destroy(&chroma_table);
+        return NULL;
+    }
+
+    /* Run tree search */
+    bfs_queue_vec bfs_queue;
+    vlc_vector_init(&bfs_queue);
+    ret = bfs_Run(from, to, max_indirect_steps + 1 , &chroma_table, flags,
+                  &bfs_queue);
+
+    vlc_vector_destroy(&chroma_table);
+
+    size_t result_count = 0;
+    for (size_t i = 1 /* skip start node */; i < bfs_queue.size; ++i)
+        if (bfs_node_IsResult(&bfs_queue.data[i], to))
+            result_count++;
+
+    if (unlikely(ret != 0) || result_count == 0)
+    {
+        vlc_vector_destroy(&bfs_queue);
+        return NULL;
+    }
+
+    /* Allocate the result array */
+    struct VLC_VECTOR(struct vlc_chroma_conv_result) result_vec =
+        VLC_VECTOR_INITIALIZER;
+    bool success = vlc_vector_push_hole(&result_vec, result_count);
+    if (!success)
+    {
+        vlc_vector_destroy(&bfs_queue);
+        return NULL;
+    }
+
+    /* Fill the result from the tree search */
+    size_t res_idx = 0;
+    for (size_t i = 1 /* skip start node */; i < bfs_queue.size; ++i)
+    {
+        const struct bfs_node *node = &bfs_queue.data[i];
+        if (!bfs_node_IsResult(node, to))
+            continue;
+
+        assert(res_idx < result_count);
+        struct vlc_chroma_conv_result *res = &result_vec.data[res_idx++];
+        vlc_chroma_conv_result_FromNode(res, node, width, height);
+    }
+    assert(res_idx == result_count);
+
+    vlc_vector_destroy(&bfs_queue);
+
+    /* Sort */
+    bool sort_by_quality = (flags & VLC_CHROMA_CONV_FLAG_SORT_COST) == 0;
+    vlc_qsort(result_vec.data, result_count,
+              sizeof(struct vlc_chroma_conv_result), SortResults,
+              &sort_by_quality);
+
+    /* Remove duplicate entries, it can happen when more the 2 modules probe
+     * the same conversion. They are not necessarily one after the other as
+     * they might have different quality. */
+    for (size_t i = 0; i < result_vec.size - 1; ++ i)
+    {
+        struct vlc_chroma_conv_result *cur = &result_vec.data[i];
+
+        size_t j = i + 1;
+        while (j < result_vec.size)
+        {
+            struct vlc_chroma_conv_result *next = &result_vec.data[j];
+            if (vlc_chroma_conv_result_Equals(cur, next))
+            {
+                /* Keep the lowest cost */
+                if (next->cost < cur->cost)
+                    cur->cost = next->cost;
+                vlc_vector_remove(&result_vec, j);
+            }
+            else
+                j++;
+        }
+    }
+
+    *count = result_vec.size;
+    return result_vec.data;
+}
+
+char *
+vlc_chroma_conv_result_ToString(const struct vlc_chroma_conv_result *res)
+{
+    struct vlc_memstream ms;
+    int ret = vlc_memstream_open(&ms);
+    if (ret != 0)
+        return NULL;
+    vlc_memstream_printf(&ms, "[c=%u|q=%u] ", res->cost, res->quality);
+
+    for (size_t i = 0; i < res->chain_count; ++i)
+    {
+        vlc_memstream_printf(&ms, "%4.4s", (const char *) &res->chain[i]);
+        if (i != res->chain_count - 1)
+            vlc_memstream_puts(&ms, " -> ");
+    }
+    ret = vlc_memstream_close(&ms);
+    return ret == 0 ? ms.ptr : NULL;
+}
diff --git a/src/misc/fourcc.c b/src/misc/fourcc.c
index 636d096345369efd0924043a5853900fadee2742..bd74d6abe03697158b943e2483506b89dfd4cab8 100644
--- a/src/misc/fourcc.c
+++ b/src/misc/fourcc.c
@@ -31,6 +31,7 @@
 #include <vlc_common.h>
 #include <vlc_fourcc.h>
 #include <vlc_es.h>
+#include <vlc_chroma_probe.h>
 #include <assert.h>
 
 #include "fourcc_tables.h"
@@ -206,656 +207,236 @@ const char *vlc_fourcc_GetDescription(int cat, vlc_fourcc_t fourcc)
     return LookupCat(fourcc, &ret, cat) ? ret : "";
 }
 
-
-/* */
-#define VLC_CODEC_YUV_PLANAR_420 \
-    VLC_CODEC_I420, VLC_CODEC_YV12
-
-#define VLC_CODEC_YUV_SEMIPLANAR_420 \
-    VLC_CODEC_NV12, VLC_CODEC_NV21
-
-#define VLC_CODEC_YUV_PLANAR_420_16 \
-    VLC_CODEC_I420_16L, VLC_CODEC_I420_16B, VLC_CODEC_I420_12L, VLC_CODEC_I420_12B, VLC_CODEC_I420_10L, VLC_CODEC_I420_10B, VLC_CODEC_I420_9L, VLC_CODEC_I420_9B
-
-#define VLC_CODEC_YUV_SEMIPLANAR_420_16 \
-    VLC_CODEC_P010, VLC_CODEC_P012 ,VLC_CODEC_P016
-
-#define VLC_CODEC_YUV_SEMIPLANAR_422 \
-    VLC_CODEC_NV16, VLC_CODEC_NV61
-
-#define VLC_CODEC_YUV_PLANAR_422_16 \
-    VLC_CODEC_I422_12L, VLC_CODEC_I422_12B, VLC_CODEC_I422_10L, VLC_CODEC_I422_10B, VLC_CODEC_I422_9L, VLC_CODEC_I422_9B
-
-#define VLC_CODEC_YUV_PLANAR_444_ALPHA \
-    VLC_CODEC_YUVA, VLC_CODEC_YUVA_444_10L, VLC_CODEC_YUVA_444_10B, VLC_CODEC_YUVA_444_12L, VLC_CODEC_YUVA_444_12B
-
-#define VLC_CODEC_YUV_SEMIPLANAR_444 \
-    VLC_CODEC_NV24, VLC_CODEC_NV42
-
-#define VLC_CODEC_YUV_PLANAR_444_16 \
-    VLC_CODEC_I444_10L, VLC_CODEC_I444_10B, VLC_CODEC_I444_9L, VLC_CODEC_I444_9B, \
-    VLC_CODEC_I444_16L, VLC_CODEC_I444_16B, VLC_CODEC_I444_12L, VLC_CODEC_I444_12B
-
-#define VLC_CODEC_YUV_PACKED \
-    VLC_CODEC_YUYV, VLC_CODEC_YVYU, \
-    VLC_CODEC_UYVY, VLC_CODEC_VYUY, \
-    VLC_CODEC_VUYA, VLC_CODEC_Y210, \
-    VLC_CODEC_Y410
-
-#define VLC_CODEC_FALLBACK_420 \
-    VLC_CODEC_I422, VLC_CODEC_YUV_PACKED, \
-    VLC_CODEC_I444, VLC_CODEC_I440, \
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211
-
-static const vlc_fourcc_t p_I420_fallback[] = {
-    VLC_CODEC_I420, VLC_CODEC_YV12, VLC_CODEC_FALLBACK_420, 0
-};
-static const vlc_fourcc_t p_YV12_fallback[] = {
-    VLC_CODEC_YV12, VLC_CODEC_I420, VLC_CODEC_FALLBACK_420, 0
-};
-static const vlc_fourcc_t p_NV12_fallback[] = {
-    VLC_CODEC_NV12, VLC_CODEC_I420, VLC_CODEC_FALLBACK_420, 0
-};
-
-#define VLC_CODEC_FALLBACK_420_16 \
-    VLC_CODEC_I420, VLC_CODEC_YV12, VLC_CODEC_FALLBACK_420
-
-static const vlc_fourcc_t p_I420_9L_fallback[] = {
-    VLC_CODEC_I420_9L, VLC_CODEC_I420_9B, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_9B_fallback[] = {
-    VLC_CODEC_I420_9B, VLC_CODEC_I420_9L, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_10L_fallback[] = {
-    VLC_CODEC_I420_10L, VLC_CODEC_I420_10B, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_10B_fallback[] = {
-    VLC_CODEC_I420_10B, VLC_CODEC_I420_10L, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_12L_fallback[] = {
-    VLC_CODEC_I420_12L, VLC_CODEC_I420_12B, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_12B_fallback[] = {
-    VLC_CODEC_I420_12B, VLC_CODEC_I420_12L, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_16L_fallback[] = {
-    VLC_CODEC_I420_16L, VLC_CODEC_I420_16B, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_I420_16B_fallback[] = {
-    VLC_CODEC_I420_16B, VLC_CODEC_I420_16L, VLC_CODEC_FALLBACK_420_16, 0
-};
-static const vlc_fourcc_t p_P010_fallback[] = {
-    VLC_CODEC_P010, VLC_CODEC_FALLBACK_420_16, 0
-};
-
-
-#define VLC_CODEC_FALLBACK_422 \
-    VLC_CODEC_YUV_PACKED, VLC_CODEC_YUV_PLANAR_420, \
-    VLC_CODEC_I444, VLC_CODEC_I440, \
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211
-
-static const vlc_fourcc_t p_I422_fallback[] = {
-    VLC_CODEC_I422, VLC_CODEC_FALLBACK_422, 0
-};
-
-#define VLC_CODEC_FALLBACK_422_16 \
-    VLC_CODEC_I422, VLC_CODEC_FALLBACK_422
-
-static const vlc_fourcc_t p_I422_9L_fallback[] = {
-    VLC_CODEC_I422_9L, VLC_CODEC_I422_9B, VLC_CODEC_FALLBACK_422_16, 0
-};
-static const vlc_fourcc_t p_I422_9B_fallback[] = {
-    VLC_CODEC_I422_9B, VLC_CODEC_I422_9L, VLC_CODEC_FALLBACK_422_16, 0
-};
-static const vlc_fourcc_t p_I422_10L_fallback[] = {
-    VLC_CODEC_I422_10L, VLC_CODEC_I422_10B, VLC_CODEC_FALLBACK_422_16, 0
-};
-static const vlc_fourcc_t p_I422_10B_fallback[] = {
-    VLC_CODEC_I422_10B, VLC_CODEC_I422_10L, VLC_CODEC_FALLBACK_422_16, 0
-};
-static const vlc_fourcc_t p_I422_12L_fallback[] = {
-    VLC_CODEC_I422_12L, VLC_CODEC_I422_12B, VLC_CODEC_FALLBACK_422_16, 0
-};
-static const vlc_fourcc_t p_I422_12B_fallback[] = {
-    VLC_CODEC_I422_12B, VLC_CODEC_I422_12L, VLC_CODEC_FALLBACK_422_16, 0
-};
-
-#define VLC_CODEC_FALLBACK_444 \
-    VLC_CODEC_I422, VLC_CODEC_YUV_PACKED, \
-    VLC_CODEC_YUV_PLANAR_420, VLC_CODEC_I440, \
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211
-
-static const vlc_fourcc_t p_I444_fallback[] = {
-    VLC_CODEC_I444, VLC_CODEC_FALLBACK_444, 0
-};
-
-#define VLC_CODEC_FALLBACK_444_16 \
-    VLC_CODEC_I444, VLC_CODEC_FALLBACK_444
-
-static const vlc_fourcc_t p_I444_9L_fallback[] = {
-    VLC_CODEC_I444_9L, VLC_CODEC_I444_9B, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_9B_fallback[] = {
-    VLC_CODEC_I444_9B, VLC_CODEC_I444_9L, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_10L_fallback[] = {
-    VLC_CODEC_I444_10L, VLC_CODEC_I444_10B, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_10B_fallback[] = {
-    VLC_CODEC_I444_10B, VLC_CODEC_I444_10L, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_12L_fallback[] = {
-    VLC_CODEC_I444_12L, VLC_CODEC_I444_12B, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_12B_fallback[] = {
-    VLC_CODEC_I444_12B, VLC_CODEC_I444_12L, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_16L_fallback[] = {
-    VLC_CODEC_I444_16L, VLC_CODEC_I444_16B, VLC_CODEC_FALLBACK_444_16, 0
-};
-static const vlc_fourcc_t p_I444_16B_fallback[] = {
-    VLC_CODEC_I444_16B, VLC_CODEC_I444_16L, VLC_CODEC_FALLBACK_444_16, 0
-};
-
-
-/* Fallbacks for cvpx */
-static const vlc_fourcc_t p_CVPX_VIDEO_NV12_fallback[] = {
-    VLC_CODEC_CVPX_NV12, VLC_CODEC_NV12, VLC_CODEC_I420, 0,
-};
-static const vlc_fourcc_t p_CVPX_VIDEO_UYVY_fallback[] = {
-    VLC_CODEC_CVPX_UYVY, VLC_CODEC_UYVY, 0,
-};
-static const vlc_fourcc_t p_CVPX_VIDEO_I420_fallback[] = {
-    VLC_CODEC_CVPX_I420, VLC_CODEC_I420, 0,
-};
-static const vlc_fourcc_t p_CVPX_VIDEO_BGRA_fallback[] = {
-    VLC_CODEC_CVPX_BGRA, VLC_CODEC_BGRA, 0,
-};
-static const vlc_fourcc_t p_CVPX_VIDEO_P010_fallback[] = {
-    VLC_CODEC_CVPX_P010, VLC_CODEC_P010, VLC_CODEC_I420_10L, 0
-};
-
-static const vlc_fourcc_t p_VAAPI_420_fallback[] = {
-    VLC_CODEC_VAAPI_420, VLC_CODEC_I420, 0,
-};
-
-static const vlc_fourcc_t p_VAAPI_420_10BPP_fallback[] = {
-    VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_P010, VLC_CODEC_I420_10L, 0,
-};
-
-static const vlc_fourcc_t p_VAAPI_420_12BPP_fallback[] = {
-    VLC_CODEC_VAAPI_420_12BPP, VLC_CODEC_P012, VLC_CODEC_I420_12L, 0,
-};
-
-static const vlc_fourcc_t p_D3D9_OPAQUE_fallback[] = {
-    VLC_CODEC_D3D9_OPAQUE, VLC_CODEC_I420, 0,
-};
-
-static const vlc_fourcc_t p_D3D9_OPAQUE_10B_fallback[] = {
-    VLC_CODEC_D3D9_OPAQUE_10B, VLC_CODEC_P010, VLC_CODEC_I420_10L, 0,
-};
-
-static const vlc_fourcc_t p_D3D11_OPAQUE_fallback[] = {
-    VLC_CODEC_D3D11_OPAQUE, VLC_CODEC_NV12, 0,
-};
-
-static const vlc_fourcc_t p_D3D11_OPAQUE_10B_fallback[] = {
-    VLC_CODEC_D3D11_OPAQUE_10B, VLC_CODEC_P010, VLC_CODEC_I420_10L, 0,
-};
-
-static const vlc_fourcc_t p_D3D11_OPAQUE_RGBA_fallback[] = {
-    VLC_CODEC_D3D11_OPAQUE_RGBA, VLC_CODEC_RGBA, 0,
-};
-
-static const vlc_fourcc_t p_NVDEC_OPAQUE_fallback[] = {
-    VLC_CODEC_NVDEC_OPAQUE, VLC_CODEC_NV12, 0,
-};
-
-static const vlc_fourcc_t p_NVDEC_OPAQUE_10B_fallback[] = {
-    VLC_CODEC_NVDEC_OPAQUE_10B,
-    VLC_CODEC_P010,
-    VLC_CODEC_I420_10L, 0,
-};
-
-static const vlc_fourcc_t p_NVDEC_OPAQUE_16B_fallback[] = {
-    VLC_CODEC_NVDEC_OPAQUE_16B,
-    VLC_CODEC_P016, VLC_CODEC_P010,
-    VLC_CODEC_I420_16L, VLC_CODEC_I420_12L, VLC_CODEC_I420_10L, 0,
-};
-
-static const vlc_fourcc_t p_I440_fallback[] = {
-    VLC_CODEC_I440,
-    VLC_CODEC_YUV_PLANAR_420,
-    VLC_CODEC_I422,
-    VLC_CODEC_I444,
-    VLC_CODEC_YUV_PACKED,
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211, 0
-};
-
-#define VLC_CODEC_FALLBACK_PACKED \
-    VLC_CODEC_I422, VLC_CODEC_YUV_PLANAR_420, \
-    VLC_CODEC_I444, VLC_CODEC_I440, \
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211
-
-static const vlc_fourcc_t p_YUYV_fallback[] = {
-    VLC_CODEC_YUYV,
-    VLC_CODEC_YVYU,
-    VLC_CODEC_UYVY,
-    VLC_CODEC_VYUY,
-    VLC_CODEC_FALLBACK_PACKED, 0
-};
-static const vlc_fourcc_t p_YVYU_fallback[] = {
-    VLC_CODEC_YVYU,
-    VLC_CODEC_YUYV,
-    VLC_CODEC_UYVY,
-    VLC_CODEC_VYUY,
-    VLC_CODEC_FALLBACK_PACKED, 0
-};
-static const vlc_fourcc_t p_UYVY_fallback[] = {
-    VLC_CODEC_UYVY,
-    VLC_CODEC_VYUY,
-    VLC_CODEC_YUYV,
-    VLC_CODEC_YVYU,
-    VLC_CODEC_FALLBACK_PACKED, 0
-};
-static const vlc_fourcc_t p_VYUY_fallback[] = {
-    VLC_CODEC_VYUY,
-    VLC_CODEC_UYVY,
-    VLC_CODEC_YUYV,
-    VLC_CODEC_YVYU,
-    VLC_CODEC_FALLBACK_PACKED, 0
-};
-
-static const vlc_fourcc_t *const pp_YUV_fallback[] = {
-    p_YV12_fallback,
-    p_I420_fallback,
-    p_I420_9L_fallback,
-    p_I420_9B_fallback,
-    p_I420_10L_fallback,
-    p_I420_10B_fallback,
-    p_I420_12L_fallback,
-    p_I420_12B_fallback,
-    p_I420_16L_fallback,
-    p_I420_16B_fallback,
-    p_I422_fallback,
-    p_I422_9L_fallback,
-    p_I422_9B_fallback,
-    p_I422_10L_fallback,
-    p_I422_10B_fallback,
-    p_I422_12L_fallback,
-    p_I422_12B_fallback,
-    p_I444_fallback,
-    p_I444_9L_fallback,
-    p_I444_9B_fallback,
-    p_I444_10L_fallback,
-    p_I444_10B_fallback,
-    p_I444_12L_fallback,
-    p_I444_12B_fallback,
-    p_I444_16L_fallback,
-    p_I444_16B_fallback,
-    p_I440_fallback,
-    p_YUYV_fallback,
-    p_YVYU_fallback,
-    p_UYVY_fallback,
-    p_VYUY_fallback,
-    p_NV12_fallback,
-    p_P010_fallback,
-    p_CVPX_VIDEO_NV12_fallback,
-    p_CVPX_VIDEO_UYVY_fallback,
-    p_CVPX_VIDEO_I420_fallback,
-    p_CVPX_VIDEO_P010_fallback,
-    p_VAAPI_420_fallback,
-    p_VAAPI_420_10BPP_fallback,
-    p_VAAPI_420_12BPP_fallback,
-    p_D3D9_OPAQUE_fallback,
-    p_D3D9_OPAQUE_10B_fallback,
-    p_D3D11_OPAQUE_fallback,
-    p_D3D11_OPAQUE_10B_fallback,
-    p_NVDEC_OPAQUE_fallback,
-    p_NVDEC_OPAQUE_10B_fallback,
-    p_NVDEC_OPAQUE_16B_fallback,
-    NULL,
-};
-
-static const vlc_fourcc_t p_list_YUV[] = {
-    VLC_CODEC_YUV_PLANAR_420,
-    VLC_CODEC_YUV_SEMIPLANAR_420,
-    VLC_CODEC_I422,
-    VLC_CODEC_YUV_SEMIPLANAR_422,
-    VLC_CODEC_I440,
-    VLC_CODEC_I444,
-    VLC_CODEC_YUV_PLANAR_444_ALPHA,
-    VLC_CODEC_YUV_SEMIPLANAR_444,
-    VLC_CODEC_YUV_PACKED,
-    VLC_CODEC_I411, VLC_CODEC_I410, VLC_CODEC_Y211,
-    VLC_CODEC_YUV_PLANAR_420_16,
-    VLC_CODEC_YUV_SEMIPLANAR_420_16,
-    VLC_CODEC_YUV_PLANAR_422_16,
-    VLC_CODEC_YUV_PLANAR_444_16,
-    VLC_CODEC_VDPAU_VIDEO,
-    VLC_CODEC_CVPX_NV12,
-    VLC_CODEC_CVPX_UYVY,
-    VLC_CODEC_CVPX_I420,
-    VLC_CODEC_CVPX_P010,
-    VLC_CODEC_VAAPI_420,
-    VLC_CODEC_VAAPI_420_10BPP,
-    VLC_CODEC_D3D9_OPAQUE,
-    VLC_CODEC_D3D9_OPAQUE_10B,
-    VLC_CODEC_D3D11_OPAQUE,
-    VLC_CODEC_D3D11_OPAQUE_10B,
-    VLC_CODEC_D3D11_OPAQUE_ALPHA,
-    VLC_CODEC_NVDEC_OPAQUE,
-    VLC_CODEC_NVDEC_OPAQUE_10B,
-    VLC_CODEC_NVDEC_OPAQUE_16B,
-    VLC_CODEC_NVDEC_OPAQUE_444,
-    VLC_CODEC_NVDEC_OPAQUE_444_16B,
-    VLC_CODEC_YUV420A,
-    VLC_CODEC_YUV422A,
-    0,
-};
-
-static const vlc_fourcc_t p_list_YUV_no_fallback[] = {
-    VLC_CODEC_V308,
-};
-
-/* */
-static const vlc_fourcc_t p_RGB32_fallback[] = {
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_RGB565LE,
-    VLC_CODEC_RGB555LE,
-    VLC_CODEC_RGB233,
-    VLC_CODEC_BGR233,
-    VLC_CODEC_RGB332,
-    0,
-};
-static const vlc_fourcc_t p_RGB24_fallback[] = {
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB565LE,
-    VLC_CODEC_RGB555LE,
-    VLC_CODEC_RGB233,
-    VLC_CODEC_BGR233,
-    VLC_CODEC_RGB332,
-    0,
-};
-static const vlc_fourcc_t p_RGB16_fallback[] = {
-    VLC_CODEC_RGB565LE,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB555LE,
-    VLC_CODEC_RGB233,
-    VLC_CODEC_BGR233,
-    VLC_CODEC_RGB332,
-    0,
-};
-static const vlc_fourcc_t p_RGB15_fallback[] = {
-    VLC_CODEC_RGB555LE,
-    VLC_CODEC_RGB565LE,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    VLC_CODEC_RGB233,
-    VLC_CODEC_BGR233,
-    VLC_CODEC_RGB332,
-    0,
-};
-static const vlc_fourcc_t p_RGB8_fallback[] = {
-    VLC_CODEC_RGB233,
-    VLC_CODEC_BGR233,
-    VLC_CODEC_RGB332,
-    VLC_CODEC_RGB555LE,
-    VLC_CODEC_RGB565LE,
-    VLC_CODEC_RGB24,
-    VLC_CODEC_BGR24,
-    VLC_CODEC_XRGB,
-    VLC_CODEC_BGRX,
-    0,
-};
-static const vlc_fourcc_t *const pp_RGB_fallback[] = {
-    p_RGB32_fallback,
-    p_RGB24_fallback,
-    p_RGB16_fallback,
-    p_RGB15_fallback,
-    p_RGB8_fallback,
-    p_CVPX_VIDEO_BGRA_fallback,
-    p_D3D11_OPAQUE_RGBA_fallback,
-
-    NULL,
-};
-
-
-/* */
-static const vlc_fourcc_t *GetFallback( vlc_fourcc_t i_fourcc,
-                                        const vlc_fourcc_t *const *pp_fallback,
-                                        const vlc_fourcc_t p_list[] )
+static vlc_fourcc_t *GetFallback( vlc_fourcc_t i_fourcc, int flags )
 {
-    for( unsigned i = 0; pp_fallback[i]; i++ )
+    size_t count;
+    struct vlc_chroma_conv_result *results =
+        vlc_chroma_conv_Probe(i_fourcc, 0, 0, 0, 0, flags, &count);
+    if (results == NULL)
+        return NULL;
+
+    vlc_fourcc_t *list = vlc_alloc(count + 1, sizeof(vlc_fourcc_t));
+    if (list == NULL)
     {
-        if( pp_fallback[i][0] == i_fourcc )
-            return pp_fallback[i];
+        free(results);
+        return NULL;
     }
-    return p_list;
+
+    for (size_t i = 0; i < count; ++i)
+    {
+        struct vlc_chroma_conv_result *res = &results[i];
+        assert(res->chain_count == 2);
+
+        list[i] = res->chain[1];
+    }
+    list[count] = 0;
+    free(results);
+    return list;
 }
 
-const vlc_fourcc_t *vlc_fourcc_GetYUVFallback( vlc_fourcc_t i_fourcc )
+vlc_fourcc_t *vlc_fourcc_GetYUVFallback( vlc_fourcc_t i_fourcc )
 {
-    return GetFallback( i_fourcc, pp_YUV_fallback, p_list_YUV );
+    return GetFallback( i_fourcc, VLC_CHROMA_CONV_FLAG_ONLY_YUV );
 }
-const vlc_fourcc_t *vlc_fourcc_GetRGBFallback( vlc_fourcc_t i_fourcc )
+vlc_fourcc_t *vlc_fourcc_GetRGBFallback( vlc_fourcc_t i_fourcc )
 {
-    return GetFallback( i_fourcc, pp_RGB_fallback, p_RGB32_fallback );
+    return GetFallback( i_fourcc, VLC_CHROMA_CONV_FLAG_ONLY_RGB );
 }
 
-const vlc_fourcc_t *vlc_fourcc_GetFallback( vlc_fourcc_t i_fourcc )
+vlc_fourcc_t *vlc_fourcc_GetFallback( vlc_fourcc_t i_fourcc )
 {
     return vlc_fourcc_IsYUV( i_fourcc)
             ? vlc_fourcc_GetYUVFallback( i_fourcc )
             : vlc_fourcc_GetRGBFallback( i_fourcc );
 }
 
-bool vlc_fourcc_IsYUV(vlc_fourcc_t fcc)
-{
-    for( unsigned i = 0; p_list_YUV[i]; i++ )
-    {
-        if( p_list_YUV[i] == fcc )
-            return true;
-    }
-
-    for (size_t i = 0; i < ARRAY_SIZE(p_list_YUV_no_fallback); i++)
-    {
-        if (p_list_YUV_no_fallback[i] == fcc)
-            return true;
-    }
-    return false;
-}
-
-#define PLANAR(n, w_den, h_den, bits) \
+#define PLANAR(subtype_, n, w_den, h_den, bits) \
+      .subtype = VLC_CHROMA_SUBTYPE_##subtype_, \
       .plane_count = n, \
       .p = { {.w = {1,    1}, .h = {1,    1}}, \
              {.w = {1,w_den}, .h = {1,h_den}}, \
              {.w = {1,w_den}, .h = {1,h_den}}, \
              {.w = {1,    1}, .h = {1,    1}} }, \
       .pixel_size = ((bits + 7) / 8), \
-      .pixel_bits = bits
+      .pixel_bits = bits, \
+      .color_bits = bits
 
-#define PLANAR_8(n, w_den, h_den)        PLANAR(n, w_den, h_den, 8)
-#define PLANAR_16(n, w_den, h_den, bits) PLANAR(n, w_den, h_den, bits)
+#define PLANAR_8(subtype_, n, w_den, h_den) PLANAR(subtype_, n, w_den, h_den, 8)
+#define PLANAR_16(subtype_, n, w_den, h_den, bits) PLANAR(subtype_, n, w_den, h_den, bits)
 
-#define SEMIPLANAR(w_den, h_den, bits) \
+#define SEMIPLANAR(subtype_, w_den, h_den, bits) \
+      .subtype = VLC_CHROMA_SUBTYPE_##subtype_, \
       .plane_count = 2, \
       .p = { {.w = {1,    1}, .h = {1,    1}}, \
              {.w = {2,w_den}, .h = {1,h_den}} }, \
       .pixel_size = ((bits + 7) / 8), \
-      .pixel_bits = bits
+      .pixel_bits = bits, \
+      .color_bits = bits
 
-#define PACKED_FMT(size, bits) \
+#define PACKED_FMT(subtype_, size, bits, color_bits_) \
+      .subtype = VLC_CHROMA_SUBTYPE_##subtype_, \
       .plane_count = 1, \
       .p = { {.w = {1,1}, .h = {1,1}} }, \
       .pixel_size = size, \
-      .pixel_bits = bits
+      .pixel_bits = bits, \
+      .color_bits = color_bits_
 
 /* Zero planes for hardware picture handles. Cannot be manipulated directly. */
-#define FAKE_FMT() \
+#define GPU_FMT(subtype_, color_bits_) \
+      .subtype = VLC_CHROMA_SUBTYPE_##subtype_, \
       .plane_count = 0, \
       .p = { {.w = {1,1}, .h = {1,1}} }, \
       .pixel_size = 0, \
-      .pixel_bits = 0
+      .pixel_bits = 0, \
+      .color_bits = color_bits_
 
 static const vlc_chroma_description_t p_list_chroma_description[] = {
-    { VLC_CODEC_I411,                  PLANAR_8(3, 4, 1) },
-    { VLC_CODEC_I410,                  PLANAR_8(3, 4, 4) },
-    { VLC_CODEC_I420,                  PLANAR_8(3, 2, 2) },
-    { VLC_CODEC_YV12,                  PLANAR_8(3, 2, 2) },
-    { VLC_CODEC_NV12,                  SEMIPLANAR(2, 2, 8) },
-    { VLC_CODEC_NV21,                  SEMIPLANAR(2, 2, 8) },
-    { VLC_CODEC_I422,                  PLANAR_8(3, 2, 1) },
-    { VLC_CODEC_NV16,                  SEMIPLANAR(2, 1, 8) },
-    { VLC_CODEC_NV61,                  SEMIPLANAR(2, 1, 8) },
-    { VLC_CODEC_I440,                  PLANAR_8(3, 1, 2) },
-    { VLC_CODEC_I444,                  PLANAR_8(3, 1, 1) },
-    { VLC_CODEC_NV24,                  SEMIPLANAR(1, 1, 8) },
-    { VLC_CODEC_NV42,                  SEMIPLANAR(1, 1, 8) },
-    { VLC_CODEC_YUVA,                  PLANAR_8(4, 1, 1) },
-    { VLC_CODEC_YUV420A,               PLANAR_8(4, 2, 2) },
-    { VLC_CODEC_YUV422A,               PLANAR_8(4, 2, 1) },
-
-    { VLC_CODEC_GBR_PLANAR,            PLANAR_8(3, 1, 1) },
-    { VLC_CODEC_GBR_PLANAR_9L,         PLANAR_16(3, 1, 1, 9) },
-    { VLC_CODEC_GBR_PLANAR_9B,         PLANAR_16(3, 1, 1, 9) },
-    { VLC_CODEC_GBR_PLANAR_10L,        PLANAR_16(3, 1, 1, 10) },
-    { VLC_CODEC_GBR_PLANAR_10B,        PLANAR_16(3, 1, 1, 10) },
-    { VLC_CODEC_GBR_PLANAR_12L,        PLANAR_16(3, 1, 1, 12) },
-    { VLC_CODEC_GBR_PLANAR_12B,        PLANAR_16(3, 1, 1, 12) },
-    { VLC_CODEC_GBR_PLANAR_14L,        PLANAR_16(3, 1, 1, 14) },
-    { VLC_CODEC_GBR_PLANAR_14B,        PLANAR_16(3, 1, 1, 14) },
-    { VLC_CODEC_GBR_PLANAR_16L,        PLANAR_16(3, 1, 1, 16) },
-    { VLC_CODEC_GBR_PLANAR_16B,        PLANAR_16(3, 1, 1, 16) },
-    { VLC_CODEC_GBRA_PLANAR,           PLANAR_8(4, 1, 1) },
-    { VLC_CODEC_GBRA_PLANAR_10L,       PLANAR_16(4, 1, 1, 10) },
-    { VLC_CODEC_GBRA_PLANAR_10B,       PLANAR_16(4, 1, 1, 10) },
-    { VLC_CODEC_GBRA_PLANAR_12L,       PLANAR_16(4, 1, 1, 12) },
-    { VLC_CODEC_GBRA_PLANAR_12B,       PLANAR_16(4, 1, 1, 12) },
-    { VLC_CODEC_GBRA_PLANAR_16L,       PLANAR_16(4, 1, 1, 16) },
-    { VLC_CODEC_GBRA_PLANAR_16B,       PLANAR_16(4, 1, 1, 16) },
-
-    { VLC_CODEC_I420_16L,              PLANAR_16(3, 2, 2, 16) },
-    { VLC_CODEC_I420_16B,              PLANAR_16(3, 2, 2, 16) },
-    { VLC_CODEC_I420_12L,              PLANAR_16(3, 2, 2, 12) },
-    { VLC_CODEC_I420_12B,              PLANAR_16(3, 2, 2, 12) },
-    { VLC_CODEC_I420_10L,              PLANAR_16(3, 2, 2, 10) },
-    { VLC_CODEC_I420_10B,              PLANAR_16(3, 2, 2, 10) },
-    { VLC_CODEC_I420_9L,               PLANAR_16(3, 2, 2,  9) },
-    { VLC_CODEC_I420_9B,               PLANAR_16(3, 2, 2,  9) },
-    { VLC_CODEC_I422_16L,              PLANAR_16(3, 2, 1, 16) },
-    { VLC_CODEC_I422_16B,              PLANAR_16(3, 2, 1, 16) },
-    { VLC_CODEC_I422_12L,              PLANAR_16(3, 2, 1, 12) },
-    { VLC_CODEC_I422_12B,              PLANAR_16(3, 2, 1, 12) },
-    { VLC_CODEC_I422_10L,              PLANAR_16(3, 2, 1, 10) },
-    { VLC_CODEC_I422_10B,              PLANAR_16(3, 2, 1, 10) },
-    { VLC_CODEC_I422_9L,               PLANAR_16(3, 2, 1,  9) },
-    { VLC_CODEC_I422_9B,               PLANAR_16(3, 2, 1,  9) },
-    { VLC_CODEC_I444_12L,              PLANAR_16(3, 1, 1, 12) },
-    { VLC_CODEC_I444_12B,              PLANAR_16(3, 1, 1, 12) },
-    { VLC_CODEC_I444_10L,              PLANAR_16(3, 1, 1, 10) },
-    { VLC_CODEC_I444_10B,              PLANAR_16(3, 1, 1, 10) },
-    { VLC_CODEC_I444_9L,               PLANAR_16(3, 1, 1,  9) },
-    { VLC_CODEC_I444_9B,               PLANAR_16(3, 1, 1,  9) },
-    { VLC_CODEC_I444_16L,              PLANAR_16(3, 1, 1, 16) },
-    { VLC_CODEC_I444_16B,              PLANAR_16(3, 1, 1, 16) },
-    { VLC_CODEC_YUVA_444_10L,          PLANAR_16(4, 1, 1, 10) },
-    { VLC_CODEC_YUVA_444_10B,          PLANAR_16(4, 1, 1, 10) },
-    { VLC_CODEC_YUVA_444_12L,          PLANAR_16(4, 1, 1, 12) },
-    { VLC_CODEC_YUVA_444_12B,          PLANAR_16(4, 1, 1, 12) },
-    { VLC_CODEC_P010,                  SEMIPLANAR(2, 2, 10) },
-    { VLC_CODEC_P012,                  SEMIPLANAR(2, 2, 12) },
-    { VLC_CODEC_P016,                  SEMIPLANAR(2, 2, 16) },
-
-    { VLC_CODEC_V308,                  PACKED_FMT(1, 24) },
-    { VLC_CODEC_YUYV,                  PACKED_FMT(2, 16) },
-    { VLC_CODEC_YVYU,                  PACKED_FMT(2, 16) },
-    { VLC_CODEC_UYVY,                  PACKED_FMT(2, 16) },
-    { VLC_CODEC_VYUY,                  PACKED_FMT(2, 16) },
-    { VLC_CODEC_YUV2,                  PACKED_FMT(2, 16) },
-    { VLC_CODEC_RGB233,                PACKED_FMT(1, 8) },
-    { VLC_CODEC_BGR233,                PACKED_FMT(1, 8) },
-    { VLC_CODEC_RGB332,                PACKED_FMT(1, 8) },
-    { VLC_CODEC_YUVP,                  PACKED_FMT(1, 8) },
-    { VLC_CODEC_RGBP,                  PACKED_FMT(1, 8) },
-    { VLC_CODEC_GREY,                  PACKED_FMT(1, 8) },
-    { VLC_CODEC_GREY_10L,              PACKED_FMT(2, 10) },
-    { VLC_CODEC_GREY_10B,              PACKED_FMT(2, 10) },
-    { VLC_CODEC_GREY_12L,              PACKED_FMT(2, 12) },
-    { VLC_CODEC_GREY_12B,              PACKED_FMT(2, 12) },
-    { VLC_CODEC_GREY_16L,              PACKED_FMT(2, 16) },
-    { VLC_CODEC_GREY_16B,              PACKED_FMT(2, 16) },
-
-    { VLC_CODEC_RGB555BE,              PACKED_FMT(2, 15) },
-    { VLC_CODEC_RGB555LE,              PACKED_FMT(2, 15) },
-    { VLC_CODEC_BGR555LE,              PACKED_FMT(2, 15) },
-    { VLC_CODEC_BGR555BE,              PACKED_FMT(2, 15) },
-    { VLC_CODEC_RGB565LE,              PACKED_FMT(2, 16) },
-    { VLC_CODEC_RGB565BE,              PACKED_FMT(2, 16) },
-    { VLC_CODEC_BGR565LE,              PACKED_FMT(2, 16) },
-    { VLC_CODEC_BGR565BE,              PACKED_FMT(2, 16) },
-    { VLC_CODEC_RGB24,                 PACKED_FMT(3, 24) },
-    { VLC_CODEC_BGR24,                 PACKED_FMT(3, 24) },
-    { VLC_CODEC_RGBX,                  PACKED_FMT(4, 24) },
-    { VLC_CODEC_XRGB,                  PACKED_FMT(4, 24) },
-    { VLC_CODEC_BGRX,                  PACKED_FMT(4, 24) },
-    { VLC_CODEC_XBGR,                  PACKED_FMT(4, 24) },
-    { VLC_CODEC_RGBA,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_ARGB,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_BGRA,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_ABGR,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_RGBA10LE,              PACKED_FMT(4, 32) },
-    { VLC_CODEC_RGBA64,                PACKED_FMT(8, 64) },
-    { VLC_CODEC_VUYA,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_Y210,                  PACKED_FMT(4, 32) },
-    { VLC_CODEC_Y410,                  PACKED_FMT(4, 32) },
-
-    { VLC_CODEC_Y211,                 1, { {{1,4}, {1,1}} }, 4, 32 },
-    { VLC_CODEC_XYZ_12L,               PACKED_FMT(6, 48) },
-    { VLC_CODEC_XYZ_12B,               PACKED_FMT(6, 48) },
-
-    { VLC_CODEC_VDPAU_VIDEO,           FAKE_FMT() },
-    { VLC_CODEC_VDPAU_OUTPUT,          FAKE_FMT() },
-    { VLC_CODEC_ANDROID_OPAQUE,        FAKE_FMT() },
-    { VLC_CODEC_MMAL_OPAQUE,           FAKE_FMT() },
-    { VLC_CODEC_D3D9_OPAQUE,           FAKE_FMT() },
-    { VLC_CODEC_D3D11_OPAQUE,          FAKE_FMT() },
-    { VLC_CODEC_D3D9_OPAQUE_10B,       FAKE_FMT() },
-    { VLC_CODEC_D3D11_OPAQUE_10B,      FAKE_FMT() },
-    { VLC_CODEC_D3D11_OPAQUE_RGBA,     FAKE_FMT() },
-    { VLC_CODEC_D3D11_OPAQUE_BGRA,     FAKE_FMT() },
-    { VLC_CODEC_D3D11_OPAQUE_ALPHA,    FAKE_FMT() },
-
-    { VLC_CODEC_NVDEC_OPAQUE_16B,      FAKE_FMT() },
-    { VLC_CODEC_NVDEC_OPAQUE_10B,      FAKE_FMT() },
-    { VLC_CODEC_NVDEC_OPAQUE,          FAKE_FMT() },
-
-    { VLC_CODEC_NVDEC_OPAQUE_444,      FAKE_FMT() },
-    { VLC_CODEC_NVDEC_OPAQUE_444_16B,  FAKE_FMT() },
-
-    { VLC_CODEC_CVPX_NV12,             FAKE_FMT() },
-    { VLC_CODEC_CVPX_UYVY,             FAKE_FMT() },
-    { VLC_CODEC_CVPX_I420,             FAKE_FMT() },
-    { VLC_CODEC_CVPX_BGRA,             FAKE_FMT() },
-
-    { VLC_CODEC_CVPX_P010,             FAKE_FMT() },
-
-    { VLC_CODEC_GST_MEM_OPAQUE,        FAKE_FMT() },
-
-    { VLC_CODEC_VAAPI_420,             FAKE_FMT() },
-    { VLC_CODEC_VAAPI_420_10BPP,       FAKE_FMT() },
-    { VLC_CODEC_VAAPI_420_12BPP,       FAKE_FMT() },
+    { VLC_CODEC_I411,                  PLANAR_8(YUV411, 3, 4, 1) },
+    { VLC_CODEC_I410,                  PLANAR_8(YUV411, 3, 4, 4) },
+    { VLC_CODEC_I420,                  PLANAR_8(YUV420, 3, 2, 2) },
+    { VLC_CODEC_YV12,                  PLANAR_8(YUV420, 3, 2, 2) },
+    { VLC_CODEC_NV12,                  SEMIPLANAR(YUV420, 2, 2, 8) },
+    { VLC_CODEC_NV21,                  SEMIPLANAR(YUV420, 2, 2, 8) },
+    { VLC_CODEC_I422,                  PLANAR_8(YUV422, 3, 2, 1) },
+    { VLC_CODEC_NV16,                  SEMIPLANAR(YUV422, 2, 1, 8) },
+    { VLC_CODEC_NV61,                  SEMIPLANAR(YUV422, 2, 1, 8) },
+    { VLC_CODEC_I440,                  PLANAR_8(YUV440, 3, 1, 2) },
+    { VLC_CODEC_I444,                  PLANAR_8(YUV444, 3, 1, 1) },
+    { VLC_CODEC_NV24,                  SEMIPLANAR(YUV444, 1, 1, 8) },
+    { VLC_CODEC_NV42,                  SEMIPLANAR(YUV444, 1, 1, 8) },
+    { VLC_CODEC_YUVA,                  PLANAR_8(YUV444, 4, 1, 1) },
+    { VLC_CODEC_YUV420A,               PLANAR_8(YUV420, 4, 2, 2) },
+    { VLC_CODEC_YUV422A,               PLANAR_8(YUV422, 4, 2, 1) },
+
+    { VLC_CODEC_GBR_PLANAR,            PLANAR_8(RGB, 3, 1, 1) },
+    { VLC_CODEC_GBR_PLANAR_9L,         PLANAR_16(RGB, 3, 1, 1, 9) },
+    { VLC_CODEC_GBR_PLANAR_9B,         PLANAR_16(RGB, 3, 1, 1, 9) },
+    { VLC_CODEC_GBR_PLANAR_10L,        PLANAR_16(RGB, 3, 1, 1, 10) },
+    { VLC_CODEC_GBR_PLANAR_10B,        PLANAR_16(RGB, 3, 1, 1, 10) },
+    { VLC_CODEC_GBR_PLANAR_12L,        PLANAR_16(RGB, 3, 1, 1, 12) },
+    { VLC_CODEC_GBR_PLANAR_12B,        PLANAR_16(RGB, 3, 1, 1, 12) },
+    { VLC_CODEC_GBR_PLANAR_14L,        PLANAR_16(RGB, 3, 1, 1, 14) },
+    { VLC_CODEC_GBR_PLANAR_14B,        PLANAR_16(RGB, 3, 1, 1, 14) },
+    { VLC_CODEC_GBR_PLANAR_16L,        PLANAR_16(RGB, 3, 1, 1, 16) },
+    { VLC_CODEC_GBR_PLANAR_16B,        PLANAR_16(RGB, 3, 1, 1, 16) },
+    { VLC_CODEC_GBRA_PLANAR,           PLANAR_8(RGB, 4, 1, 1) },
+    { VLC_CODEC_GBRA_PLANAR_10L,       PLANAR_16(RGB, 4, 1, 1, 10) },
+    { VLC_CODEC_GBRA_PLANAR_10B,       PLANAR_16(RGB, 4, 1, 1, 10) },
+    { VLC_CODEC_GBRA_PLANAR_12L,       PLANAR_16(RGB, 4, 1, 1, 12) },
+    { VLC_CODEC_GBRA_PLANAR_12B,       PLANAR_16(RGB, 4, 1, 1, 12) },
+    { VLC_CODEC_GBRA_PLANAR_16L,       PLANAR_16(RGB, 4, 1, 1, 16) },
+    { VLC_CODEC_GBRA_PLANAR_16B,       PLANAR_16(RGB, 4, 1, 1, 16) },
+
+    { VLC_CODEC_I420_16L,              PLANAR_16(YUV420, 3, 2, 2, 16) },
+    { VLC_CODEC_I420_16B,              PLANAR_16(YUV420, 3, 2, 2, 16) },
+    { VLC_CODEC_I420_12L,              PLANAR_16(YUV420, 3, 2, 2, 12) },
+    { VLC_CODEC_I420_12B,              PLANAR_16(YUV420, 3, 2, 2, 12) },
+    { VLC_CODEC_I420_10L,              PLANAR_16(YUV420, 3, 2, 2, 10) },
+    { VLC_CODEC_I420_10B,              PLANAR_16(YUV420, 3, 2, 2, 10) },
+    { VLC_CODEC_I420_9L,               PLANAR_16(YUV420, 3, 2, 2,  9) },
+    { VLC_CODEC_I420_9B,               PLANAR_16(YUV420, 3, 2, 2,  9) },
+    { VLC_CODEC_I422_16L,              PLANAR_16(YUV422, 3, 2, 1, 16) },
+    { VLC_CODEC_I422_16B,              PLANAR_16(YUV422, 3, 2, 1, 16) },
+    { VLC_CODEC_I422_12L,              PLANAR_16(YUV422, 3, 2, 1, 12) },
+    { VLC_CODEC_I422_12B,              PLANAR_16(YUV422, 3, 2, 1, 12) },
+    { VLC_CODEC_I422_10L,              PLANAR_16(YUV422, 3, 2, 1, 10) },
+    { VLC_CODEC_I422_10B,              PLANAR_16(YUV422, 3, 2, 1, 10) },
+    { VLC_CODEC_I422_9L,               PLANAR_16(YUV422, 3, 2, 1,  9) },
+    { VLC_CODEC_I422_9B,               PLANAR_16(YUV422, 3, 2, 1,  9) },
+    { VLC_CODEC_I444_12L,              PLANAR_16(YUV444, 3, 1, 1, 12) },
+    { VLC_CODEC_I444_12B,              PLANAR_16(YUV444, 3, 1, 1, 12) },
+    { VLC_CODEC_I444_10L,              PLANAR_16(YUV444, 3, 1, 1, 10) },
+    { VLC_CODEC_I444_10B,              PLANAR_16(YUV444, 3, 1, 1, 10) },
+    { VLC_CODEC_I444_9L,               PLANAR_16(YUV444, 3, 1, 1,  9) },
+    { VLC_CODEC_I444_9B,               PLANAR_16(YUV444, 3, 1, 1,  9) },
+    { VLC_CODEC_I444_16L,              PLANAR_16(YUV444, 3, 1, 1, 16) },
+    { VLC_CODEC_I444_16B,              PLANAR_16(YUV444, 3, 1, 1, 16) },
+    { VLC_CODEC_YUVA_444_10L,          PLANAR_16(YUV444, 4, 1, 1, 10) },
+    { VLC_CODEC_YUVA_444_10B,          PLANAR_16(YUV444, 4, 1, 1, 10) },
+    { VLC_CODEC_YUVA_444_12L,          PLANAR_16(YUV444, 4, 1, 1, 12) },
+    { VLC_CODEC_YUVA_444_12B,          PLANAR_16(YUV444, 4, 1, 1, 12) },
+    { VLC_CODEC_P010,                  SEMIPLANAR(YUV420, 2, 2, 10) },
+    { VLC_CODEC_P012,                  SEMIPLANAR(YUV420, 2, 2, 12) },
+    { VLC_CODEC_P016,                  SEMIPLANAR(YUV420, 2, 2, 16) },
+
+    { VLC_CODEC_V308,                  PACKED_FMT(YUV444, 1, 24, 8) },
+    { VLC_CODEC_YUYV,                  PACKED_FMT(YUV422, 2, 16, 8) },
+    { VLC_CODEC_YVYU,                  PACKED_FMT(YUV422, 2, 16, 8) },
+    { VLC_CODEC_UYVY,                  PACKED_FMT(YUV422, 2, 16, 8) },
+    { VLC_CODEC_VYUY,                  PACKED_FMT(YUV422, 2, 16, 8) },
+    { VLC_CODEC_YUV2,                  PACKED_FMT(YUV422, 2, 16, 8) },
+    { VLC_CODEC_RGB233,                PACKED_FMT(RGB, 1, 8, 2.6) },
+    { VLC_CODEC_BGR233,                PACKED_FMT(RGB, 1, 8, 2.6) },
+    { VLC_CODEC_RGB332,                PACKED_FMT(RGB, 1, 8, 2.6) },
+    { VLC_CODEC_YUVP,                  PACKED_FMT(OTHER, 1, 8, 0) },
+    { VLC_CODEC_RGBP,                  PACKED_FMT(OTHER, 1, 8, 0) },
+    { VLC_CODEC_GREY,                  PACKED_FMT(GREY, 1, 8, 8) },
+    { VLC_CODEC_GREY_10L,              PACKED_FMT(GREY, 2, 10, 10) },
+    { VLC_CODEC_GREY_10B,              PACKED_FMT(GREY, 2, 10, 10) },
+    { VLC_CODEC_GREY_12L,              PACKED_FMT(GREY, 2, 12, 12) },
+    { VLC_CODEC_GREY_12B,              PACKED_FMT(GREY, 2, 12, 12) },
+    { VLC_CODEC_GREY_16L,              PACKED_FMT(GREY, 2, 16, 16) },
+    { VLC_CODEC_GREY_16B,              PACKED_FMT(GREY, 2, 16, 16) },
+
+    { VLC_CODEC_RGB555BE,              PACKED_FMT(RGB, 2, 15, 5) },
+    { VLC_CODEC_RGB555LE,              PACKED_FMT(RGB, 2, 15, 5) },
+    { VLC_CODEC_BGR555LE,              PACKED_FMT(RGB, 2, 15, 5) },
+    { VLC_CODEC_BGR555BE,              PACKED_FMT(RGB, 2, 15, 5) },
+    { VLC_CODEC_RGB565LE,              PACKED_FMT(RGB, 2, 16, 5.3) },
+    { VLC_CODEC_RGB565BE,              PACKED_FMT(RGB, 2, 16, 5.3) },
+    { VLC_CODEC_BGR565LE,              PACKED_FMT(RGB, 2, 16, 5.3), },
+    { VLC_CODEC_BGR565BE,              PACKED_FMT(RGB, 2, 16, 5.3) },
+    { VLC_CODEC_RGB24,                 PACKED_FMT(RGB, 3, 24, 8) },
+    { VLC_CODEC_BGR24,                 PACKED_FMT(RGB, 3, 24, 8) },
+    { VLC_CODEC_RGBX,                  PACKED_FMT(RGB, 4, 24, 8) },
+    { VLC_CODEC_XRGB,                  PACKED_FMT(RGB, 4, 24, 8) },
+    { VLC_CODEC_BGRX,                  PACKED_FMT(RGB, 4, 24, 8) },
+    { VLC_CODEC_XBGR,                  PACKED_FMT(RGB, 4, 24, 8) },
+    { VLC_CODEC_RGBA,                  PACKED_FMT(RGB, 4, 32, 8) },
+    { VLC_CODEC_ARGB,                  PACKED_FMT(RGB, 4, 32, 8) },
+    { VLC_CODEC_BGRA,                  PACKED_FMT(RGB, 4, 32, 8) },
+    { VLC_CODEC_ABGR,                  PACKED_FMT(RGB, 4, 32, 8) },
+    { VLC_CODEC_RGBA10LE,              PACKED_FMT(RGB, 4, 32, 10) },
+    { VLC_CODEC_RGBA64,                PACKED_FMT(RGB, 8, 64, 16) },
+    { VLC_CODEC_VUYA,                  PACKED_FMT(YUV444, 4, 32, 8) },
+    { VLC_CODEC_Y210,                  PACKED_FMT(YUV422, 4, 32, 10) },
+    { VLC_CODEC_Y410,                  PACKED_FMT(YUV444, 4, 32, 10) },
+
+    { VLC_CODEC_Y211, VLC_CHROMA_SUBTYPE_YUV211,1, { {{1,4}, {1,1}} }, 4, 32, 8 },
+    { VLC_CODEC_XYZ_12L,               PACKED_FMT(OTHER, 6, 48, 12) },
+    { VLC_CODEC_XYZ_12B,               PACKED_FMT(OTHER, 6, 48, 12) },
+
+    { VLC_CODEC_VDPAU_VIDEO,           GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_VDPAU_OUTPUT,          GPU_FMT(RGB, 8) },
+    { VLC_CODEC_ANDROID_OPAQUE,        GPU_FMT(OTHER, 0) },
+    { VLC_CODEC_MMAL_OPAQUE,           GPU_FMT(OTHER, 0) },
+    { VLC_CODEC_D3D9_OPAQUE,           GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_D3D11_OPAQUE,          GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_D3D9_OPAQUE_10B,       GPU_FMT(YUV420, 10) },
+    { VLC_CODEC_D3D11_OPAQUE_10B,      GPU_FMT(YUV420, 10) },
+    { VLC_CODEC_D3D11_OPAQUE_RGBA,     GPU_FMT(RGB, 8) },
+    { VLC_CODEC_D3D11_OPAQUE_BGRA,     GPU_FMT(RGB, 8) },
+    { VLC_CODEC_D3D11_OPAQUE_ALPHA,    GPU_FMT(YUV420, 8) },
+
+    { VLC_CODEC_NVDEC_OPAQUE_16B,      GPU_FMT(YUV420, 16) },
+    { VLC_CODEC_NVDEC_OPAQUE_10B,      GPU_FMT(YUV420, 10) },
+    { VLC_CODEC_NVDEC_OPAQUE,          GPU_FMT(YUV420, 8) },
+
+    { VLC_CODEC_NVDEC_OPAQUE_444,      GPU_FMT(YUV444, 8) },
+    { VLC_CODEC_NVDEC_OPAQUE_444_16B,  GPU_FMT(YUV444, 16) },
+
+    { VLC_CODEC_CVPX_NV12,             GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_CVPX_UYVY,             GPU_FMT(YUV422, 8) },
+    { VLC_CODEC_CVPX_I420,             GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_CVPX_BGRA,             GPU_FMT(RGB, 8) },
+
+    { VLC_CODEC_CVPX_P010,             GPU_FMT(YUV420, 10) },
+
+    { VLC_CODEC_GST_MEM_OPAQUE,        GPU_FMT(OTHER, 0) },
+
+    { VLC_CODEC_VAAPI_420,             GPU_FMT(YUV420, 8) },
+    { VLC_CODEC_VAAPI_420_10BPP,       GPU_FMT(YUV420, 10) },
+    { VLC_CODEC_VAAPI_420_12BPP,       GPU_FMT(YUV420, 12) },
 };
 
 #undef PACKED_FMT
diff --git a/test/Makefile.am b/test/Makefile.am
index ac13c9c6366413f4d3e3a9cc8e3b0be8afe4eb97..ac59418b15090f4ede571d9a2016bc7b342702a1 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -37,6 +37,7 @@ check_PROGRAMS = \
 	test_src_interface_dialog \
 	test_src_media_source \
 	test_src_misc_bits \
+	test_src_misc_chroma_probe \
 	test_src_misc_epg \
 	test_src_misc_keystore \
 	test_src_misc_image \
@@ -219,6 +220,9 @@ test_src_input_decoder_SOURCES = \
 	src/input/decoder/input_decoder_scenarios.c
 test_src_input_decoder_LDADD = $(LIBVLCCORE) $(LIBVLC)
 
+test_src_misc_chroma_probe_SOURCES = src/misc/chroma_probe.c
+test_src_misc_chroma_probe_LDADD = $(LIBVLCCORE) $(LIBVLC)
+
 test_src_misc_image_SOURCES = src/misc/image.c
 test_src_misc_image_LDADD = $(LIBVLCCORE) $(LIBVLC)
 
diff --git a/test/src/misc/chroma_probe.c b/test/src/misc/chroma_probe.c
new file mode 100644
index 0000000000000000000000000000000000000000..f2849baca0a8e9fb347a209980355f3588483b9f
--- /dev/null
+++ b/test/src/misc/chroma_probe.c
@@ -0,0 +1,379 @@
+/*****************************************************************************
+ * chroma_probe.c: test for chroma_probe
+ *****************************************************************************
+ * Copyright (C) 2025 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+/* Define a builtin module for mocked parts */
+#define MODULE_NAME test_chroma_probe
+#undef VLC_DYNAMIC_PLUGIN
+#include "../../libvlc/test.h"
+
+#include <vlc/vlc.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_chroma_probe.h>
+#include <vlc_fourcc.h>
+
+#include <assert.h>
+
+const char vlc_module_name[] = MODULE_STRING;
+
+#define RESULT_MAX 6
+struct scenario_result
+{
+    unsigned cost;
+    unsigned quality;
+    vlc_fourcc_t chain[VLC_CHROMA_CONV_CHAIN_COUNT_MAX - 1 /* exclude 'from' */];
+};
+
+struct scenario
+{
+    unsigned max_indirect_steps;
+    int flags;
+    vlc_fourcc_t in;
+    vlc_fourcc_t out;
+    struct scenario_result results[RESULT_MAX];
+    size_t result_count;
+};
+
+static const struct scenario scenario_array[] =
+{
+#define COST VLC_CHROMA_CONV_FLAG_SORT_COST
+#define ONLY_YUV VLC_CHROMA_CONV_FLAG_ONLY_YUV
+#define ONLY_RGB VLC_CHROMA_CONV_FLAG_ONLY_RGB
+#define RESULT(cost, quality, chain0, chain1 ) \
+    { cost, quality, { chain0, chain1 } }
+
+#define SCENARIO0(max_indirect_steps_, from, to) { \
+    .max_indirect_steps = max_indirect_steps_, \
+    .flags = 0, \
+    .in = from, .out = to, \
+    .result_count = 0, \
+}
+
+#define SCENARIO1(max_indirect_steps_, sort_, from, to, cost, quality, chain0) { \
+    .max_indirect_steps = max_indirect_steps_, \
+    .flags = sort_, \
+    .in = from, .out = to, \
+    .results = { RESULT(cost, quality, chain0, 0) }, \
+    .result_count = 1, \
+}
+
+#define SCENARIO2(max_indirect_steps_, sort_, from, to, \
+                  result0_cost, result0_quality, result0_chain0, \
+                  result1_cost, result1_quality, result1_chain0) { \
+    .max_indirect_steps = max_indirect_steps_, \
+    .flags = sort_, \
+    .in = from, .out = to, \
+    .results = { RESULT(result0_cost, result0_quality, result0_chain0, 0), \
+                 RESULT(result1_cost, result1_quality, result1_chain0, 0), }, \
+    .result_count = 2, \
+}
+
+#define SCENARIOX(max_indirect_steps_, sort_, from, to, count, ...) { \
+    .max_indirect_steps = max_indirect_steps_, \
+    .flags = sort_, \
+    .in = from, .out = to, \
+    .results = { __VA_ARGS__ }, \
+    .result_count = count, \
+}
+
+    /* Success with a depth of 0 (Direct conversion) */
+    SCENARIO1(0, 0, VLC_CODEC_VAAPI_420, VLC_CODEC_I420, 26, 100, 0),
+
+    /* Success with a depth of 1 */
+    SCENARIO1(1, 0, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_I420,
+              47, 80, VLC_CODEC_P010),
+
+    /* Fail because it require a depth of 1 */
+    SCENARIO0(0, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_I420),
+
+    /* Check duplicated entries are removed and that we keep the lowest cost */
+    SCENARIO1(1, 0, VLC_CODEC_NV12, VLC_CODEC_I420,
+              18, 100, 0),
+
+    /* Check two_way is doing as expected */
+    SCENARIO1(1, 0, VLC_CODEC_I420, VLC_CODEC_VAAPI_420_10BPP,
+              47, 100, VLC_CODEC_P010),
+
+    /* Fail because it requires a depth of 2 */
+    SCENARIO0(1, VLC_CODEC_CVPX_P010, VLC_CODEC_P010),
+
+    /* Fail because conversion is not two-way */
+    SCENARIO0(1, VLC_CODEC_P010, VLC_CODEC_CVPX_P010),
+
+    /* Check low cost of GPU <-> GPU */
+    SCENARIO1(1, 0, VLC_CODEC_CVPX_P010, VLC_CODEC_CVPX_BGRA, 11, 80, 0),
+
+    /* Check cost and quality of direct conversion */
+    SCENARIO1(0, 0, VLC_CODEC_YUVA_444_12L, VLC_CODEC_I420, 60, 33, 0),
+
+    /* Check 1 depth conversions are correctly sorted */
+    SCENARIOX(1, 0, VLC_CODEC_VAAPI_420_10BPP, 0, 6,
+              RESULT(33, 100, VLC_CODEC_P010, 0),
+              RESULT(33, 100, VLC_CODEC_I420_10L, 0),
+              RESULT(66, 100, VLC_CODEC_P010, VLC_CODEC_I420_10L),
+              RESULT(66, 100, VLC_CODEC_I420_10L, VLC_CODEC_P010),
+              RESULT(47, 80, VLC_CODEC_P010, VLC_CODEC_I420),
+              RESULT(84, 72, VLC_CODEC_P010, VLC_CODEC_RGBA)),
+
+    /* Check default QUALITY order */
+    SCENARIOX(0, 0, VLC_CODEC_YUVA_444_12L, 0, 4,
+              RESULT(112, 90, VLC_CODEC_RGBA64, 0),
+              RESULT(88, 83, VLC_CODEC_YUVA_444_10L, 0),
+              RESULT(80, 60, VLC_CODEC_RGBA, 0),
+              RESULT(60, 33, VLC_CODEC_I420, 0)),
+
+    /* Check ONLY_YUV */
+    SCENARIOX(0, ONLY_YUV, VLC_CODEC_YUVA_444_12L, 0, 2,
+              RESULT(88, 83, VLC_CODEC_YUVA_444_10L, 0),
+              RESULT(60, 33, VLC_CODEC_I420, 0)),
+
+    /* Check ONLY_RGB */
+    SCENARIOX(0, ONLY_RGB, VLC_CODEC_YUVA_444_12L, 0, 2,
+              RESULT(112, 90, VLC_CODEC_RGBA64, 0),
+              RESULT(80, 60,  VLC_CODEC_RGBA, 0)),
+
+    /* Check COST order */
+    SCENARIOX(0, COST, VLC_CODEC_YUVA_444_12L, 0, 4,
+              RESULT(60, 33, VLC_CODEC_I420, 0),
+              RESULT(80, 60,  VLC_CODEC_RGBA, 0),
+              RESULT(88, 83, VLC_CODEC_YUVA_444_10L, 0),
+              RESULT(112, 90, VLC_CODEC_RGBA64, 0)),
+
+    /* Check VLC_CHROMA_CONV_ADD_IN_OUTTLIST, and quality order with smaller
+     * RGB chromas */
+    SCENARIOX(0, 0, VLC_CODEC_YV12, 0, 5,
+              RESULT(36, 90, VLC_CODEC_XRGB, 0),
+              RESULT(28, 59, VLC_CODEC_RGB565, 0),
+              RESULT(28, 59, VLC_CODEC_BGR565, 0),
+              RESULT(27, 56, VLC_CODEC_RGB555, 0),
+              RESULT(27, 56, VLC_CODEC_BGR555, 0)),
+
+    /* Check VLC_CHROMA_CONV_ADD_OUT_INLIST */
+    SCENARIO1(0, 0, VLC_CODEC_NV16, VLC_CODEC_I422, 32, 100, 0),
+    SCENARIO1(0, 0, VLC_CODEC_YUYV, VLC_CODEC_I422, 32, 100, 0),
+    SCENARIO1(0, 0, VLC_CODEC_UYVY, VLC_CODEC_I422, 32, 100, 0),
+
+    /* Check VLC_CHROMA_CONV_ADD_ALL */
+    SCENARIO1(1, 0, VLC_CODEC_I444_12L, VLC_CODEC_I444, 60, 66, 0),
+    SCENARIO1(1, 0, VLC_CODEC_I444, VLC_CODEC_I444_12L, 60, 100, 0),
+    SCENARIO1(1, 0, VLC_CODEC_I444_12L, VLC_CODEC_I444_10L, 66, 83, 0),
+    SCENARIO1(1, 0, VLC_CODEC_I444_10L, VLC_CODEC_I444_12L, 66, 100, 0),
+};
+
+static void ProbeChroma(vlc_chroma_conv_vec *vec)
+{
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420, VLC_CODEC_I420, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_VAAPI_420_10BPP, VLC_CODEC_I420_10L, true);
+
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_I420, VLC_CODEC_NV12, true);
+    vlc_chroma_conv_add(vec, 0.75, VLC_CODEC_I420, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_I420_10L, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_RGBA, VLC_CODEC_P010, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_RGBA, VLC_CODEC_NV12, true);
+
+    /* Test duplicated entries are removed */
+    vlc_chroma_conv_add(vec, 1.0, VLC_CODEC_I420, VLC_CODEC_NV12, true);
+
+    /* Don't change this order as this is used to test to cost sort
+     * (we don't want the result to be naturally sorted) */
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUVA_444_12L, VLC_CODEC_RGBA, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUVA_444_12L, VLC_CODEC_I420, true);
+
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUVA_444_12L, VLC_CODEC_YUVA_444_10L, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_YUVA_444_12L, VLC_CODEC_RGBA64, true);
+
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_NV12, VLC_CODEC_CVPX_BGRA, false);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_NV12, VLC_CODEC_NV12, false);
+    vlc_chroma_conv_add(vec, 0.25, VLC_CODEC_CVPX_P010, VLC_CODEC_CVPX_BGRA, false);
+    vlc_chroma_conv_add(vec, 1.1, VLC_CODEC_CVPX_BGRA, VLC_CODEC_RGBA, false);
+
+    vlc_chroma_conv_add_in_outlist(vec, 1, VLC_CODEC_YV12, VLC_CODEC_XRGB,
+                                    VLC_CODEC_RGB565, VLC_CODEC_BGR565,
+                                    VLC_CODEC_RGB555, VLC_CODEC_BGR555);
+
+    vlc_chroma_conv_add_out_inlist(vec, 1, VLC_CODEC_I422, VLC_CODEC_NV16,
+            VLC_CODEC_YUYV, VLC_CODEC_UYVY);
+
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_I444, VLC_CODEC_I444_10L, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_I444, VLC_CODEC_I444_12L, true);
+    vlc_chroma_conv_add(vec, 1, VLC_CODEC_I444_10L, VLC_CODEC_I444_12L, true);
+
+    /* Test duplicated entries are removed */
+    vlc_chroma_conv_add(vec, 1.0, VLC_CODEC_I420, VLC_CODEC_NV12, true);
+}
+
+vlc_module_begin()
+    set_callback_chroma_conv_probe(ProbeChroma)
+vlc_module_end()
+
+VLC_EXPORT vlc_plugin_cb vlc_static_modules[] = {
+    VLC_SYMBOL(vlc_entry),
+    NULL
+};
+
+static void
+print_results(const struct vlc_chroma_conv_result *array, size_t count)
+{
+    for (size_t i = 0; i < count; ++i)
+    {
+        const struct vlc_chroma_conv_result *res = &array[i];
+        char *res_str = vlc_chroma_conv_result_ToString(res);
+        assert(res_str != NULL);
+        fprintf(stderr, "\tres[%zu]: %s\n", i, res_str);
+        free(res_str);
+    }
+}
+
+static void
+check_results(const struct scenario *scr,
+              const struct vlc_chroma_conv_result *results)
+{
+    for (size_t i = 0; i < scr->result_count; ++i)
+    {
+        const struct vlc_chroma_conv_result *result = &results[i];
+        const struct scenario_result *scr_result = &scr->results[i];
+
+        assert(result->chain_count > 1);
+        assert(result->chain_count <= VLC_CHROMA_CONV_CHAIN_COUNT_MAX);
+
+        /*  Reconstruct the expected fourcc array from the scenario */
+        vlc_fourcc_t scr_chain[VLC_CHROMA_CONV_CHAIN_COUNT_MAX];
+        bool end_reached = false;
+        size_t scr_count = 1;
+        scr_chain[0] = scr->in;
+        for (size_t j = 1; j < VLC_CHROMA_CONV_CHAIN_COUNT_MAX; ++j)
+        {
+            if (end_reached)
+            {
+                scr_chain[j] = 0;
+                continue;
+            }
+
+            if (scr_result->chain[j - 1] != 0)
+            {
+                scr_chain[j] = scr_result->chain[j - 1];
+                scr_count++;
+            }
+            else
+            {
+                if (scr->out != 0)
+                {
+                    scr_chain[j] = scr->out;
+                    scr_count++;
+                }
+                end_reached = true;
+            }
+        }
+
+        assert(scr_count == result->chain_count);
+        size_t j;
+        for (j = 0; j < result->chain_count; ++j)
+            assert(result->chain[j] == scr_chain[j]);
+        for (; j < VLC_CHROMA_CONV_CHAIN_COUNT_MAX - 1; ++j)
+            assert(scr_result->chain[j - 1] == 0);
+
+        assert(result->cost == scr_result->cost);
+        assert(result->quality == scr_result->quality);
+    }
+}
+
+int main(int argc, const char *argv[])
+{
+    test_init();
+
+    if (argc > 1 && strlen(argv[1]) >= 4)
+    {
+        /* Disable test module (use all VLC modules) */
+        vlc_static_modules[0] = NULL;
+
+        unsigned max_indirect_steps = 1;
+        if (argc > 2)
+            max_indirect_steps = atoi(argv[2]);
+
+        int flags = 0;
+        if (argc > 3)
+            flags = atoi(argv[3]);
+
+        const char *f = argv[1];
+        vlc_fourcc_t from_fourcc = VLC_FOURCC(f[0], f[1], f[2], f[3]);
+        vlc_fourcc_t to_fourcc = 0;
+        if (f[4] == '-' && strlen(f) >= 9)
+            to_fourcc = VLC_FOURCC(f[5], f[6], f[7], f[8]);
+        libvlc_instance_t *vlc = libvlc_new(0, NULL);
+        assert(vlc != NULL);
+
+        size_t count;
+        struct vlc_chroma_conv_result *results =
+            vlc_chroma_conv_Probe(from_fourcc, to_fourcc, 0, 0,
+                                  max_indirect_steps, flags, &count);
+        assert(results != NULL);
+        print_results(results, count);
+        free(results);
+
+        libvlc_release(vlc);
+        return 0;
+    }
+
+    /* Disable all modules except the one from this test */
+    const char *libvlc_argv[] = {
+        "--no-plugins-cache",
+        "--no-plugins-scan",
+    };
+    int libvlc_argc = ARRAY_SIZE(libvlc_argv);
+
+    libvlc_instance_t *vlc = libvlc_new(libvlc_argc, libvlc_argv);
+    assert(vlc != NULL);
+
+    size_t scenario_count = ARRAY_SIZE(scenario_array);
+    for (size_t i = 0; i < scenario_count; i++)
+    {
+        const struct scenario *scr = &scenario_array[i];
+
+        fprintf(stderr, "scenario: %4.4s -> %4.4s flags: 0x%x, "
+                "max_indirect_steps: %u result_count: %zu\n",
+                (const char *)&scr->in, (const char *)&scr->out,
+                scr->flags,
+                scr->max_indirect_steps, scr->result_count);
+
+        size_t count;
+        struct vlc_chroma_conv_result *results =
+            vlc_chroma_conv_Probe(scr->in, scr->out, 0, 0,
+                                  scr->max_indirect_steps, scr->flags, &count);
+        if (results == NULL)
+        {
+            assert(scr->result_count == 0);
+            continue;
+        }
+        print_results(results, count);
+        assert(count == scr->result_count);
+        check_results(scr, results);
+        free(results);
+    }
+
+    libvlc_release(vlc);
+
+    return 0;
+}