blob: 14e6f831b5008d16b989382ffbfbb665ea36ce98 [file] [log] [blame] [edit]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/base/vector_math.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
#include "base/containers/span_reader.h"
#include "base/containers/span_writer.h"
#include "base/cpu.h"
#include "base/memory/aligned_memory.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringize_macros.h"
#include "base/types/zip.h"
#include "build/build_config.h"
#include "media/base/vector_math_testing.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace media {
// Default test values.
static constexpr float kScale = 0.5;
static constexpr float kInputFillValue = 1.0;
static constexpr float kOutputFillValue = 3.0;
static constexpr int kVectorSize = 8192;
// List of unclamped values that are out of bounds and within bounds.
static const float kUnclampedInputValues[] = {
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::signaling_NaN(),
-std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::infinity(),
-2.0,
-1.0,
-0.5,
0.0,
0.5,
1.0,
2.0,
};
// Expected result of clamping `kUnclampedInputValues`.
static const float kClampedOutputValues[] = {0, 0, -1.0, 1.0, -1.0, -1.0,
-0.5, 0.0, 0.5, 1.0, 1.0};
static_assert(std::size(kUnclampedInputValues) ==
std::size(kClampedOutputValues));
class VectorMathTest : public testing::Test {
public:
VectorMathTest() {
// Initialize input and output vectors.
input_array_ = base::AlignedUninit<float>(kVectorSize,
vector_math::kRequiredAlignment);
output_array_ = base::AlignedUninit<float>(kVectorSize,
vector_math::kRequiredAlignment);
}
VectorMathTest(const VectorMathTest&) = delete;
VectorMathTest& operator=(const VectorMathTest&) = delete;
void FillTestVectors(float input, float output) {
// Setup input and output vectors.
std::ranges::fill(input_array_, input);
std::ranges::fill(output_array_, output);
}
void FillTestClampingVectors(base::span<const float> input, float output) {
// Setup input and output vectors.
FillSpan(input_array_, input);
std::ranges::fill(output_array_, output);
}
void VerifyOutput(float value) {
EXPECT_TRUE(std::ranges::all_of(
output_array_, [value](float datum) { return datum == value; }));
}
void VerifyClampOutput(base::span<const float> values) {
auto reader = base::SpanReader(base::span(output_array_));
while (reader.remaining() > values.size()) {
auto output_values = *reader.Read(values.size());
EXPECT_EQ(output_values, values);
}
if (reader.remaining()) {
auto remaining_values = reader.remaining_span();
EXPECT_EQ(remaining_values, values.first(remaining_values.size()));
}
}
protected:
base::AlignedHeapArray<float> input_array_;
base::AlignedHeapArray<float> output_array_;
private:
// Fills `dest` with `values`, repeating `values`.
void FillSpan(base::span<float> dest, base::span<const float> values) {
auto writer = base::SpanWriter(dest);
// Fill as much as possible with `values`.
while (writer.remaining() > values.size()) {
writer.Write(values);
}
// Fill the remaining space with the start of values.
if (writer.remaining()) {
writer.Write(values.first((writer.remaining())));
}
}
};
// Ensure each optimized vector_math::FMAC() method returns the same value.
TEST_F(VectorMathTest, FMAC) {
static const float kResult = kInputFillValue * kScale + kOutputFillValue;
{
SCOPED_TRACE("FMAC");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC(input_array_, kScale, output_array_);
VerifyOutput(kResult);
}
{
SCOPED_TRACE("FMAC_C");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC_C(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
#if defined(ARCH_CPU_X86_FAMILY)
{
SCOPED_TRACE("FMAC_SSE");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC_SSE(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
{
base::CPU cpu;
if (cpu.has_avx2() && cpu.has_fma3()) {
SCOPED_TRACE("FMAC_AVX2");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC_AVX2(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
}
#endif
#if defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON)
{
SCOPED_TRACE("FMAC_NEON");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC_NEON(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
#endif
}
// Ensure each optimized vector_math::FMUL() method returns the same value.
TEST_F(VectorMathTest, FMUL) {
static const float kResult = kInputFillValue * kScale;
{
SCOPED_TRACE("FMUL");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL(input_array_, kScale, output_array_);
VerifyOutput(kResult);
}
{
SCOPED_TRACE("FMUL_C");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL_C(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
#if defined(ARCH_CPU_X86_FAMILY)
{
SCOPED_TRACE("FMUL_SSE");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL_SSE(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
{
base::CPU cpu;
if (cpu.has_avx2()) {
SCOPED_TRACE("FMUL_AVX2");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL_AVX2(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
}
#endif
#if defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON)
{
SCOPED_TRACE("FMUL_NEON");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL_NEON(input_array_.data(), kScale, kVectorSize,
output_array_.data());
VerifyOutput(kResult);
}
#endif
}
// Ensure each optimized vector_math::FCLAMP() method returns the same value.
TEST_F(VectorMathTest, FCLAMP) {
{
SCOPED_TRACE("FCLAMP");
FillTestClampingVectors(kUnclampedInputValues, kOutputFillValue);
vector_math::FCLAMP(input_array_, output_array_);
VerifyClampOutput(kClampedOutputValues);
}
{
SCOPED_TRACE("FCLAMP_C");
FillTestClampingVectors(kUnclampedInputValues, kOutputFillValue);
vector_math::FCLAMP_C(input_array_.data(), kVectorSize,
output_array_.data());
VerifyClampOutput(kClampedOutputValues);
}
#if defined(ARCH_CPU_X86_FAMILY)
{
SCOPED_TRACE("FCLAMP_SSE");
FillTestClampingVectors(kUnclampedInputValues, kOutputFillValue);
vector_math::FCLAMP_SSE(input_array_.data(), kVectorSize,
output_array_.data());
VerifyClampOutput(kClampedOutputValues);
}
{
base::CPU cpu;
if (cpu.has_avx()) {
SCOPED_TRACE("FCLAMP_AVX");
FillTestClampingVectors(kUnclampedInputValues, kOutputFillValue);
vector_math::FCLAMP_AVX(input_array_.data(), kVectorSize,
output_array_.data());
VerifyClampOutput(kClampedOutputValues);
}
}
#endif
#if defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON)
{
SCOPED_TRACE("FCLAMP_NEON");
FillTestClampingVectors(kUnclampedInputValues, kOutputFillValue);
vector_math::FCLAMP_NEON(input_array_.data(), kVectorSize,
output_array_.data());
VerifyClampOutput(kClampedOutputValues);
}
#endif
}
// Algorithms handle "leftover" data that is too small to fill an SIMD
// instruction differently. Make sure that this data is also properly sanitized.
TEST_F(VectorMathTest, FCLAMP_remainder_data) {
// Feed in values 1 at a time to guarantee we don't use SIMD.
static constexpr int kSmallVectorSize = 1;
static constexpr float kGuardValue = 123.0f;
const auto run_per_value_clamp_test =
[&](void (*fn)(const float[], int, float[])) {
for (auto [input, output] :
base::zip(kUnclampedInputValues, kClampedOutputValues)) {
input_array_[0] = input;
output_array_[0] = kGuardValue;
fn(input_array_.data(), kSmallVectorSize, output_array_.data());
EXPECT_EQ(output_array_[0], output);
}
};
{
SCOPED_TRACE("FCLAMP_C");
run_per_value_clamp_test(vector_math::FCLAMP_C);
}
#if defined(ARCH_CPU_X86_FAMILY)
{
SCOPED_TRACE("FCLAMP_SSE");
run_per_value_clamp_test(vector_math::FCLAMP_SSE);
}
{
base::CPU cpu;
if (cpu.has_avx()) {
SCOPED_TRACE("FCLAMP_AVX");
run_per_value_clamp_test(vector_math::FCLAMP_AVX);
}
}
#endif
#if defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON)
{
SCOPED_TRACE("FCLAMP_NEON");
run_per_value_clamp_test(vector_math::FCLAMP_NEON);
}
#endif
}
TEST_F(VectorMathTest, EmptyInputs) {
{
SCOPED_TRACE("FMUL");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMUL(base::span<float>(), kScale, output_array_);
VerifyOutput(kOutputFillValue);
}
{
SCOPED_TRACE("FMAC");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC(base::span<float>(), kScale, output_array_);
VerifyOutput(kOutputFillValue);
}
{
SCOPED_TRACE("FCLAMP");
FillTestVectors(kInputFillValue, kOutputFillValue);
vector_math::FMAC(base::span<float>(), kScale, output_array_);
VerifyOutput(kOutputFillValue);
}
}
class EWMATestScenario {
public:
EWMATestScenario(float initial_value,
base::span<const float> src,
float smoothing_factor)
: initial_value_(initial_value),
smoothing_factor_(smoothing_factor),
expected_final_avg_(initial_value) {
CopyDataAligned(src);
}
// Copy constructor and assignment operator for ::testing::Values(...).
EWMATestScenario(const EWMATestScenario& other) { *this = other; }
EWMATestScenario& operator=(const EWMATestScenario& other) {
this->initial_value_ = other.initial_value_;
this->smoothing_factor_ = other.smoothing_factor_;
this->CopyDataAligned(other.data_);
this->expected_final_avg_ = other.expected_final_avg_;
this->expected_max_ = other.expected_max_;
return *this;
}
EWMATestScenario ScaledBy(float scale) const {
EWMATestScenario result(*this);
std::ranges::for_each(result.data_,
[scale](float& datum) { datum *= scale; });
return result;
}
EWMATestScenario WithImpulse(float value, int offset) const {
EWMATestScenario result(*this);
result.data_[offset] = value;
return result;
}
EWMATestScenario HasExpectedResult(float final_avg_value,
float max_value) const {
EWMATestScenario result(*this);
result.expected_final_avg_ = final_avg_value;
result.expected_max_ = max_value;
return result;
}
void RunTest() const {
{
SCOPED_TRACE("EWMAAndMaxPower");
const std::pair<float, float>& result = vector_math::EWMAAndMaxPower(
initial_value_, data_, smoothing_factor_);
EXPECT_NEAR(expected_final_avg_, result.first, 0.0000001f);
EXPECT_NEAR(expected_max_, result.second, 0.0000001f);
}
{
SCOPED_TRACE("EWMAAndMaxPower_C");
const std::pair<float, float>& result = vector_math::EWMAAndMaxPower_C(
initial_value_, data_.data(), data_.size(), smoothing_factor_);
EXPECT_NEAR(expected_final_avg_, result.first, 0.0000001f);
EXPECT_NEAR(expected_max_, result.second, 0.0000001f);
}
#if defined(ARCH_CPU_X86_FAMILY)
{
SCOPED_TRACE("EWMAAndMaxPower_SSE");
const std::pair<float, float>& result = vector_math::EWMAAndMaxPower_SSE(
initial_value_, data_.data(), data_.size(), smoothing_factor_);
EXPECT_NEAR(expected_final_avg_, result.first, 0.0000001f);
EXPECT_NEAR(expected_max_, result.second, 0.0000001f);
}
{
base::CPU cpu;
if (cpu.has_avx2() && cpu.has_fma3()) {
SCOPED_TRACE("EWMAAndMaxPower_AVX2");
const std::pair<float, float>& result =
vector_math::EWMAAndMaxPower_AVX2(initial_value_, data_.data(),
data_.size(), smoothing_factor_);
EXPECT_NEAR(expected_final_avg_, result.first, 0.0000001f);
EXPECT_NEAR(expected_max_, result.second, 0.0000001f);
}
}
#endif
#if defined(ARCH_CPU_ARM_FAMILY) && defined(USE_NEON)
{
SCOPED_TRACE("EWMAAndMaxPower_NEON");
const std::pair<float, float>& result = vector_math::EWMAAndMaxPower_NEON(
initial_value_, data_.data(), data_.size(), smoothing_factor_);
EXPECT_NEAR(expected_final_avg_, result.first, 0.0000001f);
EXPECT_NEAR(expected_max_, result.second, 0.0000001f);
}
#endif
}
private:
void CopyDataAligned(base::span<const float> src) {
if (src.empty()) {
data_ = base::AlignedHeapArray<float>();
return;
}
data_ =
base::AlignedUninit<float>(src.size(), vector_math::kRequiredAlignment);
data_.copy_from(src);
}
float initial_value_;
base::AlignedHeapArray<float> data_;
float smoothing_factor_;
float expected_final_avg_;
float expected_max_ = 0.0f;
};
typedef testing::TestWithParam<EWMATestScenario> VectorMathEWMAAndMaxPowerTest;
TEST_P(VectorMathEWMAAndMaxPowerTest, Correctness) {
GetParam().RunTest();
}
static const float kZeros[] = { // 32 zeros
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
static const float kOnes[] = { // 32 ones
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};
static const float kCheckerboard[] = { // 32 alternating 0, 1
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1
};
static const float kInverseCheckerboard[] = { // 32 alternating 1, 0
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0
};
INSTANTIATE_TEST_SUITE_P(
Scenarios,
VectorMathEWMAAndMaxPowerTest,
::testing::Values(
// Zero-length input: Result should equal initial value.
EWMATestScenario(0.0f, base::span<float>(), 0.0f)
.HasExpectedResult(0.0f, 0.0f),
EWMATestScenario(1.0f, base::span<float>(), 0.0f)
.HasExpectedResult(1.0f, 0.0f),
// Smoothing factor of zero: Samples have no effect on result.
EWMATestScenario(0.0f, kOnes, 0.0f).HasExpectedResult(0.0f, 1.0f),
EWMATestScenario(1.0f, kZeros, 0.0f).HasExpectedResult(1.0f, 0.0f),
// Smothing factor of one: Result = last sample squared.
EWMATestScenario(0.0f, kCheckerboard, 1.0f)
.ScaledBy(2.0f)
.HasExpectedResult(4.0f, 4.0f),
EWMATestScenario(1.0f, kInverseCheckerboard, 1.0f)
.ScaledBy(2.0f)
.HasExpectedResult(0.0f, 4.0f),
// Smoothing factor of 1/4, muted signal.
EWMATestScenario(1.0f, base::span(kZeros).first(1u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 1.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(2u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 2.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(3u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 3.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(12u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 12.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(13u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 13.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(14u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 14.0f), 0.0f),
EWMATestScenario(1.0f, base::span(kZeros).first(15u), 0.25f)
.HasExpectedResult(std::pow(0.75f, 15.0f), 0.0f),
// Smoothing factor of 1/4, constant full-amplitude signal.
EWMATestScenario(0.0f, base::span(kOnes).first(1u), 0.25f)
.HasExpectedResult(0.25f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(2u), 0.25f)
.HasExpectedResult(0.4375f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(3u), 0.25f)
.HasExpectedResult(0.578125f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(12u), 0.25f)
.HasExpectedResult(0.96832365f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(13u), 0.25f)
.HasExpectedResult(0.97624274f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(14u), 0.25f)
.HasExpectedResult(0.98218205f, 1.0f),
EWMATestScenario(0.0f, base::span(kOnes).first(15u), 0.25f)
.HasExpectedResult(0.98663654f, 1.0f),
// Smoothing factor of 1/4, checkerboard signal.
EWMATestScenario(0.0f, base::span(kCheckerboard).first(1u), 0.25f)
.HasExpectedResult(0.0f, 0.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(2u), 0.25f)
.HasExpectedResult(0.25f, 1.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(3u), 0.25f)
.HasExpectedResult(0.1875f, 1.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(12u), 0.25f)
.HasExpectedResult(0.55332780f, 1.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(13u), 0.25f)
.HasExpectedResult(0.41499585f, 1.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(14u), 0.25f)
.HasExpectedResult(0.56124689f, 1.0f),
EWMATestScenario(0.0f, base::span(kCheckerboard).first(15u), 0.25f)
.HasExpectedResult(0.42093517f, 1.0f),
// Smoothing factor of 1/4, inverse checkerboard signal.
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(1u),
0.25f)
.HasExpectedResult(0.25f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(2u),
0.25f)
.HasExpectedResult(0.1875f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(3u),
0.25f)
.HasExpectedResult(0.390625f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(12u),
0.25f)
.HasExpectedResult(0.41499585f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(13u),
0.25f)
.HasExpectedResult(0.56124689f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(14u),
0.25f)
.HasExpectedResult(0.42093517f, 1.0f),
EWMATestScenario(0.0f,
base::span(kInverseCheckerboard).first(15u),
0.25f)
.HasExpectedResult(0.56570137f, 1.0f),
// Smoothing factor of 1/4, impluse signal.
EWMATestScenario(0.0f, base::span(kZeros).first(3u), 0.25f)
.WithImpulse(2.0f, 0)
.HasExpectedResult(0.562500f, 4.0f),
EWMATestScenario(0.0f, base::span(kZeros).first(3u), 0.25f)
.WithImpulse(2.0f, 1)
.HasExpectedResult(0.75f, 4.0f),
EWMATestScenario(0.0f, base::span(kZeros).first(3u), 0.25f)
.WithImpulse(2.0f, 2)
.HasExpectedResult(1.0f, 4.0f),
EWMATestScenario(0.0f, kZeros, 0.25f)
.WithImpulse(2.0f, 0)
.HasExpectedResult(0.00013394f, 4.0f),
EWMATestScenario(0.0f, kZeros, 0.25f)
.WithImpulse(2.0f, 1)
.HasExpectedResult(0.00017858f, 4.0f),
EWMATestScenario(0.0f, kZeros, 0.25f)
.WithImpulse(2.0f, 2)
.HasExpectedResult(0.00023811f, 4.0f),
EWMATestScenario(0.0f, kZeros, 0.25f)
.WithImpulse(2.0f, 3)
.HasExpectedResult(0.00031748f, 4.0f)));
} // namespace media