clutter/color-state: Match reference luminance
This uses the luminance levels of the color states to anchor the white of content instead of hard-coding the levels. This also starts using uniforms for parts of the mapping which means we don't have to generate and compile a shader when the luminance levels change. Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/3953>
This commit is contained in:
parent
03aad0d99e
commit
86a0797819
3 changed files with 91 additions and 125 deletions
|
@ -54,6 +54,8 @@
|
||||||
#include "clutter/clutter-enum-types.h"
|
#include "clutter/clutter-enum-types.h"
|
||||||
#include "clutter/clutter-private.h"
|
#include "clutter/clutter-private.h"
|
||||||
|
|
||||||
|
#define UNIFORM_NAME_LUMINANCE_MAPPING "luminance_mapping"
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
PROP_0,
|
PROP_0,
|
||||||
|
@ -505,9 +507,9 @@ clutter_color_state_new_full (ClutterContext *context,
|
||||||
|
|
||||||
static const char pq_eotf_source[] =
|
static const char pq_eotf_source[] =
|
||||||
"// pq_eotf:\n"
|
"// pq_eotf:\n"
|
||||||
"// @pq: Normalized ([0,1]) electrical signal value\n"
|
"// @color: Normalized ([0,1]) electrical signal value\n"
|
||||||
"// Returns: Luminance in cd/m²\n"
|
"// Returns: tristimulus values ([0,1])\n"
|
||||||
"vec3 pq_eotf (vec3 pq)\n"
|
"vec3 pq_eotf (vec3 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" const float c1 = 0.8359375;\n"
|
" const float c1 = 0.8359375;\n"
|
||||||
" const float c2 = 18.8515625;\n"
|
" const float c2 = 18.8515625;\n"
|
||||||
|
@ -516,44 +518,43 @@ static const char pq_eotf_source[] =
|
||||||
" const float oo_m1 = 1.0 / 0.1593017578125;\n"
|
" const float oo_m1 = 1.0 / 0.1593017578125;\n"
|
||||||
" const float oo_m2 = 1.0 / 78.84375;\n"
|
" const float oo_m2 = 1.0 / 78.84375;\n"
|
||||||
"\n"
|
"\n"
|
||||||
" vec3 num = max (pow (pq, vec3 (oo_m2)) - c1, vec3 (0.0));\n"
|
" vec3 num = max (pow (color, vec3 (oo_m2)) - c1, vec3 (0.0));\n"
|
||||||
" vec3 den = c2 - c3 * pow (pq, vec3 (oo_m2));\n"
|
" vec3 den = c2 - c3 * pow (color, vec3 (oo_m2));\n"
|
||||||
"\n"
|
"\n"
|
||||||
" return 10000.0 * pow (num / den, vec3 (oo_m1));\n"
|
" return pow (num / den, vec3 (oo_m1));\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"\n"
|
"\n"
|
||||||
"vec4 pq_eotf (vec4 pq)\n"
|
"vec4 pq_eotf (vec4 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" return vec4 (pq_eotf (pq.rgb), pq.a);\n"
|
" return vec4 (pq_eotf (color.rgb), color.a);\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
||||||
static const char pq_inv_eotf_source[] =
|
static const char pq_inv_eotf_source[] =
|
||||||
"// pq_inv_eotf:\n"
|
"// pq_inv_eotf:\n"
|
||||||
"// @nits: Optical signal value in cd/m²\n"
|
"// @color: Normalized tristimulus values ([0,1])"
|
||||||
"// Returns: Normalized ([0,1]) electrical signal value\n"
|
"// Returns: Normalized ([0,1]) electrical signal value\n"
|
||||||
"vec3 pq_inv_eotf (vec3 nits)\n"
|
"vec3 pq_inv_eotf (vec3 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" vec3 normalized = clamp (nits / 10000.0, vec3 (0), vec3 (1));\n"
|
|
||||||
" float m1 = 0.1593017578125;\n"
|
" float m1 = 0.1593017578125;\n"
|
||||||
" float m2 = 78.84375;\n"
|
" float m2 = 78.84375;\n"
|
||||||
" float c1 = 0.8359375;\n"
|
" float c1 = 0.8359375;\n"
|
||||||
" float c2 = 18.8515625;\n"
|
" float c2 = 18.8515625;\n"
|
||||||
" float c3 = 18.6875;\n"
|
" float c3 = 18.6875;\n"
|
||||||
" vec3 normalized_pow_m1 = pow (normalized, vec3 (m1));\n"
|
" vec3 color_pow_m1 = pow (color, vec3 (m1));\n"
|
||||||
" vec3 num = vec3 (c1) + c2 * normalized_pow_m1;\n"
|
" vec3 num = vec3 (c1) + c2 * color_pow_m1;\n"
|
||||||
" vec3 denum = vec3 (1.0) + c3 * normalized_pow_m1;\n"
|
" vec3 denum = vec3 (1.0) + c3 * color_pow_m1;\n"
|
||||||
" return pow (num / denum, vec3 (m2));\n"
|
" return pow (num / denum, vec3 (m2));\n"
|
||||||
"}\n"
|
"}\n"
|
||||||
"\n"
|
"\n"
|
||||||
"vec4 pq_inv_eotf (vec4 nits)\n"
|
"vec4 pq_inv_eotf (vec4 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" return vec4 (pq_inv_eotf (nits.rgb), nits.a);\n"
|
" return vec4 (pq_inv_eotf (color.rgb), color.a);\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
||||||
static const char srgb_eotf_source[] =
|
static const char srgb_eotf_source[] =
|
||||||
"// srgb_eotf:\n"
|
"// srgb_eotf:\n"
|
||||||
"// @color: Normalized ([0,1]) electrical signal value.\n"
|
"// @color: Normalized ([0,1]) electrical signal value.\n"
|
||||||
"// Returns: Normalized luminance ([0,1])\n"
|
"// Returns: Normalized tristimulus values ([0,1])\n"
|
||||||
"vec3 srgb_eotf (vec3 color)\n"
|
"vec3 srgb_eotf (vec3 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
" bvec3 is_low = lessThanEqual (color, vec3 (0.04045));\n"
|
" bvec3 is_low = lessThanEqual (color, vec3 (0.04045));\n"
|
||||||
|
@ -569,7 +570,7 @@ static const char srgb_eotf_source[] =
|
||||||
|
|
||||||
static const char srgb_inv_eotf_source[] =
|
static const char srgb_inv_eotf_source[] =
|
||||||
"// srgb_inv_eotf:\n"
|
"// srgb_inv_eotf:\n"
|
||||||
"// @color: Normalized ([0,1]) optical signal value\n"
|
"// @color: Normalized ([0,1]) tristimulus values\n"
|
||||||
"// Returns: Normalized ([0,1]) electrical signal value\n"
|
"// Returns: Normalized ([0,1]) electrical signal value\n"
|
||||||
"vec3 srgb_inv_eotf (vec3 color)\n"
|
"vec3 srgb_inv_eotf (vec3 color)\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
|
@ -585,20 +586,6 @@ static const char srgb_inv_eotf_source[] =
|
||||||
" return vec4 (srgb_inv_eotf (color.rgb), color.a);\n"
|
" return vec4 (srgb_inv_eotf (color.rgb), color.a);\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
|
||||||
/* Luminance gain default value (203) retrieved from
|
|
||||||
* https://github.com/w3c/ColorWeb-CG/blob/feature/add-mastering-display-info/hdr_html_canvas_element.md#srgb-to-rec2100-pq */
|
|
||||||
static const char srgb_luminance_gain_source[] =
|
|
||||||
"vec3 srgb_luminance_gain (vec3 value)\n"
|
|
||||||
"{\n"
|
|
||||||
" return 203.0 * value;\n"
|
|
||||||
"}\n";
|
|
||||||
|
|
||||||
static const char pq_luminance_clamp_source[] =
|
|
||||||
"vec3 pq_luminance_clamp (vec3 value)\n"
|
|
||||||
"{\n"
|
|
||||||
" return clamp (value, 0.0, 203.0) / 203.0;\n"
|
|
||||||
"}\n";
|
|
||||||
|
|
||||||
/* Calculated using:
|
/* Calculated using:
|
||||||
* numpy.dot(colour.models.RGB_COLOURSPACE_BT2020.matrix_XYZ_to_RGB,
|
* numpy.dot(colour.models.RGB_COLOURSPACE_BT2020.matrix_XYZ_to_RGB,
|
||||||
* colour.models.RGB_COLOURSPACE_BT709.matrix_RGB_to_XYZ)
|
* colour.models.RGB_COLOURSPACE_BT709.matrix_RGB_to_XYZ)
|
||||||
|
@ -652,16 +639,6 @@ static const TransferFunction srgb_inv_eotf = {
|
||||||
.name = "srgb_inv_eotf",
|
.name = "srgb_inv_eotf",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const TransferFunction srgb_luminance_gain = {
|
|
||||||
.source = srgb_luminance_gain_source,
|
|
||||||
.name = "srgb_luminance_gain",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const TransferFunction pq_luminance_clamp = {
|
|
||||||
.source = pq_luminance_clamp_source,
|
|
||||||
.name = "pq_luminance_clamp",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const MatrixMultiplication bt709_to_bt2020 = {
|
static const MatrixMultiplication bt709_to_bt2020 = {
|
||||||
.source = bt709_to_bt2020_matrix_source,
|
.source = bt709_to_bt2020_matrix_source,
|
||||||
.name = "bt709_to_bt2020",
|
.name = "bt709_to_bt2020",
|
||||||
|
@ -741,86 +718,18 @@ get_inv_eotf (ClutterColorState *color_state)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const TransferFunction *
|
|
||||||
get_denormalize_function (ClutterColorState *color_state,
|
|
||||||
ClutterColorState *target_color_state)
|
|
||||||
{
|
|
||||||
ClutterColorStatePrivate *priv =
|
|
||||||
clutter_color_state_get_instance_private (color_state);
|
|
||||||
ClutterColorStatePrivate *target_priv =
|
|
||||||
clutter_color_state_get_instance_private (target_color_state);
|
|
||||||
|
|
||||||
switch (priv->transfer_function)
|
|
||||||
{
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_SRGB:
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_DEFAULT:
|
|
||||||
switch (target_priv->transfer_function)
|
|
||||||
{
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_PQ:
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_LINEAR:
|
|
||||||
return &srgb_luminance_gain;
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_SRGB:
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_DEFAULT:
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_PQ:
|
|
||||||
switch (target_priv->transfer_function)
|
|
||||||
{
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_PQ:
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_LINEAR:
|
|
||||||
return NULL;
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_SRGB:
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_DEFAULT:
|
|
||||||
return &pq_luminance_clamp;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CLUTTER_TRANSFER_FUNCTION_LINEAR:
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_return_val_if_reached (NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
get_transfer_functions (ClutterColorState *color_state,
|
get_transfer_functions (ClutterColorState *color_state,
|
||||||
ClutterColorState *target_color_state,
|
ClutterColorState *target_color_state,
|
||||||
const TransferFunction **pre_transfer_function,
|
const TransferFunction **pre_transfer_function,
|
||||||
const TransferFunction **denormalize_function,
|
|
||||||
const TransferFunction **post_transfer_function)
|
const TransferFunction **post_transfer_function)
|
||||||
{
|
{
|
||||||
ClutterColorStatePrivate *priv =
|
if (clutter_color_state_equals (color_state, target_color_state))
|
||||||
clutter_color_state_get_instance_private (color_state);
|
|
||||||
ClutterColorStatePrivate *target_priv =
|
|
||||||
clutter_color_state_get_instance_private (target_color_state);
|
|
||||||
|
|
||||||
if (priv->colorspace == target_priv->colorspace &&
|
|
||||||
priv->transfer_function == target_priv->transfer_function)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (priv->transfer_function != CLUTTER_TRANSFER_FUNCTION_LINEAR &&
|
|
||||||
target_priv->transfer_function == CLUTTER_TRANSFER_FUNCTION_LINEAR)
|
|
||||||
{
|
|
||||||
*pre_transfer_function = get_eotf (color_state);
|
*pre_transfer_function = get_eotf (color_state);
|
||||||
*denormalize_function = get_denormalize_function (color_state,
|
|
||||||
target_color_state);
|
|
||||||
}
|
|
||||||
else if (priv->transfer_function == CLUTTER_TRANSFER_FUNCTION_LINEAR &&
|
|
||||||
target_priv->transfer_function != CLUTTER_TRANSFER_FUNCTION_LINEAR)
|
|
||||||
{
|
|
||||||
*denormalize_function = get_denormalize_function (color_state,
|
|
||||||
target_color_state);
|
|
||||||
*post_transfer_function = get_inv_eotf (target_color_state);
|
*post_transfer_function = get_inv_eotf (target_color_state);
|
||||||
}
|
}
|
||||||
else if (priv->transfer_function != CLUTTER_TRANSFER_FUNCTION_LINEAR &&
|
|
||||||
target_priv->transfer_function != CLUTTER_TRANSFER_FUNCTION_LINEAR)
|
|
||||||
{
|
|
||||||
*pre_transfer_function = get_eotf (color_state);
|
|
||||||
*denormalize_function = get_denormalize_function (color_state,
|
|
||||||
target_color_state);
|
|
||||||
*post_transfer_function = get_inv_eotf (target_color_state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static const MatrixMultiplication *
|
static const MatrixMultiplication *
|
||||||
get_color_space_mapping_matrix (ClutterColorState *color_state,
|
get_color_space_mapping_matrix (ClutterColorState *color_state,
|
||||||
|
@ -869,7 +778,6 @@ clutter_color_state_get_transform_snippet (ClutterColorState *color_state,
|
||||||
CoglSnippet *snippet;
|
CoglSnippet *snippet;
|
||||||
const MatrixMultiplication *color_space_mapping = NULL;
|
const MatrixMultiplication *color_space_mapping = NULL;
|
||||||
const TransferFunction *pre_transfer_function = NULL;
|
const TransferFunction *pre_transfer_function = NULL;
|
||||||
const TransferFunction *denormalize_function = NULL;
|
|
||||||
const TransferFunction *post_transfer_function = NULL;
|
const TransferFunction *post_transfer_function = NULL;
|
||||||
g_autoptr (GString) globals_source = NULL;
|
g_autoptr (GString) globals_source = NULL;
|
||||||
g_autoptr (GString) snippet_source = NULL;
|
g_autoptr (GString) snippet_source = NULL;
|
||||||
|
@ -890,19 +798,19 @@ clutter_color_state_get_transform_snippet (ClutterColorState *color_state,
|
||||||
|
|
||||||
get_transfer_functions (color_state, target_color_state,
|
get_transfer_functions (color_state, target_color_state,
|
||||||
&pre_transfer_function,
|
&pre_transfer_function,
|
||||||
&denormalize_function,
|
|
||||||
&post_transfer_function);
|
&post_transfer_function);
|
||||||
|
|
||||||
globals_source = g_string_new (NULL);
|
globals_source = g_string_new (NULL);
|
||||||
if (pre_transfer_function)
|
if (pre_transfer_function)
|
||||||
g_string_append_printf (globals_source, "%s\n", pre_transfer_function->source);
|
g_string_append_printf (globals_source, "%s\n", pre_transfer_function->source);
|
||||||
if (denormalize_function)
|
|
||||||
g_string_append_printf (globals_source, "%s\n", denormalize_function->source);
|
|
||||||
if (post_transfer_function)
|
if (post_transfer_function)
|
||||||
g_string_append_printf (globals_source, "%s\n", post_transfer_function->source);
|
g_string_append_printf (globals_source, "%s\n", post_transfer_function->source);
|
||||||
if (color_space_mapping)
|
if (color_space_mapping)
|
||||||
g_string_append_printf (globals_source, "%s\n", color_space_mapping->source);
|
g_string_append_printf (globals_source, "%s\n", color_space_mapping->source);
|
||||||
|
|
||||||
|
g_string_append (globals_source,
|
||||||
|
"uniform float " UNIFORM_NAME_LUMINANCE_MAPPING ";\n");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The following statements generate a shader snippet that transforms colors
|
* The following statements generate a shader snippet that transforms colors
|
||||||
* from one color state (transfer function, color space, color encoding) into
|
* from one color state (transfer function, color space, color encoding) into
|
||||||
|
@ -938,12 +846,10 @@ clutter_color_state_get_transform_snippet (ClutterColorState *color_state,
|
||||||
pre_transfer_function->name);
|
pre_transfer_function->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (denormalize_function)
|
g_string_append (snippet_source,
|
||||||
{
|
" color_state_color = "
|
||||||
g_string_append_printf (snippet_source,
|
UNIFORM_NAME_LUMINANCE_MAPPING " * color_state_color;\n");
|
||||||
" color_state_color = %s (color_state_color);\n",
|
|
||||||
denormalize_function->name);
|
|
||||||
}
|
|
||||||
if (color_space_mapping)
|
if (color_space_mapping)
|
||||||
{
|
{
|
||||||
g_string_append_printf (snippet_source,
|
g_string_append_printf (snippet_source,
|
||||||
|
@ -974,6 +880,45 @@ clutter_color_state_get_transform_snippet (ClutterColorState *color_state,
|
||||||
return snippet;
|
return snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static float
|
||||||
|
get_luminance_mapping (ClutterColorState *color_state,
|
||||||
|
ClutterColorState *target_color_state)
|
||||||
|
{
|
||||||
|
float min_lum, max_lum, ref_lum;
|
||||||
|
float target_min_lum, target_max_lum, target_ref_lum;
|
||||||
|
|
||||||
|
clutter_color_state_get_luminances (color_state,
|
||||||
|
&min_lum, &max_lum, &ref_lum);
|
||||||
|
|
||||||
|
clutter_color_state_get_luminances (target_color_state,
|
||||||
|
&target_min_lum,
|
||||||
|
&target_max_lum,
|
||||||
|
&target_ref_lum);
|
||||||
|
|
||||||
|
/* this is a very basic, non-contrast preserving way of matching the reference
|
||||||
|
* luminance level */
|
||||||
|
return (target_ref_lum / ref_lum) * (max_lum / target_max_lum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
clutter_color_state_update_uniforms (ClutterColorState *color_state,
|
||||||
|
ClutterColorState *target_color_state,
|
||||||
|
CoglPipeline *pipeline)
|
||||||
|
{
|
||||||
|
float luminance_mapping;
|
||||||
|
int uniform_location_luminance_mapping;
|
||||||
|
|
||||||
|
luminance_mapping = get_luminance_mapping (color_state, target_color_state);
|
||||||
|
|
||||||
|
uniform_location_luminance_mapping =
|
||||||
|
cogl_pipeline_get_uniform_location (pipeline,
|
||||||
|
UNIFORM_NAME_LUMINANCE_MAPPING);
|
||||||
|
|
||||||
|
cogl_pipeline_set_uniform_1f (pipeline,
|
||||||
|
uniform_location_luminance_mapping,
|
||||||
|
luminance_mapping);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
clutter_color_state_add_pipeline_transform (ClutterColorState *color_state,
|
clutter_color_state_add_pipeline_transform (ClutterColorState *color_state,
|
||||||
ClutterColorState *target_color_state,
|
ClutterColorState *target_color_state,
|
||||||
|
@ -987,6 +932,10 @@ clutter_color_state_add_pipeline_transform (ClutterColorState *color_state,
|
||||||
snippet = clutter_color_state_get_transform_snippet (color_state,
|
snippet = clutter_color_state_get_transform_snippet (color_state,
|
||||||
target_color_state);
|
target_color_state);
|
||||||
cogl_pipeline_add_snippet (pipeline, snippet);
|
cogl_pipeline_add_snippet (pipeline, snippet);
|
||||||
|
|
||||||
|
clutter_color_state_update_uniforms (color_state,
|
||||||
|
target_color_state,
|
||||||
|
pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
|
|
|
@ -74,6 +74,12 @@ void clutter_color_state_add_pipeline_transform (ClutterColorState *color_state,
|
||||||
ClutterColorState *target_color_state,
|
ClutterColorState *target_color_state,
|
||||||
CoglPipeline *pipeline);
|
CoglPipeline *pipeline);
|
||||||
|
|
||||||
|
CLUTTER_EXPORT
|
||||||
|
void clutter_color_state_update_uniforms (ClutterColorState *color_state,
|
||||||
|
ClutterColorState *target_color_state,
|
||||||
|
CoglPipeline *pipeline);
|
||||||
|
|
||||||
|
|
||||||
CLUTTER_EXPORT
|
CLUTTER_EXPORT
|
||||||
gboolean clutter_color_state_equals (ClutterColorState *color_state,
|
gboolean clutter_color_state_equals (ClutterColorState *color_state,
|
||||||
ClutterColorState *other_color_state);
|
ClutterColorState *other_color_state);
|
||||||
|
|
|
@ -116,11 +116,22 @@ clutter_pipeline_cache_get_pipeline (ClutterPipelineCache *pipeline_cache,
|
||||||
pipeline = g_hash_table_lookup (group_entry->slots[slot], &key);
|
pipeline = g_hash_table_lookup (group_entry->slots[slot], &key);
|
||||||
|
|
||||||
if (pipeline)
|
if (pipeline)
|
||||||
return cogl_pipeline_copy (pipeline);
|
{
|
||||||
|
CoglPipeline *new_pipeline;
|
||||||
|
|
||||||
|
new_pipeline = cogl_pipeline_copy (pipeline);
|
||||||
|
clutter_color_state_update_uniforms (source_color_state,
|
||||||
|
target_color_state,
|
||||||
|
new_pipeline);
|
||||||
|
return new_pipeline;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* clutter_pipeline_cache_sdd_pipeline: (skip)
|
* clutter_pipeline_cache_sdd_pipeline: (skip)
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue