chromium/media/gpu/v4l2/mt21/mt21_decompressor_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "build/build_config.h"

#if BUILDFLAG(IS_CHROMEOS) && defined(ARCH_CPU_ARM_FAMILY) && \
    (defined(COMPILER_GCC) || defined(__clang__))

#include "media/gpu/v4l2/mt21/mt21_decompressor.h"

#include <stdlib.h>
#include <unistd.h>
#include <algorithm>

#include "base/bits.h"
#include "base/command_line.h"
#include "base/test/launcher/unit_test_launcher.h"
#include "base/test/test_suite.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/perf/perf_result_reporter.h"
#include "third_party/libyuv/include/libyuv/planar_functions.h"

namespace media {
namespace {

constexpr size_t kBitsInByte = 8;
constexpr size_t kMT21SubblockWidth = 16;
constexpr size_t kMT21SubblockHeight = 4;
constexpr size_t kMT21SubblockSize = kMT21SubblockWidth * kMT21SubblockHeight;
constexpr size_t kMT21BlockSize = 2 * kMT21SubblockSize;
constexpr size_t kMT21TileHeight = 32;
constexpr size_t kMT21YFooterAlignment = 4096;
constexpr size_t kMT21UVFooterAlignment = kMT21YFooterAlignment / 2;

// Utility class to write a compressed MT21 block. We use this for generating
// synthetic compressed frames.
class MT21BlockWriter {
 public:
  MT21BlockWriter(uint8_t* block);

  bool WriteBit(bool bit);
  bool WriteNBits(int val, int n);
  void PadToRow();
  size_t GetNumRows();
  void SetPos(size_t bit_idx);
  size_t GetPos();

 private:
  uint8_t* block_;
  size_t bit_idx_;
};

MT21BlockWriter::MT21BlockWriter(uint8_t* block) {
  block_ = block;
  bit_idx_ = 0;
}

bool MT21BlockWriter::WriteBit(bool bit) {
  size_t byte_idx = bit_idx_ / kBitsInByte;
  if (byte_idx >= kMT21BlockSize) {
    return false;
  }

  size_t row = byte_idx / kMT21SubblockWidth;
  size_t col = (kMT21SubblockWidth - 1) - (byte_idx % kMT21SubblockWidth);
  byte_idx = row * kMT21SubblockWidth + col;
  block_[byte_idx] |= (int)bit
                      << ((kBitsInByte - 1) - (bit_idx_ % kBitsInByte));
  bit_idx_++;

  return true;
}

bool MT21BlockWriter::WriteNBits(int val, int n) {
  for (int i = n - 1; i >= 0; i--) {
    if (!WriteBit((val >> i) & 0x1)) {
      return false;
    }
  }

  return true;
}

void MT21BlockWriter::PadToRow() {
  bit_idx_ = base::bits::AlignUp(bit_idx_, kMT21SubblockWidth * kBitsInByte);
}

size_t MT21BlockWriter::GetNumRows() {
  return base::bits::AlignUp(bit_idx_, kMT21SubblockWidth * kBitsInByte) /
         (kMT21SubblockWidth * kBitsInByte);
}

void MT21BlockWriter::SetPos(size_t bit_idx) {
  bit_idx_ = bit_idx;
}

size_t MT21BlockWriter::GetPos() {
  return bit_idx_;
}

// Get a random number according to a double sided geometric distribution. This
// means that outputs further away from zero will be exponentially less likely.
//
// The algorithm we use for generating numbers according to this distribution is
// to first generate a uniform random number, and then count the number of
// leading zeros. In a uniform distribution, every bit has an equal probability
// of being either 0 or 1. This means that the probability of the first N bits
// being 0 is 1/(2^N).
int GeometricRandomNum() {
  uint32_t uniform_random_num = rand();
  int ret = __builtin_clz(uniform_random_num);
  // The above algorithm only generates positive numbers according to a
  // geometric distribution, but we really want both positive and negative.
  if (rand() & 1) {
    return -1 * ret;
  } else {
    return ret;
  }
}

// Encode a value using MT21's Golomb-Rice variant.
void GolombRiceEncode(MT21BlockWriter& writer, int symbol, int k) {
  if (!symbol) {
    writer.WriteNBits(0, k);
    return;
  }

  int base = 1 << k;
  if (symbol < 0) {
    symbol = symbol * -2 + 1;
  } else {
    symbol *= 2;
  }

  int escape_sequence = 7 + k;
  if (symbol / base >= escape_sequence) {
    writer.WriteNBits((1 << escape_sequence) - 1, escape_sequence);
    writer.WriteNBits(symbol - (escape_sequence * base), k >= 4 ? 7 : 8);
    return;
  }

  while (symbol >= base) {
    writer.WriteBit(1);
    symbol -= base;
  }
  writer.WriteBit(0);
  writer.WriteNBits(symbol, k);
}

// Unoptimized version of our pixel prediction algorithm. This is essentially a
// copy of the version here:
// https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/platform/drm-tests/pixel_formats/mt21_converter.c;drc=091692f34d333dec8fd3a8e375a4ad5a65682cb2;l=173
uint8_t PredictPixelValue(const uint8_t* subblock, int x, int y, int width) {
  if (y == 0) {
    return subblock[x + 1];
  } else if (x == width - 1) {
    return subblock[(y - 1) * width + x];
  } else if (x == 0) {
    int up_right = subblock[(y - 1) * width + x + 1];
    int up = subblock[(y - 1) * width + x];
    int right = subblock[y * width + x + 1];
    int max_up_right = std::max(up, right);
    int min_up_right = std::min(up, right);

    if (up_right <= max_up_right && up_right >= min_up_right) {
      return right + up - up_right;
    } else if (up_right > max_up_right) {
      return max_up_right;
    } else {
      return min_up_right;
    }
  } else {
    int up_left = subblock[(y - 1) * width + x - 1];
    int up_right = subblock[(y - 1) * width + x + 1];
    int up = subblock[(y - 1) * width + x];
    int right = subblock[y * width + x + 1];
    int max_up_right = std::max(up, right);
    int min_up_right = std::min(up, right);

    if (up_right <= max_up_right && up_right >= min_up_right) {
      return right + up - up_right;
    } else if (up_left <= max_up_right && up_left >= min_up_right) {
      return up - up_left + right;
    } else if (up_left >= max_up_right) {
      return max_up_right;
    } else {
      return min_up_right;
    }
  }
}

void GenerateRandomSubblock(MT21BlockWriter& writer,
                            uint8_t* golden_subblock,
                            int width) {
  constexpr int k = 1;
  uint8_t top_right = (rand() & 0x7F) + 0x7F;

  golden_subblock[width - 1] = top_right;

  writer.WriteNBits(k - 1, 3);
  writer.WriteNBits(top_right, 8);

  for (size_t y = 0; y < kMT21SubblockHeight; y++) {
    for (int x = (y ? (width - 1) : (width - 2)); x >= 0; x--) {
      int random_delta = GeometricRandomNum();

      GolombRiceEncode(writer, random_delta, k);

      golden_subblock[y * width + x] =
          PredictPixelValue(golden_subblock, x, y, width) + random_delta;
    }
  }
}

void GenerateRandomYBlock(uint8_t* mt21_block,
                          uint8_t* mm21_block,
                          size_t& subblock_len1,
                          size_t& subblock_len2) {
  MT21BlockWriter writer(mt21_block);

  // There's a chance we generate a subblock that is too entropic to compress,
  // so we just re-do until we get a good subblock.
  do {
    writer.SetPos(0);
    GenerateRandomSubblock(writer, mm21_block, kMT21SubblockWidth);
  } while (writer.GetNumRows() >= kMT21SubblockHeight);

  subblock_len1 = writer.GetNumRows();
  writer.PadToRow();

  int bit_idx = writer.GetPos();
  do {
    writer.SetPos(bit_idx);
    GenerateRandomSubblock(writer, mm21_block + kMT21SubblockSize,
                           kMT21SubblockWidth);
  } while (writer.GetNumRows() - subblock_len1 >= kMT21SubblockHeight);
  subblock_len2 = writer.GetNumRows() - subblock_len1;
}

void InterleaveUV(uint8_t* mm21_subblock,
                  uint8_t* u_subblock,
                  uint8_t* v_subblock) {
  for (size_t i = 0; i < kMT21SubblockWidth * kMT21SubblockHeight / 2; i++) {
    *mm21_subblock = *u_subblock;
    mm21_subblock++;
    u_subblock++;
    *mm21_subblock = *v_subblock;
    mm21_subblock++;
    v_subblock++;
  }
}

void GenerateRandomUVBlock(uint8_t* mt21_block,
                           uint8_t* mm21_block,
                           size_t& subblock_len1,
                           size_t& subblock_len2) {
  MT21BlockWriter writer(mt21_block);
  uint8_t v_subblock[kMT21SubblockWidth / 2 * kMT21SubblockHeight];
  uint8_t u_subblock[kMT21SubblockWidth / 2 * kMT21SubblockHeight];

  do {
    writer.SetPos(0);
    GenerateRandomSubblock(writer, v_subblock, kMT21SubblockWidth / 2);
    GenerateRandomSubblock(writer, u_subblock, kMT21SubblockWidth / 2);
  } while (writer.GetNumRows() >= kMT21SubblockHeight);

  writer.PadToRow();
  subblock_len1 = writer.GetNumRows();
  InterleaveUV(mm21_block, u_subblock, v_subblock);

  int bit_idx = writer.GetPos();
  do {
    writer.SetPos(bit_idx);
    GenerateRandomSubblock(writer, v_subblock, kMT21SubblockWidth / 2);
    GenerateRandomSubblock(writer, u_subblock, kMT21SubblockWidth / 2);
  } while (writer.GetNumRows() - subblock_len1 >= kMT21SubblockHeight);

  subblock_len2 = writer.GetNumRows() - subblock_len1;
  InterleaveUV(mm21_block + kMT21SubblockSize, u_subblock, v_subblock);
}

void GenerateRandomCompressedFrame(uint8_t* mt21_frame_y,
                                   uint8_t* mt21_frame_uv,
                                   uint8_t* mt21_footer_y,
                                   uint8_t* mt21_footer_uv,
                                   uint8_t* nv12_frame_y,
                                   uint8_t* nv12_frame_uv,
                                   int width,
                                   int height) {
  uint8_t* mm21_frame_y =
      static_cast<uint8_t*>(aligned_alloc(16, width * height));
  uint8_t* mm21_frame_uv =
      static_cast<uint8_t*>(aligned_alloc(16, width * height / 2));

  uint8_t* mm21_block = mm21_frame_y;
  size_t subblock_len1;
  size_t subblock_len2;
  for (int i = 0; i < width * height; i += kMT21BlockSize) {
    GenerateRandomYBlock(mt21_frame_y, mm21_block, subblock_len1,
                         subblock_len2);
    mt21_frame_y += kMT21BlockSize;
    mm21_block += kMT21BlockSize;
    subblock_len1--;
    subblock_len2--;
    if ((i / kMT21BlockSize) % 2 == 0) {
      mt21_footer_y[i / kMT21BlockSize / 2] |=
          subblock_len1 | (subblock_len2 << 2);
    } else {
      mt21_footer_y[i / kMT21BlockSize / 2] |=
          (subblock_len1 << 4) | (subblock_len2 << 6);
    }
  }
  mm21_block = mm21_frame_uv;
  for (int i = 0; i < width * height / 2; i += kMT21BlockSize) {
    GenerateRandomUVBlock(mt21_frame_uv, mm21_block, subblock_len1,
                          subblock_len2);
    mt21_frame_uv += kMT21BlockSize;
    mm21_block += kMT21BlockSize;
    subblock_len1--;
    subblock_len2--;
    if ((i / kMT21BlockSize) % 2 == 0) {
      mt21_footer_uv[i / kMT21BlockSize / 2] |=
          subblock_len1 | (subblock_len2 << 2);
    } else {
      mt21_footer_uv[i / kMT21BlockSize / 2] |=
          (subblock_len1 << 4) | (subblock_len2 << 6);
    }
  }

  libyuv::DetilePlane(mm21_frame_y, width, nv12_frame_y, width, width, height,
                      kMT21TileHeight);
  libyuv::DetilePlane(mm21_frame_uv, width, nv12_frame_uv, width, width,
                      height / 2, kMT21TileHeight / 2);

  free(mm21_frame_y);
  free(mm21_frame_uv);
}

void AllocateMT21Plane(gfx::Size& resolution,
                       bool is_chroma,
                       size_t& plane_size,
                       uint8_t** plane,
                       size_t& footer_offset) {
  plane_size = resolution.GetArea();
  if (is_chroma) {
    plane_size /= 2;
    footer_offset = base::bits::AlignUp(
        plane_size, static_cast<size_t>(kMT21UVFooterAlignment));
  } else {
    footer_offset = base::bits::AlignUp(
        plane_size, static_cast<size_t>(kMT21YFooterAlignment));
  }
  size_t footer_size = base::bits::AlignUp(plane_size / kMT21SubblockSize * 2,
                                           static_cast<size_t>(kBitsInByte)) /
                       kBitsInByte;
  plane_size = footer_offset + footer_size;
  *plane = static_cast<uint8_t*>(aligned_alloc(16, plane_size));
}

TEST(MT21DecompressorTest, TestMT21DecompressorPerfTest) {
  gfx::Size resolution(1920, 1088);
  uint8_t* golden_y =
      static_cast<uint8_t*>(aligned_alloc(16, resolution.GetArea()));
  uint8_t* decompressed_y =
      static_cast<uint8_t*>(aligned_alloc(16, resolution.GetArea()));
  uint8_t* golden_uv =
      static_cast<uint8_t*>(aligned_alloc(16, resolution.GetArea() / 2));
  uint8_t* decompressed_uv =
      static_cast<uint8_t*>(aligned_alloc(16, resolution.GetArea() / 2));
  uint8_t* mt21_y;
  uint8_t* mt21_uv;
  size_t mt21_y_size, mt21_uv_size, mt21_y_footer_offset, mt21_uv_footer_offset;

  AllocateMT21Plane(resolution, false, mt21_y_size, &mt21_y,
                    mt21_y_footer_offset);
  AllocateMT21Plane(resolution, true, mt21_uv_size, &mt21_uv,
                    mt21_uv_footer_offset);

  GenerateRandomCompressedFrame(mt21_y, mt21_uv, mt21_y + mt21_y_footer_offset,
                                mt21_uv + mt21_uv_footer_offset, golden_y,
                                golden_uv, resolution.width(),
                                resolution.height());

  MT21Decompressor decompressor(resolution);

  perf_test::PerfResultReporter reporter("MT21Decompressor", "Uncapped Test");
  reporter.RegisterImportantMetric(".decompress_latency", "us");

  memset(decompressed_y, 0, resolution.GetArea());
  memset(decompressed_uv, 0, resolution.GetArea() / 2);
  constexpr int kNumIterations = 1000;
  auto start_time = base::TimeTicks::Now();
  for (int i = 0; i < kNumIterations; i++) {
    decompressor.MT21ToNV12(mt21_y, mt21_uv, mt21_y_size, mt21_uv_size,
                            decompressed_y, decompressed_uv);
  }
  auto end_time = base::TimeTicks::Now();
  auto delta_time = end_time - start_time;
  reporter.AddResult(
      ".decompress_latency",
      static_cast<size_t>(delta_time.InMicroseconds() / kNumIterations));

  for (int i = 0; i < resolution.GetArea(); i++) {
    ASSERT_TRUE(decompressed_y[i] == golden_y[i]);
  }
  for (int i = 0; i < resolution.GetArea() / 2; i++) {
    ASSERT_TRUE(decompressed_uv[i] == golden_uv[i]);
  }

  free(golden_y);
  free(decompressed_y);
  free(golden_uv);
  free(decompressed_uv);
  free(mt21_y);
  free(mt21_uv);
}

}  // namespace
}  // namespace media

int main(int argc, char** argv) {
  base::CommandLine::Init(argc, argv);

  testing::InitGoogleTest(&argc, argv);

  return RUN_ALL_TESTS();
}

#endif