diff mbox series

[FFmpeg-devel,1/4] lavfi/vf_libplacebo: switch to new gamut mapping API

Message ID 20230521122438.78375-1-ffmpeg@haasn.xyz
State Accepted
Commit d637f20f057586a0a3976fecab3a619e3efcae92
Headers show
Series [FFmpeg-devel,1/4] lavfi/vf_libplacebo: switch to new gamut mapping API | expand

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Niklas Haas May 21, 2023, 12:24 p.m. UTC
From: Niklas Haas <git@haasn.dev>

Upstream deprecated the old ad-hoc, enum/intent-based gamut mapping API
and added a new API based on colorimetrically accurate gamut mapping
functions.

The relevant change for us is the addition of several new modes, as well
as deprecation of the old options. Update the documentation accordingly.
---
 doc/filters.texi            | 47 ++++++++++----------
 libavfilter/vf_libplacebo.c | 86 +++++++++++++++++++++++++++++--------
 2 files changed, 92 insertions(+), 41 deletions(-)

Comments

Niklas Haas May 22, 2023, 8:35 a.m. UTC | #1
On Sun, 21 May 2023 14:24:35 +0200 Niklas Haas <ffmpeg@haasn.xyz> wrote:
> From: Niklas Haas <git@haasn.dev>
> 
> Upstream deprecated the old ad-hoc, enum/intent-based gamut mapping API
> and added a new API based on colorimetrically accurate gamut mapping
> functions.
> 
> The relevant change for us is the addition of several new modes, as well
> as deprecation of the old options. Update the documentation accordingly.
> ---
>  doc/filters.texi            | 47 ++++++++++----------
>  libavfilter/vf_libplacebo.c | 86 +++++++++++++++++++++++++++++--------
>  2 files changed, 92 insertions(+), 41 deletions(-)
> 
> diff --git a/doc/filters.texi b/doc/filters.texi
> index ddc8529bbe9..97023d5f2e8 100644
> --- a/doc/filters.texi
> +++ b/doc/filters.texi
> @@ -16353,37 +16353,36 @@ gamut-mapping when dealing with mismatches between wide-gamut or HDR content.
>  In general, libplacebo relies on accurate source tagging and mastering display
>  gamut information to produce the best results.
>  @table @option
> -@item intent
> -Rendering intent to use when adapting between different primary color gamuts
> -(after tone-mapping).
> -@table @samp
> -@item perceptual
> -Perceptual gamut mapping. Currently equivalent to relative colorimetric.
> -@item relative
> -Relative colorimetric. This is the default.
> -@item absolute
> -Absolute colorimetric.
> -@item saturation
> -Saturation mapping. Forcibly stretches the source gamut to the target gamut.
> -@end table
> -
>  @item gamut_mode
>  How to handle out-of-gamut colors that can occur as a result of colorimetric
>  gamut mapping.
>  @table @samp
>  @item clip
> -Do nothing, simply clip out-of-range colors to the RGB volume. This is the
> -default.
> -@item warn
> -Highlight out-of-gamut pixels (by coloring them pink).
> -@item darken
> -Linearly reduces content brightness to preserves saturated details, followed by
> -clipping the remaining out-of-gamut colors. As the name implies, this makes
> -everything darker, but provides a good balance between preserving details and
> -colors.
> +Do nothing, simply clip out-of-range colors to the RGB volume. Low quality but
> +extremely fast.
> +@item perceptual
> +Perceptually soft-clip colors to the gamut volume. This is the default.
> +@item relative
> +Relative colorimetric hard-clip. Similar to @code{perceptual} but without
> +the soft knee.
> +@item saturation
> +Saturation mapping, maps primaries directly to primaries in RGB space.
> +Not recommended except for artificial computer graphics for which a bright,
> +saturated display is desired.
> +@item absolute
> +Absolute colorimetric hard-clip. Performs no adjustment of the white point.
>  @item desaturate
>  Hard-desaturates out-of-gamut colors towards white, while preserving the
> -luminance. Has a tendency to shift colors.
> +luminance. Has a tendency to distort the visual appearance of bright objects.
> +@item darken
> +Linearly reduces content brightness to preserves saturated details, followed by
> +clipping the remaining out-of-gamut colors.
> +@item warn
> +Highlight out-of-gamut pixels (by inverting/marking them).
> +@item linear
> +Linearly reduces chromaticity of the entire image to make it fit within the
> +target color volume. Be careful when using this on BT.2020 sources without
> +proper mastering metadata, as doing so will lead to excessive desaturation.
>  @end table
>  
>  @item tonemapping
> diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
> index f26d0126beb..09bb3dfac86 100644
> --- a/libavfilter/vf_libplacebo.c
> +++ b/libavfilter/vf_libplacebo.c
> @@ -56,6 +56,19 @@ enum {
>      TONE_MAP_COUNT,
>  };
>  
> +enum {
> +    GAMUT_MAP_CLIP,
> +    GAMUT_MAP_PERCEPTUAL,
> +    GAMUT_MAP_RELATIVE,
> +    GAMUT_MAP_SATURATION,
> +    GAMUT_MAP_ABSOLUTE,
> +    GAMUT_MAP_DESATURATE,
> +    GAMUT_MAP_DARKEN,
> +    GAMUT_MAP_HIGHLIGHT,
> +    GAMUT_MAP_LINEAR,
> +    GAMUT_MAP_COUNT,
> +};
> +
>  static const char *const var_names[] = {
>      "in_w", "iw",   ///< width  of the input video frame
>      "in_h", "ih",   ///< height of the input video frame
> @@ -186,7 +199,6 @@ typedef struct LibplaceboContext {
>  
>      /* pl_color_map_params */
>      struct pl_color_map_params color_map_params;
> -    int intent;
>      int gamut_mode;
>      int tonemapping;
>      float tonemapping_param;
> @@ -202,6 +214,7 @@ typedef struct LibplaceboContext {
>      int gamut_warning;
>      int gamut_clipping;
>      int force_icc_lut;
> +    int intent;
>  #endif
>  
>      /* pl_dither_params */
> @@ -272,6 +285,34 @@ static const struct pl_tone_map_function *pl_get_tonemapping_func(int tm) {
>      }
>  }
>  
> +static void set_gamut_mode(struct pl_color_map_params *p, int gamut_mode)
> +{
> +    switch (gamut_mode) {
> +#if PL_API_VER >= 269
> +    case GAMUT_MAP_CLIP:       p->gamut_mapping = &pl_gamut_map_clip; return;
> +    case GAMUT_MAP_PERCEPTUAL: p->gamut_mapping = &pl_gamut_map_perceptual; return;
> +    case GAMUT_MAP_RELATIVE:   p->gamut_mapping = &pl_gamut_map_relative; return;
> +    case GAMUT_MAP_SATURATION: p->gamut_mapping = &pl_gamut_map_saturation; return;
> +    case GAMUT_MAP_ABSOLUTE:   p->gamut_mapping = &pl_gamut_map_absolute; return;
> +    case GAMUT_MAP_DESATURATE: p->gamut_mapping = &pl_gamut_map_desaturate; return;
> +    case GAMUT_MAP_DARKEN:     p->gamut_mapping = &pl_gamut_map_darken; return;
> +    case GAMUT_MAP_HIGHLIGHT:  p->gamut_mapping = &pl_gamut_map_highlight; return;
> +    case GAMUT_MAP_LINEAR:     p->gamut_mapping = &pl_gamut_map_linear; return;
> +#else
> +    case GAMUT_MAP_RELATIVE:   p->intent = PL_INTENT_RELATIVE_COLORIMETRIC; return;
> +    case GAMUT_MAP_SATURATION: p->intent = PL_INTENT_SATURATION; return;
> +    case GAMUT_MAP_ABSOLUTE:   p->intent = PL_INTENT_ABSOLUTE_COLORIMETRIC; return;
> +    case GAMUT_MAP_DESATURATE: p->gamut_mode = PL_GAMUT_DESATURATE; return;
> +    case GAMUT_MAP_DARKEN:     p->gamut_mode = PL_GAMUT_DARKEN; return;
> +    case GAMUT_MAP_HIGHLIGHT:  p->gamut_mode = PL_GAMUT_WARN; return;
> +    /* Use defaults for all other cases */
> +    default: return;
> +#endif
> +    }
> +
> +    av_assert0(0);
> +};
> +
>  static int parse_shader(AVFilterContext *avctx, const void *shader, size_t len)
>  {
>      LibplaceboContext *s = avctx->priv;
> @@ -317,7 +358,7 @@ static int update_settings(AVFilterContext *ctx)
>      int err = 0;
>      LibplaceboContext *s = ctx->priv;
>      enum pl_tone_map_mode tonemapping_mode = s->tonemapping_mode;
> -    enum pl_gamut_mode gamut_mode = s->gamut_mode;
> +    int gamut_mode = s->gamut_mode;
>      uint8_t color_rgba[4];
>  
>      RET(av_parse_color(color_rgba, s->fillcolor, -1, s));
> @@ -336,10 +377,16 @@ static int update_settings(AVFilterContext *ctx)
>          }
>      }
>  
> +    switch (s->intent) {
> +    case PL_INTENT_SATURATION:            gamut_mode = GAMUT_MAP_SATURATION; break;
> +    case PL_INTENT_RELATIVE_COLORIMETRIC: gamut_mode = GAMUT_MAP_RELATIVE; break;
> +    case PL_INTENT_ABSOLUTE_COLORIMETRIC: gamut_mode = GAMUT_MAP_ABSOLUTE; break;
> +    }
> +
>      if (s->gamut_warning)
> -        gamut_mode = PL_GAMUT_WARN;
> +        gamut_mode = GAMUT_MAP_HIGHLIGHT;
>      if (s->gamut_clipping)
> -        gamut_mode = PL_GAMUT_DESATURATE;
> +        gamut_mode = GAMUT_MAP_DESATURATE;
>  #endif
>  
>      s->deband_params = *pl_deband_params(
> @@ -366,8 +413,6 @@ static int update_settings(AVFilterContext *ctx)
>      );
>  
>      s->color_map_params = *pl_color_map_params(
> -        .intent = s->intent,
> -        .gamut_mode = gamut_mode,
>          .tone_mapping_function = pl_get_tonemapping_func(s->tonemapping),
>          .tone_mapping_param = s->tonemapping_param,
>          .tone_mapping_mode = tonemapping_mode,
> @@ -376,6 +421,8 @@ static int update_settings(AVFilterContext *ctx)
>          .lut_size = s->tonemapping_lut_size,
>      );
>  
> +    set_gamut_mode(&s->color_map_params, gamut_mode);
> +
>      s->dither_params = *pl_dither_params(
>          .method = s->dithering,
>          .lut_size = s->dither_lut_size,
> @@ -1143,16 +1190,16 @@ static const AVOption libplacebo_options[] = {
>      { "scene_threshold_high", "Scene change high threshold", OFFSET(scene_high), AV_OPT_TYPE_FLOAT, {.dbl = 10.0}, -1.0, 100.0, DYNAMIC },
>      { "overshoot", "Tone-mapping overshoot margin", OFFSET(overshoot), AV_OPT_TYPE_FLOAT, {.dbl = 0.05}, 0.0, 1.0, DYNAMIC },
>  
> -    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 3, DYNAMIC, "intent" },
> -        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
> -        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
> -        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
> -        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
> -    { "gamut_mode", "Gamut-mapping mode", OFFSET(gamut_mode), AV_OPT_TYPE_INT, {.i64 = PL_GAMUT_CLIP}, 0, PL_GAMUT_MODE_COUNT - 1, DYNAMIC, "gamut_mode" },
> -        { "clip", "Hard-clip gamut boundary", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_CLIP}, 0, 0, STATIC, "gamut_mode" },
> -        { "warn", "Highlight out-of-gamut colors", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_WARN}, 0, 0, STATIC, "gamut_mode" },
> -        { "darken", "Darken image to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_DARKEN}, 0, 0, STATIC, "gamut_mode" },
> -        { "desaturate", "Colorimetrically desaturate colors", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_DESATURATE}, 0, 0, STATIC, "gamut_mode" },
> +    { "gamut_mode", "Gamut-mapping mode", OFFSET(gamut_mode), AV_OPT_TYPE_INT, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, GAMUT_MAP_COUNT - 1, DYNAMIC, "gamut_mode" },
> +        { "clip", "Hard-clip (RGB per-channel)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_CLIP}, 0, 0, STATIC, "gamut_mode" },
> +        { "perceptual", "Colorimetric soft clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, 0, STATIC, "gamut_mode" },
> +        { "relative", "Relative colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_RELATIVE}, 0, 0, STATIC, "gamut_mode" },
> +        { "saturation", "Saturation mapping (RGB -> RGB)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_SATURATION}, 0, 0, STATIC, "gamut_mode" },
> +        { "absolute", "Absolute colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_ABSOLUTE}, 0, 0, STATIC, "gamut_mode" },
> +        { "desaturate", "Colorimetrically desaturate colors towards white", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DESATURATE}, 0, 0, STATIC, "gamut_mode" },
> +        { "darken", "Colorimetric clip with bias towards darkening image to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DARKEN}, 0, 0, STATIC, "gamut_mode" },
> +        { "warn", "Highlight out-of-gamut colors", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_HIGHLIGHT}, 0, 0, STATIC, "gamut_mode" },
> +        { "linear", "Linearly reduce chromaticity to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_LINEAR}, 0, 0, STATIC, "gamut_mode" },
>      { "tonemapping", "Tone-mapping algorithm", OFFSET(tonemapping), AV_OPT_TYPE_INT, {.i64 = TONE_MAP_AUTO}, 0, TONE_MAP_COUNT - 1, DYNAMIC, "tonemap" },
>          { "auto", "Automatic selection", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_AUTO}, 0, 0, STATIC, "tonemap" },
>          { "clip", "No tone mapping (clip", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_CLIP}, 0, 0, STATIC, "tonemap" },
> @@ -1184,7 +1231,12 @@ static const AVOption libplacebo_options[] = {
>      { "desaturation_strength", "Desaturation strength", OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = -1.0}, -1.0, 1.0, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
>      { "desaturation_exponent", "Desaturation exponent", OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = -1.0}, -1.0, 10.0, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
>      { "gamut_warning", "Highlight out-of-gamut colors", OFFSET(gamut_warning), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
> -    { "gamut_clipping", "Enable colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
> +    { "gamut_clipping", "Enable desaturating colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
> +    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 3, DYNAMIC | AV_OPT_FLAG_DEPRECATED, "intent" },
> +        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
> +        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
> +        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
> +        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
>  #endif
>  
>      { "dithering", "Dither method to use", OFFSET(dithering), AV_OPT_TYPE_INT, {.i64 = PL_DITHER_BLUE_NOISE}, -1, PL_DITHER_METHOD_COUNT - 1, DYNAMIC, "dither" },
> -- 
> 2.40.1
> 

Merged as d637f20f057586a0a3...877ccaf776c92866e
diff mbox series

Patch

diff --git a/doc/filters.texi b/doc/filters.texi
index ddc8529bbe9..97023d5f2e8 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -16353,37 +16353,36 @@  gamut-mapping when dealing with mismatches between wide-gamut or HDR content.
 In general, libplacebo relies on accurate source tagging and mastering display
 gamut information to produce the best results.
 @table @option
-@item intent
-Rendering intent to use when adapting between different primary color gamuts
-(after tone-mapping).
-@table @samp
-@item perceptual
-Perceptual gamut mapping. Currently equivalent to relative colorimetric.
-@item relative
-Relative colorimetric. This is the default.
-@item absolute
-Absolute colorimetric.
-@item saturation
-Saturation mapping. Forcibly stretches the source gamut to the target gamut.
-@end table
-
 @item gamut_mode
 How to handle out-of-gamut colors that can occur as a result of colorimetric
 gamut mapping.
 @table @samp
 @item clip
-Do nothing, simply clip out-of-range colors to the RGB volume. This is the
-default.
-@item warn
-Highlight out-of-gamut pixels (by coloring them pink).
-@item darken
-Linearly reduces content brightness to preserves saturated details, followed by
-clipping the remaining out-of-gamut colors. As the name implies, this makes
-everything darker, but provides a good balance between preserving details and
-colors.
+Do nothing, simply clip out-of-range colors to the RGB volume. Low quality but
+extremely fast.
+@item perceptual
+Perceptually soft-clip colors to the gamut volume. This is the default.
+@item relative
+Relative colorimetric hard-clip. Similar to @code{perceptual} but without
+the soft knee.
+@item saturation
+Saturation mapping, maps primaries directly to primaries in RGB space.
+Not recommended except for artificial computer graphics for which a bright,
+saturated display is desired.
+@item absolute
+Absolute colorimetric hard-clip. Performs no adjustment of the white point.
 @item desaturate
 Hard-desaturates out-of-gamut colors towards white, while preserving the
-luminance. Has a tendency to shift colors.
+luminance. Has a tendency to distort the visual appearance of bright objects.
+@item darken
+Linearly reduces content brightness to preserves saturated details, followed by
+clipping the remaining out-of-gamut colors.
+@item warn
+Highlight out-of-gamut pixels (by inverting/marking them).
+@item linear
+Linearly reduces chromaticity of the entire image to make it fit within the
+target color volume. Be careful when using this on BT.2020 sources without
+proper mastering metadata, as doing so will lead to excessive desaturation.
 @end table
 
 @item tonemapping
diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
index f26d0126beb..09bb3dfac86 100644
--- a/libavfilter/vf_libplacebo.c
+++ b/libavfilter/vf_libplacebo.c
@@ -56,6 +56,19 @@  enum {
     TONE_MAP_COUNT,
 };
 
+enum {
+    GAMUT_MAP_CLIP,
+    GAMUT_MAP_PERCEPTUAL,
+    GAMUT_MAP_RELATIVE,
+    GAMUT_MAP_SATURATION,
+    GAMUT_MAP_ABSOLUTE,
+    GAMUT_MAP_DESATURATE,
+    GAMUT_MAP_DARKEN,
+    GAMUT_MAP_HIGHLIGHT,
+    GAMUT_MAP_LINEAR,
+    GAMUT_MAP_COUNT,
+};
+
 static const char *const var_names[] = {
     "in_w", "iw",   ///< width  of the input video frame
     "in_h", "ih",   ///< height of the input video frame
@@ -186,7 +199,6 @@  typedef struct LibplaceboContext {
 
     /* pl_color_map_params */
     struct pl_color_map_params color_map_params;
-    int intent;
     int gamut_mode;
     int tonemapping;
     float tonemapping_param;
@@ -202,6 +214,7 @@  typedef struct LibplaceboContext {
     int gamut_warning;
     int gamut_clipping;
     int force_icc_lut;
+    int intent;
 #endif
 
     /* pl_dither_params */
@@ -272,6 +285,34 @@  static const struct pl_tone_map_function *pl_get_tonemapping_func(int tm) {
     }
 }
 
+static void set_gamut_mode(struct pl_color_map_params *p, int gamut_mode)
+{
+    switch (gamut_mode) {
+#if PL_API_VER >= 269
+    case GAMUT_MAP_CLIP:       p->gamut_mapping = &pl_gamut_map_clip; return;
+    case GAMUT_MAP_PERCEPTUAL: p->gamut_mapping = &pl_gamut_map_perceptual; return;
+    case GAMUT_MAP_RELATIVE:   p->gamut_mapping = &pl_gamut_map_relative; return;
+    case GAMUT_MAP_SATURATION: p->gamut_mapping = &pl_gamut_map_saturation; return;
+    case GAMUT_MAP_ABSOLUTE:   p->gamut_mapping = &pl_gamut_map_absolute; return;
+    case GAMUT_MAP_DESATURATE: p->gamut_mapping = &pl_gamut_map_desaturate; return;
+    case GAMUT_MAP_DARKEN:     p->gamut_mapping = &pl_gamut_map_darken; return;
+    case GAMUT_MAP_HIGHLIGHT:  p->gamut_mapping = &pl_gamut_map_highlight; return;
+    case GAMUT_MAP_LINEAR:     p->gamut_mapping = &pl_gamut_map_linear; return;
+#else
+    case GAMUT_MAP_RELATIVE:   p->intent = PL_INTENT_RELATIVE_COLORIMETRIC; return;
+    case GAMUT_MAP_SATURATION: p->intent = PL_INTENT_SATURATION; return;
+    case GAMUT_MAP_ABSOLUTE:   p->intent = PL_INTENT_ABSOLUTE_COLORIMETRIC; return;
+    case GAMUT_MAP_DESATURATE: p->gamut_mode = PL_GAMUT_DESATURATE; return;
+    case GAMUT_MAP_DARKEN:     p->gamut_mode = PL_GAMUT_DARKEN; return;
+    case GAMUT_MAP_HIGHLIGHT:  p->gamut_mode = PL_GAMUT_WARN; return;
+    /* Use defaults for all other cases */
+    default: return;
+#endif
+    }
+
+    av_assert0(0);
+};
+
 static int parse_shader(AVFilterContext *avctx, const void *shader, size_t len)
 {
     LibplaceboContext *s = avctx->priv;
@@ -317,7 +358,7 @@  static int update_settings(AVFilterContext *ctx)
     int err = 0;
     LibplaceboContext *s = ctx->priv;
     enum pl_tone_map_mode tonemapping_mode = s->tonemapping_mode;
-    enum pl_gamut_mode gamut_mode = s->gamut_mode;
+    int gamut_mode = s->gamut_mode;
     uint8_t color_rgba[4];
 
     RET(av_parse_color(color_rgba, s->fillcolor, -1, s));
@@ -336,10 +377,16 @@  static int update_settings(AVFilterContext *ctx)
         }
     }
 
+    switch (s->intent) {
+    case PL_INTENT_SATURATION:            gamut_mode = GAMUT_MAP_SATURATION; break;
+    case PL_INTENT_RELATIVE_COLORIMETRIC: gamut_mode = GAMUT_MAP_RELATIVE; break;
+    case PL_INTENT_ABSOLUTE_COLORIMETRIC: gamut_mode = GAMUT_MAP_ABSOLUTE; break;
+    }
+
     if (s->gamut_warning)
-        gamut_mode = PL_GAMUT_WARN;
+        gamut_mode = GAMUT_MAP_HIGHLIGHT;
     if (s->gamut_clipping)
-        gamut_mode = PL_GAMUT_DESATURATE;
+        gamut_mode = GAMUT_MAP_DESATURATE;
 #endif
 
     s->deband_params = *pl_deband_params(
@@ -366,8 +413,6 @@  static int update_settings(AVFilterContext *ctx)
     );
 
     s->color_map_params = *pl_color_map_params(
-        .intent = s->intent,
-        .gamut_mode = gamut_mode,
         .tone_mapping_function = pl_get_tonemapping_func(s->tonemapping),
         .tone_mapping_param = s->tonemapping_param,
         .tone_mapping_mode = tonemapping_mode,
@@ -376,6 +421,8 @@  static int update_settings(AVFilterContext *ctx)
         .lut_size = s->tonemapping_lut_size,
     );
 
+    set_gamut_mode(&s->color_map_params, gamut_mode);
+
     s->dither_params = *pl_dither_params(
         .method = s->dithering,
         .lut_size = s->dither_lut_size,
@@ -1143,16 +1190,16 @@  static const AVOption libplacebo_options[] = {
     { "scene_threshold_high", "Scene change high threshold", OFFSET(scene_high), AV_OPT_TYPE_FLOAT, {.dbl = 10.0}, -1.0, 100.0, DYNAMIC },
     { "overshoot", "Tone-mapping overshoot margin", OFFSET(overshoot), AV_OPT_TYPE_FLOAT, {.dbl = 0.05}, 0.0, 1.0, DYNAMIC },
 
-    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 3, DYNAMIC, "intent" },
-        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
-        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
-        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
-        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
-    { "gamut_mode", "Gamut-mapping mode", OFFSET(gamut_mode), AV_OPT_TYPE_INT, {.i64 = PL_GAMUT_CLIP}, 0, PL_GAMUT_MODE_COUNT - 1, DYNAMIC, "gamut_mode" },
-        { "clip", "Hard-clip gamut boundary", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_CLIP}, 0, 0, STATIC, "gamut_mode" },
-        { "warn", "Highlight out-of-gamut colors", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_WARN}, 0, 0, STATIC, "gamut_mode" },
-        { "darken", "Darken image to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_DARKEN}, 0, 0, STATIC, "gamut_mode" },
-        { "desaturate", "Colorimetrically desaturate colors", 0, AV_OPT_TYPE_CONST, {.i64 = PL_GAMUT_DESATURATE}, 0, 0, STATIC, "gamut_mode" },
+    { "gamut_mode", "Gamut-mapping mode", OFFSET(gamut_mode), AV_OPT_TYPE_INT, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, GAMUT_MAP_COUNT - 1, DYNAMIC, "gamut_mode" },
+        { "clip", "Hard-clip (RGB per-channel)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_CLIP}, 0, 0, STATIC, "gamut_mode" },
+        { "perceptual", "Colorimetric soft clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, 0, STATIC, "gamut_mode" },
+        { "relative", "Relative colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_RELATIVE}, 0, 0, STATIC, "gamut_mode" },
+        { "saturation", "Saturation mapping (RGB -> RGB)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_SATURATION}, 0, 0, STATIC, "gamut_mode" },
+        { "absolute", "Absolute colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_ABSOLUTE}, 0, 0, STATIC, "gamut_mode" },
+        { "desaturate", "Colorimetrically desaturate colors towards white", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DESATURATE}, 0, 0, STATIC, "gamut_mode" },
+        { "darken", "Colorimetric clip with bias towards darkening image to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DARKEN}, 0, 0, STATIC, "gamut_mode" },
+        { "warn", "Highlight out-of-gamut colors", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_HIGHLIGHT}, 0, 0, STATIC, "gamut_mode" },
+        { "linear", "Linearly reduce chromaticity to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_LINEAR}, 0, 0, STATIC, "gamut_mode" },
     { "tonemapping", "Tone-mapping algorithm", OFFSET(tonemapping), AV_OPT_TYPE_INT, {.i64 = TONE_MAP_AUTO}, 0, TONE_MAP_COUNT - 1, DYNAMIC, "tonemap" },
         { "auto", "Automatic selection", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_AUTO}, 0, 0, STATIC, "tonemap" },
         { "clip", "No tone mapping (clip", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_CLIP}, 0, 0, STATIC, "tonemap" },
@@ -1184,7 +1231,12 @@  static const AVOption libplacebo_options[] = {
     { "desaturation_strength", "Desaturation strength", OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = -1.0}, -1.0, 1.0, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
     { "desaturation_exponent", "Desaturation exponent", OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = -1.0}, -1.0, 10.0, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
     { "gamut_warning", "Highlight out-of-gamut colors", OFFSET(gamut_warning), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
-    { "gamut_clipping", "Enable colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
+    { "gamut_clipping", "Enable desaturating colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC | AV_OPT_FLAG_DEPRECATED },
+    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 3, DYNAMIC | AV_OPT_FLAG_DEPRECATED, "intent" },
+        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
+        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
 #endif
 
     { "dithering", "Dither method to use", OFFSET(dithering), AV_OPT_TYPE_INT, {.i64 = PL_DITHER_BLUE_NOISE}, -1, PL_DITHER_METHOD_COUNT - 1, DYNAMIC, "dither" },