libyuv/unit_test/scale_plane_test.cc
Sida Zhu ae48f8dd61 Fix same-width vertical point upsampling
ScalePlane's same-width vertical fast path used the endpoint-oriented
FixedDiv1 step for all upsampling. That stepping is appropriate for
bilinear interpolation, but not for point sampling, where source rows
should cover equally sized destination cells.

Use the same centered FixedDiv stepping as the generic point sampler.
Allow the point path to select the final source row while passing a
zero interpolation stride so row kernels cannot form a pointer beyond
it. Add 8-bit and 16-bit point regression coverage and verify that
bilinear upsampling remains unchanged.

Bug: 537818862
Change-Id: I7bee21b85913b31c146ecf7fe9f5f08608a8a17d
Reviewed-on: https://chromium-review.googlesource.com/c/libyuv/libyuv/+/8264449
Reviewed-by: Wan-Teh Chang <wtc@google.com>
Commit-Queue: Wan-Teh Chang <wtc@google.com>
Reviewed-by: Frank Barchard <fbarchard@google.com>
2026-08-24 19:28:50 -07:00

809 lines
28 KiB
C++

/*
* Copyright 2023 The LibYuv Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <new>
#include "../unit_test/unit_test.h"
#include "libyuv/cpu_id.h"
#include "libyuv/scale.h"
#ifdef ENABLE_ROW_TESTS
#include "libyuv/scale_row.h" // For ScaleRowDown2Box_Odd_C
#endif
#define STRINGIZE(line) #line
#define FILELINESTR(file, line) file ":" STRINGIZE(line)
#if (defined(__riscv) && !defined(__clang__)) || defined(__hexagon__)
#define DISABLE_SLOW_TESTS
#undef ENABLE_FULL_TESTS
#undef ENABLE_ROW_TESTS
#define LEAN_TESTS
#endif
#if !defined(DISABLE_SLOW_TESTS) || defined(__x86_64__) || defined(__i386__)
// SLOW TESTS are those that are unoptimized C code.
// FULL TESTS are optimized but test many variations of the same code.
#define ENABLE_FULL_TESTS
#endif
namespace libyuv {
// POC: int row_stride = src_stride * 2 overflows to a small negative value
// when src_stride is close to INT_MAX, causing src_ptr to walk backward
// past the start of the source allocation on the second loop iteration.
// With src_stride = 0x7FFFFFFE, row_stride = (int)0xFFFFFFFC = -4, so on
// y=1 ScaleRowDown2Box reads 4 bytes before the heap allocation.
TEST_F(LibYUVScaleTest, ScalePlaneDown2_RowStrideOverflow) {
constexpr int kSrcStride = 0x7FFFFFFE; // INT_MAX - 1
constexpr int kSrcW = 64;
constexpr int kSrcH = 4;
constexpr int kDstW = 32;
constexpr int kDstH = 2;
// src_size = (kSrcH - 1) * stride + width.
size_t src_size = kSrcH - 1;
if (src_size > SIZE_MAX / kSrcStride) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size *= kSrcStride;
if (src_size > SIZE_MAX - kSrcW) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size += kSrcW;
#if defined(__aarch64__)
// Infer malloc can accept a large size for cpu with dot product (a76/a55)
int has_large_malloc = TestCpuFlag(kCpuHasNeonDotProd);
#else
int has_large_malloc = 1;
#endif
if (!has_large_malloc) {
GTEST_SKIP() << "large allocation may assert for " << src_size << " bytes";
}
uint8_t* src = new (std::nothrow) uint8_t[src_size];
if (!src) {
GTEST_SKIP() << "could not allocate " << src_size << " bytes";
}
uint8_t dst[kDstW * kDstH];
uint8_t* src_row = src;
for (int i = 0; i < kSrcH; i++) {
memset(src_row, 0x41, kSrcW);
src_row += kSrcStride;
}
// Force the C row kernel: the SIMD kernels are inline asm that ASAN does not
// instrument, so they silently read OOB without a report.
MaskCpuFlags(1);
// 2*dst == src on both axes -> ScalePlane dispatches to ScalePlaneDown2.
// int row_stride = kSrcStride * 2 wraps to -4; on y=1 src_ptr underflows.
ScalePlane(src, kSrcStride, kSrcW, kSrcH, dst, kDstW, kDstW, kDstH,
kFilterBox);
MaskCpuFlags(0);
delete[] src;
}
// POC: same defect in the 1/4 fast path. src_stride = 0x3FFFFFFF gives
// int row_stride = src_stride * 4 = (int)0xFFFFFFFC = -4.
TEST_F(LibYUVScaleTest, ScalePlaneDown4_RowStrideOverflow) {
constexpr int kSrcStride = 0x3FFFFFFF; // INT_MAX / 4 (rounded down)
constexpr int kSrcW = 64;
constexpr int kSrcH = 8;
constexpr int kDstW = 16;
constexpr int kDstH = 2;
// src_size = (kSrcH - 1) * stride + width.
size_t src_size = kSrcH - 1;
if (src_size > SIZE_MAX / kSrcStride) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size *= kSrcStride;
if (src_size > SIZE_MAX - kSrcW) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size += kSrcW;
#if defined(__aarch64__)
// Infer malloc can accept a large size for cpu with dot product (a76/a55)
int has_large_malloc = TestCpuFlag(kCpuHasNeonDotProd);
#else
int has_large_malloc = 1;
#endif
if (!has_large_malloc) {
GTEST_SKIP() << "large allocation may assert for " << src_size << " bytes";
}
uint8_t* src = new (std::nothrow) uint8_t[src_size];
if (!src) {
GTEST_SKIP() << "could not allocate " << src_size << " bytes";
}
uint8_t dst[kDstW * kDstH];
uint8_t* src_row = src;
for (int i = 0; i < kSrcH; i++) {
memset(src_row, 0x41, kSrcW);
src_row += kSrcStride;
}
// Force the C row kernel: the SIMD kernels are inline asm that ASAN does not
// instrument, so they silently read OOB without a report.
MaskCpuFlags(1);
// 4*dst == src on both axes with kFilterBox -> ScalePlaneDown4.
ScalePlane(src, kSrcStride, kSrcW, kSrcH, dst, kDstW, kDstW, kDstH,
kFilterBox);
MaskCpuFlags(0);
delete[] src;
}
#ifdef ENABLE_ROW_TESTS
#ifdef HAS_SCALEROWDOWN2_SSSE3
TEST_F(LibYUVScaleTest, TestScaleRowDown2Box_Odd_SSSE3) {
SIMD_ALIGNED(uint8_t orig_pixels[128 * 2]);
SIMD_ALIGNED(uint8_t dst_pixels_opt[64]);
SIMD_ALIGNED(uint8_t dst_pixels_c[64]);
memset(orig_pixels, 0, sizeof(orig_pixels));
memset(dst_pixels_opt, 0, sizeof(dst_pixels_opt));
memset(dst_pixels_c, 0, sizeof(dst_pixels_c));
int has_ssse3 = TestCpuFlag(kCpuHasSSSE3);
if (!has_ssse3) {
printf("Warning SSSE3 not detected; Skipping test.\n");
} else {
// TL.
orig_pixels[0] = 255u;
orig_pixels[1] = 0u;
orig_pixels[128 + 0] = 0u;
orig_pixels[128 + 1] = 0u;
// TR.
orig_pixels[2] = 0u;
orig_pixels[3] = 100u;
orig_pixels[128 + 2] = 0u;
orig_pixels[128 + 3] = 0u;
// BL.
orig_pixels[4] = 0u;
orig_pixels[5] = 0u;
orig_pixels[128 + 4] = 50u;
orig_pixels[128 + 5] = 0u;
// BR.
orig_pixels[6] = 0u;
orig_pixels[7] = 0u;
orig_pixels[128 + 6] = 0u;
orig_pixels[128 + 7] = 20u;
// Odd.
orig_pixels[126] = 4u;
orig_pixels[127] = 255u;
orig_pixels[128 + 126] = 16u;
orig_pixels[128 + 127] = 255u;
// Test regular half size.
ScaleRowDown2Box_C(orig_pixels, 128, dst_pixels_c, 64);
ASSERT_EQ(64u, dst_pixels_c[0]);
ASSERT_EQ(25u, dst_pixels_c[1]);
ASSERT_EQ(13u, dst_pixels_c[2]);
ASSERT_EQ(5u, dst_pixels_c[3]);
ASSERT_EQ(0u, dst_pixels_c[4]);
ASSERT_EQ(133u, dst_pixels_c[63]);
// Test Odd width version - Last pixel is just 1 horizontal pixel.
ScaleRowDown2Box_Odd_C(orig_pixels, 128, dst_pixels_c, 64);
ASSERT_EQ(64u, dst_pixels_c[0]);
ASSERT_EQ(25u, dst_pixels_c[1]);
ASSERT_EQ(13u, dst_pixels_c[2]);
ASSERT_EQ(5u, dst_pixels_c[3]);
ASSERT_EQ(0u, dst_pixels_c[4]);
ASSERT_EQ(10u, dst_pixels_c[63]);
// Test one pixel less, should skip the last pixel.
memset(dst_pixels_c, 0, sizeof(dst_pixels_c));
ScaleRowDown2Box_Odd_C(orig_pixels, 128, dst_pixels_c, 63);
ASSERT_EQ(64u, dst_pixels_c[0]);
ASSERT_EQ(25u, dst_pixels_c[1]);
ASSERT_EQ(13u, dst_pixels_c[2]);
ASSERT_EQ(5u, dst_pixels_c[3]);
ASSERT_EQ(0u, dst_pixels_c[4]);
ASSERT_EQ(0u, dst_pixels_c[63]);
// Test regular half size SSSE3.
ScaleRowDown2Box_SSSE3(orig_pixels, 128, dst_pixels_opt, 64);
ASSERT_EQ(64u, dst_pixels_opt[0]);
ASSERT_EQ(25u, dst_pixels_opt[1]);
ASSERT_EQ(13u, dst_pixels_opt[2]);
ASSERT_EQ(5u, dst_pixels_opt[3]);
ASSERT_EQ(0u, dst_pixels_opt[4]);
ASSERT_EQ(133u, dst_pixels_opt[63]);
// Compare C and SSSE3 match.
ScaleRowDown2Box_Odd_C(orig_pixels, 128, dst_pixels_c, 64);
ScaleRowDown2Box_Odd_SSSE3(orig_pixels, 128, dst_pixels_opt, 64);
for (int i = 0; i < 64; ++i) {
ASSERT_EQ(dst_pixels_c[i], dst_pixels_opt[i]);
}
}
}
#endif // HAS_SCALEROWDOWN2_SSSE3
TEST_F(LibYUVScaleTest, TestScaleRowDown2Box_16) {
SIMD_ALIGNED(uint16_t orig_pixels[2560 * 2]);
SIMD_ALIGNED(uint16_t dst_pixels_c[1280]);
SIMD_ALIGNED(uint16_t dst_pixels_opt[1280]);
memset(orig_pixels, 0, sizeof(orig_pixels));
memset(dst_pixels_c, 1, sizeof(dst_pixels_c));
memset(dst_pixels_opt, 2, sizeof(dst_pixels_opt));
for (int i = 0; i < 2560 * 2; ++i) {
orig_pixels[i] = i;
}
ScaleRowDown2Box_16_C(&orig_pixels[0], 2560, &dst_pixels_c[0], 1280);
for (int i = 0; i < benchmark_pixels_div1280_; ++i) {
#if !defined(LIBYUV_DISABLE_NEON) && defined(__aarch64__)
int has_neon = TestCpuFlag(kCpuHasNEON);
if (has_neon) {
ScaleRowDown2Box_16_NEON(&orig_pixels[0], 2560, &dst_pixels_opt[0], 1280);
} else {
ScaleRowDown2Box_16_C(&orig_pixels[0], 2560, &dst_pixels_opt[0], 1280);
}
#else
ScaleRowDown2Box_16_C(&orig_pixels[0], 2560, &dst_pixels_opt[0], 1280);
#endif
}
for (int i = 0; i < 1280; ++i) {
ASSERT_EQ(dst_pixels_c[i], dst_pixels_opt[i]);
}
ASSERT_EQ(dst_pixels_c[0], (0 + 1 + 2560 + 2561 + 2) / 4);
ASSERT_EQ(dst_pixels_c[1279], 3839);
}
#endif // ENABLE_ROW_TESTS
// Test scaling plane with 8 bit C vs 12 bit C and return maximum pixel
// difference.
// 0 = exact.
static int TestPlaneFilter_16(int src_width,
int src_height,
int dst_width,
int dst_height,
FilterMode f,
int benchmark_iterations,
int disable_cpu_flags,
int benchmark_cpu_info) {
if (!SizeValid(src_width, src_height, dst_width, dst_height)) {
return 0;
}
int i;
int64_t src_y_plane_size = (Abs(src_width)) * (Abs(src_height));
int src_stride_y = Abs(src_width);
int dst_y_plane_size = dst_width * dst_height;
int dst_stride_y = dst_width;
align_buffer_page_end(src_y, src_y_plane_size);
align_buffer_page_end(src_y_16, src_y_plane_size * 2);
align_buffer_page_end(dst_y_8, dst_y_plane_size);
align_buffer_page_end(dst_y_16, dst_y_plane_size * 2);
uint16_t* p_src_y_16 = reinterpret_cast<uint16_t*>(src_y_16);
uint16_t* p_dst_y_16 = reinterpret_cast<uint16_t*>(dst_y_16);
MemRandomize(src_y, src_y_plane_size);
memset(dst_y_8, 0, dst_y_plane_size);
memset(dst_y_16, 1, dst_y_plane_size * 2);
for (i = 0; i < src_y_plane_size; ++i) {
p_src_y_16[i] = src_y[i] & 255;
}
MaskCpuFlags(disable_cpu_flags); // Disable all CPU optimization.
ScalePlane(src_y, src_stride_y, src_width, src_height, dst_y_8, dst_stride_y,
dst_width, dst_height, f);
MaskCpuFlags(benchmark_cpu_info); // Enable all CPU optimization.
for (i = 0; i < benchmark_iterations; ++i) {
ScalePlane_16(p_src_y_16, src_stride_y, src_width, src_height, p_dst_y_16,
dst_stride_y, dst_width, dst_height, f);
}
// Expect an exact match.
int max_diff = 0;
for (i = 0; i < dst_y_plane_size; ++i) {
int abs_diff = Abs(dst_y_8[i] - p_dst_y_16[i]);
if (abs_diff > max_diff) {
max_diff = abs_diff;
}
}
free_aligned_buffer_page_end(dst_y_8);
free_aligned_buffer_page_end(dst_y_16);
free_aligned_buffer_page_end(src_y);
free_aligned_buffer_page_end(src_y_16);
return max_diff;
}
// The following adjustments in dimensions ensure the scale factor will be
// exactly achieved.
// 2 is chroma subsample.
#define DX(x, nom, denom) static_cast<int>(((Abs(x) / nom + 1) / 2) * nom * 2)
#define SX(x, nom, denom) static_cast<int>(((x / nom + 1) / 2) * denom * 2)
#define TEST_FACTOR1(name, filter, nom, denom, max_diff) \
TEST_F(LibYUVScaleTest, DISABLED_##ScalePlaneDownBy##name##_##filter##_16) { \
int diff = TestPlaneFilter_16( \
SX(benchmark_width_, nom, denom), SX(benchmark_height_, nom, denom), \
DX(benchmark_width_, nom, denom), DX(benchmark_height_, nom, denom), \
kFilter##filter, benchmark_iterations_, disable_cpu_flags_, \
benchmark_cpu_info_); \
ASSERT_LE(diff, max_diff); \
}
// Test a scale factor with all 4 filters. Expect unfiltered to be exact, but
// filtering is different fixed point implementations for SSSE3, Neon and C.
#define TEST_FACTOR(name, nom, denom, boxdiff) \
TEST_FACTOR1(name, None, nom, denom, 0) \
TEST_FACTOR1(name, Linear, nom, denom, boxdiff) \
TEST_FACTOR1(name, Bilinear, nom, denom, boxdiff) \
TEST_FACTOR1(name, Box, nom, denom, boxdiff)
TEST_FACTOR(2, 1, 2, 0)
TEST_FACTOR(4, 1, 4, 0)
// TEST_FACTOR(8, 1, 8, 0) Disable for benchmark performance. Takes 90 seconds.
TEST_FACTOR(3by4, 3, 4, 1)
TEST_FACTOR(3by8, 3, 8, 1)
TEST_FACTOR(3, 1, 3, 0)
#undef TEST_FACTOR1
#undef TEST_FACTOR
#undef SX
#undef DX
TEST_F(LibYUVScaleTest, PlaneTest3x) {
const int kSrcStride = 480;
const int kDstStride = 160;
const int kSize = kSrcStride * 3;
align_buffer_page_end(orig_pixels, kSize);
for (int i = 0; i < 480 * 3; ++i) {
orig_pixels[i] = i;
}
align_buffer_page_end(dest_pixels, kDstStride);
int iterations160 = (benchmark_width_ * benchmark_height_ + (160 - 1)) / 160 *
benchmark_iterations_;
for (int i = 0; i < iterations160; ++i) {
ScalePlane(orig_pixels, kSrcStride, 480, 3, dest_pixels, kDstStride, 160, 1,
kFilterBilinear);
}
ASSERT_EQ(225, dest_pixels[0]);
ScalePlane(orig_pixels, kSrcStride, 480, 3, dest_pixels, kDstStride, 160, 1,
kFilterNone);
ASSERT_EQ(225, dest_pixels[0]);
free_aligned_buffer_page_end(dest_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
TEST_F(LibYUVScaleTest, PlaneTest4x) {
const int kSrcStride = 640;
const int kDstStride = 160;
const int kSize = kSrcStride * 4;
align_buffer_page_end(orig_pixels, kSize);
for (int i = 0; i < 640 * 4; ++i) {
orig_pixels[i] = i;
}
align_buffer_page_end(dest_pixels, kDstStride);
int iterations160 = (benchmark_width_ * benchmark_height_ + (160 - 1)) / 160 *
benchmark_iterations_;
for (int i = 0; i < iterations160; ++i) {
ScalePlane(orig_pixels, kSrcStride, 640, 4, dest_pixels, kDstStride, 160, 1,
kFilterBilinear);
}
ASSERT_EQ(66, dest_pixels[0]);
ScalePlane(orig_pixels, kSrcStride, 640, 4, dest_pixels, kDstStride, 160, 1,
kFilterNone);
ASSERT_EQ(2, dest_pixels[0]); // expect the 3rd pixel of the 3rd row
free_aligned_buffer_page_end(dest_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
// Intent is to test 200x50 to 50x200 but width and height can be parameters.
TEST_F(LibYUVScaleTest, PlaneTestRotate_None) {
const int kSize = benchmark_width_ * benchmark_height_;
align_buffer_page_end(orig_pixels, kSize);
for (int i = 0; i < kSize; ++i) {
orig_pixels[i] = i;
}
align_buffer_page_end(dest_opt_pixels, kSize);
align_buffer_page_end(dest_c_pixels, kSize);
MaskCpuFlags(disable_cpu_flags_); // Disable all CPU optimization.
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_, benchmark_height_,
dest_c_pixels, benchmark_height_, benchmark_height_,
benchmark_width_, kFilterNone);
MaskCpuFlags(benchmark_cpu_info_); // Enable all CPU optimization.
for (int i = 0; i < benchmark_iterations_; ++i) {
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_,
benchmark_height_, dest_opt_pixels, benchmark_height_,
benchmark_height_, benchmark_width_, kFilterNone);
}
for (int i = 0; i < kSize; ++i) {
ASSERT_EQ(dest_c_pixels[i], dest_opt_pixels[i]);
}
free_aligned_buffer_page_end(dest_c_pixels);
free_aligned_buffer_page_end(dest_opt_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
TEST_F(LibYUVScaleTest, PlaneTestRotate_Bilinear) {
const int kSize = benchmark_width_ * benchmark_height_;
align_buffer_page_end(orig_pixels, kSize);
for (int i = 0; i < kSize; ++i) {
orig_pixels[i] = i;
}
align_buffer_page_end(dest_opt_pixels, kSize);
align_buffer_page_end(dest_c_pixels, kSize);
MaskCpuFlags(disable_cpu_flags_); // Disable all CPU optimization.
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_, benchmark_height_,
dest_c_pixels, benchmark_height_, benchmark_height_,
benchmark_width_, kFilterBilinear);
MaskCpuFlags(benchmark_cpu_info_); // Enable all CPU optimization.
for (int i = 0; i < benchmark_iterations_; ++i) {
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_,
benchmark_height_, dest_opt_pixels, benchmark_height_,
benchmark_height_, benchmark_width_, kFilterBilinear);
}
for (int i = 0; i < kSize; ++i) {
ASSERT_EQ(dest_c_pixels[i], dest_opt_pixels[i]);
}
free_aligned_buffer_page_end(dest_c_pixels);
free_aligned_buffer_page_end(dest_opt_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
// Intent is to test 200x50 to 50x200 but width and height can be parameters.
TEST_F(LibYUVScaleTest, PlaneTestRotate_Box) {
const int kSize = benchmark_width_ * benchmark_height_;
align_buffer_page_end(orig_pixels, kSize);
for (int i = 0; i < kSize; ++i) {
orig_pixels[i] = i;
}
align_buffer_page_end(dest_opt_pixels, kSize);
align_buffer_page_end(dest_c_pixels, kSize);
MaskCpuFlags(disable_cpu_flags_); // Disable all CPU optimization.
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_, benchmark_height_,
dest_c_pixels, benchmark_height_, benchmark_height_,
benchmark_width_, kFilterBox);
MaskCpuFlags(benchmark_cpu_info_); // Enable all CPU optimization.
for (int i = 0; i < benchmark_iterations_; ++i) {
ScalePlane(orig_pixels, benchmark_width_, benchmark_width_,
benchmark_height_, dest_opt_pixels, benchmark_height_,
benchmark_height_, benchmark_width_, kFilterBox);
}
for (int i = 0; i < kSize; ++i) {
ASSERT_EQ(dest_c_pixels[i], dest_opt_pixels[i]);
}
free_aligned_buffer_page_end(dest_c_pixels);
free_aligned_buffer_page_end(dest_opt_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
TEST_F(LibYUVScaleTest, PlaneTest1_Box) {
align_buffer_page_end(orig_pixels, 3);
align_buffer_page_end(dst_pixels, 3);
// Pad the 1x1 byte image with invalid values before and after in case libyuv
// reads outside the memory boundaries.
orig_pixels[0] = 0;
orig_pixels[1] = 1; // scale this pixel
orig_pixels[2] = 2;
dst_pixels[0] = 3;
dst_pixels[1] = 3;
dst_pixels[2] = 3;
libyuv::ScalePlane(orig_pixels + 1, /* src_stride= */ 1, /* src_width= */ 1,
/* src_height= */ 1, dst_pixels, /* dst_stride= */ 1,
/* dst_width= */ 1, /* dst_height= */ 2,
libyuv::kFilterBox);
ASSERT_EQ(dst_pixels[0], 1);
ASSERT_EQ(dst_pixels[1], 1);
ASSERT_EQ(dst_pixels[2], 3);
free_aligned_buffer_page_end(dst_pixels);
free_aligned_buffer_page_end(orig_pixels);
}
TEST_F(LibYUVScaleTest, PlaneTest1_16_Box) {
align_buffer_page_end(orig_pixels_alloc, 3 * 2);
align_buffer_page_end(dst_pixels_alloc, 3 * 2);
uint16_t* orig_pixels = (uint16_t*)orig_pixels_alloc;
uint16_t* dst_pixels = (uint16_t*)dst_pixels_alloc;
// Pad the 1x1 byte image with invalid values before and after in case libyuv
// reads outside the memory boundaries.
orig_pixels[0] = 0;
orig_pixels[1] = 1; // scale this pixel
orig_pixels[2] = 2;
dst_pixels[0] = 3;
dst_pixels[1] = 3;
dst_pixels[2] = 3;
libyuv::ScalePlane_16(
orig_pixels + 1, /* src_stride= */ 1, /* src_width= */ 1,
/* src_height= */ 1, dst_pixels, /* dst_stride= */ 1,
/* dst_width= */ 1, /* dst_height= */ 2, libyuv::kFilterNone);
ASSERT_EQ(dst_pixels[0], 1);
ASSERT_EQ(dst_pixels[1], 1);
ASSERT_EQ(dst_pixels[2], 3);
free_aligned_buffer_page_end(dst_pixels_alloc);
free_aligned_buffer_page_end(orig_pixels_alloc);
}
TEST_F(LibYUVScaleTest, ScalePlaneVerticalPointUp) {
const int kWidth = 32;
const uint8_t kSentinel = 0xFF;
const uint8_t expected[] = {10, 10, 20, 30, 30};
// An extra sentinel row exposes accidental interpolation past the last valid
// row. 3->5 is the uneven ratio that used to drop the last source row.
align_buffer_page_end(src, kWidth * 4);
align_buffer_page_end(dst, kWidth * 5);
memset(src, kSentinel, kWidth * 4);
memset(dst, kSentinel, kWidth * 5);
memset(src, 10, kWidth);
memset(src + kWidth, 20, kWidth);
memset(src + 2 * kWidth, 30, kWidth);
ASSERT_EQ(0, ScalePlane(src, kWidth, kWidth, 3, dst, kWidth, kWidth, 5,
kFilterNone));
for (int y = 0; y < 5; ++y) {
for (int x = 0; x < kWidth; ++x) {
EXPECT_EQ(expected[y], dst[y * kWidth + x]);
}
}
free_aligned_buffer_page_end(dst);
free_aligned_buffer_page_end(src);
}
TEST_F(LibYUVScaleTest, ScalePlaneVerticalPointUp_16) {
const int kWidth = 32;
const uint16_t kSentinel = 0xFFFF;
const uint16_t expected[] = {10, 10, 20, 30, 30};
align_buffer_page_end(src_alloc, kWidth * 4 * 2);
align_buffer_page_end(dst_alloc, kWidth * 5 * 2);
uint16_t* src = reinterpret_cast<uint16_t*>(src_alloc);
uint16_t* dst = reinterpret_cast<uint16_t*>(dst_alloc);
for (int i = 0; i < kWidth * 4; ++i) {
src[i] = kSentinel;
}
for (int i = 0; i < kWidth * 5; ++i) {
dst[i] = kSentinel;
}
for (int x = 0; x < kWidth; ++x) {
src[x] = 10;
src[kWidth + x] = 20;
src[2 * kWidth + x] = 30;
}
ASSERT_EQ(0, ScalePlane_16(src, kWidth, kWidth, 3, dst, kWidth, kWidth, 5,
kFilterNone));
for (int y = 0; y < 5; ++y) {
for (int x = 0; x < kWidth; ++x) {
EXPECT_EQ(expected[y], dst[y * kWidth + x]);
}
}
free_aligned_buffer_page_end(dst_alloc);
free_aligned_buffer_page_end(src_alloc);
}
TEST_F(LibYUVScaleTest, ScalePlaneVerticalBilinearUp) {
const int kWidth = 32;
const uint8_t kSentinel = 0xFF;
// Same-width 2x upsampling still uses FixedDiv1. With y = 0 the 8.8
// fractions are 0, 85, 170, 255. InterpolateRow_C:
// (a * (256 - yf) + b * yf + 128) >> 8.
// Nearest 2x would duplicate rows as {10, 10, 200, 200}.
const uint8_t expected[] = {10, 73, 136, 199};
align_buffer_page_end(src, kWidth * 3);
align_buffer_page_end(dst, kWidth * 4);
memset(src, kSentinel, kWidth * 3);
memset(dst, kSentinel, kWidth * 4);
memset(src, 10, kWidth);
memset(src + kWidth, 200, kWidth);
ASSERT_EQ(0, ScalePlane(src, kWidth, kWidth, 2, dst, kWidth, kWidth, 4,
kFilterBilinear));
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < kWidth; ++x) {
EXPECT_EQ(expected[y], dst[y * kWidth + x]);
}
}
free_aligned_buffer_page_end(dst);
free_aligned_buffer_page_end(src);
}
TEST_F(LibYUVScaleTest, ScalePlaneVerticalBilinearUp_16) {
const int kWidth = 32;
const uint16_t kSentinel = 0xFFFF;
const uint16_t expected[] = {10, 73, 136, 199};
align_buffer_page_end(src_alloc, kWidth * 3 * 2);
align_buffer_page_end(dst_alloc, kWidth * 4 * 2);
uint16_t* src = reinterpret_cast<uint16_t*>(src_alloc);
uint16_t* dst = reinterpret_cast<uint16_t*>(dst_alloc);
for (int i = 0; i < kWidth * 3; ++i) {
src[i] = kSentinel;
}
for (int i = 0; i < kWidth * 4; ++i) {
dst[i] = kSentinel;
}
for (int x = 0; x < kWidth; ++x) {
src[x] = 10;
src[kWidth + x] = 200;
}
ASSERT_EQ(0, ScalePlane_16(src, kWidth, kWidth, 2, dst, kWidth, kWidth, 4,
kFilterBilinear));
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < kWidth; ++x) {
EXPECT_EQ(expected[y], dst[y * kWidth + x]);
}
}
free_aligned_buffer_page_end(dst_alloc);
free_aligned_buffer_page_end(src_alloc);
}
// POC: int * int overflow in ScalePlaneVertical (scale_common.cc).
//
// `yi * src_stride` is evaluated as int * int. When the product exceeds
// INT_MAX it wraps negative and InterpolateRow reads from BEFORE the
// source allocation.
//
// Parameters:
// - dst_width == src_width
// -> ScalePlane dispatches to ScalePlaneVertical
// - src_height == 5, dst_height == 1
// -> single iteration with yi == 2
// - src_stride == 0x7FFFFFF8
// -> 2 * 0x7FFFFFF8 == 0xFFFFFFF0 == -16 (int)
//
// The source buffer is sized so that the *correct* 64-bit offset
// (2 * 0x7FFFFFF8 == 4294967280) plus kWidth bytes is in-bounds. With the
// bug, the 32-bit product is -16 and ASAN reports a heap-buffer-overflow
// READ "16 bytes before" the allocation.
TEST_F(LibYUVScaleTest, ScalePlaneVertical_IntStrideOverflow) {
const int kWidth = 16;
const int kSrcHeight = 5;
const int kDstHeight = 1;
const int kStride = 0x7FFFFFF8; // 2147483640
// src_size is big enough for the only row this call legitimately touches
// (yi == 2) when computed in 64-bit: 2 * stride + width = 4 GiB.
size_t src_size = kStride;
if (src_size > SIZE_MAX / 2) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size *= 2;
if (src_size > SIZE_MAX - kWidth) {
GTEST_SKIP() << "could not represent allocation size in size_t";
}
src_size += kWidth;
#if defined(__aarch64__)
// Infer malloc can accept a large size for cpu with dot product (a76/a55)
int has_large_malloc = TestCpuFlag(kCpuHasNeonDotProd);
#else
int has_large_malloc = 1;
#endif
if (!has_large_malloc) {
GTEST_SKIP() << "large allocation may assert for " << src_size << " bytes";
}
uint8_t* src = new (std::nothrow) uint8_t[src_size];
if (!src) {
GTEST_SKIP() << "could not allocate " << src_size << " bytes";
}
uint8_t* dst = new uint8_t[kWidth];
memset(dst, 0, kWidth);
// Force the scalar path so the crash site is deterministic
// (InterpolateRow_C -> memcpy when yf == 0).
MaskCpuFlags(disable_cpu_flags_);
int r = ScalePlane(src, kStride, kWidth, kSrcHeight, dst, kWidth, kWidth,
kDstHeight, kFilterNone);
// Not reached under ASAN.
ASSERT_EQ(0, r);
delete[] src;
delete[] dst;
}
TEST_F(LibYUVScaleTest, ScalePlane_InvalidInputs) {
uint8_t src[16] = {0};
uint8_t dst[16] = {0};
// NULL src/dst
EXPECT_EQ(-1, ScalePlane(nullptr, 4, 4, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 4, nullptr, 4, 4, 4, kFilterNone));
// Width/height <= 0 (except src_height which can be negative but not 0)
EXPECT_EQ(-1, ScalePlane(src, 4, 0, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, -1, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 0, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 4, dst, 4, 0, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 4, dst, 4, -1, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 4, dst, 4, 4, 0, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 4, dst, 4, 4, -1, kFilterNone));
// Width/height too large (> 32768)
EXPECT_EQ(-1, ScalePlane(src, 4, 32769, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, 32769, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane(src, 4, 4, -32769, dst, 4, 4, 4, kFilterNone));
// Valid edge cases
EXPECT_EQ(0, ScalePlane(src, 4, 1, 1, dst, 4, 1, 1, kFilterNone));
EXPECT_EQ(0, ScalePlane(src, 4, 1, -1, dst, 4, 1, 1, kFilterNone));
}
TEST_F(LibYUVScaleTest, ScalePlane_16_InvalidInputs) {
uint16_t src[16] = {0};
uint16_t dst[16] = {0};
EXPECT_EQ(-1, ScalePlane_16(nullptr, 4, 4, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_16(src, 4, 4, 4, nullptr, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_16(src, 4, 0, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_16(src, 4, 32769, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_16(src, 4, 4, -32769, dst, 4, 4, 4, kFilterNone));
}
TEST_F(LibYUVScaleTest, ScalePlane_12_InvalidInputs) {
uint16_t src[16] = {0};
uint16_t dst[16] = {0};
EXPECT_EQ(-1, ScalePlane_12(nullptr, 4, 4, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_12(src, 4, 4, 4, nullptr, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_12(src, 4, 0, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_12(src, 4, 32769, 4, dst, 4, 4, 4, kFilterNone));
EXPECT_EQ(-1, ScalePlane_12(src, 4, 4, -32769, dst, 4, 4, 4, kFilterNone));
}
} // namespace libyuv