| /* | |
| * Copyright 2025 Rive | |
| */ | |
| #include "common/rand.hpp" | |
| #include <catch.hpp> | |
| // <array> is included in "cpp.glsl" so include it here first so that it | |
| // doesn't try to put all of its functions into the glsl_cross namespace | |
| #include <array> | |
| // common.glsl writes GLSL constructors and reads the flush uniforms. | |
| #define RIVE_GLSL_CROSS_WANT_CONSTRUCTORS | |
| // Included out here, not in the namespace below: common.glsl defines its own | |
| // make_half() and friends, which shadow these rather than collide with them. | |
| #include "cpp.glsl" | |
| namespace glsl_cross | |
| { | |
| #ifdef _MSC_VER | |
| #pragma warning(push) | |
| // The shader code writes float constants without the "f" suffix, so disable | |
| // this warning since there ends up being a lot of it: | |
| // truncation from 'double' to 'glsl_cross::half' | |
| #pragma warning(disable : 4305) | |
| #endif | |
| RIVE_GLSL_CROSS_CONSTRUCTORS | |
| #define FRAGMENT | |
| #define ENABLE_ADVANCED_BLEND true | |
| #define ENABLE_HSL_BLEND_MODES true | |
| // The GLSL-flavored casts in common.glsl are the ones that compile as C++. | |
| #define GLSL | |
| #include "generated/shaders/constants.minified.glsl" | |
| // unmultiply_rgb(), min_component() and max_component() come from here rather | |
| // than from hand-written copies, so a change to them can't pass this test. | |
| #include "generated/shaders/common.minified.glsl" | |
| #include "generated/shaders/advanced_blend.minified.glsl" | |
| #ifdef _MSC_VER | |
| #pragma warning(pop) | |
| #endif | |
| constexpr static float INF = std::numeric_limits<float>::infinity(); | |
| TEST_CASE("glsl_mix", "[advanced_blend]") | |
| { | |
| CHECK(simd::all( | |
| mix(make_half3(0, 0, 0), | |
| make_half3(1, 1, 1), | |
| equal(make_half3(1, 2, 3), make_half3(2))) == make_half3(0, 1, 0))); | |
| } | |
| TEST_CASE("glsl_sign", "[advanced_blend]") | |
| { | |
| CHECK( | |
| simd::all(sign(make_half4(-1, 0, .001f, 1)) == make_half4(-1, 0, 1, 1))); | |
| } | |
| TEST_CASE("glsl_unmultiply_rgb", "[advanced_blend]") | |
| { | |
| // 0 if a == 0 | |
| CHECK(simd::all(unmultiply_rgb(make_half4(0, 0, 0, 0)) == make_half3(0))); | |
| CHECK(simd::all( | |
| unmultiply_rgb(make_half4(.25f, .5f, .75f, 0)) == make_half3(0))); | |
| CHECK(simd::all(unmultiply_rgb(make_half4(-1, 0, 1, 0)) == make_half3(0))); | |
| CHECK(simd::all( | |
| unmultiply_rgb(make_half4(-1.0001f, 1, 1.0001f, 0)) == make_half3(0))); | |
| // rgb / a otherwise | |
| CHECK(simd::all( | |
| unmultiply_rgb(make_half4(.1f, .2f, .4f, .5f)) == | |
| make_half3(.2f, .4f, .8f))); | |
| } | |
| static half3 advanced_blend_coeffs_with_dst_alpha( | |
| half3 src, | |
| half3 dst, | |
| float dstAlpha, | |
| uint16_t mode) | |
| { | |
| return advanced_blend_coeffs( | |
| src, | |
| make_half4( | |
| dst.x * dstAlpha, | |
| dst.y * dstAlpha, | |
| dst.z * dstAlpha, | |
| dstAlpha), | |
| mode); | |
| } | |
| TEST_CASE("glsl_colordodge", "[advanced_blend]") | |
| { | |
| // If dstAlpha == 0, always 0, even if dst is invalid premul data. | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(0), | |
| make_half4(0, 0, 0, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(1, 1.001f, INF), | |
| make_half4(0, 0, 0, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(-.0001f, -1, -INF), | |
| make_half4(0, 0, 0, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(0), | |
| make_half4(.0001f, 1, INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(1, 1.001f, INF), | |
| make_half4(.0001f, 1, INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(-.0001f, -1, -INF), | |
| make_half4(.0001f, 1, INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(0), | |
| make_half4(-.0001f, -1, -INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(1, 1.001f, INF), | |
| make_half4(-.0001f, -1, -INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs( | |
| make_half3(-.0001f, -1, -INF), | |
| make_half4(-.0001f, -1, -INF, 0), | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| for (float dstAlpha : | |
| {0.f, 1.f / 255, .25f, .5f, 254.f / 255, 1.f, 256.f / 255}) | |
| { | |
| // 0, if Cd <= 0 | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0), | |
| make_half3(0, -.001f, -1), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0), | |
| make_half3(-10, -100, -INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1), | |
| make_half3(0, -.001f, -1), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1), | |
| make_half3(-10, -100, -INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-INF, INF, NAN), | |
| make_half3(0, -.001f, -1), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-INF, INF, NAN), | |
| make_half3(-10, -100, -INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 0)); | |
| // 1, if Cd > 0 and Cs >= 1 | |
| if (dstAlpha != 0) | |
| { | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1, 1.00001f, 2), | |
| make_half3(1e-10f), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(10, 100, INF), | |
| make_half3(1e-10f), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1, 1.00001f, 2), | |
| make_half3(1), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(10, 100, INF), | |
| make_half3(1), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1, 1.00001f, 2), | |
| make_half3(1.0001f), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(10, 100, INF), | |
| make_half3(1.0001f), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1, 1.00001f, 2), | |
| make_half3(INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(10, 100, INF), | |
| make_half3(INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE) == 1)); | |
| } | |
| // min(1,Cd/(1-Cs)), if Cd > 0 and Cs < 1 | |
| Rand rando; | |
| for (int j = 0; j < 100; ++j) | |
| { | |
| half3 src = make_half3(rando.f32(), rando.f32(), rando.f32()); | |
| assert(simd::all(0 <= src && src <= 1)); | |
| half3 dst = dstAlpha == 0 | |
| ? make_half3(0) | |
| : make_half3(rando.f32(), rando.f32(), rando.f32()); | |
| assert(simd::all(0 <= dst && dst <= 1)); | |
| half3 coeffs = advanced_blend_coeffs_with_dst_alpha( | |
| src, | |
| dst, | |
| dstAlpha, | |
| BLEND_MODE_COLORDODGE); | |
| for (int i = 0; i < 3; ++i) | |
| { | |
| if (dst[i] <= 0) | |
| CHECK(coeffs[i] == 0); | |
| else if (src[i] >= 1) | |
| CHECK(coeffs[i] == 1); | |
| else | |
| CHECK(coeffs[i] == Approx(std::min(1.f, dst[i] / (1 - src[i])))); | |
| } | |
| } | |
| } | |
| } | |
| TEST_CASE("glsl_colorburn", "[advanced_blend]") | |
| { | |
| for (float dstAlpha : | |
| {.0f, 1.f / 255, .25f, .5f, 254.f / 255, 1.f, 256.f / 255}) | |
| { | |
| // 1, if Cd >= 1 | |
| if (dstAlpha != 0) | |
| { | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0), | |
| make_half3(1, 1.001f, 2), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0), | |
| make_half3(10, 100, INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1), | |
| make_half3(1, 1.001f, 2), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(1), | |
| make_half3(10, 100, INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-INF, INF, NAN), | |
| make_half3(1, 1.001f, 2), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-INF, INF, NAN), | |
| make_half3(10, 100, INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 1)); | |
| } | |
| // 0, if Cd < 1 and Cs <= 0 | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0, -1e-1f, -1), | |
| make_half3(1 - 1e-6f), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-10, -100, -INF), | |
| make_half3(1 - 1e-6f), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0, -1e-1f, -1), | |
| make_half3(0), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-10, -100, -INF), | |
| make_half3(0), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0, -1e-1f, -1), | |
| make_half3(-1e-6f), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-10, -100, -INF), | |
| make_half3(-1e-6f), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(0, -1e-1f, -1), | |
| make_half3(-INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| CHECK(simd::all( | |
| advanced_blend_coeffs_with_dst_alpha( | |
| make_half3(-10, -100, -INF), | |
| make_half3(-INF), | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN) == 0)); | |
| // 1 - min(1,(1-Cd)/Cs), if Cd < 1 and Cs > 0 | |
| Rand rando; | |
| for (int j = 0; j < 100; ++j) | |
| { | |
| half3 src = make_half3(rando.f32(), rando.f32(), rando.f32()); | |
| assert(simd::all(0 <= src && src <= 1)); | |
| half3 dst = dstAlpha == 0 | |
| ? make_half3(0) | |
| : make_half3(rando.f32(), rando.f32(), rando.f32()); | |
| assert(simd::all(0 <= dst && dst <= 1)); | |
| half3 coeffs = advanced_blend_coeffs_with_dst_alpha( | |
| src, | |
| dst, | |
| dstAlpha, | |
| BLEND_MODE_COLORBURN); | |
| for (int i = 0; i < 3; ++i) | |
| { | |
| if (dst[i] >= 1 && dstAlpha != 0) | |
| CHECK(coeffs[i] == 1); | |
| else if (src[i] <= 0) | |
| CHECK(coeffs[i] == 0); | |
| else | |
| CHECK( | |
| coeffs[i] == | |
| Approx(1.f - std::min(1.f, (1.f - dst[i]) / src[i]))); | |
| } | |
| } | |
| } | |
| } | |
| namespace blend_spec_functions | |
| { | |
| using vec3 = simd::gvec<float, 3>; | |
| using simd::dot; | |
| // Functions copied (nearly) unmodified (except for formatting) from | |
| // https://registry.khronos.org/OpenGL/extensions/NV/NV_blend_equation_advanced.txt | |
| float minv3(vec3 c) { return min(min(c.r, c.g), c.b); } | |
| float maxv3(vec3 c) { return max(max(c.r, c.g), c.b); } | |
| float lumv3(vec3 c) { return dot(c, vec3{0.30f, 0.59f, 0.11f}); } | |
| float satv3(vec3 c) { return maxv3(c) - minv3(c); } | |
| // If any color components are outside [0,1], adjust the color to | |
| // get the components in range. | |
| vec3 ClipColor(vec3 color) | |
| { | |
| float lum = lumv3(color); | |
| float mincol = minv3(color); | |
| float maxcol = maxv3(color); | |
| if (mincol < 0.0) | |
| { | |
| color = lum + ((color - lum) * lum) / (lum - mincol); | |
| } | |
| if (maxcol > 1.0) | |
| { | |
| color = lum + ((color - lum) * (1 - lum)) / (maxcol - lum); | |
| } | |
| return color; | |
| } | |
| // Take the base RGB color <cbase> and override its luminosity | |
| // with that of the RGB color <clum>. | |
| vec3 SetLum(vec3 cbase, vec3 clum) | |
| { | |
| float lbase = lumv3(cbase); | |
| float llum = lumv3(clum); | |
| float ldiff = llum - lbase; | |
| vec3 color = cbase + vec3(ldiff); | |
| return ClipColor(color); | |
| } | |
| // Take the base RGB color <cbase> and override its saturation with | |
| // that of the RGB color <csat>. The override the luminosity of the | |
| // result with that of the RGB color <clum>. | |
| vec3 SetLumSat(vec3 cbase, vec3 csat, vec3 clum) | |
| { | |
| float minbase = minv3(cbase); | |
| float sbase = satv3(cbase); | |
| float ssat = satv3(csat); | |
| vec3 color; | |
| if (sbase > 0) | |
| { | |
| // Equivalent (modulo rounding errors) to setting the | |
| // smallest (R,G,B) component to 0, the largest to <ssat>, | |
| // and interpolating the "middle" component based on its | |
| // original value relative to the smallest/largest. | |
| color = (cbase - minbase) * ssat / sbase; | |
| } | |
| else | |
| { | |
| color = vec3(0.0); | |
| } | |
| return SetLum(color, clum); | |
| } | |
| } // namespace blend_spec_functions | |
| // Helper to compare the outputs of 2 functions over a reasonable selection of | |
| // color values. | |
| template <typename Func1, typename Func2> | |
| void test_color_pairs(Func1&& func1, Func2&& func2) | |
| { | |
| constexpr auto COLOR_STEP_COUNT = 6; | |
| for (int riX = 0; riX < COLOR_STEP_COUNT; riX++) | |
| { | |
| for (int giX = 0; giX < COLOR_STEP_COUNT; giX++) | |
| { | |
| for (int biX = 0; biX < COLOR_STEP_COUNT; biX++) | |
| { | |
| for (int riY = 0; riY < COLOR_STEP_COUNT; riY++) | |
| { | |
| for (int giY = 0; giY < COLOR_STEP_COUNT; giY++) | |
| { | |
| for (int biY = 0; biY < COLOR_STEP_COUNT; biY++) | |
| { | |
| simd::gvec<float, 3> x = { | |
| float(riX) / float(COLOR_STEP_COUNT - 1), | |
| float(giX) / float(COLOR_STEP_COUNT - 1), | |
| float(biX) / float(COLOR_STEP_COUNT - 1), | |
| }; | |
| simd::gvec<float, 3> y = { | |
| float(riY) / float(COLOR_STEP_COUNT - 1), | |
| float(giY) / float(COLOR_STEP_COUNT - 1), | |
| float(biY) / float(COLOR_STEP_COUNT - 1), | |
| }; | |
| // Snap the colors to 8-bit values | |
| x = simd::floor(x * 255.0f) / 255.0f; | |
| y = simd::floor(x * 255.0f) / 255.0f; | |
| auto r1 = func1(x, y); | |
| auto r2 = func2(x, y); | |
| auto d = r1 - r2; | |
| float maxDiff = max( | |
| std::abs(blend_spec_functions::minv3(d)), | |
| blend_spec_functions::maxv3(d)); | |
| CHECK(maxDiff <= 1e-4); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| TEST_CASE("glsl_color_blend", "[advanced_blend]") | |
| { | |
| test_color_pairs( | |
| [](auto a, auto b) { return blend_spec_functions::SetLum(a, b); }, | |
| [](auto a, auto b) { | |
| return glsl_cross::advanced_blend_coeffs_with_dst_alpha( | |
| a, | |
| b, | |
| 1.0f, | |
| BLEND_MODE_COLOR); | |
| }); | |
| } | |
| TEST_CASE("glsl_luminosity_blend", "[advanced_blend]") | |
| { | |
| test_color_pairs( | |
| [](auto a, auto b) { return blend_spec_functions::SetLum(b, a); }, | |
| [](auto a, auto b) { | |
| return glsl_cross::advanced_blend_coeffs_with_dst_alpha( | |
| a, | |
| b, | |
| 1.0f, | |
| BLEND_MODE_LUMINOSITY); | |
| }); | |
| } | |
| TEST_CASE("glsl_saturation_blend", "[advanced_blend]") | |
| { | |
| test_color_pairs( | |
| [](auto a, auto b) { return blend_spec_functions::SetLumSat(b, a, b); }, | |
| [](auto a, auto b) { | |
| return glsl_cross::advanced_blend_coeffs_with_dst_alpha( | |
| a, | |
| b, | |
| 1.0f, | |
| BLEND_MODE_SATURATION); | |
| }); | |
| } | |
| TEST_CASE("glsl_hue_blend", "[advanced_blend]") | |
| { | |
| test_color_pairs( | |
| [](auto a, auto b) { return blend_spec_functions::SetLumSat(a, b, b); }, | |
| [](auto a, auto b) { | |
| return glsl_cross::advanced_blend_coeffs_with_dst_alpha( | |
| a, | |
| b, | |
| 1.0f, | |
| BLEND_MODE_HUE); | |
| }); | |
| } | |
| } // namespace glsl_cross |