1 // Unit tests for ResampleEffect.
4 #include <gtest/gtest.h>
9 #include "effect_chain.h"
10 #include "flat_input.h"
12 #include "image_format.h"
14 #include "resample_effect.h"
15 #include "test_util.h"
25 return sin(M_PI * x) / (M_PI * x);
28 float lanczos(float x, float a)
33 return sinc(x) * sinc(x / a);
39 TEST(ResampleEffectTest, IdentityTransformDoesNothing) {
42 float data[size * size] = {
48 float out_data[size * size];
50 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
51 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
52 ASSERT_TRUE(resample_effect->set_int("width", 4));
53 ASSERT_TRUE(resample_effect->set_int("height", 4));
54 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
56 expect_equal(data, out_data, size, size);
59 TEST(ResampleEffectTest, UpscaleByTwoGetsCorrectPixelCenters) {
62 float data[size * size] = {
63 0.0, 0.0, 0.0, 0.0, 0.0,
64 0.0, 0.0, 0.0, 0.0, 0.0,
65 0.0, 0.0, 1.0, 0.0, 0.0,
66 0.0, 0.0, 0.0, 0.0, 0.0,
67 0.0, 0.0, 0.0, 0.0, 0.0,
69 float expected_data[size * size * 4], out_data[size * size * 4];
71 for (int y = 0; y < size * 2; ++y) {
72 for (int x = 0; x < size * 2; ++x) {
73 float weight = lanczos((x - size + 0.5f) * 0.5f, 3.0f);
74 weight *= lanczos((y - size + 0.5f) * 0.5f, 3.0f);
75 expected_data[y * (size * 2) + x] = weight;
79 EffectChainTester tester(nullptr, size * 2, size * 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
82 format.color_space = COLORSPACE_sRGB;
83 format.gamma_curve = GAMMA_LINEAR;
85 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size, size);
86 input->set_pixel_data(data);
87 tester.get_chain()->add_input(input);
89 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
90 ASSERT_TRUE(resample_effect->set_int("width", size * 2));
91 ASSERT_TRUE(resample_effect->set_int("height", size * 2));
92 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
94 expect_equal(expected_data, out_data, size * 2, size * 2);
97 TEST(ResampleEffectTest, DownscaleByTwoGetsCorrectPixelCenters) {
100 // This isn't a perfect dot, since the Lanczos filter has a slight
101 // sharpening effect; the most important thing is that we have kept
102 // the texel center right (everything is nicely symmetric).
103 // The approximate magnitudes have been checked against ImageMagick.
104 float expected_data[size * size] = {
105 0.0045, -0.0067, -0.0599, -0.0067, 0.0045,
106 -0.0067, 0.0100, 0.0892, 0.0100, -0.0067,
107 -0.0599, 0.0890, 0.7925, 0.0892, -0.0599,
108 -0.0067, 0.0100, 0.0890, 0.0100, -0.0067,
109 0.0045, -0.0067, -0.0599, -0.0067, 0.0045,
111 float data[size * size * 4], out_data[size * size];
113 for (int y = 0; y < size * 2; ++y) {
114 for (int x = 0; x < size * 2; ++x) {
115 float weight = lanczos((x - size + 0.5f) * 0.5f, 3.0f);
116 weight *= lanczos((y - size + 0.5f) * 0.5f, 3.0f);
117 data[y * (size * 2) + x] = weight;
121 EffectChainTester tester(nullptr, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
124 format.color_space = COLORSPACE_sRGB;
125 format.gamma_curve = GAMMA_LINEAR;
127 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size * 2, size * 2);
128 input->set_pixel_data(data);
129 tester.get_chain()->add_input(input);
131 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
132 ASSERT_TRUE(resample_effect->set_int("width", size));
133 ASSERT_TRUE(resample_effect->set_int("height", size));
134 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
136 expect_equal(expected_data, out_data, size, size);
139 TEST(ResampleEffectTest, UpscaleByThreeGetsCorrectPixelCenters) {
142 float data[size * size] = {
143 0.0, 0.0, 0.0, 0.0, 0.0,
144 0.0, 0.0, 0.0, 0.0, 0.0,
145 0.0, 0.0, 1.0, 0.0, 0.0,
146 0.0, 0.0, 0.0, 0.0, 0.0,
147 0.0, 0.0, 0.0, 0.0, 0.0,
149 float out_data[size * size * 9];
151 EffectChainTester tester(nullptr, size * 3, size * 3, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
154 format.color_space = COLORSPACE_sRGB;
155 format.gamma_curve = GAMMA_LINEAR;
157 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size, size);
158 input->set_pixel_data(data);
159 tester.get_chain()->add_input(input);
161 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
162 ASSERT_TRUE(resample_effect->set_int("width", size * 3));
163 ASSERT_TRUE(resample_effect->set_int("height", size * 3));
164 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
166 // We only bother checking that the middle pixel is still correct,
167 // and that symmetry holds. Note that the middle weight in practice
168 // becomes something like 0.99999 due to the normalization
169 // (some supposedly zero weights become 1e-6 or so), and then after
170 // squaring, the error compounds. Ironically, less texture precision
171 // here will give a more accurate result, since the weight can get
172 // rounded towards 1.0.
173 EXPECT_NEAR(1.0, out_data[7 * (size * 3) + 7], 1e-3);
174 for (unsigned y = 0; y < size * 3; ++y) {
175 for (unsigned x = 0; x < size * 3; ++x) {
176 EXPECT_NEAR(out_data[y * (size * 3) + x], out_data[(size * 3 - y - 1) * (size * 3) + x], 1e-6);
177 EXPECT_NEAR(out_data[y * (size * 3) + x], out_data[y * (size * 3) + (size * 3 - x - 1)], 1e-6);
182 TEST(ResampleEffectTest, HeavyResampleGetsSumRight) {
183 // Do only one resample pass, more specifically the last one, which goes to
184 // our fp32 output. This allows us to analyze the precision without intermediate
186 const int swidth = 1, sheight = 1280;
187 const int dwidth = 1, dheight = 64;
189 float data[swidth * sheight], out_data[dwidth * dheight], expected_data[dwidth * dheight];
190 for (int y = 0; y < sheight; ++y) {
191 for (int x = 0; x < swidth; ++x) {
192 data[y * swidth + x] = 1.0f;
195 for (int y = 0; y < dheight; ++y) {
196 for (int x = 0; x < dwidth; ++x) {
197 expected_data[y * dwidth + x] = 1.0f;
201 EffectChainTester tester(nullptr, dwidth, dheight, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
204 format.color_space = COLORSPACE_sRGB;
205 format.gamma_curve = GAMMA_LINEAR;
207 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, swidth, sheight);
208 input->set_pixel_data(data);
210 tester.get_chain()->add_input(input);
211 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
212 ASSERT_TRUE(resample_effect->set_int("width", dwidth));
213 ASSERT_TRUE(resample_effect->set_int("height", dheight));
214 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
216 // Require that we are within 10-bit accuracy. Note that this limit is for
217 // one pass only, but the limit is tight enough that it should be good enough
218 // for 10-bit accuracy even after two passes.
219 expect_equal(expected_data, out_data, dwidth, dheight, 0.12 / 1023.0);
222 TEST(ResampleEffectTest, ReadWholePixelFromLeft) {
225 float data[size * size] = {
226 0.0, 0.0, 0.0, 0.0, 0.0,
227 0.0, 0.0, 0.0, 0.0, 0.0,
228 0.0, 0.0, 1.0, 0.0, 0.0,
229 0.0, 0.0, 0.0, 0.0, 0.0,
230 0.0, 0.0, 0.0, 0.0, 0.0,
232 float expected_data[size * size] = {
233 0.0, 0.0, 0.0, 0.0, 0.0,
234 0.0, 0.0, 0.0, 0.0, 0.0,
235 0.0, 1.0, 0.0, 0.0, 0.0,
236 0.0, 0.0, 0.0, 0.0, 0.0,
237 0.0, 0.0, 0.0, 0.0, 0.0,
239 float out_data[size * size];
241 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
242 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
243 ASSERT_TRUE(resample_effect->set_int("width", size));
244 ASSERT_TRUE(resample_effect->set_int("height", size));
245 ASSERT_TRUE(resample_effect->set_float("left", 1.0f));
246 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
248 expect_equal(expected_data, out_data, size, size);
251 TEST(ResampleEffectTest, ReadQuarterPixelFromLeft) {
254 float data[size * size] = {
255 0.0, 0.0, 0.0, 0.0, 0.0,
256 0.0, 0.0, 0.0, 0.0, 0.0,
257 0.0, 0.0, 1.0, 0.0, 0.0,
258 0.0, 0.0, 0.0, 0.0, 0.0,
259 0.0, 0.0, 0.0, 0.0, 0.0,
262 float expected_data[size * size] = {
263 0.0, 0.0, 0.0, 0.0, 0.0,
264 0.0, 0.0, 0.0, 0.0, 0.0,
266 // sin(x*pi)/(x*pi) * sin(x*pi/3)/(x*pi/3) for
267 // x = -1.75, -0.75, 0.25, 1.25, 2.25.
268 // Note that the weight is mostly on the left side.
269 -0.06779, 0.27019, 0.89007, -0.13287, 0.03002,
271 0.0, 0.0, 0.0, 0.0, 0.0,
272 0.0, 0.0, 0.0, 0.0, 0.0,
274 float out_data[size * size];
276 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
277 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
278 ASSERT_TRUE(resample_effect->set_int("width", size));
279 ASSERT_TRUE(resample_effect->set_int("height", size));
280 ASSERT_TRUE(resample_effect->set_float("left", 0.25f));
281 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
283 expect_equal(expected_data, out_data, size, size);
286 TEST(ResampleEffectTest, ReadQuarterPixelFromTop) {
288 const int height = 5;
290 float data[width * height] = {
298 // See ReadQuarterPixelFromLeft for explanation of the data.
299 float expected_data[width * height] = {
306 float out_data[width * height];
308 EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
309 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
310 ASSERT_TRUE(resample_effect->set_int("width", width));
311 ASSERT_TRUE(resample_effect->set_int("height", height));
312 ASSERT_TRUE(resample_effect->set_float("top", 0.25f));
313 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
315 expect_equal(expected_data, out_data, width, height);
318 TEST(ResampleEffectTest, ReadHalfPixelFromLeftAndScale) {
319 const int src_width = 4;
320 const int dst_width = 8;
322 float data[src_width * 1] = {
325 float expected_data[dst_width * 1] = {
326 // Empirical; the real test is that we are the same for 0.499 and 0.501.
327 1.1553, 1.7158, 2.2500, 2.7461, 3.2812, 3.8418, 4.0703, 4.0508
329 float out_data[dst_width * 1];
331 EffectChainTester tester(nullptr, dst_width, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
334 format.color_space = COLORSPACE_sRGB;
335 format.gamma_curve = GAMMA_LINEAR;
337 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, src_width, 1);
338 input->set_pixel_data(data);
339 tester.get_chain()->add_input(input);
341 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
342 ASSERT_TRUE(resample_effect->set_int("width", dst_width));
343 ASSERT_TRUE(resample_effect->set_int("height", 1));
345 // Check that we are (almost) the same no matter the rounding.
346 ASSERT_TRUE(resample_effect->set_float("left", 0.499f));
347 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
348 expect_equal(expected_data, out_data, dst_width, 1, 1.5f / 255.0f, 0.4f / 255.0f);
350 ASSERT_TRUE(resample_effect->set_float("left", 0.501f));
351 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
352 expect_equal(expected_data, out_data, dst_width, 1, 1.5f / 255.0f, 0.4f / 255.0f);
355 TEST(ResampleEffectTest, Zoom) {
357 const int height = 3;
359 float data[width * height] = {
360 0.0, 0.0, 0.0, 0.0, 0.0,
361 0.2, 0.4, 0.6, 0.4, 0.2,
362 0.0, 0.0, 0.0, 0.0, 0.0,
364 float expected_data[width * height] = {
365 0.0, 0.0, 0.0, 0.0, 0.0,
366 0.4, 0.5396, 0.6, 0.5396, 0.4,
367 0.0, 0.0, 0.0, 0.0, 0.0,
369 float out_data[width * height];
371 EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
372 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
373 ASSERT_TRUE(resample_effect->set_int("width", width));
374 ASSERT_TRUE(resample_effect->set_int("height", height));
375 ASSERT_TRUE(resample_effect->set_float("zoom_x", 2.0f));
376 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
378 expect_equal(expected_data, out_data, width, height);
381 TEST(ResampleEffectTest, VerticalZoomFromTop) {
383 const int height = 5;
385 float data[width * height] = {
386 0.2, 0.4, 0.6, 0.4, 0.2,
387 0.0, 0.0, 0.0, 0.0, 0.0,
388 0.0, 0.0, 0.0, 0.0, 0.0,
389 0.0, 0.0, 0.0, 0.0, 0.0,
390 0.0, 0.0, 0.0, 0.0, 0.0,
393 // Largely empirical data; the main point is that the top line
394 // is unchanged, since that's our zooming point.
395 float expected_data[width * height] = {
396 0.2000, 0.4000, 0.6000, 0.4000, 0.2000,
397 0.1389, 0.2778, 0.4167, 0.2778, 0.1389,
398 0.0600, 0.1199, 0.1798, 0.1199, 0.0600,
399 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
400 -0.0229, -0.0459, -0.0688, -0.0459, -0.0229,
402 float out_data[width * height];
404 EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
405 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
406 ASSERT_TRUE(resample_effect->set_int("width", width));
407 ASSERT_TRUE(resample_effect->set_int("height", height));
408 ASSERT_TRUE(resample_effect->set_float("zoom_y", 3.0f));
409 ASSERT_TRUE(resample_effect->set_float("zoom_center_y", 0.5f / height));
410 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
412 expect_equal(expected_data, out_data, width, height);
415 TEST(ResampleEffectTest, Precision) {
416 const int size = 1920; // Difficult non-power-of-two size.
417 const int offset = 5;
419 // Deliberately put the data of interest very close to the right,
420 // where texture coordinates are farther from 0 and thus less precise.
421 float data[size * 2] = {0};
422 data[size - offset] = 1.0f;
423 float expected_data[size * 2] = {0};
424 for (int x = 0; x < size * 2; ++x) {
425 expected_data[x] = lanczos((x - (size - 2 * offset + 1) + 0.5f) * 0.5f, 3.0f);
427 float out_data[size * 2];
429 EffectChainTester tester(data, size * 2, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
430 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
431 ASSERT_TRUE(resample_effect->set_int("width", size * 2));
432 ASSERT_TRUE(resample_effect->set_int("height", 1));
433 ASSERT_TRUE(resample_effect->set_float("zoom_x", 2.0f));
434 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
436 expect_equal(expected_data, out_data, size, 1);
439 #ifdef HAVE_BENCHMARK
440 template<> inline uint8_t from_fp32<uint8_t>(float x) { return lrintf(x * 255.0f); }
443 void BM_ResampleEffect(benchmark::State &state, GammaCurve gamma_curve, GLenum output_format, const std::string &shader_type)
445 DisableComputeShadersTemporarily disabler(shader_type == "fragment");
446 if (disabler.should_skip(&state)) return;
448 unsigned in_width = state.range(0), in_height = state.range(1);
449 unsigned out_width = state.range(2), out_height = state.range(3);
451 unique_ptr<T[]> data(new T[in_width * in_height * 4]);
452 unique_ptr<T[]> out_data(new T[out_width * out_height * 4]);
454 for (unsigned i = 0; i < in_width * in_height * 4; ++i) {
455 data[i] = from_fp32<T>(rand() / (RAND_MAX + 1.0));
458 EffectChainTester tester(nullptr, out_width, out_height, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, gamma_curve, output_format);
459 tester.add_input(data.get(), FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, gamma_curve, in_width, in_height);
460 Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
462 ASSERT_TRUE(resample_effect->set_int("width", out_width));
463 ASSERT_TRUE(resample_effect->set_int("height", out_height));
465 tester.benchmark(state, out_data.get(), GL_BGRA, COLORSPACE_sRGB, gamma_curve, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
468 void BM_ResampleEffectHalf(benchmark::State &state, GammaCurve gamma_curve, const std::string &shader_type)
470 BM_ResampleEffect<fp16_int_t>(state, gamma_curve, GL_RGBA16F, shader_type);
473 void BM_ResampleEffectInt8(benchmark::State &state, GammaCurve gamma_curve, const std::string &shader_type)
475 BM_ResampleEffect<uint8_t>(state, gamma_curve, GL_RGBA8, shader_type);
478 BENCHMARK_CAPTURE(BM_ResampleEffectInt8, Int8Upscale, GAMMA_REC_709, "fragment")->Args({640, 360, 1280, 720})->Args({320, 180, 1280, 720})->Args({321, 181, 1280, 720})->UseRealTime()->Unit(benchmark::kMicrosecond);
479 BENCHMARK_CAPTURE(BM_ResampleEffectHalf, Float16Upscale, GAMMA_LINEAR, "fragment")->Args({640, 360, 1280, 720})->Args({320, 180, 1280, 720})->Args({321, 181, 1280, 720})->UseRealTime()->Unit(benchmark::kMicrosecond);
480 BENCHMARK_CAPTURE(BM_ResampleEffectInt8, Int8Downscale, GAMMA_REC_709, "fragment")->Args({1280, 720, 640, 360})->Args({1280, 720, 320, 180})->Args({1280, 720, 321, 181})->UseRealTime()->Unit(benchmark::kMicrosecond);
481 BENCHMARK_CAPTURE(BM_ResampleEffectHalf, Float16Downscale, GAMMA_LINEAR, "fragment")->Args({1280, 720, 640, 360})->Args({1280, 720, 320, 180})->Args({1280, 720, 321, 181})->UseRealTime()->Unit(benchmark::kMicrosecond);
483 void BM_ComputeBilinearScalingWeights(benchmark::State &state)
485 constexpr unsigned src_size = 1280;
486 constexpr unsigned dst_size = 35;
487 int old_precision = movit_texel_subpixel_precision;
488 movit_texel_subpixel_precision = 64; // To get consistent results across GPUs; this is a CPU test.
490 // One iteration warmup to make sure the Lanczos table is computed.
491 calculate_bilinear_scaling_weights(src_size, dst_size, 0.999f, 0.0f);
493 for (auto _ : state) {
494 ScalingWeights weights = calculate_bilinear_scaling_weights(src_size, dst_size, 0.999f, 0.0f);
497 movit_texel_subpixel_precision = old_precision;
499 BENCHMARK(BM_ComputeBilinearScalingWeights)->Unit(benchmark::kMicrosecond);