]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
use Requires for the libs movit depends on
[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 <epoxy/gl.h>
6 #include <assert.h>
7
8 #include "effect.h"
9 #include "effect_chain.h"
10 #include "flat_input.h"
11 #include "gtest/gtest.h"
12 #include "init.h"
13 #include "input.h"
14 #include "mirror_effect.h"
15 #include "multiply_effect.h"
16 #include "resize_effect.h"
17 #include "test_util.h"
18 #include "util.h"
19
20 using namespace std;
21
22 namespace movit {
23
24 TEST(EffectChainTest, EmptyChain) {
25         float data[] = {
26                 0.0f, 0.25f, 0.3f,
27                 0.75f, 1.0f, 1.0f,
28         };
29         float out_data[6];
30         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
31         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
32
33         expect_equal(data, out_data, 3, 2);
34 }
35
36 // An effect that does nothing.
37 class IdentityEffect : public Effect {
38 public:
39         IdentityEffect() {}
40         virtual string effect_type_id() const { return "IdentityEffect"; }
41         string output_fragment_shader() { return read_file("identity.frag"); }
42 };
43
44 TEST(EffectChainTest, Identity) {
45         float data[] = {
46                 0.0f, 0.25f, 0.3f,
47                 0.75f, 1.0f, 1.0f,
48         };
49         float out_data[6];
50         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
51         tester.get_chain()->add_effect(new IdentityEffect());
52         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
53
54         expect_equal(data, out_data, 3, 2);
55 }
56
57 // An effect that does nothing, but requests texture bounce.
58 class BouncingIdentityEffect : public Effect {
59 public:
60         BouncingIdentityEffect() {}
61         virtual string effect_type_id() const { return "IdentityEffect"; }
62         string output_fragment_shader() { return read_file("identity.frag"); }
63         bool needs_texture_bounce() const { return true; }
64         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
65 };
66
67 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
68         float data[] = {
69                 0.0f, 0.25f, 0.3f,
70                 0.75f, 1.0f, 1.0f,
71         };
72         float out_data[6];
73         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
74         tester.get_chain()->add_effect(new BouncingIdentityEffect());
75         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
76
77         expect_equal(data, out_data, 3, 2);
78 }
79
80 TEST(MirrorTest, BasicTest) {
81         float data[] = {
82                 0.0f, 0.25f, 0.3f,
83                 0.75f, 1.0f, 1.0f,
84         };
85         float expected_data[6] = {
86                 0.3f, 0.25f, 0.0f,
87                 1.0f, 1.0f, 0.75f,
88         };
89         float out_data[6];
90         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
91         tester.get_chain()->add_effect(new MirrorEffect());
92         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
93
94         expect_equal(expected_data, out_data, 3, 2);
95 }
96
97 // A dummy effect that inverts its input.
98 class InvertEffect : public Effect {
99 public:
100         InvertEffect() {}
101         virtual string effect_type_id() const { return "InvertEffect"; }
102         string output_fragment_shader() { return read_file("invert_effect.frag"); }
103
104         // A real invert would actually care about its alpha,
105         // but in this unit test, it only complicates things.
106         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
107 };
108
109 // Like IdentityEffect, but rewrites itself out of the loop,
110 // splicing in a different effect instead. Also stores the new node,
111 // so we later can check whatever properties we'd like about the graph.
112 template<class T>
113 class RewritingEffect : public Effect {
114 public:
115         RewritingEffect() : effect(new T()), replaced_node(NULL) {}
116         virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
117         string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
118         virtual void rewrite_graph(EffectChain *graph, Node *self) {
119                 replaced_node = graph->add_node(effect);
120                 graph->replace_receiver(self, replaced_node);
121                 graph->replace_sender(self, replaced_node);
122                 self->disabled = true;
123         }
124
125         T *effect;
126         Node *replaced_node;
127 };
128
129 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
130         float data[] = {
131                 0.0f, 0.25f, 0.3f,
132                 0.75f, 1.0f, 1.0f,
133         };
134         float expected_data[6] = {
135                 1.0f, 0.9771f, 0.9673f,
136                 0.7192f, 0.0f, 0.0f,
137         };
138         float out_data[6];
139         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
140         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
141         tester.get_chain()->add_effect(effect);
142         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
143
144         Node *node = effect->replaced_node;
145         ASSERT_EQ(1, node->incoming_links.size());
146         ASSERT_EQ(1, node->outgoing_links.size());
147         EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
148         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
149
150         expect_equal(expected_data, out_data, 3, 2);
151 }
152
153 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
154         unsigned char data[] = {
155                   0,   0,   0, 255,
156                  64,  64,  64, 255,
157                 128, 128, 128, 255,
158                 255, 255, 255, 255,
159         };
160         float expected_data[] = {
161                 1.0000f, 1.0000f, 1.0000f, 1.0000f,
162                 0.9771f, 0.9771f, 0.9771f, 1.0000f,
163                 0.8983f, 0.8983f, 0.8983f, 1.0000f,
164                 0.0000f, 0.0000f, 0.0000f, 1.0000f
165         };
166         float out_data[4 * 4];
167         EffectChainTester tester(NULL, 1, 4);
168         tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
169         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
170         tester.get_chain()->add_effect(effect);
171         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
172
173         Node *node = effect->replaced_node;
174         ASSERT_EQ(1, node->incoming_links.size());
175         ASSERT_EQ(1, node->outgoing_links.size());
176         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
177         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
178
179         expect_equal(expected_data, out_data, 4, 4);
180 }
181
182 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
183         float data[] = {
184                 0.0f, 0.25f, 0.3f,
185                 0.75f, 1.0f, 1.0f,
186         };
187         float expected_data[6] = {
188                 1.0f, 0.75f, 0.7f,
189                 0.25f, 0.0f, 0.0f,
190         };
191         float out_data[6];
192         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
193         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
194         tester.get_chain()->add_effect(effect);
195         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
196
197         Node *node = effect->replaced_node;
198         ASSERT_EQ(1, node->incoming_links.size());
199         ASSERT_EQ(1, node->outgoing_links.size());
200         EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
201         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
202
203         expect_equal(expected_data, out_data, 3, 2);
204 }
205
206 // A fake input that can change its output colorspace and gamma between instantiation
207 // and finalize.
208 class UnknownColorspaceInput : public FlatInput {
209 public:
210         UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
211             : FlatInput(format, pixel_format, type, width, height),
212               overridden_color_space(format.color_space),
213               overridden_gamma_curve(format.gamma_curve) {}
214         virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
215
216         void set_color_space(Colorspace colorspace) {
217                 overridden_color_space = colorspace;
218         }
219         void set_gamma_curve(GammaCurve gamma_curve) {
220                 overridden_gamma_curve = gamma_curve;
221         }
222         Colorspace get_color_space() const { return overridden_color_space; }
223         GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
224
225 private:
226         Colorspace overridden_color_space;
227         GammaCurve overridden_gamma_curve;
228 };
229
230 TEST(EffectChainTest, HandlesInputChangingColorspace) {
231         const int size = 4;
232
233         float data[size] = {
234                 0.0,
235                 0.5,
236                 0.7,
237                 1.0,
238         };
239         float out_data[size];
240
241         EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
242
243         // First say that we have sRGB, linear input.
244         ImageFormat format;
245         format.color_space = COLORSPACE_sRGB;
246         format.gamma_curve = GAMMA_LINEAR;
247
248         UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
249         input->set_pixel_data(data);
250         tester.get_chain()->add_input(input);
251
252         // Now we change to Rec. 601 input.
253         input->set_color_space(COLORSPACE_REC_601_625);
254         input->set_gamma_curve(GAMMA_REC_601);
255
256         // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
257         tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
258         expect_equal(data, out_data, 4, 1);
259 }
260
261 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
262         float data[] = {
263                 0.0f, 0.25f, 0.3f,
264                 0.75f, 1.0f, 1.0f,
265         };
266         float expected_data[6] = {
267                 0.3f, 0.25f, 0.0f,
268                 1.0f, 1.0f, 0.75f,
269         };
270         float out_data[6];
271         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
272         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
273         tester.get_chain()->add_effect(effect);
274         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
275
276         Node *node = effect->replaced_node;
277         ASSERT_EQ(1, node->incoming_links.size());
278         EXPECT_EQ(0, node->outgoing_links.size());
279         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
280
281         expect_equal(expected_data, out_data, 3, 2);
282 }
283
284 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
285         float data[] = {
286                 0.0f, 0.25f, 0.3f,
287                 0.75f, 1.0f, 1.0f,
288         };
289         float expected_data[6] = {
290                 0.3f, 0.25f, 0.0f,
291                 1.0f, 1.0f, 0.75f,
292         };
293         float out_data[6];
294         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
295         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
296         tester.get_chain()->add_effect(effect);
297         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
298
299         Node *node = effect->replaced_node;
300         ASSERT_EQ(1, node->incoming_links.size());
301         EXPECT_EQ(0, node->outgoing_links.size());
302         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
303
304         expect_equal(expected_data, out_data, 3, 2);
305 }
306
307 // The identity effect needs linear light, and thus will get conversions on both sides.
308 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
309 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
310         float data[256];
311         for (unsigned i = 0; i < 256; ++i) {
312                 data[i] = i / 255.0;
313         };
314         float out_data[256];
315         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
316         tester.get_chain()->add_effect(new IdentityEffect());
317         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
318
319         expect_equal(data, out_data, 256, 1);
320 }
321
322 // Same, but uses the forward sRGB table from the GPU.
323 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
324         unsigned char data[256];
325         float expected_data[256];
326         for (unsigned i = 0; i < 256; ++i) {
327                 data[i] = i;
328                 expected_data[i] = i / 255.0;
329         };
330         float out_data[256];
331         EffectChainTester tester(NULL, 256, 1);
332         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
333         tester.get_chain()->add_effect(new IdentityEffect());
334         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
335
336         expect_equal(expected_data, out_data, 256, 1);
337 }
338
339 // Same, for the Rec. 601/709 gamma curve.
340 TEST(EffectChainTest, IdentityThroughRec709) {
341         float data[256];
342         for (unsigned i = 0; i < 256; ++i) {
343                 data[i] = i / 255.0;
344         };
345         float out_data[256];
346         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
347         tester.get_chain()->add_effect(new IdentityEffect());
348         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
349
350         expect_equal(data, out_data, 256, 1);
351 }
352
353 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
354 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
355         const int size = 3;
356         float data[4 * size] = {
357                 0.8f, 0.0f, 0.0f, 0.5f,
358                 0.0f, 0.2f, 0.2f, 0.3f,
359                 0.1f, 0.0f, 1.0f, 1.0f,
360         };
361         float out_data[4 * size];
362         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
363         tester.get_chain()->add_effect(new IdentityEffect());
364         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
365
366         expect_equal(data, out_data, 4, size);
367 }
368
369 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
370         const int size = 3;
371         float data[4 * size] = {
372                 0.8f, 0.0f, 0.0f, 0.5f,
373                 0.0f, 0.2f, 0.2f, 0.3f,
374                 0.1f, 0.0f, 1.0f, 1.0f,
375         };
376         float expected_data[4 * size] = {
377                 0.1f, 0.0f, 1.0f, 1.0f,
378                 0.0f, 0.2f, 0.2f, 0.3f,
379                 0.8f, 0.0f, 0.0f, 0.5f,
380         };
381         float out_data[4 * size];
382         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
383         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
384         tester.get_chain()->add_effect(effect);
385         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
386
387         Node *node = effect->replaced_node;
388         ASSERT_EQ(1, node->incoming_links.size());
389         EXPECT_EQ(0, node->outgoing_links.size());
390         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
391
392         expect_equal(expected_data, out_data, 4, size);
393 }
394
395 // An input that outputs only blue, which has blank alpha.
396 class BlueInput : public Input {
397 public:
398         BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
399         virtual string effect_type_id() const { return "IdentityEffect"; }
400         string output_fragment_shader() { return read_file("blue.frag"); }
401         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
402         virtual void finalize() {}
403         virtual bool can_output_linear_gamma() const { return true; }
404         virtual unsigned get_width() const { return 1; }
405         virtual unsigned get_height() const { return 1; }
406         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
407         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
408
409 private:
410         int needs_mipmaps;
411 };
412
413 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
414 // which outputs blank alpha.
415 class RewritingToBlueInput : public Input {
416 public:
417         RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
418         virtual string effect_type_id() const { return "RewritingToBlueInput"; }
419         string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
420         virtual void rewrite_graph(EffectChain *graph, Node *self) {
421                 Node *blue_node = graph->add_node(new BlueInput());
422                 graph->replace_receiver(self, blue_node);
423                 graph->replace_sender(self, blue_node);
424
425                 self->disabled = true;
426                 this->blue_node = blue_node;
427         }
428
429         // Dummy values that we need to implement because we inherit from Input.
430         // Same as BlueInput.
431         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
432         virtual void finalize() {}
433         virtual bool can_output_linear_gamma() const { return true; }
434         virtual unsigned get_width() const { return 1; }
435         virtual unsigned get_height() const { return 1; }
436         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
437         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
438
439         Node *blue_node;
440
441 private:
442         int needs_mipmaps;
443 };
444
445 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
446         const int size = 3;
447         float data[4 * size] = {
448                 0.0f, 0.0f, 1.0f, 1.0f,
449                 0.0f, 0.0f, 1.0f, 1.0f,
450                 0.0f, 0.0f, 1.0f, 1.0f,
451         };
452         float out_data[4 * size];
453         EffectChainTester tester(NULL, size, 1);
454         RewritingToBlueInput *input = new RewritingToBlueInput();
455         tester.get_chain()->add_input(input);
456         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
457
458         Node *node = input->blue_node;
459         EXPECT_EQ(0, node->incoming_links.size());
460         EXPECT_EQ(0, node->outgoing_links.size());
461
462         expect_equal(data, out_data, 4, size);
463 }
464
465 // An effect that does nothing, and specifies that it preserves blank alpha.
466 class BlankAlphaPreservingEffect : public Effect {
467 public:
468         BlankAlphaPreservingEffect() {}
469         virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
470         string output_fragment_shader() { return read_file("identity.frag"); }
471         virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
472 };
473
474 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
475         const int size = 3;
476         float data[4 * size] = {
477                 0.0f, 0.0f, 1.0f, 1.0f,
478                 0.0f, 0.0f, 1.0f, 1.0f,
479                 0.0f, 0.0f, 1.0f, 1.0f,
480         };
481         float out_data[4 * size];
482         EffectChainTester tester(NULL, size, 1);
483         tester.get_chain()->add_input(new BlueInput());
484         tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
485         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
486         tester.get_chain()->add_effect(effect);
487         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
488
489         Node *node = effect->replaced_node;
490         EXPECT_EQ(1, node->incoming_links.size());
491         EXPECT_EQ(0, node->outgoing_links.size());
492
493         expect_equal(data, out_data, 4, size);
494 }
495
496 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
497 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
498 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
499 // with other tests.)
500 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
501         const int size = 3;
502         float data[4 * size] = {
503                 0.0f, 0.0f, 1.0f, 1.0f,
504                 0.0f, 0.0f, 1.0f, 1.0f,
505                 0.0f, 0.0f, 1.0f, 1.0f,
506         };
507         float out_data[4 * size];
508         EffectChainTester tester(NULL, size, 1);
509         tester.get_chain()->add_input(new BlueInput());
510         tester.get_chain()->add_effect(new IdentityEffect());  // Not BlankAlphaPreservingEffect.
511         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
512         tester.get_chain()->add_effect(effect);
513         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
514
515         Node *node = effect->replaced_node;
516         EXPECT_EQ(1, node->incoming_links.size());
517         EXPECT_EQ(1, node->outgoing_links.size());
518         EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
519
520         expect_equal(data, out_data, 4, size);
521 }
522
523 // Effectively scales down its input linearly by 4x (and repeating it),
524 // which is not attainable without mipmaps.
525 class MipmapNeedingEffect : public Effect {
526 public:
527         MipmapNeedingEffect() {}
528         virtual bool needs_mipmaps() const { return true; }
529
530         // To be allowed to mess with the sampler state.
531         virtual bool needs_texture_bounce() const { return true; }
532
533         virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
534         string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
535         virtual void inform_added(EffectChain *chain) { this->chain = chain; }
536
537         void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
538         {
539                 Node *self = chain->find_node_for_effect(this);
540                 glActiveTexture(chain->get_input_sampler(self, 0));
541                 check_error();
542                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
543                 check_error();
544                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
545                 check_error();
546         }
547
548 private:
549         EffectChain *chain;
550 };
551
552 TEST(EffectChainTest, MipmapGenerationWorks) {
553         float data[] = {  // In 4x4 blocks.
554                 1.0f, 0.0f, 0.0f, 0.0f,
555                 0.0f, 0.0f, 0.0f, 0.0f,
556                 0.0f, 0.0f, 0.0f, 0.0f,
557                 0.0f, 0.0f, 0.0f, 1.0f,
558
559                 0.0f, 0.0f, 0.0f, 0.0f,
560                 0.0f, 0.5f, 0.0f, 0.0f,
561                 0.0f, 0.0f, 1.0f, 0.0f,
562                 0.0f, 0.0f, 0.0f, 0.0f,
563
564                 1.0f, 1.0f, 1.0f, 1.0f,
565                 1.0f, 1.0f, 1.0f, 1.0f,
566                 1.0f, 1.0f, 1.0f, 1.0f,
567                 1.0f, 1.0f, 1.0f, 1.0f,
568
569                 0.0f, 0.0f, 0.0f, 0.0f,
570                 0.0f, 1.0f, 1.0f, 0.0f,
571                 0.0f, 1.0f, 1.0f, 0.0f,
572                 0.0f, 0.0f, 0.0f, 0.0f,
573         };
574         float expected_data[] = {  // Repeated four times each way.
575                 0.125f,   0.125f,   0.125f,   0.125f,
576                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
577                 1.0f,     1.0f,     1.0f,     1.0f,
578                 0.25f,    0.25f,    0.25f,    0.25f,
579
580                 0.125f,   0.125f,   0.125f,   0.125f,
581                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
582                 1.0f,     1.0f,     1.0f,     1.0f,
583                 0.25f,    0.25f,    0.25f,    0.25f,
584
585                 0.125f,   0.125f,   0.125f,   0.125f,
586                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
587                 1.0f,     1.0f,     1.0f,     1.0f,
588                 0.25f,    0.25f,    0.25f,    0.25f,
589
590                 0.125f,   0.125f,   0.125f,   0.125f,
591                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
592                 1.0f,     1.0f,     1.0f,     1.0f,
593                 0.25f,    0.25f,    0.25f,    0.25f,
594         };
595         float out_data[4 * 16];
596         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
597         tester.get_chain()->add_effect(new MipmapNeedingEffect());
598         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
599
600         expect_equal(expected_data, out_data, 4, 16);
601 }
602
603 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
604         float data[] = {  // In 4x4 blocks.
605                 1.0f, 0.0f, 0.0f, 0.0f,
606                 0.0f, 0.0f, 0.0f, 0.0f,
607                 0.0f, 0.0f, 0.0f, 0.0f,
608                 0.0f, 0.0f, 0.0f, 1.0f,
609
610                 0.0f, 0.0f, 0.0f, 0.0f,
611                 0.0f, 0.5f, 0.0f, 0.0f,
612                 0.0f, 0.0f, 1.0f, 0.0f,
613                 0.0f, 0.0f, 0.0f, 0.0f,
614
615                 1.0f, 1.0f, 1.0f, 1.0f,
616                 1.0f, 1.0f, 1.0f, 1.0f,
617                 1.0f, 1.0f, 1.0f, 1.0f,
618                 1.0f, 1.0f, 1.0f, 1.0f,
619
620                 0.0f, 0.0f, 0.0f, 0.0f,
621                 0.0f, 1.0f, 1.0f, 0.0f,
622                 0.0f, 1.0f, 1.0f, 0.0f,
623                 0.0f, 0.0f, 0.0f, 0.0f,
624         };
625         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
626                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
627                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
628                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
629                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
630                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
631                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
632                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
633                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
634                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
635                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
636                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
637                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
638                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
639                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
640                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
641                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
642         };
643         float out_data[4 * 16];
644
645         ResizeEffect *downscale = new ResizeEffect();
646         ASSERT_TRUE(downscale->set_int("width", 1));
647         ASSERT_TRUE(downscale->set_int("height", 4));
648
649         ResizeEffect *upscale = new ResizeEffect();
650         ASSERT_TRUE(upscale->set_int("width", 4));
651         ASSERT_TRUE(upscale->set_int("height", 16));
652
653         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
654         tester.get_chain()->add_effect(downscale);
655         tester.get_chain()->add_effect(upscale);
656         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
657
658         expect_equal(expected_data, out_data, 4, 16);
659 }
660
661 // An effect that adds its two inputs together. Used below.
662 class AddEffect : public Effect {
663 public:
664         AddEffect() {}
665         virtual string effect_type_id() const { return "AddEffect"; }
666         string output_fragment_shader() { return read_file("add.frag"); }
667         virtual unsigned num_inputs() const { return 2; }
668         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
669 };
670
671 // Constructs the graph
672 //
673 //             FlatInput               |
674 //            /         \              |
675 //  MultiplyEffect  MultiplyEffect     |
676 //            \         /              |
677 //             AddEffect               |
678 //
679 // and verifies that it gives the correct output.
680 TEST(EffectChainTest, DiamondGraph) {
681         float data[] = {
682                 1.0f, 1.0f,
683                 1.0f, 0.0f,
684         };
685         float expected_data[] = {
686                 2.5f, 2.5f,
687                 2.5f, 0.0f,
688         };
689         float out_data[2 * 2];
690
691         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
692         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
693
694         MultiplyEffect *mul_half = new MultiplyEffect();
695         ASSERT_TRUE(mul_half->set_vec4("factor", half));
696         
697         MultiplyEffect *mul_two = new MultiplyEffect();
698         ASSERT_TRUE(mul_two->set_vec4("factor", two));
699
700         EffectChainTester tester(NULL, 2, 2);
701
702         ImageFormat format;
703         format.color_space = COLORSPACE_sRGB;
704         format.gamma_curve = GAMMA_LINEAR;
705
706         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
707         input->set_pixel_data(data);
708
709         tester.get_chain()->add_input(input);
710         tester.get_chain()->add_effect(mul_half, input);
711         tester.get_chain()->add_effect(mul_two, input);
712         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
713         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
714
715         expect_equal(expected_data, out_data, 2, 2);
716 }
717
718 // Constructs the graph
719 //
720 //             FlatInput                     |
721 //            /         \                    |
722 //  MultiplyEffect  MultiplyEffect           |
723 //         \             |                   |
724 //          \    BouncingIdentityEffect      |  
725 //            \         /                    |
726 //             AddEffect                     |
727 //
728 // and verifies that it gives the correct output.
729 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
730         float data[] = {
731                 1.0f, 1.0f,
732                 1.0f, 0.0f,
733         };
734         float expected_data[] = {
735                 2.5f, 2.5f,
736                 2.5f, 0.0f,
737         };
738         float out_data[2 * 2];
739
740         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
741         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
742
743         MultiplyEffect *mul_half = new MultiplyEffect();
744         ASSERT_TRUE(mul_half->set_vec4("factor", half));
745         
746         MultiplyEffect *mul_two = new MultiplyEffect();
747         ASSERT_TRUE(mul_two->set_vec4("factor", two));
748         
749         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
750
751         EffectChainTester tester(NULL, 2, 2);
752
753         ImageFormat format;
754         format.color_space = COLORSPACE_sRGB;
755         format.gamma_curve = GAMMA_LINEAR;
756
757         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
758         input->set_pixel_data(data);
759
760         tester.get_chain()->add_input(input);
761         tester.get_chain()->add_effect(mul_half, input);
762         tester.get_chain()->add_effect(mul_two, input);
763         tester.get_chain()->add_effect(bounce, mul_two);
764         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
765         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
766
767         expect_equal(expected_data, out_data, 2, 2);
768 }
769
770 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
771         float data[] = {
772                 0.735f, 0.0f,
773                 0.735f, 0.0f,
774         };
775         float expected_data[] = {
776                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
777                 0.0f, 0.5f,
778         };
779         float out_data[2 * 2];
780         
781         EffectChainTester tester(NULL, 2, 2);
782         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
783
784         // MirrorEffect does not get linear light, so the conversions will be
785         // inserted after it, not before.
786         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
787         tester.get_chain()->add_effect(effect);
788
789         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
790         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
791         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
792         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
793
794         expect_equal(expected_data, out_data, 2, 2);
795
796         Node *node = effect->replaced_node;
797         ASSERT_EQ(1, node->incoming_links.size());
798         ASSERT_EQ(1, node->outgoing_links.size());
799         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
800         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
801 }
802
803 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
804         float data[] = {
805                 0.5f, 0.0f,
806                 0.5f, 0.0f,
807         };
808         float expected_data[] = {
809                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
810                 0.0f, 0.5f,
811         };
812         float out_data[2 * 2];
813         
814         EffectChainTester tester(NULL, 2, 2);
815         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
816
817         // MirrorEffect does not get linear light, so the conversions will be
818         // inserted after it, not before.
819         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
820         tester.get_chain()->add_effect(effect);
821
822         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
823         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
824         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
825         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
826
827         expect_equal(expected_data, out_data, 2, 2);
828
829         Node *node = effect->replaced_node;
830         ASSERT_EQ(1, node->incoming_links.size());
831         ASSERT_EQ(1, node->outgoing_links.size());
832         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
833         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
834 }
835
836 // An effect that does nothing, but requests texture bounce and stores
837 // its input size.
838 class SizeStoringEffect : public BouncingIdentityEffect {
839 public:
840         SizeStoringEffect() : input_width(-1), input_height(-1) {}
841         virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
842                 assert(input_num == 0);
843                 input_width = width;
844                 input_height = height;
845         }
846         virtual string effect_type_id() const { return "SizeStoringEffect"; }
847
848         int input_width, input_height;
849 };
850
851 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
852         float data[2 * 2] = {
853                 0.0f, 0.0f,
854                 0.0f, 0.0f,
855         };
856         float out_data[2 * 2];
857         
858         EffectChainTester tester(NULL, 4, 3);  // Note non-square aspect.
859
860         ImageFormat format;
861         format.color_space = COLORSPACE_sRGB;
862         format.gamma_curve = GAMMA_LINEAR;
863
864         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
865         input1->set_pixel_data(data);
866         
867         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
868         input2->set_pixel_data(data);
869
870         SizeStoringEffect *input_store = new SizeStoringEffect();
871
872         tester.get_chain()->add_input(input1);
873         tester.get_chain()->add_input(input2);
874         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
875         tester.get_chain()->add_effect(input_store);
876         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
877
878         EXPECT_EQ(2, input_store->input_width);
879         EXPECT_EQ(2, input_store->input_height);
880 }
881
882 TEST(EffectChainTest, AspectRatioConversion) {
883         float data1[4 * 3] = {
884                 0.0f, 0.0f, 0.0f, 0.0f,
885                 0.0f, 0.0f, 0.0f, 0.0f,
886                 0.0f, 0.0f, 0.0f, 0.0f,
887         };
888         float data2[7 * 7] = {
889                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
890                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
891                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
892                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
893                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
894                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
895                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
896         };
897
898         // The right conversion here is that the 7x7 image decides the size,
899         // since it is the biggest, so everything is scaled up to 9x7
900         // (keep the height, round the width 9.333 to 9). 
901         float out_data[9 * 7];
902         
903         EffectChainTester tester(NULL, 4, 3);
904
905         ImageFormat format;
906         format.color_space = COLORSPACE_sRGB;
907         format.gamma_curve = GAMMA_LINEAR;
908
909         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
910         input1->set_pixel_data(data1);
911         
912         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
913         input2->set_pixel_data(data2);
914
915         SizeStoringEffect *input_store = new SizeStoringEffect();
916
917         tester.get_chain()->add_input(input1);
918         tester.get_chain()->add_input(input2);
919         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
920         tester.get_chain()->add_effect(input_store);
921         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
922
923         EXPECT_EQ(9, input_store->input_width);
924         EXPECT_EQ(7, input_store->input_height);
925 }
926
927 // An effect that does nothing except changing its output sizes.
928 class VirtualResizeEffect : public Effect {
929 public:
930         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
931                 : width(width),
932                   height(height),
933                   virtual_width(virtual_width),
934                   virtual_height(virtual_height) {}
935         virtual string effect_type_id() const { return "VirtualResizeEffect"; }
936         string output_fragment_shader() { return read_file("identity.frag"); }
937
938         virtual bool changes_output_size() const { return true; }
939
940         virtual void get_output_size(unsigned *width, unsigned *height,
941                                      unsigned *virtual_width, unsigned *virtual_height) const {
942                 *width = this->width;
943                 *height = this->height;
944                 *virtual_width = this->virtual_width;
945                 *virtual_height = this->virtual_height;
946         }
947
948 private:
949         int width, height, virtual_width, virtual_height;
950 };
951
952 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
953         const int size = 2, bigger_size = 3;
954         float data[size * size] = {
955                 1.0f, 0.0f,
956                 0.0f, 1.0f,
957         };
958         float out_data[size * size];
959         
960         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
961
962         SizeStoringEffect *size_store = new SizeStoringEffect();
963
964         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
965         tester.get_chain()->add_effect(size_store);
966         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
967         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
968
969         EXPECT_EQ(bigger_size, size_store->input_width);
970         EXPECT_EQ(bigger_size, size_store->input_height);
971
972         // If the resize is implemented as non-virtual, we'll fail here,
973         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
974         expect_equal(data, out_data, size, size);
975 }
976
977 // Does not use EffectChainTest, so that it can construct an EffectChain without
978 // a shared ResourcePool (which is also properly destroyed afterwards).
979 // Also turns on debugging to test that code path.
980 TEST(EffectChainTest, IdentityWithOwnPool) {
981         const int width = 3, height = 2;
982         float data[] = {
983                 0.0f, 0.25f, 0.3f,
984                 0.75f, 1.0f, 1.0f,
985         };
986         const float expected_data[] = {
987                 0.75f, 1.0f, 1.0f,
988                 0.0f, 0.25f, 0.3f,
989         };
990         float out_data[6], temp[6 * 4];
991
992         EffectChain chain(width, height);
993         movit_debug_level = MOVIT_DEBUG_ON;
994
995         ImageFormat format;
996         format.color_space = COLORSPACE_sRGB;
997         format.gamma_curve = GAMMA_LINEAR;
998
999         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1000         input->set_pixel_data(data);
1001         chain.add_input(input);
1002         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1003
1004         GLuint texnum, fbo;
1005         glGenTextures(1, &texnum);
1006         check_error();
1007         glBindTexture(GL_TEXTURE_2D, texnum);
1008         check_error();
1009         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
1010         check_error();
1011
1012         glGenFramebuffers(1, &fbo);
1013         check_error();
1014         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1015         check_error();
1016         glFramebufferTexture2D(
1017                 GL_FRAMEBUFFER,
1018                 GL_COLOR_ATTACHMENT0,
1019                 GL_TEXTURE_2D,
1020                 texnum,
1021                 0);
1022         check_error();
1023         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1024         check_error();
1025
1026         chain.finalize();
1027
1028         chain.render_to_fbo(fbo, width, height);
1029
1030         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1031         check_error();
1032         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1033         check_error();
1034         for (unsigned i = 0; i < 6; ++i) {
1035                 out_data[i] = temp[i * 4];
1036         }
1037
1038         expect_equal(expected_data, out_data, width, height);
1039
1040         // Reset the debug status again.
1041         movit_debug_level = MOVIT_DEBUG_OFF;
1042 }
1043
1044 }  // namespace movit