Merge branch 'GL_EXT_YUV_target' into 'main'

Draft: add support for GL_EXT_YUV_target

See merge request mesa/mesa!40191
This commit is contained in:
Erik Faye-Lund 2026-03-11 05:56:16 +01:00
commit ebe7eebe18
22 changed files with 1072 additions and 4 deletions

View file

@ -584,6 +584,7 @@ move_tex_coords(struct move_tex_coords_state *state, nir_function_impl *impl, ni
case GLSL_SAMPLER_DIM_3D:
case GLSL_SAMPLER_DIM_CUBE:
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
break;
case GLSL_SAMPLER_DIM_RECT:
case GLSL_SAMPLER_DIM_BUF:

View file

@ -100,6 +100,8 @@ simple_type("dmat4x3", "GL_DOUBLE_MAT4x3", "GLSL_TYPE_DOUBLE", 3, 4)
simple_type("atomic_uint", "GL_UNSIGNED_INT_ATOMIC_COUNTER", "GLSL_TYPE_ATOMIC_UINT", 1, 1)
simple_type("yuvCscStandardEXT", "GL_INVALID_ENUM", "GLSL_TYPE_INT", 1, 1)
sampler_type("sampler", "GL_SAMPLER_1D", "GLSL_TYPE_SAMPLER", "GLSL_SAMPLER_DIM_1D", 0, 0, "GLSL_TYPE_VOID")
sampler_type("sampler1D", "GL_SAMPLER_1D", "GLSL_TYPE_SAMPLER", "GLSL_SAMPLER_DIM_1D", 0, 0, "GLSL_TYPE_FLOAT")
sampler_type("sampler2D", "GL_SAMPLER_2D", "GLSL_TYPE_SAMPLER", "GLSL_SAMPLER_DIM_2D", 0, 0, "GLSL_TYPE_FLOAT")
@ -148,6 +150,8 @@ sampler_type("sampler2DRectShadow", "GL_SAMPLER_2D_RECT_SHADOW", "GLSL
sampler_type("samplerExternalOES", "GL_SAMPLER_EXTERNAL_OES", "GLSL_TYPE_SAMPLER", "GLSL_SAMPLER_DIM_EXTERNAL", 0, 0, "GLSL_TYPE_FLOAT")
sampler_type("samplerExternal2DY2YEXT", "GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT", "GLSL_TYPE_SAMPLER", "GLSL_SAMPLER_DIM_2D", 0, 0, "GLSL_TYPE_FLOAT")
sampler_type("texture1D", "GL_SAMPLER_1D", "GLSL_TYPE_TEXTURE", "GLSL_SAMPLER_DIM_1D", 0, 0, "GLSL_TYPE_FLOAT")
sampler_type("texture2D", "GL_SAMPLER_2D", "GLSL_TYPE_TEXTURE", "GLSL_SAMPLER_DIM_2D", 0, 0, "GLSL_TYPE_FLOAT")
sampler_type("texture3D", "GL_SAMPLER_3D", "GLSL_TYPE_TEXTURE", "GLSL_SAMPLER_DIM_3D", 0, 0, "GLSL_TYPE_FLOAT")

View file

@ -204,6 +204,8 @@ enum ast_operators {
ast_int64_constant,
ast_uint64_constant,
ast_csc_standard,
ast_sequence,
ast_aggregate
@ -266,6 +268,7 @@ public:
double double_constant;
uint64_t uint64_constant;
int64_t int64_constant;
enum yuv_csc_standard csc_standard;
} primary_expression;
@ -687,6 +690,9 @@ struct ast_type_qualifier {
unsigned task_payload:1;
unsigned per_primitive:1;
unsigned max_primitives:1;
/** GL_EXT_YUV_target */
unsigned yuv:1;
}
/** \brief Set of flags, accessed by name. */
q;

View file

@ -2156,7 +2156,8 @@ ast_function_expression::handle_method(ir_exec_list *instructions,
static inline bool is_valid_constructor(const glsl_type *type,
struct _mesa_glsl_parse_state *state)
{
return glsl_type_is_numeric(type) || glsl_type_is_boolean(type) ||
return (glsl_type_is_numeric(type) && type != &glsl_type_builtin_yuvCscStandardEXT) ||
glsl_type_is_boolean(type) ||
(state->has_bindless() && (glsl_type_is_sampler(type) || glsl_type_is_image(type)));
}

View file

@ -363,6 +363,12 @@ apply_implicit_conversion(const glsl_type *to, ir_rvalue * &from,
}
}
static bool
is_arithmetic_type(const glsl_type *type)
{
return glsl_type_is_numeric(type) &&
type != &glsl_type_builtin_yuvCscStandardEXT;
}
static const struct glsl_type *
arithmetic_result_type(ir_rvalue * &value_a, ir_rvalue * &value_b,
@ -378,7 +384,7 @@ arithmetic_result_type(ir_rvalue * &value_a, ir_rvalue * &value_b,
* multiply (*), and divide (/) operate on integer and
* floating-point scalars, vectors, and matrices."
*/
if (!glsl_type_is_numeric(type_a) || !glsl_type_is_numeric(type_b)) {
if (!is_arithmetic_type(type_a) || !is_arithmetic_type(type_b)) {
_mesa_glsl_error(loc, state,
"operands to arithmetic operators must be numeric");
return &glsl_type_builtin_error;
@ -517,7 +523,7 @@ unary_arithmetic_result_type(const struct glsl_type *type,
* component-wise on their operands. These result with the same type
* they operated on."
*/
if (!glsl_type_is_numeric(type)) {
if (!is_arithmetic_type(type)) {
_mesa_glsl_error(loc, state,
"operands to arithmetic operators must be numeric");
return &glsl_type_builtin_error;
@ -1624,6 +1630,10 @@ ast_expression::do_hir(ir_exec_list *instructions,
glsl_contains_opaque(op[1]->type))) {
_mesa_glsl_error(&loc, state, "opaque type comparisons forbidden");
error_emitted = true;
} else if (op[0]->type == &glsl_type_builtin_yuvCscStandardEXT ||
op[1]->type == &glsl_type_builtin_yuvCscStandardEXT) {
_mesa_glsl_error(&loc, state, "yuvCscStandardEXT comparisons forbidden");
error_emitted = true;
}
if (error_emitted) {
@ -2197,6 +2207,14 @@ ast_expression::do_hir(ir_exec_list *instructions,
result = new(linalloc) ir_constant(this->primary_expression.int64_constant);
break;
case ast_csc_standard: {
ir_constant_data data = { { 0 } };
data.i[0] = this->primary_expression.csc_standard;
result = new(linalloc) ir_constant(&glsl_type_builtin_yuvCscStandardEXT,
&data);
break;
}
case ast_sequence: {
/* It should not be possible to generate a sequence in the AST without
* any expressions in it.
@ -2326,6 +2344,7 @@ ast_expression::has_sequence_subexpression() const
case ast_double_constant:
case ast_int64_constant:
case ast_uint64_constant:
case ast_csc_standard:
return false;
case ast_aggregate:
@ -2633,6 +2652,13 @@ get_type_name_for_precision_qualifier(const glsl_type *type)
};
return names[type_idx];
}
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y: {
assert(glsl_type_is_sampler(type));
static const char *const names[4] = {
"__samplerExternal2DY2YEXT", NULL, NULL, NULL
};
return names[type_idx];
}
default:
UNREACHABLE("Unsupported sampler/image dimensionality");
} /* sampler/image float dimensionality */
@ -4221,6 +4247,9 @@ apply_type_qualifier_to_variable(const struct ast_type_qualifier *qual,
if (qual->flags.q.patch)
var->data.patch = 1;
if (qual->flags.q.yuv)
var->data.yuv = 1;
if (qual->flags.q.attribute && state->stage != MESA_SHADER_VERTEX) {
var->type = &glsl_type_builtin_error;
_mesa_glsl_error(loc, state,
@ -9455,10 +9484,13 @@ detect_conflicting_assignments(struct _mesa_glsl_parse_state *state,
{
bool gl_FragColor_assigned = false;
bool gl_FragData_assigned = false;
bool gl_FragDepth_assigned = false;
bool gl_FragSecondaryColor_assigned = false;
bool gl_FragSecondaryData_assigned = false;
bool user_defined_fs_output_assigned = false;
ir_variable *user_defined_fs_output = NULL;
bool yuv_layout_used = false;
int color_outputs = 0;
/* It would be nice to have proper location information. */
YYLTYPE loc;
@ -9467,7 +9499,13 @@ detect_conflicting_assignments(struct _mesa_glsl_parse_state *state,
ir_foreach_in_list(ir_instruction, node, instructions) {
ir_variable *var = node->as_variable();
if (!var || !var->data.assigned)
if (!var)
continue;
if (var->data.yuv)
yuv_layout_used = true;
if (!var->data.assigned)
continue;
if (strcmp(var->name, "gl_FragColor") == 0) {
@ -9485,11 +9523,14 @@ detect_conflicting_assignments(struct _mesa_glsl_parse_state *state,
gl_FragSecondaryColor_assigned = true;
else if (strcmp(var->name, "gl_SecondaryFragDataEXT") == 0)
gl_FragSecondaryData_assigned = true;
else if (strcmp(var->name, "gl_FragDepth") == 0)
gl_FragDepth_assigned = true;
else if (!is_gl_identifier(var->name)) {
if (state->stage == MESA_SHADER_FRAGMENT &&
var->data.mode == ir_var_shader_out) {
user_defined_fs_output_assigned = true;
user_defined_fs_output = var;
color_outputs++;
}
}
}
@ -9539,6 +9580,28 @@ detect_conflicting_assignments(struct _mesa_glsl_parse_state *state,
_mesa_glsl_error(&loc, state,
"Dual source blending requires EXT_blend_func_extended");
}
if (yuv_layout_used) {
/**
* From the GL_EXT_YUV_target spec:
*
* "Additionally if the shader qualifies fragment shader output with
* the new yuv qualifier and write depth or multiple color output,
* it would cause compilation failure."
*
* However, since the extension requires GLSL ES 3.00, we don't need to
* consider interactions with gl_FragColor and gl_FragData.
*/
if (gl_FragDepth_assigned) {
_mesa_glsl_error(&loc, state, "fragment shader uses yuv-layout and "
"`gl_FragDepth'");
} else if (color_outputs > 1) {
_mesa_glsl_error(&loc, state, "fragment shader uses yuv-layout and "
"multiple color-outputs");
}
}
}
static void

View file

@ -546,6 +546,25 @@ ast_type_qualifier::merge_qualifier(YYLTYPE *loc,
}
}
if ((this->flags.q.explicit_location && q.flags.q.yuv) ||
(this->flags.q.yuv && q.flags.q.explicit_location)) {
/***
* The EXT_YUV_target spec says:
* The new yuv layout qualifier can't be combined with any other
* layout qualifier, <snip>
*
* However, forbidding *all* layout qualifiers seems like a massive
* over-reaction, and limiting potentially useful stuff. So instead,
* let's just disallow the location=n layout. This is the only valid
* output layout in GLSL ES 3.00, and the extension spec explicitly
* says that it's not valid together with multiple color outputs
* anyway.
*/
_mesa_glsl_error(loc, state, "yuv layout can't be combined with any "
"other layout qualifier");
return false;
}
return r;
}

View file

@ -87,6 +87,7 @@
#include <math.h>
#include "builtin_functions.h"
#include "util/hash_table.h"
#include "util/u_ycbcr.h"
#ifndef M_PIf
#define M_PIf ((float) M_PI)
@ -230,6 +231,14 @@ texture_external_es3(const _mesa_glsl_parse_state *state)
state->is_version(0, 300);
}
static bool
texture_external_2d_y2y(const _mesa_glsl_parse_state *state)
{
return state->EXT_YUV_target_enable &&
state->es_shader &&
state->is_version(0, 300);
}
static bool
texture_shadow2Dext(const _mesa_glsl_parse_state *state)
{
@ -1226,6 +1235,11 @@ private:
unsigned num_arguments,
unsigned flags);
ir_variable *get_to_ycbcr_matrix(ir_factory &body, const char *name,
const float coeffs[3], bool full_range);
ir_variable *get_to_rgb_matrix(ir_factory &body, const char *name,
const float coeffs[3], bool full_range);
/**
* Create a new image built-in function for all known image types.
* \p flags is a bitfield of \c image_function_flags flags.
@ -1599,6 +1613,9 @@ private:
ir_function_signature *_set_mesh_outputs_intrinsic();
ir_function_signature *_set_mesh_outputs();
ir_function_signature *_rgb_2_yuv();
ir_function_signature *_yuv_2_rgb();
#undef B0
#undef B1
#undef B2
@ -2856,6 +2873,8 @@ builtin_builder::create_builtins()
_textureSize(texture_multisample_array, &glsl_type_builtin_ivec3, &glsl_type_builtin_usampler2DMSArray),
_textureSize(texture_external_es3, &glsl_type_builtin_ivec2, &glsl_type_builtin_samplerExternalOES),
_textureSize(texture_external_2d_y2y, &glsl_type_builtin_ivec2, &glsl_type_builtin_samplerExternal2DY2YEXT),
NULL);
add_function("textureSize1D",
@ -2964,6 +2983,8 @@ builtin_builder::create_builtins()
_texture(ir_tex, texture_external_es3, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternalOES, &glsl_type_builtin_vec2),
_texture(ir_tex, texture_external_2d_y2y, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternal2DY2YEXT, &glsl_type_builtin_vec2),
_texture(ir_txb, v130_derivatives_only, &glsl_type_builtin_vec4, &glsl_type_builtin_sampler1D, &glsl_type_builtin_float),
_texture(ir_txb, v130_derivatives_only, &glsl_type_builtin_ivec4, &glsl_type_builtin_isampler1D, &glsl_type_builtin_float),
_texture(ir_txb, v130_derivatives_only, &glsl_type_builtin_uvec4, &glsl_type_builtin_usampler1D, &glsl_type_builtin_float),
@ -3209,6 +3230,8 @@ builtin_builder::create_builtins()
_texture(ir_tex, v130, &glsl_type_builtin_ivec4, &glsl_type_builtin_isampler2DRect, &glsl_type_builtin_vec3, TEX_PROJECT),
_texture(ir_tex, texture_external_es3, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternalOES, &glsl_type_builtin_vec3, TEX_PROJECT),
_texture(ir_tex, texture_external_es3, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternalOES, &glsl_type_builtin_vec4, TEX_PROJECT),
_texture(ir_tex, texture_external_2d_y2y, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternal2DY2YEXT, &glsl_type_builtin_vec3, TEX_PROJECT),
_texture(ir_tex, texture_external_2d_y2y, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternal2DY2YEXT, &glsl_type_builtin_vec4, TEX_PROJECT),
_texture(ir_tex, v130, &glsl_type_builtin_uvec4, &glsl_type_builtin_usampler2DRect, &glsl_type_builtin_vec3, TEX_PROJECT),
_texture(ir_tex, v130, &glsl_type_builtin_vec4, &glsl_type_builtin_sampler2DRect, &glsl_type_builtin_vec4, TEX_PROJECT),
@ -3278,6 +3301,8 @@ builtin_builder::create_builtins()
_texelFetch(texture_external_es3, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternalOES, &glsl_type_builtin_ivec2),
_texelFetch(texture_external_2d_y2y, &glsl_type_builtin_vec4, &glsl_type_builtin_samplerExternal2DY2YEXT, &glsl_type_builtin_ivec2),
NULL);
add_function("texelFetch1D",
@ -6044,6 +6069,9 @@ builtin_builder::create_builtins()
add_function("EmitMeshTasksEXT", _emit_mesh_tasks(), NULL);
add_function("SetMeshOutputsEXT", _set_mesh_outputs(), NULL);
add_function("rgb_2_yuv", _rgb_2_yuv(), NULL);
add_function("yuv_2_rgb", _yuv_2_rgb(), NULL);
#undef F
#undef FI
#undef FIUDHF_VEC
@ -9623,6 +9651,158 @@ ir_function_signature *builtin_builder::_set_mesh_outputs()
return sig;
}
ir_variable *builtin_builder::get_to_ycbcr_matrix(ir_factory &body,
const char *name,
const float coeffs[3],
bool full_range)
{
float data[3][4];
util_get_rgb_to_ycbcr_matrix(data, coeffs);
const unsigned bpc[3] = { 8, 8, 8 };
float range[3][2];
if (full_range)
util_get_full_range_coeffs(range, bpc);
else
util_get_narrow_range_coeffs(range, bpc);
util_ycbcr_adjust_to_range(data, range);
ir_variable *var = body.make_temp(&glsl_type_builtin_mat4x3, name);
for (int i = 0; i < 4; ++i) {
ir_constant_data col_data;
col_data.f[0] = data[0][i];
col_data.f[1] = data[1][i];
col_data.f[2] = data[2][i];
ir_constant *col = imm(&glsl_type_builtin_vec3, col_data);
body.emit(assign(array_ref(var, i), col));
}
return var;
}
ir_variable *builtin_builder::get_to_rgb_matrix(ir_factory &body,
const char *name,
const float coeffs[3],
bool full_range)
{
float data[3][4];
util_get_ycbcr_to_rgb_matrix(data, coeffs);
const unsigned bpc[3] = { 8, 8, 8 };
float range[3][2];
if (full_range)
util_get_full_range_coeffs(range, bpc);
else
util_get_narrow_range_coeffs(range, bpc);
util_ycbcr_adjust_from_range(data, range);
ir_variable *var = body.make_temp(&glsl_type_builtin_mat4x3, name);
for (int i = 0; i < 4; ++i) {
ir_constant_data col_data;
col_data.f[0] = data[0][i];
col_data.f[1] = data[1][i];
col_data.f[2] = data[2][i];
ir_constant *col = imm(&glsl_type_builtin_vec3, col_data);
body.emit(assign(array_ref(var, i), col));
}
return var;
}
ir_function_signature *
builtin_builder::_rgb_2_yuv()
{
ir_variable *color = in_var(&glsl_type_builtin_vec3, "color");
ir_variable *conv_standard = in_var(&glsl_type_builtin_yuvCscStandardEXT, "conv_standard");
MAKE_SIG(&glsl_type_builtin_vec3, texture_external_2d_y2y, 2, color, conv_standard);
ir_swizzle *r = swizzle(color, SWIZZLE_XXXX, 3);
ir_swizzle *g = swizzle(color, SWIZZLE_YYYY, 3);
ir_swizzle *b = swizzle(color, SWIZZLE_ZZZZ, 3);
ir_constant_data data;
memset(&data, 0, sizeof(data));
data.i[0] = YUV_CSC_STANDARD_601;
ir_constant *itu_601 = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
data.i[0] = YUV_CSC_STANDARD_601_FULL_RANGE;
ir_constant *itu_601_full_range = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
data.i[0] = YUV_CSC_STANDARD_709;
ir_constant *itu_709 = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
ir_variable *m = body.make_temp(&glsl_type_builtin_mat4x3, "m");
body.emit(
if_tree(
equal(conv_standard, itu_601),
assign(m, get_to_ycbcr_matrix(body, "m_601", util_ycbcr_bt601_coeffs, false)),
if_tree(
equal(conv_standard, itu_601_full_range),
assign(m, get_to_ycbcr_matrix(body, "m_601f", util_ycbcr_bt601_coeffs, true)),
if_tree(
equal(conv_standard, itu_709),
assign(m, get_to_ycbcr_matrix(body, "m_709", util_ycbcr_bt709_coeffs, false))
)
)
)
);
body.emit(ret(
ir_builder::fma(r, array_ref(m, 0),
ir_builder::fma(g, array_ref(m, 1),
ir_builder::fma(b, array_ref(m, 2),
array_ref(m, 3))))));
return sig;
}
ir_function_signature *
builtin_builder::_yuv_2_rgb()
{
ir_variable *color = in_var(&glsl_type_builtin_vec3, "color");
ir_variable *conv_standard = in_var(&glsl_type_builtin_yuvCscStandardEXT, "conv_standard");
MAKE_SIG(&glsl_type_builtin_vec3, texture_external_2d_y2y, 2, color, conv_standard);
ir_swizzle *y = swizzle(color, SWIZZLE_XXXX, 3);
ir_swizzle *u = swizzle(color, SWIZZLE_YYYY, 3);
ir_swizzle *v = swizzle(color, SWIZZLE_ZZZZ, 3);
ir_constant_data data;
memset(&data, 0, sizeof(data));
data.i[0] = YUV_CSC_STANDARD_601;
ir_constant *itu_601 = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
data.i[0] = YUV_CSC_STANDARD_601_FULL_RANGE;
ir_constant *itu_601_full_range = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
data.i[0] = YUV_CSC_STANDARD_709;
ir_constant *itu_709 = imm(&glsl_type_builtin_yuvCscStandardEXT, data);
ir_variable *m = body.make_temp(&glsl_type_builtin_mat4x3, "m");
body.emit(
if_tree(
equal(conv_standard, itu_601),
assign(m, get_to_rgb_matrix(body, "m_601", util_ycbcr_bt601_coeffs, false)),
if_tree(
equal(conv_standard, itu_601_full_range),
assign(m, get_to_rgb_matrix(body, "m_601f", util_ycbcr_bt601_coeffs, true)),
if_tree(
equal(conv_standard, itu_709),
assign(m, get_to_rgb_matrix(body, "m_709", util_ycbcr_bt709_coeffs, false))
)
)
)
);
body.emit(ret(
ir_builder::fma(y, array_ref(m, 0),
ir_builder::fma(u, array_ref(m, 1),
ir_builder::fma(v, array_ref(m, 2),
array_ref(m, 3))))));
return sig;
}
/** @} */
/******************************************************************************/

View file

@ -438,6 +438,32 @@ mat4x2 TYPE(120, 300, 120, 300, &glsl_type_builtin_mat4x2);
mat4x3 TYPE(120, 300, 120, 300, &glsl_type_builtin_mat4x3);
mat4x4 TYPE(120, 300, 120, 300, &glsl_type_builtin_mat4);
yuvCscStandardEXT TYPE_WITH_ALT(0, 0, 0, 0, yyextra->EXT_YUV_target_enable, &glsl_type_builtin_yuvCscStandardEXT);
itu_601 {
if (!yyextra->EXT_YUV_target_enable)
return classify_identifier(yyextra, yytext, yyleng, yylval);
yylval->csc_standard = YUV_CSC_STANDARD_601;
return CSCSTANDARD;
}
itu_601_full_range {
if (!yyextra->EXT_YUV_target_enable)
return classify_identifier(yyextra, yytext, yyleng, yylval);
yylval->csc_standard = YUV_CSC_STANDARD_601_FULL_RANGE;
return CSCSTANDARD;
}
itu_709 {
if (!yyextra->EXT_YUV_target_enable)
return classify_identifier(yyextra, yytext, yyleng, yylval);
yylval->csc_standard = YUV_CSC_STANDARD_709;
return CSCSTANDARD;
}
in return IN_TOK;
out return OUT_TOK;
inout return INOUT_TOK;
@ -502,6 +528,14 @@ samplerExternalOES {
return IDENTIFIER;
}
__samplerExternal2DY2YEXT {
if (yyextra->EXT_YUV_target_enable) {
yylval->type = &glsl_type_builtin_samplerExternal2DY2YEXT;
return BASIC_TYPE_TOK;
} else
return IDENTIFIER;
}
/* keywords available with ARB_gpu_shader5 */
precise KEYWORD_WITH_ALT(400, 310, 400, 320, yyextra->ARB_gpu_shader5_enable || yyextra->EXT_gpu_shader5_enable || yyextra->OES_gpu_shader5_enable, PRECISE);

View file

@ -105,6 +105,7 @@ static bool match_layout_qualifier(const char *s1, const char *s2,
float real;
double dreal;
const char *identifier;
enum yuv_csc_standard csc_standard;
struct ast_type_qualifier type_qualifier;
@ -159,6 +160,7 @@ static bool match_layout_qualifier(const char *s1, const char *s2,
%token <n> INTCONSTANT UINTCONSTANT BOOLCONSTANT
%token <n64> INT64CONSTANT UINT64CONSTANT
%token <identifier> FIELD_SELECTION
%token <csc_standard> CSCSTANDARD
%token LEFT_OP RIGHT_OP
%token INC_OP DEC_OP LE_OP GE_OP EQ_OP NE_OP
%token AND_OP OR_OP XOR_OP MUL_ASSIGN DIV_ASSIGN ADD_ASSIGN
@ -311,6 +313,7 @@ translation_unit:
}
state->symbols->add_default_precision_qualifier("sampler2D", ast_precision_low);
state->symbols->add_default_precision_qualifier("samplerExternalOES", ast_precision_low);
state->symbols->add_default_precision_qualifier("__samplerExternal2DY2YEXT", ast_precision_low);
state->symbols->add_default_precision_qualifier("samplerCube", ast_precision_low);
state->symbols->add_default_precision_qualifier("atomic_uint", ast_precision_high);
}
@ -493,6 +496,13 @@ primary_expression:
$$->set_location(@1);
$$->primary_expression.bool_constant = $1;
}
| CSCSTANDARD
{
linear_ctx *ctx = state->linalloc;
$$ = new(ctx) ast_expression(ast_csc_standard, NULL, NULL, NULL);
$$->set_location(@1);
$$->primary_expression.csc_standard = $1;
}
| '(' expression ')'
{
$$ = $2;
@ -1743,6 +1753,24 @@ layout_qualifier_id:
}
}
/* Layout qualifier for EXT_YUV_target. */
if (match_layout_qualifier($1, "yuv", state) == 0) {
if (state->stage != MESA_SHADER_FRAGMENT) {
_mesa_glsl_error(& @1, state,
"yuv layout qualifier only valid in fragment "
"shaders");
}
if (state->EXT_YUV_target_enable) {
$$.flags.q.yuv = 1;
} else {
_mesa_glsl_error(& @1, state,
"yuv layout qualifier present, but the "
"EXT_YUV_target_enable extension is not "
"enabled.");
}
}
if (!$$.flags.i) {
_mesa_glsl_error(& @1, state, "unrecognized layout identifier "
"`%s'", $1);

View file

@ -853,6 +853,7 @@ static const _mesa_glsl_extension _mesa_glsl_supported_extensions[] = {
EXT_AEP(EXT_texture_cube_map_array),
EXT(EXT_texture_query_lod),
EXT(EXT_texture_shadow_lod),
EXT(EXT_YUV_target),
EXT(INTEL_conservative_rasterization),
EXT(INTEL_shader_atomic_float_minmax),
EXT(INTEL_shader_integer_functions2),
@ -1355,6 +1356,8 @@ _mesa_ast_type_qualifier_print(const struct ast_type_qualifier *q)
printf("noperspective ");
if (q->flags.q.per_primitive)
printf("per_primitive ");
if (q->flags.q.yuv)
printf("yuv ");
}
@ -1537,6 +1540,17 @@ ast_expression::print(void) const
break;
}
case ast_csc_standard:
switch (primary_expression.csc_standard) {
case YUV_CSC_STANDARD_601:
printf("itu_601 ");
case YUV_CSC_STANDARD_601_FULL_RANGE:
printf("itu_601_full_range ");
case YUV_CSC_STANDARD_709:
printf("itu_709 ");
}
break;
default:
assert(0);
break;

View file

@ -937,6 +937,8 @@ struct _mesa_glsl_parse_state {
bool EXT_texture_query_lod_warn;
bool EXT_texture_shadow_lod_enable;
bool EXT_texture_shadow_lod_warn;
bool EXT_YUV_target_enable;
bool EXT_YUV_target_warn;
bool INTEL_conservative_rasterization_enable;
bool INTEL_conservative_rasterization_warn;
bool INTEL_shader_atomic_float_minmax_enable;

View file

@ -475,6 +475,7 @@ nir_visitor::visit(ir_variable *ir)
var->data.implicit_sized_array = ir->data.implicit_sized_array;
var->data.from_ssbo_unsized_array = ir->data.from_ssbo_unsized_array;
var->data.per_primitive = ir->data.per_primitive;
var->data.yuv = ir->data.yuv;
switch(ir->data.mode) {
case ir_var_auto:

View file

@ -870,6 +870,11 @@ public:
*/
unsigned pixel_local_storage:2;
/**
* Non-zero if the fragment shader output produce YUV color output
*/
unsigned yuv:1;
/**
* Emit a warning if this variable is accessed.
*/

View file

@ -876,6 +876,11 @@ glsl_sampler_type(enum glsl_sampler_dim dim, bool shadow,
return &glsl_type_builtin_error;
else
return &glsl_type_builtin_samplerExternalOES;
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
if (shadow || array)
return &glsl_type_builtin_error;
else
return &glsl_type_builtin_samplerExternal2DY2YEXT;
case GLSL_SAMPLER_DIM_SUBPASS:
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_error;
@ -906,6 +911,7 @@ glsl_sampler_type(enum glsl_sampler_dim dim, bool shadow,
case GLSL_SAMPLER_DIM_MS:
return (array ? &glsl_type_builtin_isampler2DMSArray : &glsl_type_builtin_isampler2DMS);
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
case GLSL_SAMPLER_DIM_SUBPASS:
case GLSL_SAMPLER_DIM_SUBPASS_MS:
@ -937,6 +943,7 @@ glsl_sampler_type(enum glsl_sampler_dim dim, bool shadow,
case GLSL_SAMPLER_DIM_MS:
return (array ? &glsl_type_builtin_usampler2DMSArray : &glsl_type_builtin_usampler2DMS);
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
case GLSL_SAMPLER_DIM_SUBPASS:
case GLSL_SAMPLER_DIM_SUBPASS_MS:
@ -999,6 +1006,8 @@ glsl_texture_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type typ
return &glsl_type_builtin_error;
else
return &glsl_type_builtin_textureExternalOES;
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
case GLSL_TYPE_INT:
@ -1028,6 +1037,7 @@ glsl_texture_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type typ
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_itextureSubpassInputMS;
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1058,6 +1068,7 @@ glsl_texture_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type typ
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_utextureSubpassInputMS;
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1114,6 +1125,7 @@ glsl_image_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type type)
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_subpassInputMS;
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1144,6 +1156,7 @@ glsl_image_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type type)
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_isubpassInputMS;
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1174,6 +1187,7 @@ glsl_image_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type type)
case GLSL_SAMPLER_DIM_SUBPASS_MS:
return &glsl_type_builtin_usubpassInputMS;
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1202,6 +1216,7 @@ glsl_image_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type type)
case GLSL_SAMPLER_DIM_SUBPASS:
case GLSL_SAMPLER_DIM_SUBPASS_MS:
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;
@ -1230,6 +1245,7 @@ glsl_image_type(enum glsl_sampler_dim dim, bool array, enum glsl_base_type type)
case GLSL_SAMPLER_DIM_SUBPASS:
case GLSL_SAMPLER_DIM_SUBPASS_MS:
case GLSL_SAMPLER_DIM_EXTERNAL:
case GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y:
return &glsl_type_builtin_error;
}
break;

View file

@ -696,6 +696,11 @@ typedef struct nir_variable {
*/
unsigned depth_layout : 3;
/**
* Whether the variable is a YUV color-output.
*/
unsigned yuv : 1;
/**
* Vertex stream output identifier.
*

View file

@ -1641,11 +1641,18 @@ enum glsl_sampler_dim {
GLSL_SAMPLER_DIM_RECT,
GLSL_SAMPLER_DIM_BUF,
GLSL_SAMPLER_DIM_EXTERNAL,
GLSL_SAMPLER_DIM_EXTERNAL_2D_Y2Y,
GLSL_SAMPLER_DIM_MS,
GLSL_SAMPLER_DIM_SUBPASS, /* for vulkan input attachments */
GLSL_SAMPLER_DIM_SUBPASS_MS, /* for multisampled vulkan input attachments */
};
enum yuv_csc_standard {
YUV_CSC_STANDARD_601,
YUV_CSC_STANDARD_601_FULL_RANGE,
YUV_CSC_STANDARD_709,
};
#ifdef __cplusplus
} /* extern "C" */
#endif

View file

@ -219,6 +219,7 @@ struct gl_extensions
GLboolean EXT_timer_query;
GLboolean EXT_vertex_array_bgra;
GLboolean EXT_window_rectangles;
GLboolean EXT_YUV_target;
GLboolean OES_copy_image;
GLboolean OES_primitive_bounding_box;
GLboolean OES_sample_variables;

View file

@ -287,6 +287,7 @@ _mesa_init_extensions(struct gl_extensions *extensions)
extensions->EXT_shadow_samplers = GL_TRUE;
extensions->EXT_stencil_two_side = GL_TRUE;
extensions->EXT_texture_env_dot3 = GL_TRUE;
extensions->EXT_YUV_target = GL_TRUE;
extensions->ATI_fragment_shader = GL_TRUE;
extensions->ATI_texture_env_combine3 = GL_TRUE;

View file

@ -365,6 +365,7 @@ EXT(EXT_vertex_array , dummy_true
EXT(EXT_vertex_array_bgra , EXT_vertex_array_bgra , GLL, GLC, x , x , 2008)
EXT(EXT_vertex_attrib_64bit , ARB_vertex_attrib_64bit , 32, GLC, x , x , 2010)
EXT(EXT_window_rectangles , EXT_window_rectangles , GLL, GLC, x , 30, 2016)
EXT(EXT_YUV_target , EXT_YUV_target , x , x , x , 30, 2013)
EXT(GREMEDY_string_marker , GREMEDY_string_marker , GLL, GLC, x , x , 2007)

View file

@ -466,6 +466,7 @@ if with_tests
'tests/u_memstream_test.cpp',
'tests/u_printf_test.cpp',
'tests/u_qsort_test.cpp',
'tests/u_ycbcr_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-7);
EXPECT_NEAR(inputs[i][1], result[1], 1e-7);
EXPECT_NEAR(inputs[i][2], result[2], 1e-7);
}
}
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 */