tests/alpha-blending: add sRGB linear light case

Now that GL-renderer and color manager implement linear light blending
for sRGB EOTF, add a test case to verify the result is expected.

As noted in test comments, this new tests is quite powerful in ensuring
the whole linear light pipeline is working correctly with 1D LUTs in
GL-renderer. This test will even catch smashing source_lut.scale = 1.0f
and source_lut.offset = 0.0f which would result in wrong texture sample
positions for LUT data.

As the assumption is that by default content and outputs are in sRGB
color space, this test should not need fix-ups or become stale when more
color management features are implemented.

The sRGB EOTF can be found in:
http://www.color.org/sRGB.pdf (beware, typos)
https://www.w3.org/Graphics/Color/srgb
https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#TRANSFER_SRGB

Note on AMD Polaris 11 error threshold: this is quite likely due to
using fp16 format shadow framebuffer and GCN fp32 to fp16 conversion
instruction rounding mode. When using fp32 shadow framebuffer, the error
glitch is not present and the threshold could be significantly lower.

Signed-off-by: Pekka Paalanen <pekka.paalanen@collabora.com>
This commit is contained in:
Pekka Paalanen 2021-04-16 17:42:40 +03:00 committed by Pekka Paalanen
parent e70aa1fde2
commit 35cae567d6
2 changed files with 117 additions and 6 deletions

View File

@ -33,6 +33,7 @@
struct setup_args {
struct fixture_metadata meta;
enum renderer_type renderer;
bool color_management;
};
static const int ALPHA_STEPS = 256;
@ -41,12 +42,19 @@ static const int BLOCK_WIDTH = 3;
static const struct setup_args my_setup_args[] = {
{
.renderer = RENDERER_PIXMAN,
.color_management = false,
.meta.name = "pixman"
},
{
.renderer = RENDERER_GL,
.color_management = false,
.meta.name = "GL"
},
{
.renderer = RENDERER_GL,
.color_management = true,
.meta.name = "GL sRGB EOTF"
},
};
static enum test_result_code
@ -60,6 +68,12 @@ fixture_setup(struct weston_test_harness *harness, const struct setup_args *arg)
setup.height = 16;
setup.shell = SHELL_TEST_DESKTOP;
if (arg->color_management) {
weston_ini_setup(&setup,
cfgln("[core]"),
cfgln("color-management=true"));
}
return weston_test_harness_execute_as_client(harness, &setup);
}
DECLARE_FIXTURE_SETUP_WITH_ARG(fixture_setup, my_setup_args, meta);
@ -154,6 +168,46 @@ unpremult_float(struct color_float *cf)
}
}
static float
sRGB_EOTF(float e)
{
assert(e >= 0.0f);
assert(e <= 1.0f);
if (e <= 0.04045)
return e / 12.92;
else
return pow((e + 0.055) / 1.055, 2.4);
}
static void
sRGB_linearize(struct color_float *cf)
{
cf->r = sRGB_EOTF(cf->r);
cf->g = sRGB_EOTF(cf->g);
cf->b = sRGB_EOTF(cf->b);
}
static float
sRGB_EOTF_inv(float o)
{
assert(o >= 0.0f);
assert(o <= 1.0f);
if (o <= 0.04045 / 12.92)
return o * 12.92;
else
return pow(o, 1.0 / 2.4) * 1.055 - 0.055;
}
static void
sRGB_delinearize(struct color_float *cf)
{
cf->r = sRGB_EOTF_inv(cf->r);
cf->g = sRGB_EOTF_inv(cf->g);
cf->b = sRGB_EOTF_inv(cf->b);
}
static bool
compare_float(float ref, float dst, int x, const char *chan, float *max_diff)
{
@ -188,7 +242,18 @@ compare_float(float ref, float dst, int x, const char *chan, float *max_diff)
if (diff > *max_diff)
*max_diff = diff;
if (diff < 0.5f / 255.f)
/*
* Allow for +/- 1.5 code points of error in non-linear 8-bit channel
* value. This is necessary for the BLEND_LINEAR case.
*
* With llvmpipe, we could go as low as +/- 0.65 code points of error
* and still pass.
*
* AMD Polaris 11 would be ok with +/- 1.0 code points error threshold
* if not for one particular case of blending (a=254, r=0) into r=255,
* which results in error of 1.29 code points.
*/
if (diff < 1.5f / 255.f)
return true;
testlog("x=%d %s: ref %f != dst %f, delta %f\n",
@ -197,9 +262,15 @@ compare_float(float ref, float dst, int x, const char *chan, float *max_diff)
return false;
}
enum blend_space {
BLEND_NONLINEAR,
BLEND_LINEAR,
};
static bool
verify_sRGB_blend_a8r8g8b8(uint32_t bg32, uint32_t fg32, uint32_t dst32,
int x, struct color_float *max_diff)
int x, struct color_float *max_diff,
enum blend_space space)
{
struct color_float bg = a8r8g8b8_to_float(bg32);
struct color_float fg = a8r8g8b8_to_float(fg32);
@ -211,10 +282,18 @@ verify_sRGB_blend_a8r8g8b8(uint32_t bg32, uint32_t fg32, uint32_t dst32,
unpremult_float(&fg);
unpremult_float(&dst);
if (space == BLEND_LINEAR) {
sRGB_linearize(&bg);
sRGB_linearize(&fg);
}
ref.r = (1.0f - fg.a) * bg.r + fg.a * fg.r;
ref.g = (1.0f - fg.a) * bg.g + fg.a * fg.g;
ref.b = (1.0f - fg.a) * bg.b + fg.a * fg.b;
if (space == BLEND_LINEAR)
sRGB_delinearize(&ref);
ok = compare_float(ref.r, dst.r, x, "r", &max_diff->r) && ok;
ok = compare_float(ref.g, dst.g, x, "g", &max_diff->g) && ok;
ok = compare_float(ref.b, dst.b, x, "b", &max_diff->b) && ok;
@ -268,7 +347,8 @@ get_middle_row(struct buffer *buf)
}
static bool
check_blend_pattern(struct buffer *bg, struct buffer *fg, struct buffer *shot)
check_blend_pattern(struct buffer *bg, struct buffer *fg, struct buffer *shot,
enum blend_space space)
{
uint32_t *bg_row = get_middle_row(bg);
uint32_t *fg_row = get_middle_row(fg);
@ -282,7 +362,8 @@ check_blend_pattern(struct buffer *bg, struct buffer *fg, struct buffer *shot)
ret = false;
if (!verify_sRGB_blend_a8r8g8b8(bg_row[x], fg_row[x],
shot_row[x], x, &max_diff))
shot_row[x], x, &max_diff,
space))
ret = false;
}
@ -309,6 +390,24 @@ check_blend_pattern(struct buffer *bg, struct buffer *fg, struct buffer *shot)
* - red goes from 1.0 to 0.0, monotonic
* - green is not monotonic
* - blue goes from 0.0 to 1.0, monotonic
*
* This test has two modes: BLEND_NONLINEAR and BLEND_LINEAR.
*
* BLEND_NONLINEAR does blending with pixel values as is, which are non-linear,
* and therefore result in "physically incorrect" blending result. Yet, people
* have accustomed to seeing this effect. This mode hits pipeline_premult()
* in fragment.glsl.
*
* BLEND_LINEAR has sRGB encoded pixels (non-linear). These are converted to
* linear light (optical) values, blended, and converted back to non-linear
* (electrical) values. This results in "physically more correct" blending
* result for some value of "physical". This mode hits pipeline_straight()
* in fragment.glsl, and tests even more things:
* - gl-renderer implementation of 1D LUT is correct
* - color-lcms instantiates the correct sRGB EOTF and inverse LUTs
* - color space conversions do not happen when both content and output are
* using their default color spaces
* - blending through gl-renderer shadow framebuffer
*/
TEST(alpha_blend)
{
@ -320,6 +419,7 @@ TEST(alpha_blend)
.blue = 0x0000,
.alpha = 0xffff
};
const struct setup_args *args;
struct client *client;
struct buffer *bg;
struct buffer *fg;
@ -328,6 +428,17 @@ TEST(alpha_blend)
struct wl_subsurface *sub;
struct buffer *shot;
bool match;
int seq_no;
enum blend_space space;
args = &my_setup_args[get_test_fixture_index()];
if (args->color_management) {
seq_no = 1;
space = BLEND_LINEAR;
} else {
seq_no = 0;
space = BLEND_NONLINEAR;
}
client = create_client();
subco = bind_to_singleton_global(client, &wl_subcompositor_interface, 1);
@ -361,10 +472,10 @@ TEST(alpha_blend)
shot = capture_screenshot_of_output(client);
assert(shot);
match = verify_image(shot, "alpha_blend", 0, NULL, 0);
match = verify_image(shot, "alpha_blend", seq_no, NULL, seq_no);
assert(check_blend_pattern(bg, fg, shot, space));
assert(match);
assert(check_blend_pattern(bg, fg, shot));
buffer_destroy(shot);
wl_subsurface_destroy(sub);

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B