blob: ad6c81dafbe6ee0341b75b82374ddbccad9695e5 [file]
/*
* Copyright 2024 Rive
*/
#include "common/render_context_null.hpp"
#include "rive/math/math_types.hpp"
#include "rive/renderer/draw.hpp"
#include "rive/renderer/gpu.hpp"
#include "rive/renderer/rive_render_paint.hpp"
#include "rive_render_path.hpp"
#include "shaders/constants.glsl"
#include <catch.hpp>
#include <cstring>
#include <map>
#include <set>
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.0f);
CHECK(gaussianTable[gpu::GAUSSIAN_TABLE_SIZE - 1] == 1.0f);
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 / 90;
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);
}
// ForEachUbershaderPermutation() has to enumerate every DrawType a backend can
// actually bind. The dynamic-state DrawTypes were missing from msaa's
// list, so no ubershader ever got built for them and a draw that fell back to
// the async path had nothing to fall back to.
TEST_CASE("ForEachUbershaderPermutation", "[gpu]")
{
auto enumerateDrawTypes = [](bool supportsPipelineDynamicState) {
gpu::PlatformFeatures platformFeatures;
platformFeatures
.supportsPipelineDynamicState = supportsPipelineDynamicState;
std::set<gpu::DrawType> drawTypes;
gpu::ForEachUbershaderPermutation(
gpu::InterlockMode::depthStencil,
platformFeatures,
[&drawTypes](
gpu::DrawType drawType,
gpu::ShaderFeatures,
gpu::ShaderMiscFlags) {
drawTypes.insert(drawType);
return true; // Keep iterating.
});
return drawTypes;
};
const std::set<gpu::DrawType> withDynamicState = enumerateDrawTypes(true);
CHECK(withDynamicState.count(gpu::DrawType::stencilDynamicMidpointFans) == 1);
CHECK(withDynamicState.count(gpu::DrawType::stencilDynamicOuterCubics) == 1);
// Backends that can't switch depth/stencil/cull/color-write without
// rebinding a pipeline never bind these, so they don't precompile them.
const std::set<gpu::DrawType> withoutDynamicState = enumerateDrawTypes(false);
CHECK(
withoutDynamicState.count(gpu::DrawType::stencilDynamicMidpointFans) ==
0);
CHECK(
withoutDynamicState.count(gpu::DrawType::stencilDynamicOuterCubics) == 0);
// The passes they collapse are enumerated either way.
for (auto drawType :
{gpu::DrawType::stencilMidpointFanBorrowedCoverage,
gpu::DrawType::stencilMidpointFans,
gpu::DrawType::stencilMidpointFanReset})
{
CHECK(withDynamicState.count(drawType) == 1);
CHECK(withoutDynamicState.count(drawType) == 1);
}
}
// ShaderUniqueKey() packs shaderMiscFlags densely, using only the bits that are
// relevant to the interlockMode. Draw types that share a drawTypeKey therefore
// have to agree on that layout -- otherwise the same packed value means
// different flags for each, and two different shaders land on one key.
TEST_CASE("shader_unique_keys_do_not_collide", "[gpu]")
{
gpu::PlatformFeatures platformFeatures;
platformFeatures.supportsPipelineDynamicState = true;
for (auto interlockMode :
{gpu::InterlockMode::rasterOrdering,
gpu::InterlockMode::atomics,
gpu::InterlockMode::clockwise,
gpu::InterlockMode::clockwiseAtomic,
gpu::InterlockMode::depthStencil})
{
// Two draws may share a key only if they compile to the same shader,
// which requires identical shaderMiscFlags.
std::map<uint32_t, gpu::ShaderMiscFlags> keyToMiscFlags;
gpu::ForEachUbershaderPermutation(
interlockMode,
platformFeatures,
[&](gpu::DrawType drawType,
gpu::ShaderFeatures shaderFeatures,
gpu::ShaderMiscFlags shaderMiscFlags) {
uint32_t key = gpu::ShaderUniqueKey(
drawType,
shaderFeatures,
interlockMode,
shaderMiscFlags);
auto [it, inserted] = keyToMiscFlags.insert({key, shaderMiscFlags});
if (!inserted)
{
INFO(
"interlockMode "
<< static_cast<uint32_t>(interlockMode) << ", drawType "
<< static_cast<uint32_t>(drawType) << ", key " << key);
CHECK(it->second == shaderMiscFlags);
}
return true; // Keep iterating.
});
}
}
TEST_CASE("layer_mask_mode_stays_out_of_the_clip_id", "[gpu]")
{
auto ctx = RenderContextNULL::MakeContext();
ctx->beginFrame({.renderTargetWidth = 64, .renderTargetHeight = 64});
auto path = ctx->makeEmptyRenderPath();
path->moveTo(8, 8);
path->lineTo(56, 8);
path->lineTo(56, 56);
path->lineTo(8, 56);
path->close();
// The paint's params word, which follows the 6-float matrix in the record.
auto params = [&](LayerMaskMode mode) {
RiveRenderPaint paint;
paint.layerMask(nullptr, mode);
gpu::DrawUniquePtr draw = gpu::PathDraw::Make(
ctx.get(),
Mat2D(),
nullptr,
static_rcp_cast<RiveRenderPath>(path),
FillRule::nonZero,
&paint,
/*modulatedOpacity=*/1.f,
/*modulatedColor=*/0xffffffff);
REQUIRE(draw != nullptr);
draw->setClipID(2);
gpu::PathData data(
*static_cast<const gpu::PathDraw*>(draw.get()),
/*zIndex=*/0,
ctx->frameInterlockMode(),
ctx->platformFeatures());
uint32_t out;
memcpy(&out, reinterpret_cast<const char*>(&data) + 24, sizeof(out));
return out;
};
uint32_t valid = params(LayerMaskMode::invertedLuminance);
CHECK((valid >> 16) == 2);
CHECK(
((valid & PAINT_LAYER_MASK_MODE_MASK) >> PAINT_LAYER_MASK_MODE_SHIFT) ==
LAYER_MASK_MODE_INVERTED_LUMINANCE);
// A number bound to the mode can carry any value. 8 used to land on the
// low bit of the clipID.
uint32_t outOfRange = params(static_cast<LayerMaskMode>(8));
CHECK((outOfRange >> 16) == 2);
auto target = ctx->static_impl_cast<RenderContextNULL>()->makeRenderTarget(
64,
64);
ctx->flush({.renderTarget = target.get()});
}
} // namespace rive