Commit c8c8db39a7 for aom
commit c8c8db39a705f78836ec85bf28f2f3b516c06706
Author: Frank Galligan <fgalligan@google.com>
Date: Fri Oct 30 12:06:54 2026 -0700
av1: Optimize HoG bin lookup and Sobel window
Accelerate Histogram of Oriented Gradients (HoG) feature extraction for
intra mode search: - Replace the 4-way branch and linear scan in
get_hist_bin_idx with a branchless SSE2 16-bin comparison and trailing-
zero count (with a branchless 5-step binary search fallback). - Replace
redundant 3x3 pixel loads in lowbd/highbd_generate_hog and
lowbd/highbd_compute_gradient_info_sb with a horizontal sliding Sobel
window that loads only the 3 incoming right-column pixels per step.
Benchmarking results (incremental vs prev CL / cumulative vs baseline):
- Stock avifenc still-image (Default 1T): -2.28% / -16.71% encode time
- Stock avifenc still-image (Default 8T): -2.36% / -14.03% encode time
- 48-image multi-rate corpus (Full Suite 1T): -2.67% / -17.13% time
- Bit-exact output (1T and 8T)
TAG=agy
CONV=b929a630-a6cd-45a0-979c-1767c30178ee
Change-Id: I455796cc713c639d35c7bb85d1bef57b3e808df4
diff --git a/av1/av1.cmake b/av1/av1.cmake
index c95a0478c1..09741a2d80 100644
--- a/av1/av1.cmake
+++ b/av1/av1.cmake
@@ -344,6 +344,7 @@ list(APPEND AOM_AV1_ENCODER_INTRIN_SSE2
"${AOM_ROOT}/av1/encoder/x86/av1_quantize_sse2.c"
"${AOM_ROOT}/av1/encoder/x86/encodetxb_sse2.c"
"${AOM_ROOT}/av1/encoder/x86/error_intrin_sse2.c"
+ "${AOM_ROOT}/av1/encoder/x86/intra_mode_search_sse2.c"
"${AOM_ROOT}/av1/encoder/x86/model_rd_sse2.c"
"${AOM_ROOT}/av1/encoder/x86/reconinter_enc_sse2.c"
"${AOM_ROOT}/av1/encoder/x86/temporal_filter_sse2.c"
diff --git a/av1/common/av1_rtcd_defs.pl b/av1/common/av1_rtcd_defs.pl
index 7296f90376..4a7dcceaf6 100644
--- a/av1/common/av1_rtcd_defs.pl
+++ b/av1/common/av1_rtcd_defs.pl
@@ -498,6 +498,20 @@ if (aom_config("CONFIG_AV1_ENCODER") eq "yes") {
add_proto qw/void av1_interp_cubic_rate_dist/, "const double *p1, const double *p2, double x, double rate_dist_f[2]";
specialize qw/av1_interp_cubic_rate_dist sse2/;
+ add_proto qw/void av1_lowbd_generate_hog/, "const uint8_t *src, int stride, int rows, int cols, float *hist";
+ specialize qw/av1_lowbd_generate_hog sse2/;
+
+ add_proto qw/void av1_lowbd_compute_gradient_info_sb/, "struct macroblock *const x, BLOCK_SIZE sb_size, PLANE_TYPE plane";
+ specialize qw/av1_lowbd_compute_gradient_info_sb sse2/;
+
+ if (aom_config("CONFIG_AV1_HIGHBITDEPTH") eq "yes") {
+ add_proto qw/void av1_highbd_generate_hog/, "const uint8_t *src8, int stride, int rows, int cols, float *hist";
+ specialize qw/av1_highbd_generate_hog sse2/;
+
+ add_proto qw/void av1_highbd_compute_gradient_info_sb/, "struct macroblock *const x, BLOCK_SIZE sb_size, PLANE_TYPE plane";
+ specialize qw/av1_highbd_compute_gradient_info_sb sse2/;
+ }
+
add_proto qw/void av1_nn_predict/, "const float *input_nodes, const NN_CONFIG *const nn_config, int reduce_prec, float *const output";
add_proto qw/void av1_nn_fast_softmax_16/, "const float *input_nodes, float *output";
diff --git a/av1/encoder/intra_mode_search.c b/av1/encoder/intra_mode_search.c
index cad0cdc5ba..8c6cade9cc 100644
--- a/av1/encoder/intra_mode_search.c
+++ b/av1/encoder/intra_mode_search.c
@@ -1754,3 +1754,27 @@ int64_t av1_rd_pick_intra_sby_mode(const AV1_COMP *const cpi, MACROBLOCK *x,
av1_copy_array(xd->tx_type_map, ctx->tx_type_map, ctx->num_4x4_blk);
return best_rd;
}
+
+void av1_lowbd_generate_hog_c(const uint8_t *src, int stride, int rows,
+ int cols, float *hist) {
+ lowbd_generate_hog(src, stride, rows, cols, hist, get_hist_bin_idx);
+}
+
+void av1_lowbd_compute_gradient_info_sb_c(MACROBLOCK *const x,
+ BLOCK_SIZE sb_size,
+ PLANE_TYPE plane) {
+ lowbd_compute_gradient_info_sb(x, sb_size, plane, get_hist_bin_idx);
+}
+
+#if CONFIG_AV1_HIGHBITDEPTH
+void av1_highbd_generate_hog_c(const uint8_t *src8, int stride, int rows,
+ int cols, float *hist) {
+ highbd_generate_hog(src8, stride, rows, cols, hist, get_hist_bin_idx);
+}
+
+void av1_highbd_compute_gradient_info_sb_c(MACROBLOCK *const x,
+ BLOCK_SIZE sb_size,
+ PLANE_TYPE plane) {
+ highbd_compute_gradient_info_sb(x, sb_size, plane, get_hist_bin_idx);
+}
+#endif // CONFIG_AV1_HIGHBITDEPTH
diff --git a/av1/encoder/intra_mode_search_utils.h b/av1/encoder/intra_mode_search_utils.h
index 3947437aeb..c7f07dbdd5 100644
--- a/av1/encoder/intra_mode_search_utils.h
+++ b/av1/encoder/intra_mode_search_utils.h
@@ -17,6 +17,8 @@
#ifndef AOM_AV1_ENCODER_INTRA_MODE_SEARCH_UTILS_H_
#define AOM_AV1_ENCODER_INTRA_MODE_SEARCH_UTILS_H_
+#include "config/av1_rtcd.h"
+
#include "av1/common/enums.h"
#include "av1/common/pred_common.h"
#include "av1/common/reconintra.h"
@@ -37,13 +39,14 @@ extern "C" {
// intra_rd_variance_factor().
#define INTRA_RD_VAR_THRESH(X) (1.0 - (0.25 * (X)))
-#define BINS 32
+#define HOG_BINS 32
+#define HOG_FIX_PREC_BITS 16
static const float av1_intra_hog_model_bias[DIRECTIONAL_MODES] = {
0.450578f, 0.695518f, -0.717944f, -0.639894f,
-0.602019f, -0.453454f, 0.055857f, -0.465480f,
};
-static const float av1_intra_hog_model_weights[BINS * DIRECTIONAL_MODES] = {
+static const float av1_intra_hog_model_weights[HOG_BINS * DIRECTIONAL_MODES] = {
-3.076402f, -3.757063f, -3.275266f, -3.180665f, -3.452105f, -3.216593f,
-2.871212f, -3.134296f, -1.822324f, -2.401411f, -1.541016f, -1.195322f,
-0.434156f, 0.322868f, 2.260546f, 3.368715f, 3.989290f, 3.308487f,
@@ -90,7 +93,7 @@ static const float av1_intra_hog_model_weights[BINS * DIRECTIONAL_MODES] = {
};
static const NN_CONFIG av1_intra_hog_model_nnconfig = {
- BINS, // num_inputs
+ HOG_BINS, // num_inputs
DIRECTIONAL_MODES, // num_outputs
0, // num_hidden_layers
{ 0 },
@@ -102,79 +105,82 @@ static const NN_CONFIG av1_intra_hog_model_nnconfig = {
},
};
-#define FIX_PREC_BITS (16)
-static inline int get_hist_bin_idx(int dx, int dy) {
- const int32_t ratio = (dy * (1 << FIX_PREC_BITS)) / dx;
+typedef int (*hog_bin_lookup_fn)(int dx, int dy);
- // Find index by bisection
- static const int thresholds[BINS] = {
+static AOM_FORCE_INLINE const int32_t *av1_get_hog_thresholds(void) {
+ DECLARE_ALIGNED(64, static const int32_t, thresholds[HOG_BINS]) = {
-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
};
+ return thresholds;
+}
- int lo_idx = 0, hi_idx = BINS - 1;
- // Divide into segments of size 8 gives better performance than binary search
- // here.
- if (ratio <= thresholds[7]) {
- lo_idx = 0;
- hi_idx = 7;
- } else if (ratio <= thresholds[15]) {
- lo_idx = 8;
- hi_idx = 15;
- } else if (ratio <= thresholds[23]) {
- lo_idx = 16;
- hi_idx = 23;
- } else {
- lo_idx = 24;
- hi_idx = 31;
- }
-
- for (int idx = lo_idx; idx <= hi_idx; idx++) {
- if (ratio <= thresholds[idx]) {
- return idx;
- }
- }
- assert(0 && "No valid histogram bin found!");
- return BINS - 1;
+static inline int get_hist_bin_idx(int dx, int dy) {
+ if (dy == 0) return 16;
+ const int32_t ratio = (dy * (1 << HOG_FIX_PREC_BITS)) / dx;
+ const int32_t *const thresholds = av1_get_hog_thresholds();
+
+ // Branchless 5-step binary search across the 32 thresholds.
+ int idx = (ratio > thresholds[15]) << 4;
+ idx += (ratio > thresholds[idx + 7]) << 3;
+ idx += (ratio > thresholds[idx + 3]) << 2;
+ idx += (ratio > thresholds[idx + 1]) << 1;
+ idx += (ratio > thresholds[idx]);
+ return idx;
}
-#undef FIX_PREC_BITS
// Normalizes the hog data.
static inline void normalize_hog(float total, float *hist) {
- for (int i = 0; i < BINS; ++i) hist[i] /= total;
+ for (int i = 0; i < HOG_BINS; ++i) hist[i] /= total;
}
-static inline void lowbd_generate_hog(const uint8_t *src, int stride, int rows,
- int cols, float *hist) {
+static AOM_FORCE_INLINE void lowbd_generate_hog(const uint8_t *src, int stride,
+ int rows, int cols, float *hist,
+ hog_bin_lookup_fn get_bin_idx) {
float total = 0.1f;
- src += stride;
- for (int r = 1; r < rows - 1; ++r) {
- for (int c = 1; c < cols - 1; ++c) {
- const uint8_t *above = &src[c - stride];
- const uint8_t *below = &src[c + stride];
- const uint8_t *left = &src[c - 1];
- const uint8_t *right = &src[c + 1];
- // Calculate gradient using Sobel filters.
- const int dx = (right[-stride] + 2 * right[0] + right[stride]) -
- (left[-stride] + 2 * left[0] + left[stride]);
- const int dy = (below[-1] + 2 * below[0] + below[1]) -
- (above[-1] + 2 * above[0] + above[1]);
- if (dx == 0 && dy == 0) continue;
- const int temp = abs(dx) + abs(dy);
- if (!temp) continue;
- total += temp;
- if (dx == 0) {
- hist[0] += temp / 2;
- hist[BINS - 1] += temp / 2;
- } else {
- const int idx = get_hist_bin_idx(dx, dy);
- assert(idx >= 0 && idx < BINS);
- hist[idx] += temp;
+ if (cols >= 3) {
+ src += stride;
+ for (int r = 1; r < rows - 1; ++r) {
+ const uint8_t *row_above = src - stride;
+ const uint8_t *row_cur = src;
+ const uint8_t *row_below = src + stride;
+
+ // Prime the 3x3 sliding Sobel window with columns 0 and 1.
+ int v0 = row_above[0] + 2 * row_cur[0] + row_below[0];
+ int v1 = row_above[1] + 2 * row_cur[1] + row_below[1];
+ int d0 = row_below[0] - row_above[0];
+ int d1 = row_below[1] - row_above[1];
+
+ for (int c = 1; c < cols - 1; ++c) {
+ // Load the incoming right column (c + 1) and update Sobel gradients.
+ const int above_r = row_above[c + 1];
+ const int cur_r = row_cur[c + 1];
+ const int below_r = row_below[c + 1];
+ const int v2 = above_r + 2 * cur_r + below_r;
+ const int d2 = below_r - above_r;
+ const int dx = v2 - v0;
+ const int dy = d0 + 2 * d1 + d2;
+ v0 = v1;
+ v1 = v2;
+ d0 = d1;
+ d1 = d2;
+
+ if ((dx | dy) == 0) continue;
+ const int temp = abs(dx) + abs(dy);
+ total += temp;
+ if (dx == 0) {
+ hist[0] += temp / 2;
+ hist[HOG_BINS - 1] += temp / 2;
+ } else {
+ const int idx = get_bin_idx(dx, dy);
+ assert(idx >= 0 && idx < HOG_BINS);
+ hist[idx] += temp;
+ }
}
+ src += stride;
}
- src += stride;
}
normalize_hog(total, hist);
@@ -182,9 +188,9 @@ static inline void lowbd_generate_hog(const uint8_t *src, int stride, int rows,
// Computes and stores pixel level gradient information of a given superblock
// for LBD encode.
-static inline void lowbd_compute_gradient_info_sb(MACROBLOCK *const x,
- BLOCK_SIZE sb_size,
- PLANE_TYPE plane) {
+static AOM_FORCE_INLINE void lowbd_compute_gradient_info_sb(
+ MACROBLOCK *const x, BLOCK_SIZE sb_size, PLANE_TYPE plane,
+ hog_bin_lookup_fn get_bin_idx) {
PixelLevelGradientInfo *const grad_info_sb =
x->pixel_gradient_info + plane * MAX_SB_SQUARE;
const uint8_t *src = x->plane[plane].src.buf;
@@ -193,59 +199,94 @@ static inline void lowbd_compute_gradient_info_sb(MACROBLOCK *const x,
const int ss_y = x->e_mbd.plane[plane].subsampling_y;
const int sb_height = block_size_high[sb_size] >> ss_y;
const int sb_width = block_size_wide[sb_size] >> ss_x;
+ if (sb_width < 3) return;
src += stride;
for (int r = 1; r < sb_height - 1; ++r) {
+ const uint8_t *row_above = src - stride;
+ const uint8_t *row_cur = src;
+ const uint8_t *row_below = src + stride;
+
+ // Prime the 3x3 sliding Sobel window with columns 0 and 1.
+ int v0 = row_above[0] + 2 * row_cur[0] + row_below[0];
+ int v1 = row_above[1] + 2 * row_cur[1] + row_below[1];
+ int d0 = row_below[0] - row_above[0];
+ int d1 = row_below[1] - row_above[1];
+ PixelLevelGradientInfo *const row_info = grad_info_sb + r * sb_width;
+
for (int c = 1; c < sb_width - 1; ++c) {
- const uint8_t *above = &src[c - stride];
- const uint8_t *below = &src[c + stride];
- const uint8_t *left = &src[c - 1];
- const uint8_t *right = &src[c + 1];
- // Calculate gradient using Sobel filters.
- const int dx = (right[-stride] + 2 * right[0] + right[stride]) -
- (left[-stride] + 2 * left[0] + left[stride]);
- const int dy = (below[-1] + 2 * below[0] + below[1]) -
- (above[-1] + 2 * above[0] + above[1]);
- grad_info_sb[r * sb_width + c].is_dx_zero = (dx == 0);
- grad_info_sb[r * sb_width + c].abs_dx_abs_dy_sum =
- (uint16_t)(abs(dx) + abs(dy));
- grad_info_sb[r * sb_width + c].hist_bin_idx =
- (dx != 0) ? get_hist_bin_idx(dx, dy) : -1;
+ // Load the incoming right column (c + 1) and update Sobel gradients.
+ const int above_r = row_above[c + 1];
+ const int cur_r = row_cur[c + 1];
+ const int below_r = row_below[c + 1];
+ const int v2 = above_r + 2 * cur_r + below_r;
+ const int d2 = below_r - above_r;
+ const int dx = v2 - v0;
+ const int dy = d0 + 2 * d1 + d2;
+ v0 = v1;
+ v1 = v2;
+ d0 = d1;
+ d1 = d2;
+
+ row_info[c].abs_dx_abs_dy_sum = (uint16_t)(abs(dx) + abs(dy));
+ if (dx == 0) {
+ row_info[c].is_dx_zero = 1;
+ row_info[c].hist_bin_idx = -1;
+ } else {
+ row_info[c].is_dx_zero = 0;
+ row_info[c].hist_bin_idx = (int8_t)get_bin_idx(dx, dy);
+ }
}
src += stride;
}
}
#if CONFIG_AV1_HIGHBITDEPTH
-static inline void highbd_generate_hog(const uint8_t *src8, int stride,
- int rows, int cols, float *hist) {
+static AOM_FORCE_INLINE void highbd_generate_hog(
+ const uint8_t *src8, int stride, int rows, int cols, float *hist,
+ hog_bin_lookup_fn get_bin_idx) {
float total = 0.1f;
- const uint16_t *src = CONVERT_TO_SHORTPTR(src8);
- src += stride;
- for (int r = 1; r < rows - 1; ++r) {
- for (int c = 1; c < cols - 1; ++c) {
- const uint16_t *above = &src[c - stride];
- const uint16_t *below = &src[c + stride];
- const uint16_t *left = &src[c - 1];
- const uint16_t *right = &src[c + 1];
- // Calculate gradient using Sobel filters.
- const int dx = (right[-stride] + 2 * right[0] + right[stride]) -
- (left[-stride] + 2 * left[0] + left[stride]);
- const int dy = (below[-1] + 2 * below[0] + below[1]) -
- (above[-1] + 2 * above[0] + above[1]);
- if (dx == 0 && dy == 0) continue;
- const int temp = abs(dx) + abs(dy);
- if (!temp) continue;
- total += temp;
- if (dx == 0) {
- hist[0] += temp / 2;
- hist[BINS - 1] += temp / 2;
- } else {
- const int idx = get_hist_bin_idx(dx, dy);
- assert(idx >= 0 && idx < BINS);
- hist[idx] += temp;
+ if (cols >= 3) {
+ const uint16_t *src = CONVERT_TO_SHORTPTR(src8);
+ src += stride;
+ for (int r = 1; r < rows - 1; ++r) {
+ const uint16_t *row_above = src - stride;
+ const uint16_t *row_cur = src;
+ const uint16_t *row_below = src + stride;
+
+ // Prime the 3x3 sliding Sobel window with columns 0 and 1.
+ int v0 = row_above[0] + 2 * row_cur[0] + row_below[0];
+ int v1 = row_above[1] + 2 * row_cur[1] + row_below[1];
+ int d0 = row_below[0] - row_above[0];
+ int d1 = row_below[1] - row_above[1];
+
+ for (int c = 1; c < cols - 1; ++c) {
+ // Load the incoming right column (c + 1) and update Sobel gradients.
+ const int above_r = row_above[c + 1];
+ const int cur_r = row_cur[c + 1];
+ const int below_r = row_below[c + 1];
+ const int v2 = above_r + 2 * cur_r + below_r;
+ const int d2 = below_r - above_r;
+ const int dx = v2 - v0;
+ const int dy = d0 + 2 * d1 + d2;
+ v0 = v1;
+ v1 = v2;
+ d0 = d1;
+ d1 = d2;
+
+ if ((dx | dy) == 0) continue;
+ const int temp = abs(dx) + abs(dy);
+ total += temp;
+ if (dx == 0) {
+ hist[0] += temp / 2;
+ hist[HOG_BINS - 1] += temp / 2;
+ } else {
+ const int idx = get_bin_idx(dx, dy);
+ assert(idx >= 0 && idx < HOG_BINS);
+ hist[idx] += temp;
+ }
}
+ src += stride;
}
- src += stride;
}
normalize_hog(total, hist);
@@ -253,9 +294,9 @@ static inline void highbd_generate_hog(const uint8_t *src8, int stride,
// Computes and stores pixel level gradient information of a given superblock
// for HBD encode.
-static inline void highbd_compute_gradient_info_sb(MACROBLOCK *const x,
- BLOCK_SIZE sb_size,
- PLANE_TYPE plane) {
+static AOM_FORCE_INLINE void highbd_compute_gradient_info_sb(
+ MACROBLOCK *const x, BLOCK_SIZE sb_size, PLANE_TYPE plane,
+ hog_bin_lookup_fn get_bin_idx) {
PixelLevelGradientInfo *const grad_info_sb =
x->pixel_gradient_info + plane * MAX_SB_SQUARE;
const uint16_t *src = CONVERT_TO_SHORTPTR(x->plane[plane].src.buf);
@@ -264,23 +305,42 @@ static inline void highbd_compute_gradient_info_sb(MACROBLOCK *const x,
const int ss_y = x->e_mbd.plane[plane].subsampling_y;
const int sb_height = block_size_high[sb_size] >> ss_y;
const int sb_width = block_size_wide[sb_size] >> ss_x;
+ if (sb_width < 3) return;
src += stride;
for (int r = 1; r < sb_height - 1; ++r) {
+ const uint16_t *row_above = src - stride;
+ const uint16_t *row_cur = src;
+ const uint16_t *row_below = src + stride;
+
+ // Prime the 3x3 sliding Sobel window with columns 0 and 1.
+ int v0 = row_above[0] + 2 * row_cur[0] + row_below[0];
+ int v1 = row_above[1] + 2 * row_cur[1] + row_below[1];
+ int d0 = row_below[0] - row_above[0];
+ int d1 = row_below[1] - row_above[1];
+ PixelLevelGradientInfo *const row_info = grad_info_sb + r * sb_width;
+
for (int c = 1; c < sb_width - 1; ++c) {
- const uint16_t *above = &src[c - stride];
- const uint16_t *below = &src[c + stride];
- const uint16_t *left = &src[c - 1];
- const uint16_t *right = &src[c + 1];
- // Calculate gradient using Sobel filters.
- const int dx = (right[-stride] + 2 * right[0] + right[stride]) -
- (left[-stride] + 2 * left[0] + left[stride]);
- const int dy = (below[-1] + 2 * below[0] + below[1]) -
- (above[-1] + 2 * above[0] + above[1]);
- grad_info_sb[r * sb_width + c].is_dx_zero = (dx == 0);
- grad_info_sb[r * sb_width + c].abs_dx_abs_dy_sum =
- (uint16_t)(abs(dx) + abs(dy));
- grad_info_sb[r * sb_width + c].hist_bin_idx =
- (dx != 0) ? get_hist_bin_idx(dx, dy) : -1;
+ // Load the incoming right column (c + 1) and update Sobel gradients.
+ const int above_r = row_above[c + 1];
+ const int cur_r = row_cur[c + 1];
+ const int below_r = row_below[c + 1];
+ const int v2 = above_r + 2 * cur_r + below_r;
+ const int d2 = below_r - above_r;
+ const int dx = v2 - v0;
+ const int dy = d0 + 2 * d1 + d2;
+ v0 = v1;
+ v1 = v2;
+ d0 = d1;
+ d1 = d2;
+
+ row_info[c].abs_dx_abs_dy_sum = (uint16_t)(abs(dx) + abs(dy));
+ if (dx == 0) {
+ row_info[c].is_dx_zero = 1;
+ row_info[c].hist_bin_idx = -1;
+ } else {
+ row_info[c].is_dx_zero = 0;
+ row_info[c].hist_bin_idx = (int8_t)get_bin_idx(dx, dy);
+ }
}
src += stride;
}
@@ -291,13 +351,13 @@ static inline void generate_hog(const uint8_t *src8, int stride, int rows,
int cols, float *hist, int highbd) {
#if CONFIG_AV1_HIGHBITDEPTH
if (highbd) {
- highbd_generate_hog(src8, stride, rows, cols, hist);
+ av1_highbd_generate_hog(src8, stride, rows, cols, hist);
return;
}
#else
(void)highbd;
#endif // CONFIG_AV1_HIGHBITDEPTH
- lowbd_generate_hog(src8, stride, rows, cols, hist);
+ av1_lowbd_generate_hog(src8, stride, rows, cols, hist);
}
static inline void compute_gradient_info_sb(MACROBLOCK *const x,
@@ -305,11 +365,11 @@ static inline void compute_gradient_info_sb(MACROBLOCK *const x,
PLANE_TYPE plane) {
#if CONFIG_AV1_HIGHBITDEPTH
if (is_cur_buf_hbd(&x->e_mbd)) {
- highbd_compute_gradient_info_sb(x, sb_size, plane);
+ av1_highbd_compute_gradient_info_sb(x, sb_size, plane);
return;
}
#endif // CONFIG_AV1_HIGHBITDEPTH
- lowbd_compute_gradient_info_sb(x, sb_size, plane);
+ av1_lowbd_compute_gradient_info_sb(x, sb_size, plane);
}
// Gradient caching at superblock level is allowed only if all of the following
@@ -392,10 +452,10 @@ static inline void generate_hog_using_gradient_cache(const MACROBLOCK *x,
const bool is_dx_zero = grad_info_blk[r * sb_width + c].is_dx_zero;
if (is_dx_zero) {
hist[0] += abs_dx_abs_dy_sum >> 1;
- hist[BINS - 1] += abs_dx_abs_dy_sum >> 1;
+ hist[HOG_BINS - 1] += abs_dx_abs_dy_sum >> 1;
} else {
const int8_t idx = grad_info_blk[r * sb_width + c].hist_bin_idx;
- assert(idx >= 0 && idx < BINS);
+ assert(idx >= 0 && idx < HOG_BINS);
hist[idx] += abs_dx_abs_dy_sum;
}
}
@@ -429,7 +489,7 @@ static inline void collect_hog_data(const MACROBLOCK *x, BLOCK_SIZE bsize,
}
// Scale the hog so the luma and chroma are on the same scale
- for (int b = 0; b < BINS; ++b) {
+ for (int b = 0; b < HOG_BINS; ++b) {
hog[b] *= (1 + ss_x) * (1 + ss_y);
}
}
@@ -438,7 +498,7 @@ static inline void prune_intra_mode_with_hog(
const MACROBLOCK *x, BLOCK_SIZE bsize, BLOCK_SIZE sb_size, float th,
uint8_t *directional_mode_skip_mask, int is_chroma) {
const int plane = is_chroma ? AOM_PLANE_U : AOM_PLANE_Y;
- float hist[BINS] = { 0.0f };
+ float hist[HOG_BINS] = { 0.0f };
collect_hog_data(x, bsize, sb_size, plane, hist);
// Make prediction for each of the mode
@@ -451,7 +511,6 @@ static inline void prune_intra_mode_with_hog(
}
}
}
-#undef BINS
int av1_calc_normalized_variance(aom_variance_fn_t vf, const uint8_t *const buf,
const int stride, const int is_hbd);
diff --git a/av1/encoder/x86/intra_mode_search_sse2.c b/av1/encoder/x86/intra_mode_search_sse2.c
new file mode 100644
index 0000000000..3bde785a61
--- /dev/null
+++ b/av1/encoder/x86/intra_mode_search_sse2.c
@@ -0,0 +1,69 @@
+/*
+ * 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 <emmintrin.h>
+
+#include "config/aom_config.h"
+#include "config/av1_rtcd.h"
+
+#include "aom_ports/bitops.h"
+#include "av1/encoder/intra_mode_search_utils.h"
+
+static inline int get_hist_bin_idx_sse2(int dx, int dy) {
+ if (dy == 0) return 16;
+ const int32_t ratio = (dy * (1 << HOG_FIX_PREC_BITS)) / dx;
+ const int32_t *const thresholds = av1_get_hog_thresholds();
+
+ // Select the lower (0..15) or upper (16..31) half of thresholds, then
+ // compare all 16 bins in parallel using SSE2.
+ const int lo_idx = (ratio > thresholds[15]) << 4;
+ const __m128i ratio_vec = _mm_set1_epi32(ratio);
+ const __m128i c0 = _mm_cmpgt_epi32(
+ ratio_vec, _mm_load_si128((const __m128i *)&thresholds[lo_idx]));
+ const __m128i c1 = _mm_cmpgt_epi32(
+ ratio_vec, _mm_load_si128((const __m128i *)&thresholds[lo_idx + 4]));
+ const __m128i c2 = _mm_cmpgt_epi32(
+ ratio_vec, _mm_load_si128((const __m128i *)&thresholds[lo_idx + 8]));
+ const __m128i c3 = _mm_cmpgt_epi32(
+ ratio_vec, _mm_load_si128((const __m128i *)&thresholds[lo_idx + 12]));
+
+ // Pack 32-bit comparison masks down to 8-bit lanes and find the first bin
+ // where ratio <= thresholds[idx].
+ const __m128i p01 = _mm_packs_epi32(c0, c1);
+ const __m128i p23 = _mm_packs_epi32(c2, c3);
+ const __m128i p0123 = _mm_packs_epi16(p01, p23);
+ const uint32_t inv_mask = ~(uint32_t)_mm_movemask_epi8(p0123);
+ return lo_idx + get_msb(inv_mask ^ (inv_mask - 1));
+}
+
+void av1_lowbd_generate_hog_sse2(const uint8_t *src, int stride, int rows,
+ int cols, float *hist) {
+ lowbd_generate_hog(src, stride, rows, cols, hist, get_hist_bin_idx_sse2);
+}
+
+void av1_lowbd_compute_gradient_info_sb_sse2(MACROBLOCK *const x,
+ BLOCK_SIZE sb_size,
+ PLANE_TYPE plane) {
+ lowbd_compute_gradient_info_sb(x, sb_size, plane, get_hist_bin_idx_sse2);
+}
+
+#if CONFIG_AV1_HIGHBITDEPTH
+void av1_highbd_generate_hog_sse2(const uint8_t *src8, int stride, int rows,
+ int cols, float *hist) {
+ highbd_generate_hog(src8, stride, rows, cols, hist, get_hist_bin_idx_sse2);
+}
+
+void av1_highbd_compute_gradient_info_sb_sse2(MACROBLOCK *const x,
+ BLOCK_SIZE sb_size,
+ PLANE_TYPE plane) {
+ highbd_compute_gradient_info_sb(x, sb_size, plane, get_hist_bin_idx_sse2);
+}
+#endif // CONFIG_AV1_HIGHBITDEPTH
diff --git a/test/intra_hog_test.cc b/test/intra_hog_test.cc
new file mode 100644
index 0000000000..404e1b9768
--- /dev/null
+++ b/test/intra_hog_test.cc
@@ -0,0 +1,477 @@
+/*
+ * 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
diff --git a/test/test.cmake b/test/test.cmake
index 16622260ae..3ba70283b4 100644
--- a/test/test.cmake
+++ b/test/test.cmake
@@ -231,6 +231,7 @@ if(NOT BUILD_SHARED_LIBS)
"${AOM_ROOT}/test/fwht4x4_test.cc"
"${AOM_ROOT}/test/hadamard_test.cc"
"${AOM_ROOT}/test/horver_correlation_test.cc"
+ "${AOM_ROOT}/test/intra_hog_test.cc"
"${AOM_ROOT}/test/masked_sad_test.cc"
"${AOM_ROOT}/test/masked_variance_test.cc"
"${AOM_ROOT}/test/metadata_test.cc"