#!/bin/sh
## Copyright (c) 2023, 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.
##
##  This script checks the bit exactness between C and SIMD
##  implementations of AV1 encoder.
##
. $(dirname $0)/tools_common.sh

PRESETS="good rt"
LOWBD_CIF_CLIP="yuv_raw_input"
LOWBD_480p_CLIP="yuv_480p_raw_input"
LOWBD_720p_CLIP="y4m_720p_input"
HIGHBD_CLIP="y4m_360p_10bit_input"
SC_CLIP="y4m_screen_input"
OUT_FILE_SUFFIX=".ivf"
SCRIPT_DIR=$(dirname "$0")
LIBAOM_SOURCE_DIR=$(cd ${SCRIPT_DIR}/..; pwd)

# Clips used in test.
YUV_RAW_INPUT="${LIBAOM_TEST_DATA_PATH}/hantro_collage_w352h288.yuv"
YUV_480P_RAW_INPUT="${LIBAOM_TEST_DATA_PATH}/niklas_640_480_30.yuv"
Y4M_360P_10BIT_INPUT="${LIBAOM_TEST_DATA_PATH}/crowd_run_360p_10_150f.y4m"
Y4M_720P_INPUT="${LIBAOM_TEST_DATA_PATH}/niklas_1280_720_30.y4m"
Y4M_SCREEN_INPUT="${LIBAOM_TEST_DATA_PATH}/wikipedia_420_360p_60f.y4m"

# Number of frames to test.
AV1_ENCODE_C_VS_SIMD_TEST_FRAME_LIMIT=35

# Create a temporary directory for output files.
if [ -n "${TMPDIR}" ]; then
  AOM_TEST_TEMP_ROOT="${TMPDIR}"
elif [ -n "${TEMPDIR}" ]; then
  AOM_TEST_TEMP_ROOT="${TEMPDIR}"
else
  AOM_TEST_TEMP_ROOT=/tmp
fi

AOM_TEST_OUTPUT_DIR="${AOM_TEST_TEMP_ROOT}/av1_test_$$"

if ! mkdir -p "${AOM_TEST_OUTPUT_DIR}" || \
   [ ! -d "${AOM_TEST_OUTPUT_DIR}" ]; then
  echo "${0##*/}: Cannot create output directory, giving up."
  echo "${0##*/}:   AOM_TEST_OUTPUT_DIR=${AOM_TEST_OUTPUT_DIR}"
  exit 1
fi

elog() {
  echo "$@" 1>&2
}

# Echoes path to $1 when it's executable and exists in ${AOM_TEST_OUTPUT_DIR},
# or an empty string. Caller is responsible for testing the string once the
# function returns.
av1_enc_tool_path() {
  local target="$1"
  local preset="$2"
  local tool_path="${AOM_TEST_OUTPUT_DIR}/build_target_${target}/aomenc_${preset}"

  if [ ! -x "${tool_path}" ]; then
    tool_path=""
  fi
  echo "${tool_path}"
}

# Environment check: Make sure input and source directories are available.
av1_c_vs_simd_enc_verify_environment () {
  if [ ! -e "${YUV_RAW_INPUT}" ]; then
    elog "libaom test data must exist in LIBAOM_TEST_DATA_PATH."
    return 1
  fi
  if [ ! -e "${Y4M_360P_10BIT_INPUT}" ]; then
    elog "libaom test data must exist in LIBAOM_TEST_DATA_PATH."
    return 1
  fi
  if [ ! -e "${YUV_480P_RAW_INPUT}" ]; then
    elog "libaom test data must exist in LIBAOM_TEST_DATA_PATH."
    return 1
  fi
  if [ ! -e "${Y4M_720P_INPUT}" ]; then
    elog "libaom test data must exist in LIBAOM_TEST_DATA_PATH."
    return 1
  fi
  if [ ! -e "${Y4M_SCREEN_INPUT}" ]; then
    elog "libaom test data must exist in LIBAOM_TEST_DATA_PATH."
    return 1
  fi
  if [ ! -d "$LIBAOM_SOURCE_DIR" ]; then
    elog "LIBAOM_SOURCE_DIR does not exist."
    return 1
  fi
}

# This is not needed since tools_common.sh does the same cleanup.
# Keep the code here for our reference.
# cleanup() {
#  rm -rf  ${AOM_TEST_OUTPUT_DIR}
# }

# Echo AOM_SIMD_CAPS_MASK for different instruction set architecture.
avx512() {
   echo "0x3FF"
}

avx2() {
   echo "0x1FF"
}

avx() {
   echo "0x17F"
}

sse4_2() {
   echo "0x13F"
}

sse4_1() {
   echo "0x03F"
}

ssse3() {
   echo "0x01F"
}

sse3() {
   echo "0x00F"
}

sse2() {
   echo "0x007"
}

get_bitrates() {
  local content=$1
  local preset=$2

  # Bit-rates:
  local bitrate_lowres_good="300"
  local bitrate_480p_good="500"
  local bitrate_720p_good="1000"
  local bitrate_scc_360p_good="500"
  local bitrate_lowres_rt="200"
  local bitrate_480p_rt="300"
  local bitrate_720p_rt="600"
  local bitrate_scc_360p_rt="300"
  local bitrate_hbd_360p="500"

  if [ "${preset}" = "good" ]; then
    if [ "${content}" = "yuv_raw_input" ]; then
      echo "${bitrate_lowres_good}"
    elif [ "${content}" = "yuv_480p_raw_input" ]; then
      echo "${bitrate_480p_good}"
    elif [ "${content}" = "y4m_720p_input" ]; then
      echo "${bitrate_720p_good}"
    elif [ "${content}" = "y4m_screen_input" ]; then
      echo "${bitrate_scc_360p_good}"
    elif [ "${content}" = "y4m_360p_10bit_input" ]; then
      echo "${bitrate_hbd_360p}"
    else
      elog "Invalid content"
    fi
  elif  [ "${preset}" = "rt" ]; then
    if [ "${content}" = "yuv_raw_input" ]; then
      echo "${bitrate_lowres_rt}"
    elif [ "${content}" = "yuv_480p_raw_input" ]; then
      echo "${bitrate_480p_rt}"
    elif [ "${content}" = "y4m_720p_input" ]; then
      echo "${bitrate_720p_rt}"
    elif [ "${content}" = "y4m_screen_input" ]; then
      echo "${bitrate_scc_360p_rt}"
    elif [ "${content}" = "y4m_360p_10bit_input" ]; then
      echo "${bitrate_hbd_360p}"
    else
      elog "Invalid content"
    fi
  else
    elog "invalid preset"
  fi
}

# Echo clip details to be used as input to aomenc.
yuv_raw_input() {
  echo ""${YUV_RAW_INPUT}"
       --width=352
       --height=288
       --bit-depth=8"
}

y4m_360p_10bit_input() {
  echo ""${Y4M_360P_10BIT_INPUT}"
       --bit-depth=10"
}

yuv_480p_raw_input() {
  echo ""${YUV_480P_RAW_INPUT}"
       --width=640
       --height=480
       --bit-depth=8"
}

y4m_720p_input() {
  echo ""${Y4M_720P_INPUT}"
       --bit-depth=8"
}

y4m_screen_input() {
  echo ""${Y4M_SCREEN_INPUT}"
       --tune-content=screen
       --enable-palette=1
       --bit-depth=8"
}

has_x86_isa_extn() {
  instruction_set=$1
  if ! grep -q "$instruction_set" /proc/cpuinfo; then
    # This instruction set is not supported.
    return 1
  fi
}

# Echo good encode params for use with AV1 encoder.
av1_encode_good_params() {
  echo "--good \
  --ivf \
  --profile=0 \
  --static-thresh=0 \
  --threads=1 \
  --tile-columns=0 \
  --tile-rows=0 \
  --verbose \
  --end-usage=vbr \
  --kf-max-dist=160 \
  --kf-min-dist=0 \
  --max-q=63 \
  --min-q=0 \
  --overshoot-pct=100 \
  --undershoot-pct=100 \
  --passes=2 \
  --arnr-maxframes=7 \
  --arnr-strength=5 \
  --auto-alt-ref=1 \
  --drop-frame=0 \
  --frame-parallel=0 \
  --lag-in-frames=35 \
  --maxsection-pct=2000 \
  --minsection-pct=0 \
  --sharpness=0"
}

# Echo realtime encode params for use with AV1 encoder.
av1_encode_rt_params() {
  echo "--rt \
  --ivf \
  --profile=0 \
  --static-thresh=0 \
  --threads=1 \
  --tile-columns=0 \
  --tile-rows=0 \
  --verbose \
  --end-usage=cbr \
  --kf-max-dist=90000 \
  --max-q=58 \
  --min-q=2 \
  --overshoot-pct=50 \
  --undershoot-pct=50 \
  --passes=1 \
  --aq-mode=3 \
  --buf-initial-sz=500 \
  --buf-optimal-sz=600 \
  --buf-sz=1000 \
  --coeff-cost-upd-freq=3 \
  --dv-cost-upd-freq=3 \
  --mode-cost-upd-freq=3 \
  --mv-cost-upd-freq=3 \
  --deltaq-mode=0 \
  --enable-global-motion=0 \
  --enable-obmc=0 \
  --enable-order-hint=0 \
  --enable-ref-frame-mvs=0 \
  --enable-tpl-model=0 \
  --enable-warped-motion=0 \
  --lag-in-frames=0 \
  --max-intra-rate=300 \
  --noise-sensitivity=0"
}

# Configures for the given target in AOM_TEST_OUTPUT_DIR/build_target_${target}
# directory.
av1_enc_build() {
  local target="$1"
  local cmake_command="$2"
  local tmp_build_dir=${AOM_TEST_OUTPUT_DIR}/build_target_${target}
  if [ -d "$tmp_build_dir" ]; then
    rm -rf $tmp_build_dir
  fi

  mkdir -p $tmp_build_dir
  cd $tmp_build_dir

  local cmake_common_args="--fresh -DCONFIG_EXCLUDE_SIMD_MISMATCH=1 \
           -DCONFIG_HIGHWAY=1 \
           -DCMAKE_BUILD_TYPE=Release \
           -DENABLE_CCACHE=1 \
           '-DCMAKE_C_FLAGS_RELEASE=-O3 -g' \
           '-DCMAKE_CXX_FLAGS_RELEASE=-O3 -g' \
           -DENABLE_DOCS=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0"

  for preset in $PRESETS; do
    echo "Building target[${preset} encoding]: ${target}"
    if [ "${preset}" = "good" ]; then
      local cmake_extra_args="-DCONFIG_AV1_HIGHBITDEPTH=1"
    elif [ "${preset}" = "rt" ]; then
      local cmake_extra_args="-DCONFIG_REALTIME_ONLY=1 -DCONFIG_AV1_HIGHBITDEPTH=0"
    else
      elog "Invalid preset"
      return 1
    fi
    if ! eval "$cmake_command" "${cmake_common_args}" "${cmake_extra_args}" \
      ${devnull}; then
      elog "cmake failure"
      return 1
    fi
    if ! eval make -j$(nproc) aomenc ${devnull}; then
      elog "build failure"
      return 1
    fi

    mv aomenc aomenc_${preset}
  done
  echo "Done building target: ${target}"
}

compare_enc_output() {
  local target=$1
  local cpu=$2
  local clip=$3
  local bitrate=$4
  local preset=$5
  if ! diff -q ${AOM_TEST_OUTPUT_DIR}/Out-generic-"${clip}"-${preset}-${bitrate}kbps-cpu${cpu}${OUT_FILE_SUFFIX} \
       ${AOM_TEST_OUTPUT_DIR}/Out-${target}-"${clip}"-${preset}-${bitrate}kbps-cpu${cpu}${OUT_FILE_SUFFIX}; then
    elog "C vs ${target} encode mismatches for ${clip}, at ${bitrate} kbps, speed ${cpu}, ${preset} preset"
    return 1
  fi
}

av1_enc_test() {
  local encoder="$1"
  local arch="$2"
  local target="$3"
  local preset="$4"
  if [ -z "$(av1_enc_tool_path "${target}"  "${preset}")" ]; then
    elog "aomenc_${preset} not found. It must exist in ${AOM_TEST_OUTPUT_DIR}/build_target_${target} path"
    return 1
  fi

  if [ "${preset}" = "good" ]; then
    case "${arch}" in
      arm64)
        # Speed 0 is not tested as arm64 is run under emulation.
        local min_cpu_used=1
        local max_cpu_used=6
        ;;
      x86)
        # x86 has a good amount of overlap with x86-64. Only a few values are
        # tested to improve the runtime of the script.
        local min_cpu_used=2
        local max_cpu_used=3
        ;;
      *)
        local min_cpu_used=0
        local max_cpu_used=6
        ;;
    esac
    local test_params=av1_encode_good_params
  elif [ "${preset}" = "rt" ]; then
    local min_cpu_used=5
    local max_cpu_used=11
    local test_params=av1_encode_rt_params
  else
    elog "Invalid preset"
    return 1
  fi

  for cpu in $(seq $min_cpu_used $max_cpu_used); do
    if [ "${preset}" = "good" ]; then
      if [ "${arch}" = "x86_64" -o "${arch}" = "arm64" ]; then
        if [ "${cpu}" -lt 2 ]; then
          local test_clips="${LOWBD_CIF_CLIP} ${HIGHBD_CLIP}"
        elif [ "${cpu}" -lt 5 ]; then
          local test_clips="${LOWBD_480p_CLIP} ${HIGHBD_CLIP}"
        else
          local test_clips="${LOWBD_720p_CLIP} ${HIGHBD_CLIP}"
        fi
      elif [ "${arch}" = "x86" ]; then
        local test_clips="${LOWBD_CIF_CLIP} ${HIGHBD_CLIP}"
      else
        elog "Unknown architecture: ${arch}"
        return 1
      fi
    elif [ "${preset}" = "rt" ]; then
      if [ "${cpu}" -lt 8 ]; then
        local test_clips="${LOWBD_CIF_CLIP} ${SC_CLIP}"
      else
        local test_clips="${LOWBD_480p_CLIP} ${SC_CLIP}"
      fi
    else
      elog "Invalid preset"
      return 1
    fi

    for clip in ${test_clips}; do
      local test_bitrates=$(get_bitrates ${clip} ${preset})
      for bitrate in ${test_bitrates}; do
        eval "${encoder}" $($clip) $($test_params) \
        "--limit=${AV1_ENCODE_C_VS_SIMD_TEST_FRAME_LIMIT}" \
        "--cpu-used=${cpu}" "--target-bitrate=${bitrate}" "-o" \
        ${AOM_TEST_OUTPUT_DIR}/Out-${target}-"${clip}"-${preset}-${bitrate}kbps-cpu${cpu}${OUT_FILE_SUFFIX} \
        ${devnull}

        if [ "${target}" != "generic" ]; then
          if ! compare_enc_output ${target} $cpu ${clip} $bitrate ${preset}; then
            # Found a mismatch
            return 1
          fi
        fi
      done
    done
  done
}

av1_test_generic() {
  local arch=$1
  local target="generic"
  if [ $arch = "x86_64" ]; then
    local cmake_command="cmake $LIBAOM_SOURCE_DIR -DAOM_TARGET_CPU=${target}"
  elif [ $arch = "x86" ]; then
    # As AV1 encode output differs for x86 32-bit and 64-bit platforms
    # (BUG=aomedia:3479), the x86 32-bit C-only build is generated separately.
    # The cmake command line option -DENABLE_MMX=0 flag disables all SIMD
    # optimizations, and generates a C-only binary.
    local cmake_command="cmake $LIBAOM_SOURCE_DIR -DENABLE_MMX=0 \
      -DCMAKE_TOOLCHAIN_FILE=${LIBAOM_SOURCE_DIR}/cmake/toolchains/i686-linux-gcc.cmake"
  fi

  echo "Build for: Generic ${arch}"
  if ! av1_enc_build "${target}" "${cmake_command}"; then
    return 1
  fi

  for preset in $PRESETS; do
    local encoder="$(av1_enc_tool_path "${target}" "${preset}")"
    av1_enc_test $encoder "${arch}" "${target}" "${preset}"
  done
}

# This function encodes AV1 bitstream by enabling SSE2, SSE3, SSSE3, SSE4_1, SSE4_2, AVX, AVX2,
# AVX512 as there are no functions with MMX and SSE specialization.
# The value of environment variable 'AOM_SIMD_CAPS_MASK' controls enabling of different instruction
# set extension optimizations. The value of the flag 'AOM_SIMD_CAPS_MASK' and the corresponding
# instruction set extension optimization enabled are as follows:
# AVX512 SSE4_2 AVX2 AVX SSE4_1 SSSE3 SSE3 SSE2 SSE MMX
#   1      1     1    1    1      1    1    1    1   1  -> 0x3FF -> Enable AVX512 and lower variants
#   0      1     1    1    1      1    1    1    1   1  -> 0x1FF -> Enable AVX2 and lower variants
#   0      1     0    1    1      1    1    1    1   1  -> 0x17F -> Enable AVX and lower variants
#   0      1     0    0    1      1    1    1    1   1  -> 0x13F -> Enable SSE4_2 and lower variants
#   0      0     0    0    1      1    1    1    1   1  -> 0x03F -> Enable SSE4_1 and lower variants
#   0      0     0    0    0      1    1    1    1   1  -> 0x01F -> Enable SSSE3 and lower variants
#   0      0     0    0    0      0    1    1    1   1  -> 0x00F -> Enable SSE3 and lower variants
#   0      0     0    0    0      0    0    1    1   1  -> 0x007 -> Enable SSE2 and lower variants
#   0      0     0    0    0      0    0    0    1   1  -> 0x003 -> Enable SSE and lower variants
#   0      0     0    0    0      0    0    0    0   1  -> 0x001 -> Enable MMX
## NOTE: In x86_64 platform, it is not possible to enable sse/mmx/c using "AOM_SIMD_CAPS_MASK" as
#  all x86_64 platforms implement sse2.
av1_test_x86() {
  local arch=$1

  if ! uname -m | grep -q "x86"; then
    elog "Machine architecture is not x86 or x86_64"
    return 0
  fi

  if [ $arch = "x86" ]; then
    local target="x86-linux"
    local cmake_command="cmake \
    $LIBAOM_SOURCE_DIR \
    -DCMAKE_TOOLCHAIN_FILE=${LIBAOM_SOURCE_DIR}/cmake/toolchains/i686-linux-gcc.cmake"
  elif [ $arch = "x86_64" ]; then
    local target="x86_64-linux"
    local cmake_command="cmake $LIBAOM_SOURCE_DIR"
  fi

  # Available x86 isa variants: "avx512 avx2 avx sse4_2 sse4_1 ssse3 sse3 sse2"
  local x86_isa_variants="avx512 avx2 sse4_2 sse2"

  echo "Build for x86: ${target}"
  if ! av1_enc_build "${target}" "${cmake_command}"; then
    return 1
  fi

  for preset in $PRESETS; do
    local encoder="$(av1_enc_tool_path "${target}" "${preset}")"
    for isa in $x86_isa_variants; do
      # Note that if has_x86_isa_extn returns 1, it is false, and vice versa.
      if ! has_x86_isa_extn $isa; then
        echo "${isa} is not supported in this machine"
        continue
      fi
      export AOM_SIMD_CAPS_MASK=$($isa)
      if ! av1_enc_test $encoder "${arch}" "${target}" "${preset}"; then
        # Found a mismatch
        return 1
      fi
      unset AOM_SIMD_CAPS_MASK
    done
  done
}

av1_test_arm() {
  local arch="arm64"
  local target="arm64-linux-gcc"
  local cmake_command="cmake $LIBAOM_SOURCE_DIR \
        -DCMAKE_TOOLCHAIN_FILE=$LIBAOM_SOURCE_DIR/cmake/toolchains/${target}.cmake \
        -DCMAKE_C_FLAGS=-Wno-maybe-uninitialized"
  echo "Build for arm64: ${target}"
  if ! av1_enc_build "${target}" "${cmake_command}"; then
    return 1
  fi

  for preset in $PRESETS; do
    local encoder="$(av1_enc_tool_path "${target}" "${preset}")"
    if ! av1_enc_test "qemu-aarch64 -L /usr/aarch64-linux-gnu ${encoder}" "${arch}" "${target}" "${preset}"; then
      # Found a mismatch
      return 1
    fi
  done
}

av1_c_vs_simd_enc_test () {
  # Test x86 (32 bit)
  # x86 requires the i686-linux-gnu toolchain:
  # $ sudo apt-get install g++-i686-linux-gnu
  echo "av1 test for x86 (32 bit): Started."
  # Encode 'C' only
  av1_test_generic "x86"
  # Encode with SIMD optimizations enabled
  if ! av1_test_x86 "x86"; then
    echo "av1 test for x86 (32 bit): Done, test failed."
    return 1
  else
    echo "av1 test for x86 (32 bit): Done, all tests passed."
  fi

  # Test x86_64 (64 bit)
  if [ "$(eval uname -m)" = "x86_64" ]; then
    echo "av1 test for x86_64 (64 bit): Started."
    # Encode 'C' only
    av1_test_generic "x86_64"
    # Encode with SIMD optimizations enabled
    if ! av1_test_x86 "x86_64"; then
      echo "av1 test for x86_64 (64 bit): Done, test failed."
      return 1
    else
      echo "av1 test for x86_64 (64 bit): Done, all tests passed."
    fi
  fi

  # Test ARM
  echo "av1_test_arm: Started."
  if ! av1_test_arm; then
    echo "av1 test for arm: Done, test failed."
    return 1
  else
    echo "av1 test for arm: Done, all tests passed."
  fi
}

run_tests av1_c_vs_simd_enc_verify_environment av1_c_vs_simd_enc_test
