Skip to content

Commit

Permalink
colorspace: refactor HDR levels fields
Browse files Browse the repository at this point in the history
So, I was growing increasingly dissatisfied with the need to override
the HDR mastering display metadata for PQ inputs, and wanted to cleanly
separate concerns here.

After this refactor, the *mastering metadata* remains purely
informative, and all of the relevant defaulting logic goes on inside
`pl_color_space`. This field will also be consulted for any operations
relevant to tone-mapping, and can take into account both per-scene
metadata as well as static metadata.

As an aside, I decided to just completely drop back-compatibility with
the deprecated `sig_*` fields, because it's been years.
  • Loading branch information
haasn committed Feb 13, 2023
1 parent 0994b6e commit edad2c4
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 78 deletions.
20 changes: 11 additions & 9 deletions demos/plplay.c
Original file line number Diff line number Diff line change
Expand Up @@ -1250,17 +1250,18 @@ static void update_settings(struct plplay *p)
nk_layout_row_dynamic(nk, 24, 2);
struct pl_color_space fix = *tcol;
pl_color_space_infer(&fix);
fix.hdr.min_luma *= 1000; // better value range
fix.nominal_min *= 1000; // better value range
nk_property_float(nk, "White point (cd/m²)",
1e-2, &fix.hdr.max_luma, 10000.0,
fix.hdr.max_luma / 100, fix.hdr.max_luma / 1000);
1e-2, &fix.nominal_max, 10000.0,
fix.nominal_max / 100, fix.nominal_max / 1000);
nk_property_float(nk, "Black point (mcd/m²)",
1e-3, &fix.hdr.min_luma, 10000.0,
fix.hdr.min_luma / 100, fix.hdr.min_luma / 1000);
fix.hdr.min_luma /= 1000;
1e-3, &fix.nominal_min, 10000.0,
fix.nominal_min / 100, fix.nominal_min / 1000);
fix.nominal_min /= 1000;
pl_color_space_infer(&fix);
tcol->hdr = fix.hdr;
iccpar->max_luma = fix.hdr.max_luma;
tcol->nominal_min = fix.nominal_min;
tcol->nominal_max = fix.nominal_max;
iccpar->max_luma = fix.nominal_max;
} else {
reset_levels = true;
}
Expand Down Expand Up @@ -1358,7 +1359,8 @@ static void update_settings(struct plplay *p)
}

if (reset_levels) {
tcol->hdr = (struct pl_hdr_metadata) {0};
tcol->nominal_min = 0;
tcol->nominal_max = 0;
iccpar->max_luma = 0;
}

Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ project('libplacebo', ['c', 'cpp'],
5,
# API version
{
'243': 'add `pl_color_space.nominal_min/max`',
'242': 'add `pl_hdr_metadata.scene_max/avg` and `pl_hdr_metadata.ootf`',
'241': 'add `pl_plane_data.swapped`',
'240': 'add `PL_COLOR_TRC_ST428`',
Expand Down
77 changes: 30 additions & 47 deletions src/colorspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,7 @@ const struct pl_color_space pl_color_space_monitor = {

bool pl_color_space_is_hdr(const struct pl_color_space *csp)
{
return csp->hdr.max_luma > PL_COLOR_SDR_WHITE ||
csp->sig_scale > 1 ||
return csp->nominal_max > PL_COLOR_SDR_WHITE ||
pl_color_transfer_is_hdr(csp->transfer);
}

Expand Down Expand Up @@ -420,33 +419,35 @@ void pl_color_space_infer(struct pl_color_space *space)
space->primaries = PL_COLOR_PRIM_BT_709;
if (!space->transfer)
space->transfer = PL_COLOR_TRC_BT_1886;

// Backwards-compatibility with deprecated fields
if (space->sig_peak) {
space->hdr.max_luma = space->sig_peak * PL_COLOR_SDR_WHITE;
space->sig_peak = 0;
}
if (space->sig_floor) {
space->hdr.min_luma = space->sig_floor * PL_COLOR_SDR_WHITE;
space->sig_floor = 0;
}
if (space->sig_avg) {
space->hdr.scene_avg = space->sig_avg * PL_COLOR_SDR_WHITE;
space->sig_avg = 0;
if (!space->nominal_min)
space->nominal_min = space->hdr.min_luma;
if (!space->nominal_max) {
// Use the per-scene measured values if known, fall back to the
// mastering display metadata otherwise
const float scene_max = PL_MAX3(space->hdr.scene_max[0],
space->hdr.scene_max[1],
space->hdr.scene_max[2]);
space->nominal_max = PL_DEF(scene_max, space->hdr.max_luma);
}

reinfer_peaks:
if (space->hdr.max_luma < 1 || space->hdr.max_luma > 10000) {
space->hdr.max_luma = pl_color_transfer_nominal_peak(space->transfer)
* PL_COLOR_SDR_WHITE;
// PQ is always scaled down to absolute black, regardless of what the
// metadata says, so strip this information, while preserving the mastering
// metadata.
if (space->transfer == PL_COLOR_TRC_PQ)
space->nominal_min = 0;

reinfer_levels:
if (space->nominal_max < 1 || space->nominal_max > 10000) {
space->nominal_max = pl_color_transfer_nominal_peak(space->transfer)
* PL_COLOR_SDR_WHITE;

// Exception: For HLG content, we want to infer a value of 1000 cd/m²,
// a value which is considered the "reference" HLG display.
if (space->transfer == PL_COLOR_TRC_HLG)
space->hdr.max_luma = 1000;
space->nominal_max = 1000;
}

if (space->hdr.min_luma <= 0 || space->hdr.min_luma > 100) {
if (space->nominal_min <= 0 || space->nominal_min > 100) {
if (pl_color_transfer_is_hdr(space->transfer)) {
// Use a slightly nonzero black level, for the following reasons:
// - 0 may be seen as 'missing/undefined' in HDR10 metadata structs
Expand All @@ -458,23 +459,16 @@ void pl_color_space_infer(struct pl_color_space *space)
// - true infinite contrast does not exist in reality, even 1e-7
// is extremely generous considering typical viewing environments
// are not absolutely devoid of stray ambient light
space->hdr.min_luma = 1e-7;
space->nominal_min = 1e-7;
} else {
space->hdr.min_luma = space->hdr.max_luma / 1000; // Typical SDR contrast
space->nominal_min = space->nominal_max / 1000; // Typical SDR contrast
}
}

pl_assert(space->hdr.min_luma && space->hdr.max_luma);
if (space->hdr.max_luma < space->hdr.min_luma) { // sanity
space->hdr.max_luma = space->hdr.min_luma = 0;
goto reinfer_peaks;
}

if (space->sig_scale && !pl_color_transfer_is_hdr(space->transfer)) {
space->hdr.max_luma *= space->sig_scale;
space->hdr.min_luma *= space->sig_scale;
space->hdr.scene_avg *= space->sig_scale;
space->sig_scale = 0;
pl_assert(space->nominal_min && space->nominal_max);
if (space->nominal_max < space->nominal_min) { // sanity
space->nominal_max = space->nominal_min = 0;
goto reinfer_levels;
}

if (!pl_primaries_valid(&space->hdr.prim))
Expand Down Expand Up @@ -549,18 +543,7 @@ void pl_color_space_infer_ref(struct pl_color_space *space,
void pl_color_space_infer_map(struct pl_color_space *src,
struct pl_color_space *dst)
{
bool unknown_contrast = !src->hdr.min_luma;

// PQ is always scaled down to absolute black, regardless of what the
// metadata says. So treat it as such. We do this here, rather than in e.g.
// pl_color_space_infer, because the latter is also used to sanitize values
// in cases where we don't want to override mastering metadata.
// Additionally, doing it here allows e.g. correct propagation of black
// levels for BT.1886 -> PQ expansion in the step further below.
if (src->transfer == PL_COLOR_TRC_PQ)
src->hdr.min_luma = 0;
if (dst->transfer == PL_COLOR_TRC_PQ)
dst->hdr.min_luma = 0;
bool unknown_contrast = !src->nominal_min;

infer_both_ref(dst, src);

Expand All @@ -571,7 +554,7 @@ void pl_color_space_infer_map(struct pl_color_space *src,
src->transfer == PL_COLOR_TRC_BT_1886;

if (unknown_contrast && dynamic_contrast)
src->hdr.min_luma = dst->hdr.min_luma;
src->nominal_min = dst->nominal_min;
}

const struct pl_color_adjustment pl_color_adjustment_neutral = {
Expand Down
29 changes: 20 additions & 9 deletions src/include/libplacebo/colorspace.h
Original file line number Diff line number Diff line change
Expand Up @@ -394,17 +394,28 @@ struct pl_color_space {
enum pl_color_primaries primaries;
enum pl_color_transfer transfer;

// HDR metadata for this color space. Note that this can also be combined
// with SDR color transfers, in which case it's assumed that the color
// transfer in question is linearly "stretched" relative to these values.
// Nominal minimum/maximum signal levels (in cd/m²), for tone-mapping.
// These values are inferred from the mastering display metadata, per-scene
// metadata (if present), and transfer function. Users should generally not
// set this directly, but let `pl_color_space_infer` auto-pick it based on
// the values supplied in `pl_hdr_metadata`.
//
// These values will always be set to sane defaults even in the absence of
// such metadata. Note that they can also be combined with SDR color
// transforms, in which case it's assumed that the color transfer in
// question is linearly "stretched" relative to these values.
float nominal_min;
float nominal_max;

// HDR metadata for this color space, if present. (Optional)
struct pl_hdr_metadata hdr;

// Deprecated fields
enum pl_color_light light PL_DEPRECATED; // ignored
float sig_peak PL_DEPRECATED; // replaced by `hdr.max_luma`
float sig_avg PL_DEPRECATED; // replaced by `hdr.scene_avg`
float sig_floor PL_DEPRECATED; // replaced by `hdr.min_luma`
float sig_scale PL_DEPRECATED; // merged into `hdr.max/min_luma`
// Deprecated fields (Ignored)
enum pl_color_light light PL_DEPRECATED;
float sig_peak PL_DEPRECATED;
float sig_avg PL_DEPRECATED;
float sig_floor PL_DEPRECATED;
float sig_scale PL_DEPRECATED;
};

#define pl_color_space(...) (&(struct pl_color_space) { __VA_ARGS__ })
Expand Down
4 changes: 2 additions & 2 deletions src/renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ static void hdr_update_peak(struct pass_state *pass)
if (pass->fbofmt[4] && !(pass->fbofmt[4]->caps & PL_FMT_CAP_STORABLE))
goto cleanup;

if (pass->img.color.hdr.max_luma <= pass->target.color.hdr.max_luma + 1e-6)
if (pass->img.color.nominal_max <= pass->target.color.nominal_max + 1e-6)
goto cleanup; // no adaptation needed

if (params->lut && params->lut_type == PL_LUT_CONVERSION)
Expand Down Expand Up @@ -1241,7 +1241,7 @@ static bool plane_deband(struct pass_state *pass, struct img *img, float neutral
// of the source as possible, even though it happens this early in the
// process (well before any linearization / output adaptation)
struct pl_deband_params dparams = *params->deband_params;
dparams.grain /= image->color.hdr.max_luma / PL_COLOR_SDR_WHITE;
dparams.grain /= image->color.nominal_max / PL_COLOR_SDR_WHITE;
memcpy(dparams.grain_neutral, neutral, sizeof(dparams.grain_neutral));

img->tex = NULL;
Expand Down
20 changes: 10 additions & 10 deletions src/shaders/colorspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ void pl_shader_linearize(pl_shader sh, const struct pl_color_space *csp)
GLSL("// pl_shader_linearize \n"
"color.rgb = max(color.rgb, 0.0); \n");

float csp_min = csp->hdr.min_luma / PL_COLOR_SDR_WHITE;
float csp_max = csp->hdr.max_luma / PL_COLOR_SDR_WHITE;
float csp_min = csp->nominal_min / PL_COLOR_SDR_WHITE;
float csp_max = csp->nominal_max / PL_COLOR_SDR_WHITE;
csp_max = PL_DEF(csp_max, 1);

switch (csp->transfer) {
Expand Down Expand Up @@ -755,8 +755,8 @@ void pl_shader_delinearize(pl_shader sh, const struct pl_color_space *csp)
return;

GLSL("// pl_shader_delinearize \n");
float csp_min = csp->hdr.min_luma / PL_COLOR_SDR_WHITE;
float csp_max = csp->hdr.max_luma / PL_COLOR_SDR_WHITE;
float csp_min = csp->nominal_min / PL_COLOR_SDR_WHITE;
float csp_max = csp->nominal_max / PL_COLOR_SDR_WHITE;
csp_max = PL_DEF(csp_max, 1);

switch (csp->transfer) {
Expand Down Expand Up @@ -1253,10 +1253,10 @@ static void tone_map(pl_shader sh,
pl_shader_obj *state,
const struct pl_color_map_params *params)
{
float src_min = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, src->hdr.min_luma),
src_max = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, src->hdr.max_luma),
dst_min = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, dst->hdr.min_luma),
dst_max = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, dst->hdr.max_luma);
float src_min = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, src->nominal_min),
src_max = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, src->nominal_max),
dst_min = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, dst->nominal_min),
dst_max = pl_hdr_rescale(PL_HDR_NITS, PL_HDR_NORM, dst->nominal_max);

// Some tone mapping functions don't handle values of absolute 0 very well,
// so clip the minimums to a very small positive value
Expand Down Expand Up @@ -1559,8 +1559,8 @@ static void adapt_colors(pl_shader sh,
pl_get_color_mapping_matrix(&src->hdr.prim, &dst->hdr.prim, params->intent);

// Normalize colors to range [0-1]
float lb = dst->hdr.min_luma / PL_COLOR_SDR_WHITE;
float lw = dst->hdr.max_luma / PL_COLOR_SDR_WHITE;
float lb = dst->nominal_min / PL_COLOR_SDR_WHITE;
float lw = dst->nominal_max / PL_COLOR_SDR_WHITE;
GLSL("color.rgb = %s * color.rgb + %s; \n",
SH_FLOAT(1 / (lw - lb)), SH_FLOAT(-lb / (lw - lb)));

Expand Down
2 changes: 1 addition & 1 deletion src/tests/colorspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ int main()
};

pl_color_space_infer(&hlg);
REQUIRE(hlg.hdr.max_luma == 1000.0f);
REQUIRE(hlg.nominal_max == 1000.0f);

struct pl_color_space unknown = {0};
struct pl_color_space display = {
Expand Down

0 comments on commit edad2c4

Please sign in to comment.