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