blob: 404e1b976855dcd938be39ce06a50083dbffb88c [file] [edit]
/*
* Copyright (c) 2026, Alliance for Open Media. All rights reserved.
*
* This source code is subject to the terms of the BSD 2 Clause License and
* the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
* was not distributed with this source code in the LICENSE file, you can
* obtain it at www.aomedia.org/license/software. If the Alliance for Open
* Media Patent License 1.0 was not distributed with this source code in the
* PATENTS file, you can obtain it at www.aomedia.org/license/patent.
*/
#include <algorithm>
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <tuple>
#include <vector>
#include "gtest/gtest.h"
#include "config/aom_config.h"
#include "config/av1_rtcd.h"
#include "aom_mem/aom_mem.h"
#include "av1/encoder/block.h"
#include "test/acm_random.h"
#include "test/register_state_check.h"
#include "test/util.h"
namespace {
using libaom_test::ACMRandom;
constexpr int kHogBins = 32;
constexpr int kFixPrecBits = 16;
constexpr float kHogTol = 1e-6f;
constexpr int32_t kHogThresholds[kHogBins] = {
-1334015, -441798, -261605, -183158, -138560, -109331, -88359, -72303,
-59392, -48579, -39272, -30982, -23445, -16400, -9715, -3194,
3227, 9748, 16433, 23478, 31015, 39305, 48611, 59425,
72336, 88392, 109364, 138593, 183191, 261638, 441831, INT32_MAX
};
int RefGetHistBinIdx(int dx, int dy) {
if (dy == 0) return 16;
const int32_t ratio = (dy * (1 << kFixPrecBits)) / dx;
for (int idx = 0; idx < kHogBins - 1; ++idx) {
if (ratio <= kHogThresholds[idx]) return idx;
}
return kHogBins - 1;
}
template <typename Pixel>
void RefGenerateHog(const Pixel *src, int stride, int rows, int cols,
float *hist) {
static constexpr int kSobelX[3][3] = { { -1, 0, 1 },
{ -2, 0, 2 },
{ -1, 0, 1 } };
static constexpr int kSobelY[3][3] = { { -1, -2, -1 },
{ 0, 0, 0 },
{ 1, 2, 1 } };
float total = 0.1f;
for (int r = 1; r < rows - 1; ++r) {
for (int c = 1; c < cols - 1; ++c) {
int dx = 0;
int dy = 0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
const int val = src[(r + y) * stride + (c + x)];
dx += kSobelX[y + 1][x + 1] * val;
dy += kSobelY[y + 1][x + 1] * val;
}
}
if (dx == 0 && dy == 0) continue;
const int temp = std::abs(dx) + std::abs(dy);
total += temp;
if (dx == 0) {
hist[0] += temp / 2;
hist[kHogBins - 1] += temp / 2;
} else {
hist[RefGetHistBinIdx(dx, dy)] += temp;
}
}
}
for (int i = 0; i < kHogBins; ++i) hist[i] /= total;
}
enum class Pattern {
kRandom = 0,
kUniform, // dx == 0 && dy == 0
kHorizontalEdge, // dx == 0, dy != 0
kVerticalEdge, // dx != 0, dy == 0
kMaxChecker, // alternating 0 and max_val
kMaxDiagStep, // maximum simultaneous |dx| and |dy|
kGradientRamp, // sweeps many dy/dx ratios across threshold boundaries
};
template <typename Pixel>
void FillPattern(Pixel *buf, int stride, int rows, int cols, int bd,
Pattern pat, ACMRandom *rnd) {
const int max_val = (1 << bd) - 1;
// Fill full stride including padding columns with sentinel noise to catch any
// out-of-bounds column reads (such as cols < 3 priming).
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < stride; ++c) {
buf[r * stride + c] = static_cast<Pixel>(rnd->Rand16() & max_val);
}
}
const int const_val = rnd->Rand16() & max_val;
for (int r = 0; r < rows; ++r) {
for (int c = 0; c < cols; ++c) {
int v = 0;
switch (pat) {
case Pattern::kRandom: v = rnd->Rand16() & max_val; break;
case Pattern::kUniform: v = const_val; break;
case Pattern::kHorizontalEdge: v = (r & 2) ? max_val : 0; break;
case Pattern::kVerticalEdge: v = (c & 2) ? max_val : 0; break;
case Pattern::kMaxChecker: v = ((r ^ c) & 1) ? max_val : 0; break;
case Pattern::kMaxDiagStep: v = (r + c >= 3) ? max_val : 0; break;
case Pattern::kGradientRamp:
v = ((r * 37 + c * 13 + (r * c) * 3) * (1 << (bd - 8))) & max_val;
break;
}
buf[r * stride + c] = static_cast<Pixel>(v);
}
}
}
void ExpectHogNear(const float *ref, const float *tst, int rows, int cols,
int stride, int bd) {
for (int b = 0; b < kHogBins; ++b) {
EXPECT_NEAR(ref[b], tst[b], kHogTol)
<< "bin=" << b << " rows=" << rows << " cols=" << cols
<< " stride=" << stride << " bd=" << bd;
}
}
void ExpectGradInfoEq(const PixelLevelGradientInfo *ref,
const PixelLevelGradientInfo *tst, size_t count,
BLOCK_SIZE sb_size, PLANE_TYPE plane, int ss_x, int ss_y,
int stride, int bd) {
for (size_t i = 0; i < count; ++i) {
ASSERT_EQ(ref[i].abs_dx_abs_dy_sum, tst[i].abs_dx_abs_dy_sum)
<< "idx=" << i << " sb_size=" << sb_size << " plane=" << plane
<< " ss=(" << ss_x << "," << ss_y << ") stride=" << stride
<< " bd=" << bd;
ASSERT_EQ(ref[i].hist_bin_idx, tst[i].hist_bin_idx)
<< "idx=" << i << " sb_size=" << sb_size << " plane=" << plane;
ASSERT_EQ(ref[i].is_dx_zero, tst[i].is_dx_zero)
<< "idx=" << i << " sb_size=" << sb_size << " plane=" << plane;
}
}
// Exercises the C Sobel sliding window and branchless binary search directly
// against an independent 3x3 Sobel + linear-scan oracle on all platforms,
// including cols == 1 with stride == 1 under ASan to verify no OOB column load.
TEST(IntraHogRefTest, LowbdGenerateHogMatchesTextbookOracle) {
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr int kDims[] = { 1, 2, 3, 4, 5, 7, 8, 16, 33 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
for (int rows : kDims) {
for (int cols : kDims) {
for (int pad : { 0, 1, 13 }) {
const int stride = cols + pad;
// Exact-sized heap buffer so ASan catches any OOB read when cols < 3.
std::vector<uint8_t> buf(static_cast<size_t>(rows * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, rows, cols, 8, pat, &rnd);
float hist_ref[kHogBins] = { 0.0f };
float hist_c[kHogBins] = { 0.0f };
if (rows < 3 || cols < 3) {
// Also verify non-zero initial bins are normalized identically.
hist_ref[0] = hist_c[0] = 1.0f;
}
RefGenerateHog(buf.data(), stride, rows, cols, hist_ref);
av1_lowbd_generate_hog_c(buf.data(), stride, rows, cols, hist_c);
ExpectHogNear(hist_ref, hist_c, rows, cols, stride, 8);
}
}
}
}
// Probe random 3x3 patches to cover diverse signed dy/dx ratios.
uint8_t patch[9];
for (int iter = 0; iter < 1000; ++iter) {
for (int i = 0; i < 9; ++i) patch[i] = rnd.Rand8();
float hist_ref[kHogBins] = { 0.0f };
float hist_c[kHogBins] = { 0.0f };
RefGenerateHog(patch, 3, 3, 3, hist_ref);
av1_lowbd_generate_hog_c(patch, 3, 3, 3, hist_c);
ExpectHogNear(hist_ref, hist_c, 3, 3, 3, 8);
}
}
#if CONFIG_AV1_HIGHBITDEPTH
TEST(IntraHogRefTest, HighbdGenerateHogMatchesTextbookOracle) {
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr int kDims[] = { 1, 2, 3, 4, 5, 8, 17 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
for (int bd : { 8, 10, 12 }) {
for (int rows : kDims) {
for (int cols : kDims) {
for (int pad : { 0, 7 }) {
const int stride = cols + pad;
std::vector<uint16_t> buf(static_cast<size_t>(rows * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, rows, cols, bd, pat, &rnd);
float hist_ref[kHogBins] = { 0.0f };
float hist_c[kHogBins] = { 0.0f };
RefGenerateHog(buf.data(), stride, rows, cols, hist_ref);
av1_highbd_generate_hog_c(CONVERT_TO_BYTEPTR(buf.data()), stride,
rows, cols, hist_c);
ExpectHogNear(hist_ref, hist_c, rows, cols, stride, bd);
}
}
}
}
}
// Explicitly probe every threshold boundary kHogThresholds[0..30] on both C
// and SSE2 using 12-bit 3x3 patches where dx = +/-128 (via p12=64 or p10=64,
// which leaves dy=0) and dy sweeps across the exact boundary value (via
// p21/p01 and p20=p22, keeping dx unchanged when even, or shifting 1 from
// p12=63 + p02=1 + p8=1 for even/odd dy).
for (int t = 0; t < kHogBins - 1; ++t) {
for (int sign_dx : { 1, -1 }) {
const int dx = sign_dx * 128;
const int center_dy = static_cast<int>(
(static_cast<int64_t>(kHogThresholds[t]) * dx) / (1 << kFixPrecBits));
for (int delta = -2; delta <= 2; ++delta) {
const int dy = center_dy + delta;
if (std::abs(dy) > 8190 || (dy & 1) != 0) continue;
uint16_t patch[9] = { 0 };
if (dx > 0) {
patch[5] = 64; // dx = +128, dy += 0
} else {
patch[3] = 64; // dx = -128, dy += 0
}
if (dy >= 0) {
patch[7] = static_cast<uint16_t>(dy / 2); // dy = +2 * patch[7]
} else {
patch[1] = static_cast<uint16_t>(-dy / 2); // dy = -2 * patch[1]
}
float hist_ref[kHogBins] = { 0.0f };
float hist_c[kHogBins] = { 0.0f };
RefGenerateHog(patch, 3, 3, 3, hist_ref);
av1_highbd_generate_hog_c(CONVERT_TO_BYTEPTR(patch), 3, 3, 3, hist_c);
ExpectHogNear(hist_ref, hist_c, 3, 3, 3, 12);
#if HAVE_SSE2
float hist_sse2[kHogBins] = { 0.0f };
av1_highbd_generate_hog_sse2(CONVERT_TO_BYTEPTR(patch), 3, 3, 3,
hist_sse2);
ExpectHogNear(hist_ref, hist_sse2, 3, 3, 3, 12);
#endif
}
}
}
}
#endif // CONFIG_AV1_HIGHBITDEPTH
using GenerateHogFunc = void (*)(const uint8_t *src, int stride, int rows,
int cols, float *hist);
using ComputeGradSbFunc = void (*)(MACROBLOCK *const x, BLOCK_SIZE sb_size,
PLANE_TYPE plane);
class LowbdIntraHogSimdTest
: public ::testing::TestWithParam<
std::tuple<GenerateHogFunc, ComputeGradSbFunc>> {};
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LowbdIntraHogSimdTest);
TEST_P(LowbdIntraHogSimdTest, GenerateHogMatchesC) {
const GenerateHogFunc test_fn = std::get<0>(GetParam());
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr int kDims[] = { 1, 2, 3, 4, 5, 7, 8, 16, 32, 33, 64 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
for (int rows : kDims) {
for (int cols : kDims) {
for (int pad : { 0, 1, 19 }) {
const int stride = cols + pad;
std::vector<uint8_t> buf(static_cast<size_t>(rows * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, rows, cols, 8, pat, &rnd);
float hist_ref[kHogBins] = { 0.0f };
float hist_tst[kHogBins] = { 0.0f };
av1_lowbd_generate_hog_c(buf.data(), stride, rows, cols, hist_ref);
API_REGISTER_STATE_CHECK(
test_fn(buf.data(), stride, rows, cols, hist_tst));
ExpectHogNear(hist_ref, hist_tst, rows, cols, stride, 8);
}
}
}
}
}
TEST_P(LowbdIntraHogSimdTest, ComputeGradientInfoSbMatchesC) {
const ComputeGradSbFunc test_fn = std::get<1>(GetParam());
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr BLOCK_SIZE kSbSizes[] = { BLOCK_4X4, BLOCK_4X8, BLOCK_8X4,
BLOCK_16X16, BLOCK_64X64, BLOCK_128X128 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
const size_t grad_count = PLANE_TYPES * MAX_SB_SQUARE;
std::vector<PixelLevelGradientInfo> grad_ref(grad_count);
std::vector<PixelLevelGradientInfo> grad_tst(grad_count);
for (BLOCK_SIZE sb_size : kSbSizes) {
for (PLANE_TYPE plane : { PLANE_TYPE_Y, PLANE_TYPE_UV }) {
for (int ss_x : { 0, 1 }) {
for (int ss_y : { 0, 1 }) {
if (plane == PLANE_TYPE_Y && (ss_x != 0 || ss_y != 0)) continue;
const int sb_h = block_size_high[sb_size] >> ss_y;
const int sb_w = block_size_wide[sb_size] >> ss_x;
for (int pad : { 0, 17 }) {
const int stride = sb_w + pad;
std::vector<uint8_t> buf(static_cast<size_t>(sb_h * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, sb_h, sb_w, 8, pat, &rnd);
const PixelLevelGradientInfo kCanary = { 0x5a5a, 0x5a, true };
std::fill(grad_ref.begin(), grad_ref.end(), kCanary);
std::fill(grad_tst.begin(), grad_tst.end(), kCanary);
auto x = std::make_unique<MACROBLOCK>();
memset(x.get(), 0, sizeof(*x));
x->plane[plane].src.buf = buf.data();
x->plane[plane].src.stride = stride;
x->e_mbd.plane[plane].subsampling_x = ss_x;
x->e_mbd.plane[plane].subsampling_y = ss_y;
x->pixel_gradient_info = grad_ref.data();
av1_lowbd_compute_gradient_info_sb_c(x.get(), sb_size, plane);
x->pixel_gradient_info = grad_tst.data();
API_REGISTER_STATE_CHECK(test_fn(x.get(), sb_size, plane));
ExpectGradInfoEq(grad_ref.data(), grad_tst.data(), grad_count,
sb_size, plane, ss_x, ss_y, stride, 8);
}
}
}
}
}
}
}
#if HAVE_SSE2
INSTANTIATE_TEST_SUITE_P(SSE2, LowbdIntraHogSimdTest,
::testing::Values(std::make_tuple(
&av1_lowbd_generate_hog_sse2,
&av1_lowbd_compute_gradient_info_sb_sse2)));
#endif // HAVE_SSE2
#if CONFIG_AV1_HIGHBITDEPTH
class HighbdIntraHogSimdTest
: public ::testing::TestWithParam<
std::tuple<GenerateHogFunc, ComputeGradSbFunc, int>> {};
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HighbdIntraHogSimdTest);
TEST_P(HighbdIntraHogSimdTest, GenerateHogMatchesC) {
const GenerateHogFunc test_fn = std::get<0>(GetParam());
const int bd = std::get<2>(GetParam());
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr int kDims[] = { 1, 2, 3, 4, 5, 7, 8, 16, 33, 64 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
for (int rows : kDims) {
for (int cols : kDims) {
for (int pad : { 0, 1, 11 }) {
const int stride = cols + pad;
std::vector<uint16_t> buf(static_cast<size_t>(rows * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, rows, cols, bd, pat, &rnd);
float hist_ref[kHogBins] = { 0.0f };
float hist_tst[kHogBins] = { 0.0f };
const uint8_t *const src8 = CONVERT_TO_BYTEPTR(buf.data());
av1_highbd_generate_hog_c(src8, stride, rows, cols, hist_ref);
API_REGISTER_STATE_CHECK(test_fn(src8, stride, rows, cols, hist_tst));
ExpectHogNear(hist_ref, hist_tst, rows, cols, stride, bd);
}
}
}
}
}
TEST_P(HighbdIntraHogSimdTest, ComputeGradientInfoSbMatchesC) {
const ComputeGradSbFunc test_fn = std::get<1>(GetParam());
const int bd = std::get<2>(GetParam());
ACMRandom rnd(ACMRandom::DeterministicSeed());
constexpr BLOCK_SIZE kSbSizes[] = { BLOCK_4X4, BLOCK_4X8, BLOCK_8X4,
BLOCK_16X16, BLOCK_64X64, BLOCK_128X128 };
constexpr Pattern kPatterns[] = {
Pattern::kRandom, Pattern::kUniform, Pattern::kHorizontalEdge,
Pattern::kVerticalEdge, Pattern::kMaxChecker, Pattern::kMaxDiagStep,
Pattern::kGradientRamp,
};
const size_t grad_count = PLANE_TYPES * MAX_SB_SQUARE;
std::vector<PixelLevelGradientInfo> grad_ref(grad_count);
std::vector<PixelLevelGradientInfo> grad_tst(grad_count);
for (BLOCK_SIZE sb_size : kSbSizes) {
for (PLANE_TYPE plane : { PLANE_TYPE_Y, PLANE_TYPE_UV }) {
for (int ss_x : { 0, 1 }) {
for (int ss_y : { 0, 1 }) {
if (plane == PLANE_TYPE_Y && (ss_x != 0 || ss_y != 0)) continue;
const int sb_h = block_size_high[sb_size] >> ss_y;
const int sb_w = block_size_wide[sb_size] >> ss_x;
for (int pad : { 0, 13 }) {
const int stride = sb_w + pad;
std::vector<uint16_t> buf(static_cast<size_t>(sb_h * stride));
for (Pattern pat : kPatterns) {
FillPattern(buf.data(), stride, sb_h, sb_w, bd, pat, &rnd);
const PixelLevelGradientInfo kCanary = { 0x5a5a, 0x5a, true };
std::fill(grad_ref.begin(), grad_ref.end(), kCanary);
std::fill(grad_tst.begin(), grad_tst.end(), kCanary);
auto x = std::make_unique<MACROBLOCK>();
memset(x.get(), 0, sizeof(*x));
x->plane[plane].src.buf = CONVERT_TO_BYTEPTR(buf.data());
x->plane[plane].src.stride = stride;
x->e_mbd.plane[plane].subsampling_x = ss_x;
x->e_mbd.plane[plane].subsampling_y = ss_y;
x->e_mbd.bd = bd;
x->e_mbd.cur_buf = nullptr;
x->pixel_gradient_info = grad_ref.data();
av1_highbd_compute_gradient_info_sb_c(x.get(), sb_size, plane);
x->pixel_gradient_info = grad_tst.data();
API_REGISTER_STATE_CHECK(test_fn(x.get(), sb_size, plane));
ExpectGradInfoEq(grad_ref.data(), grad_tst.data(), grad_count,
sb_size, plane, ss_x, ss_y, stride, bd);
}
}
}
}
}
}
}
#if HAVE_SSE2
INSTANTIATE_TEST_SUITE_P(
SSE2, HighbdIntraHogSimdTest,
::testing::Combine(
::testing::Values(&av1_highbd_generate_hog_sse2),
::testing::Values(&av1_highbd_compute_gradient_info_sb_sse2),
::testing::Values(8, 10, 12)));
#endif // HAVE_SSE2
#endif // CONFIG_AV1_HIGHBITDEPTH
} // namespace