util: add common ycbcr coefficient math code

This adds some reusable math to set up YCbCr to RGB color transforms. It
covers ITU BT.601, ITU BT.709 and ITU BT.2020 YUV <-> RGB conversion, as
well as "narrow"" and "full" range.

This code is intended to replace three different implementations of
YUV-transforms already present in Mesa, all of them with different
parameterizations and differences in data-formats. These implementations
are: nir_lower_tex.c, vk_nir_convert_ycbcr.c and vl_csc.c.

None of the exising implementations seems to fully cover all of the needs
of the others. The one that comes the closest is the one in vl_csc.c, but
it has a few issues:

1. It doesn't differentiate between per-channel bit-sizes, which the
   Vulkan code needs.
2. It uses enums from p_video_enums.h in Gallium to paremeterize the
   behavior.
3. It's written in a monolithic way, handling up to two
   range-remappings, which the other implementations doesn't need.

While it could be possible to entangle all of that, that would likely
end up being a more or less a new implementation anyway. So let's instead
try to pick the best of all three implementations into one new one,
that's broken into smaller pieces that can be assembled into either of
the three.

In addition, this implementation has a bunch of unit-tests, to make sure
we don't introduce subtle breakages down the line.

Reviewed-by: Eric R. Smith <eric.smith@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40175>
This commit is contained in:
Erik Faye-Lund 2026-02-16 13:56:49 +01:00 committed by Marge Bot
parent 1f93e1b831
commit f6f2e16e35
3 changed files with 679 additions and 0 deletions

View file

@ -466,6 +466,7 @@ if with_tests
'tests/u_memstream_test.cpp', 'tests/u_memstream_test.cpp',
'tests/u_printf_test.cpp', 'tests/u_printf_test.cpp',
'tests/u_qsort_test.cpp', 'tests/u_qsort_test.cpp',
'tests/u_ycbcr_test.cpp',
'tests/vector_test.cpp', 'tests/vector_test.cpp',
) )

View file

@ -0,0 +1,520 @@
/**
* Copyright (c) 2026 Collabora Ltd.
*
* SPDX-License-Identifier: MIT
*/
#include <math.h>
#include <gtest/gtest.h>
#include "util/u_ycbcr.h"
#include "macros.h"
static void
test_to_rgb_coeffs(const float coeffs[3])
{
float mat[3][4];
util_get_ycbcr_to_rgb_matrix(mat, coeffs);
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 2 - 2 * c;
float e = 2 - 2 * a;
const struct {
float input[3];
float expected[3];
} test_data[] = { {
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 1.0f, 1.0f },
}, {
{ 0.0f, 0.0f, 0.0f},
{ 0.0f, 0.0f, 0.0f },
}, {
{ 0.5f, 0.0f, 0.0f },
{ 0.5f, 0.5f, 0.5f },
}, {
{ a, -a / d, 0.5f },
{ 1.0f, 0.0f, 0.0f },
}, {
{ b, -b / d, -b / e },
{ 0.0f, 1.0f, 0.0f },
}, {
{ c, 0.5f, -c / e },
{ 0.0f, 0.0f, 1.0f },
}, {
{ 1 - c, -0.5f, c / e },
{ 1.0f, 1.0f, 0.0f },
}, {
{ 1 - a, a / d, (a - 1) / e },
{ 0.0f, 1.0f, 1.0f },
}, {
{ 1 - b, b / d, b / e },
{ 1.0f, 0.0f, 1.0f },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = test_data[i].input[0] * mat[c][0] +
test_data[i].input[1] * mat[c][1] +
test_data[i].input[2] * mat[c][2];
}
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7);
/* verify with reference equation */
float Y = test_data[i].input[0];
float Cb = test_data[i].input[1];
float Cr = test_data[i].input[2];
result[0] = Y + e * Cr;
result[1] = Y - (a * e / b) * Cr - (c * d / b) * Cb;
result[2] = Y + d * Cb;
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-6);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-6);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-6);
}
}
TEST(u_ycbcr_test, to_rgb)
{
test_to_rgb_coeffs(util_ycbcr_bt601_coeffs);
test_to_rgb_coeffs(util_ycbcr_bt709_coeffs);
test_to_rgb_coeffs(util_ycbcr_bt2020_coeffs);
}
static void
test_to_ycbcr_coeffs(const float coeffs[3])
{
float mat[3][4];
util_get_rgb_to_ycbcr_matrix(mat, coeffs);
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 2 - 2 * c;
float e = 2 - 2 * a;
const struct {
float input[3];
float expected[3];
} test_data[] = { {
{ 1.0f, 1.0f, 1.0f },
{ 1.0f, 0.0f, 0.0f},
}, {
{ 0.0f, 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.0f},
}, {
{ 0.5f, 0.5f, 0.5f },
{ 0.5f, 0.0f, 0.0f },
}, {
{ 1.0f, 0.0f, 0.0f },
{ a, -a / d, 0.5f },
}, {
{ 0.0f, 1.0f, 0.0f },
{ b, -b / d, -b / e },
}, {
{ 0.0f, 0.0f, 1.0f },
{ c, 0.5f, -c / e },
}, {
{ 1.0f, 1.0f, 0.0f },
{ 1 - c, -0.5f, c / e },
}, {
{ 0.0f, 1.0f, 1.0f },
{ 1 - a, a / d, (a - 1) / e },
}, {
{ 1.0f, 0.0f, 1.0f },
{ 1 - b, b / d, b / e },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = test_data[i].input[0] * mat[c][0] +
test_data[i].input[1] * mat[c][1] +
test_data[i].input[2] * mat[c][2];
}
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7);
/* verify with reference equation */
float R = test_data[i].input[0];
float G = test_data[i].input[1];
float B = test_data[i].input[2];
float Y = a * R + b * G + c * B;;
result[0] = Y;
result[1] = (B - Y) / d;
result[2] = (R - Y) / e;
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7);
}
}
TEST(u_ycbcr_test, to_ycbcr)
{
test_to_ycbcr_coeffs(util_ycbcr_bt601_coeffs);
test_to_ycbcr_coeffs(util_ycbcr_bt709_coeffs);
test_to_ycbcr_coeffs(util_ycbcr_bt2020_coeffs);
}
static void
test_to_ycbcr_and_back_coeffs(const float coeffs[3])
{
float to_ycbcr[3][4], to_rgb[3][4];
util_get_rgb_to_ycbcr_matrix(to_ycbcr, coeffs);
util_get_ycbcr_to_rgb_matrix(to_rgb, coeffs);
float inputs[][3] = {
{ 1.0f, 1.0f, 1.0f },
{ 0.0f, 0.0f, 0.0f },
{ 0.5f, 0.5f, 0.5f },
{ 1.0f, 0.0f, 0.0f },
{ 0.0f, 1.0f, 0.0f },
{ 0.0f, 0.0f, 1.0f },
{ 1.0f, 1.0f, 0.0f },
{ 0.0f, 1.0f, 1.0f },
{ 1.0f, 0.0f, 1.0f },
};
for (size_t i = 0; i < ARRAY_SIZE(inputs); ++i) {
float ycbcr[3];
for (int c = 0; c < 3; ++c) {
ycbcr[c] = inputs[i][0] * to_ycbcr[c][0] +
inputs[i][1] * to_ycbcr[c][1] +
inputs[i][2] * to_ycbcr[c][2];
}
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = ycbcr[0] * to_rgb[c][0] +
ycbcr[1] * to_rgb[c][1] +
ycbcr[2] * to_rgb[c][2];
}
EXPECT_NEAR(inputs[i][0], result[0], 1e-6);
EXPECT_NEAR(inputs[i][1], result[1], 1e-6);
EXPECT_NEAR(inputs[i][2], result[2], 1e-6);
}
}
TEST(u_ycbcr_test, to_ycbcr_and_back)
{
test_to_ycbcr_and_back_coeffs(util_ycbcr_bt601_coeffs);
test_to_ycbcr_and_back_coeffs(util_ycbcr_bt709_coeffs);
test_to_ycbcr_and_back_coeffs(util_ycbcr_bt2020_coeffs);
}
TEST(u_ycbcr_test, full_range)
{
float range[3][2];
unsigned bpc[3] = {8, 8, 8};
util_get_full_range_coeffs(range, bpc);
const struct {
uint8_t input[3];
float expected[3];
} test_data[] = { {
{ 0, 128, 128 },
{ 0.0f, 0.0f, 0.0f },
}, {
{ 255, 128, 128 },
{ 1.0f, 0.0f, 0.0f },
}, {
{ 0, 0, 255 },
{ 0.0f, -128.0f / 255, 127.0f / 255 },
}, {
{ 255, 255, 0 },
{ 1.0f, 127.0f / 255, -128.0f / 255 },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float input[3] = {
test_data[i].input[0] / 255.0f,
test_data[i].input[1] / 255.0f,
test_data[i].input[2] / 255.0f,
};
float result[3];
for (int c = 0; c < 3; ++c)
result[c] = input[c] * range[c][0] + range[c][1];
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7);
}
}
TEST(u_ycbcr_test, narrow_range)
{
float range[3][2];
unsigned bpc[3] = {8, 8, 8};
util_get_narrow_range_coeffs(range, bpc);
const struct {
uint8_t input[3];
float expected[3];
} test_data[] = { {
{ 16, 128, 128 },
{ 0.0f, 0.0f, 0.0f },
}, {
{ 235, 128, 128 },
{ 1.0f, 0.0f, 0.0f },
}, {
{ 16, 16, 240 },
{ 0.0f, -0.5f, 0.5f },
}, {
{ 235, 240, 16 },
{ 1.0f, 0.5f, -0.5f },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float input[3] = {
test_data[i].input[0] / 255.0f,
test_data[i].input[1] / 255.0f,
test_data[i].input[2] / 255.0f,
};
float result[3];
for (int c = 0; c < 3; ++c)
result[c] = input[c] * range[c][0] + range[c][1];
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7);
}
}
static void
test_to_rgb_narrow_range_coeffs(const float coeffs[3])
{
const unsigned bpc[3] = {8, 8, 10};
float range[3][2];
util_get_narrow_range_coeffs(range, bpc);
float mat[3][4];
util_get_ycbcr_to_rgb_matrix(mat, coeffs);
util_ycbcr_adjust_from_range(mat, range);
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 2 - 2 * c;
float e = 2 - 2 * a;
const struct {
float input[3];
float expected[3];
} test_data[] = { {
{ 1.0f, 0.0f, 0.0f},
{ 1.0f, 1.0f, 1.0f },
}, {
{ 0.0f, 0.0f, 0.0f},
{ 0.0f, 0.0f, 0.0f },
}, {
{ 0.5f, 0.0f, 0.0f },
{ 0.5f, 0.5f, 0.5f },
}, {
{ a, -a / d, 0.5f },
{ 1.0f, 0.0f, 0.0f },
}, {
{ b, -b / d, -b / e },
{ 0.0f, 1.0f, 0.0f },
}, {
{ c, 0.5f, -c / e },
{ 0.0f, 0.0f, 1.0f },
}, {
{ 1 - c, -0.5f, c / e },
{ 1.0f, 1.0f, 0.0f },
}, {
{ 1 - a, a / d, (a - 1) / e },
{ 0.0f, 1.0f, 1.0f },
}, {
{ 1 - b, b / d, b / e },
{ 1.0f, 0.0f, 1.0f },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float input[3] = {
(16 + test_data[i].input[0] * (235 - 16)) / 255.0f,
(16 + (test_data[i].input[1] + 0.5f) * (240 - 16)) / 255.0f,
(16 + (test_data[i].input[2] + 0.5f) * (240 - 16)) / 255.75f,
};
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = input[0] * mat[c][0] +
input[1] * mat[c][1] +
input[2] * mat[c][2] +
mat[c][3];
}
EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-6);
EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-6);
EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-6);
}
}
TEST(u_ycbcr_test, bt601_to_rgb_narrow_range)
{
test_to_rgb_narrow_range_coeffs(util_ycbcr_bt601_coeffs);
test_to_rgb_narrow_range_coeffs(util_ycbcr_bt709_coeffs);
test_to_rgb_narrow_range_coeffs(util_ycbcr_bt2020_coeffs);
}
static void
test_to_ycbcr_narrow_range_coeffs(const float coeffs[3])
{
const unsigned bpc[3] = {8, 8, 10};
float range[3][2];
util_get_narrow_range_coeffs(range, bpc);
float mat[3][4];
util_get_rgb_to_ycbcr_matrix(mat, coeffs);
util_ycbcr_adjust_to_range(mat, range);
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 2 - 2 * c;
float e = 2 - 2 * a;
const struct {
float input[3];
float expected[3];
} test_data[] = { {
{ 1.0f, 1.0f, 1.0f },
{ 1.0f, 0.0f, 0.0f},
}, {
{ 0.0f, 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.0f},
}, {
{ 0.5f, 0.5f, 0.5f },
{ 0.5f, 0.0f, 0.0f },
}, {
{ 1.0f, 0.0f, 0.0f },
{ a, -a / d, 0.5f },
}, {
{ 0.0f, 1.0f, 0.0f },
{ b, -b / d, -b / e },
}, {
{ 0.0f, 0.0f, 1.0f },
{ c, 0.5f, -c / e },
}, {
{ 1.0f, 1.0f, 0.0f },
{ 1 - c, -0.5f, c / e },
}, {
{ 0.0f, 1.0f, 1.0f },
{ 1 - a, a / d, (a - 1) / e },
}, {
{ 1.0f, 0.0f, 1.0f },
{ 1 - b, b / d, b / e },
}
};
for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) {
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = test_data[i].input[0] * mat[c][0] +
test_data[i].input[1] * mat[c][1] +
test_data[i].input[2] * mat[c][2] +
mat[c][3];
}
float expected[3] = {
(16 + test_data[i].expected[0] * (235 - 16)) / 255.0f,
(16 + (test_data[i].expected[1] + 0.5f) * (240 - 16)) / 255.0f,
(16 + (test_data[i].expected[2] + 0.5f) * (240 - 16)) / 255.75f,
};
EXPECT_NEAR(expected[0], result[0], 1e-6);
EXPECT_NEAR(expected[1], result[1], 1e-6);
EXPECT_NEAR(expected[2], result[2], 1e-6);
}
}
TEST(u_ycbcr_test, bt601_to_ycbcr_narrow_range)
{
test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt601_coeffs);
test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt709_coeffs);
test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt2020_coeffs);
}
static void
test_to_ycbcr_range_and_back_coeffs(const float coeffs[3])
{
const unsigned bpc[3] = {8, 8, 10};
float range[3][2];
util_get_narrow_range_coeffs(range, bpc);
float to_ycbcr[3][4];
util_get_rgb_to_ycbcr_matrix(to_ycbcr, coeffs);
util_ycbcr_adjust_to_range(to_ycbcr, range);
float to_rgb[3][4];
util_get_ycbcr_to_rgb_matrix(to_rgb, coeffs);
util_ycbcr_adjust_from_range(to_rgb, range);
float inputs[][3] = {
{ 1.0f, 1.0f, 1.0f },
{ 0.0f, 0.0f, 0.0f },
{ 0.5f, 0.5f, 0.5f },
{ 1.0f, 0.0f, 0.0f },
{ 0.0f, 1.0f, 0.0f },
{ 0.0f, 0.0f, 1.0f },
{ 1.0f, 1.0f, 0.0f },
{ 0.0f, 1.0f, 1.0f },
{ 1.0f, 0.0f, 1.0f },
};
for (size_t i = 0; i < ARRAY_SIZE(inputs); ++i) {
float ycbcr[3];
for (int c = 0; c < 3; ++c) {
ycbcr[c] = inputs[i][0] * to_ycbcr[c][0] +
inputs[i][1] * to_ycbcr[c][1] +
inputs[i][2] * to_ycbcr[c][2];
}
float result[3];
for (int c = 0; c < 3; ++c) {
result[c] = ycbcr[0] * to_rgb[c][0] +
ycbcr[1] * to_rgb[c][1] +
ycbcr[2] * to_rgb[c][2];
}
EXPECT_NEAR(inputs[i][0], result[0], 1e-6);
EXPECT_NEAR(inputs[i][1], result[1], 1e-6);
EXPECT_NEAR(inputs[i][2], result[2], 1e-6);
}
}
TEST(u_ycbcr_test, to_ycbcr_range_and_back)
{
test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt601_coeffs);
test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt709_coeffs);
test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt2020_coeffs);
}

158
src/util/u_ycbcr.h Normal file
View file

@ -0,0 +1,158 @@
/**
* Copyright (c) 2026 Collabora Ltd.
*
* SPDX-License-Identifier: MIT
*/
#ifndef U_YCBCR_H
#define U_YCBCR_H
/* BT.601 coefficients */
static const float util_ycbcr_bt601_coeffs[3] = {
0.299f, 0.587f, 0.114f
};
/* BT.701 coefficients */
static const float util_ycbcr_bt709_coeffs[3] = {
0.2126f, 0.7152f, 0.0722f
};
/* BT.2020 coefficients */
static const float util_ycbcr_bt2020_coeffs[3] = {
0.2627f, 0.6780f, 0.0593f
};
/* SMPTE 240M coefficients */
static const float util_ycbcr_smpte240m_coeffs[3] = {
0.2122f, 0.7013f, 0.0865f
};
static inline void
util_get_ycbcr_to_rgb_matrix(float m[3][4], const float coeffs[3])
{
/**
* Sets up a 3x4 matrix that computes:
*
* R = Y + e * Cr
* G = Y - (a * e / b) * Cr - (c * d / b) * Cb
* B = Y + d * Cb
*/
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 2 - 2 * c;
float e = 2 - 2 * a;
float f = 1.0f / b;
m[0][0] = 1; m[0][1] = 0; m[0][2] = e; m[0][3] = 0;
m[1][0] = 1; m[1][1] = -c * d * f; m[1][2] = -a * e * f; m[1][3] = 0;
m[2][0] = 1; m[2][1] = d; m[2][2] = 0; m[2][3] = 0;
}
static inline void
util_get_rgb_to_ycbcr_matrix(float m[3][4], const float coeffs[3])
{
/**
* Sets up a 3x4 matrix that computes:
*
* Y = a * R + b * G + c * B
* Cb = (B - Y) / d
* Cr = (R - Y) / e
*/
float a = coeffs[0];
float b = coeffs[1];
float c = coeffs[2];
float d = 0.5f / (c - 1);
float e = 0.5f / (a - 1);
m[0][0] = a; m[0][1] = b; m[0][2] = c; m[0][3] = 0;
m[1][0] = d * a; m[1][1] = d * b; m[1][2] = 0.5f; m[1][3] = 0;
m[2][0] = 0.5f; m[2][1] = e * b; m[2][2] = e * c; m[2][3] = 0;
}
static inline float
util_get_full_range_chroma_bias(unsigned bpc)
{
return -(1 << (bpc - 1)) / ((1 << bpc) - 1.0f);
}
static inline void
util_get_full_range_coeffs(float out[3][2], const unsigned bpc[3])
{
out[0][0] = 1; out[0][1] = 0;
out[1][0] = 1; out[1][1] = util_get_full_range_chroma_bias(bpc[1]);
out[2][0] = 1; out[2][1] = util_get_full_range_chroma_bias(bpc[2]);
}
static inline float
util_get_narrow_range(unsigned bpc)
{
return 1 - 1.0f / (1 << bpc);
}
static inline float
util_get_narrow_range_luma_factor(unsigned bpc)
{
return util_get_narrow_range(bpc) * (256.0f / 219);
}
static inline float
util_get_narrow_range_chroma_factor(unsigned bpc)
{
return util_get_narrow_range(bpc) * (256.0f / 224);
}
static inline void
util_get_narrow_range_coeffs(float out[3][2], const unsigned bpc[3])
{
float y_factor = util_get_narrow_range_luma_factor(bpc[0]);
float cb_factor = util_get_narrow_range_chroma_factor(bpc[1]);
float cr_factor = util_get_narrow_range_chroma_factor(bpc[2]);
float y_bias = -16.0f / 219;
float c_bias = -128.0f / 224;
out[0][0] = y_factor; out[0][1] = y_bias;
out[1][0] = cb_factor; out[1][1] = c_bias;
out[2][0] = cr_factor; out[2][1] = c_bias;
}
static inline void
util_get_identity_range_coeffs(float out[3][2])
{
out[0][0] = out[1][0] = out[2][0] = 1.0f;
out[0][1] = out[1][1] = out[2][1] = 0.0f;
}
static inline void
util_ycbcr_adjust_from_range(float mat[3][4],
const float range[3][2])
{
for (int i = 0; i < 3; ++i) {
mat[i][3] = range[0][1] * mat[i][0] +
range[1][1] * mat[i][1] +
range[2][1] * mat[i][2] +
mat[i][3];
mat[i][0] = mat[i][0] * range[0][0];
mat[i][1] = mat[i][1] * range[1][0];
mat[i][2] = mat[i][2] * range[2][0];
}
}
static inline void
util_ycbcr_adjust_to_range(float mat[3][4],
const float range[3][2])
{
for (int i = 0; i < 3; ++i) {
float tmp = 1.0f / range[i][0];
mat[i][0] = mat[i][0] * tmp;
mat[i][1] = mat[i][1] * tmp;
mat[i][2] = mat[i][2] * tmp;
mat[i][3] -= range[i][1] * tmp;
}
}
#endif /* U_YCBCR_H */