Defer fetching inputs' color spaces and gamma to finalize().
[movit] / effect_chain_test.cpp
1 // Unit tests for EffectChain.
2 //
3 // Note that this also contains the tests for some of the simpler effects.
4
5 #include <GL/glew.h>
6
7 #include "effect_chain.h"
8 #include "flat_input.h"
9 #include "gtest/gtest.h"
10 #include "mirror_effect.h"
11 #include "resize_effect.h"
12 #include "test_util.h"
13
14 TEST(EffectChainTest, EmptyChain) {
15         float data[] = {
16                 0.0f, 0.25f, 0.3f,
17                 0.75f, 1.0f, 1.0f,
18         };
19         float out_data[6];
20         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
21         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
22
23         expect_equal(data, out_data, 3, 2);
24 }
25
26 // An effect that does nothing.
27 class IdentityEffect : public Effect {
28 public:
29         IdentityEffect() {}
30         virtual std::string effect_type_id() const { return "IdentityEffect"; }
31         std::string output_fragment_shader() { return read_file("identity.frag"); }
32 };
33
34 TEST(EffectChainTest, Identity) {
35         float data[] = {
36                 0.0f, 0.25f, 0.3f,
37                 0.75f, 1.0f, 1.0f,
38         };
39         float out_data[6];
40         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
41         tester.get_chain()->add_effect(new IdentityEffect());
42         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
43
44         expect_equal(data, out_data, 3, 2);
45 }
46
47 // An effect that does nothing, but requests texture bounce.
48 class BouncingIdentityEffect : public Effect {
49 public:
50         BouncingIdentityEffect() {}
51         virtual std::string effect_type_id() const { return "IdentityEffect"; }
52         std::string output_fragment_shader() { return read_file("identity.frag"); }
53         bool needs_texture_bounce() const { return true; }
54 };
55
56 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
57         float data[] = {
58                 0.0f, 0.25f, 0.3f,
59                 0.75f, 1.0f, 1.0f,
60         };
61         float out_data[6];
62         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
63         tester.get_chain()->add_effect(new BouncingIdentityEffect());
64         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
65
66         expect_equal(data, out_data, 3, 2);
67 }
68
69 TEST(MirrorTest, BasicTest) {
70         float data[] = {
71                 0.0f, 0.25f, 0.3f,
72                 0.75f, 1.0f, 1.0f,
73         };
74         float expected_data[6] = {
75                 0.3f, 0.25f, 0.0f,
76                 1.0f, 1.0f, 0.75f,
77         };
78         float out_data[6];
79         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
80         tester.get_chain()->add_effect(new MirrorEffect());
81         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
82
83         expect_equal(expected_data, out_data, 3, 2);
84 }
85
86 // A dummy effect that inverts its input.
87 class InvertEffect : public Effect {
88 public:
89         InvertEffect() {}
90         virtual std::string effect_type_id() const { return "InvertEffect"; }
91         std::string output_fragment_shader() { return read_file("invert_effect.frag"); }
92 };
93
94 // Like IdentityEffect, but rewrites itself out of the loop,
95 // splicing in a InvertEffect instead. Also stores the new node,
96 // so we later can check that there are gamma conversion effects
97 // on both sides.
98 class RewritingToInvertEffect : public Effect {
99 public:
100         RewritingToInvertEffect() {}
101         virtual std::string effect_type_id() const { return "RewritingToInvertEffect"; }
102         std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
103         virtual void rewrite_graph(EffectChain *graph, Node *self) {
104                 Node *invert_node = graph->add_node(new InvertEffect());
105                 graph->replace_receiver(self, invert_node);
106                 graph->replace_sender(self, invert_node);
107
108                 self->disabled = true;
109                 this->invert_node = invert_node;
110         }
111
112         Node *invert_node;      
113 };
114
115 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
116         float data[] = {
117                 0.0f, 0.25f, 0.3f,
118                 0.75f, 1.0f, 1.0f,
119         };
120         float expected_data[6] = {
121                 1.0f, 0.9771f, 0.9673f,
122                 0.7192f, 0.0f, 0.0f,
123         };
124         float out_data[6];
125         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
126         RewritingToInvertEffect *effect = new RewritingToInvertEffect();
127         tester.get_chain()->add_effect(effect);
128         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
129
130         Node *node = effect->invert_node;
131         ASSERT_EQ(1, node->incoming_links.size());
132         ASSERT_EQ(1, node->outgoing_links.size());
133         EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
134         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
135
136         expect_equal(expected_data, out_data, 3, 2);
137 }
138
139 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
140         unsigned char data[] = {
141                 0, 64,
142                 128, 255,
143         };
144         float expected_data[4] = {
145                 1.0f, 0.9771f,
146                 0.8983f, 0.0f,
147         };
148         float out_data[2];
149         EffectChainTester tester(NULL, 2, 2);
150         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
151         RewritingToInvertEffect *effect = new RewritingToInvertEffect();
152         tester.get_chain()->add_effect(effect);
153         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
154
155         Node *node = effect->invert_node;
156         ASSERT_EQ(1, node->incoming_links.size());
157         ASSERT_EQ(1, node->outgoing_links.size());
158         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
159         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
160
161         expect_equal(expected_data, out_data, 2, 2);
162 }
163
164 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
165         float data[] = {
166                 0.0f, 0.25f, 0.3f,
167                 0.75f, 1.0f, 1.0f,
168         };
169         float expected_data[6] = {
170                 1.0f, 0.75f, 0.7f,
171                 0.25f, 0.0f, 0.0f,
172         };
173         float out_data[6];
174         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
175         RewritingToInvertEffect *effect = new RewritingToInvertEffect();
176         tester.get_chain()->add_effect(effect);
177         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
178
179         Node *node = effect->invert_node;
180         ASSERT_EQ(1, node->incoming_links.size());
181         ASSERT_EQ(1, node->outgoing_links.size());
182         EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
183         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
184
185         expect_equal(expected_data, out_data, 3, 2);
186 }
187
188 // A fake input that can change its output colorspace and gamma between instantiation
189 // and finalize.
190 class UnknownColorspaceInput : public FlatInput {
191 public:
192         UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
193             : FlatInput(format, pixel_format, type, width, height),
194               overridden_color_space(format.color_space),
195               overridden_gamma_curve(format.gamma_curve) {}
196         virtual std::string effect_type_id() const { return "UnknownColorspaceInput"; }
197
198         void set_color_space(Colorspace colorspace) {
199                 overridden_color_space = colorspace;
200         }
201         void set_gamma_curve(GammaCurve gamma_curve) {
202                 overridden_gamma_curve = gamma_curve;
203         }
204         Colorspace get_color_space() const { return overridden_color_space; }
205         GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
206
207 private:
208         Colorspace overridden_color_space;
209         GammaCurve overridden_gamma_curve;
210 };
211
212 TEST(EffectChainTester, HandlesInputChangingColorspace) {
213         const int size = 4;
214
215         float data[size] = {
216                 0.0,
217                 0.5,
218                 0.7,
219                 1.0,
220         };
221         float out_data[size];
222
223         EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
224
225         // First say that we have sRGB, linear input.
226         ImageFormat format;
227         format.color_space = COLORSPACE_sRGB;
228         format.gamma_curve = GAMMA_LINEAR;
229
230         UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
231         input->set_pixel_data(data);
232         tester.get_chain()->add_input(input);
233
234         // Now we change to Rec. 601 input.
235         input->set_color_space(COLORSPACE_REC_601_625);
236         input->set_gamma_curve(GAMMA_REC_601);
237
238         // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
239         tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
240         expect_equal(data, out_data, 4, 1);
241 }
242
243 // Like RewritingToInvertEffect, but splicing in a MirrorEffect instead,
244 // which does not need linear light or sRGB primaries.
245 class RewritingToMirrorEffect : public Effect {
246 public:
247         RewritingToMirrorEffect() {}
248         virtual std::string effect_type_id() const { return "RewritingToMirrorEffect"; }
249         std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
250         virtual void rewrite_graph(EffectChain *graph, Node *self) {
251                 Node *mirror_node = graph->add_node(new MirrorEffect());
252                 graph->replace_receiver(self, mirror_node);
253                 graph->replace_sender(self, mirror_node);
254
255                 self->disabled = true;
256                 this->mirror_node = mirror_node;
257         }
258
259         Node *mirror_node;
260 };
261
262 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
263         float data[] = {
264                 0.0f, 0.25f, 0.3f,
265                 0.75f, 1.0f, 1.0f,
266         };
267         float expected_data[6] = {
268                 0.3f, 0.25f, 0.0f,
269                 1.0f, 1.0f, 0.75f,
270         };
271         float out_data[6];
272         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
273         RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
274         tester.get_chain()->add_effect(effect);
275         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
276
277         Node *node = effect->mirror_node;
278         ASSERT_EQ(1, node->incoming_links.size());
279         EXPECT_EQ(0, node->outgoing_links.size());
280         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
281
282         expect_equal(expected_data, out_data, 3, 2);
283 }
284
285 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
286         float data[] = {
287                 0.0f, 0.25f, 0.3f,
288                 0.75f, 1.0f, 1.0f,
289         };
290         float expected_data[6] = {
291                 0.3f, 0.25f, 0.0f,
292                 1.0f, 1.0f, 0.75f,
293         };
294         float out_data[6];
295         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
296         RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
297         tester.get_chain()->add_effect(effect);
298         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
299
300         Node *node = effect->mirror_node;
301         ASSERT_EQ(1, node->incoming_links.size());
302         EXPECT_EQ(0, node->outgoing_links.size());
303         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
304
305         expect_equal(expected_data, out_data, 3, 2);
306 }
307
308 // The identity effect needs linear light, and thus will get conversions on both sides.
309 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
310 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
311         float data[256];
312         for (unsigned i = 0; i < 256; ++i) {
313                 data[i] = i / 255.0;
314         };
315         float out_data[256];
316         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
317         tester.get_chain()->add_effect(new IdentityEffect());
318         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
319
320         expect_equal(data, out_data, 256, 1);
321 }
322
323 // Same, but uses the forward sRGB table from the GPU.
324 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
325         unsigned char data[256];
326         float expected_data[256];
327         for (unsigned i = 0; i < 256; ++i) {
328                 data[i] = i;
329                 expected_data[i] = i / 255.0;
330         };
331         float out_data[256];
332         EffectChainTester tester(NULL, 256, 1);
333         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
334         tester.get_chain()->add_effect(new IdentityEffect());
335         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
336
337         expect_equal(expected_data, out_data, 256, 1);
338 }
339
340 // Same, for the Rec. 601/709 gamma curve.
341 TEST(EffectChainTest, IdentityThroughRec709) {
342         float data[256];
343         for (unsigned i = 0; i < 256; ++i) {
344                 data[i] = i / 255.0;
345         };
346         float out_data[256];
347         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
348         tester.get_chain()->add_effect(new IdentityEffect());
349         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
350
351         expect_equal(data, out_data, 256, 1);
352 }
353
354 // Effectively scales down its input linearly by 4x (and repeating it),
355 // which is not attainable without mipmaps.
356 class MipmapNeedingEffect : public Effect {
357 public:
358         MipmapNeedingEffect() {}
359         virtual bool needs_mipmaps() const { return true; }
360         virtual std::string effect_type_id() const { return "MipmapNeedingEffect"; }
361         std::string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
362         void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num)
363         {
364                 glActiveTexture(GL_TEXTURE0);
365                 check_error();
366                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
367                 check_error();
368                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
369                 check_error();
370         }
371 };
372
373 TEST(EffectChainTest, MipmapGenerationWorks) {
374         float data[] = {  // In 4x4 blocks.
375                 1.0f, 0.0f, 0.0f, 0.0f,
376                 0.0f, 0.0f, 0.0f, 0.0f,
377                 0.0f, 0.0f, 0.0f, 0.0f,
378                 0.0f, 0.0f, 0.0f, 1.0f,
379
380                 0.0f, 0.0f, 0.0f, 0.0f,
381                 0.0f, 0.5f, 0.0f, 0.0f,
382                 0.0f, 0.0f, 1.0f, 0.0f,
383                 0.0f, 0.0f, 0.0f, 0.0f,
384
385                 1.0f, 1.0f, 1.0f, 1.0f,
386                 1.0f, 1.0f, 1.0f, 1.0f,
387                 1.0f, 1.0f, 1.0f, 1.0f,
388                 1.0f, 1.0f, 1.0f, 1.0f,
389
390                 0.0f, 0.0f, 0.0f, 0.0f,
391                 0.0f, 1.0f, 1.0f, 0.0f,
392                 0.0f, 1.0f, 1.0f, 0.0f,
393                 0.0f, 0.0f, 0.0f, 0.0f,
394         };
395         float expected_data[] = {  // Repeated four times each way.
396                 0.125f,   0.125f,   0.125f,   0.125f,
397                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
398                 1.0f,     1.0f,     1.0f,     1.0f,
399                 0.25f,    0.25f,    0.25f,    0.25f,
400
401                 0.125f,   0.125f,   0.125f,   0.125f,
402                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
403                 1.0f,     1.0f,     1.0f,     1.0f,
404                 0.25f,    0.25f,    0.25f,    0.25f,
405
406                 0.125f,   0.125f,   0.125f,   0.125f,
407                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
408                 1.0f,     1.0f,     1.0f,     1.0f,
409                 0.25f,    0.25f,    0.25f,    0.25f,
410
411                 0.125f,   0.125f,   0.125f,   0.125f,
412                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
413                 1.0f,     1.0f,     1.0f,     1.0f,
414                 0.25f,    0.25f,    0.25f,    0.25f,
415         };
416         float out_data[4 * 16];
417         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
418         tester.get_chain()->add_effect(new MipmapNeedingEffect());
419         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
420
421         expect_equal(expected_data, out_data, 4, 16);
422 }
423
424 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
425         float data[] = {  // In 4x4 blocks.
426                 1.0f, 0.0f, 0.0f, 0.0f,
427                 0.0f, 0.0f, 0.0f, 0.0f,
428                 0.0f, 0.0f, 0.0f, 0.0f,
429                 0.0f, 0.0f, 0.0f, 1.0f,
430
431                 0.0f, 0.0f, 0.0f, 0.0f,
432                 0.0f, 0.5f, 0.0f, 0.0f,
433                 0.0f, 0.0f, 1.0f, 0.0f,
434                 0.0f, 0.0f, 0.0f, 0.0f,
435
436                 1.0f, 1.0f, 1.0f, 1.0f,
437                 1.0f, 1.0f, 1.0f, 1.0f,
438                 1.0f, 1.0f, 1.0f, 1.0f,
439                 1.0f, 1.0f, 1.0f, 1.0f,
440
441                 0.0f, 0.0f, 0.0f, 0.0f,
442                 0.0f, 1.0f, 1.0f, 0.0f,
443                 0.0f, 1.0f, 1.0f, 0.0f,
444                 0.0f, 0.0f, 0.0f, 0.0f,
445         };
446         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
447                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
448                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
449                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
450                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
451                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
452                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
453                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
454                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
455                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
456                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
457                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
458                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
459                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
460                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
461                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
462                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
463         };
464         float out_data[4 * 16];
465
466         ResizeEffect *downscale = new ResizeEffect();
467         ASSERT_TRUE(downscale->set_int("width", 1));
468         ASSERT_TRUE(downscale->set_int("height", 4));
469
470         ResizeEffect *upscale = new ResizeEffect();
471         ASSERT_TRUE(upscale->set_int("width", 4));
472         ASSERT_TRUE(upscale->set_int("height", 16));
473
474         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
475         tester.get_chain()->add_effect(downscale);
476         tester.get_chain()->add_effect(upscale);
477         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
478
479         expect_equal(expected_data, out_data, 4, 16);
480 }