| /* |
| * Copyright 2024 Rive |
| */ |
| |
| #include "rive/math/math_types.hpp" |
| #include "rive/renderer/gpu.hpp" |
| #include "shaders/constants.glsl" |
| #include <catch.hpp> |
| |
| namespace rive |
| { |
| TEST_CASE("find_transformed_area", "[gpu]") |
| { |
| AABB unitSquare{0, 0, 1, 1}; |
| CHECK(gpu::find_transformed_area(unitSquare, Mat2D()) == 1); |
| CHECK(gpu::find_transformed_area(unitSquare, Mat2D::fromScale(2, 2)) == 4); |
| CHECK(gpu::find_transformed_area(unitSquare, Mat2D::fromScale(2, 1)) == 2); |
| CHECK(gpu::find_transformed_area(unitSquare, Mat2D::fromScale(0, 1)) == 0); |
| CHECK(gpu::find_transformed_area(unitSquare, |
| Mat2D::fromRotation(math::PI / 4)) == |
| Approx(1.f)); |
| CHECK(gpu::find_transformed_area( |
| unitSquare, |
| Mat2D::fromRotation(math::PI / 8).scale({2, 2})) == Approx(4.f)); |
| CHECK(gpu::find_transformed_area( |
| unitSquare, |
| Mat2D::fromRotation(math::PI / 16).scale({2, 1})) == Approx(2.f)); |
| CHECK( |
| gpu::find_transformed_area(unitSquare, {1, .87f, 8, 8 * .87f, 0, 0}) == |
| Approx(0.f).margin(math::EPSILON)); |
| } |
| |
| TEST_CASE("gaussian_integral_table", "[gpu]") |
| { |
| float gaussianTable[gpu::GAUSSIAN_TABLE_SIZE]; |
| for (int i = 0; i < gpu::GAUSSIAN_TABLE_SIZE; i += 4) |
| { |
| float4 f32s = gpu::cast_f16_to_f32( |
| simd::load<uint16_t, 4>(gpu::g_gaussianIntegralTableF16 + i)); |
| simd::store(gaussianTable + i, f32s); |
| }; |
| |
| CHECK(gaussianTable[0] >= 0); |
| CHECK(gaussianTable[0] <= expf(-.5f * FEATHER_TEXTURE_STDDEVS)); |
| CHECK(gaussianTable[0] == MIN_FEATHER); |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1] <= 1); |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1] >= |
| 1 - expf(-.5f * FEATHER_TEXTURE_STDDEVS)); |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1] == MAX_FEATHER); |
| if (gpu::GAUSSIAN_TABLE_SIZE & 1) |
| { |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2] == .5f); |
| } |
| else |
| { |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2 - 1] <= .5f); |
| CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2] >= .5f); |
| CHECK((gaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2 - 1] + |
| gaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2]) / |
| 2 == |
| Approx(.5f).margin(1e-3f)); |
| } |
| for (int i = 1; i < gpu::GAUSSIAN_TABLE_SIZE; ++i) |
| { |
| CHECK(gaussianTable[i - 1] <= gaussianTable[i]); |
| } |
| for (int i = 0; i < (gpu::GAUSSIAN_TABLE_SIZE + 1) / 2; ++i) |
| { |
| CHECK(gaussianTable[i] + |
| gaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1 - i] == |
| Approx(1).margin(1e-3f)); |
| } |
| } |
| |
| // Looks up the value of "x" in the given function table, with linear filtering. |
| static float function_table_lookup(float x, const float* table, float tableSize) |
| { |
| x = fminf(fmaxf(0, x), 1); |
| float sampleRegionLeft = x * tableSize - .5f; |
| int rightIdx = static_cast<int>(fminf(sampleRegionLeft + 1, tableSize - 1)); |
| int leftIdx = std::max(rightIdx - 1, 0); |
| float t = fminf(fmaxf(0, sampleRegionLeft - leftIdx), 1); |
| return lerp(table[leftIdx], table[rightIdx], t); |
| } |
| |
| TEST_CASE("inverse_gaussian_integral_table", "[gpu]") |
| { |
| float gaussianTable[gpu::GAUSSIAN_TABLE_SIZE]; |
| for (int i = 0; i < gpu::GAUSSIAN_TABLE_SIZE; i += 4) |
| { |
| float4 f32s = gpu::cast_f16_to_f32( |
| simd::load<uint16_t, 4>(gpu::g_gaussianIntegralTableF16 + i)); |
| simd::store(gaussianTable + i, f32s); |
| }; |
| auto gaussianIntegral = [&gaussianTable](float x) { |
| return function_table_lookup(x, |
| gaussianTable, |
| std::size(gaussianTable)); |
| }; |
| |
| float inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE]; |
| for (int i = 0; i < gpu::GAUSSIAN_TABLE_SIZE; i += 4) |
| { |
| float4 f32s = gpu::cast_f16_to_f32(simd::load<uint16_t, 4>( |
| gpu::g_inverseGaussianIntegralTableF16 + i)); |
| simd::store(inverseGaussianTable + i, f32s); |
| }; |
| auto inverseGaussianIntegral = [&inverseGaussianTable](float y) { |
| return function_table_lookup(y, |
| inverseGaussianTable, |
| std::size(inverseGaussianTable)); |
| }; |
| |
| CHECK(inverseGaussianTable[0] == 0); |
| CHECK(inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1] == 1); |
| if (gpu::GAUSSIAN_TABLE_SIZE & 1) |
| { |
| CHECK(inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2] == .5f); |
| } |
| else |
| { |
| CHECK((inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2 - 1] + |
| inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE / 2]) / |
| 2 == |
| Approx(.5f).margin(1e-4f)); |
| } |
| for (int i = 1; i < gpu::GAUSSIAN_TABLE_SIZE; ++i) |
| { |
| CHECK(inverseGaussianTable[i - 1] <= inverseGaussianTable[i]); |
| } |
| for (int i = 0; i < (gpu::GAUSSIAN_TABLE_SIZE + 1) / 2 - 4; ++i) |
| { |
| CHECK(inverseGaussianTable[i] + |
| inverseGaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1 - i] == |
| Approx(1).margin(i > 100 ? 5e-4f |
| : i > 4 ? 1e-3f |
| : 1e-2f)); |
| } |
| |
| // Check that the inverse table is actually an inverse of the gaussian |
| // integral. |
| float M = 21; |
| for (float x = 0; x <= 1; x += 1.f / (gpu::GAUSSIAN_TABLE_SIZE * M)) |
| { |
| float y = gaussianIntegral(x); |
| // The inverse table loses precision at the inner and outer cells. |
| float margin = x > .125f && x < .875f ? 1.f / 512 |
| : x > .04f && x < .96f ? 1.f / 256 |
| : x > .02f && x < .98f ? 1.f / 128 |
| : 1.f / 95; |
| CHECK(inverseGaussianIntegral(y) == Approx(x).margin(margin)); |
| } |
| |
| // Check inverse_gaussian_integral edge cases. |
| CHECK(inverseGaussianIntegral(-1) == 0); |
| CHECK(inverseGaussianIntegral(2) == 1); |
| CHECK(inverseGaussianIntegral(-std::numeric_limits<float>::infinity()) == |
| 0); |
| CHECK(inverseGaussianIntegral(std::numeric_limits<float>::infinity()) == 1); |
| CHECK(inverseGaussianIntegral(std::numeric_limits<float>::quiet_NaN()) == |
| 0); |
| } |
| } // namespace rive |