]> git.sesse.net Git - movit/blob - resample_effect_test.cpp
7955c5ba99f480a94f048385f1697ffa0fb09091
[movit] / resample_effect_test.cpp
1 // Unit tests for ResampleEffect.
2
3 #include <epoxy/gl.h>
4 #include <gtest/gtest.h>
5 #include <math.h>
6
7 #include <memory>
8
9 #include "effect_chain.h"
10 #include "flat_input.h"
11 #include "image_format.h"
12 #include "resample_effect.h"
13 #include "test_util.h"
14
15 using namespace std;
16
17 namespace movit {
18
19 namespace {
20
21 float sinc(float x)
22 {
23         return sin(M_PI * x) / (M_PI * x);
24 }
25
26 float lanczos(float x, float a)
27 {
28         if (fabs(x) >= a) {
29                 return 0.0f;
30         } else {
31                 return sinc(x) * sinc(x / a);
32         }
33 }
34
35 }  // namespace
36
37 TEST(ResampleEffectTest, IdentityTransformDoesNothing) {
38         const int size = 4;
39
40         float data[size * size] = {
41                 0.0, 1.0, 0.0, 1.0,
42                 0.0, 1.0, 1.0, 0.0,
43                 0.0, 0.5, 1.0, 0.5,
44                 0.0, 0.0, 0.0, 0.0,
45         };
46         float out_data[size * size];
47
48         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
49         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
50         ASSERT_TRUE(resample_effect->set_int("width", 4));
51         ASSERT_TRUE(resample_effect->set_int("height", 4));
52         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
53
54         expect_equal(data, out_data, size, size);
55 }
56
57 TEST(ResampleEffectTest, UpscaleByTwoGetsCorrectPixelCenters) {
58         const int size = 5;
59
60         float data[size * size] = {
61                 0.0, 0.0, 0.0, 0.0, 0.0,
62                 0.0, 0.0, 0.0, 0.0, 0.0,
63                 0.0, 0.0, 1.0, 0.0, 0.0,
64                 0.0, 0.0, 0.0, 0.0, 0.0,
65                 0.0, 0.0, 0.0, 0.0, 0.0,
66         };
67         float expected_data[size * size * 4], out_data[size * size * 4];
68
69         for (int y = 0; y < size * 2; ++y) {
70                 for (int x = 0; x < size * 2; ++x) {
71                         float weight = lanczos((x - size + 0.5f) * 0.5f, 3.0f);
72                         weight *= lanczos((y - size + 0.5f) * 0.5f, 3.0f);
73                         expected_data[y * (size * 2) + x] = weight;
74                 }
75         }
76
77         EffectChainTester tester(nullptr, size * 2, size * 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
78
79         ImageFormat format;
80         format.color_space = COLORSPACE_sRGB;
81         format.gamma_curve = GAMMA_LINEAR;
82
83         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size, size);
84         input->set_pixel_data(data);
85         tester.get_chain()->add_input(input);
86
87         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
88         ASSERT_TRUE(resample_effect->set_int("width", size * 2));
89         ASSERT_TRUE(resample_effect->set_int("height", size * 2));
90         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
91
92         expect_equal(expected_data, out_data, size * 2, size * 2);
93 }
94
95 TEST(ResampleEffectTest, DownscaleByTwoGetsCorrectPixelCenters) {
96         const int size = 5;
97
98         // This isn't a perfect dot, since the Lanczos filter has a slight
99         // sharpening effect; the most important thing is that we have kept
100         // the texel center right (everything is nicely symmetric).
101         // The approximate magnitudes have been checked against ImageMagick.
102         float expected_data[size * size] = {
103                  0.0045, -0.0067, -0.0599, -0.0067,  0.0045,
104                 -0.0067,  0.0100,  0.0892,  0.0100, -0.0067,
105                 -0.0599,  0.0890,  0.7925,  0.0892, -0.0599,
106                 -0.0067,  0.0100,  0.0890,  0.0100, -0.0067,
107                  0.0045, -0.0067, -0.0599, -0.0067,  0.0045,
108         };
109         float data[size * size * 4], out_data[size * size];
110
111         for (int y = 0; y < size * 2; ++y) {
112                 for (int x = 0; x < size * 2; ++x) {
113                         float weight = lanczos((x - size + 0.5f) * 0.5f, 3.0f);
114                         weight *= lanczos((y - size + 0.5f) * 0.5f, 3.0f);
115                         data[y * (size * 2) + x] = weight;
116                 }
117         }
118
119         EffectChainTester tester(nullptr, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
120
121         ImageFormat format;
122         format.color_space = COLORSPACE_sRGB;
123         format.gamma_curve = GAMMA_LINEAR;
124
125         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size * 2, size * 2);
126         input->set_pixel_data(data);
127         tester.get_chain()->add_input(input);
128
129         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
130         ASSERT_TRUE(resample_effect->set_int("width", size));
131         ASSERT_TRUE(resample_effect->set_int("height", size));
132         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
133
134         expect_equal(expected_data, out_data, size, size);
135 }
136
137 TEST(ResampleEffectTest, UpscaleByThreeGetsCorrectPixelCenters) {
138         const int size = 5;
139
140         float data[size * size] = {
141                 0.0, 0.0, 0.0, 0.0, 0.0,
142                 0.0, 0.0, 0.0, 0.0, 0.0,
143                 0.0, 0.0, 1.0, 0.0, 0.0,
144                 0.0, 0.0, 0.0, 0.0, 0.0,
145                 0.0, 0.0, 0.0, 0.0, 0.0,
146         };
147         float out_data[size * size * 9];
148
149         EffectChainTester tester(nullptr, size * 3, size * 3, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
150
151         ImageFormat format;
152         format.color_space = COLORSPACE_sRGB;
153         format.gamma_curve = GAMMA_LINEAR;
154
155         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, size, size);
156         input->set_pixel_data(data);
157         tester.get_chain()->add_input(input);
158
159         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
160         ASSERT_TRUE(resample_effect->set_int("width", size * 3));
161         ASSERT_TRUE(resample_effect->set_int("height", size * 3));
162         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
163
164         // We only bother checking that the middle pixel is still correct,
165         // and that symmetry holds.
166         EXPECT_FLOAT_EQ(1.0, out_data[7 * (size * 3) + 7]);
167         for (unsigned y = 0; y < size * 3; ++y) {
168                 for (unsigned x = 0; x < size * 3; ++x) {
169                         EXPECT_NEAR(out_data[y * (size * 3) + x], out_data[(size * 3 - y - 1) * (size * 3) + x], 1e-6);
170                         EXPECT_NEAR(out_data[y * (size * 3) + x], out_data[y * (size * 3) + (size * 3 - x - 1)], 1e-6);
171                 }
172         }
173 }
174
175 TEST(ResampleEffectTest, HeavyResampleGetsSumRight) {
176         // Do only one resample pass, more specifically the last one, which goes to
177         // our fp32 output. This allows us to analyze the precision without intermediate
178         // fp16 rounding.
179         const int swidth = 1, sheight = 1280;
180         const int dwidth = 1, dheight = 64;
181
182         float data[swidth * sheight], out_data[dwidth * dheight], expected_data[dwidth * dheight];
183         for (int y = 0; y < sheight; ++y) {
184                 for (int x = 0; x < swidth; ++x) {
185                         data[y * swidth + x] = 1.0f;
186                 }
187         }
188         for (int y = 0; y < dheight; ++y) {
189                 for (int x = 0; x < dwidth; ++x) {
190                         expected_data[y * dwidth + x] = 1.0f;
191                 }
192         }
193
194         EffectChainTester tester(nullptr, dwidth, dheight, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
195
196         ImageFormat format;
197         format.color_space = COLORSPACE_sRGB;
198         format.gamma_curve = GAMMA_LINEAR;
199
200         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, swidth, sheight);
201         input->set_pixel_data(data);
202
203         tester.get_chain()->add_input(input);
204         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
205         ASSERT_TRUE(resample_effect->set_int("width", dwidth));
206         ASSERT_TRUE(resample_effect->set_int("height", dheight));
207         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
208
209         // Require that we are within 10-bit accuracy. Note that this limit is for
210         // one pass only, but the limit is tight enough that it should be good enough
211         // for 10-bit accuracy even after two passes.
212         expect_equal(expected_data, out_data, dwidth, dheight, 0.12 / 1023.0);
213 }
214
215 TEST(ResampleEffectTest, ReadWholePixelFromLeft) {
216         const int size = 5;
217
218         float data[size * size] = {
219                 0.0, 0.0, 0.0, 0.0, 0.0,
220                 0.0, 0.0, 0.0, 0.0, 0.0,
221                 0.0, 0.0, 1.0, 0.0, 0.0,
222                 0.0, 0.0, 0.0, 0.0, 0.0,
223                 0.0, 0.0, 0.0, 0.0, 0.0,
224         };
225         float expected_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, 1.0, 0.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,
231         };
232         float out_data[size * size];
233
234         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
235         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
236         ASSERT_TRUE(resample_effect->set_int("width", size));
237         ASSERT_TRUE(resample_effect->set_int("height", size));
238         ASSERT_TRUE(resample_effect->set_float("left", 1.0f));
239         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
240
241         expect_equal(expected_data, out_data, size, size);
242 }
243
244 TEST(ResampleEffectTest, ReadQuarterPixelFromLeft) {
245         const int size = 5;
246
247         float data[size * size] = {
248                 0.0, 0.0, 0.0, 0.0, 0.0,
249                 0.0, 0.0, 0.0, 0.0, 0.0,
250                 0.0, 0.0, 1.0, 0.0, 0.0,
251                 0.0, 0.0, 0.0, 0.0, 0.0,
252                 0.0, 0.0, 0.0, 0.0, 0.0,
253         };
254
255         float expected_data[size * size] = {
256                 0.0, 0.0, 0.0, 0.0, 0.0,
257                 0.0, 0.0, 0.0, 0.0, 0.0,
258
259                 // sin(x*pi)/(x*pi) * sin(x*pi/3)/(x*pi/3) for
260                 // x = -1.75, -0.75, 0.25, 1.25, 2.25.
261                 // Note that the weight is mostly on the left side.
262                 -0.06779, 0.27019, 0.89007, -0.13287, 0.03002,
263
264                 0.0, 0.0, 0.0, 0.0, 0.0,
265                 0.0, 0.0, 0.0, 0.0, 0.0,
266         };
267         float out_data[size * size];
268
269         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
270         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
271         ASSERT_TRUE(resample_effect->set_int("width", size));
272         ASSERT_TRUE(resample_effect->set_int("height", size));
273         ASSERT_TRUE(resample_effect->set_float("left", 0.25f));
274         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
275
276         expect_equal(expected_data, out_data, size, size);
277 }
278
279 TEST(ResampleEffectTest, ReadQuarterPixelFromTop) {
280         const int width = 3;
281         const int height = 5;
282
283         float data[width * height] = {
284                 0.0, 0.0, 0.0,
285                 0.0, 0.0, 0.0,
286                 1.0, 0.0, 0.0,
287                 0.0, 0.0, 0.0,
288                 0.0, 0.0, 0.0,
289         };
290
291         // See ReadQuarterPixelFromLeft for explanation of the data.
292         float expected_data[width * height] = {
293                 -0.06779, 0.0, 0.0,
294                  0.27019, 0.0, 0.0,
295                  0.89007, 0.0, 0.0,
296                 -0.13287, 0.0, 0.0,
297                  0.03002, 0.0, 0.0,
298         };
299         float out_data[width * height];
300
301         EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
302         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
303         ASSERT_TRUE(resample_effect->set_int("width", width));
304         ASSERT_TRUE(resample_effect->set_int("height", height));
305         ASSERT_TRUE(resample_effect->set_float("top", 0.25f));
306         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
307
308         expect_equal(expected_data, out_data, width, height);
309 }
310
311 TEST(ResampleEffectTest, ReadHalfPixelFromLeftAndScale) {
312         const int src_width = 4;
313         const int dst_width = 8;
314
315         float data[src_width * 1] = {
316                 1.0, 2.0, 3.0, 4.0,
317         };
318         float expected_data[dst_width * 1] = {
319                 // Empirical; the real test is that we are the same for 0.499 and 0.501.
320                 1.1553, 1.7158, 2.2500, 2.7461, 3.2812, 3.8418, 4.0703, 4.0508
321         };
322         float out_data[dst_width * 1];
323
324         EffectChainTester tester(nullptr, dst_width, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
325
326         ImageFormat format;
327         format.color_space = COLORSPACE_sRGB;
328         format.gamma_curve = GAMMA_LINEAR;
329
330         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, src_width, 1);
331         input->set_pixel_data(data);
332         tester.get_chain()->add_input(input);
333
334         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
335         ASSERT_TRUE(resample_effect->set_int("width", dst_width));
336         ASSERT_TRUE(resample_effect->set_int("height", 1));
337
338         // Check that we are (almost) the same no matter the rounding.
339         ASSERT_TRUE(resample_effect->set_float("left", 0.499f));
340         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
341         expect_equal(expected_data, out_data, dst_width, 1, 1.5f / 255.0f, 0.4f / 255.0f);
342
343         ASSERT_TRUE(resample_effect->set_float("left", 0.501f));
344         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
345         expect_equal(expected_data, out_data, dst_width, 1, 1.5f / 255.0f, 0.4f / 255.0f);
346 }
347
348 TEST(ResampleEffectTest, Zoom) {
349         const int width = 5;
350         const int height = 3;
351
352         float data[width * height] = {
353                 0.0, 0.0, 0.0, 0.0, 0.0,
354                 0.2, 0.4, 0.6, 0.4, 0.2,
355                 0.0, 0.0, 0.0, 0.0, 0.0,
356         };
357         float expected_data[width * height] = {
358                 0.0, 0.0,    0.0, 0.0,    0.0,
359                 0.4, 0.5396, 0.6, 0.5396, 0.4,
360                 0.0, 0.0,    0.0, 0.0,    0.0,
361         };
362         float out_data[width * height];
363
364         EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
365         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
366         ASSERT_TRUE(resample_effect->set_int("width", width));
367         ASSERT_TRUE(resample_effect->set_int("height", height));
368         ASSERT_TRUE(resample_effect->set_float("zoom_x", 2.0f));
369         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
370
371         expect_equal(expected_data, out_data, width, height);
372 }
373
374 TEST(ResampleEffectTest, VerticalZoomFromTop) {
375         const int width = 5;
376         const int height = 5;
377
378         float data[width * height] = {
379                 0.2, 0.4, 0.6, 0.4, 0.2,
380                 0.0, 0.0, 0.0, 0.0, 0.0,
381                 0.0, 0.0, 0.0, 0.0, 0.0,
382                 0.0, 0.0, 0.0, 0.0, 0.0,
383                 0.0, 0.0, 0.0, 0.0, 0.0,
384         };
385
386         // Largely empirical data; the main point is that the top line
387         // is unchanged, since that's our zooming point.
388         float expected_data[width * height] = {
389                  0.2000,  0.4000,  0.6000,  0.4000,  0.2000,
390                  0.1389,  0.2778,  0.4167,  0.2778,  0.1389,
391                  0.0600,  0.1199,  0.1798,  0.1199,  0.0600,
392                  0.0000,  0.0000,  0.0000,  0.0000,  0.0000,
393                 -0.0229, -0.0459, -0.0688, -0.0459, -0.0229,
394         };
395         float out_data[width * height];
396
397         EffectChainTester tester(data, width, height, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
398         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
399         ASSERT_TRUE(resample_effect->set_int("width", width));
400         ASSERT_TRUE(resample_effect->set_int("height", height));
401         ASSERT_TRUE(resample_effect->set_float("zoom_y", 3.0f));
402         ASSERT_TRUE(resample_effect->set_float("zoom_center_y", 0.5f / height));
403         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
404
405         expect_equal(expected_data, out_data, width, height);
406 }
407
408 TEST(ResampleEffectTest, Precision) {
409         const int size = 1920;  // Difficult non-power-of-two size.
410         const int offset = 5;
411
412         // Deliberately put the data of interest very close to the right,
413         // where texture coordinates are farther from 0 and thus less precise.
414         float data[size * 2] = {0};
415         data[size - offset] = 1.0f;
416         float expected_data[size * 2] = {0};
417         for (int x = 0; x < size * 2; ++x) {
418                 expected_data[x] = lanczos((x - (size - 2 * offset + 1) + 0.5f) * 0.5f, 3.0f);
419         }
420         float out_data[size * 2];
421
422         EffectChainTester tester(data, size * 2, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
423         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
424         ASSERT_TRUE(resample_effect->set_int("width", size * 2));
425         ASSERT_TRUE(resample_effect->set_int("height", 1));
426         ASSERT_TRUE(resample_effect->set_float("zoom_x", 2.0f));
427         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
428
429         expect_equal(expected_data, out_data, size, 1);
430 }
431
432 #ifdef HAVE_BENCHMARK
433 template<class T>
434 void BM_ResampleEffect(benchmark::State &state, GammaCurve gamma_curve, GLenum output_format, const std::string &shader_type)
435 {
436         DisableComputeShadersTemporarily disabler(shader_type == "fragment");
437         if (disabler.should_skip(&state)) return;
438
439         unsigned in_width = state.range(0), in_height = state.range(1);
440         unsigned out_width = state.range(2), out_height = state.range(3);
441
442         unique_ptr<T[]> data(new T[in_width * in_height * 4]);
443         unique_ptr<T[]> out_data(new T[out_width * out_height * 4]);
444
445         for (unsigned i = 0; i < in_width * in_height * 4; ++i) {
446                 data[i] = rand();
447         }
448
449         EffectChainTester tester(nullptr, out_width, out_height, FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, gamma_curve, output_format);
450         tester.add_input(data.get(), FORMAT_BGRA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, gamma_curve, in_width, in_height);
451         Effect *resample_effect = tester.get_chain()->add_effect(new ResampleEffect());
452
453         ASSERT_TRUE(resample_effect->set_int("width", out_width));
454         ASSERT_TRUE(resample_effect->set_int("height", out_height));
455
456         tester.benchmark(state, out_data.get(), GL_BGRA, COLORSPACE_sRGB, gamma_curve, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
457 }
458
459 void BM_ResampleEffectFloat(benchmark::State &state, GammaCurve gamma_curve, const std::string &shader_type)
460 {
461         BM_ResampleEffect<float>(state, gamma_curve, GL_RGBA16F, shader_type);
462 }
463
464 void BM_ResampleEffectInt8(benchmark::State &state, GammaCurve gamma_curve, const std::string &shader_type)
465 {
466         BM_ResampleEffect<uint8_t>(state, gamma_curve, GL_RGBA8, shader_type);
467 }
468
469 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);
470 BENCHMARK_CAPTURE(BM_ResampleEffectFloat, Float32Upscale, GAMMA_LINEAR, "fragment")->Args({640, 360, 1280, 720})->Args({320, 180, 1280, 720})->Args({321, 181, 1280, 720})->UseRealTime()->Unit(benchmark::kMicrosecond);
471 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);
472 BENCHMARK_CAPTURE(BM_ResampleEffectFloat, Float32Downscale, GAMMA_LINEAR, "fragment")->Args({1280, 720, 640, 360})->Args({1280, 720, 320, 180})->Args({1280, 720, 321, 181})->UseRealTime()->Unit(benchmark::kMicrosecond);
473
474 #endif
475
476 }  // namespace movit