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"