File mpv_PR12802.patch of Package mpv

From 01bf36c4864d8cd54e0be5ed3c6d3dfc3b5d3162 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <kasper93@gmail.com>
Date: Sat, 4 Nov 2023 03:55:38 +0100
Subject: [PATCH 1/5] csputils: replace mp_colorspace with pl_color_space

---
 demux/demux_mkv.c             |  21 +-
 demux/stheader.h              |   5 +-
 filters/f_decoder_wrapper.c   |   3 +-
 player/command.c              |  10 +-
 sub/draw_bmp.c                |   4 +-
 sub/sd_ass.c                  |  66 +++---
 test/img_format.c             |   2 +-
 test/repack.c                 |  32 +--
 test/scale_test.c             |   4 +-
 video/csputils.c              | 369 +++++++++++-----------------------
 video/csputils.h              | 122 ++---------
 video/filter/vf_d3d11vpp.c    |   4 +-
 video/filter/vf_fingerprint.c |   2 +-
 video/filter/vf_format.c      |  28 +--
 video/filter/vf_vapoursynth.c |   7 +-
 video/filter/vf_vavpp.c       |   2 +-
 video/image_writer.c          |  31 +--
 video/img_format.c            |  10 +-
 video/img_format.h            |   6 +-
 video/mp_image.c              | 157 ++++++++-------
 video/mp_image.h              |   4 +-
 video/out/d3d11/context.c     |   2 +-
 video/out/gpu/context.h       |   2 +-
 video/out/gpu/d3d11_helpers.c |  38 ++--
 video/out/gpu/d3d11_helpers.h |   4 +-
 video/out/gpu/lcms.c          |  40 ++--
 video/out/gpu/lcms.h          |   6 +-
 video/out/gpu/video.c         | 132 ++++++------
 video/out/gpu/video.h         |   1 -
 video/out/gpu/video_shaders.c |  95 ++++-----
 video/out/gpu/video_shaders.h |   9 +-
 video/out/opengl/hwdec_rpi.c  |  10 +-
 video/out/placebo/utils.c     | 148 --------------
 video/out/placebo/utils.h     |   7 -
 video/out/vo_gpu_next.c       |  34 ++--
 video/out/vo_lavc.c           |   8 +-
 video/out/vo_rpi.c            |  10 +-
 video/out/vo_vaapi.c          |   2 +-
 video/out/vo_xv.c             |   6 +-
 video/repack.c                |  16 +-
 video/sws_utils.c             |  19 +-
 video/vaapi.c                 |   8 +-
 video/vaapi.h                 |   2 +-
 video/zimg.c                  | 102 +++++-----
 44 files changed, 610 insertions(+), 980 deletions(-)

diff --git a/demux/demux_mkv.c b/demux/demux_mkv.c
index 41226c56ba75..45719ede8922 100644
--- a/demux/demux_mkv.c
+++ b/demux/demux_mkv.c
@@ -36,6 +36,8 @@
 #include <libavcodec/avcodec.h>
 #include <libavcodec/version.h>
 
+#include <libplacebo/utils/libav.h>
+
 #include "config.h"
 
 #if HAVE_ZLIB
@@ -108,7 +110,8 @@ typedef struct mkv_track {
     double v_frate;
     uint32_t colorspace;
     int stereo_mode;
-    struct mp_colorspace color;
+    struct pl_color_repr repr;
+    struct pl_color_space color;
     uint32_t v_crop_top, v_crop_left, v_crop_right, v_crop_bottom;
     float v_projection_pose_roll;
     bool v_projection_pose_roll_set;
@@ -573,24 +576,24 @@ static void parse_trackcolour(struct demuxer *demuxer, struct mkv_track *track,
     // 23001-8:2013/DCOR1, which is the same order used by libavutil/pixfmt.h,
     // so we can just re-use our avcol_ conversion functions.
     if (colour->n_matrix_coefficients) {
-        track->color.space = avcol_spc_to_mp_csp(colour->matrix_coefficients);
+        track->repr.sys = pl_system_from_av(colour->matrix_coefficients);
         MP_DBG(demuxer, "|    + Matrix: %s\n",
-                   m_opt_choice_str(mp_csp_names, track->color.space));
+                   m_opt_choice_str(pl_csp_names, track->repr.sys));
     }
     if (colour->n_primaries) {
-        track->color.primaries = avcol_pri_to_mp_csp_prim(colour->primaries);
+        track->color.primaries = pl_primaries_from_av(colour->primaries);
         MP_DBG(demuxer, "|    + Primaries: %s\n",
-                   m_opt_choice_str(mp_csp_prim_names, track->color.primaries));
+                   m_opt_choice_str(pl_csp_prim_names, track->color.primaries));
     }
     if (colour->n_transfer_characteristics) {
-        track->color.gamma = avcol_trc_to_mp_csp_trc(colour->transfer_characteristics);
+        track->color.transfer = pl_transfer_from_av(colour->transfer_characteristics);
         MP_DBG(demuxer, "|    + Gamma: %s\n",
-                   m_opt_choice_str(mp_csp_trc_names, track->color.gamma));
+                   m_opt_choice_str(pl_csp_trc_names, track->color.transfer));
     }
     if (colour->n_range) {
-        track->color.levels = avcol_range_to_mp_csp_levels(colour->range);
+        track->repr.levels = pl_levels_from_av(colour->range);
         MP_DBG(demuxer, "|    + Levels: %s\n",
-                   m_opt_choice_str(mp_csp_levels_names, track->color.levels));
+                   m_opt_choice_str(pl_csp_levels_names, track->repr.levels));
     }
     if (colour->n_max_cll) {
         track->color.hdr.max_cll = colour->max_cll;
diff --git a/demux/stheader.h b/demux/stheader.h
index 1bc036d648df..597c97843968 100644
--- a/demux/stheader.h
+++ b/demux/stheader.h
@@ -105,8 +105,9 @@ struct mp_codec_params {
     int disp_w, disp_h;   // display size
     int rotate;           // intended display rotation, in degrees, [0, 359]
     int stereo_mode;      // mp_stereo3d_mode (0 if none/unknown)
-    struct mp_colorspace color; // colorspace info where available
-    struct mp_rect crop;        // crop to be applied
+    struct pl_color_space color; // colorspace info where available
+    struct pl_color_repr repr;   // color representaion info where available
+    struct mp_rect crop;         // crop to be applied
 
     // STREAM_VIDEO + STREAM_AUDIO
     int bits_per_coded_sample;
diff --git a/filters/f_decoder_wrapper.c b/filters/f_decoder_wrapper.c
index 87489ccdf1bd..12535c926c96 100644
--- a/filters/f_decoder_wrapper.c
+++ b/filters/f_decoder_wrapper.c
@@ -616,7 +616,8 @@ static void fix_image_params(struct priv *p,
         m.rotate = (m.rotate + opts->video_rotate) % 360;
     }
 
-    mp_colorspace_merge(&m.color, &c->color);
+    pl_color_space_merge(&m.color, &c->color);
+    pl_color_repr_merge(&m.repr, &c->repr);
 
     // Guess missing colorspace fields from metadata. This guarantees all
     // fields are at least set to legal values afterwards.
diff --git a/player/command.c b/player/command.c
index 8d6ab07d03f6..33d30508e79f 100644
--- a/player/command.c
+++ b/player/command.c
@@ -2317,16 +2317,16 @@ static int property_imgparams(struct mp_image_params p, int action, void *arg)
         {"aspect-name",     SUB_PROP_STR(aspect_name), .unavailable = !aspect_name},
         {"par",             SUB_PROP_FLOAT(p.p_w / (double)p.p_h)},
         {"colormatrix",
-            SUB_PROP_STR(m_opt_choice_str(mp_csp_names, p.color.space))},
+            SUB_PROP_STR(m_opt_choice_str(pl_csp_names, p.repr.sys))},
         {"colorlevels",
-            SUB_PROP_STR(m_opt_choice_str(mp_csp_levels_names, p.color.levels))},
+            SUB_PROP_STR(m_opt_choice_str(pl_csp_levels_names, p.repr.levels))},
         {"primaries",
-            SUB_PROP_STR(m_opt_choice_str(mp_csp_prim_names, p.color.primaries))},
+            SUB_PROP_STR(m_opt_choice_str(pl_csp_prim_names, p.color.primaries))},
         {"gamma",
-            SUB_PROP_STR(m_opt_choice_str(mp_csp_trc_names, p.color.gamma))},
+            SUB_PROP_STR(m_opt_choice_str(pl_csp_trc_names, p.color.transfer))},
         {"sig-peak", SUB_PROP_FLOAT(p.color.hdr.max_luma / MP_REF_WHITE)},
         {"light",
-            SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.color.light))},
+            SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.light))},
         {"chroma-location",
             SUB_PROP_STR(m_opt_choice_str(mp_chroma_names, p.chroma_location))},
         {"stereo-in",
diff --git a/sub/draw_bmp.c b/sub/draw_bmp.c
index 58db162a56d3..cde8f503f334 100644
--- a/sub/draw_bmp.c
+++ b/sub/draw_bmp.c
@@ -546,7 +546,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
     mp_get_regular_imgfmt(&vfdesc, mp_repack_get_format_dst(p->video_to_f32));
     assert(vfdesc.num_planes); // must have succeeded
 
-    if (params->color.space == MP_CSP_RGB && vfdesc.num_planes >= 3) {
+    if (params->repr.sys == PL_COLOR_SYSTEM_RGB && vfdesc.num_planes >= 3) {
         use_shortcut = true;
 
         if (vfdesc.component_type == MP_COMPONENT_TYPE_UINT &&
@@ -724,7 +724,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
             p->alpha_overlay->stride[0] = p->video_overlay->stride[aplane];
 
             // Full range gray always has the same range as alpha.
-            p->alpha_overlay->params.color.levels = MP_CSP_LEVELS_PC;
+            p->alpha_overlay->params.repr.levels = PL_COLOR_LEVELS_FULL;
             mp_image_params_guess_csp(&p->alpha_overlay->params);
 
             p->calpha_overlay =
diff --git a/sub/sd_ass.c b/sub/sd_ass.c
index 6742f6f658fd..aa651375bec1 100644
--- a/sub/sd_ass.c
+++ b/sub/sd_ass.c
@@ -897,27 +897,27 @@ static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
 {
     struct mp_subtitle_opts *opts = sd->opts;
     struct sd_ass_priv *ctx = sd->priv;
-    enum mp_csp csp = 0;
-    enum mp_csp_levels levels = 0;
+    enum pl_color_system csp = 0;
+    enum pl_color_levels levels = 0;
     if (opts->ass_vsfilter_color_compat == 0) // "no"
         return;
     bool force_601 = opts->ass_vsfilter_color_compat == 3;
     ASS_Track *track = ctx->ass_track;
     static const int ass_csp[] = {
-        [YCBCR_BT601_TV]        = MP_CSP_BT_601,
-        [YCBCR_BT601_PC]        = MP_CSP_BT_601,
-        [YCBCR_BT709_TV]        = MP_CSP_BT_709,
-        [YCBCR_BT709_PC]        = MP_CSP_BT_709,
-        [YCBCR_SMPTE240M_TV]    = MP_CSP_SMPTE_240M,
-        [YCBCR_SMPTE240M_PC]    = MP_CSP_SMPTE_240M,
+        [YCBCR_BT601_TV]        = PL_COLOR_SYSTEM_BT_601,
+        [YCBCR_BT601_PC]        = PL_COLOR_SYSTEM_BT_601,
+        [YCBCR_BT709_TV]        = PL_COLOR_SYSTEM_BT_709,
+        [YCBCR_BT709_PC]        = PL_COLOR_SYSTEM_BT_709,
+        [YCBCR_SMPTE240M_TV]    = PL_COLOR_SYSTEM_SMPTE_240M,
+        [YCBCR_SMPTE240M_PC]    = PL_COLOR_SYSTEM_SMPTE_240M,
     };
     static const int ass_levels[] = {
-        [YCBCR_BT601_TV]        = MP_CSP_LEVELS_TV,
-        [YCBCR_BT601_PC]        = MP_CSP_LEVELS_PC,
-        [YCBCR_BT709_TV]        = MP_CSP_LEVELS_TV,
-        [YCBCR_BT709_PC]        = MP_CSP_LEVELS_PC,
-        [YCBCR_SMPTE240M_TV]    = MP_CSP_LEVELS_TV,
-        [YCBCR_SMPTE240M_PC]    = MP_CSP_LEVELS_PC,
+        [YCBCR_BT601_TV]        = PL_COLOR_LEVELS_LIMITED,
+        [YCBCR_BT601_PC]        = PL_COLOR_LEVELS_FULL,
+        [YCBCR_BT709_TV]        = PL_COLOR_LEVELS_LIMITED,
+        [YCBCR_BT709_PC]        = PL_COLOR_LEVELS_FULL,
+        [YCBCR_SMPTE240M_TV]    = PL_COLOR_LEVELS_LIMITED,
+        [YCBCR_SMPTE240M_PC]    = PL_COLOR_LEVELS_FULL,
     };
     int trackcsp = track->YCbCrMatrix;
     if (force_601)
@@ -930,8 +930,8 @@ static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
     if (trackcsp < sizeof(ass_levels) / sizeof(ass_levels[0]))
         levels = ass_levels[trackcsp];
     if (trackcsp == YCBCR_DEFAULT) {
-        csp = MP_CSP_BT_601;
-        levels = MP_CSP_LEVELS_TV;
+        csp = PL_COLOR_SYSTEM_BT_601;
+        levels = PL_COLOR_LEVELS_LIMITED;
     }
     // Unknown colorspace (either YCBCR_UNKNOWN, or a valid value unknown to us)
     if (!csp || !levels)
@@ -940,42 +940,42 @@ static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
     struct mp_image_params params = ctx->video_params;
 
     if (force_601) {
-        params.color = (struct mp_colorspace){
-            .space = MP_CSP_BT_709,
-            .levels = MP_CSP_LEVELS_TV,
+        params.repr = (struct pl_color_repr){
+            .sys = PL_COLOR_SYSTEM_BT_709,
+            .levels = PL_COLOR_LEVELS_LIMITED,
         };
     }
 
-    if ((csp == params.color.space && levels == params.color.levels) ||
-            params.color.space == MP_CSP_RGB) // Even VSFilter doesn't mangle on RGB video
+    if ((csp == params.repr.sys && levels == params.repr.levels) ||
+            params.repr.sys == PL_COLOR_SYSTEM_RGB) // Even VSFilter doesn't mangle on RGB video
         return;
 
-    bool basic_conv = params.color.space == MP_CSP_BT_709 &&
-                      params.color.levels == MP_CSP_LEVELS_TV &&
-                      csp == MP_CSP_BT_601 &&
-                      levels == MP_CSP_LEVELS_TV;
+    bool basic_conv = params.repr.sys == PL_COLOR_SYSTEM_BT_709 &&
+                      params.repr.levels == PL_COLOR_LEVELS_LIMITED &&
+                      csp == PL_COLOR_SYSTEM_BT_601 &&
+                      levels == PL_COLOR_LEVELS_LIMITED;
 
     // With "basic", only do as much as needed for basic compatibility.
     if (opts->ass_vsfilter_color_compat == 1 && !basic_conv)
         return;
 
-    if (params.color.space != ctx->last_params.color.space ||
-        params.color.levels != ctx->last_params.color.levels)
+    if (params.repr.sys != ctx->last_params.repr.sys ||
+        params.repr.levels != ctx->last_params.repr.levels)
     {
         int msgl = basic_conv ? MSGL_V : MSGL_WARN;
         ctx->last_params = params;
         MP_MSG(sd, msgl, "mangling colors like vsfilter: "
                "RGB -> %s %s -> %s %s -> RGB\n",
-               m_opt_choice_str(mp_csp_names, csp),
-               m_opt_choice_str(mp_csp_levels_names, levels),
-               m_opt_choice_str(mp_csp_names, params.color.space),
-               m_opt_choice_str(mp_csp_names, params.color.levels));
+               m_opt_choice_str(pl_csp_names, csp),
+               m_opt_choice_str(pl_csp_levels_names, levels),
+               m_opt_choice_str(pl_csp_names, params.repr.sys),
+               m_opt_choice_str(pl_csp_names, params.repr.levels));
     }
 
     // Conversion that VSFilter would use
     struct mp_csp_params vs_params = MP_CSP_PARAMS_DEFAULTS;
-    vs_params.color.space = csp;
-    vs_params.color.levels = levels;
+    vs_params.repr.sys = csp;
+    vs_params.repr.levels = levels;
     struct mp_cmat vs_yuv2rgb, vs_rgb2yuv;
     mp_get_csp_matrix(&vs_params, &vs_yuv2rgb);
     mp_invert_cmat(&vs_rgb2yuv, &vs_yuv2rgb);
diff --git a/test/img_format.c b/test/img_format.c
index 3cc8ff5fe01d..3b553f6531aa 100644
--- a/test/img_format.c
+++ b/test/img_format.c
@@ -40,7 +40,7 @@ int main(int argc, char *argv[])
 
         int fcsp = mp_imgfmt_get_forced_csp(mpfmt);
         if (fcsp)
-            fprintf(f, "fcsp=%s ", m_opt_choice_str(mp_csp_names, fcsp));
+            fprintf(f, "fcsp=%s ", m_opt_choice_str(pl_csp_names, fcsp));
         fprintf(f, "ctype=%s\n", comp_type(mp_imgfmt_get_component_type(mpfmt)));
 
         struct mp_imgfmt_desc d = mp_imgfmt_get_desc(mpfmt);
diff --git a/test/repack.c b/test/repack.c
index a37559b70545..c6ec506ecee5 100644
--- a/test/repack.c
+++ b/test/repack.c
@@ -326,8 +326,8 @@ static int try_repack(FILE *f, int imgfmt, int flags, int not_if_fmt)
     return b;
 }
 
-static void check_float_repack(int imgfmt, enum mp_csp csp,
-                               enum mp_csp_levels levels)
+static void check_float_repack(int imgfmt, enum pl_color_system csp,
+                               enum pl_color_levels levels)
 {
     imgfmt = UNFUCK(imgfmt);
 
@@ -349,12 +349,12 @@ static void check_float_repack(int imgfmt, enum mp_csp csp,
     struct mp_image *src = mp_image_alloc(imgfmt, w, 1);
     assert(src);
 
-    src->params.color.space = csp;
-    src->params.color.levels = levels;
+    src->params.repr.sys = csp;
+    src->params.repr.levels = levels;
     mp_image_params_guess_csp(&src->params);
     // mpv may not allow all combinations
-    assert(src->params.color.space == csp);
-    assert(src->params.color.levels == levels);
+    assert(src->params.repr.sys == csp);
+    assert(src->params.repr.levels == levels);
 
     for (int p = 0; p < src->num_planes; p++) {
         int val = 0;
@@ -384,6 +384,8 @@ static void check_float_repack(int imgfmt, enum mp_csp csp,
 
     z_f->params.color = r_f->params.color = z_i->params.color =
         r_i->params.color = src->params.color;
+    z_f->params.repr = r_f->params.repr = z_i->params.repr =
+        r_i->params.repr = src->params.repr;
 
     // The idea is to use zimg to cross-check conversion.
     struct mp_sws_context *s = mp_sws_alloc(NULL);
@@ -503,15 +505,15 @@ int main(int argc, char *argv[])
     assert_text_files_equal(refdir, outdir, "repack.txt",
                             "This can fail if FFmpeg/libswscale adds or removes pixfmts.");
 
-    check_float_repack(-AV_PIX_FMT_GBRAP, MP_CSP_RGB, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_GBRAP10, MP_CSP_RGB, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_GBRAP16, MP_CSP_RGB, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_YUVA444P, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_YUVA444P, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
-    check_float_repack(-AV_PIX_FMT_YUVA444P10, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_YUVA444P10, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
-    check_float_repack(-AV_PIX_FMT_YUVA444P16, MP_CSP_BT_709, MP_CSP_LEVELS_PC);
-    check_float_repack(-AV_PIX_FMT_YUVA444P16, MP_CSP_BT_709, MP_CSP_LEVELS_TV);
+    check_float_repack(-AV_PIX_FMT_GBRAP, PL_COLOR_SYSTEM_RGB, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_GBRAP10, PL_COLOR_SYSTEM_RGB, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_GBRAP16, PL_COLOR_SYSTEM_RGB, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_YUVA444P, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_YUVA444P, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_LIMITED);
+    check_float_repack(-AV_PIX_FMT_YUVA444P10, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_YUVA444P10, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_LIMITED);
+    check_float_repack(-AV_PIX_FMT_YUVA444P16, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_FULL);
+    check_float_repack(-AV_PIX_FMT_YUVA444P16, PL_COLOR_SYSTEM_BT_709, PL_COLOR_LEVELS_LIMITED);
 
     // Determine the list of possible draw_bmp input formats. Do this here
     // because it mostly depends on repack and imgformat stuff.
diff --git a/test/scale_test.c b/test/scale_test.c
index f919dca1e241..fa7886de4823 100644
--- a/test/scale_test.c
+++ b/test/scale_test.c
@@ -10,7 +10,7 @@ static struct mp_image *gen_repack_test_img(int w, int h, int bytes, bool rgb,
     struct mp_regular_imgfmt planar_desc = {
         .component_type = MP_COMPONENT_TYPE_UINT,
         .component_size = bytes,
-        .forced_csp = rgb ? MP_CSP_RGB : 0,
+        .forced_csp = rgb ? PL_COLOR_SYSTEM_RGB : 0,
         .num_planes = alpha ? 4 : 3,
         .planes = {
             {1, {rgb ? 2 : 1}},
@@ -129,7 +129,7 @@ void repack_test_run(struct scale_test *stest)
             if (!mp_get_regular_imgfmt(&rdesc, ofmt))
                 continue;
         }
-        if (rdesc.num_planes > 1 || rdesc.forced_csp != MP_CSP_RGB)
+        if (rdesc.num_planes > 1 || rdesc.forced_csp != PL_COLOR_SYSTEM_RGB)
             continue;
 
         struct mp_image *test_img = NULL;
diff --git a/video/csputils.c b/video/csputils.c
index 59200c5669a9..3b43ba9f5b69 100644
--- a/video/csputils.c
+++ b/video/csputils.c
@@ -32,66 +32,66 @@
 #include "options/m_config.h"
 #include "options/m_option.h"
 
-const struct m_opt_choice_alternatives mp_csp_names[] = {
-    {"auto",        MP_CSP_AUTO},
-    {"bt.601",      MP_CSP_BT_601},
-    {"bt.709",      MP_CSP_BT_709},
-    {"smpte-240m",  MP_CSP_SMPTE_240M},
-    {"bt.2020-ncl", MP_CSP_BT_2020_NC},
-    {"bt.2020-cl",  MP_CSP_BT_2020_C},
-    {"rgb",         MP_CSP_RGB},
-    {"xyz",         MP_CSP_XYZ},
-    {"ycgco",       MP_CSP_YCGCO},
+const struct m_opt_choice_alternatives pl_csp_names[] = {
+    {"auto",        PL_COLOR_SYSTEM_UNKNOWN},
+    {"bt.601",      PL_COLOR_SYSTEM_BT_601},
+    {"bt.709",      PL_COLOR_SYSTEM_BT_709},
+    {"smpte-240m",  PL_COLOR_SYSTEM_SMPTE_240M},
+    {"bt.2020-ncl", PL_COLOR_SYSTEM_BT_2020_NC},
+    {"bt.2020-cl",  PL_COLOR_SYSTEM_BT_2020_C},
+    {"rgb",         PL_COLOR_SYSTEM_RGB},
+    {"xyz",         PL_COLOR_SYSTEM_XYZ},
+    {"ycgco",       PL_COLOR_SYSTEM_YCGCO},
     {0}
 };
 
-const struct m_opt_choice_alternatives mp_csp_levels_names[] = {
-    {"auto",        MP_CSP_LEVELS_AUTO},
-    {"limited",     MP_CSP_LEVELS_TV},
-    {"full",        MP_CSP_LEVELS_PC},
+const struct m_opt_choice_alternatives pl_csp_levels_names[] = {
+    {"auto",        PL_COLOR_LEVELS_UNKNOWN},
+    {"limited",     PL_COLOR_LEVELS_LIMITED},
+    {"full",        PL_COLOR_LEVELS_FULL},
     {0}
 };
 
-const struct m_opt_choice_alternatives mp_csp_prim_names[] = {
-    {"auto",        MP_CSP_PRIM_AUTO},
-    {"bt.601-525",  MP_CSP_PRIM_BT_601_525},
-    {"bt.601-625",  MP_CSP_PRIM_BT_601_625},
-    {"bt.709",      MP_CSP_PRIM_BT_709},
-    {"bt.2020",     MP_CSP_PRIM_BT_2020},
-    {"bt.470m",     MP_CSP_PRIM_BT_470M},
-    {"apple",       MP_CSP_PRIM_APPLE},
-    {"adobe",       MP_CSP_PRIM_ADOBE},
-    {"prophoto",    MP_CSP_PRIM_PRO_PHOTO},
-    {"cie1931",     MP_CSP_PRIM_CIE_1931},
-    {"dci-p3",      MP_CSP_PRIM_DCI_P3},
-    {"display-p3",  MP_CSP_PRIM_DISPLAY_P3},
-    {"v-gamut",     MP_CSP_PRIM_V_GAMUT},
-    {"s-gamut",     MP_CSP_PRIM_S_GAMUT},
-    {"ebu3213",     MP_CSP_PRIM_EBU_3213},
-    {"film-c",      MP_CSP_PRIM_FILM_C},
-    {"aces-ap0",    MP_CSP_PRIM_ACES_AP0},
-    {"aces-ap1",    MP_CSP_PRIM_ACES_AP1},
+const struct m_opt_choice_alternatives pl_csp_prim_names[] = {
+    {"auto",        PL_COLOR_PRIM_UNKNOWN},
+    {"bt.601-525",  PL_COLOR_PRIM_BT_601_525},
+    {"bt.601-625",  PL_COLOR_PRIM_BT_601_625},
+    {"bt.709",      PL_COLOR_PRIM_BT_709},
+    {"bt.2020",     PL_COLOR_PRIM_BT_2020},
+    {"bt.470m",     PL_COLOR_PRIM_BT_470M},
+    {"apple",       PL_COLOR_PRIM_APPLE},
+    {"adobe",       PL_COLOR_PRIM_ADOBE},
+    {"prophoto",    PL_COLOR_PRIM_PRO_PHOTO},
+    {"cie1931",     PL_COLOR_PRIM_CIE_1931},
+    {"dci-p3",      PL_COLOR_PRIM_DCI_P3},
+    {"display-p3",  PL_COLOR_PRIM_DISPLAY_P3},
+    {"v-gamut",     PL_COLOR_PRIM_V_GAMUT},
+    {"s-gamut",     PL_COLOR_PRIM_S_GAMUT},
+    {"ebu3213",     PL_COLOR_PRIM_EBU_3213},
+    {"film-c",      PL_COLOR_PRIM_FILM_C},
+    {"aces-ap0",    PL_COLOR_PRIM_ACES_AP0},
+    {"aces-ap1",    PL_COLOR_PRIM_ACES_AP1},
     {0}
 };
 
-const struct m_opt_choice_alternatives mp_csp_trc_names[] = {
-    {"auto",        MP_CSP_TRC_AUTO},
-    {"bt.1886",     MP_CSP_TRC_BT_1886},
-    {"srgb",        MP_CSP_TRC_SRGB},
-    {"linear",      MP_CSP_TRC_LINEAR},
-    {"gamma1.8",    MP_CSP_TRC_GAMMA18},
-    {"gamma2.0",    MP_CSP_TRC_GAMMA20},
-    {"gamma2.2",    MP_CSP_TRC_GAMMA22},
-    {"gamma2.4",    MP_CSP_TRC_GAMMA24},
-    {"gamma2.6",    MP_CSP_TRC_GAMMA26},
-    {"gamma2.8",    MP_CSP_TRC_GAMMA28},
-    {"prophoto",    MP_CSP_TRC_PRO_PHOTO},
-    {"pq",          MP_CSP_TRC_PQ},
-    {"hlg",         MP_CSP_TRC_HLG},
-    {"v-log",       MP_CSP_TRC_V_LOG},
-    {"s-log1",      MP_CSP_TRC_S_LOG1},
-    {"s-log2",      MP_CSP_TRC_S_LOG2},
-    {"st428",       MP_CSP_TRC_ST428},
+const struct m_opt_choice_alternatives pl_csp_trc_names[] = {
+    {"auto",        PL_COLOR_TRC_UNKNOWN},
+    {"bt.1886",     PL_COLOR_TRC_BT_1886},
+    {"srgb",        PL_COLOR_TRC_SRGB},
+    {"linear",      PL_COLOR_TRC_LINEAR},
+    {"gamma1.8",    PL_COLOR_TRC_GAMMA18},
+    {"gamma2.0",    PL_COLOR_TRC_GAMMA20},
+    {"gamma2.2",    PL_COLOR_TRC_GAMMA22},
+    {"gamma2.4",    PL_COLOR_TRC_GAMMA24},
+    {"gamma2.6",    PL_COLOR_TRC_GAMMA26},
+    {"gamma2.8",    PL_COLOR_TRC_GAMMA28},
+    {"prophoto",    PL_COLOR_TRC_PRO_PHOTO},
+    {"pq",          PL_COLOR_TRC_PQ},
+    {"hlg",         PL_COLOR_TRC_HLG},
+    {"v-log",       PL_COLOR_TRC_V_LOG},
+    {"s-log1",      PL_COLOR_TRC_S_LOG1},
+    {"s-log2",      PL_COLOR_TRC_S_LOG2},
+    {"st428",       PL_COLOR_TRC_ST428},
     {0}
 };
 
@@ -119,21 +119,6 @@ const struct m_opt_choice_alternatives mp_alpha_names[] = {
     {0}
 };
 
-void mp_colorspace_merge(struct mp_colorspace *orig, struct mp_colorspace *new)
-{
-    if (!orig->space)
-        orig->space = new->space;
-    if (!orig->levels)
-        orig->levels = new->levels;
-    if (!orig->primaries)
-        orig->primaries = new->primaries;
-    if (!orig->gamma)
-        orig->gamma = new->gamma;
-    if (!orig->light)
-        orig->light = new->light;
-    pl_hdr_metadata_merge(&orig->hdr, &new->hdr);
-}
-
 // The short name _must_ match with what vf_stereo3d accepts (if supported).
 // The long name in comments is closer to the Matroska spec (StereoMode element).
 // The numeric index matches the Matroska StereoMode value. If you add entries
@@ -158,139 +143,27 @@ const struct m_opt_choice_alternatives mp_stereo3d_names[] = {
     {0}
 };
 
-enum mp_csp avcol_spc_to_mp_csp(int avcolorspace)
-{
-    switch (avcolorspace) {
-    case AVCOL_SPC_BT709:       return MP_CSP_BT_709;
-    case AVCOL_SPC_BT470BG:     return MP_CSP_BT_601;
-    case AVCOL_SPC_BT2020_NCL:  return MP_CSP_BT_2020_NC;
-    case AVCOL_SPC_BT2020_CL:   return MP_CSP_BT_2020_C;
-    case AVCOL_SPC_SMPTE170M:   return MP_CSP_BT_601;
-    case AVCOL_SPC_SMPTE240M:   return MP_CSP_SMPTE_240M;
-    case AVCOL_SPC_RGB:         return MP_CSP_RGB;
-    case AVCOL_SPC_YCOCG:       return MP_CSP_YCGCO;
-    default:                    return MP_CSP_AUTO;
-    }
-}
-
-enum mp_csp_levels avcol_range_to_mp_csp_levels(int avrange)
-{
-    switch (avrange) {
-    case AVCOL_RANGE_MPEG:      return MP_CSP_LEVELS_TV;
-    case AVCOL_RANGE_JPEG:      return MP_CSP_LEVELS_PC;
-    default:                    return MP_CSP_LEVELS_AUTO;
-    }
-}
-
-enum mp_csp_prim avcol_pri_to_mp_csp_prim(int avpri)
-{
-    switch (avpri) {
-    case AVCOL_PRI_SMPTE240M:   // Same as below
-    case AVCOL_PRI_SMPTE170M:   return MP_CSP_PRIM_BT_601_525;
-    case AVCOL_PRI_BT470BG:     return MP_CSP_PRIM_BT_601_625;
-    case AVCOL_PRI_BT709:       return MP_CSP_PRIM_BT_709;
-    case AVCOL_PRI_BT2020:      return MP_CSP_PRIM_BT_2020;
-    case AVCOL_PRI_BT470M:      return MP_CSP_PRIM_BT_470M;
-    case AVCOL_PRI_SMPTE431:    return MP_CSP_PRIM_DCI_P3;
-    case AVCOL_PRI_SMPTE432:    return MP_CSP_PRIM_DISPLAY_P3;
-    default:                    return MP_CSP_PRIM_AUTO;
-    }
-}
-
-enum mp_csp_trc avcol_trc_to_mp_csp_trc(int avtrc)
-{
-    switch (avtrc) {
-    case AVCOL_TRC_BT709:
-    case AVCOL_TRC_SMPTE170M:
-    case AVCOL_TRC_SMPTE240M:
-    case AVCOL_TRC_BT1361_ECG:
-    case AVCOL_TRC_BT2020_10:
-    case AVCOL_TRC_BT2020_12:    return MP_CSP_TRC_BT_1886;
-    case AVCOL_TRC_IEC61966_2_1: return MP_CSP_TRC_SRGB;
-    case AVCOL_TRC_LINEAR:       return MP_CSP_TRC_LINEAR;
-    case AVCOL_TRC_GAMMA22:      return MP_CSP_TRC_GAMMA22;
-    case AVCOL_TRC_GAMMA28:      return MP_CSP_TRC_GAMMA28;
-    case AVCOL_TRC_SMPTEST2084:  return MP_CSP_TRC_PQ;
-    case AVCOL_TRC_ARIB_STD_B67: return MP_CSP_TRC_HLG;
-    case AVCOL_TRC_SMPTE428:     return MP_CSP_TRC_ST428;
-    default:                     return MP_CSP_TRC_AUTO;
-    }
-}
-
-int mp_csp_to_avcol_spc(enum mp_csp colorspace)
-{
-    switch (colorspace) {
-    case MP_CSP_BT_709:         return AVCOL_SPC_BT709;
-    case MP_CSP_BT_601:         return AVCOL_SPC_BT470BG;
-    case MP_CSP_BT_2020_NC:     return AVCOL_SPC_BT2020_NCL;
-    case MP_CSP_BT_2020_C:      return AVCOL_SPC_BT2020_CL;
-    case MP_CSP_SMPTE_240M:     return AVCOL_SPC_SMPTE240M;
-    case MP_CSP_RGB:            return AVCOL_SPC_RGB;
-    case MP_CSP_YCGCO:          return AVCOL_SPC_YCOCG;
-    default:                    return AVCOL_SPC_UNSPECIFIED;
-    }
-}
-
-int mp_csp_levels_to_avcol_range(enum mp_csp_levels range)
-{
-    switch (range) {
-    case MP_CSP_LEVELS_TV:      return AVCOL_RANGE_MPEG;
-    case MP_CSP_LEVELS_PC:      return AVCOL_RANGE_JPEG;
-    default:                    return AVCOL_RANGE_UNSPECIFIED;
-    }
-}
-
-int mp_csp_prim_to_avcol_pri(enum mp_csp_prim prim)
-{
-    switch (prim) {
-    case MP_CSP_PRIM_BT_601_525: return AVCOL_PRI_SMPTE170M;
-    case MP_CSP_PRIM_BT_601_625: return AVCOL_PRI_BT470BG;
-    case MP_CSP_PRIM_BT_709:     return AVCOL_PRI_BT709;
-    case MP_CSP_PRIM_BT_2020:    return AVCOL_PRI_BT2020;
-    case MP_CSP_PRIM_BT_470M:    return AVCOL_PRI_BT470M;
-    case MP_CSP_PRIM_DCI_P3:     return AVCOL_PRI_SMPTE431;
-    case MP_CSP_PRIM_DISPLAY_P3: return AVCOL_PRI_SMPTE432;
-    default:                     return AVCOL_PRI_UNSPECIFIED;
-    }
-}
-
-int mp_csp_trc_to_avcol_trc(enum mp_csp_trc trc)
-{
-    switch (trc) {
-    // We just call it BT.1886 since we're decoding, but it's still BT.709
-    case MP_CSP_TRC_BT_1886:      return AVCOL_TRC_BT709;
-    case MP_CSP_TRC_SRGB:         return AVCOL_TRC_IEC61966_2_1;
-    case MP_CSP_TRC_LINEAR:       return AVCOL_TRC_LINEAR;
-    case MP_CSP_TRC_GAMMA22:      return AVCOL_TRC_GAMMA22;
-    case MP_CSP_TRC_GAMMA28:      return AVCOL_TRC_GAMMA28;
-    case MP_CSP_TRC_PQ:           return AVCOL_TRC_SMPTEST2084;
-    case MP_CSP_TRC_HLG:          return AVCOL_TRC_ARIB_STD_B67;
-    case MP_CSP_TRC_ST428:        return AVCOL_TRC_SMPTE428;
-    default:                      return AVCOL_TRC_UNSPECIFIED;
-    }
-}
-
-enum mp_csp mp_csp_guess_colorspace(int width, int height)
+enum pl_color_system mp_csp_guess_colorspace(int width, int height)
 {
-    return width >= 1280 || height > 576 ? MP_CSP_BT_709 : MP_CSP_BT_601;
+    return width >= 1280 || height > 576 ? PL_COLOR_SYSTEM_BT_709 : PL_COLOR_SYSTEM_BT_601;
 }
 
-enum mp_csp_prim mp_csp_guess_primaries(int width, int height)
+enum pl_color_primaries mp_csp_guess_primaries(int width, int height)
 {
     // HD content
     if (width >= 1280 || height > 576)
-        return MP_CSP_PRIM_BT_709;
+        return PL_COLOR_PRIM_BT_709;
 
     switch (height) {
     case 576: // Typical PAL content, including anamorphic/squared
-        return MP_CSP_PRIM_BT_601_625;
+        return PL_COLOR_PRIM_BT_601_625;
 
     case 480: // Typical NTSC content, including squared
     case 486: // NTSC Pro or anamorphic NTSC
-        return MP_CSP_PRIM_BT_601_525;
+        return PL_COLOR_PRIM_BT_601_525;
 
     default: // No good metric, just pick BT.709 to minimize damage
-        return MP_CSP_PRIM_BT_709;
+        return PL_COLOR_PRIM_BT_709;
     }
 }
 
@@ -369,7 +242,7 @@ static void mp_mul_matrix3x3(float a[3][3], float b[3][3])
 }
 
 // return the primaries associated with a certain mp_csp_primaries val
-struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
+struct mp_csp_primaries mp_get_csp_primaries(enum pl_color_primaries spc)
 {
     /*
     Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0
@@ -391,21 +264,21 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
         e   = {1.0/3.0, 1.0/3.0};
 
     switch (spc) {
-    case MP_CSP_PRIM_BT_470M:
+    case PL_COLOR_PRIM_BT_470M:
         return (struct mp_csp_primaries) {
             .red   = {0.670, 0.330},
             .green = {0.210, 0.710},
             .blue  = {0.140, 0.080},
             .white = c
         };
-    case MP_CSP_PRIM_BT_601_525:
+    case PL_COLOR_PRIM_BT_601_525:
         return (struct mp_csp_primaries) {
             .red   = {0.630, 0.340},
             .green = {0.310, 0.595},
             .blue  = {0.155, 0.070},
             .white = d65
         };
-    case MP_CSP_PRIM_BT_601_625:
+    case PL_COLOR_PRIM_BT_601_625:
         return (struct mp_csp_primaries) {
             .red   = {0.640, 0.330},
             .green = {0.290, 0.600},
@@ -414,43 +287,43 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
         };
     // This is the default assumption if no colorspace information could
     // be determined, eg. for files which have no video channel.
-    case MP_CSP_PRIM_AUTO:
-    case MP_CSP_PRIM_BT_709:
+    case PL_COLOR_PRIM_UNKNOWN:
+    case PL_COLOR_PRIM_BT_709:
         return (struct mp_csp_primaries) {
             .red   = {0.640, 0.330},
             .green = {0.300, 0.600},
             .blue  = {0.150, 0.060},
             .white = d65
         };
-    case MP_CSP_PRIM_BT_2020:
+    case PL_COLOR_PRIM_BT_2020:
         return (struct mp_csp_primaries) {
             .red   = {0.708, 0.292},
             .green = {0.170, 0.797},
             .blue  = {0.131, 0.046},
             .white = d65
         };
-    case MP_CSP_PRIM_APPLE:
+    case PL_COLOR_PRIM_APPLE:
         return (struct mp_csp_primaries) {
             .red   = {0.625, 0.340},
             .green = {0.280, 0.595},
             .blue  = {0.115, 0.070},
             .white = d65
         };
-    case MP_CSP_PRIM_ADOBE:
+    case PL_COLOR_PRIM_ADOBE:
         return (struct mp_csp_primaries) {
             .red   = {0.640, 0.330},
             .green = {0.210, 0.710},
             .blue  = {0.150, 0.060},
             .white = d65
         };
-    case MP_CSP_PRIM_PRO_PHOTO:
+    case PL_COLOR_PRIM_PRO_PHOTO:
         return (struct mp_csp_primaries) {
             .red   = {0.7347, 0.2653},
             .green = {0.1596, 0.8404},
             .blue  = {0.0366, 0.0001},
             .white = d50
         };
-    case MP_CSP_PRIM_CIE_1931:
+    case PL_COLOR_PRIM_CIE_1931:
         return (struct mp_csp_primaries) {
             .red   = {0.7347, 0.2653},
             .green = {0.2738, 0.7174},
@@ -458,16 +331,16 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = e
         };
     // From SMPTE RP 431-2 and 432-1
-    case MP_CSP_PRIM_DCI_P3:
-    case MP_CSP_PRIM_DISPLAY_P3:
+    case PL_COLOR_PRIM_DCI_P3:
+    case PL_COLOR_PRIM_DISPLAY_P3:
         return (struct mp_csp_primaries) {
             .red   = {0.680, 0.320},
             .green = {0.265, 0.690},
             .blue  = {0.150, 0.060},
-            .white = spc == MP_CSP_PRIM_DCI_P3 ? dci : d65
+            .white = spc == PL_COLOR_PRIM_DCI_P3 ? dci : d65
         };
     // From Panasonic VARICAM reference manual
-    case MP_CSP_PRIM_V_GAMUT:
+    case PL_COLOR_PRIM_V_GAMUT:
         return (struct mp_csp_primaries) {
             .red   = {0.730, 0.280},
             .green = {0.165, 0.840},
@@ -475,7 +348,7 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = d65
         };
     // From Sony S-Log reference manual
-    case MP_CSP_PRIM_S_GAMUT:
+    case PL_COLOR_PRIM_S_GAMUT:
         return (struct mp_csp_primaries) {
             .red   = {0.730, 0.280},
             .green = {0.140, 0.855},
@@ -483,7 +356,7 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = d65
         };
     // from EBU Tech. 3213-E
-    case MP_CSP_PRIM_EBU_3213:
+    case PL_COLOR_PRIM_EBU_3213:
         return (struct mp_csp_primaries) {
             .red   = {0.630, 0.340},
             .green = {0.295, 0.605},
@@ -491,7 +364,7 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = d65
         };
     // From H.273, traditional film with Illuminant C
-    case MP_CSP_PRIM_FILM_C:
+    case PL_COLOR_PRIM_FILM_C:
         return (struct mp_csp_primaries) {
             .red   = {0.681, 0.319},
             .green = {0.243, 0.692},
@@ -499,7 +372,7 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = c
         };
     // From libplacebo source code
-    case MP_CSP_PRIM_ACES_AP0:
+    case PL_COLOR_PRIM_ACES_AP0:
         return (struct mp_csp_primaries) {
             .red   = {0.7347, 0.2653},
             .green = {0.0000, 1.0000},
@@ -507,7 +380,7 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
             .white = {0.32168, 0.33767},
         };
     // From libplacebo source code
-    case MP_CSP_PRIM_ACES_AP1:
+    case PL_COLOR_PRIM_ACES_AP1:
         return (struct mp_csp_primaries) {
             .red   = {0.713, 0.293},
             .green = {0.165, 0.830},
@@ -522,20 +395,20 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc)
 // Get the nominal peak for a given colorspace, relative to the reference white
 // level. In other words, this returns the brightest encodable value that can
 // be represented by a given transfer curve.
-float mp_trc_nom_peak(enum mp_csp_trc trc)
+float mp_trc_nom_peak(enum pl_color_transfer trc)
 {
     switch (trc) {
-    case MP_CSP_TRC_PQ:           return 10000.0 / MP_REF_WHITE;
-    case MP_CSP_TRC_HLG:          return 12.0 / MP_REF_WHITE_HLG;
-    case MP_CSP_TRC_V_LOG:        return 46.0855;
-    case MP_CSP_TRC_S_LOG1:       return 6.52;
-    case MP_CSP_TRC_S_LOG2:       return 9.212;
+    case PL_COLOR_TRC_PQ:           return 10000.0 / MP_REF_WHITE;
+    case PL_COLOR_TRC_HLG:          return 12.0 / MP_REF_WHITE_HLG;
+    case PL_COLOR_TRC_V_LOG:        return 46.0855;
+    case PL_COLOR_TRC_S_LOG1:       return 6.52;
+    case PL_COLOR_TRC_S_LOG2:       return 9.212;
     }
 
     return 1.0;
 }
 
-bool mp_trc_is_hdr(enum mp_csp_trc trc)
+bool mp_trc_is_hdr(enum pl_color_transfer trc)
 {
     return mp_trc_nom_peak(trc) > 1.0;
 }
@@ -660,7 +533,7 @@ static void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params,
                                   enum mp_render_intent intent, struct mp_cmat *m)
 {
     // Convert to DCI-P3
-    struct mp_csp_primaries prim = mp_get_csp_primaries(MP_CSP_PRIM_DCI_P3);
+    struct mp_csp_primaries prim = mp_get_csp_primaries(PL_COLOR_PRIM_DCI_P3);
     float brightness = params->brightness;
     mp_get_rgb2xyz_matrix(prim, m->m);
     mp_invert_matrix3x3(m->m);
@@ -685,7 +558,7 @@ static void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params,
 // Get multiplication factor required if image data is fit within the LSBs of a
 // higher smaller bit depth fixed-point texture data.
 // This is broken. Use mp_get_csp_uint_mul().
-double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits)
+double mp_get_csp_mul(enum pl_color_system csp, int input_bits, int texture_bits)
 {
     assert(texture_bits >= input_bits);
 
@@ -694,10 +567,10 @@ double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits)
         return 1;
 
     // RGB always uses the full range available.
-    if (csp == MP_CSP_RGB)
+    if (csp == PL_COLOR_SYSTEM_RGB)
         return ((1LL << input_bits) - 1.) / ((1LL << texture_bits) - 1.);
 
-    if (csp == MP_CSP_XYZ)
+    if (csp == PL_COLOR_SYSTEM_XYZ)
         return 1;
 
     // High bit depth YUV uses a range shifted from 8 bit.
@@ -716,24 +589,24 @@ double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits)
 //  bits: number of significant bits, e.g. 10 for yuv420p10, 16 for p010
 //  out_m: returns factor to multiply the uint number with
 //  out_o: returns offset to add after multiplication
-void mp_get_csp_uint_mul(enum mp_csp csp, enum mp_csp_levels levels,
+void mp_get_csp_uint_mul(enum pl_color_system csp, enum pl_color_levels levels,
                          int bits, int component, double *out_m, double *out_o)
 {
     uint16_t i_min = 0;
     uint16_t i_max = (1u << bits) - 1;
     double f_min = 0; // min. float value
 
-    if (csp != MP_CSP_RGB && component != 4) {
+    if (csp != PL_COLOR_SYSTEM_RGB && component != 4) {
         if (component == 2 || component == 3) {
             f_min = (1u << (bits - 1)) / -(double)i_max; // force center => 0
 
-            if (levels != MP_CSP_LEVELS_PC && bits >= 8) {
+            if (levels != PL_COLOR_LEVELS_FULL && bits >= 8) {
                 i_min = 16  << (bits - 8); // => -0.5
                 i_max = 240 << (bits - 8); // =>  0.5
                 f_min = -0.5;
             }
         } else {
-            if (levels != MP_CSP_LEVELS_PC && bits >= 8) {
+            if (levels != PL_COLOR_LEVELS_FULL && bits >= 8) {
                 i_min = 16  << (bits - 8); // => 0
                 i_max = 235 << (bits - 8); // => 1
             }
@@ -778,19 +651,19 @@ static void luma_coeffs(struct mp_cmat *mat, float lr, float lg, float lb)
 // get the coefficients of the yuv -> rgb conversion matrix
 void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
 {
-    enum mp_csp colorspace = params->color.space;
-    if (colorspace <= MP_CSP_AUTO || colorspace >= MP_CSP_COUNT)
-        colorspace = MP_CSP_BT_601;
-    enum mp_csp_levels levels_in = params->color.levels;
-    if (levels_in <= MP_CSP_LEVELS_AUTO || levels_in >= MP_CSP_LEVELS_COUNT)
-        levels_in = MP_CSP_LEVELS_TV;
+    enum pl_color_system colorspace = params->repr.sys;
+    if (colorspace <= PL_COLOR_SYSTEM_UNKNOWN || colorspace >= PL_COLOR_SYSTEM_COUNT)
+        colorspace = PL_COLOR_SYSTEM_BT_601;
+    enum pl_color_levels levels_in = params->repr.levels;
+    if (levels_in <= PL_COLOR_LEVELS_UNKNOWN || levels_in >= PL_COLOR_LEVELS_COUNT)
+        levels_in = PL_COLOR_LEVELS_LIMITED;
 
     switch (colorspace) {
-    case MP_CSP_BT_601:     luma_coeffs(m, 0.299,  0.587,  0.114 ); break;
-    case MP_CSP_BT_709:     luma_coeffs(m, 0.2126, 0.7152, 0.0722); break;
-    case MP_CSP_SMPTE_240M: luma_coeffs(m, 0.2122, 0.7013, 0.0865); break;
-    case MP_CSP_BT_2020_NC: luma_coeffs(m, 0.2627, 0.6780, 0.0593); break;
-    case MP_CSP_BT_2020_C: {
+    case PL_COLOR_SYSTEM_BT_601:     luma_coeffs(m, 0.299,  0.587,  0.114 ); break;
+    case PL_COLOR_SYSTEM_BT_709:     luma_coeffs(m, 0.2126, 0.7152, 0.0722); break;
+    case PL_COLOR_SYSTEM_SMPTE_240M: luma_coeffs(m, 0.2122, 0.7013, 0.0865); break;
+    case PL_COLOR_SYSTEM_BT_2020_NC: luma_coeffs(m, 0.2627, 0.6780, 0.0593); break;
+    case PL_COLOR_SYSTEM_BT_2020_C: {
         // Note: This outputs into the [-0.5,0.5] range for chroma information.
         // If this clips on any VO, a constant 0.5 coefficient can be added
         // to the chroma channels to normalize them into [0,1]. This is not
@@ -798,12 +671,12 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
         *m = (struct mp_cmat){{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}};
         break;
     }
-    case MP_CSP_RGB: {
+    case PL_COLOR_SYSTEM_RGB: {
         *m = (struct mp_cmat){{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
         levels_in = -1;
         break;
     }
-    case MP_CSP_XYZ: {
+    case PL_COLOR_SYSTEM_XYZ: {
         // The vo should probably not be using a matrix generated by this
         // function for XYZ sources, but if it does, let's just convert it to
         // an equivalent RGB space based on the colorimetry metadata it
@@ -813,7 +686,7 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
         levels_in = -1;
         break;
     }
-    case MP_CSP_YCGCO: {
+    case PL_COLOR_SYSTEM_YCGCO: {
         *m = (struct mp_cmat) {
             {{1,  -1,  1},
              {1,   1,  0},
@@ -828,8 +701,8 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
     if (params->is_float)
         levels_in = -1;
 
-    if ((colorspace == MP_CSP_BT_601 || colorspace == MP_CSP_BT_709 ||
-         colorspace == MP_CSP_SMPTE_240M || colorspace == MP_CSP_BT_2020_NC))
+    if ((colorspace == PL_COLOR_SYSTEM_BT_601 || colorspace == PL_COLOR_SYSTEM_BT_709 ||
+         colorspace == PL_COLOR_SYSTEM_SMPTE_240M || colorspace == PL_COLOR_SYSTEM_BT_2020_NC))
     {
         // Hue is equivalent to rotating input [U, V] subvector around the origin.
         // Saturation scales [U, V].
@@ -855,23 +728,23 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
         anyfull = {  0*s, 255*s, 255*s/2, 0 }, // cmax picked to make cmul=ymul
         yuvlev;
     switch (levels_in) {
-    case MP_CSP_LEVELS_TV: yuvlev = yuvlim; break;
-    case MP_CSP_LEVELS_PC: yuvlev = yuvfull; break;
+    case PL_COLOR_LEVELS_LIMITED: yuvlev = yuvlim; break;
+    case PL_COLOR_LEVELS_FULL: yuvlev = yuvfull; break;
     case -1: yuvlev = anyfull; break;
     default:
         MP_ASSERT_UNREACHABLE();
     }
 
     int levels_out = params->levels_out;
-    if (levels_out <= MP_CSP_LEVELS_AUTO || levels_out >= MP_CSP_LEVELS_COUNT)
-        levels_out = MP_CSP_LEVELS_PC;
+    if (levels_out <= PL_COLOR_LEVELS_UNKNOWN || levels_out >= PL_COLOR_LEVELS_COUNT)
+        levels_out = PL_COLOR_LEVELS_FULL;
     struct rgblevels { double min, max; }
         rgblim =  { 16/255., 235/255. },
         rgbfull = {      0,        1  },
         rgblev;
     switch (levels_out) {
-    case MP_CSP_LEVELS_TV: rgblev = rgblim; break;
-    case MP_CSP_LEVELS_PC: rgblev = rgbfull; break;
+    case PL_COLOR_LEVELS_LIMITED: rgblev = rgblim; break;
+    case PL_COLOR_LEVELS_FULL: rgblev = rgbfull; break;
     default:
         MP_ASSERT_UNREACHABLE();
     }
@@ -904,16 +777,6 @@ void mp_csp_set_image_params(struct mp_csp_params *params,
     params->color = p.color;
 }
 
-bool mp_colorspace_equal(struct mp_colorspace c1, struct mp_colorspace c2)
-{
-    return c1.space == c2.space &&
-           c1.levels == c2.levels &&
-           c1.primaries == c2.primaries &&
-           c1.gamma == c2.gamma &&
-           c1.light == c2.light &&
-           pl_hdr_metadata_equal(&c1.hdr, &c2.hdr);
-}
-
 enum mp_csp_equalizer_param {
     MP_CSP_EQ_BRIGHTNESS,
     MP_CSP_EQ_CONTRAST,
@@ -946,7 +809,7 @@ const struct m_sub_options mp_csp_equalizer_conf = {
         {"gamma", OPT_FLOAT(values[MP_CSP_EQ_GAMMA]),
             M_RANGE(-100, 100)},
         {"video-output-levels",
-            OPT_CHOICE_C(output_levels, mp_csp_levels_names)},
+            OPT_CHOICE_C(output_levels, pl_csp_levels_names)},
         {0}
     },
     .size = sizeof(struct mp_csp_equalizer_opts),
diff --git a/video/csputils.h b/video/csputils.h
index 3a904cbf3861..80901659cf3c 100644
--- a/video/csputils.h
+++ b/video/csputils.h
@@ -30,76 +30,10 @@
  * nonzero at vf/vo level.
  */
 
-enum mp_csp {
-    MP_CSP_AUTO,
-    MP_CSP_BT_601,
-    MP_CSP_BT_709,
-    MP_CSP_SMPTE_240M,
-    MP_CSP_BT_2020_NC,
-    MP_CSP_BT_2020_C,
-    MP_CSP_RGB,
-    MP_CSP_XYZ,
-    MP_CSP_YCGCO,
-    MP_CSP_COUNT
-};
-
-extern const struct m_opt_choice_alternatives mp_csp_names[];
-
-enum mp_csp_levels {
-    MP_CSP_LEVELS_AUTO,
-    MP_CSP_LEVELS_TV,
-    MP_CSP_LEVELS_PC,
-    MP_CSP_LEVELS_COUNT,
-};
-
-extern const struct m_opt_choice_alternatives mp_csp_levels_names[];
-
-enum mp_csp_prim {
-    MP_CSP_PRIM_AUTO,
-    MP_CSP_PRIM_BT_601_525,
-    MP_CSP_PRIM_BT_601_625,
-    MP_CSP_PRIM_BT_709,
-    MP_CSP_PRIM_BT_2020,
-    MP_CSP_PRIM_BT_470M,
-    MP_CSP_PRIM_APPLE,
-    MP_CSP_PRIM_ADOBE,
-    MP_CSP_PRIM_PRO_PHOTO,
-    MP_CSP_PRIM_CIE_1931,
-    MP_CSP_PRIM_DCI_P3,
-    MP_CSP_PRIM_DISPLAY_P3,
-    MP_CSP_PRIM_V_GAMUT,
-    MP_CSP_PRIM_S_GAMUT,
-    MP_CSP_PRIM_EBU_3213,
-    MP_CSP_PRIM_FILM_C,
-    MP_CSP_PRIM_ACES_AP0,
-    MP_CSP_PRIM_ACES_AP1,
-    MP_CSP_PRIM_COUNT
-};
-
-extern const struct m_opt_choice_alternatives mp_csp_prim_names[];
-
-enum mp_csp_trc {
-    MP_CSP_TRC_AUTO,
-    MP_CSP_TRC_BT_1886,
-    MP_CSP_TRC_SRGB,
-    MP_CSP_TRC_LINEAR,
-    MP_CSP_TRC_GAMMA18,
-    MP_CSP_TRC_GAMMA20,
-    MP_CSP_TRC_GAMMA22,
-    MP_CSP_TRC_GAMMA24,
-    MP_CSP_TRC_GAMMA26,
-    MP_CSP_TRC_GAMMA28,
-    MP_CSP_TRC_PRO_PHOTO,
-    MP_CSP_TRC_PQ,
-    MP_CSP_TRC_HLG,
-    MP_CSP_TRC_V_LOG,
-    MP_CSP_TRC_S_LOG1,
-    MP_CSP_TRC_S_LOG2,
-    MP_CSP_TRC_ST428,
-    MP_CSP_TRC_COUNT
-};
-
-extern const struct m_opt_choice_alternatives mp_csp_trc_names[];
+extern const struct m_opt_choice_alternatives pl_csp_names[];
+extern const struct m_opt_choice_alternatives pl_csp_levels_names[];
+extern const struct m_opt_choice_alternatives pl_csp_prim_names[];
+extern const struct m_opt_choice_alternatives pl_csp_trc_names[];
 
 enum mp_csp_light {
     MP_CSP_LIGHT_AUTO,
@@ -141,15 +75,6 @@ extern const struct m_opt_choice_alternatives mp_stereo3d_names[];
 #define MP_STEREO3D_NAME_DEF(x, def) \
     (MP_STEREO3D_NAME(x) ? MP_STEREO3D_NAME(x) : (def))
 
-struct mp_colorspace {
-    enum mp_csp space;
-    enum mp_csp_levels levels;
-    enum mp_csp_prim primaries;
-    enum mp_csp_trc gamma;
-    enum mp_csp_light light;
-    struct pl_hdr_metadata hdr;
-};
-
 // For many colorspace conversions, in particular those involving HDR, an
 // implicit reference white level is needed. Since this magic constant shows up
 // a lot, give it an explicit name. The value of 203 cd/m² comes from ITU-R
@@ -158,12 +83,10 @@ struct mp_colorspace {
 #define MP_REF_WHITE 203.0
 #define MP_REF_WHITE_HLG 3.17955
 
-// Replaces unknown values in the first struct by those of the second struct
-void mp_colorspace_merge(struct mp_colorspace *orig, struct mp_colorspace *new);
-
 struct mp_csp_params {
-    struct mp_colorspace color; // input colorspace
-    enum mp_csp_levels levels_out; // output device
+    struct pl_color_repr repr;
+    struct pl_color_space color;
+    enum pl_color_levels levels_out; // output device
     float brightness;
     float contrast;
     float hue;
@@ -179,9 +102,8 @@ struct mp_csp_params {
 };
 
 #define MP_CSP_PARAMS_DEFAULTS {                                \
-    .color = { .space = MP_CSP_BT_601,                          \
-               .levels = MP_CSP_LEVELS_TV },                    \
-    .levels_out = MP_CSP_LEVELS_PC,                             \
+    .repr = pl_color_repr_sdtv,                                 \
+    .levels_out = PL_COLOR_LEVELS_FULL,                         \
     .brightness = 0, .contrast = 1, .hue = 0, .saturation = 1,  \
     .gamma = 1, .texture_bits = 8, .input_bits = 8}
 
@@ -189,8 +111,6 @@ struct mp_image_params;
 void mp_csp_set_image_params(struct mp_csp_params *params,
                              const struct mp_image_params *imgparams);
 
-bool mp_colorspace_equal(struct mp_colorspace c1, struct mp_colorspace c2);
-
 enum mp_chroma_location {
     MP_CHROMA_AUTO,
     MP_CHROMA_TOPLEFT,  // uhd
@@ -234,26 +154,16 @@ struct mp_csp_primaries {
     struct mp_csp_col_xy red, green, blue, white;
 };
 
-enum mp_csp avcol_spc_to_mp_csp(int avcolorspace);
-enum mp_csp_levels avcol_range_to_mp_csp_levels(int avrange);
-enum mp_csp_prim avcol_pri_to_mp_csp_prim(int avpri);
-enum mp_csp_trc avcol_trc_to_mp_csp_trc(int avtrc);
-
-int mp_csp_to_avcol_spc(enum mp_csp colorspace);
-int mp_csp_levels_to_avcol_range(enum mp_csp_levels range);
-int mp_csp_prim_to_avcol_pri(enum mp_csp_prim prim);
-int mp_csp_trc_to_avcol_trc(enum mp_csp_trc trc);
-
-enum mp_csp mp_csp_guess_colorspace(int width, int height);
-enum mp_csp_prim mp_csp_guess_primaries(int width, int height);
+enum pl_color_system mp_csp_guess_colorspace(int width, int height);
+enum pl_color_primaries mp_csp_guess_primaries(int width, int height);
 
 enum mp_chroma_location avchroma_location_to_mp(int avloc);
 int mp_chroma_location_to_av(enum mp_chroma_location mploc);
 void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y);
 
-struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim csp);
-float mp_trc_nom_peak(enum mp_csp_trc trc);
-bool mp_trc_is_hdr(enum mp_csp_trc trc);
+struct mp_csp_primaries mp_get_csp_primaries(enum pl_color_primaries csp);
+float mp_trc_nom_peak(enum pl_color_transfer trc);
+bool mp_trc_is_hdr(enum pl_color_transfer trc);
 
 /* Color conversion matrix: RGB = m * YUV + c
  * m is in row-major matrix, with m[row][col], e.g.:
@@ -277,8 +187,8 @@ void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3]);
 void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest,
                        enum mp_render_intent intent, float cms_matrix[3][3]);
 
-double mp_get_csp_mul(enum mp_csp csp, int input_bits, int texture_bits);
-void mp_get_csp_uint_mul(enum mp_csp csp, enum mp_csp_levels levels,
+double mp_get_csp_mul(enum pl_color_system csp, int input_bits, int texture_bits);
+void mp_get_csp_uint_mul(enum pl_color_system csp, enum pl_color_levels levels,
                          int bits, int component, double *out_m, double *out_o);
 void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *out);
 
diff --git a/video/filter/vf_d3d11vpp.c b/video/filter/vf_d3d11vpp.c
index 3f00c5aa4dca..d63acd11ce0a 100644
--- a/video/filter/vf_d3d11vpp.c
+++ b/video/filter/vf_d3d11vpp.c
@@ -210,8 +210,8 @@ static int recreate_video_proc(struct mp_filter *vf)
                                                          FALSE, 0);
 
     D3D11_VIDEO_PROCESSOR_COLOR_SPACE csp = {
-        .YCbCr_Matrix = p->params.color.space != MP_CSP_BT_601,
-        .Nominal_Range = p->params.color.levels == MP_CSP_LEVELS_TV ? 1 : 2,
+        .YCbCr_Matrix = p->params.repr.sys != PL_COLOR_SYSTEM_BT_601,
+        .Nominal_Range = p->params.repr.levels == PL_COLOR_LEVELS_LIMITED ? 1 : 2,
     };
     ID3D11VideoContext_VideoProcessorSetStreamColorSpace(p->video_ctx,
                                                          p->video_proc,
diff --git a/video/filter/vf_fingerprint.c b/video/filter/vf_fingerprint.c
index 8714382ff360..87248193d621 100644
--- a/video/filter/vf_fingerprint.c
+++ b/video/filter/vf_fingerprint.c
@@ -104,7 +104,7 @@ static void f_process(struct mp_filter *f)
     // "portable" across source video.
     p->scaled->params.color = mpi->params.color;
     // Make output always full range; no reason to lose precision.
-    p->scaled->params.color.levels = MP_CSP_LEVELS_PC;
+    p->scaled->params.repr.levels = PL_COLOR_LEVELS_FULL;
 
     if (!mp_zimg_convert(p->zimg, p->scaled, mpi)) {
         if (!p->fallback_warning) {
diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c
index 4997d6f17371..4a14fc91024d 100644
--- a/video/filter/vf_format.c
+++ b/video/filter/vf_format.c
@@ -65,26 +65,26 @@ static void set_params(struct vf_format_opts *p, struct mp_image_params *out,
                        bool set_size)
 {
     if (p->colormatrix)
-        out->color.space = p->colormatrix;
+        out->repr.sys = p->colormatrix;
     if (p->colorlevels)
-        out->color.levels = p->colorlevels;
+        out->repr.levels = p->colorlevels;
     if (p->primaries)
         out->color.primaries = p->primaries;
     if (p->gamma) {
-        enum mp_csp_trc in_gamma = p->gamma;
-        out->color.gamma = p->gamma;
-        if (in_gamma != out->color.gamma) {
+        enum pl_color_transfer in_gamma = p->gamma;
+        out->color.transfer = p->gamma;
+        if (in_gamma != out->color.transfer) {
             // When changing the gamma function explicitly, also reset stuff
             // related to the gamma function since that information will almost
             // surely be false now and have to be re-inferred
             out->color.hdr = (struct pl_hdr_metadata){0};
-            out->color.light = MP_CSP_LIGHT_AUTO;
+            out->light = MP_CSP_LIGHT_AUTO;
         }
     }
     if (p->sig_peak)
         out->color.hdr = (struct pl_hdr_metadata){ .max_luma = p->sig_peak * MP_REF_WHITE };
     if (p->light)
-        out->color.light = p->light;
+        out->light = p->light;
     if (p->chroma_location)
         out->chroma_location = p->chroma_location;
     if (p->stereo_in)
@@ -122,10 +122,10 @@ static void vf_format_process(struct mp_filter *f)
             int outfmt = priv->opts->fmt;
 
             // If we convert from RGB to YUV, default to limited range.
-            if (mp_imgfmt_get_forced_csp(img->imgfmt) == MP_CSP_RGB &&
-                outfmt && mp_imgfmt_get_forced_csp(outfmt) == MP_CSP_AUTO)
+            if (mp_imgfmt_get_forced_csp(img->imgfmt) == PL_COLOR_SYSTEM_RGB &&
+                outfmt && mp_imgfmt_get_forced_csp(outfmt) == PL_COLOR_SYSTEM_UNKNOWN)
             {
-                par.color.levels = MP_CSP_LEVELS_TV;
+                par.repr.levels = PL_COLOR_LEVELS_LIMITED;
             }
 
             set_params(priv->opts, &par, true);
@@ -204,10 +204,10 @@ static struct mp_filter *vf_format_create(struct mp_filter *parent, void *option
 #define OPT_BASE_STRUCT struct vf_format_opts
 static const m_option_t vf_opts_fields[] = {
     {"fmt", OPT_IMAGEFORMAT(fmt)},
-    {"colormatrix", OPT_CHOICE_C(colormatrix, mp_csp_names)},
-    {"colorlevels", OPT_CHOICE_C(colorlevels, mp_csp_levels_names)},
-    {"primaries", OPT_CHOICE_C(primaries, mp_csp_prim_names)},
-    {"gamma", OPT_CHOICE_C(gamma, mp_csp_trc_names)},
+    {"colormatrix", OPT_CHOICE_C(colormatrix, pl_csp_names)},
+    {"colorlevels", OPT_CHOICE_C(colorlevels, pl_csp_levels_names)},
+    {"primaries", OPT_CHOICE_C(primaries, pl_csp_prim_names)},
+    {"gamma", OPT_CHOICE_C(gamma, pl_csp_trc_names)},
     {"sig-peak", OPT_FLOAT(sig_peak)},
     {"light", OPT_CHOICE_C(light, mp_csp_light_names)},
     {"chroma-location", OPT_CHOICE_C(chroma_location, mp_chroma_names)},
diff --git a/video/filter/vf_vapoursynth.c b/video/filter/vf_vapoursynth.c
index 583a196f3b42..5a3ce423d1d7 100644
--- a/video/filter/vf_vapoursynth.c
+++ b/video/filter/vf_vapoursynth.c
@@ -27,6 +27,7 @@
 
 #include <libavutil/rational.h>
 #include <libavutil/cpu.h>
+#include <libplacebo/utils/libav.h>
 
 #include "common/msg.h"
 #include "filters/f_autoconvert.h"
@@ -184,13 +185,13 @@ static void copy_mp_to_vs_frame_props_map(struct priv *p, VSMap *map,
     struct mp_image_params *params = &img->params;
     p->vsapi->propSetInt(map, "_SARNum", params->p_w, 0);
     p->vsapi->propSetInt(map, "_SARDen", params->p_h, 0);
-    if (params->color.levels) {
+    if (params->repr.levels) {
         p->vsapi->propSetInt(map, "_ColorRange",
-                params->color.levels == MP_CSP_LEVELS_TV, 0);
+                params->repr.levels == PL_COLOR_LEVELS_LIMITED, 0);
     }
     // The docs explicitly say it uses libavcodec values.
     p->vsapi->propSetInt(map, "_ColorSpace",
-            mp_csp_to_avcol_spc(params->color.space), 0);
+            pl_system_to_av(params->repr.sys), 0);
     if (params->chroma_location) {
         p->vsapi->propSetInt(map, "_ChromaLocation",
                 params->chroma_location == MP_CHROMA_CENTER, 0);
diff --git a/video/filter/vf_vavpp.c b/video/filter/vf_vavpp.c
index 52be14811174..f70fe1f4a6d3 100644
--- a/video/filter/vf_vavpp.c
+++ b/video/filter/vf_vavpp.c
@@ -208,7 +208,7 @@ static struct mp_image *render(struct mp_filter *vf)
 
     mp_image_copy_attributes(img, in);
 
-    unsigned int flags = va_get_colorspace_flag(p->params.color.space);
+    unsigned int flags = va_get_colorspace_flag(p->params.repr.sys);
     if (!mp_refqueue_should_deint(p->queue)) {
         flags |= VA_FRAME_PICTURE;
     } else if (mp_refqueue_is_top_field(p->queue)) {
diff --git a/video/image_writer.c b/video/image_writer.c
index 288d809be4c1..df43830d7900 100644
--- a/video/image_writer.c
+++ b/video/image_writer.c
@@ -25,6 +25,7 @@
 #include <libavutil/mem.h>
 #include <libavutil/opt.h>
 #include <libavutil/pixdesc.h>
+#include <libplacebo/utils/libav.h>
 
 #include "common/msg.h"
 #include "config.h"
@@ -137,16 +138,16 @@ static void prepare_avframe(AVFrame *pic, AVCodecContext *avctx,
     pic->width = avctx->width;
     pic->height = avctx->height;
     avctx->color_range = pic->color_range =
-        mp_csp_levels_to_avcol_range(image->params.color.levels);
+        pl_levels_to_av(image->params.repr.levels);
 
     if (!tag_csp)
         return;
     avctx->color_primaries = pic->color_primaries =
-        mp_csp_prim_to_avcol_pri(image->params.color.primaries);
+        pl_primaries_to_av(image->params.color.primaries);
     avctx->color_trc = pic->color_trc =
-        mp_csp_trc_to_avcol_trc(image->params.color.gamma);
+        pl_transfer_to_av(image->params.color.transfer);
     avctx->colorspace = pic->colorspace =
-        mp_csp_to_avcol_spc(image->params.color.space);
+        pl_system_to_av(image->params.repr.sys);
     avctx->chroma_sample_location = pic->chroma_location =
         mp_chroma_location_to_av(image->params.chroma_location);
     mp_dbg(log, "mapped color params:\n"
@@ -195,7 +196,7 @@ static bool write_lavc(struct image_writer_ctx *ctx, mp_image_t *image, const ch
     avctx->pix_fmt = imgfmt2pixfmt(image->imgfmt);
     if (codec->id == AV_CODEC_ID_MJPEG) {
         // Annoying deprecated garbage for the jpg encoder.
-        if (image->params.color.levels == MP_CSP_LEVELS_PC)
+        if (image->params.repr.levels == PL_COLOR_LEVELS_FULL)
             avctx->pix_fmt = replace_j_format(avctx->pix_fmt);
     }
     if (avctx->pix_fmt == AV_PIX_FMT_NONE) {
@@ -616,7 +617,7 @@ int image_writer_format_from_ext(const char *ext)
 }
 
 static struct mp_image *convert_image(struct mp_image *image, int destfmt,
-                                      enum mp_csp_levels yuv_levels,
+                                      enum pl_color_levels yuv_levels,
                                       const struct image_writer_opts *opts,
                                       struct mpv_global *global,
                                       struct mp_log *log)
@@ -636,13 +637,13 @@ static struct mp_image *convert_image(struct mp_image *image, int destfmt,
 
     if (!image_writer_flexible_csp(opts)) {
         // If our format can't tag csps, set something sane
-        p.color.primaries = MP_CSP_PRIM_BT_709;
-        p.color.gamma = MP_CSP_TRC_AUTO;
-        p.color.light = MP_CSP_LIGHT_DISPLAY;
+        p.color.primaries = PL_COLOR_PRIM_BT_709;
+        p.color.transfer = PL_COLOR_TRC_UNKNOWN;
+        p.light = MP_CSP_LIGHT_DISPLAY;
         p.color.hdr = (struct pl_hdr_metadata){0};
-        if (p.color.space != MP_CSP_RGB) {
-            p.color.levels = yuv_levels;
-            p.color.space = MP_CSP_BT_601;
+        if (p.repr.sys != PL_COLOR_SYSTEM_RGB) {
+            p.repr.levels = yuv_levels;
+            p.repr.sys = PL_COLOR_SYSTEM_BT_601;
             p.chroma_location = MP_CHROMA_CENTER;
         }
         mp_image_params_guess_csp(&p);
@@ -730,11 +731,11 @@ bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
     if (!destfmt)
         destfmt = get_target_format(&ctx);
 
-    enum mp_csp_levels levels; // Ignored if destfmt is a RGB format
+    enum pl_color_levels levels; // Ignored if destfmt is a RGB format
     if (opts->format == AV_CODEC_ID_WEBP) {
-        levels = MP_CSP_LEVELS_TV;
+        levels = PL_COLOR_LEVELS_LIMITED;
     } else {
-        levels = MP_CSP_LEVELS_PC;
+        levels = PL_COLOR_LEVELS_FULL;
     }
 
     struct mp_image *dst = convert_image(image, destfmt, levels, opts, global, log);
diff --git a/video/img_format.c b/video/img_format.c
index 6b7857f0b4c1..c9e2a3c0e647 100644
--- a/video/img_format.c
+++ b/video/img_format.c
@@ -664,18 +664,18 @@ static bool validate_regular_imgfmt(const struct mp_regular_imgfmt *fmt)
     return true;
 }
 
-static enum mp_csp get_forced_csp_from_flags(int flags)
+static enum pl_color_system get_forced_csp_from_flags(int flags)
 {
     if (flags & MP_IMGFLAG_COLOR_XYZ)
-        return MP_CSP_XYZ;
+        return PL_COLOR_SYSTEM_XYZ;
 
     if (flags & MP_IMGFLAG_COLOR_RGB)
-        return MP_CSP_RGB;
+        return PL_COLOR_SYSTEM_RGB;
 
-    return MP_CSP_AUTO;
+    return PL_COLOR_SYSTEM_UNKNOWN;
 }
 
-enum mp_csp mp_imgfmt_get_forced_csp(int imgfmt)
+enum pl_color_system mp_imgfmt_get_forced_csp(int imgfmt)
 {
     return get_forced_csp_from_flags(mp_imgfmt_get_desc(imgfmt).flags);
 }
diff --git a/video/img_format.h b/video/img_format.h
index 075382936690..e342de65dad6 100644
--- a/video/img_format.h
+++ b/video/img_format.h
@@ -155,9 +155,9 @@ int mp_imgfmt_desc_get_num_comps(struct mp_imgfmt_desc *desc);
 // luma pixel. luma_offsets[0] == mp_imgfmt_desc.comps[0].offset.
 bool mp_imgfmt_get_packed_yuv_locations(int imgfmt, uint8_t *luma_offsets);
 
-// MP_CSP_AUTO for YUV, MP_CSP_RGB or MP_CSP_XYZ otherwise.
+// PL_COLOR_SYSTEM_UNKNOWN for YUV, PL_COLOR_SYSTEM_RGB or PL_COLOR_SYSTEM_XYZ otherwise.
 // (Because IMGFMT/AV_PIX_FMT conflate format and csp for RGB and XYZ.)
-enum mp_csp mp_imgfmt_get_forced_csp(int imgfmt);
+enum pl_color_system mp_imgfmt_get_forced_csp(int imgfmt);
 
 enum mp_component_type {
     MP_COMPONENT_TYPE_UNKNOWN = 0,
@@ -184,7 +184,7 @@ struct mp_regular_imgfmt {
     // See mp_imgfmt_get_forced_csp(). Normally code should use
     // mp_image_params.colors. This field is only needed to map the format
     // unambiguously to FFmpeg formats.
-    enum mp_csp forced_csp;
+    enum pl_color_system forced_csp;
 
     // Size of each component in bytes.
     uint8_t component_size;
diff --git a/video/mp_image.c b/video/mp_image.c
index dff2051d392e..ed7543b8ff76 100644
--- a/video/mp_image.c
+++ b/video/mp_image.c
@@ -493,7 +493,7 @@ void mp_image_copy(struct mp_image *dst, struct mp_image *src)
         memcpy(dst->planes[1], src->planes[1], AVPALETTE_SIZE);
 }
 
-static enum mp_csp mp_image_params_get_forced_csp(struct mp_image_params *params)
+static enum pl_color_system mp_image_params_get_forced_csp(struct mp_image_params *params)
 {
     int imgfmt = params->hw_subfmt ? params->hw_subfmt : params->imgfmt;
     return mp_imgfmt_get_forced_csp(imgfmt);
@@ -522,15 +522,17 @@ void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src)
     dst->params.p_w = src->params.p_w;
     dst->params.p_h = src->params.p_h;
     dst->params.color = src->params.color;
+    dst->params.repr = src->params.repr;
+    dst->params.light = src->params.light;
     dst->params.chroma_location = src->params.chroma_location;
     dst->params.alpha = src->params.alpha;
     dst->params.crop = src->params.crop;
     dst->nominal_fps = src->nominal_fps;
 
     // ensure colorspace consistency
-    enum mp_csp dst_forced_csp = mp_image_params_get_forced_csp(&dst->params);
+    enum pl_color_system dst_forced_csp = mp_image_params_get_forced_csp(&dst->params);
     if (mp_image_params_get_forced_csp(&src->params) != dst_forced_csp) {
-        dst->params.color.space = dst_forced_csp != MP_CSP_AUTO ?
+        dst->params.repr.sys = dst_forced_csp != PL_COLOR_SYSTEM_UNKNOWN ?
                                     dst_forced_csp :
                                     mp_csp_guess_colorspace(src->w, src->h);
     }
@@ -670,8 +672,8 @@ void mp_image_clear(struct mp_image *img, int x0, int y0, int x1, int y1)
                 plane_size[cd->plane] = plane_bits / 8u;
                 int depth = cd->size + MPMIN(cd->pad, 0);
                 double m, o;
-                mp_get_csp_uint_mul(area.params.color.space,
-                                    area.params.color.levels,
+                mp_get_csp_uint_mul(area.params.repr.sys,
+                                    area.params.repr.levels,
                                     depth, c + 1, &m, &o);
                 uint64_t val = MPCLAMP(lrint((0 - o) / m), 0, 1ull << depth);
                 plane_clear_i[cd->plane] |= val << cd->offset;
@@ -773,11 +775,11 @@ char *mp_image_params_to_str_buf(char *b, size_t bs,
         if (p->hw_subfmt)
             mp_snprintf_cat(b, bs, "[%s]", mp_imgfmt_to_name(p->hw_subfmt));
         mp_snprintf_cat(b, bs, " %s/%s/%s/%s/%s",
-                        m_opt_choice_str(mp_csp_names, p->color.space),
-                        m_opt_choice_str(mp_csp_prim_names, p->color.primaries),
-                        m_opt_choice_str(mp_csp_trc_names, p->color.gamma),
-                        m_opt_choice_str(mp_csp_levels_names, p->color.levels),
-                        m_opt_choice_str(mp_csp_light_names, p->color.light));
+                        m_opt_choice_str(pl_csp_names, p->repr.sys),
+                        m_opt_choice_str(pl_csp_prim_names, p->color.primaries),
+                        m_opt_choice_str(pl_csp_trc_names, p->color.transfer),
+                        m_opt_choice_str(pl_csp_levels_names, p->repr.levels),
+                        m_opt_choice_str(mp_csp_light_names, p->light));
         mp_snprintf_cat(b, bs, " CL=%s",
                         m_opt_choice_str(mp_chroma_names, p->chroma_location));
         if (mp_image_crop_valid(p)) {
@@ -836,7 +838,9 @@ bool mp_image_params_equal(const struct mp_image_params *p1,
            p1->w == p2->w && p1->h == p2->h &&
            p1->p_w == p2->p_w && p1->p_h == p2->p_h &&
            p1->force_window == p2->force_window &&
-           mp_colorspace_equal(p1->color, p2->color) &&
+           pl_color_space_equal(&p1->color, &p2->color) &&
+           pl_color_repr_equal(&p1->repr, &p2->repr) &&
+           p1->light == p2->light &&
            p1->chroma_location == p2->chroma_location &&
            p1->rotate == p2->rotate &&
            p1->stereo3d == p2->stereo3d &&
@@ -854,11 +858,11 @@ void mp_image_set_attributes(struct mp_image *image,
     nparams.w = image->w;
     nparams.h = image->h;
     if (nparams.imgfmt != params->imgfmt)
-        nparams.color = (struct mp_colorspace){0};
+        nparams.color = (struct pl_color_space){0};
     mp_image_set_params(image, &nparams);
 }
 
-static enum mp_csp_levels infer_levels(enum mp_imgfmt imgfmt)
+static enum pl_color_levels infer_levels(enum mp_imgfmt imgfmt)
 {
     switch (imgfmt2pixfmt(imgfmt)) {
     case AV_PIX_FMT_YUVJ420P:
@@ -880,9 +884,9 @@ static enum mp_csp_levels infer_levels(enum mp_imgfmt imgfmt)
     case AV_PIX_FMT_GRAY16BE:
     case AV_PIX_FMT_YA16BE:
     case AV_PIX_FMT_YA16LE:
-        return MP_CSP_LEVELS_PC;
+        return PL_COLOR_LEVELS_FULL;
     default:
-        return MP_CSP_LEVELS_TV;
+        return PL_COLOR_LEVELS_LIMITED;
     }
 }
 
@@ -891,100 +895,100 @@ static enum mp_csp_levels infer_levels(enum mp_imgfmt imgfmt)
 // the colorspace as implied by the pixel format.
 void mp_image_params_guess_csp(struct mp_image_params *params)
 {
-    enum mp_csp forced_csp = mp_image_params_get_forced_csp(params);
-    if (forced_csp == MP_CSP_AUTO) { // YUV/other
-        if (params->color.space != MP_CSP_BT_601 &&
-            params->color.space != MP_CSP_BT_709 &&
-            params->color.space != MP_CSP_BT_2020_NC &&
-            params->color.space != MP_CSP_BT_2020_C &&
-            params->color.space != MP_CSP_SMPTE_240M &&
-            params->color.space != MP_CSP_YCGCO)
+    enum pl_color_system forced_csp = mp_image_params_get_forced_csp(params);
+    if (forced_csp == PL_COLOR_SYSTEM_UNKNOWN) { // YUV/other
+        if (params->repr.sys != PL_COLOR_SYSTEM_BT_601 &&
+            params->repr.sys != PL_COLOR_SYSTEM_BT_709 &&
+            params->repr.sys != PL_COLOR_SYSTEM_BT_2020_NC &&
+            params->repr.sys != PL_COLOR_SYSTEM_BT_2020_C &&
+            params->repr.sys != PL_COLOR_SYSTEM_SMPTE_240M &&
+            params->repr.sys != PL_COLOR_SYSTEM_YCGCO)
         {
             // Makes no sense, so guess instead
             // YCGCO should be separate, but libavcodec disagrees
-            params->color.space = MP_CSP_AUTO;
+            params->repr.sys = PL_COLOR_SYSTEM_UNKNOWN;
         }
-        if (params->color.space == MP_CSP_AUTO)
-            params->color.space = mp_csp_guess_colorspace(params->w, params->h);
-        if (params->color.levels == MP_CSP_LEVELS_AUTO) {
-            if (params->color.gamma == MP_CSP_TRC_V_LOG) {
-                params->color.levels = MP_CSP_LEVELS_PC;
+        if (params->repr.sys == PL_COLOR_SYSTEM_UNKNOWN)
+            params->repr.sys = mp_csp_guess_colorspace(params->w, params->h);
+        if (params->repr.levels == PL_COLOR_LEVELS_UNKNOWN) {
+            if (params->color.transfer == PL_COLOR_TRC_V_LOG) {
+                params->repr.levels = PL_COLOR_LEVELS_FULL;
             } else {
-                params->color.levels = infer_levels(params->imgfmt);
+                params->repr.levels = infer_levels(params->imgfmt);
             }
         }
-        if (params->color.primaries == MP_CSP_PRIM_AUTO) {
+        if (params->color.primaries == PL_COLOR_PRIM_UNKNOWN) {
             // Guess based on the colormatrix as a first priority
-            if (params->color.space == MP_CSP_BT_2020_NC ||
-                params->color.space == MP_CSP_BT_2020_C) {
-                params->color.primaries = MP_CSP_PRIM_BT_2020;
-            } else if (params->color.space == MP_CSP_BT_709) {
-                params->color.primaries = MP_CSP_PRIM_BT_709;
+            if (params->repr.sys == PL_COLOR_SYSTEM_BT_2020_NC ||
+                params->repr.sys == PL_COLOR_SYSTEM_BT_2020_C) {
+                params->color.primaries = PL_COLOR_PRIM_BT_2020;
+            } else if (params->repr.sys == PL_COLOR_SYSTEM_BT_709) {
+                params->color.primaries = PL_COLOR_PRIM_BT_709;
             } else {
                 // Ambiguous colormatrix for BT.601, guess based on res
                 params->color.primaries = mp_csp_guess_primaries(params->w, params->h);
             }
         }
-        if (params->color.gamma == MP_CSP_TRC_AUTO)
-            params->color.gamma = MP_CSP_TRC_BT_1886;
-    } else if (forced_csp == MP_CSP_RGB) {
-        params->color.space = MP_CSP_RGB;
-        params->color.levels = MP_CSP_LEVELS_PC;
+        if (params->color.transfer == PL_COLOR_TRC_UNKNOWN)
+            params->color.transfer = PL_COLOR_TRC_BT_1886;
+    } else if (forced_csp == PL_COLOR_SYSTEM_RGB) {
+        params->repr.sys = PL_COLOR_SYSTEM_RGB;
+        params->repr.levels = PL_COLOR_LEVELS_FULL;
 
         // The majority of RGB content is either sRGB or (rarely) some other
         // color space which we don't even handle, like AdobeRGB or
         // ProPhotoRGB. The only reasonable thing we can do is assume it's
         // sRGB and hope for the best, which should usually just work out fine.
         // Note: sRGB primaries = BT.709 primaries
-        if (params->color.primaries == MP_CSP_PRIM_AUTO)
-            params->color.primaries = MP_CSP_PRIM_BT_709;
-        if (params->color.gamma == MP_CSP_TRC_AUTO)
-            params->color.gamma = MP_CSP_TRC_SRGB;
-    } else if (forced_csp == MP_CSP_XYZ) {
-        params->color.space = MP_CSP_XYZ;
-        params->color.levels = MP_CSP_LEVELS_PC;
+        if (params->color.primaries == PL_COLOR_PRIM_UNKNOWN)
+            params->color.primaries = PL_COLOR_PRIM_BT_709;
+        if (params->color.transfer == PL_COLOR_TRC_UNKNOWN)
+            params->color.transfer = PL_COLOR_TRC_SRGB;
+    } else if (forced_csp == PL_COLOR_SYSTEM_XYZ) {
+        params->repr.sys = PL_COLOR_SYSTEM_XYZ;
+        params->repr.levels = PL_COLOR_LEVELS_FULL;
         // Force gamma to ST428 as this is the only correct for DCDM X'Y'Z'
-        params->color.gamma = MP_CSP_TRC_ST428;
+        params->color.transfer = PL_COLOR_TRC_ST428;
         // Don't care about primaries, they shouldn't be used, or if anything
         // MP_CSP_PRIM_ST428 should be defined.
     } else {
         // We have no clue.
-        params->color.space = MP_CSP_AUTO;
-        params->color.levels = MP_CSP_LEVELS_AUTO;
-        params->color.primaries = MP_CSP_PRIM_AUTO;
-        params->color.gamma = MP_CSP_TRC_AUTO;
+        params->repr.sys = PL_COLOR_SYSTEM_UNKNOWN;
+        params->repr.levels = PL_COLOR_LEVELS_UNKNOWN;
+        params->color.primaries = PL_COLOR_PRIM_UNKNOWN;
+        params->color.transfer = PL_COLOR_TRC_UNKNOWN;
     }
 
     if (!params->color.hdr.max_luma) {
-        if (params->color.gamma == MP_CSP_TRC_HLG) {
+        if (params->color.transfer == PL_COLOR_TRC_HLG) {
             params->color.hdr.max_luma = 1000; // reference display
         } else {
             // If the signal peak is unknown, we're forced to pick the TRC's
             // nominal range as the signal peak to prevent clipping
-            params->color.hdr.max_luma = mp_trc_nom_peak(params->color.gamma) * MP_REF_WHITE;
+            params->color.hdr.max_luma = mp_trc_nom_peak(params->color.transfer) * MP_REF_WHITE;
         }
     }
 
-    if (!mp_trc_is_hdr(params->color.gamma)) {
+    if (!mp_trc_is_hdr(params->color.transfer)) {
         // Some clips have leftover HDR metadata after conversion to SDR, so to
         // avoid blowing up the tone mapping code, strip/sanitize it
         params->color.hdr = pl_hdr_metadata_empty;
     }
 
     if (params->chroma_location == MP_CHROMA_AUTO) {
-        if (params->color.levels == MP_CSP_LEVELS_TV)
+        if (params->repr.levels == PL_COLOR_LEVELS_LIMITED)
             params->chroma_location = MP_CHROMA_LEFT;
-        if (params->color.levels == MP_CSP_LEVELS_PC)
+        if (params->repr.levels == PL_COLOR_LEVELS_FULL)
             params->chroma_location = MP_CHROMA_CENTER;
     }
 
-    if (params->color.light == MP_CSP_LIGHT_AUTO) {
+    if (params->light == MP_CSP_LIGHT_AUTO) {
         // HLG is always scene-referred (using its own OOTF), everything else
         // we assume is display-referred by default.
-        if (params->color.gamma == MP_CSP_TRC_HLG) {
-            params->color.light = MP_CSP_LIGHT_SCENE_HLG;
+        if (params->color.transfer == PL_COLOR_TRC_HLG) {
+            params->light = MP_CSP_LIGHT_SCENE_HLG;
         } else {
-            params->color.light = MP_CSP_LIGHT_DISPLAY;
+            params->light = MP_CSP_LIGHT_DISPLAY;
         }
     }
 }
@@ -1033,11 +1037,14 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src)
     if (src->repeat_pict == 1)
         dst->fields |= MP_IMGFIELD_REPEAT_FIRST;
 
-    dst->params.color = (struct mp_colorspace){
-        .space = avcol_spc_to_mp_csp(src->colorspace),
-        .levels = avcol_range_to_mp_csp_levels(src->color_range),
-        .primaries = avcol_pri_to_mp_csp_prim(src->color_primaries),
-        .gamma = avcol_trc_to_mp_csp_trc(src->color_trc),
+    dst->params.repr = (struct pl_color_repr){
+        .sys = pl_system_from_av(src->colorspace),
+        .levels = pl_levels_from_av(src->color_range),
+    };
+
+    dst->params.color = (struct pl_color_space){
+        .primaries = pl_primaries_from_av(src->color_primaries),
+        .transfer = pl_transfer_from_av(src->color_trc),
     };
 
     dst->params.chroma_location = avchroma_location_to_mp(src->chroma_location);
@@ -1046,7 +1053,7 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src)
         struct mp_image_params *p = (void *)src->opaque_ref->data;
         dst->params.stereo3d = p->stereo3d;
         // Might be incorrect if colorspace changes.
-        dst->params.color.light = p->color.light;
+        dst->params.light = p->light;
         dst->params.alpha = p->alpha;
     }
 
@@ -1163,11 +1170,11 @@ struct AVFrame *mp_image_to_av_frame(struct mp_image *src)
     if (src->fields & MP_IMGFIELD_REPEAT_FIRST)
         dst->repeat_pict = 1;
 
-    dst->colorspace = mp_csp_to_avcol_spc(src->params.color.space);
-    dst->color_range = mp_csp_levels_to_avcol_range(src->params.color.levels);
+    dst->colorspace = pl_system_to_av(src->params.repr.sys);
+    dst->color_range = pl_levels_to_av(src->params.repr.levels);
     dst->color_primaries =
-        mp_csp_prim_to_avcol_pri(src->params.color.primaries);
-    dst->color_trc = mp_csp_trc_to_avcol_trc(src->params.color.gamma);
+        pl_primaries_to_av(src->params.color.primaries);
+    dst->color_trc = pl_transfer_to_av(src->params.color.transfer);
 
     dst->chroma_location = mp_chroma_location_to_av(src->params.chroma_location);
 
@@ -1183,11 +1190,7 @@ struct AVFrame *mp_image_to_av_frame(struct mp_image *src)
         new_ref->icc_profile = NULL;
     }
 
-    pl_avframe_set_color(dst, (struct pl_color_space){
-        .primaries = mp_prim_to_pl(src->params.color.primaries),
-        .transfer = mp_trc_to_pl(src->params.color.gamma),
-        .hdr = src->params.color.hdr,
-    });
+    pl_avframe_set_color(dst, src->params.color);
 
     {
         AVFrameSideData *sd = av_frame_new_side_data(dst,
diff --git a/video/mp_image.h b/video/mp_image.h
index 0408aab3df99..c1ba89ee0ac7 100644
--- a/video/mp_image.h
+++ b/video/mp_image.h
@@ -47,7 +47,9 @@ struct mp_image_params {
     int w, h;                   // image dimensions
     int p_w, p_h;               // define pixel aspect ratio (undefined: 0/0)
     bool force_window;          // fake image created by handle_force_window
-    struct mp_colorspace color;
+    struct pl_color_space color;
+    struct pl_color_repr repr;
+    enum mp_csp_light light;
     enum mp_chroma_location chroma_location;
     // The image should be rotated clockwise (0-359 degrees).
     int rotate;
diff --git a/video/out/d3d11/context.c b/video/out/d3d11/context.c
index 05f04fdb6b08..9f866e2e8042 100644
--- a/video/out/d3d11/context.c
+++ b/video/out/d3d11/context.c
@@ -100,7 +100,7 @@ struct priv {
     struct ra_tex *backbuffer;
     ID3D11Device *device;
     IDXGISwapChain *swapchain;
-    struct mp_colorspace swapchain_csp;
+    struct pl_color_space swapchain_csp;
 
     int64_t perf_freq;
     unsigned sync_refresh_count;
diff --git a/video/out/gpu/context.h b/video/out/gpu/context.h
index 6788e6fd89bb..447f40b07501 100644
--- a/video/out/gpu/context.h
+++ b/video/out/gpu/context.h
@@ -72,7 +72,7 @@ struct ra_fbo {
 
     // Host system's colorspace that it will be interpreting
     // the frame buffer as.
-    struct mp_colorspace color_space;
+    struct pl_color_space color_space;
 };
 
 struct ra_swapchain_fns {
diff --git a/video/out/gpu/d3d11_helpers.c b/video/out/gpu/d3d11_helpers.c
index 30d9eae56f64..dce40b333485 100644
--- a/video/out/gpu/d3d11_helpers.c
+++ b/video/out/gpu/d3d11_helpers.c
@@ -228,9 +228,9 @@ static const char *d3d11_get_csp_name(DXGI_COLOR_SPACE_TYPE csp)
 }
 
 static bool d3d11_get_mp_csp(DXGI_COLOR_SPACE_TYPE csp,
-                             struct mp_colorspace *mp_csp)
+                             struct pl_color_space *pl_color_system)
 {
-    if (!mp_csp)
+    if (!pl_color_system)
         return false;
 
     // Colorspaces utilizing gamma 2.2 (G22) are set to
@@ -243,27 +243,27 @@ static bool d3d11_get_mp_csp(DXGI_COLOR_SPACE_TYPE csp,
     // regarding not doing conversion from BT.601 to BT.709.
     switch (csp) {
     case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709:
-        *mp_csp = (struct mp_colorspace){
-            .gamma     = MP_CSP_TRC_AUTO,
-            .primaries = MP_CSP_PRIM_AUTO,
+        *pl_color_system = (struct pl_color_space){
+            .transfer  = PL_COLOR_TRC_UNKNOWN,
+            .primaries = PL_COLOR_PRIM_UNKNOWN,
         };
         break;
     case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709:
-        *mp_csp = (struct mp_colorspace) {
-            .gamma     = MP_CSP_TRC_LINEAR,
-            .primaries = MP_CSP_PRIM_AUTO,
+        *pl_color_system = (struct pl_color_space) {
+            .transfer  = PL_COLOR_TRC_LINEAR,
+            .primaries = PL_COLOR_PRIM_UNKNOWN,
         };
         break;
     case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020:
-        *mp_csp = (struct mp_colorspace) {
-            .gamma     = MP_CSP_TRC_PQ,
-            .primaries = MP_CSP_PRIM_BT_2020,
+        *pl_color_system = (struct pl_color_space) {
+            .transfer  = PL_COLOR_TRC_PQ,
+            .primaries = PL_COLOR_PRIM_BT_2020,
         };
         break;
     case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020:
-        *mp_csp = (struct mp_colorspace) {
-            .gamma     = MP_CSP_TRC_AUTO,
-            .primaries = MP_CSP_PRIM_BT_2020,
+        *pl_color_system = (struct pl_color_space) {
+            .transfer  = PL_COLOR_TRC_UNKNOWN,
+            .primaries = PL_COLOR_PRIM_BT_2020,
         };
         break;
     default:
@@ -793,7 +793,7 @@ static bool configure_created_swapchain(struct mp_log *log,
                                         IDXGISwapChain *swapchain,
                                         DXGI_FORMAT requested_format,
                                         DXGI_COLOR_SPACE_TYPE requested_csp,
-                                        struct mp_colorspace *configured_csp)
+                                        struct pl_color_space *configured_csp)
 {
     DXGI_FORMAT probed_format = DXGI_FORMAT_UNKNOWN;
     DXGI_FORMAT selected_format = DXGI_FORMAT_UNKNOWN;
@@ -801,7 +801,7 @@ static bool configure_created_swapchain(struct mp_log *log,
     DXGI_COLOR_SPACE_TYPE selected_colorspace;
     const char *format_name = NULL;
     const char *csp_name = NULL;
-    struct mp_colorspace mp_csp = { 0 };
+    struct pl_color_space pl_color_system = { 0 };
     bool mp_csp_mapped = false;
 
     query_output_format_and_colorspace(log, swapchain,
@@ -817,7 +817,7 @@ static bool configure_created_swapchain(struct mp_log *log,
                           requested_csp : probed_colorspace;
     format_name   = d3d11_get_format_name(selected_format);
     csp_name      = d3d11_get_csp_name(selected_colorspace);
-    mp_csp_mapped = d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+    mp_csp_mapped = d3d11_get_mp_csp(selected_colorspace, &pl_color_system);
 
     mp_verbose(log, "Selected swapchain format %s (%d), attempting "
                     "to utilize it.\n",
@@ -848,7 +848,7 @@ static bool configure_created_swapchain(struct mp_log *log,
                      "mapping! Overriding to standard sRGB!\n",
                 csp_name, selected_colorspace);
         selected_colorspace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
-        d3d11_get_mp_csp(selected_colorspace, &mp_csp);
+        d3d11_get_mp_csp(selected_colorspace, &pl_color_system);
     }
 
     mp_verbose(log, "Selected swapchain color space %s (%d), attempting to "
@@ -860,7 +860,7 @@ static bool configure_created_swapchain(struct mp_log *log,
     }
 
     if (configured_csp) {
-        *configured_csp = mp_csp;
+        *configured_csp = pl_color_system;
     }
 
     return true;
diff --git a/video/out/gpu/d3d11_helpers.h b/video/out/gpu/d3d11_helpers.h
index c115d330d5f3..c452823cdc04 100644
--- a/video/out/gpu/d3d11_helpers.h
+++ b/video/out/gpu/d3d11_helpers.h
@@ -80,10 +80,10 @@ struct d3d11_swapchain_opts {
     DXGI_FORMAT format;
     DXGI_COLOR_SPACE_TYPE color_space;
 
-    // mp_colorspace mapping of the configured swapchain colorspace
+    // pl_color_space mapping of the configured swapchain colorspace
     // shall be written into this memory location if configuration
     // succeeds. Will be ignored if NULL.
-    struct mp_colorspace *configured_csp;
+    struct pl_color_space *configured_csp;
 
     // Use DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL if possible
     bool flip;
diff --git a/video/out/gpu/lcms.c b/video/out/gpu/lcms.c
index 7006a96776d5..00d819d2f0e3 100644
--- a/video/out/gpu/lcms.c
+++ b/video/out/gpu/lcms.c
@@ -46,8 +46,8 @@ struct gl_lcms {
     char *current_profile;
     bool using_memory_profile;
     bool changed;
-    enum mp_csp_prim current_prim;
-    enum mp_csp_trc current_trc;
+    enum pl_color_primaries current_prim;
+    enum pl_color_transfer current_trc;
 
     struct mp_log *log;
     struct mpv_global *global;
@@ -162,8 +162,8 @@ static bool vid_profile_eq(struct AVBufferRef *a, struct AVBufferRef *b)
 
 // Return whether the profile or config has changed since the last time it was
 // retrieved. If it has changed, gl_lcms_get_lut3d() should be called.
-bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
-                         enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+bool gl_lcms_has_changed(struct gl_lcms *p, enum pl_color_primaries prim,
+                         enum pl_color_transfer trc, struct AVBufferRef *vid_profile)
 {
     if (p->changed || p->current_prim != prim || p->current_trc != trc)
         return true;
@@ -180,7 +180,7 @@ bool gl_lcms_has_profile(struct gl_lcms *p)
 
 static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
                                    cmsHPROFILE disp_profile,
-                                   enum mp_csp_prim prim, enum mp_csp_trc trc)
+                                   enum pl_color_primaries prim, enum pl_color_transfer trc)
 {
     if (p->opts->use_embedded && p->vid_profile) {
         // Try using the embedded ICC profile
@@ -207,26 +207,26 @@ static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
 
     cmsToneCurve *tonecurve[3] = {0};
     switch (trc) {
-    case MP_CSP_TRC_LINEAR:  tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
-    case MP_CSP_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
-    case MP_CSP_TRC_GAMMA20: tonecurve[0] = cmsBuildGamma(cms, 2.0); break;
-    case MP_CSP_TRC_GAMMA22: tonecurve[0] = cmsBuildGamma(cms, 2.2); break;
-    case MP_CSP_TRC_GAMMA24: tonecurve[0] = cmsBuildGamma(cms, 2.4); break;
-    case MP_CSP_TRC_GAMMA26: tonecurve[0] = cmsBuildGamma(cms, 2.6); break;
-    case MP_CSP_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
-
-    case MP_CSP_TRC_SRGB:
+    case PL_COLOR_TRC_LINEAR:  tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
+    case PL_COLOR_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
+    case PL_COLOR_TRC_GAMMA20: tonecurve[0] = cmsBuildGamma(cms, 2.0); break;
+    case PL_COLOR_TRC_GAMMA22: tonecurve[0] = cmsBuildGamma(cms, 2.2); break;
+    case PL_COLOR_TRC_GAMMA24: tonecurve[0] = cmsBuildGamma(cms, 2.4); break;
+    case PL_COLOR_TRC_GAMMA26: tonecurve[0] = cmsBuildGamma(cms, 2.6); break;
+    case PL_COLOR_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
+
+    case PL_COLOR_TRC_SRGB:
         // Values copied from Little-CMS
         tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
                 (double[5]){2.40, 1/1.055, 0.055/1.055, 1/12.92, 0.04045});
         break;
 
-    case MP_CSP_TRC_PRO_PHOTO:
+    case PL_COLOR_TRC_PRO_PHOTO:
         tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
                 (double[5]){1.8, 1.0, 0.0, 1/16.0, 0.03125});
         break;
 
-    case MP_CSP_TRC_BT_1886: {
+    case PL_COLOR_TRC_BT_1886: {
         double src_black[3];
         if (p->opts->contrast < 0) {
             // User requested infinite contrast, return 2.4 profile
@@ -300,7 +300,7 @@ static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
 }
 
 bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
-                       enum mp_csp_prim prim, enum mp_csp_trc trc,
+                       enum pl_color_primaries prim, enum pl_color_transfer trc,
                        struct AVBufferRef *vid_profile)
 {
     int s_r, s_g, s_b;
@@ -474,8 +474,8 @@ struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
 void gl_lcms_update_options(struct gl_lcms *p) { }
 bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile) {return false;}
 
-bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
-                         enum mp_csp_trc trc, struct AVBufferRef *vid_profile)
+bool gl_lcms_has_changed(struct gl_lcms *p, enum pl_color_primaries prim,
+                         enum pl_color_transfer trc, struct AVBufferRef *vid_profile)
 {
     return false;
 }
@@ -486,7 +486,7 @@ bool gl_lcms_has_profile(struct gl_lcms *p)
 }
 
 bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
-                       enum mp_csp_prim prim, enum mp_csp_trc trc,
+                       enum pl_color_primaries prim, enum pl_color_transfer trc,
                        struct AVBufferRef *vid_profile)
 {
     return false;
diff --git a/video/out/gpu/lcms.h b/video/out/gpu/lcms.h
index 607353a84ff3..d0b0fe5b8927 100644
--- a/video/out/gpu/lcms.h
+++ b/video/out/gpu/lcms.h
@@ -37,10 +37,10 @@ void gl_lcms_update_options(struct gl_lcms *p);
 bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile);
 bool gl_lcms_has_profile(struct gl_lcms *p);
 bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **,
-                       enum mp_csp_prim prim, enum mp_csp_trc trc,
+                       enum pl_color_primaries prim, enum pl_color_transfer trc,
                        struct AVBufferRef *vid_profile);
-bool gl_lcms_has_changed(struct gl_lcms *p, enum mp_csp_prim prim,
-                         enum mp_csp_trc trc, struct AVBufferRef *vid_profile);
+bool gl_lcms_has_changed(struct gl_lcms *p, enum pl_color_primaries prim,
+                         enum pl_color_transfer trc, struct AVBufferRef *vid_profile);
 
 static inline bool gl_parse_3dlut_size(const char *arg, int *p1, int *p2, int *p3)
 {
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
index 0f4826138981..62388d65c32b 100644
--- a/video/out/gpu/video.c
+++ b/video/out/gpu/video.c
@@ -368,13 +368,13 @@ const struct m_sub_options gl_video_conf = {
             .deprecation_message = "no replacement"},
         {"gamma-auto", OPT_BOOL(gamma_auto),
             .deprecation_message = "no replacement"},
-        {"target-prim", OPT_CHOICE_C(target_prim, mp_csp_prim_names)},
-        {"target-trc", OPT_CHOICE_C(target_trc, mp_csp_trc_names)},
+        {"target-prim", OPT_CHOICE_C(target_prim, pl_csp_prim_names)},
+        {"target-trc", OPT_CHOICE_C(target_trc, pl_csp_trc_names)},
         {"target-peak", OPT_CHOICE(target_peak, {"auto", 0}),
             M_RANGE(10, 10000)},
         {"target-contrast", OPT_CHOICE(target_contrast, {"auto", 0}, {"inf", -1}),
             M_RANGE(10, 1000000)},
-        {"target-gamut", OPT_CHOICE_C(target_gamut, mp_csp_prim_names)},
+        {"target-gamut", OPT_CHOICE_C(target_gamut, pl_csp_prim_names)},
         {"tone-mapping", OPT_CHOICE(tone_map.curve,
             {"auto",     TONE_MAPPING_AUTO},
             {"clip",     TONE_MAPPING_CLIP},
@@ -605,15 +605,6 @@ bool gl_video_gamma_auto_enabled(struct gl_video *p)
     return p->opts.gamma_auto;
 }
 
-struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p)
-{
-    return (struct mp_colorspace) {
-        .primaries = p->opts.target_prim,
-        .gamma = p->opts.target_trc,
-        .hdr.max_luma = p->opts.target_peak,
-    };
-}
-
 // Warning: profile.start must point to a ta allocation, and the function
 //          takes over ownership.
 void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data)
@@ -627,8 +618,8 @@ bool gl_video_icc_auto_enabled(struct gl_video *p)
     return p->opts.icc_opts ? p->opts.icc_opts->profile_auto : false;
 }
 
-static bool gl_video_get_lut3d(struct gl_video *p, enum mp_csp_prim prim,
-                               enum mp_csp_trc trc)
+static bool gl_video_get_lut3d(struct gl_video *p, enum pl_color_primaries prim,
+                               enum pl_color_transfer trc)
 {
     if (!p->use_lut_3d)
         return false;
@@ -796,9 +787,9 @@ static void pass_get_images(struct gl_video *p, struct video_image *vimg,
                 ctype = PLANE_NONE;
             } else if (c == 4) {
                 ctype = PLANE_ALPHA;
-            } else if (p->image_params.color.space == MP_CSP_RGB) {
+            } else if (p->image_params.repr.sys == PL_COLOR_SYSTEM_RGB) {
                 ctype = PLANE_RGB;
-            } else if (p->image_params.color.space == MP_CSP_XYZ) {
+            } else if (p->image_params.repr.sys == PL_COLOR_SYSTEM_XYZ) {
                 ctype = PLANE_XYZ;
             } else {
                 ctype = c == 1 ? PLANE_LUMA : PLANE_CHROMA;
@@ -810,7 +801,7 @@ static void pass_get_images(struct gl_video *p, struct video_image *vimg,
 
         int msb_valid_bits =
             p->ra_format.component_bits + MPMIN(p->ra_format.component_pad, 0);
-        int csp = type == PLANE_ALPHA ? MP_CSP_RGB : p->image_params.color.space;
+        int csp = type == PLANE_ALPHA ? PL_COLOR_SYSTEM_RGB : p->image_params.repr.sys;
         float tex_mul =
             1.0 / mp_get_csp_mul(csp, msb_valid_bits, p->ra_format.component_bits);
         if (p->ra_format.component_type == RA_CTYPE_FLOAT)
@@ -1954,7 +1945,7 @@ static void deband_hook(struct gl_video *p, struct image img,
 {
     pass_describe(p, "debanding (%s)", plane_names[img.type]);
     pass_sample_deband(p->sc, p->opts.deband_opts, &p->lfg,
-                       p->image_params.color.gamma);
+                       p->image_params.color.transfer);
 }
 
 static void unsharp_hook(struct gl_video *p, struct image img,
@@ -2344,8 +2335,8 @@ static void pass_convert_yuv(struct gl_video *p)
         GLSLF("color = color.%s;\n", p->color_swizzle);
 
     // Pre-colormatrix input gamma correction
-    if (cparams.color.space == MP_CSP_XYZ)
-        pass_linearize(p->sc, p->image_params.color.gamma);
+    if (cparams.repr.sys == PL_COLOR_SYSTEM_XYZ)
+        pass_linearize(p->sc, p->image_params.color.transfer);
 
     // We always explicitly normalize the range in pass_read_video
     cparams.input_bits = cparams.texture_bits = 0;
@@ -2359,14 +2350,14 @@ static void pass_convert_yuv(struct gl_video *p)
 
     GLSL(color.rgb = mat3(colormatrix) * color.rgb + colormatrix_c;)
 
-    if (cparams.color.space == MP_CSP_XYZ) {
-        pass_delinearize(p->sc, p->image_params.color.gamma);
+    if (cparams.repr.sys == PL_COLOR_SYSTEM_XYZ) {
+        pass_delinearize(p->sc, p->image_params.color.transfer);
         // mp_get_csp_matrix implicitly converts XYZ to DCI-P3
-        p->image_params.color.space = MP_CSP_RGB;
-        p->image_params.color.primaries = MP_CSP_PRIM_DCI_P3;
+        p->image_params.repr.sys = PL_COLOR_SYSTEM_RGB;
+        p->image_params.color.primaries = PL_COLOR_PRIM_DCI_P3;
     }
 
-    if (p->image_params.color.space == MP_CSP_BT_2020_C) {
+    if (p->image_params.repr.sys == PL_COLOR_SYSTEM_BT_2020_C) {
         // Conversion for C'rcY'cC'bc via the BT.2020 CL system:
         // C'bc = (B'-Y'c) / 1.9404  | C'bc <= 0
         //      = (B'-Y'c) / 1.5816  | C'bc >  0
@@ -2490,7 +2481,7 @@ static void pass_scale_main(struct gl_video *p)
         // Linear light downscaling results in nasty artifacts for HDR curves
         // due to the potentially extreme brightness differences severely
         // compounding any ringing. So just scale in gamma light instead.
-        if (mp_trc_is_hdr(p->image_params.color.gamma))
+        if (mp_trc_is_hdr(p->image_params.color.transfer))
             use_linear = false;
     } else if (upscaling) {
         use_linear = p->opts.linear_upscaling || p->opts.sigmoid_upscaling;
@@ -2498,7 +2489,7 @@ static void pass_scale_main(struct gl_video *p)
 
     if (use_linear) {
         p->use_linear = true;
-        pass_linearize(p->sc, p->image_params.color.gamma);
+        pass_linearize(p->sc, p->image_params.color.transfer);
         pass_opt_hook_point(p, "LINEAR", NULL);
     }
 
@@ -2551,8 +2542,9 @@ static void pass_scale_main(struct gl_video *p)
 // rendering)
 // If OSD is true, ignore any changes that may have been made to the video
 // by previous passes (i.e. linear scaling)
-static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
-                             struct mp_colorspace fbo_csp, int flags, bool osd)
+static void pass_colormanage(struct gl_video *p, struct pl_color_space src,
+                             enum mp_csp_light src_light,
+                             struct pl_color_space fbo_csp, int flags, bool osd)
 {
     struct ra *ra = p->ra;
 
@@ -2560,18 +2552,17 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
     // unless specific transfer function, primaries or target peak
     // is set. If values are set to _AUTO, the most likely intended
     // values are guesstimated later in this function.
-    struct mp_colorspace dst = {
-        .gamma = p->opts.target_trc == MP_CSP_TRC_AUTO ?
-                 fbo_csp.gamma : p->opts.target_trc,
-        .primaries = p->opts.target_prim == MP_CSP_PRIM_AUTO ?
+    struct pl_color_space dst = {
+        .transfer = p->opts.target_trc == PL_COLOR_TRC_UNKNOWN ?
+                        fbo_csp.transfer : p->opts.target_trc,
+        .primaries = p->opts.target_prim == PL_COLOR_PRIM_UNKNOWN ?
                      fbo_csp.primaries : p->opts.target_prim,
-        .light = MP_CSP_LIGHT_DISPLAY,
         .hdr.max_luma = !p->opts.target_peak ?
                         fbo_csp.hdr.max_luma : p->opts.target_peak,
     };
 
     if (!p->colorspace_override_warned &&
-        ((fbo_csp.gamma && dst.gamma != fbo_csp.gamma) ||
+        ((fbo_csp.transfer && dst.transfer != fbo_csp.transfer) ||
          (fbo_csp.primaries && dst.primaries != fbo_csp.primaries)))
     {
         MP_WARN(p, "One or more colorspace value is being overridden "
@@ -2579,44 +2570,44 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
                    "transfer function: (dst: %s, fbo: %s), "
                    "primaries: (dst: %s, fbo: %s). "
                    "Rendering can lead to incorrect results!\n",
-                m_opt_choice_str(mp_csp_trc_names,  dst.gamma),
-                m_opt_choice_str(mp_csp_trc_names,  fbo_csp.gamma),
-                m_opt_choice_str(mp_csp_prim_names, dst.primaries),
-                m_opt_choice_str(mp_csp_prim_names, fbo_csp.primaries));
+                m_opt_choice_str(pl_csp_trc_names,  dst.transfer),
+                m_opt_choice_str(pl_csp_trc_names,  fbo_csp.transfer),
+                m_opt_choice_str(pl_csp_prim_names, dst.primaries),
+                m_opt_choice_str(pl_csp_prim_names, fbo_csp.primaries));
         p->colorspace_override_warned = true;
     }
 
-    if (dst.gamma == MP_CSP_TRC_HLG)
-        dst.light = MP_CSP_LIGHT_SCENE_HLG;
+    enum mp_csp_light dst_light = dst.transfer == PL_COLOR_TRC_HLG ?
+                                    MP_CSP_LIGHT_SCENE_HLG : MP_CSP_LIGHT_DISPLAY;
 
     if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
         // The 3DLUT is always generated against the video's original source
         // space, *not* the reference space. (To avoid having to regenerate
         // the 3DLUT for the OSD on every frame)
-        enum mp_csp_prim prim_orig = p->image_params.color.primaries;
-        enum mp_csp_trc trc_orig = p->image_params.color.gamma;
+        enum pl_color_primaries prim_orig = p->image_params.color.primaries;
+        enum pl_color_transfer trc_orig = p->image_params.color.transfer;
 
         // One exception: HDR is not implemented by LittleCMS for technical
         // limitation reasons, so we use a gamma 2.2 input curve here instead.
         // We could pick any value we want here, the difference is just coding
         // efficiency.
         if (mp_trc_is_hdr(trc_orig))
-            trc_orig = MP_CSP_TRC_GAMMA22;
+            trc_orig = PL_COLOR_TRC_GAMMA22;
 
         if (gl_video_get_lut3d(p, prim_orig, trc_orig)) {
             dst.primaries = prim_orig;
-            dst.gamma = trc_orig;
-            assert(dst.primaries && dst.gamma);
+            dst.transfer = trc_orig;
+            assert(dst.primaries && dst.transfer);
         }
     }
 
-    if (dst.primaries == MP_CSP_PRIM_AUTO) {
+    if (dst.primaries == PL_COLOR_PRIM_UNKNOWN) {
         // The vast majority of people are on sRGB or BT.709 displays, so pick
         // this as the default output color space.
-        dst.primaries = MP_CSP_PRIM_BT_709;
+        dst.primaries = PL_COLOR_PRIM_BT_709;
 
-        if (src.primaries == MP_CSP_PRIM_BT_601_525 ||
-            src.primaries == MP_CSP_PRIM_BT_601_625)
+        if (src.primaries == PL_COLOR_PRIM_BT_601_525 ||
+            src.primaries == PL_COLOR_PRIM_BT_601_625)
         {
             // Since we auto-pick BT.601 and BT.709 based on the dimensions,
             // combined with the fact that they're very similar to begin with,
@@ -2626,28 +2617,28 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
         }
     }
 
-    if (dst.gamma == MP_CSP_TRC_AUTO) {
+    if (dst.transfer == PL_COLOR_TRC_UNKNOWN) {
         // Most people seem to complain when the image is darker or brighter
         // than what they're "used to", so just avoid changing the gamma
         // altogether by default. The only exceptions to this rule apply to
         // very unusual TRCs, which even hardcode technoluddites would probably
         // not enjoy viewing unaltered.
-        dst.gamma = src.gamma;
+        dst.transfer = src.transfer;
 
         // Avoid outputting linear light or HDR content "by default". For these
         // just pick gamma 2.2 as a default, since it's a good estimate for
         // the response of typical displays
-        if (dst.gamma == MP_CSP_TRC_LINEAR || mp_trc_is_hdr(dst.gamma))
-            dst.gamma = MP_CSP_TRC_GAMMA22;
+        if (dst.transfer == PL_COLOR_TRC_LINEAR || mp_trc_is_hdr(dst.transfer))
+            dst.transfer = PL_COLOR_TRC_GAMMA22;
     }
 
     // If there's no specific signal peak known for the output display, infer
     // it from the chosen transfer function. Also normalize the src peak, in
     // case it was unknown
     if (!dst.hdr.max_luma)
-        dst.hdr.max_luma = mp_trc_nom_peak(dst.gamma) * MP_REF_WHITE;
+        dst.hdr.max_luma = mp_trc_nom_peak(dst.transfer) * MP_REF_WHITE;
     if (!src.hdr.max_luma)
-        src.hdr.max_luma = mp_trc_nom_peak(src.gamma) * MP_REF_WHITE;
+        src.hdr.max_luma = mp_trc_nom_peak(src.transfer) * MP_REF_WHITE;
 
     // Whitelist supported modes
     switch (p->opts.tone_map.curve) {
@@ -2679,7 +2670,7 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
     }
 
     struct gl_tone_map_opts tone_map = p->opts.tone_map;
-    bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.gamma)
+    bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.transfer)
                        && src.hdr.max_luma > dst.hdr.max_luma;
 
     if (detect_peak && !p->hdr_peak_ssbo) {
@@ -2718,7 +2709,7 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src,
     }
 
     // Adapt from src to dst as necessary
-    pass_color_map(p->sc, p->use_linear && !osd, src, dst, &tone_map);
+    pass_color_map(p->sc, p->use_linear && !osd, src, dst, src_light, dst_light, &tone_map);
 
     if (p->use_lut_3d && (flags & RENDER_SCREEN_COLOR)) {
         gl_sc_uniform_texture(p->sc, "lut_3d", p->lut_3d_texture);
@@ -2909,13 +2900,13 @@ static void pass_draw_osd(struct gl_video *p, int osd_flags, int frame_flags,
         // When subtitles need to be color managed, assume they're in sRGB
         // (for lack of anything saner to do)
         if (cms) {
-            static const struct mp_colorspace csp_srgb = {
-                .primaries = MP_CSP_PRIM_BT_709,
-                .gamma = MP_CSP_TRC_SRGB,
-                .light = MP_CSP_LIGHT_DISPLAY,
+            static const struct pl_color_space csp_srgb = {
+                .primaries = PL_COLOR_PRIM_BT_709,
+                .transfer = PL_COLOR_TRC_SRGB,
             };
 
-            pass_colormanage(p, csp_srgb, fbo.color_space, frame_flags, true);
+            pass_colormanage(p, csp_srgb, MP_CSP_LIGHT_DISPLAY, fbo.color_space,
+                             frame_flags, true);
         }
         mpgl_osd_draw_finish(p->osd, n, p->sc, fbo);
     }
@@ -3039,7 +3030,7 @@ static bool pass_render_frame(struct gl_video *p, struct mp_image *mpi,
         rect.mt *= scale[1]; rect.mb *= scale[1];
         // We should always blend subtitles in non-linear light
         if (p->use_linear) {
-            pass_delinearize(p->sc, p->image_params.color.gamma);
+            pass_delinearize(p->sc, p->image_params.color.transfer);
             p->use_linear = false;
         }
         finish_pass_tex(p, &p->blend_subs_tex, p->texture_w, p->texture_h);
@@ -3066,7 +3057,8 @@ static void pass_draw_to_screen(struct gl_video *p, struct ra_fbo fbo, int flags
         GLSL(color.rgb = pow(color.rgb, vec3(user_gamma));)
     }
 
-    pass_colormanage(p, p->image_params.color, fbo.color_space, flags, false);
+    pass_colormanage(p, p->image_params.color, p->image_params.light,
+                     fbo.color_space, flags, false);
 
     // Since finish_pass_fbo doesn't work with compute shaders, and neither
     // does the checkerboard/dither code, we may need an indirection via
@@ -3121,7 +3113,7 @@ static bool update_surface(struct gl_video *p, struct mp_image *mpi,
     // because mixing in compressed light artificially darkens the results
     if (!p->use_linear) {
         p->use_linear = true;
-        pass_linearize(p->sc, p->image_params.color.gamma);
+        pass_linearize(p->sc, p->image_params.color.transfer);
     }
 
     finish_pass_tex(p, &surf->tex, vp_w, vp_h);
@@ -3912,8 +3904,8 @@ static void check_gl_features(struct gl_video *p)
         }
     }
 
-    int use_cms = p->opts.target_prim != MP_CSP_PRIM_AUTO ||
-                  p->opts.target_trc != MP_CSP_TRC_AUTO || p->use_lut_3d;
+    int use_cms = p->opts.target_prim != PL_COLOR_PRIM_UNKNOWN ||
+                  p->opts.target_trc != PL_COLOR_TRC_UNKNOWN || p->use_lut_3d;
 
     // mix() is needed for some gamma functions
     if (!have_mglsl && (p->opts.linear_downscaling ||
@@ -3925,8 +3917,8 @@ static void check_gl_features(struct gl_video *p)
         MP_WARN(p, "Disabling linear/sigmoid scaling (GLSL version too old).\n");
     }
     if (!have_mglsl && use_cms) {
-        p->opts.target_prim = MP_CSP_PRIM_AUTO;
-        p->opts.target_trc = MP_CSP_TRC_AUTO;
+        p->opts.target_prim = PL_COLOR_PRIM_UNKNOWN;
+        p->opts.target_trc = PL_COLOR_TRC_UNKNOWN;
         p->use_lut_3d = false;
         MP_WARN(p, "Disabling color management (GLSL version too old).\n");
     }
diff --git a/video/out/gpu/video.h b/video/out/gpu/video.h
index 411d336a2191..d1b7b3fc519c 100644
--- a/video/out/gpu/video.h
+++ b/video/out/gpu/video.h
@@ -215,7 +215,6 @@ void gl_video_set_ambient_lux(struct gl_video *p, int lux);
 void gl_video_set_icc_profile(struct gl_video *p, bstr icc_data);
 bool gl_video_icc_auto_enabled(struct gl_video *p);
 bool gl_video_gamma_auto_enabled(struct gl_video *p);
-struct mp_colorspace gl_video_get_output_colorspace(struct gl_video *p);
 
 void gl_video_reset(struct gl_video *p);
 bool gl_video_showing_interpolated_frame(struct gl_video *p);
diff --git a/video/out/gpu/video_shaders.c b/video/out/gpu/video_shaders.c
index 6c0e8a815e56..2201e12d4639 100644
--- a/video/out/gpu/video_shaders.c
+++ b/video/out/gpu/video_shaders.c
@@ -338,9 +338,9 @@ static const float SLOG_A = 0.432699,
 // These functions always output to a normalized scale of [0,1], for
 // convenience of the video.c code that calls it. To get the values in an
 // absolute scale, multiply the result by `mp_trc_nom_peak(trc)`
-void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+void pass_linearize(struct gl_shader_cache *sc, enum pl_color_transfer trc)
 {
-    if (trc == MP_CSP_TRC_LINEAR)
+    if (trc == PL_COLOR_TRC_LINEAR)
         return;
 
     GLSLF("// linearize\n");
@@ -353,40 +353,40 @@ void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
     GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
 
     switch (trc) {
-    case MP_CSP_TRC_SRGB:
+    case PL_COLOR_TRC_SRGB:
         GLSLF("color.rgb = mix(color.rgb * vec3(1.0/12.92),             \n"
               "                pow((color.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)), \n"
               "                %s(lessThan(vec3(0.04045), color.rgb))); \n",
               gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_BT_1886:
+    case PL_COLOR_TRC_BT_1886:
         GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
         break;
-    case MP_CSP_TRC_GAMMA18:
+    case PL_COLOR_TRC_GAMMA18:
         GLSL(color.rgb = pow(color.rgb, vec3(1.8));)
         break;
-    case MP_CSP_TRC_GAMMA20:
+    case PL_COLOR_TRC_GAMMA20:
         GLSL(color.rgb = pow(color.rgb, vec3(2.0));)
         break;
-    case MP_CSP_TRC_GAMMA22:
+    case PL_COLOR_TRC_GAMMA22:
         GLSL(color.rgb = pow(color.rgb, vec3(2.2));)
         break;
-    case MP_CSP_TRC_GAMMA24:
+    case PL_COLOR_TRC_GAMMA24:
         GLSL(color.rgb = pow(color.rgb, vec3(2.4));)
         break;
-    case MP_CSP_TRC_GAMMA26:
+    case PL_COLOR_TRC_GAMMA26:
         GLSL(color.rgb = pow(color.rgb, vec3(2.6));)
         break;
-    case MP_CSP_TRC_GAMMA28:
+    case PL_COLOR_TRC_GAMMA28:
         GLSL(color.rgb = pow(color.rgb, vec3(2.8));)
         break;
-    case MP_CSP_TRC_PRO_PHOTO:
+    case PL_COLOR_TRC_PRO_PHOTO:
         GLSLF("color.rgb = mix(color.rgb * vec3(1.0/16.0),              \n"
               "                pow(color.rgb, vec3(1.8)),               \n"
               "                %s(lessThan(vec3(0.03125), color.rgb))); \n",
               gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_PQ:
+    case PL_COLOR_TRC_PQ:
         GLSLF("color.rgb = pow(color.rgb, vec3(1.0/%f));\n", PQ_M2);
         GLSLF("color.rgb = max(color.rgb - vec3(%f), vec3(0.0)) \n"
               "             / (vec3(%f) - vec3(%f) * color.rgb);\n",
@@ -396,33 +396,33 @@ void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
         // MP_REF_WHITE instead, so rescale
         GLSLF("color.rgb *= vec3(%f);\n", 10000 / MP_REF_WHITE);
         break;
-    case MP_CSP_TRC_HLG:
+    case PL_COLOR_TRC_HLG:
         GLSLF("color.rgb = mix(vec3(4.0) * color.rgb * color.rgb,\n"
               "                exp((color.rgb - vec3(%f)) * vec3(1.0/%f)) + vec3(%f),\n"
               "                %s(lessThan(vec3(0.5), color.rgb)));\n",
               HLG_C, HLG_A, HLG_B, gl_sc_bvec(sc, 3));
         GLSLF("color.rgb *= vec3(1.0/%f);\n", MP_REF_WHITE_HLG);
         break;
-    case MP_CSP_TRC_V_LOG:
+    case PL_COLOR_TRC_V_LOG:
         GLSLF("color.rgb = mix((color.rgb - vec3(0.125)) * vec3(1.0/5.6), \n"
               "    pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
               "              - vec3(%f),                                  \n"
               "    %s(lessThanEqual(vec3(0.181), color.rgb)));            \n",
               VLOG_D, VLOG_C, VLOG_B, gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_S_LOG1:
+    case PL_COLOR_TRC_S_LOG1:
         GLSLF("color.rgb = pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f))\n"
               "            - vec3(%f);\n",
               SLOG_C, SLOG_A, SLOG_B);
         break;
-    case MP_CSP_TRC_S_LOG2:
+    case PL_COLOR_TRC_S_LOG2:
         GLSLF("color.rgb = mix((color.rgb - vec3(%f)) * vec3(1.0/%f),      \n"
               "    (pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
               "              - vec3(%f)) * vec3(1.0/%f),                   \n"
               "    %s(lessThanEqual(vec3(%f), color.rgb)));                \n",
               SLOG_Q, SLOG_P, SLOG_C, SLOG_A, SLOG_B, SLOG_K2, gl_sc_bvec(sc, 3), SLOG_Q);
         break;
-    case MP_CSP_TRC_ST428:
+    case PL_COLOR_TRC_ST428:
         GLSL(color.rgb = vec3(52.37/48.0) * pow(color.rgb, vec3(2.6)););
         break;
     default:
@@ -438,9 +438,9 @@ void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
 // reference monitor.
 //
 // Like pass_linearize, this functions ingests values on an normalized scale
-void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
+void pass_delinearize(struct gl_shader_cache *sc, enum pl_color_transfer trc)
 {
-    if (trc == MP_CSP_TRC_LINEAR)
+    if (trc == PL_COLOR_TRC_LINEAR)
         return;
 
     GLSLF("// delinearize\n");
@@ -448,41 +448,41 @@ void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
     GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(trc));
 
     switch (trc) {
-    case MP_CSP_TRC_SRGB:
+    case PL_COLOR_TRC_SRGB:
         GLSLF("color.rgb = mix(color.rgb * vec3(12.92),                       \n"
               "               vec3(1.055) * pow(color.rgb, vec3(1.0/2.4))     \n"
               "                   - vec3(0.055),                              \n"
               "               %s(lessThanEqual(vec3(0.0031308), color.rgb))); \n",
               gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_BT_1886:
+    case PL_COLOR_TRC_BT_1886:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
         break;
-    case MP_CSP_TRC_GAMMA18:
+    case PL_COLOR_TRC_GAMMA18:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/1.8));)
         break;
-    case MP_CSP_TRC_GAMMA20:
+    case PL_COLOR_TRC_GAMMA20:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.0));)
         break;
-    case MP_CSP_TRC_GAMMA22:
+    case PL_COLOR_TRC_GAMMA22:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.2));)
         break;
-    case MP_CSP_TRC_GAMMA24:
+    case PL_COLOR_TRC_GAMMA24:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.4));)
         break;
-    case MP_CSP_TRC_GAMMA26:
+    case PL_COLOR_TRC_GAMMA26:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.6));)
         break;
-    case MP_CSP_TRC_GAMMA28:
+    case PL_COLOR_TRC_GAMMA28:
         GLSL(color.rgb = pow(color.rgb, vec3(1.0/2.8));)
         break;
-    case MP_CSP_TRC_PRO_PHOTO:
+    case PL_COLOR_TRC_PRO_PHOTO:
         GLSLF("color.rgb = mix(color.rgb * vec3(16.0),                        \n"
               "                pow(color.rgb, vec3(1.0/1.8)),                 \n"
               "                %s(lessThanEqual(vec3(0.001953), color.rgb))); \n",
               gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_PQ:
+    case PL_COLOR_TRC_PQ:
         GLSLF("color.rgb *= vec3(1.0/%f);\n", 10000 / MP_REF_WHITE);
         GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M1);
         GLSLF("color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
@@ -490,32 +490,32 @@ void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc)
               PQ_C1, PQ_C2, PQ_C3);
         GLSLF("color.rgb = pow(color.rgb, vec3(%f));\n", PQ_M2);
         break;
-    case MP_CSP_TRC_HLG:
+    case PL_COLOR_TRC_HLG:
         GLSLF("color.rgb *= vec3(%f);\n", MP_REF_WHITE_HLG);
         GLSLF("color.rgb = mix(vec3(0.5) * sqrt(color.rgb),\n"
               "                vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f),\n"
               "                %s(lessThan(vec3(1.0), color.rgb)));\n",
               HLG_A, HLG_B, HLG_C, gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_V_LOG:
+    case PL_COLOR_TRC_V_LOG:
         GLSLF("color.rgb = mix(vec3(5.6) * color.rgb + vec3(0.125),   \n"
               "                vec3(%f) * log(color.rgb + vec3(%f))   \n"
               "                    + vec3(%f),                        \n"
               "                %s(lessThanEqual(vec3(0.01), color.rgb))); \n",
               VLOG_C / M_LN10, VLOG_B, VLOG_D, gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_S_LOG1:
+    case PL_COLOR_TRC_S_LOG1:
         GLSLF("color.rgb = vec3(%f) * log(color.rgb + vec3(%f)) + vec3(%f);\n",
               SLOG_A / M_LN10, SLOG_B, SLOG_C);
         break;
-    case MP_CSP_TRC_S_LOG2:
+    case PL_COLOR_TRC_S_LOG2:
         GLSLF("color.rgb = mix(vec3(%f) * color.rgb + vec3(%f),                \n"
               "                vec3(%f) * log(vec3(%f) * color.rgb + vec3(%f)) \n"
               "                    + vec3(%f),                                 \n"
               "                %s(lessThanEqual(vec3(0.0), color.rgb)));       \n",
               SLOG_P, SLOG_Q, SLOG_A / M_LN10, SLOG_K2, SLOG_B, SLOG_C, gl_sc_bvec(sc, 3));
         break;
-    case MP_CSP_TRC_ST428:
+    case PL_COLOR_TRC_ST428:
         GLSL(color.rgb = pow(color.rgb * vec3(48.0/52.37), vec3(1.0/2.6)););
         break;
     default:
@@ -834,7 +834,8 @@ static void pass_tone_map(struct gl_shader_cache *sc,
 // the caller to have already bound the appropriate SSBO and set up the compute
 // shader metadata
 void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
-                    struct mp_colorspace src, struct mp_colorspace dst,
+                    struct pl_color_space src, struct pl_color_space dst,
+                    enum mp_csp_light src_light, enum mp_csp_light dst_light,
                     const struct gl_tone_map_opts *opts)
 {
     GLSLF("// color mapping\n");
@@ -847,29 +848,29 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
     mp_get_rgb2xyz_matrix(mp_get_csp_primaries(dst.primaries), rgb2xyz);
     gl_sc_uniform_vec3(sc, "dst_luma", rgb2xyz[1]);
 
-    bool need_ootf = src.light != dst.light;
-    if (src.light == MP_CSP_LIGHT_SCENE_HLG && src.hdr.max_luma != dst.hdr.max_luma)
+    bool need_ootf = src_light != dst_light;
+    if (src_light == MP_CSP_LIGHT_SCENE_HLG && src.hdr.max_luma != dst.hdr.max_luma)
         need_ootf = true;
 
     // All operations from here on require linear light as a starting point,
-    // so we linearize even if src.gamma == dst.gamma when one of the other
+    // so we linearize even if src.gamma == dst.transfer when one of the other
     // operations needs it
-    bool need_linear = src.gamma != dst.gamma ||
+    bool need_linear = src.transfer != dst.transfer ||
                        src.primaries != dst.primaries ||
                        src.hdr.max_luma != dst.hdr.max_luma ||
                        need_ootf;
 
     if (need_linear && !is_linear) {
         // We also pull it up so that 1.0 is the reference white
-        pass_linearize(sc, src.gamma);
+        pass_linearize(sc, src.transfer);
         is_linear = true;
     }
 
     // Pre-scale the incoming values into an absolute scale
-    GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(src.gamma));
+    GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(src.transfer));
 
     if (need_ootf)
-        pass_ootf(sc, src.light, src.hdr.max_luma / MP_REF_WHITE);
+        pass_ootf(sc, src_light, src.hdr.max_luma / MP_REF_WHITE);
 
     // Tone map to prevent clipping due to excessive brightness
     if (src.hdr.max_luma > dst.hdr.max_luma) {
@@ -900,14 +901,14 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
     }
 
     if (need_ootf)
-        pass_inverse_ootf(sc, dst.light, dst.hdr.max_luma / MP_REF_WHITE);
+        pass_inverse_ootf(sc, dst_light, dst.hdr.max_luma / MP_REF_WHITE);
 
     // Post-scale the outgoing values from absolute scale to normalized.
     // For SDR, we normalize to the chosen signal peak. For HDR, we normalize
     // to the encoding range of the transfer function.
     float dst_range = dst.hdr.max_luma / MP_REF_WHITE;
-    if (mp_trc_is_hdr(dst.gamma))
-        dst_range = mp_trc_nom_peak(dst.gamma);
+    if (mp_trc_is_hdr(dst.transfer))
+        dst_range = mp_trc_nom_peak(dst.transfer);
 
     GLSLF("color.rgb *= vec3(%f);\n", 1.0 / dst_range);
 
@@ -919,7 +920,7 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
     }
 
     if (is_linear)
-        pass_delinearize(sc, dst.gamma);
+        pass_delinearize(sc, dst.transfer);
 }
 
 // Wide usage friendly PRNG, shamelessly stolen from a GLSL tricks forum post.
@@ -964,7 +965,7 @@ const struct m_sub_options deband_conf = {
 
 // Stochastically sample a debanded result from a hooked texture.
 void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
-                        AVLFG *lfg, enum mp_csp_trc trc)
+                        AVLFG *lfg, enum pl_color_transfer trc)
 {
     // Initialize the PRNG
     GLSLF("{\n");
diff --git a/video/out/gpu/video_shaders.h b/video/out/gpu/video_shaders.h
index 27e7874c6d19..7547df6a02cb 100644
--- a/video/out/gpu/video_shaders.h
+++ b/video/out/gpu/video_shaders.h
@@ -44,15 +44,16 @@ void pass_sample_bicubic_fast(struct gl_shader_cache *sc);
 void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
                             int w, int h);
 
-void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
-void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
+void pass_linearize(struct gl_shader_cache *sc, enum pl_color_transfer trc);
+void pass_delinearize(struct gl_shader_cache *sc, enum pl_color_transfer trc);
 
 void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
-                    struct mp_colorspace src, struct mp_colorspace dst,
+                    struct pl_color_space src, struct pl_color_space dst,
+                    enum mp_csp_light src_light, enum mp_csp_light dst_light,
                     const struct gl_tone_map_opts *opts);
 
 void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
-                        AVLFG *lfg, enum mp_csp_trc trc);
+                        AVLFG *lfg, enum pl_color_transfer trc);
 
 void pass_sample_unsharp(struct gl_shader_cache *sc, float param);
 
diff --git a/video/out/opengl/hwdec_rpi.c b/video/out/opengl/hwdec_rpi.c
index 5362832b0334..b90fae571d1e 100644
--- a/video/out/opengl/hwdec_rpi.c
+++ b/video/out/opengl/hwdec_rpi.c
@@ -82,12 +82,12 @@ static size_t layout_buffer(struct mp_image *mpi, MMAL_BUFFER_HEADER_T *buffer,
     return size;
 }
 
-static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+static MMAL_FOURCC_T map_csp(enum pl_color_system csp)
 {
     switch (csp) {
-    case MP_CSP_BT_601:     return MMAL_COLOR_SPACE_ITUR_BT601;
-    case MP_CSP_BT_709:     return MMAL_COLOR_SPACE_ITUR_BT709;
-    case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+    case PL_COLOR_SYSTEM_BT_601:     return MMAL_COLOR_SPACE_ITUR_BT601;
+    case PL_COLOR_SYSTEM_BT_709:     return MMAL_COLOR_SPACE_ITUR_BT709;
+    case PL_COLOR_SYSTEM_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
     default:                return MMAL_COLOR_SPACE_UNKNOWN;
     }
 }
@@ -201,7 +201,7 @@ static int enable_renderer(struct ra_hwdec *hw)
     input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
     input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
     input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
-    input->format->es->video.color_space = map_csp(params->color.space);
+    input->format->es->video.color_space = map_csp(params->repr.sys);
 
     if (mmal_port_format_commit(input))
         return -1;
diff --git a/video/out/placebo/utils.c b/video/out/placebo/utils.c
index 1209b721217d..7bb96dd20af6 100644
--- a/video/out/placebo/utils.c
+++ b/video/out/placebo/utils.c
@@ -66,154 +66,6 @@ void mppl_log_set_probing(pl_log log, bool probing)
     pl_log_update(log, &params);
 }
 
-enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim)
-{
-    switch (prim) {
-    case MP_CSP_PRIM_AUTO:          return PL_COLOR_PRIM_UNKNOWN;
-    case MP_CSP_PRIM_BT_601_525:    return PL_COLOR_PRIM_BT_601_525;
-    case MP_CSP_PRIM_BT_601_625:    return PL_COLOR_PRIM_BT_601_625;
-    case MP_CSP_PRIM_BT_709:        return PL_COLOR_PRIM_BT_709;
-    case MP_CSP_PRIM_BT_2020:       return PL_COLOR_PRIM_BT_2020;
-    case MP_CSP_PRIM_BT_470M:       return PL_COLOR_PRIM_BT_470M;
-    case MP_CSP_PRIM_APPLE:         return PL_COLOR_PRIM_APPLE;
-    case MP_CSP_PRIM_ADOBE:         return PL_COLOR_PRIM_ADOBE;
-    case MP_CSP_PRIM_PRO_PHOTO:     return PL_COLOR_PRIM_PRO_PHOTO;
-    case MP_CSP_PRIM_CIE_1931:      return PL_COLOR_PRIM_CIE_1931;
-    case MP_CSP_PRIM_DCI_P3:        return PL_COLOR_PRIM_DCI_P3;
-    case MP_CSP_PRIM_DISPLAY_P3:    return PL_COLOR_PRIM_DISPLAY_P3;
-    case MP_CSP_PRIM_V_GAMUT:       return PL_COLOR_PRIM_V_GAMUT;
-    case MP_CSP_PRIM_S_GAMUT:       return PL_COLOR_PRIM_S_GAMUT;
-    case MP_CSP_PRIM_EBU_3213:      return PL_COLOR_PRIM_EBU_3213;
-    case MP_CSP_PRIM_FILM_C:        return PL_COLOR_PRIM_FILM_C;
-    case MP_CSP_PRIM_ACES_AP0:      return PL_COLOR_PRIM_ACES_AP0;
-    case MP_CSP_PRIM_ACES_AP1:      return PL_COLOR_PRIM_ACES_AP1;
-    case MP_CSP_PRIM_COUNT:         return PL_COLOR_PRIM_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim)
-{
-    switch (prim){
-    case PL_COLOR_PRIM_UNKNOWN:     return MP_CSP_PRIM_AUTO;
-    case PL_COLOR_PRIM_BT_601_525:  return MP_CSP_PRIM_BT_601_525;
-    case PL_COLOR_PRIM_BT_601_625:  return MP_CSP_PRIM_BT_601_625;
-    case PL_COLOR_PRIM_BT_709:      return MP_CSP_PRIM_BT_709;
-    case PL_COLOR_PRIM_BT_2020:     return MP_CSP_PRIM_BT_2020;
-    case PL_COLOR_PRIM_BT_470M:     return MP_CSP_PRIM_BT_470M;
-    case PL_COLOR_PRIM_APPLE:       return MP_CSP_PRIM_APPLE;
-    case PL_COLOR_PRIM_ADOBE:       return MP_CSP_PRIM_ADOBE;
-    case PL_COLOR_PRIM_PRO_PHOTO:   return MP_CSP_PRIM_PRO_PHOTO;
-    case PL_COLOR_PRIM_CIE_1931:    return MP_CSP_PRIM_CIE_1931;
-    case PL_COLOR_PRIM_DCI_P3:      return MP_CSP_PRIM_DCI_P3;
-    case PL_COLOR_PRIM_DISPLAY_P3:  return MP_CSP_PRIM_DISPLAY_P3;
-    case PL_COLOR_PRIM_V_GAMUT:     return MP_CSP_PRIM_V_GAMUT;
-    case PL_COLOR_PRIM_S_GAMUT:     return MP_CSP_PRIM_S_GAMUT;
-    case PL_COLOR_PRIM_EBU_3213:    return MP_CSP_PRIM_EBU_3213;
-    case PL_COLOR_PRIM_FILM_C:      return MP_CSP_PRIM_FILM_C;
-    case PL_COLOR_PRIM_ACES_AP0:    return MP_CSP_PRIM_ACES_AP0;
-    case PL_COLOR_PRIM_ACES_AP1:    return MP_CSP_PRIM_ACES_AP1;
-    case PL_COLOR_PRIM_COUNT:       return MP_CSP_PRIM_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc)
-{
-    switch (trc) {
-    case MP_CSP_TRC_AUTO:           return PL_COLOR_TRC_UNKNOWN;
-    case MP_CSP_TRC_BT_1886:        return PL_COLOR_TRC_BT_1886;
-    case MP_CSP_TRC_SRGB:           return PL_COLOR_TRC_SRGB;
-    case MP_CSP_TRC_LINEAR:         return PL_COLOR_TRC_LINEAR;
-    case MP_CSP_TRC_GAMMA18:        return PL_COLOR_TRC_GAMMA18;
-    case MP_CSP_TRC_GAMMA20:        return PL_COLOR_TRC_GAMMA20;
-    case MP_CSP_TRC_GAMMA22:        return PL_COLOR_TRC_GAMMA22;
-    case MP_CSP_TRC_GAMMA24:        return PL_COLOR_TRC_GAMMA24;
-    case MP_CSP_TRC_GAMMA26:        return PL_COLOR_TRC_GAMMA26;
-    case MP_CSP_TRC_GAMMA28:        return PL_COLOR_TRC_GAMMA28;
-    case MP_CSP_TRC_PRO_PHOTO:      return PL_COLOR_TRC_PRO_PHOTO;
-    case MP_CSP_TRC_PQ:             return PL_COLOR_TRC_PQ;
-    case MP_CSP_TRC_HLG:            return PL_COLOR_TRC_HLG;
-    case MP_CSP_TRC_V_LOG:          return PL_COLOR_TRC_V_LOG;
-    case MP_CSP_TRC_S_LOG1:         return PL_COLOR_TRC_S_LOG1;
-    case MP_CSP_TRC_S_LOG2:         return PL_COLOR_TRC_S_LOG2;
-    case MP_CSP_TRC_ST428:          return PL_COLOR_TRC_ST428;
-    case MP_CSP_TRC_COUNT:          return PL_COLOR_TRC_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc)
-{
-    switch (trc){
-    case PL_COLOR_TRC_UNKNOWN: return MP_CSP_TRC_AUTO;
-    case PL_COLOR_TRC_BT_1886: return MP_CSP_TRC_BT_1886;
-    case PL_COLOR_TRC_SRGB: return MP_CSP_TRC_SRGB;
-    case PL_COLOR_TRC_LINEAR: return MP_CSP_TRC_LINEAR;
-    case PL_COLOR_TRC_GAMMA18: return MP_CSP_TRC_GAMMA18;
-    case PL_COLOR_TRC_GAMMA20: return MP_CSP_TRC_GAMMA20;
-    case PL_COLOR_TRC_GAMMA22: return MP_CSP_TRC_GAMMA22;
-    case PL_COLOR_TRC_GAMMA24: return MP_CSP_TRC_GAMMA24;
-    case PL_COLOR_TRC_GAMMA26: return MP_CSP_TRC_GAMMA26;
-    case PL_COLOR_TRC_GAMMA28: return MP_CSP_TRC_GAMMA28;
-    case PL_COLOR_TRC_PRO_PHOTO: return MP_CSP_TRC_PRO_PHOTO;
-    case PL_COLOR_TRC_PQ: return MP_CSP_TRC_PQ;
-    case PL_COLOR_TRC_HLG: return MP_CSP_TRC_HLG;
-    case PL_COLOR_TRC_V_LOG: return MP_CSP_TRC_V_LOG;
-    case PL_COLOR_TRC_S_LOG1: return MP_CSP_TRC_S_LOG1;
-    case PL_COLOR_TRC_S_LOG2: return MP_CSP_TRC_S_LOG2;
-    case PL_COLOR_TRC_ST428: return MP_CSP_TRC_ST428;
-    case PL_COLOR_TRC_COUNT: return MP_CSP_TRC_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum pl_color_system mp_csp_to_pl(enum mp_csp csp)
-{
-    switch (csp) {
-    case MP_CSP_AUTO:               return PL_COLOR_SYSTEM_UNKNOWN;
-    case MP_CSP_BT_601:             return PL_COLOR_SYSTEM_BT_601;
-    case MP_CSP_BT_709:             return PL_COLOR_SYSTEM_BT_709;
-    case MP_CSP_SMPTE_240M:         return PL_COLOR_SYSTEM_SMPTE_240M;
-    case MP_CSP_BT_2020_NC:         return PL_COLOR_SYSTEM_BT_2020_NC;
-    case MP_CSP_BT_2020_C:          return PL_COLOR_SYSTEM_BT_2020_C;
-    case MP_CSP_RGB:                return PL_COLOR_SYSTEM_RGB;
-    case MP_CSP_XYZ:                return PL_COLOR_SYSTEM_XYZ;
-    case MP_CSP_YCGCO:              return PL_COLOR_SYSTEM_YCGCO;
-    case MP_CSP_COUNT:              return PL_COLOR_SYSTEM_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels)
-{
-    switch (levels) {
-    case MP_CSP_LEVELS_AUTO:        return PL_COLOR_LEVELS_UNKNOWN;
-    case MP_CSP_LEVELS_TV:          return PL_COLOR_LEVELS_TV;
-    case MP_CSP_LEVELS_PC:          return PL_COLOR_LEVELS_PC;
-    case MP_CSP_LEVELS_COUNT:       return PL_COLOR_LEVELS_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
-enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels)
-{
-    switch (levels){
-    case PL_COLOR_LEVELS_UNKNOWN:   return MP_CSP_LEVELS_AUTO;
-    case PL_COLOR_LEVELS_TV:        return MP_CSP_LEVELS_TV;
-    case PL_COLOR_LEVELS_PC:        return MP_CSP_LEVELS_PC;
-    case PL_COLOR_LEVELS_COUNT:     return MP_CSP_LEVELS_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
 enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha)
 {
     switch (alpha) {
diff --git a/video/out/placebo/utils.h b/video/out/placebo/utils.h
index bf780a82b35b..103fa4ce2462 100644
--- a/video/out/placebo/utils.h
+++ b/video/out/placebo/utils.h
@@ -27,13 +27,6 @@ static inline struct pl_rect2d mp_rect2d_to_pl(struct mp_rect rc)
     };
 }
 
-enum pl_color_primaries mp_prim_to_pl(enum mp_csp_prim prim);
-enum mp_csp_prim mp_prim_from_pl(enum pl_color_primaries prim);
-enum pl_color_transfer mp_trc_to_pl(enum mp_csp_trc trc);
-enum mp_csp_trc mp_trc_from_pl(enum pl_color_transfer trc);
-enum pl_color_system mp_csp_to_pl(enum mp_csp csp);
-enum pl_color_levels mp_levels_to_pl(enum mp_csp_levels levels);
-enum mp_csp_levels mp_levels_from_pl(enum pl_color_levels levels);
 enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha);
 enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma);
 
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
index 60fcaedd4246..7b3b3891ba68 100644
--- a/video/out/vo_gpu_next.c
+++ b/video/out/vo_gpu_next.c
@@ -129,7 +129,7 @@ struct priv {
     struct mp_csp_equalizer_state *video_eq;
     struct scaler_params scalers[SCALER_COUNT];
     const struct pl_hook **hooks; // storage for `params.hooks`
-    enum mp_csp_levels output_levels;
+    enum pl_color_levels output_levels;
     char **raw_opts;
 
     struct pl_icc_params icc_params;
@@ -440,8 +440,8 @@ static int plane_data_from_imgfmt(struct pl_plane_data out_data[4],
 static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi)
 {
     struct pl_color_space csp = {
-        .primaries = mp_prim_to_pl(mpi->params.color.primaries),
-        .transfer = mp_trc_to_pl(mpi->params.color.gamma),
+        .primaries = mpi->params.color.primaries,
+        .transfer = mpi->params.color.transfer,
         .hdr = mpi->params.color.hdr,
     };
     return csp;
@@ -569,8 +569,8 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src
     *frame = (struct pl_frame) {
         .color = get_mpi_csp(vo, mpi),
         .repr = {
-            .sys = mp_csp_to_pl(par->color.space),
-            .levels = mp_levels_to_pl(par->color.levels),
+            .sys = par->repr.sys,
+            .levels = par->repr.levels,
             .alpha = mp_alpha_to_pl(par->alpha),
         },
         .profile = {
@@ -584,14 +584,14 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src
     // mp_image, like AVFrame, likes communicating RGB/XYZ/YCbCr status
     // implicitly via the image format, rather than the actual tagging.
     switch (mp_imgfmt_get_forced_csp(par->imgfmt)) {
-    case MP_CSP_RGB:
+    case PL_COLOR_SYSTEM_RGB:
         frame->repr.sys = PL_COLOR_SYSTEM_RGB;
         frame->repr.levels = PL_COLOR_LEVELS_FULL;
         break;
-    case MP_CSP_XYZ:
+    case PL_COLOR_SYSTEM_XYZ:
         frame->repr.sys = PL_COLOR_SYSTEM_XYZ;
         break;
-    case MP_CSP_AUTO:
+    case PL_COLOR_SYSTEM_UNKNOWN:
         if (!frame->repr.sys)
             frame->repr.sys = pl_color_system_guess_ycbcr(par->w, par->h);
         break;
@@ -779,11 +779,11 @@ static void apply_target_options(struct priv *p, struct pl_frame *target)
     // Colorspace overrides
     const struct gl_video_opts *opts = p->opts_cache->opts;
     if (p->output_levels)
-        target->repr.levels = mp_levels_to_pl(p->output_levels);
+        target->repr.levels = p->output_levels;
     if (opts->target_prim)
-        target->color.primaries = mp_prim_to_pl(opts->target_prim);
+        target->color.primaries = opts->target_prim;
     if (opts->target_trc)
-        target->color.transfer = mp_trc_to_pl(opts->target_trc);
+        target->color.transfer = opts->target_trc;
     // If swapchain returned a value use this, override is used in hint
     if (opts->target_peak && !target->color.hdr.max_luma)
         target->color.hdr.max_luma = opts->target_peak;
@@ -792,7 +792,7 @@ static void apply_target_options(struct priv *p, struct pl_frame *target)
     if (opts->target_gamut) {
         // Ensure resulting gamut still fits inside container
         const struct pl_raw_primaries *gamut, *container;
-        gamut = pl_raw_primaries_get(mp_prim_to_pl(opts->target_gamut));
+        gamut = pl_raw_primaries_get(opts->target_gamut);
         container = pl_raw_primaries_get(target->color.primaries);
         target->color.hdr.prim = pl_primaries_clip(gamut, container);
     }
@@ -912,9 +912,9 @@ static void draw_frame(struct vo *vo, struct vo_frame *frame)
     if (p->target_hint && frame->current) {
         struct pl_color_space hint = get_mpi_csp(vo, frame->current);
         if (opts->target_prim)
-            hint.primaries = mp_prim_to_pl(opts->target_prim);
+            hint.primaries = opts->target_prim;
         if (opts->target_trc)
-            hint.transfer = mp_trc_to_pl(opts->target_trc);
+            hint.transfer = opts->target_trc;
         if (opts->target_peak)
             hint.hdr.max_luma = opts->target_peak;
         apply_target_contrast(p, &hint);
@@ -1327,9 +1327,9 @@ static void video_screenshot(struct vo *vo, struct voctrl_screenshot *args)
     if (!args->res)
         goto done;
 
-    args->res->params.color.primaries = mp_prim_from_pl(target.color.primaries);
-    args->res->params.color.gamma = mp_trc_from_pl(target.color.transfer);
-    args->res->params.color.levels = mp_levels_from_pl(target.repr.levels);
+    args->res->params.color.primaries = target.color.primaries;
+    args->res->params.color.transfer = target.color.transfer;
+    args->res->params.repr.levels = target.repr.levels;
     args->res->params.color.hdr = target.color.hdr;
     if (args->scaled)
         args->res->params.p_w = args->res->params.p_h = 1;
diff --git a/video/out/vo_lavc.c b/video/out/vo_lavc.c
index 7170c1d5e70b..9008e2269910 100644
--- a/video/out/vo_lavc.c
+++ b/video/out/vo_lavc.c
@@ -23,6 +23,10 @@
 #include <stdio.h>
 #include <stdlib.h>
 
+#include <pthread.h>
+
+#include <libplacebo/utils/libav.h>
+
 #include "common/common.h"
 #include "options/options.h"
 #include "video/fmt-conversion.h"
@@ -112,8 +116,8 @@ static int reconfig2(struct vo *vo, struct mp_image *img)
     encoder->width = width;
     encoder->height = height;
     encoder->pix_fmt = pix_fmt;
-    encoder->colorspace = mp_csp_to_avcol_spc(params->color.space);
-    encoder->color_range = mp_csp_levels_to_avcol_range(params->color.levels);
+    encoder->colorspace = pl_system_to_av(params->repr.sys);
+    encoder->color_range = pl_levels_to_av(params->repr.levels);
 
     AVRational tb;
 
diff --git a/video/out/vo_rpi.c b/video/out/vo_rpi.c
index 55f1a68edb49..c53d027019aa 100644
--- a/video/out/vo_rpi.c
+++ b/video/out/vo_rpi.c
@@ -587,12 +587,12 @@ static int query_format(struct vo *vo, int format)
     return format == IMGFMT_MMAL || format == IMGFMT_420P;
 }
 
-static MMAL_FOURCC_T map_csp(enum mp_csp csp)
+static MMAL_FOURCC_T map_csp(enum pl_color_system csp)
 {
     switch (csp) {
-    case MP_CSP_BT_601:     return MMAL_COLOR_SPACE_ITUR_BT601;
-    case MP_CSP_BT_709:     return MMAL_COLOR_SPACE_ITUR_BT709;
-    case MP_CSP_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
+    case PL_COLOR_SYSTEM_BT_601:     return MMAL_COLOR_SPACE_ITUR_BT601;
+    case PL_COLOR_SYSTEM_BT_709:     return MMAL_COLOR_SPACE_ITUR_BT709;
+    case PL_COLOR_SYSTEM_SMPTE_240M: return MMAL_COLOR_SPACE_SMPTE240M;
     default:                return MMAL_COLOR_SPACE_UNKNOWN;
     }
 }
@@ -642,7 +642,7 @@ static int reconfig(struct vo *vo, struct mp_image_params *params)
     input->format->es->video.height = MP_ALIGN_UP(params->h, ALIGN_H);
     input->format->es->video.crop = (MMAL_RECT_T){0, 0, params->w, params->h};
     input->format->es->video.par = (MMAL_RATIONAL_T){params->p_w, params->p_h};
-    input->format->es->video.color_space = map_csp(params->color.space);
+    input->format->es->video.color_space = map_csp(params->repr.sys);
 
     if (mmal_port_format_commit(input))
         return -1;
diff --git a/video/out/vo_vaapi.c b/video/out/vo_vaapi.c
index 12888fefe974..2a241300f23b 100644
--- a/video/out/vo_vaapi.c
+++ b/video/out/vo_vaapi.c
@@ -519,7 +519,7 @@ static bool render_to_screen(struct priv *p, struct mp_image *mpi)
         CHECK_VA_STATUS(p, "vaAssociateSubpicture()");
     }
 
-    int flags = va_get_colorspace_flag(p->image_params.color.space) |
+    int flags = va_get_colorspace_flag(p->image_params.repr.sys) |
                 p->scaling | VA_FRAME_PICTURE;
     status = vaPutSurface(p->display,
                           surface,
diff --git a/video/out/vo_xv.c b/video/out/vo_xv.c
index 6c776c507ba1..378602b6554c 100644
--- a/video/out/vo_xv.c
+++ b/video/out/vo_xv.c
@@ -401,7 +401,7 @@ static void read_xv_csp(struct vo *vo)
     ctx->cached_csp = 0;
     int bt709_enabled;
     if (xv_get_eq(vo, ctx->xv_port, "bt_709", &bt709_enabled))
-        ctx->cached_csp = bt709_enabled == 100 ? MP_CSP_BT_709 : MP_CSP_BT_601;
+        ctx->cached_csp = bt709_enabled == 100 ? PL_COLOR_SYSTEM_BT_709 : PL_COLOR_SYSTEM_BT_601;
 }
 
 
@@ -519,7 +519,7 @@ static int reconfig(struct vo *vo, struct mp_image_params *params)
     ctx->current_buf = 0;
     ctx->current_ip_buf = 0;
 
-    int is_709 = params->color.space == MP_CSP_BT_709;
+    int is_709 = params->repr.sys == PL_COLOR_SYSTEM_BT_709;
     xv_set_eq(vo, ctx->xv_port, "bt_709", is_709 * 200 - 100);
     read_xv_csp(vo);
 
@@ -652,7 +652,7 @@ static struct mp_image get_xv_buffer(struct vo *vo, int buf_index)
     if (vo->params) {
         struct mp_image_params params = *vo->params;
         if (ctx->cached_csp)
-            params.color.space = ctx->cached_csp;
+            params.repr.sys = ctx->cached_csp;
         mp_image_set_attributes(&img, &params);
     }
 
diff --git a/video/repack.c b/video/repack.c
index ce3703ac5c32..00587fbb6b76 100644
--- a/video/repack.c
+++ b/video/repack.c
@@ -76,8 +76,8 @@ struct mp_repack {
     int f32_comp_size;
     float f32_m[4], f32_o[4];
     uint32_t f32_pmax[4];
-    enum mp_csp f32_csp_space;
-    enum mp_csp_levels f32_csp_levels;
+    enum pl_color_system f32_csp_space;
+    enum pl_color_levels f32_csp_levels;
 
     // REPACK_STEP_REPACK: if true, need to copy this plane
     bool copy_buf[4];
@@ -95,7 +95,7 @@ static int find_gbrp_format(int depth, int num_planes)
         return 0;
     struct mp_regular_imgfmt desc = {
         .component_type = MP_COMPONENT_TYPE_UINT,
-        .forced_csp = MP_CSP_RGB,
+        .forced_csp = PL_COLOR_SYSTEM_RGB,
         .component_size = depth > 8 ? 2 : 1,
         .component_pad = depth - (depth > 8 ? 16 : 8),
         .num_planes = num_planes,
@@ -467,7 +467,7 @@ static void setup_fringe_rgb_packer(struct mp_repack *rp)
         return;
 
     if (desc.bpp[0] > 16 || (desc.bpp[0] % 8u) ||
-        mp_imgfmt_get_forced_csp(rp->imgfmt_a) != MP_CSP_RGB ||
+        mp_imgfmt_get_forced_csp(rp->imgfmt_a) != PL_COLOR_SYSTEM_RGB ||
         desc.num_planes != 1 || desc.comps[3].size)
         return;
 
@@ -845,8 +845,8 @@ static void update_repack_float(struct mp_repack *rp)
     // Image in input format.
     struct mp_image *ui =  rp->pack ? rp->steps[rp->num_steps - 1].buf[1]
                                     : rp->steps[0].buf[0];
-    enum mp_csp csp = ui->params.color.space;
-    enum mp_csp_levels levels = ui->params.color.levels;
+    enum pl_color_system csp = ui->params.repr.sys;
+    enum pl_color_levels levels = ui->params.repr.levels;
     if (rp->f32_csp_space == csp && rp->f32_csp_levels == levels)
         return;
 
@@ -989,8 +989,8 @@ static bool setup_format_ne(struct mp_repack *rp)
                 (desc.component_size != 1 && desc.component_size != 2))
                 return false;
             rp->f32_comp_size = desc.component_size;
-            rp->f32_csp_space = MP_CSP_COUNT;
-            rp->f32_csp_levels = MP_CSP_LEVELS_COUNT;
+            rp->f32_csp_space = PL_COLOR_SYSTEM_COUNT;
+            rp->f32_csp_levels = PL_COLOR_LEVELS_COUNT;
             rp->steps[rp->num_steps++] = (struct repack_step) {
                 .type = REPACK_STEP_FLOAT,
                 .fmt = {
diff --git a/video/sws_utils.c b/video/sws_utils.c
index 5e9c35876ac8..9844dd62eb03 100644
--- a/video/sws_utils.c
+++ b/video/sws_utils.c
@@ -24,6 +24,7 @@
 #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
 #include <libavutil/pixdesc.h>
 #endif
+#include <libplacebo/utils/libav.h>
 
 #include "config.h"
 
@@ -156,11 +157,11 @@ bool mp_sws_supports_formats(struct mp_sws_context *ctx,
            sws_isSupportedOutput(imgfmt2pixfmt(imgfmt_out));
 }
 
-static int mp_csp_to_sws_colorspace(enum mp_csp csp)
+static int pl_csp_to_sws_colorspace(enum pl_color_system csp)
 {
     // The SWS_CS_* macros are just convenience redefinitions of the
     // AVCOL_SPC_* macros, inside swscale.h.
-    return mp_csp_to_avcol_spc(csp);
+    return pl_system_to_av(csp);
 }
 
 static bool cache_valid(struct mp_sws_context *ctx)
@@ -289,11 +290,11 @@ int mp_sws_reinit(struct mp_sws_context *ctx)
         return -1;
     }
 
-    int s_csp = mp_csp_to_sws_colorspace(src.color.space);
-    int s_range = src.color.levels == MP_CSP_LEVELS_PC;
+    int s_csp = pl_csp_to_sws_colorspace(src.repr.sys);
+    int s_range = src.repr.levels == PL_COLOR_LEVELS_FULL;
 
-    int d_csp = mp_csp_to_sws_colorspace(dst.color.space);
-    int d_range = dst.color.levels == MP_CSP_LEVELS_PC;
+    int d_csp = pl_csp_to_sws_colorspace(src.repr.sys);
+    int d_range = dst.repr.levels == PL_COLOR_LEVELS_FULL;
 
     av_opt_set_int(ctx->sws, "sws_flags", ctx->flags, 0);
 
@@ -407,11 +408,11 @@ int mp_sws_scale(struct mp_sws_context *ctx, struct mp_image *dst,
         return mp_zimg_convert(ctx->zimg, dst, src) ? 0 : -1;
 #endif
 
-    if (src->params.color.space == MP_CSP_XYZ && dst->params.color.space != MP_CSP_XYZ) {
+    if (src->params.repr.sys == PL_COLOR_SYSTEM_XYZ && dst->params.repr.sys != PL_COLOR_SYSTEM_XYZ) {
         // swsscale has hardcoded gamma 2.2 internally and 2.6 for XYZ
-        dst->params.color.gamma = MP_CSP_TRC_GAMMA22;
+        dst->params.color.transfer = PL_COLOR_TRC_GAMMA22;
         // and sRGB primaries...
-        dst->params.color.primaries = MP_CSP_PRIM_BT_709;
+        dst->params.color.primaries = PL_COLOR_PRIM_BT_709;
         // it doesn't adjust white point though, but it is not worth to support
         // this case. It would require custom prim with equal energy white point
         // and sRGB primaries.
diff --git a/video/vaapi.c b/video/vaapi.c
index 08248a73885a..942351ad19d4 100644
--- a/video/vaapi.c
+++ b/video/vaapi.c
@@ -47,12 +47,12 @@ const struct m_sub_options vaapi_conf = {
     .size = sizeof(struct vaapi_opts),
 };
 
-int va_get_colorspace_flag(enum mp_csp csp)
+int va_get_colorspace_flag(enum pl_color_system csp)
 {
     switch (csp) {
-    case MP_CSP_BT_601:         return VA_SRC_BT601;
-    case MP_CSP_BT_709:         return VA_SRC_BT709;
-    case MP_CSP_SMPTE_240M:     return VA_SRC_SMPTE_240;
+    case PL_COLOR_SYSTEM_BT_601:         return VA_SRC_BT601;
+    case PL_COLOR_SYSTEM_BT_709:         return VA_SRC_BT709;
+    case PL_COLOR_SYSTEM_SMPTE_240M:     return VA_SRC_SMPTE_240;
     }
     return 0;
 }
diff --git a/video/vaapi.h b/video/vaapi.h
index 56235bc05be8..3fdf3c1bf0cb 100644
--- a/video/vaapi.h
+++ b/video/vaapi.h
@@ -42,7 +42,7 @@ struct mp_vaapi_ctx {
 #define CHECK_VA_STATUS(ctx, msg) \
     CHECK_VA_STATUS_LEVEL(ctx, msg, MSGL_ERR)
 
-int                      va_get_colorspace_flag(enum mp_csp csp);
+int                      va_get_colorspace_flag(enum pl_color_system csp);
 
 struct mp_vaapi_ctx *    va_initialize(VADisplay *display, struct mp_log *plog, bool probing);
 void                     va_destroy(struct mp_vaapi_ctx *ctx);
diff --git a/video/zimg.c b/video/zimg.c
index 5ff300caab18..f744fe2c4b4f 100644
--- a/video/zimg.c
+++ b/video/zimg.c
@@ -131,66 +131,66 @@ static zimg_chroma_location_e mp_to_z_chroma(enum mp_chroma_location cl)
     }
 }
 
-static zimg_matrix_coefficients_e mp_to_z_matrix(enum mp_csp csp)
+static zimg_matrix_coefficients_e pl_to_z_matrix(enum pl_color_system csp)
 {
     switch (csp) {
-    case MP_CSP_BT_601:         return ZIMG_MATRIX_BT470_BG;
-    case MP_CSP_BT_709:         return ZIMG_MATRIX_BT709;
-    case MP_CSP_SMPTE_240M:     return ZIMG_MATRIX_ST240_M;
-    case MP_CSP_BT_2020_NC:     return ZIMG_MATRIX_BT2020_NCL;
-    case MP_CSP_BT_2020_C:      return ZIMG_MATRIX_BT2020_CL;
-    case MP_CSP_RGB:            return ZIMG_MATRIX_RGB;
-    case MP_CSP_XYZ:            return ZIMG_MATRIX_RGB;
-    case MP_CSP_YCGCO:          return ZIMG_MATRIX_YCGCO;
+    case PL_COLOR_SYSTEM_BT_601:         return ZIMG_MATRIX_BT470_BG;
+    case PL_COLOR_SYSTEM_BT_709:         return ZIMG_MATRIX_BT709;
+    case PL_COLOR_SYSTEM_SMPTE_240M:     return ZIMG_MATRIX_ST240_M;
+    case PL_COLOR_SYSTEM_BT_2020_NC:     return ZIMG_MATRIX_BT2020_NCL;
+    case PL_COLOR_SYSTEM_BT_2020_C:      return ZIMG_MATRIX_BT2020_CL;
+    case PL_COLOR_SYSTEM_RGB:            return ZIMG_MATRIX_RGB;
+    case PL_COLOR_SYSTEM_XYZ:            return ZIMG_MATRIX_RGB;
+    case PL_COLOR_SYSTEM_YCGCO:          return ZIMG_MATRIX_YCGCO;
     default:                    return ZIMG_MATRIX_BT709;
     }
 }
 
-static zimg_transfer_characteristics_e mp_to_z_trc(enum mp_csp_trc trc)
+static zimg_transfer_characteristics_e pl_to_z_trc(enum pl_color_transfer trc)
 {
     switch (trc) {
-    case MP_CSP_TRC_BT_1886:    return ZIMG_TRANSFER_BT709;
-    case MP_CSP_TRC_SRGB:       return ZIMG_TRANSFER_IEC_61966_2_1;
-    case MP_CSP_TRC_LINEAR:     return ZIMG_TRANSFER_LINEAR;
-    case MP_CSP_TRC_GAMMA22:    return ZIMG_TRANSFER_BT470_M;
-    case MP_CSP_TRC_GAMMA28:    return ZIMG_TRANSFER_BT470_BG;
-    case MP_CSP_TRC_PQ:         return ZIMG_TRANSFER_ST2084;
-    case MP_CSP_TRC_HLG:        return ZIMG_TRANSFER_ARIB_B67;
+    case PL_COLOR_TRC_BT_1886:    return ZIMG_TRANSFER_BT709;
+    case PL_COLOR_TRC_SRGB:       return ZIMG_TRANSFER_IEC_61966_2_1;
+    case PL_COLOR_TRC_LINEAR:     return ZIMG_TRANSFER_LINEAR;
+    case PL_COLOR_TRC_GAMMA22:    return ZIMG_TRANSFER_BT470_M;
+    case PL_COLOR_TRC_GAMMA28:    return ZIMG_TRANSFER_BT470_BG;
+    case PL_COLOR_TRC_PQ:         return ZIMG_TRANSFER_ST2084;
+    case PL_COLOR_TRC_HLG:        return ZIMG_TRANSFER_ARIB_B67;
 #if HAVE_ZIMG_ST428
-    case MP_CSP_TRC_ST428:      return ZIMG_TRANSFER_ST428;
+    case PL_COLOR_TRC_ST428:      return ZIMG_TRANSFER_ST428;
 #endif
-    case MP_CSP_TRC_GAMMA18:    // ?
-    case MP_CSP_TRC_GAMMA20:
-    case MP_CSP_TRC_GAMMA24:
-    case MP_CSP_TRC_GAMMA26:
-    case MP_CSP_TRC_PRO_PHOTO:
-    case MP_CSP_TRC_V_LOG:
-    case MP_CSP_TRC_S_LOG1:
-    case MP_CSP_TRC_S_LOG2:     // ?
+    case PL_COLOR_TRC_GAMMA18:    // ?
+    case PL_COLOR_TRC_GAMMA20:
+    case PL_COLOR_TRC_GAMMA24:
+    case PL_COLOR_TRC_GAMMA26:
+    case PL_COLOR_TRC_PRO_PHOTO:
+    case PL_COLOR_TRC_V_LOG:
+    case PL_COLOR_TRC_S_LOG1:
+    case PL_COLOR_TRC_S_LOG2:     // ?
     default:                    return ZIMG_TRANSFER_BT709;
     }
 }
 
-static zimg_color_primaries_e mp_to_z_prim(enum mp_csp_prim prim)
+static zimg_color_primaries_e mp_to_z_prim(enum pl_color_primaries prim)
 {
     switch (prim) {
-    case MP_CSP_PRIM_BT_601_525:return ZIMG_PRIMARIES_ST170_M;
-    case MP_CSP_PRIM_BT_601_625:return ZIMG_PRIMARIES_BT470_BG;
-    case MP_CSP_PRIM_BT_709:    return ZIMG_PRIMARIES_BT709;
-    case MP_CSP_PRIM_BT_2020:   return ZIMG_PRIMARIES_BT2020;
-    case MP_CSP_PRIM_BT_470M:   return ZIMG_PRIMARIES_BT470_M;
-    case MP_CSP_PRIM_DCI_P3:    return ZIMG_PRIMARIES_ST431_2;
-    case MP_CSP_PRIM_DISPLAY_P3:return ZIMG_PRIMARIES_ST432_1;
-    case MP_CSP_PRIM_EBU_3213:  return ZIMG_PRIMARIES_EBU3213_E;
-    case MP_CSP_PRIM_FILM_C:    return ZIMG_PRIMARIES_FILM;
-    case MP_CSP_PRIM_CIE_1931:
-    case MP_CSP_PRIM_APPLE:     // ?
-    case MP_CSP_PRIM_ADOBE:
-    case MP_CSP_PRIM_PRO_PHOTO:
-    case MP_CSP_PRIM_V_GAMUT:
-    case MP_CSP_PRIM_S_GAMUT:   // ?
-    case MP_CSP_PRIM_ACES_AP0:
-    case MP_CSP_PRIM_ACES_AP1:
+    case PL_COLOR_PRIM_BT_601_525:return ZIMG_PRIMARIES_ST170_M;
+    case PL_COLOR_PRIM_BT_601_625:return ZIMG_PRIMARIES_BT470_BG;
+    case PL_COLOR_PRIM_BT_709:    return ZIMG_PRIMARIES_BT709;
+    case PL_COLOR_PRIM_BT_2020:   return ZIMG_PRIMARIES_BT2020;
+    case PL_COLOR_PRIM_BT_470M:   return ZIMG_PRIMARIES_BT470_M;
+    case PL_COLOR_PRIM_DCI_P3:    return ZIMG_PRIMARIES_ST431_2;
+    case PL_COLOR_PRIM_DISPLAY_P3:return ZIMG_PRIMARIES_ST432_1;
+    case PL_COLOR_PRIM_EBU_3213:  return ZIMG_PRIMARIES_EBU3213_E;
+    case PL_COLOR_PRIM_FILM_C:    return ZIMG_PRIMARIES_FILM;
+    case PL_COLOR_PRIM_CIE_1931:
+    case PL_COLOR_PRIM_APPLE:     // ?
+    case PL_COLOR_PRIM_ADOBE:
+    case PL_COLOR_PRIM_PRO_PHOTO:
+    case PL_COLOR_PRIM_V_GAMUT:
+    case PL_COLOR_PRIM_S_GAMUT:   // ?
+    case PL_COLOR_PRIM_ACES_AP0:
+    case PL_COLOR_PRIM_ACES_AP1:
     default:                    return ZIMG_PRIMARIES_BT709;
     }
 }
@@ -414,7 +414,7 @@ static bool setup_format(zimg_image_format *zfmt, struct mp_zimg_repack *r,
     zfmt->color_family = ZIMG_COLOR_YUV;
     if (desc.num_planes <= 2) {
         zfmt->color_family = ZIMG_COLOR_GREY;
-    } else if (fmt.color.space == MP_CSP_RGB || fmt.color.space == MP_CSP_XYZ) {
+    } else if (fmt.repr.sys == PL_COLOR_SYSTEM_RGB || fmt.repr.sys == PL_COLOR_SYSTEM_XYZ) {
         zfmt->color_family = ZIMG_COLOR_RGB;
     }
 
@@ -441,13 +441,13 @@ static bool setup_format(zimg_image_format *zfmt, struct mp_zimg_repack *r,
     // (Formats like P010 are basically reported as P016.)
     zfmt->depth = desc.component_size * 8 + MPMIN(0, desc.component_pad);
 
-    zfmt->pixel_range = fmt.color.levels == MP_CSP_LEVELS_PC ?
+    zfmt->pixel_range = fmt.repr.levels == PL_COLOR_LEVELS_FULL ?
                         ZIMG_RANGE_FULL : ZIMG_RANGE_LIMITED;
 
-    zfmt->matrix_coefficients = mp_to_z_matrix(fmt.color.space);
-    zfmt->transfer_characteristics = mp_to_z_trc(fmt.color.gamma);
-    // For MP_CSP_XYZ only valid primaries are defined in ST 428-1
-    zfmt->color_primaries = fmt.color.space == MP_CSP_XYZ
+    zfmt->matrix_coefficients = pl_to_z_matrix(fmt.repr.sys);
+    zfmt->transfer_characteristics = pl_to_z_trc(fmt.color.transfer);
+    // For PL_COLOR_SYSTEM_XYZ only valid primaries are defined in ST 428-1
+    zfmt->color_primaries = fmt.repr.sys == PL_COLOR_SYSTEM_XYZ
                                 ? ZIMG_PRIMARIES_ST428
                                 : mp_to_z_prim(fmt.color.primaries);
     zfmt->chroma_location = mp_to_z_chroma(fmt.chroma_location);
@@ -548,7 +548,7 @@ static bool mp_zimg_state_init(struct mp_zimg_context *ctx,
         params.allow_approximate_gamma = 1;
 
     // leave at default for SDR, which means 100 cd/m^2 for zimg
-    if (ctx->dst.color.hdr.max_luma > 0 && mp_trc_is_hdr(ctx->dst.color.gamma))
+    if (ctx->dst.color.hdr.max_luma > 0 && mp_trc_is_hdr(ctx->dst.color.transfer))
         params.nominal_peak_luminance = ctx->dst.color.hdr.max_luma;
 
     st->graph = zimg_filter_graph_build(&src_fmt, &dst_fmt, &params);

From 2e5f080f2f89f63a4a02f5fa87384cda1a3a6b24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <kasper93@gmail.com>
Date: Sat, 4 Nov 2023 04:54:51 +0100
Subject: [PATCH 2/5] csputils: replace mp_alpha_type with pl_alpha_mode

---
 player/command.c          | 12 ++++++------
 sub/draw_bmp.c            | 12 ++++++------
 sub/draw_bmp.h            |  2 +-
 video/csputils.c          |  8 ++++----
 video/csputils.h          |  9 +--------
 video/filter/vf_format.c  |  4 ++--
 video/mp_image.c          |  8 +++-----
 video/mp_image.h          |  1 -
 video/out/gpu/video.c     |  2 +-
 video/out/placebo/utils.c | 11 -----------
 video/out/placebo/utils.h |  1 -
 video/out/vo_gpu_next.c   |  2 +-
 video/zimg.c              |  2 +-
 13 files changed, 26 insertions(+), 48 deletions(-)

diff --git a/player/command.c b/player/command.c
index 33d30508e79f..396ae651159f 100644
--- a/player/command.c
+++ b/player/command.c
@@ -2285,11 +2285,11 @@ static int property_imgparams(struct mp_image_params p, int action, void *arg)
     for (int i = 0; i < desc.num_planes; i++)
         bpp += desc.bpp[i] >> (desc.xs[i] + desc.ys[i]);
 
-    // Alpha type is not supported by FFmpeg, so MP_ALPHA_AUTO may mean alpha
+    // Alpha type is not supported by FFmpeg, so PL_ALPHA_UNKNOWN may mean alpha
     // is of an unknown type, or simply not present. Normalize to AUTO=no alpha.
-    if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (p.alpha != MP_ALPHA_AUTO)) {
-        p.alpha =
-            (desc.flags & MP_IMGFLAG_ALPHA) ? MP_ALPHA_STRAIGHT : MP_ALPHA_AUTO;
+    if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (p.repr.alpha != PL_ALPHA_UNKNOWN)) {
+        p.repr.alpha =
+            (desc.flags & MP_IMGFLAG_ALPHA) ? PL_ALPHA_INDEPENDENT : PL_ALPHA_UNKNOWN;
     }
 
     const struct pl_hdr_metadata *hdr = &p.color.hdr;
@@ -2333,9 +2333,9 @@ static int property_imgparams(struct mp_image_params p, int action, void *arg)
             SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p.stereo3d))},
         {"rotate",          SUB_PROP_INT(p.rotate)},
         {"alpha",
-            SUB_PROP_STR(m_opt_choice_str(mp_alpha_names, p.alpha)),
+            SUB_PROP_STR(m_opt_choice_str(pl_alpha_names, p.repr.alpha)),
             // avoid using "auto" for "no", so just make it unavailable
-            .unavailable = p.alpha == MP_ALPHA_AUTO},
+            .unavailable = p.repr.alpha == PL_ALPHA_UNKNOWN},
         {"min-luma",    SUB_PROP_FLOAT(hdr->min_luma),     .unavailable = !has_hdr10},
         {"max-luma",    SUB_PROP_FLOAT(hdr->max_luma),     .unavailable = !has_hdr10},
         {"max-cll",     SUB_PROP_FLOAT(hdr->max_cll),      .unavailable = !has_hdr10},
diff --git a/sub/draw_bmp.c b/sub/draw_bmp.c
index cde8f503f334..cf214401a81b 100644
--- a/sub/draw_bmp.c
+++ b/sub/draw_bmp.c
@@ -431,7 +431,7 @@ static bool render_rgba(struct mp_draw_sub_cache *p, struct part *part,
                 mp_image_set_size(&src_img, sw, sh);
                 src_img.planes[0] = s_ptr;
                 src_img.stride[0] = s_stride;
-                src_img.params.alpha = MP_ALPHA_PREMUL;
+                src_img.params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
                 scaled = mp_image_alloc(IMGFMT_BGRA, dw, dh);
                 if (!scaled)
@@ -525,7 +525,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
     struct mp_image_params *params = &p->params;
     mp_image_params_guess_csp(params);
 
-    bool need_premul = params->alpha != MP_ALPHA_PREMUL &&
+    bool need_premul = params->repr.alpha != PL_ALPHA_PREMULTIPLIED &&
         (mp_imgfmt_get_desc(params->imgfmt).flags & MP_IMGFLAG_ALPHA);
 
     // Intermediate format for video_overlay. Requirements:
@@ -660,7 +660,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
         return false;
 
     mp_image_params_guess_csp(&p->rgba_overlay->params);
-    p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+    p->rgba_overlay->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
     p->overlay_tmp->params.color = params->color;
     p->video_tmp->params.color = params->color;
@@ -677,7 +677,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
 
         p->video_overlay->params.color = params->color;
         p->video_overlay->params.chroma_location = params->chroma_location;
-        p->video_overlay->params.alpha = MP_ALPHA_PREMUL;
+        p->video_overlay->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
         if (p->scale_in_tiles)
             p->video_overlay->params.chroma_location = MP_CHROMA_CENTER;
@@ -762,7 +762,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
         if (!p->premul_tmp)
             return false;
         mp_image_set_params(p->premul_tmp, params);
-        p->premul_tmp->params.alpha = MP_ALPHA_PREMUL;
+        p->premul_tmp->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
         // Only zimg supports this.
         p->premul->force_scaler = MP_SWS_ZIMG;
@@ -787,7 +787,7 @@ static bool reinit_to_overlay(struct mp_draw_sub_cache *p)
         return false;
 
     mp_image_params_guess_csp(&p->rgba_overlay->params);
-    p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+    p->rgba_overlay->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
     // Some non-sense with the intention to somewhat isolate the returned image.
     mp_image_setfmt(&p->res_overlay, p->rgba_overlay->imgfmt);
diff --git a/sub/draw_bmp.h b/sub/draw_bmp.h
index fda7797c9eab..b4c737829039 100644
--- a/sub/draw_bmp.h
+++ b/sub/draw_bmp.h
@@ -15,7 +15,7 @@ struct mp_draw_sub_cache *mp_draw_sub_alloc_test(struct mp_image *dst);
 
 // Render the sub-bitmaps in sbs_list to dst. sbs_list must have been rendered
 // for an OSD resolution equivalent to dst's size (UB if not).
-// Warning: if dst is a format with alpha, and dst is not set to MP_ALPHA_PREMUL
+// Warning: if dst is a format with alpha, and dst is not set to PL_ALPHA_PREMULTIPLIED
 //          (not done by default), this will be extremely slow.
 // Warning: the caller is responsible for ensuring that dst is writable.
 //  cache: allocated instance; caches non-changing OSD parts etc.
diff --git a/video/csputils.c b/video/csputils.c
index 3b43ba9f5b69..e1b7823b3387 100644
--- a/video/csputils.c
+++ b/video/csputils.c
@@ -112,10 +112,10 @@ const struct m_opt_choice_alternatives mp_chroma_names[] = {
     {0}
 };
 
-const struct m_opt_choice_alternatives mp_alpha_names[] = {
-    {"auto",        MP_ALPHA_AUTO},
-    {"straight",    MP_ALPHA_STRAIGHT},
-    {"premul",      MP_ALPHA_PREMUL},
+const struct m_opt_choice_alternatives pl_alpha_names[] = {
+    {"auto",        PL_ALPHA_UNKNOWN},
+    {"straight",    PL_ALPHA_INDEPENDENT},
+    {"premul",      PL_ALPHA_PREMULTIPLIED},
     {0}
 };
 
diff --git a/video/csputils.h b/video/csputils.h
index 80901659cf3c..ac37e205abf1 100644
--- a/video/csputils.h
+++ b/video/csputils.h
@@ -120,14 +120,7 @@ enum mp_chroma_location {
 };
 
 extern const struct m_opt_choice_alternatives mp_chroma_names[];
-
-enum mp_alpha_type {
-    MP_ALPHA_AUTO,
-    MP_ALPHA_STRAIGHT,
-    MP_ALPHA_PREMUL,
-};
-
-extern const struct m_opt_choice_alternatives mp_alpha_names[];
+extern const struct m_opt_choice_alternatives pl_alpha_names[];
 
 extern const struct m_sub_options mp_csp_equalizer_conf;
 
diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c
index 4a14fc91024d..6a3e9fd390b4 100644
--- a/video/filter/vf_format.c
+++ b/video/filter/vf_format.c
@@ -92,7 +92,7 @@ static void set_params(struct vf_format_opts *p, struct mp_image_params *out,
     if (p->rotate >= 0)
         out->rotate = p->rotate;
     if (p->alpha)
-        out->alpha = p->alpha;
+        out->repr.alpha = p->alpha;
 
     if (p->w > 0 && set_size)
         out->w = p->w;
@@ -213,7 +213,7 @@ static const m_option_t vf_opts_fields[] = {
     {"chroma-location", OPT_CHOICE_C(chroma_location, mp_chroma_names)},
     {"stereo-in", OPT_CHOICE_C(stereo_in, mp_stereo3d_names)},
     {"rotate", OPT_INT(rotate), M_RANGE(-1, 359)},
-    {"alpha", OPT_CHOICE_C(alpha, mp_alpha_names)},
+    {"alpha", OPT_CHOICE_C(alpha, pl_alpha_names)},
     {"w", OPT_INT(w)},
     {"h", OPT_INT(h)},
     {"dw", OPT_INT(dw)},
diff --git a/video/mp_image.c b/video/mp_image.c
index ed7543b8ff76..62bcef7d7ed7 100644
--- a/video/mp_image.c
+++ b/video/mp_image.c
@@ -525,7 +525,6 @@ void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src)
     dst->params.repr = src->params.repr;
     dst->params.light = src->params.light;
     dst->params.chroma_location = src->params.chroma_location;
-    dst->params.alpha = src->params.alpha;
     dst->params.crop = src->params.crop;
     dst->nominal_fps = src->nominal_fps;
 
@@ -792,9 +791,9 @@ char *mp_image_params_to_str_buf(char *b, size_t bs,
             mp_snprintf_cat(b, bs, " stereo=%s",
                             MP_STEREO3D_NAME_DEF(p->stereo3d, "?"));
         }
-        if (p->alpha) {
+        if (p->repr.alpha) {
             mp_snprintf_cat(b, bs, " A=%s",
-                            m_opt_choice_str(mp_alpha_names, p->alpha));
+                            m_opt_choice_str(pl_alpha_names, p->repr.alpha));
         }
     } else {
         snprintf(b, bs, "???");
@@ -844,7 +843,6 @@ bool mp_image_params_equal(const struct mp_image_params *p1,
            p1->chroma_location == p2->chroma_location &&
            p1->rotate == p2->rotate &&
            p1->stereo3d == p2->stereo3d &&
-           p1->alpha == p2->alpha &&
            mp_rect_equals(&p1->crop, &p2->crop);
 }
 
@@ -1054,7 +1052,7 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src)
         dst->params.stereo3d = p->stereo3d;
         // Might be incorrect if colorspace changes.
         dst->params.light = p->light;
-        dst->params.alpha = p->alpha;
+        dst->params.repr.alpha = p->repr.alpha;
     }
 
     sd = av_frame_get_side_data(src, AV_FRAME_DATA_DISPLAYMATRIX);
diff --git a/video/mp_image.h b/video/mp_image.h
index c1ba89ee0ac7..443488c9e1a3 100644
--- a/video/mp_image.h
+++ b/video/mp_image.h
@@ -54,7 +54,6 @@ struct mp_image_params {
     // The image should be rotated clockwise (0-359 degrees).
     int rotate;
     enum mp_stereo3d_mode stereo3d; // image is encoded with this mode
-    enum mp_alpha_type alpha;   // usually auto; only set if explicitly known
     struct mp_rect crop;        // crop applied on image
 };
 
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
index 62388d65c32b..6683da688c89 100644
--- a/video/out/gpu/video.c
+++ b/video/out/gpu/video.c
@@ -2396,7 +2396,7 @@ static void pass_convert_yuv(struct gl_video *p)
     p->components = 3;
     if (!p->has_alpha || p->opts.alpha_mode == ALPHA_NO) {
         GLSL(color.a = 1.0;)
-    } else if (p->image_params.alpha == MP_ALPHA_PREMUL) {
+    } else if (p->image_params.repr.alpha == PL_ALPHA_PREMULTIPLIED) {
         p->components = 4;
     } else {
         p->components = 4;
diff --git a/video/out/placebo/utils.c b/video/out/placebo/utils.c
index 7bb96dd20af6..3d8c827d617f 100644
--- a/video/out/placebo/utils.c
+++ b/video/out/placebo/utils.c
@@ -66,17 +66,6 @@ void mppl_log_set_probing(pl_log log, bool probing)
     pl_log_update(log, &params);
 }
 
-enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha)
-{
-    switch (alpha) {
-    case MP_ALPHA_AUTO:             return PL_ALPHA_UNKNOWN;
-    case MP_ALPHA_STRAIGHT:         return PL_ALPHA_INDEPENDENT;
-    case MP_ALPHA_PREMUL:           return PL_ALPHA_PREMULTIPLIED;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
 enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma)
 {
     switch (chroma) {
diff --git a/video/out/placebo/utils.h b/video/out/placebo/utils.h
index 103fa4ce2462..84b2e0468ecd 100644
--- a/video/out/placebo/utils.h
+++ b/video/out/placebo/utils.h
@@ -27,7 +27,6 @@ static inline struct pl_rect2d mp_rect2d_to_pl(struct mp_rect rc)
     };
 }
 
-enum pl_alpha_mode mp_alpha_to_pl(enum mp_alpha_type alpha);
 enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma);
 
 void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
index 7b3b3891ba68..5488d85ee33a 100644
--- a/video/out/vo_gpu_next.c
+++ b/video/out/vo_gpu_next.c
@@ -571,7 +571,7 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src
         .repr = {
             .sys = par->repr.sys,
             .levels = par->repr.levels,
-            .alpha = mp_alpha_to_pl(par->alpha),
+            .alpha = par->repr.alpha,
         },
         .profile = {
             .data = mpi->icc_profile ? mpi->icc_profile->data : NULL,
diff --git a/video/zimg.c b/video/zimg.c
index f744fe2c4b4f..4ea32af83a88 100644
--- a/video/zimg.c
+++ b/video/zimg.c
@@ -375,7 +375,7 @@ static bool setup_format(zimg_image_format *zfmt, struct mp_zimg_repack *r,
             r->z_planes[3] = n; // alpha, always plane 4 in zimg
 
 #if HAVE_ZIMG_ALPHA
-            zfmt->alpha = fmt.alpha == MP_ALPHA_PREMUL
+            zfmt->alpha = fmt.repr.alpha == PL_ALPHA_PREMULTIPLIED
                 ? ZIMG_ALPHA_PREMULTIPLIED : ZIMG_ALPHA_STRAIGHT;
 #else
             return false;

From 1797d74280eae7baaa1e6ec842932538aa93c893 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <kasper93@gmail.com>
Date: Sat, 4 Nov 2023 05:15:27 +0100
Subject: [PATCH 3/5] csputils: replace mp_chroma_location with
 pl_chroma_location

---
 player/command.c              |  2 +-
 sub/draw_bmp.c                |  2 +-
 video/csputils.c              | 45 +++++++----------------------------
 video/csputils.h              | 14 +----------
 video/filter/vf_format.c      |  2 +-
 video/filter/vf_vapoursynth.c |  2 +-
 video/image_writer.c          |  4 ++--
 video/mp_image.c              | 12 +++++-----
 video/mp_image.h              |  2 +-
 video/out/gpu/video.c         | 10 ++++----
 video/out/placebo/utils.c     | 13 ----------
 video/out/placebo/utils.h     |  2 --
 video/out/vo_gpu_next.c       |  2 +-
 video/sws_utils.c             |  4 ++--
 video/zimg.c                  | 15 +++++++-----
 15 files changed, 39 insertions(+), 92 deletions(-)

diff --git a/player/command.c b/player/command.c
index 396ae651159f..722e0cb1675e 100644
--- a/player/command.c
+++ b/player/command.c
@@ -2328,7 +2328,7 @@ static int property_imgparams(struct mp_image_params p, int action, void *arg)
         {"light",
             SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.light))},
         {"chroma-location",
-            SUB_PROP_STR(m_opt_choice_str(mp_chroma_names, p.chroma_location))},
+            SUB_PROP_STR(m_opt_choice_str(pl_chroma_names, p.chroma_location))},
         {"stereo-in",
             SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p.stereo3d))},
         {"rotate",          SUB_PROP_INT(p.rotate)},
diff --git a/sub/draw_bmp.c b/sub/draw_bmp.c
index cf214401a81b..90529221157a 100644
--- a/sub/draw_bmp.c
+++ b/sub/draw_bmp.c
@@ -680,7 +680,7 @@ static bool reinit_to_video(struct mp_draw_sub_cache *p)
         p->video_overlay->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
 
         if (p->scale_in_tiles)
-            p->video_overlay->params.chroma_location = MP_CHROMA_CENTER;
+            p->video_overlay->params.chroma_location = PL_CHROMA_CENTER;
 
         p->rgba_to_overlay = alloc_scaler(p);
         p->rgba_to_overlay->allow_zimg = true;
diff --git a/video/csputils.c b/video/csputils.c
index e1b7823b3387..6b708f7525c9 100644
--- a/video/csputils.c
+++ b/video/csputils.c
@@ -104,11 +104,14 @@ const struct m_opt_choice_alternatives mp_csp_light_names[] = {
     {0}
 };
 
-const struct m_opt_choice_alternatives mp_chroma_names[] = {
-    {"unknown",     MP_CHROMA_AUTO},
-    {"uhd",         MP_CHROMA_TOPLEFT},
-    {"mpeg2/4/h264",MP_CHROMA_LEFT},
-    {"mpeg1/jpeg",  MP_CHROMA_CENTER},
+const struct m_opt_choice_alternatives pl_chroma_names[] = {
+    {"unknown",     PL_CHROMA_UNKNOWN},
+    {"uhd",         PL_CHROMA_TOP_LEFT},
+    {"mpeg2/4/h264",PL_CHROMA_LEFT},
+    {"mpeg1/jpeg",  PL_CHROMA_CENTER},
+    {"top",         PL_CHROMA_TOP_CENTER},
+    {"bottom left", PL_CHROMA_BOTTOM_LEFT},
+    {"bottom",      PL_CHROMA_BOTTOM_CENTER},
     {0}
 };
 
@@ -167,38 +170,6 @@ enum pl_color_primaries mp_csp_guess_primaries(int width, int height)
     }
 }
 
-enum mp_chroma_location avchroma_location_to_mp(int avloc)
-{
-    switch (avloc) {
-    case AVCHROMA_LOC_TOPLEFT:          return MP_CHROMA_TOPLEFT;
-    case AVCHROMA_LOC_LEFT:             return MP_CHROMA_LEFT;
-    case AVCHROMA_LOC_CENTER:           return MP_CHROMA_CENTER;
-    default:                            return MP_CHROMA_AUTO;
-    }
-}
-
-int mp_chroma_location_to_av(enum mp_chroma_location mploc)
-{
-    switch (mploc) {
-    case MP_CHROMA_TOPLEFT:             return AVCHROMA_LOC_TOPLEFT;
-    case MP_CHROMA_LEFT:                return AVCHROMA_LOC_LEFT;
-    case MP_CHROMA_CENTER:              return AVCHROMA_LOC_CENTER;
-    default:                            return AVCHROMA_LOC_UNSPECIFIED;
-    }
-}
-
-// Return location of chroma samples relative to luma samples. 0/0 means
-// centered. Other possible values are -1 (top/left) and +1 (right/bottom).
-void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y)
-{
-    *x = 0;
-    *y = 0;
-    if (loc == MP_CHROMA_LEFT || loc == MP_CHROMA_TOPLEFT)
-        *x = -1;
-    if (loc == MP_CHROMA_TOPLEFT)
-        *y = -1;
-}
-
 void mp_invert_matrix3x3(float m[3][3])
 {
     float m00 = m[0][0], m01 = m[0][1], m02 = m[0][2],
diff --git a/video/csputils.h b/video/csputils.h
index ac37e205abf1..9b2d0378df96 100644
--- a/video/csputils.h
+++ b/video/csputils.h
@@ -111,15 +111,7 @@ struct mp_image_params;
 void mp_csp_set_image_params(struct mp_csp_params *params,
                              const struct mp_image_params *imgparams);
 
-enum mp_chroma_location {
-    MP_CHROMA_AUTO,
-    MP_CHROMA_TOPLEFT,  // uhd
-    MP_CHROMA_LEFT,     // mpeg2/4, h264
-    MP_CHROMA_CENTER,   // mpeg1, jpeg
-    MP_CHROMA_COUNT,
-};
-
-extern const struct m_opt_choice_alternatives mp_chroma_names[];
+extern const struct m_opt_choice_alternatives pl_chroma_names[];
 extern const struct m_opt_choice_alternatives pl_alpha_names[];
 
 extern const struct m_sub_options mp_csp_equalizer_conf;
@@ -150,10 +142,6 @@ struct mp_csp_primaries {
 enum pl_color_system mp_csp_guess_colorspace(int width, int height);
 enum pl_color_primaries mp_csp_guess_primaries(int width, int height);
 
-enum mp_chroma_location avchroma_location_to_mp(int avloc);
-int mp_chroma_location_to_av(enum mp_chroma_location mploc);
-void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y);
-
 struct mp_csp_primaries mp_get_csp_primaries(enum pl_color_primaries csp);
 float mp_trc_nom_peak(enum pl_color_transfer trc);
 bool mp_trc_is_hdr(enum pl_color_transfer trc);
diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c
index 6a3e9fd390b4..206304586415 100644
--- a/video/filter/vf_format.c
+++ b/video/filter/vf_format.c
@@ -210,7 +210,7 @@ static const m_option_t vf_opts_fields[] = {
     {"gamma", OPT_CHOICE_C(gamma, pl_csp_trc_names)},
     {"sig-peak", OPT_FLOAT(sig_peak)},
     {"light", OPT_CHOICE_C(light, mp_csp_light_names)},
-    {"chroma-location", OPT_CHOICE_C(chroma_location, mp_chroma_names)},
+    {"chroma-location", OPT_CHOICE_C(chroma_location, pl_chroma_names)},
     {"stereo-in", OPT_CHOICE_C(stereo_in, mp_stereo3d_names)},
     {"rotate", OPT_INT(rotate), M_RANGE(-1, 359)},
     {"alpha", OPT_CHOICE_C(alpha, pl_alpha_names)},
diff --git a/video/filter/vf_vapoursynth.c b/video/filter/vf_vapoursynth.c
index 5a3ce423d1d7..0b798c8dafe6 100644
--- a/video/filter/vf_vapoursynth.c
+++ b/video/filter/vf_vapoursynth.c
@@ -194,7 +194,7 @@ static void copy_mp_to_vs_frame_props_map(struct priv *p, VSMap *map,
             pl_system_to_av(params->repr.sys), 0);
     if (params->chroma_location) {
         p->vsapi->propSetInt(map, "_ChromaLocation",
-                params->chroma_location == MP_CHROMA_CENTER, 0);
+                params->chroma_location == PL_CHROMA_CENTER, 0);
     }
     char pict_type = 0;
     switch (img->pict_type) {
diff --git a/video/image_writer.c b/video/image_writer.c
index df43830d7900..fc07302d64cf 100644
--- a/video/image_writer.c
+++ b/video/image_writer.c
@@ -149,7 +149,7 @@ static void prepare_avframe(AVFrame *pic, AVCodecContext *avctx,
     avctx->colorspace = pic->colorspace =
         pl_system_to_av(image->params.repr.sys);
     avctx->chroma_sample_location = pic->chroma_location =
-        mp_chroma_location_to_av(image->params.chroma_location);
+        pl_chroma_to_av(image->params.chroma_location);
     mp_dbg(log, "mapped color params:\n"
         "  trc = %s\n"
         "  primaries = %s\n"
@@ -644,7 +644,7 @@ static struct mp_image *convert_image(struct mp_image *image, int destfmt,
         if (p.repr.sys != PL_COLOR_SYSTEM_RGB) {
             p.repr.levels = yuv_levels;
             p.repr.sys = PL_COLOR_SYSTEM_BT_601;
-            p.chroma_location = MP_CHROMA_CENTER;
+            p.chroma_location = PL_CHROMA_CENTER;
         }
         mp_image_params_guess_csp(&p);
     }
diff --git a/video/mp_image.c b/video/mp_image.c
index 62bcef7d7ed7..8fbcfea77da8 100644
--- a/video/mp_image.c
+++ b/video/mp_image.c
@@ -780,7 +780,7 @@ char *mp_image_params_to_str_buf(char *b, size_t bs,
                         m_opt_choice_str(pl_csp_levels_names, p->repr.levels),
                         m_opt_choice_str(mp_csp_light_names, p->light));
         mp_snprintf_cat(b, bs, " CL=%s",
-                        m_opt_choice_str(mp_chroma_names, p->chroma_location));
+                        m_opt_choice_str(pl_chroma_names, p->chroma_location));
         if (mp_image_crop_valid(p)) {
             mp_snprintf_cat(b, bs, " crop=%dx%d+%d+%d", mp_rect_w(p->crop),
                             mp_rect_h(p->crop), p->crop.x0, p->crop.y0);
@@ -973,11 +973,11 @@ void mp_image_params_guess_csp(struct mp_image_params *params)
         params->color.hdr = pl_hdr_metadata_empty;
     }
 
-    if (params->chroma_location == MP_CHROMA_AUTO) {
+    if (params->chroma_location == PL_CHROMA_UNKNOWN) {
         if (params->repr.levels == PL_COLOR_LEVELS_LIMITED)
-            params->chroma_location = MP_CHROMA_LEFT;
+            params->chroma_location = PL_CHROMA_LEFT;
         if (params->repr.levels == PL_COLOR_LEVELS_FULL)
-            params->chroma_location = MP_CHROMA_CENTER;
+            params->chroma_location = PL_CHROMA_CENTER;
     }
 
     if (params->light == MP_CSP_LIGHT_AUTO) {
@@ -1045,7 +1045,7 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src)
         .transfer = pl_transfer_from_av(src->color_trc),
     };
 
-    dst->params.chroma_location = avchroma_location_to_mp(src->chroma_location);
+    dst->params.chroma_location = pl_chroma_from_av(src->chroma_location);
 
     if (src->opaque_ref) {
         struct mp_image_params *p = (void *)src->opaque_ref->data;
@@ -1174,7 +1174,7 @@ struct AVFrame *mp_image_to_av_frame(struct mp_image *src)
         pl_primaries_to_av(src->params.color.primaries);
     dst->color_trc = pl_transfer_to_av(src->params.color.transfer);
 
-    dst->chroma_location = mp_chroma_location_to_av(src->params.chroma_location);
+    dst->chroma_location = pl_chroma_to_av(src->params.chroma_location);
 
     dst->opaque_ref = av_buffer_alloc(sizeof(struct mp_image_params));
     MP_HANDLE_OOM(dst->opaque_ref);
diff --git a/video/mp_image.h b/video/mp_image.h
index 443488c9e1a3..149b63ead14e 100644
--- a/video/mp_image.h
+++ b/video/mp_image.h
@@ -50,7 +50,7 @@ struct mp_image_params {
     struct pl_color_space color;
     struct pl_color_repr repr;
     enum mp_csp_light light;
-    enum mp_chroma_location chroma_location;
+    enum pl_chroma_location chroma_location;
     // The image should be rotated clockwise (0-359 degrees).
     int rotate;
     enum mp_stereo3d_mode stereo3d; // image is encoded with this mode
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
index 6683da688c89..70aca4223866 100644
--- a/video/out/gpu/video.c
+++ b/video/out/gpu/video.c
@@ -762,16 +762,16 @@ static void pass_get_images(struct gl_video *p, struct video_image *vimg,
 
     struct gl_transform chroma = {{{ls_w, 0.0}, {0.0, ls_h}}};
 
-    if (p->image_params.chroma_location != MP_CHROMA_CENTER) {
-        int cx, cy;
-        mp_get_chroma_location(p->image_params.chroma_location, &cx, &cy);
+    if (p->image_params.chroma_location != PL_CHROMA_CENTER) {
+        float cx, cy;
+        pl_chroma_location_offset(p->image_params.chroma_location, &cx, &cy);
         // By default texture coordinates are such that chroma is centered with
         // any chroma subsampling. If a specific direction is given, make it
         // so that the luma and chroma sample line up exactly.
         // For 4:4:4, setting chroma location should have no effect at all.
         // luma sample size (in chroma coord. space)
-        chroma.t[0] = ls_w < 1 ? ls_w * -cx / 2 : 0;
-        chroma.t[1] = ls_h < 1 ? ls_h * -cy / 2 : 0;
+        chroma.t[0] = ls_w < 1 ? ls_w * -cx : 0;
+        chroma.t[1] = ls_h < 1 ? ls_h * -cy : 0;
     }
 
     memset(img, 0, 4 * sizeof(img[0]));
diff --git a/video/out/placebo/utils.c b/video/out/placebo/utils.c
index 3d8c827d617f..2bf655ac3fc0 100644
--- a/video/out/placebo/utils.c
+++ b/video/out/placebo/utils.c
@@ -66,19 +66,6 @@ void mppl_log_set_probing(pl_log log, bool probing)
     pl_log_update(log, &params);
 }
 
-enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma)
-{
-    switch (chroma) {
-    case MP_CHROMA_AUTO:            return PL_CHROMA_UNKNOWN;
-    case MP_CHROMA_TOPLEFT:         return PL_CHROMA_TOP_LEFT;
-    case MP_CHROMA_LEFT:            return PL_CHROMA_LEFT;
-    case MP_CHROMA_CENTER:          return PL_CHROMA_CENTER;
-    case MP_CHROMA_COUNT:           return PL_CHROMA_COUNT;
-    }
-
-    MP_ASSERT_UNREACHABLE();
-}
-
 void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
                                 struct pl_frame *frame)
 {
diff --git a/video/out/placebo/utils.h b/video/out/placebo/utils.h
index 84b2e0468ecd..d2fa619b4a91 100644
--- a/video/out/placebo/utils.h
+++ b/video/out/placebo/utils.h
@@ -27,7 +27,5 @@ static inline struct pl_rect2d mp_rect2d_to_pl(struct mp_rect rc)
     };
 }
 
-enum pl_chroma_location mp_chroma_to_pl(enum mp_chroma_location chroma);
-
 void mp_map_dovi_metadata_to_pl(struct mp_image *mpi,
                                 struct pl_frame *frame);
diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
index 5488d85ee33a..2fa2a46c2eca 100644
--- a/video/out/vo_gpu_next.c
+++ b/video/out/vo_gpu_next.c
@@ -660,7 +660,7 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src
     }
 
     // Update chroma location, must be done after initializing planes
-    pl_frame_set_chroma_location(frame, mp_chroma_to_pl(par->chroma_location));
+    pl_frame_set_chroma_location(frame, par->chroma_location);
 
     // Set the frame DOVI metadata
     mp_map_dovi_metadata_to_pl(mpi, frame);
diff --git a/video/sws_utils.c b/video/sws_utils.c
index 9844dd62eb03..a07bb5542428 100644
--- a/video/sws_utils.c
+++ b/video/sws_utils.c
@@ -309,8 +309,8 @@ int mp_sws_reinit(struct mp_sws_context *ctx)
     av_opt_set_double(ctx->sws, "param0", ctx->params[0], 0);
     av_opt_set_double(ctx->sws, "param1", ctx->params[1], 0);
 
-    int cr_src = mp_chroma_location_to_av(src.chroma_location);
-    int cr_dst = mp_chroma_location_to_av(dst.chroma_location);
+    int cr_src = pl_chroma_to_av(src.chroma_location);
+    int cr_dst = pl_chroma_to_av(dst.chroma_location);
     int cr_xpos, cr_ypos;
 #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
     if (av_chroma_location_enum_to_pos(&cr_xpos, &cr_ypos, cr_src) >= 0) {
diff --git a/video/zimg.c b/video/zimg.c
index 4ea32af83a88..35a987dfea7d 100644
--- a/video/zimg.c
+++ b/video/zimg.c
@@ -121,13 +121,16 @@ static void mp_zimg_update_from_cmdline(struct mp_zimg_context *ctx)
     ctx->opts = *opts;
 }
 
-static zimg_chroma_location_e mp_to_z_chroma(enum mp_chroma_location cl)
+static zimg_chroma_location_e pl_to_z_chroma(enum pl_chroma_location cl)
 {
     switch (cl) {
-    case MP_CHROMA_TOPLEFT:     return ZIMG_CHROMA_TOP_LEFT;
-    case MP_CHROMA_LEFT:        return ZIMG_CHROMA_LEFT;
-    case MP_CHROMA_CENTER:      return ZIMG_CHROMA_CENTER;
-    default:                    return ZIMG_CHROMA_LEFT;
+    case PL_CHROMA_LEFT:          return ZIMG_CHROMA_LEFT;
+    case PL_CHROMA_CENTER:        return ZIMG_CHROMA_CENTER;
+    case PL_CHROMA_TOP_LEFT:      return ZIMG_CHROMA_TOP_LEFT;
+    case PL_CHROMA_TOP_CENTER:    return ZIMG_CHROMA_TOP;
+    case PL_CHROMA_BOTTOM_LEFT:   return ZIMG_CHROMA_BOTTOM_LEFT;
+    case PL_CHROMA_BOTTOM_CENTER: return ZIMG_CHROMA_BOTTOM;
+    default:                      return ZIMG_CHROMA_LEFT;
     }
 }
 
@@ -450,7 +453,7 @@ static bool setup_format(zimg_image_format *zfmt, struct mp_zimg_repack *r,
     zfmt->color_primaries = fmt.repr.sys == PL_COLOR_SYSTEM_XYZ
                                 ? ZIMG_PRIMARIES_ST428
                                 : mp_to_z_prim(fmt.color.primaries);
-    zfmt->chroma_location = mp_to_z_chroma(fmt.chroma_location);
+    zfmt->chroma_location = pl_to_z_chroma(fmt.chroma_location);
 
     if (ctx && ctx->opts.fast) {
         // mpv's default for RGB output slows down zimg significantly.

From df5da7fa525c4e6ce11a70651abbe9aef01a2f2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <kasper93@gmail.com>
Date: Sat, 4 Nov 2023 06:27:38 +0100
Subject: [PATCH 4/5] csputils: replace more primitives with pl_

We can go deeper, but need to stop somewhere to not reimplement vo_gpu
using libplacebo...
---
 demux/demux_disc.c            |   2 +-
 sub/sd_ass.c                  |   8 +-
 video/csputils.c              | 430 +++++-----------------------------
 video/csputils.h              |  57 +----
 video/mp_image.c              |   4 +-
 video/out/gpu/lcms.c          |  14 +-
 video/out/gpu/video.c         |  16 +-
 video/out/gpu/video_shaders.c |  35 +--
 video/vdpau_mixer.c           |   4 +-
 video/zimg.c                  |   2 +-
 10 files changed, 98 insertions(+), 474 deletions(-)

diff --git a/demux/demux_disc.c b/demux/demux_disc.c
index 3dfff454038f..8cd4b7b5e9e6 100644
--- a/demux/demux_disc.c
+++ b/demux/demux_disc.c
@@ -94,7 +94,7 @@ static void add_dvd_streams(demuxer_t *demuxer)
 
             // emulate the extradata
             struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS;
-            struct mp_cmat cmatrix;
+            struct pl_transform3x3 cmatrix;
             mp_get_csp_matrix(&csp, &cmatrix);
 
             char *s = talloc_strdup(sh, "");
diff --git a/sub/sd_ass.c b/sub/sd_ass.c
index aa651375bec1..c50953147c41 100644
--- a/sub/sd_ass.c
+++ b/sub/sd_ass.c
@@ -976,14 +976,14 @@ static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
     struct mp_csp_params vs_params = MP_CSP_PARAMS_DEFAULTS;
     vs_params.repr.sys = csp;
     vs_params.repr.levels = levels;
-    struct mp_cmat vs_yuv2rgb, vs_rgb2yuv;
+    struct pl_transform3x3 vs_yuv2rgb;
     mp_get_csp_matrix(&vs_params, &vs_yuv2rgb);
-    mp_invert_cmat(&vs_rgb2yuv, &vs_yuv2rgb);
+    pl_transform3x3_invert(&vs_yuv2rgb);
 
     // Proper conversion to RGB
     struct mp_csp_params rgb_params = MP_CSP_PARAMS_DEFAULTS;
     rgb_params.color = params.color;
-    struct mp_cmat vs2rgb;
+    struct pl_transform3x3 vs2rgb;
     mp_get_csp_matrix(&rgb_params, &vs2rgb);
 
     for (int n = 0; n < parts->num_parts; n++) {
@@ -994,7 +994,7 @@ static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
         int b = (color >>  8u) & 0xff;
         int a = 0xff - (color & 0xff);
         int rgb[3] = {r, g, b}, yuv[3];
-        mp_map_fixp_color(&vs_rgb2yuv, 8, rgb, 8, yuv);
+        mp_map_fixp_color(&vs_yuv2rgb, 8, rgb, 8, yuv);
         mp_map_fixp_color(&vs2rgb, 8, yuv, 8, rgb);
         sb->libass.color = MP_ASS_RGBA(rgb[0], rgb[1], rgb[2], a);
     }
diff --git a/video/csputils.c b/video/csputils.c
index 6b708f7525c9..3a12fda9f020 100644
--- a/video/csputils.c
+++ b/video/csputils.c
@@ -3,8 +3,6 @@
  *
  * Copyleft (C) 2009 Reimar Döffinger <Reimar.Doeffinger@gmx.de>
  *
- * mp_invert_cmat based on DarkPlaces engine (relicensed from GPL to LGPL)
- *
  * This file is part of mpv.
  *
  * mpv is free software; you can redistribute it and/or
@@ -170,260 +168,17 @@ enum pl_color_primaries mp_csp_guess_primaries(int width, int height)
     }
 }
 
-void mp_invert_matrix3x3(float m[3][3])
-{
-    float m00 = m[0][0], m01 = m[0][1], m02 = m[0][2],
-          m10 = m[1][0], m11 = m[1][1], m12 = m[1][2],
-          m20 = m[2][0], m21 = m[2][1], m22 = m[2][2];
-
-    // calculate the adjoint
-    m[0][0] =  (m11 * m22 - m21 * m12);
-    m[0][1] = -(m01 * m22 - m21 * m02);
-    m[0][2] =  (m01 * m12 - m11 * m02);
-    m[1][0] = -(m10 * m22 - m20 * m12);
-    m[1][1] =  (m00 * m22 - m20 * m02);
-    m[1][2] = -(m00 * m12 - m10 * m02);
-    m[2][0] =  (m10 * m21 - m20 * m11);
-    m[2][1] = -(m00 * m21 - m20 * m01);
-    m[2][2] =  (m00 * m11 - m10 * m01);
-
-    // calculate the determinant (as inverse == 1/det * adjoint,
-    // adjoint * m == identity * det, so this calculates the det)
-    float det = m00 * m[0][0] + m10 * m[0][1] + m20 * m[0][2];
-    det = 1.0f / det;
-
-    for (int i = 0; i < 3; i++) {
-        for (int j = 0; j < 3; j++)
-            m[i][j] *= det;
-    }
-}
-
-// A := A * B
-static void mp_mul_matrix3x3(float a[3][3], float b[3][3])
-{
-    float a00 = a[0][0], a01 = a[0][1], a02 = a[0][2],
-          a10 = a[1][0], a11 = a[1][1], a12 = a[1][2],
-          a20 = a[2][0], a21 = a[2][1], a22 = a[2][2];
-
-    for (int i = 0; i < 3; i++) {
-        a[0][i] = a00 * b[0][i] + a01 * b[1][i] + a02 * b[2][i];
-        a[1][i] = a10 * b[0][i] + a11 * b[1][i] + a12 * b[2][i];
-        a[2][i] = a20 * b[0][i] + a21 * b[1][i] + a22 * b[2][i];
-    }
-}
-
-// return the primaries associated with a certain mp_csp_primaries val
-struct mp_csp_primaries mp_get_csp_primaries(enum pl_color_primaries spc)
-{
-    /*
-    Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0
-
-    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.470-6-199811-S!!PDF-E.pdf
-    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf
-    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf
-    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-0-201208-I!!PDF-E.pdf
-
-    Other colorspaces from https://en.wikipedia.org/wiki/RGB_color_space#Specifications
-    */
-
-    // CIE standard illuminant series
-    static const struct mp_csp_col_xy
-        d50 = {0.34577, 0.35850},
-        d65 = {0.31271, 0.32902},
-        c   = {0.31006, 0.31616},
-        dci = {0.31400, 0.35100},
-        e   = {1.0/3.0, 1.0/3.0};
-
-    switch (spc) {
-    case PL_COLOR_PRIM_BT_470M:
-        return (struct mp_csp_primaries) {
-            .red   = {0.670, 0.330},
-            .green = {0.210, 0.710},
-            .blue  = {0.140, 0.080},
-            .white = c
-        };
-    case PL_COLOR_PRIM_BT_601_525:
-        return (struct mp_csp_primaries) {
-            .red   = {0.630, 0.340},
-            .green = {0.310, 0.595},
-            .blue  = {0.155, 0.070},
-            .white = d65
-        };
-    case PL_COLOR_PRIM_BT_601_625:
-        return (struct mp_csp_primaries) {
-            .red   = {0.640, 0.330},
-            .green = {0.290, 0.600},
-            .blue  = {0.150, 0.060},
-            .white = d65
-        };
-    // This is the default assumption if no colorspace information could
-    // be determined, eg. for files which have no video channel.
-    case PL_COLOR_PRIM_UNKNOWN:
-    case PL_COLOR_PRIM_BT_709:
-        return (struct mp_csp_primaries) {
-            .red   = {0.640, 0.330},
-            .green = {0.300, 0.600},
-            .blue  = {0.150, 0.060},
-            .white = d65
-        };
-    case PL_COLOR_PRIM_BT_2020:
-        return (struct mp_csp_primaries) {
-            .red   = {0.708, 0.292},
-            .green = {0.170, 0.797},
-            .blue  = {0.131, 0.046},
-            .white = d65
-        };
-    case PL_COLOR_PRIM_APPLE:
-        return (struct mp_csp_primaries) {
-            .red   = {0.625, 0.340},
-            .green = {0.280, 0.595},
-            .blue  = {0.115, 0.070},
-            .white = d65
-        };
-    case PL_COLOR_PRIM_ADOBE:
-        return (struct mp_csp_primaries) {
-            .red   = {0.640, 0.330},
-            .green = {0.210, 0.710},
-            .blue  = {0.150, 0.060},
-            .white = d65
-        };
-    case PL_COLOR_PRIM_PRO_PHOTO:
-        return (struct mp_csp_primaries) {
-            .red   = {0.7347, 0.2653},
-            .green = {0.1596, 0.8404},
-            .blue  = {0.0366, 0.0001},
-            .white = d50
-        };
-    case PL_COLOR_PRIM_CIE_1931:
-        return (struct mp_csp_primaries) {
-            .red   = {0.7347, 0.2653},
-            .green = {0.2738, 0.7174},
-            .blue  = {0.1666, 0.0089},
-            .white = e
-        };
-    // From SMPTE RP 431-2 and 432-1
-    case PL_COLOR_PRIM_DCI_P3:
-    case PL_COLOR_PRIM_DISPLAY_P3:
-        return (struct mp_csp_primaries) {
-            .red   = {0.680, 0.320},
-            .green = {0.265, 0.690},
-            .blue  = {0.150, 0.060},
-            .white = spc == PL_COLOR_PRIM_DCI_P3 ? dci : d65
-        };
-    // From Panasonic VARICAM reference manual
-    case PL_COLOR_PRIM_V_GAMUT:
-        return (struct mp_csp_primaries) {
-            .red   = {0.730, 0.280},
-            .green = {0.165, 0.840},
-            .blue  = {0.100, -0.03},
-            .white = d65
-        };
-    // From Sony S-Log reference manual
-    case PL_COLOR_PRIM_S_GAMUT:
-        return (struct mp_csp_primaries) {
-            .red   = {0.730, 0.280},
-            .green = {0.140, 0.855},
-            .blue  = {0.100, -0.05},
-            .white = d65
-        };
-    // from EBU Tech. 3213-E
-    case PL_COLOR_PRIM_EBU_3213:
-        return (struct mp_csp_primaries) {
-            .red   = {0.630, 0.340},
-            .green = {0.295, 0.605},
-            .blue  = {0.155, 0.077},
-            .white = d65
-        };
-    // From H.273, traditional film with Illuminant C
-    case PL_COLOR_PRIM_FILM_C:
-        return (struct mp_csp_primaries) {
-            .red   = {0.681, 0.319},
-            .green = {0.243, 0.692},
-            .blue  = {0.145, 0.049},
-            .white = c
-        };
-    // From libplacebo source code
-    case PL_COLOR_PRIM_ACES_AP0:
-        return (struct mp_csp_primaries) {
-            .red   = {0.7347, 0.2653},
-            .green = {0.0000, 1.0000},
-            .blue  = {0.0001, -0.0770},
-            .white = {0.32168, 0.33767},
-        };
-    // From libplacebo source code
-    case PL_COLOR_PRIM_ACES_AP1:
-        return (struct mp_csp_primaries) {
-            .red   = {0.713, 0.293},
-            .green = {0.165, 0.830},
-            .blue  = {0.128, 0.044},
-            .white = {0.32168, 0.33767},
-        };
-    default:
-        return (struct mp_csp_primaries) {{0}};
-    }
-}
-
-// Get the nominal peak for a given colorspace, relative to the reference white
-// level. In other words, this returns the brightest encodable value that can
-// be represented by a given transfer curve.
-float mp_trc_nom_peak(enum pl_color_transfer trc)
-{
-    switch (trc) {
-    case PL_COLOR_TRC_PQ:           return 10000.0 / MP_REF_WHITE;
-    case PL_COLOR_TRC_HLG:          return 12.0 / MP_REF_WHITE_HLG;
-    case PL_COLOR_TRC_V_LOG:        return 46.0855;
-    case PL_COLOR_TRC_S_LOG1:       return 6.52;
-    case PL_COLOR_TRC_S_LOG2:       return 9.212;
-    }
-
-    return 1.0;
-}
-
-bool mp_trc_is_hdr(enum pl_color_transfer trc)
-{
-    return mp_trc_nom_peak(trc) > 1.0;
-}
-
-// Compute the RGB/XYZ matrix as described here:
-// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
-void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3])
-{
-    float S[3], X[4], Z[4];
-
-    // Convert from CIE xyY to XYZ. Note that Y=1 holds true for all primaries
-    X[0] = space.red.x   / space.red.y;
-    X[1] = space.green.x / space.green.y;
-    X[2] = space.blue.x  / space.blue.y;
-    X[3] = space.white.x / space.white.y;
-
-    Z[0] = (1 - space.red.x   - space.red.y)   / space.red.y;
-    Z[1] = (1 - space.green.x - space.green.y) / space.green.y;
-    Z[2] = (1 - space.blue.x  - space.blue.y)  / space.blue.y;
-    Z[3] = (1 - space.white.x - space.white.y) / space.white.y;
-
-    // S = XYZ^-1 * W
-    for (int i = 0; i < 3; i++) {
-        m[0][i] = X[i];
-        m[1][i] = 1;
-        m[2][i] = Z[i];
-    }
-
-    mp_invert_matrix3x3(m);
-
-    for (int i = 0; i < 3; i++)
-        S[i] = m[i][0] * X[3] + m[i][1] * 1 + m[i][2] * Z[3];
-
-    // M = [Sc * XYZc]
-    for (int i = 0; i < 3; i++) {
-        m[0][i] = S[i] * X[i];
-        m[1][i] = S[i] * 1;
-        m[2][i] = S[i] * Z[i];
-    }
-}
+// LMS<-XYZ revised matrix from CIECAM97, based on a linear transform and
+// normalized for equal energy on monochrome inputs
+static const pl_matrix3x3 m_cat97 = {{
+    {  0.8562,  0.3372, -0.1934 },
+    { -0.8360,  1.8327,  0.0033 },
+    {  0.0357, -0.0469,  1.0112 },
+}};
 
 // M := M * XYZd<-XYZs
-static void mp_apply_chromatic_adaptation(struct mp_csp_col_xy src,
-                                          struct mp_csp_col_xy dest, float m[3][3])
+static void apply_chromatic_adaptation(struct pl_cie_xy src,
+                                       struct pl_cie_xy dest, pl_matrix3x3 *mat)
 {
     // If the white points are nearly identical, this is a wasteful identity
     // operation.
@@ -432,98 +187,33 @@ static void mp_apply_chromatic_adaptation(struct mp_csp_col_xy src,
 
     // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma
     // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
-    float C[3][2], tmp[3][3] = {{0}};
-
-    // Ma = Bradford matrix, arguably most popular method in use today.
-    // This is derived experimentally and thus hard-coded.
-    float bradford[3][3] = {
-        {  0.8951,  0.2664, -0.1614 },
-        { -0.7502,  1.7135,  0.0367 },
-        {  0.0389, -0.0685,  1.0296 },
-    };
+    // For Ma, we use the CIECAM97 revised (linear) matrix
+    float C[3][2];
 
     for (int i = 0; i < 3; i++) {
         // source cone
-        C[i][0] = bradford[i][0] * mp_xy_X(src)
-                + bradford[i][1] * 1
-                + bradford[i][2] * mp_xy_Z(src);
+        C[i][0] = m_cat97.m[i][0] * pl_cie_X(src)
+                + m_cat97.m[i][1] * 1
+                + m_cat97.m[i][2] * pl_cie_Z(src);
 
         // dest cone
-        C[i][1] = bradford[i][0] * mp_xy_X(dest)
-                + bradford[i][1] * 1
-                + bradford[i][2] * mp_xy_Z(dest);
+        C[i][1] = m_cat97.m[i][0] * pl_cie_X(dest)
+                + m_cat97.m[i][1] * 1
+                + m_cat97.m[i][2] * pl_cie_Z(dest);
     }
 
     // tmp := I * [Cd/Cs] * Ma
+    pl_matrix3x3 tmp = {0};
     for (int i = 0; i < 3; i++)
-        tmp[i][i] = C[i][1] / C[i][0];
+        tmp.m[i][i] = C[i][1] / C[i][0];
 
-    mp_mul_matrix3x3(tmp, bradford);
+    pl_matrix3x3_mul(&tmp, &m_cat97);
 
     // M := M * Ma^-1 * tmp
-    mp_invert_matrix3x3(bradford);
-    mp_mul_matrix3x3(m, bradford);
-    mp_mul_matrix3x3(m, tmp);
-}
-
-// get the coefficients of the source -> dest cms matrix
-void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest,
-                       enum mp_render_intent intent, float m[3][3])
-{
-    float tmp[3][3];
-
-    // In saturation mapping, we don't care about accuracy and just want
-    // primaries to map to primaries, making this an identity transformation.
-    if (intent == MP_INTENT_SATURATION) {
-        for (int i = 0; i < 3; i++)
-            m[i][i] = 1;
-        return;
-    }
-
-    // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs
-    // Equations from: http://www.brucelindbloom.com/index.html?Math.html
-    // Note: Perceptual is treated like relative colorimetric. There's no
-    // definition for perceptual other than "make it look good".
-
-    // RGBd<-XYZd, inverted from XYZd<-RGBd
-    mp_get_rgb2xyz_matrix(dest, m);
-    mp_invert_matrix3x3(m);
-
-    // Chromatic adaptation, except in absolute colorimetric intent
-    if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC)
-        mp_apply_chromatic_adaptation(src.white, dest.white, m);
-
-    // XYZs<-RGBs
-    mp_get_rgb2xyz_matrix(src, tmp);
-    mp_mul_matrix3x3(m, tmp);
-}
-
-// get the coefficients of an ST 428-1 xyz -> rgb conversion matrix
-// intent = the rendering intent used to convert to the target primaries
-static void mp_get_xyz2rgb_coeffs(struct mp_csp_params *params,
-                                  enum mp_render_intent intent, struct mp_cmat *m)
-{
-    // Convert to DCI-P3
-    struct mp_csp_primaries prim = mp_get_csp_primaries(PL_COLOR_PRIM_DCI_P3);
-    float brightness = params->brightness;
-    mp_get_rgb2xyz_matrix(prim, m->m);
-    mp_invert_matrix3x3(m->m);
-
-    // All non-absolute mappings want to map source white to target white
-    if (intent != MP_INTENT_ABSOLUTE_COLORIMETRIC) {
-        // SMPTE EG 432-1 Annex H defines the white point as equal energy
-        static const struct mp_csp_col_xy smpte432 = {1.0/3.0, 1.0/3.0};
-        mp_apply_chromatic_adaptation(smpte432, prim.white, m->m);
-    }
-
-    // Since this outputs linear RGB rather than companded RGB, we
-    // want to linearize any brightness additions. 2 is a reasonable
-    // approximation for any sort of gamma function that could be in use.
-    // As this is an aesthetic setting only, any exact values do not matter.
-    brightness *= fabs(brightness);
-
-    for (int i = 0; i < 3; i++)
-        m->c[i] = brightness;
+    pl_matrix3x3 ma_inv = m_cat97;
+    pl_matrix3x3_invert(&ma_inv);
+    pl_matrix3x3_mul(mat, &ma_inv);
+    pl_matrix3x3_mul(mat, &tmp);
 }
 
 // Get multiplication factor required if image data is fit within the LSBs of a
@@ -608,19 +298,19 @@ void mp_get_csp_uint_mul(enum pl_color_system csp, enum pl_color_levels levels,
  * Under these conditions the given parameters lr, lg, lb uniquely
  * determine the mapping of Y, U, V to R, G, B.
  */
-static void luma_coeffs(struct mp_cmat *mat, float lr, float lg, float lb)
+static void luma_coeffs(struct pl_transform3x3 *mat, float lr, float lg, float lb)
 {
     assert(fabs(lr+lg+lb - 1) < 1e-6);
-    *mat = (struct mp_cmat) {
-        { {1, 0,                    2 * (1-lr)          },
-          {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg  },
-          {1,  2 * (1-lb),          0                   } },
+    *mat = (struct pl_transform3x3) {
+        { {{1, 0,                    2 * (1-lr)          },
+           {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg  },
+           {1,  2 * (1-lb),          0                   }} },
         // Constant coefficients (mat->c) not set here
     };
 }
 
 // get the coefficients of the yuv -> rgb conversion matrix
-void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
+void mp_get_csp_matrix(struct mp_csp_params *params, struct pl_transform3x3 *m)
 {
     enum pl_color_system colorspace = params->repr.sys;
     if (colorspace <= PL_COLOR_SYSTEM_UNKNOWN || colorspace >= PL_COLOR_SYSTEM_COUNT)
@@ -639,29 +329,30 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
         // If this clips on any VO, a constant 0.5 coefficient can be added
         // to the chroma channels to normalize them into [0,1]. This is not
         // currently needed by anything, though.
-        *m = (struct mp_cmat){{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}};
+        *m = (struct pl_transform3x3){{{{0, 0, 1}, {1, 0, 0}, {0, 1, 0}}}};
         break;
     }
     case PL_COLOR_SYSTEM_RGB: {
-        *m = (struct mp_cmat){{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}};
+        *m = (struct pl_transform3x3){{{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}};
         levels_in = -1;
         break;
     }
     case PL_COLOR_SYSTEM_XYZ: {
-        // The vo should probably not be using a matrix generated by this
-        // function for XYZ sources, but if it does, let's just convert it to
-        // an equivalent RGB space based on the colorimetry metadata it
-        // provided in mp_csp_params. (At the risk of clipping, if the
-        // chosen primaries are too small to fit the actual data)
-        mp_get_xyz2rgb_coeffs(params, MP_INTENT_RELATIVE_COLORIMETRIC, m);
+        // For lack of anything saner to do, just assume the caller wants
+        // DCI-P3 primaries, which is a reasonable assumption.
+        const struct pl_raw_primaries *dst = pl_raw_primaries_get(PL_COLOR_PRIM_DCI_P3);
+        pl_matrix3x3 mat = pl_get_xyz2rgb_matrix(dst);
+        // DCDM X'Y'Z' is expected to have equal energy white point (EG 432-1 Annex H)
+        apply_chromatic_adaptation((struct pl_cie_xy){1.0/3.0, 1.0/3.0}, dst->white, &mat);
+        *m = (struct pl_transform3x3) { .mat = mat };
         levels_in = -1;
         break;
     }
     case PL_COLOR_SYSTEM_YCGCO: {
-        *m = (struct mp_cmat) {
-            {{1,  -1,  1},
-             {1,   1,  0},
-             {1,  -1, -1}},
+        *m = (struct pl_transform3x3) {
+            {{{1,  -1,  1},
+              {1,   1,  0},
+              {1,  -1, -1}}},
         };
         break;
     }
@@ -680,9 +371,9 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
         float huecos = params->gray ? 0 : params->saturation * cos(params->hue);
         float huesin = params->gray ? 0 : params->saturation * sin(params->hue);
         for (int i = 0; i < 3; i++) {
-            float u = m->m[i][1], v = m->m[i][2];
-            m->m[i][1] = huecos * u - huesin * v;
-            m->m[i][2] = huesin * u + huecos * v;
+            float u = m->mat.m[i][1], v = m->mat.m[i][2];
+            m->mat.m[i][1] = huecos * u - huesin * v;
+            m->mat.m[i][2] = huesin * u + huecos * v;
         }
     }
 
@@ -728,13 +419,13 @@ void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *m)
     cmul *= params->contrast;
 
     for (int i = 0; i < 3; i++) {
-        m->m[i][0] *= ymul;
-        m->m[i][1] *= cmul;
-        m->m[i][2] *= cmul;
+        m->mat.m[i][0] *= ymul;
+        m->mat.m[i][1] *= cmul;
+        m->mat.m[i][2] *= cmul;
         // Set c so that Y=umin,UV=cmid maps to RGB=min (black to black),
         // also add brightness offset (black lift)
-        m->c[i] = rgblev.min - m->m[i][0] * yuvlev.ymin
-                  - (m->m[i][1] + m->m[i][2]) * yuvlev.cmid
+        m->c[i] = rgblev.min - m->mat.m[i][0] * yuvlev.ymin
+                  - (m->mat.m[i][1] + m->mat.m[i][2]) * yuvlev.cmid
                   + params->brightness;
     }
 }
@@ -822,32 +513,17 @@ void mp_csp_equalizer_state_get(struct mp_csp_equalizer_state *state,
     mp_csp_copy_equalizer_values(params, opts);
 }
 
-void mp_invert_cmat(struct mp_cmat *out, struct mp_cmat *in)
-{
-    *out = *in;
-    mp_invert_matrix3x3(out->m);
-
-    // fix the constant coefficient
-    // rgb = M * yuv + C
-    // M^-1 * rgb = yuv + M^-1 * C
-    // yuv = M^-1 * rgb - M^-1 * C
-    //                  ^^^^^^^^^^
-    out->c[0] = -(out->m[0][0] * in->c[0] + out->m[0][1] * in->c[1] + out->m[0][2] * in->c[2]);
-    out->c[1] = -(out->m[1][0] * in->c[0] + out->m[1][1] * in->c[1] + out->m[1][2] * in->c[2]);
-    out->c[2] = -(out->m[2][0] * in->c[0] + out->m[2][1] * in->c[1] + out->m[2][2] * in->c[2]);
-}
-
 // Multiply the color in c with the given matrix.
 // i/o is {R, G, B} or {Y, U, V} (depending on input/output and matrix), using
 // a fixed point representation with the given number of bits (so for bits==8,
 // [0,255] maps to [0,1]). The output is clipped to the range as needed.
-void mp_map_fixp_color(struct mp_cmat *matrix, int ibits, int in[3],
+void mp_map_fixp_color(struct pl_transform3x3 *matrix, int ibits, int in[3],
                                                int obits, int out[3])
 {
     for (int i = 0; i < 3; i++) {
         double val = matrix->c[i];
         for (int x = 0; x < 3; x++)
-            val += matrix->m[i][x] * in[x] / ((1 << ibits) - 1);
+            val += matrix->mat.m[i][x] * in[x] / ((1 << ibits) - 1);
         int ival = lrint(val * ((1 << obits) - 1));
         out[i] = av_clip(ival, 0, (1 << obits) - 1);
     }
diff --git a/video/csputils.h b/video/csputils.h
index 9b2d0378df96..5ab1287d9c69 100644
--- a/video/csputils.h
+++ b/video/csputils.h
@@ -46,15 +46,6 @@ enum mp_csp_light {
 
 extern const struct m_opt_choice_alternatives mp_csp_light_names[];
 
-// These constants are based on the ICC specification (Table 23) and match
-// up with the API of LittleCMS, which treats them as integers.
-enum mp_render_intent {
-    MP_INTENT_PERCEPTUAL = 0,
-    MP_INTENT_RELATIVE_COLORIMETRIC = 1,
-    MP_INTENT_SATURATION = 2,
-    MP_INTENT_ABSOLUTE_COLORIMETRIC = 3
-};
-
 // The numeric values (except -1) match the Matroska StereoMode element value.
 enum mp_stereo3d_mode {
     MP_STEREO3D_INVALID = -1,
@@ -123,59 +114,15 @@ bool mp_csp_equalizer_state_changed(struct mp_csp_equalizer_state *state);
 void mp_csp_equalizer_state_get(struct mp_csp_equalizer_state *state,
                                 struct mp_csp_params *params);
 
-struct mp_csp_col_xy {
-    float x, y;
-};
-
-static inline float mp_xy_X(struct mp_csp_col_xy xy) {
-    return xy.x / xy.y;
-}
-
-static inline float mp_xy_Z(struct mp_csp_col_xy xy) {
-    return (1 - xy.x - xy.y) / xy.y;
-}
-
-struct mp_csp_primaries {
-    struct mp_csp_col_xy red, green, blue, white;
-};
-
 enum pl_color_system mp_csp_guess_colorspace(int width, int height);
 enum pl_color_primaries mp_csp_guess_primaries(int width, int height);
 
-struct mp_csp_primaries mp_get_csp_primaries(enum pl_color_primaries csp);
-float mp_trc_nom_peak(enum pl_color_transfer trc);
-bool mp_trc_is_hdr(enum pl_color_transfer trc);
-
-/* Color conversion matrix: RGB = m * YUV + c
- * m is in row-major matrix, with m[row][col], e.g.:
- *     [ a11 a12 a13 ]     float m[3][3] = { { a11, a12, a13 },
- *     [ a21 a22 a23 ]                       { a21, a22, a23 },
- *     [ a31 a32 a33 ]                       { a31, a32, a33 } };
- * This is accessed as e.g.: m[2-1][1-1] = a21
- * In particular, each row contains all the coefficients for one of R, G, B,
- * while each column contains all the coefficients for one of Y, U, V:
- *     m[r,g,b][y,u,v] = ...
- * The matrix could also be viewed as group of 3 vectors, e.g. the 1st column
- * is the Y vector (1, 1, 1), the 2nd is the U vector, the 3rd the V vector.
- * The matrix might also be used for other conversions and colorspaces.
- */
-struct mp_cmat {
-    float m[3][3];
-    float c[3];
-};
-
-void mp_get_rgb2xyz_matrix(struct mp_csp_primaries space, float m[3][3]);
-void mp_get_cms_matrix(struct mp_csp_primaries src, struct mp_csp_primaries dest,
-                       enum mp_render_intent intent, float cms_matrix[3][3]);
-
 double mp_get_csp_mul(enum pl_color_system csp, int input_bits, int texture_bits);
 void mp_get_csp_uint_mul(enum pl_color_system csp, enum pl_color_levels levels,
                          int bits, int component, double *out_m, double *out_o);
-void mp_get_csp_matrix(struct mp_csp_params *params, struct mp_cmat *out);
+void mp_get_csp_matrix(struct mp_csp_params *params, struct pl_transform3x3 *out);
 
-void mp_invert_matrix3x3(float m[3][3]);
-void mp_invert_cmat(struct mp_cmat *out, struct mp_cmat *in);
-void mp_map_fixp_color(struct mp_cmat *matrix, int ibits, int in[3],
+void mp_map_fixp_color(struct pl_transform3x3 *matrix, int ibits, int in[3],
                                                int obits, int out[3]);
 
 #endif /* MPLAYER_CSPUTILS_H */
diff --git a/video/mp_image.c b/video/mp_image.c
index 8fbcfea77da8..512239c7bc42 100644
--- a/video/mp_image.c
+++ b/video/mp_image.c
@@ -963,11 +963,11 @@ void mp_image_params_guess_csp(struct mp_image_params *params)
         } else {
             // If the signal peak is unknown, we're forced to pick the TRC's
             // nominal range as the signal peak to prevent clipping
-            params->color.hdr.max_luma = mp_trc_nom_peak(params->color.transfer) * MP_REF_WHITE;
+            params->color.hdr.max_luma = pl_color_transfer_nominal_peak(params->color.transfer) * MP_REF_WHITE;
         }
     }
 
-    if (!mp_trc_is_hdr(params->color.transfer)) {
+    if (!pl_color_space_is_hdr(&params->color)) {
         // Some clips have leftover HDR metadata after conversion to SDR, so to
         // avoid blowing up the tone mapping code, strip/sanitize it
         params->color.hdr = pl_hdr_metadata_empty;
diff --git a/video/out/gpu/lcms.c b/video/out/gpu/lcms.c
index 00d819d2f0e3..1e451aa7a216 100644
--- a/video/out/gpu/lcms.c
+++ b/video/out/gpu/lcms.c
@@ -197,12 +197,12 @@ static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
 
     // The input profile for the transformation is dependent on the video
     // primaries and transfer characteristics
-    struct mp_csp_primaries csp = mp_get_csp_primaries(prim);
-    cmsCIExyY wp_xyY = {csp.white.x, csp.white.y, 1.0};
+    const struct pl_raw_primaries *csp = pl_raw_primaries_get(prim);
+    cmsCIExyY wp_xyY = {csp->white.x, csp->white.y, 1.0};
     cmsCIExyYTRIPLE prim_xyY = {
-        .Red   = {csp.red.x,   csp.red.y,   1.0},
-        .Green = {csp.green.x, csp.green.y, 1.0},
-        .Blue  = {csp.blue.x,  csp.blue.y,  1.0},
+        .Red   = {csp->red.x,   csp->red.y,   1.0},
+        .Green = {csp->green.x, csp->green.y, 1.0},
+        .Blue  = {csp->blue.x,  csp->blue.y,  1.0},
     };
 
     cmsToneCurve *tonecurve[3] = {0};
@@ -242,7 +242,7 @@ static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
             // function. Relative colorimetric is used since we want to
             // approximate the BT.1886 to the target device's actual black
             // point even in e.g. perceptual mode
-            const int intent = MP_INTENT_RELATIVE_COLORIMETRIC;
+            const int intent = PL_INTENT_RELATIVE_COLORIMETRIC;
             cmsCIEXYZ bp_XYZ;
             if (!cmsDetectBlackPoint(&bp_XYZ, disp_profile, intent, 0))
                 return false;
@@ -519,7 +519,7 @@ const struct m_sub_options mp_icc_conf = {
     .size = sizeof(struct mp_icc_opts),
     .defaults = &(const struct mp_icc_opts) {
         .size_str = "auto",
-        .intent = MP_INTENT_RELATIVE_COLORIMETRIC,
+        .intent = PL_INTENT_RELATIVE_COLORIMETRIC,
         .use_embedded = true,
         .cache = true,
     },
diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c
index 70aca4223866..5ae2816fed8a 100644
--- a/video/out/gpu/video.c
+++ b/video/out/gpu/video.c
@@ -2343,9 +2343,9 @@ static void pass_convert_yuv(struct gl_video *p)
 
     // Conversion to RGB. For RGB itself, this still applies e.g. brightness
     // and contrast controls, or expansion of e.g. LSB-packed 10 bit data.
-    struct mp_cmat m = {{{0}}};
+    struct pl_transform3x3 m = {0};
     mp_get_csp_matrix(&cparams, &m);
-    gl_sc_uniform_mat3(sc, "colormatrix", true, &m.m[0][0]);
+    gl_sc_uniform_mat3(sc, "colormatrix", true, &m.mat.m[0][0]);
     gl_sc_uniform_vec3(sc, "colormatrix_c", m.c);
 
     GLSL(color.rgb = mat3(colormatrix) * color.rgb + colormatrix_c;)
@@ -2481,7 +2481,7 @@ static void pass_scale_main(struct gl_video *p)
         // Linear light downscaling results in nasty artifacts for HDR curves
         // due to the potentially extreme brightness differences severely
         // compounding any ringing. So just scale in gamma light instead.
-        if (mp_trc_is_hdr(p->image_params.color.transfer))
+        if (pl_color_space_is_hdr(&p->image_params.color))
             use_linear = false;
     } else if (upscaling) {
         use_linear = p->opts.linear_upscaling || p->opts.sigmoid_upscaling;
@@ -2591,7 +2591,7 @@ static void pass_colormanage(struct gl_video *p, struct pl_color_space src,
         // limitation reasons, so we use a gamma 2.2 input curve here instead.
         // We could pick any value we want here, the difference is just coding
         // efficiency.
-        if (mp_trc_is_hdr(trc_orig))
+        if (pl_color_space_is_hdr(&p->image_params.color))
             trc_orig = PL_COLOR_TRC_GAMMA22;
 
         if (gl_video_get_lut3d(p, prim_orig, trc_orig)) {
@@ -2628,7 +2628,7 @@ static void pass_colormanage(struct gl_video *p, struct pl_color_space src,
         // Avoid outputting linear light or HDR content "by default". For these
         // just pick gamma 2.2 as a default, since it's a good estimate for
         // the response of typical displays
-        if (dst.transfer == PL_COLOR_TRC_LINEAR || mp_trc_is_hdr(dst.transfer))
+        if (dst.transfer == PL_COLOR_TRC_LINEAR || pl_color_space_is_hdr(&dst))
             dst.transfer = PL_COLOR_TRC_GAMMA22;
     }
 
@@ -2636,9 +2636,9 @@ static void pass_colormanage(struct gl_video *p, struct pl_color_space src,
     // it from the chosen transfer function. Also normalize the src peak, in
     // case it was unknown
     if (!dst.hdr.max_luma)
-        dst.hdr.max_luma = mp_trc_nom_peak(dst.transfer) * MP_REF_WHITE;
+        dst.hdr.max_luma = pl_color_transfer_nominal_peak(dst.transfer) * MP_REF_WHITE;
     if (!src.hdr.max_luma)
-        src.hdr.max_luma = mp_trc_nom_peak(src.transfer) * MP_REF_WHITE;
+        src.hdr.max_luma = pl_color_transfer_nominal_peak(src.transfer) * MP_REF_WHITE;
 
     // Whitelist supported modes
     switch (p->opts.tone_map.curve) {
@@ -2670,7 +2670,7 @@ static void pass_colormanage(struct gl_video *p, struct pl_color_space src,
     }
 
     struct gl_tone_map_opts tone_map = p->opts.tone_map;
-    bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.transfer)
+    bool detect_peak = tone_map.compute_peak >= 0 && pl_color_space_is_hdr(&src)
                        && src.hdr.max_luma > dst.hdr.max_luma;
 
     if (detect_peak && !p->hdr_peak_ssbo) {
diff --git a/video/out/gpu/video_shaders.c b/video/out/gpu/video_shaders.c
index 2201e12d4639..62fe11caaea1 100644
--- a/video/out/gpu/video_shaders.c
+++ b/video/out/gpu/video_shaders.c
@@ -17,6 +17,8 @@
 
 #include <math.h>
 
+#include <libplacebo/colorspace.h>
+
 #include "video_shaders.h"
 #include "video.h"
 
@@ -337,7 +339,7 @@ static const float SLOG_A = 0.432699,
 //
 // These functions always output to a normalized scale of [0,1], for
 // convenience of the video.c code that calls it. To get the values in an
-// absolute scale, multiply the result by `mp_trc_nom_peak(trc)`
+// absolute scale, multiply the result by `pl_color_transfer_nominal_peak(trc)`
 void pass_linearize(struct gl_shader_cache *sc, enum pl_color_transfer trc)
 {
     if (trc == PL_COLOR_TRC_LINEAR)
@@ -430,7 +432,7 @@ void pass_linearize(struct gl_shader_cache *sc, enum pl_color_transfer trc)
     }
 
     // Rescale to prevent clipping on non-float textures
-    GLSLF("color.rgb *= vec3(1.0/%f);\n", mp_trc_nom_peak(trc));
+    GLSLF("color.rgb *= vec3(1.0/%f);\n", pl_color_transfer_nominal_peak(trc));
 }
 
 // Delinearize (compress), given a TRC as output. This corresponds to the
@@ -445,7 +447,7 @@ void pass_delinearize(struct gl_shader_cache *sc, enum pl_color_transfer trc)
 
     GLSLF("// delinearize\n");
     GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);)
-    GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(trc));
+    GLSLF("color.rgb *= vec3(%f);\n", pl_color_transfer_nominal_peak(trc));
 
     switch (trc) {
     case PL_COLOR_TRC_SRGB:
@@ -842,11 +844,10 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
 
     // Some operations need access to the video's luma coefficients, so make
     // them available
-    float rgb2xyz[3][3];
-    mp_get_rgb2xyz_matrix(mp_get_csp_primaries(src.primaries), rgb2xyz);
-    gl_sc_uniform_vec3(sc, "src_luma", rgb2xyz[1]);
-    mp_get_rgb2xyz_matrix(mp_get_csp_primaries(dst.primaries), rgb2xyz);
-    gl_sc_uniform_vec3(sc, "dst_luma", rgb2xyz[1]);
+    pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(pl_raw_primaries_get(src.primaries));
+    gl_sc_uniform_vec3(sc, "src_luma", rgb2xyz.m[1]);
+    rgb2xyz = pl_get_rgb2xyz_matrix(pl_raw_primaries_get(dst.primaries));
+    gl_sc_uniform_vec3(sc, "dst_luma", rgb2xyz.m[1]);
 
     bool need_ootf = src_light != dst_light;
     if (src_light == MP_CSP_LIGHT_SCENE_HLG && src.hdr.max_luma != dst.hdr.max_luma)
@@ -867,7 +868,7 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
     }
 
     // Pre-scale the incoming values into an absolute scale
-    GLSLF("color.rgb *= vec3(%f);\n", mp_trc_nom_peak(src.transfer));
+    GLSLF("color.rgb *= vec3(%f);\n", pl_color_transfer_nominal_peak(src.transfer));
 
     if (need_ootf)
         pass_ootf(sc, src_light, src.hdr.max_luma / MP_REF_WHITE);
@@ -880,11 +881,11 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
 
     // Adapt to the right colorspace if necessary
     if (src.primaries != dst.primaries) {
-        struct mp_csp_primaries csp_src = mp_get_csp_primaries(src.primaries),
-                                csp_dst = mp_get_csp_primaries(dst.primaries);
-        float m[3][3] = {{0}};
-        mp_get_cms_matrix(csp_src, csp_dst, MP_INTENT_RELATIVE_COLORIMETRIC, m);
-        gl_sc_uniform_mat3(sc, "cms_matrix", true, &m[0][0]);
+        const struct pl_raw_primaries *csp_src = pl_raw_primaries_get(src.primaries),
+                                      *csp_dst = pl_raw_primaries_get(dst.primaries);
+        pl_matrix3x3 m = pl_get_color_mapping_matrix(csp_src, csp_dst,
+                                                     PL_INTENT_RELATIVE_COLORIMETRIC);
+        gl_sc_uniform_mat3(sc, "cms_matrix", true, &m.m[0][0]);
         GLSL(color.rgb = cms_matrix * color.rgb;)
 
         if (!opts->gamut_mode || opts->gamut_mode == GAMUT_DESATURATE) {
@@ -907,8 +908,8 @@ void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
     // For SDR, we normalize to the chosen signal peak. For HDR, we normalize
     // to the encoding range of the transfer function.
     float dst_range = dst.hdr.max_luma / MP_REF_WHITE;
-    if (mp_trc_is_hdr(dst.transfer))
-        dst_range = mp_trc_nom_peak(dst.transfer);
+    if (pl_color_space_is_hdr(&dst))
+        dst_range = pl_color_transfer_nominal_peak(dst.transfer);
 
     GLSLF("color.rgb *= vec3(%f);\n", 1.0 / dst_range);
 
@@ -1009,7 +1010,7 @@ void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
     GLSL(noise.z = rand(h); h = permute(h);)
 
     // Noise is scaled to the signal level to prevent extreme noise for HDR
-    float gain = opts->grain/8192.0 / mp_trc_nom_peak(trc);
+    float gain = opts->grain/8192.0 / pl_color_transfer_nominal_peak(trc);
     GLSLF("color.xyz += %f * (noise - vec3(0.5));\n", gain);
     GLSLF("}\n");
 }
diff --git a/video/vdpau_mixer.c b/video/vdpau_mixer.c
index b1aed70e78fe..5adb3b4ec3c3 100644
--- a/video/vdpau_mixer.c
+++ b/video/vdpau_mixer.c
@@ -193,7 +193,7 @@ static int create_vdp_mixer(struct mp_vdpau_mixer *mixer,
     if (!opts->chroma_deint)
         SET_VIDEO_ATTR(SKIP_CHROMA_DEINTERLACE, uint8_t, 1);
 
-    struct mp_cmat yuv2rgb;
+    struct pl_transform3x3 yuv2rgb;
     VdpCSCMatrix matrix;
 
     struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
@@ -204,7 +204,7 @@ static int create_vdp_mixer(struct mp_vdpau_mixer *mixer,
 
     for (int r = 0; r < 3; r++) {
         for (int c = 0; c < 3; c++)
-            matrix[r][c] = yuv2rgb.m[r][c];
+            matrix[r][c] = yuv2rgb.mat.m[r][c];
         matrix[r][3] = yuv2rgb.c[r];
     }
 
diff --git a/video/zimg.c b/video/zimg.c
index 35a987dfea7d..907e81deb7fc 100644
--- a/video/zimg.c
+++ b/video/zimg.c
@@ -551,7 +551,7 @@ static bool mp_zimg_state_init(struct mp_zimg_context *ctx,
         params.allow_approximate_gamma = 1;
 
     // leave at default for SDR, which means 100 cd/m^2 for zimg
-    if (ctx->dst.color.hdr.max_luma > 0 && mp_trc_is_hdr(ctx->dst.color.transfer))
+    if (ctx->dst.color.hdr.max_luma > 0 && pl_color_space_is_hdr(&ctx->dst.color))
         params.nominal_peak_luminance = ctx->dst.color.hdr.max_luma;
 
     st->graph = zimg_filter_graph_build(&src_fmt, &dst_fmt, &params);

From ae4ec861a381d06c49ffcf83572d244cba862135 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <kasper93@gmail.com>
Date: Sat, 4 Nov 2023 07:10:31 +0100
Subject: [PATCH 5/5] vo_gpu_next: simplify after recent changes

---
 video/out/vo_gpu_next.c | 24 ++++--------------------
 1 file changed, 4 insertions(+), 20 deletions(-)

diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c
index 2fa2a46c2eca..42ceb6e7a945 100644
--- a/video/out/vo_gpu_next.c
+++ b/video/out/vo_gpu_next.c
@@ -232,8 +232,6 @@ static struct mp_image *get_image(struct vo *vo, int imgfmt, int w, int h,
     return mpi;
 }
 
-static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi);
-
 static void update_overlays(struct vo *vo, struct mp_osd_res res,
                             int flags, enum pl_overlay_coords coords,
                             struct osd_state *state, struct pl_frame *frame,
@@ -318,7 +316,7 @@ static void update_overlays(struct vo *vo, struct mp_osd_res res,
             ol->repr.alpha = PL_ALPHA_PREMULTIPLIED;
             // Infer bitmap colorspace from source
             if (src) {
-                ol->color = get_mpi_csp(vo, src);
+                ol->color = src->params.color;
                 // Seems like HDR subtitles are targeting SDR white
                 if (pl_color_transfer_is_hdr(ol->color.transfer)) {
                     ol->color.hdr = (struct pl_hdr_metadata) {
@@ -437,16 +435,6 @@ static int plane_data_from_imgfmt(struct pl_plane_data out_data[4],
     return desc.num_planes;
 }
 
-static struct pl_color_space get_mpi_csp(struct vo *vo, struct mp_image *mpi)
-{
-    struct pl_color_space csp = {
-        .primaries = mpi->params.color.primaries,
-        .transfer = mpi->params.color.transfer,
-        .hdr = mpi->params.color.hdr,
-    };
-    return csp;
-}
-
 static bool hwdec_reconfig(struct priv *p, struct ra_hwdec *hwdec,
                            const struct mp_image_params *par)
 {
@@ -567,12 +555,8 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src
     }
 
     *frame = (struct pl_frame) {
-        .color = get_mpi_csp(vo, mpi),
-        .repr = {
-            .sys = par->repr.sys,
-            .levels = par->repr.levels,
-            .alpha = par->repr.alpha,
-        },
+        .color = par->color,
+        .repr = par->repr,
         .profile = {
             .data = mpi->icc_profile ? mpi->icc_profile->data : NULL,
             .len = mpi->icc_profile ? mpi->icc_profile->size : 0,
@@ -910,7 +894,7 @@ static void draw_frame(struct vo *vo, struct vo_frame *frame)
 
     const struct gl_video_opts *opts = p->opts_cache->opts;
     if (p->target_hint && frame->current) {
-        struct pl_color_space hint = get_mpi_csp(vo, frame->current);
+        struct pl_color_space hint = frame->current->params.color;
         if (opts->target_prim)
             hint.primaries = opts->target_prim;
         if (opts->target_trc)
openSUSE Build Service is sponsored by