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