]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
Hard-assert on something that has bitten me too many times now.
[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 <locale>
6 #include <sstream>
7 #include <string>
8
9 #include <epoxy/gl.h>
10 #include <assert.h>
11
12 #include "effect.h"
13 #include "effect_chain.h"
14 #include "flat_input.h"
15 #include "gtest/gtest.h"
16 #include "init.h"
17 #include "input.h"
18 #include "mirror_effect.h"
19 #include "multiply_effect.h"
20 #include "resize_effect.h"
21 #include "test_util.h"
22 #include "util.h"
23
24 using namespace std;
25
26 namespace movit {
27
28 TEST(EffectChainTest, EmptyChain) {
29         float data[] = {
30                 0.0f, 0.25f, 0.3f,
31                 0.75f, 1.0f, 1.0f,
32         };
33         float out_data[6];
34         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
35         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
36
37         expect_equal(data, out_data, 3, 2);
38 }
39
40 // An effect that does nothing.
41 class IdentityEffect : public Effect {
42 public:
43         IdentityEffect() {}
44         virtual string effect_type_id() const { return "IdentityEffect"; }
45         string output_fragment_shader() { return read_file("identity.frag"); }
46 };
47
48 TEST(EffectChainTest, Identity) {
49         float data[] = {
50                 0.0f, 0.25f, 0.3f,
51                 0.75f, 1.0f, 1.0f,
52         };
53         float out_data[6];
54         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
55         tester.get_chain()->add_effect(new IdentityEffect());
56         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
57
58         expect_equal(data, out_data, 3, 2);
59 }
60
61 // An effect that does nothing, but requests texture bounce.
62 class BouncingIdentityEffect : public Effect {
63 public:
64         BouncingIdentityEffect() {}
65         virtual string effect_type_id() const { return "IdentityEffect"; }
66         string output_fragment_shader() { return read_file("identity.frag"); }
67         bool needs_texture_bounce() const { return true; }
68         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
69 };
70
71 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
72         float data[] = {
73                 0.0f, 0.25f, 0.3f,
74                 0.75f, 1.0f, 1.0f,
75         };
76         float out_data[6];
77         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
78         tester.get_chain()->add_effect(new BouncingIdentityEffect());
79         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
80
81         expect_equal(data, out_data, 3, 2);
82 }
83
84 TEST(MirrorTest, BasicTest) {
85         float data[] = {
86                 0.0f, 0.25f, 0.3f,
87                 0.75f, 1.0f, 1.0f,
88         };
89         float expected_data[6] = {
90                 0.3f, 0.25f, 0.0f,
91                 1.0f, 1.0f, 0.75f,
92         };
93         float out_data[6];
94         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
95         tester.get_chain()->add_effect(new MirrorEffect());
96         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
97
98         expect_equal(expected_data, out_data, 3, 2);
99 }
100
101 TEST(EffectChainTest, TopLeftOrigin) {
102         float data[] = {
103                 0.0f, 0.25f, 0.3f,
104                 0.75f, 1.0f, 1.0f,
105         };
106         // Note that EffectChainTester assumes bottom-left origin, so by setting
107         // top-left, we will get flipped data back.
108         float expected_data[6] = {
109                 0.75f, 1.0f, 1.0f,
110                 0.0f, 0.25f, 0.3f,
111         };
112         float out_data[6];
113         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
114         tester.get_chain()->set_output_origin(OUTPUT_ORIGIN_TOP_LEFT);
115         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
116
117         expect_equal(expected_data, out_data, 3, 2);
118 }
119
120 // A dummy effect that inverts its input.
121 class InvertEffect : public Effect {
122 public:
123         InvertEffect() {}
124         virtual string effect_type_id() const { return "InvertEffect"; }
125         string output_fragment_shader() { return read_file("invert_effect.frag"); }
126
127         // A real invert would actually care about its alpha,
128         // but in this unit test, it only complicates things.
129         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
130 };
131
132 // Like IdentityEffect, but rewrites itself out of the loop,
133 // splicing in a different effect instead. Also stores the new node,
134 // so we later can check whatever properties we'd like about the graph.
135 template<class T>
136 class RewritingEffect : public Effect {
137 public:
138         RewritingEffect() : effect(new T()), replaced_node(NULL) {}
139         virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
140         string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
141         virtual void rewrite_graph(EffectChain *graph, Node *self) {
142                 replaced_node = graph->add_node(effect);
143                 graph->replace_receiver(self, replaced_node);
144                 graph->replace_sender(self, replaced_node);
145                 self->disabled = true;
146         }
147
148         T *effect;
149         Node *replaced_node;
150 };
151
152 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
153         float data[] = {
154                 0.0f, 0.25f, 0.3f,
155                 0.75f, 1.0f, 1.0f,
156         };
157         float expected_data[6] = {
158                 1.0f, 0.9771f, 0.9673f,
159                 0.7192f, 0.0f, 0.0f,
160         };
161         float out_data[6];
162         EffectChainTester tester(data, 3, 2, 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("GammaExpansionEffect", 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, 3, 2);
174 }
175
176 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
177         unsigned char data[] = {
178                   0,   0,   0, 255,
179                  64,  64,  64, 255,
180                 128, 128, 128, 255,
181                 255, 255, 255, 255,
182         };
183         float expected_data[] = {
184                 1.0000f, 1.0000f, 1.0000f, 1.0000f,
185                 0.9771f, 0.9771f, 0.9771f, 1.0000f,
186                 0.8983f, 0.8983f, 0.8983f, 1.0000f,
187                 0.0000f, 0.0000f, 0.0000f, 1.0000f
188         };
189         float out_data[4 * 4];
190         EffectChainTester tester(NULL, 1, 4);
191         tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
192         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
193         tester.get_chain()->add_effect(effect);
194         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
195
196         Node *node = effect->replaced_node;
197         ASSERT_EQ(1, node->incoming_links.size());
198         ASSERT_EQ(1, node->outgoing_links.size());
199         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
200         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
201
202         expect_equal(expected_data, out_data, 4, 4);
203 }
204
205 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
206         float data[] = {
207                 0.0f, 0.25f, 0.3f,
208                 0.75f, 1.0f, 1.0f,
209         };
210         float expected_data[6] = {
211                 1.0f, 0.75f, 0.7f,
212                 0.25f, 0.0f, 0.0f,
213         };
214         float out_data[6];
215         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
216         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
217         tester.get_chain()->add_effect(effect);
218         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
219
220         Node *node = effect->replaced_node;
221         ASSERT_EQ(1, node->incoming_links.size());
222         ASSERT_EQ(1, node->outgoing_links.size());
223         EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
224         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
225
226         expect_equal(expected_data, out_data, 3, 2);
227 }
228
229 // A fake input that can change its output colorspace and gamma between instantiation
230 // and finalize.
231 class UnknownColorspaceInput : public FlatInput {
232 public:
233         UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
234             : FlatInput(format, pixel_format, type, width, height),
235               overridden_color_space(format.color_space),
236               overridden_gamma_curve(format.gamma_curve) {}
237         virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
238
239         void set_color_space(Colorspace colorspace) {
240                 overridden_color_space = colorspace;
241         }
242         void set_gamma_curve(GammaCurve gamma_curve) {
243                 overridden_gamma_curve = gamma_curve;
244         }
245         Colorspace get_color_space() const { return overridden_color_space; }
246         GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
247
248 private:
249         Colorspace overridden_color_space;
250         GammaCurve overridden_gamma_curve;
251 };
252
253 TEST(EffectChainTest, HandlesInputChangingColorspace) {
254         const int size = 4;
255
256         float data[size] = {
257                 0.0,
258                 0.5,
259                 0.7,
260                 1.0,
261         };
262         float out_data[size];
263
264         EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
265
266         // First say that we have sRGB, linear input.
267         ImageFormat format;
268         format.color_space = COLORSPACE_sRGB;
269         format.gamma_curve = GAMMA_LINEAR;
270
271         UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
272         input->set_pixel_data(data);
273         tester.get_chain()->add_input(input);
274
275         // Now we change to Rec. 601 input.
276         input->set_color_space(COLORSPACE_REC_601_625);
277         input->set_gamma_curve(GAMMA_REC_601);
278
279         // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
280         tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
281         expect_equal(data, out_data, 4, 1);
282 }
283
284 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
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_sRGB, GAMMA_sRGB);
295         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
296         tester.get_chain()->add_effect(effect);
297         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
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 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
308         float data[] = {
309                 0.0f, 0.25f, 0.3f,
310                 0.75f, 1.0f, 1.0f,
311         };
312         float expected_data[6] = {
313                 0.3f, 0.25f, 0.0f,
314                 1.0f, 1.0f, 0.75f,
315         };
316         float out_data[6];
317         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
318         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
319         tester.get_chain()->add_effect(effect);
320         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
321
322         Node *node = effect->replaced_node;
323         ASSERT_EQ(1, node->incoming_links.size());
324         EXPECT_EQ(0, node->outgoing_links.size());
325         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
326
327         expect_equal(expected_data, out_data, 3, 2);
328 }
329
330 // The identity effect needs linear light, and thus will get conversions on both sides.
331 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
332 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
333         float data[256];
334         for (unsigned i = 0; i < 256; ++i) {
335                 data[i] = i / 255.0;
336         };
337         float out_data[256];
338         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
339         tester.get_chain()->add_effect(new IdentityEffect());
340         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
341
342         expect_equal(data, out_data, 256, 1);
343 }
344
345 // Same, but uses the forward sRGB table from the GPU.
346 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
347         unsigned char data[256];
348         float expected_data[256];
349         for (unsigned i = 0; i < 256; ++i) {
350                 data[i] = i;
351                 expected_data[i] = i / 255.0;
352         };
353         float out_data[256];
354         EffectChainTester tester(NULL, 256, 1);
355         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
356         tester.get_chain()->add_effect(new IdentityEffect());
357         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
358
359         expect_equal(expected_data, out_data, 256, 1);
360 }
361
362 // Same, for the Rec. 601/709 gamma curve.
363 TEST(EffectChainTest, IdentityThroughRec709) {
364         float data[256];
365         for (unsigned i = 0; i < 256; ++i) {
366                 data[i] = i / 255.0;
367         };
368         float out_data[256];
369         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
370         tester.get_chain()->add_effect(new IdentityEffect());
371         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
372
373         expect_equal(data, out_data, 256, 1);
374 }
375
376 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
377 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
378         const int size = 3;
379         float data[4 * size] = {
380                 0.8f, 0.0f, 0.0f, 0.5f,
381                 0.0f, 0.2f, 0.2f, 0.3f,
382                 0.1f, 0.0f, 1.0f, 1.0f,
383         };
384         float out_data[4 * size];
385         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
386         tester.get_chain()->add_effect(new IdentityEffect());
387         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
388
389         expect_equal(data, out_data, 4, size);
390 }
391
392 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
393         const int size = 3;
394         float data[4 * size] = {
395                 0.8f, 0.0f, 0.0f, 0.5f,
396                 0.0f, 0.2f, 0.2f, 0.3f,
397                 0.1f, 0.0f, 1.0f, 1.0f,
398         };
399         float expected_data[4 * size] = {
400                 0.1f, 0.0f, 1.0f, 1.0f,
401                 0.0f, 0.2f, 0.2f, 0.3f,
402                 0.8f, 0.0f, 0.0f, 0.5f,
403         };
404         float out_data[4 * size];
405         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
406         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
407         tester.get_chain()->add_effect(effect);
408         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
409
410         Node *node = effect->replaced_node;
411         ASSERT_EQ(1, node->incoming_links.size());
412         EXPECT_EQ(0, node->outgoing_links.size());
413         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
414
415         expect_equal(expected_data, out_data, 4, size);
416 }
417
418 // An input that outputs only blue, which has blank alpha.
419 class BlueInput : public Input {
420 public:
421         BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
422         virtual string effect_type_id() const { return "IdentityEffect"; }
423         string output_fragment_shader() { return read_file("blue.frag"); }
424         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
425         virtual void finalize() {}
426         virtual bool can_output_linear_gamma() const { return true; }
427         virtual unsigned get_width() const { return 1; }
428         virtual unsigned get_height() const { return 1; }
429         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
430         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
431
432 private:
433         int needs_mipmaps;
434 };
435
436 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
437 // which outputs blank alpha.
438 class RewritingToBlueInput : public Input {
439 public:
440         RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
441         virtual string effect_type_id() const { return "RewritingToBlueInput"; }
442         string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
443         virtual void rewrite_graph(EffectChain *graph, Node *self) {
444                 Node *blue_node = graph->add_node(new BlueInput());
445                 graph->replace_receiver(self, blue_node);
446                 graph->replace_sender(self, blue_node);
447
448                 self->disabled = true;
449                 this->blue_node = blue_node;
450         }
451
452         // Dummy values that we need to implement because we inherit from Input.
453         // Same as BlueInput.
454         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
455         virtual void finalize() {}
456         virtual bool can_output_linear_gamma() const { return true; }
457         virtual unsigned get_width() const { return 1; }
458         virtual unsigned get_height() const { return 1; }
459         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
460         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
461
462         Node *blue_node;
463
464 private:
465         int needs_mipmaps;
466 };
467
468 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
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         RewritingToBlueInput *input = new RewritingToBlueInput();
478         tester.get_chain()->add_input(input);
479         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
480
481         Node *node = input->blue_node;
482         EXPECT_EQ(0, node->incoming_links.size());
483         EXPECT_EQ(0, node->outgoing_links.size());
484
485         expect_equal(data, out_data, 4, size);
486 }
487
488 // An effect that does nothing, and specifies that it preserves blank alpha.
489 class BlankAlphaPreservingEffect : public Effect {
490 public:
491         BlankAlphaPreservingEffect() {}
492         virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
493         string output_fragment_shader() { return read_file("identity.frag"); }
494         virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
495 };
496
497 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
498         const int size = 3;
499         float data[4 * size] = {
500                 0.0f, 0.0f, 1.0f, 1.0f,
501                 0.0f, 0.0f, 1.0f, 1.0f,
502                 0.0f, 0.0f, 1.0f, 1.0f,
503         };
504         float out_data[4 * size];
505         EffectChainTester tester(NULL, size, 1);
506         tester.get_chain()->add_input(new BlueInput());
507         tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
508         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
509         tester.get_chain()->add_effect(effect);
510         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
511
512         Node *node = effect->replaced_node;
513         EXPECT_EQ(1, node->incoming_links.size());
514         EXPECT_EQ(0, node->outgoing_links.size());
515
516         expect_equal(data, out_data, 4, size);
517 }
518
519 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
520 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
521 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
522 // with other tests.)
523 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
524         const int size = 3;
525         float data[4 * size] = {
526                 0.0f, 0.0f, 1.0f, 1.0f,
527                 0.0f, 0.0f, 1.0f, 1.0f,
528                 0.0f, 0.0f, 1.0f, 1.0f,
529         };
530         float out_data[4 * size];
531         EffectChainTester tester(NULL, size, 1);
532         tester.get_chain()->add_input(new BlueInput());
533         tester.get_chain()->add_effect(new IdentityEffect());  // Not BlankAlphaPreservingEffect.
534         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
535         tester.get_chain()->add_effect(effect);
536         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
537
538         Node *node = effect->replaced_node;
539         EXPECT_EQ(1, node->incoming_links.size());
540         EXPECT_EQ(1, node->outgoing_links.size());
541         EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
542
543         expect_equal(data, out_data, 4, size);
544 }
545
546 // Effectively scales down its input linearly by 4x (and repeating it),
547 // which is not attainable without mipmaps.
548 class MipmapNeedingEffect : public Effect {
549 public:
550         MipmapNeedingEffect() {}
551         virtual bool needs_mipmaps() const { return true; }
552
553         // To be allowed to mess with the sampler state.
554         virtual bool needs_texture_bounce() const { return true; }
555
556         virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
557         string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
558         virtual void inform_added(EffectChain *chain) { this->chain = chain; }
559
560         void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
561         {
562                 Node *self = chain->find_node_for_effect(this);
563                 glActiveTexture(chain->get_input_sampler(self, 0));
564                 check_error();
565                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
566                 check_error();
567                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
568                 check_error();
569         }
570
571 private:
572         EffectChain *chain;
573 };
574
575 TEST(EffectChainTest, MipmapGenerationWorks) {
576         float data[] = {  // In 4x4 blocks.
577                 1.0f, 0.0f, 0.0f, 0.0f,
578                 0.0f, 0.0f, 0.0f, 0.0f,
579                 0.0f, 0.0f, 0.0f, 0.0f,
580                 0.0f, 0.0f, 0.0f, 1.0f,
581
582                 0.0f, 0.0f, 0.0f, 0.0f,
583                 0.0f, 0.5f, 0.0f, 0.0f,
584                 0.0f, 0.0f, 1.0f, 0.0f,
585                 0.0f, 0.0f, 0.0f, 0.0f,
586
587                 1.0f, 1.0f, 1.0f, 1.0f,
588                 1.0f, 1.0f, 1.0f, 1.0f,
589                 1.0f, 1.0f, 1.0f, 1.0f,
590                 1.0f, 1.0f, 1.0f, 1.0f,
591
592                 0.0f, 0.0f, 0.0f, 0.0f,
593                 0.0f, 1.0f, 1.0f, 0.0f,
594                 0.0f, 1.0f, 1.0f, 0.0f,
595                 0.0f, 0.0f, 0.0f, 0.0f,
596         };
597         float expected_data[] = {  // Repeated four times each way.
598                 0.125f,   0.125f,   0.125f,   0.125f,
599                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
600                 1.0f,     1.0f,     1.0f,     1.0f,
601                 0.25f,    0.25f,    0.25f,    0.25f,
602
603                 0.125f,   0.125f,   0.125f,   0.125f,
604                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
605                 1.0f,     1.0f,     1.0f,     1.0f,
606                 0.25f,    0.25f,    0.25f,    0.25f,
607
608                 0.125f,   0.125f,   0.125f,   0.125f,
609                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
610                 1.0f,     1.0f,     1.0f,     1.0f,
611                 0.25f,    0.25f,    0.25f,    0.25f,
612
613                 0.125f,   0.125f,   0.125f,   0.125f,
614                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
615                 1.0f,     1.0f,     1.0f,     1.0f,
616                 0.25f,    0.25f,    0.25f,    0.25f,
617         };
618         float out_data[4 * 16];
619         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
620         tester.get_chain()->add_effect(new MipmapNeedingEffect());
621         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
622
623         expect_equal(expected_data, out_data, 4, 16);
624 }
625
626 class NonMipmapCapableInput : public FlatInput {
627 public:
628         NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
629                 : FlatInput(format, pixel_format, type, width, height) {}
630
631         virtual bool can_supply_mipmaps() const { return false; }
632         bool set_int(const std::string& key, int value) {
633                 if (key == "needs_mipmaps") {
634                         assert(value == 0);
635                 }
636                 return FlatInput::set_int(key, value);
637         }
638 };
639
640 // The same test as MipmapGenerationWorks, but with an input that refuses
641 // to supply mipmaps.
642 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
643         float data[] = {  // In 4x4 blocks.
644                 1.0f, 0.0f, 0.0f, 0.0f,
645                 0.0f, 0.0f, 0.0f, 0.0f,
646                 0.0f, 0.0f, 0.0f, 0.0f,
647                 0.0f, 0.0f, 0.0f, 1.0f,
648
649                 0.0f, 0.0f, 0.0f, 0.0f,
650                 0.0f, 0.5f, 0.0f, 0.0f,
651                 0.0f, 0.0f, 1.0f, 0.0f,
652                 0.0f, 0.0f, 0.0f, 0.0f,
653
654                 1.0f, 1.0f, 1.0f, 1.0f,
655                 1.0f, 1.0f, 1.0f, 1.0f,
656                 1.0f, 1.0f, 1.0f, 1.0f,
657                 1.0f, 1.0f, 1.0f, 1.0f,
658
659                 0.0f, 0.0f, 0.0f, 0.0f,
660                 0.0f, 1.0f, 1.0f, 0.0f,
661                 0.0f, 1.0f, 1.0f, 0.0f,
662                 0.0f, 0.0f, 0.0f, 0.0f,
663         };
664         float expected_data[] = {  // Repeated four times each way.
665                 0.125f,   0.125f,   0.125f,   0.125f,
666                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
667                 1.0f,     1.0f,     1.0f,     1.0f,
668                 0.25f,    0.25f,    0.25f,    0.25f,
669
670                 0.125f,   0.125f,   0.125f,   0.125f,
671                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
672                 1.0f,     1.0f,     1.0f,     1.0f,
673                 0.25f,    0.25f,    0.25f,    0.25f,
674
675                 0.125f,   0.125f,   0.125f,   0.125f,
676                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
677                 1.0f,     1.0f,     1.0f,     1.0f,
678                 0.25f,    0.25f,    0.25f,    0.25f,
679
680                 0.125f,   0.125f,   0.125f,   0.125f,
681                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
682                 1.0f,     1.0f,     1.0f,     1.0f,
683                 0.25f,    0.25f,    0.25f,    0.25f,
684         };
685         float out_data[4 * 16];
686         EffectChainTester tester(NULL, 4, 16, FORMAT_GRAYSCALE);
687
688         ImageFormat format;
689         format.color_space = COLORSPACE_sRGB;
690         format.gamma_curve = GAMMA_LINEAR;
691
692         NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
693         input->set_pixel_data(data);
694         tester.get_chain()->add_input(input);
695         tester.get_chain()->add_effect(new MipmapNeedingEffect());
696         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
697
698         expect_equal(expected_data, out_data, 4, 16);
699 }
700
701 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
702         float data[] = {  // In 4x4 blocks.
703                 1.0f, 0.0f, 0.0f, 0.0f,
704                 0.0f, 0.0f, 0.0f, 0.0f,
705                 0.0f, 0.0f, 0.0f, 0.0f,
706                 0.0f, 0.0f, 0.0f, 1.0f,
707
708                 0.0f, 0.0f, 0.0f, 0.0f,
709                 0.0f, 0.5f, 0.0f, 0.0f,
710                 0.0f, 0.0f, 1.0f, 0.0f,
711                 0.0f, 0.0f, 0.0f, 0.0f,
712
713                 1.0f, 1.0f, 1.0f, 1.0f,
714                 1.0f, 1.0f, 1.0f, 1.0f,
715                 1.0f, 1.0f, 1.0f, 1.0f,
716                 1.0f, 1.0f, 1.0f, 1.0f,
717
718                 0.0f, 0.0f, 0.0f, 0.0f,
719                 0.0f, 1.0f, 1.0f, 0.0f,
720                 0.0f, 1.0f, 1.0f, 0.0f,
721                 0.0f, 0.0f, 0.0f, 0.0f,
722         };
723         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
724                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
725                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
726                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
727                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
728                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
729                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
730                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
731                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
732                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
733                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
734                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
735                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
736                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
737                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
738                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
739                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
740         };
741         float out_data[4 * 16];
742
743         ResizeEffect *downscale = new ResizeEffect();
744         ASSERT_TRUE(downscale->set_int("width", 1));
745         ASSERT_TRUE(downscale->set_int("height", 4));
746
747         ResizeEffect *upscale = new ResizeEffect();
748         ASSERT_TRUE(upscale->set_int("width", 4));
749         ASSERT_TRUE(upscale->set_int("height", 16));
750
751         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
752         tester.get_chain()->add_effect(downscale);
753         tester.get_chain()->add_effect(upscale);
754         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
755
756         expect_equal(expected_data, out_data, 4, 16);
757 }
758
759 // An effect that adds its two inputs together. Used below.
760 class AddEffect : public Effect {
761 public:
762         AddEffect() {}
763         virtual string effect_type_id() const { return "AddEffect"; }
764         string output_fragment_shader() { return read_file("add.frag"); }
765         virtual unsigned num_inputs() const { return 2; }
766         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
767 };
768
769 // Constructs the graph
770 //
771 //             FlatInput               |
772 //            /         \              |
773 //  MultiplyEffect  MultiplyEffect     |
774 //            \         /              |
775 //             AddEffect               |
776 //
777 // and verifies that it gives the correct output.
778 TEST(EffectChainTest, DiamondGraph) {
779         float data[] = {
780                 1.0f, 1.0f,
781                 1.0f, 0.0f,
782         };
783         float expected_data[] = {
784                 2.5f, 2.5f,
785                 2.5f, 0.0f,
786         };
787         float out_data[2 * 2];
788
789         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
790         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
791
792         MultiplyEffect *mul_half = new MultiplyEffect();
793         ASSERT_TRUE(mul_half->set_vec4("factor", half));
794         
795         MultiplyEffect *mul_two = new MultiplyEffect();
796         ASSERT_TRUE(mul_two->set_vec4("factor", two));
797
798         EffectChainTester tester(NULL, 2, 2);
799
800         ImageFormat format;
801         format.color_space = COLORSPACE_sRGB;
802         format.gamma_curve = GAMMA_LINEAR;
803
804         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
805         input->set_pixel_data(data);
806
807         tester.get_chain()->add_input(input);
808         tester.get_chain()->add_effect(mul_half, input);
809         tester.get_chain()->add_effect(mul_two, input);
810         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
811         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
812
813         expect_equal(expected_data, out_data, 2, 2);
814 }
815
816 // Constructs the graph
817 //
818 //             FlatInput                     |
819 //            /         \                    |
820 //  MultiplyEffect  MultiplyEffect           |
821 //         \             |                   |
822 //          \    BouncingIdentityEffect      |  
823 //            \         /                    |
824 //             AddEffect                     |
825 //
826 // and verifies that it gives the correct output.
827 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
828         float data[] = {
829                 1.0f, 1.0f,
830                 1.0f, 0.0f,
831         };
832         float expected_data[] = {
833                 2.5f, 2.5f,
834                 2.5f, 0.0f,
835         };
836         float out_data[2 * 2];
837
838         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
839         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
840
841         MultiplyEffect *mul_half = new MultiplyEffect();
842         ASSERT_TRUE(mul_half->set_vec4("factor", half));
843         
844         MultiplyEffect *mul_two = new MultiplyEffect();
845         ASSERT_TRUE(mul_two->set_vec4("factor", two));
846         
847         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
848
849         EffectChainTester tester(NULL, 2, 2);
850
851         ImageFormat format;
852         format.color_space = COLORSPACE_sRGB;
853         format.gamma_curve = GAMMA_LINEAR;
854
855         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
856         input->set_pixel_data(data);
857
858         tester.get_chain()->add_input(input);
859         tester.get_chain()->add_effect(mul_half, input);
860         tester.get_chain()->add_effect(mul_two, input);
861         tester.get_chain()->add_effect(bounce, mul_two);
862         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
863         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
864
865         expect_equal(expected_data, out_data, 2, 2);
866 }
867
868 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
869         float data[] = {
870                 0.735f, 0.0f,
871                 0.735f, 0.0f,
872         };
873         float expected_data[] = {
874                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
875                 0.0f, 0.5f,
876         };
877         float out_data[2 * 2];
878         
879         EffectChainTester tester(NULL, 2, 2);
880         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
881
882         // MirrorEffect does not get linear light, so the conversions will be
883         // inserted after it, not before.
884         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
885         tester.get_chain()->add_effect(effect);
886
887         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
888         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
889         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
890         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
891
892         expect_equal(expected_data, out_data, 2, 2);
893
894         Node *node = effect->replaced_node;
895         ASSERT_EQ(1, node->incoming_links.size());
896         ASSERT_EQ(1, node->outgoing_links.size());
897         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
898         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
899 }
900
901 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
902         float data[] = {
903                 0.5f, 0.0f,
904                 0.5f, 0.0f,
905         };
906         float expected_data[] = {
907                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
908                 0.0f, 0.5f,
909         };
910         float out_data[2 * 2];
911         
912         EffectChainTester tester(NULL, 2, 2);
913         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
914
915         // MirrorEffect does not get linear light, so the conversions will be
916         // inserted after it, not before.
917         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
918         tester.get_chain()->add_effect(effect);
919
920         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
921         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
922         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
923         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
924
925         expect_equal(expected_data, out_data, 2, 2);
926
927         Node *node = effect->replaced_node;
928         ASSERT_EQ(1, node->incoming_links.size());
929         ASSERT_EQ(1, node->outgoing_links.size());
930         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
931         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
932 }
933
934 // An effect that does nothing, but requests texture bounce and stores
935 // its input size.
936 class SizeStoringEffect : public BouncingIdentityEffect {
937 public:
938         SizeStoringEffect() : input_width(-1), input_height(-1) {}
939         virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
940                 assert(input_num == 0);
941                 input_width = width;
942                 input_height = height;
943         }
944         virtual string effect_type_id() const { return "SizeStoringEffect"; }
945
946         int input_width, input_height;
947 };
948
949 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
950         float data[2 * 2] = {
951                 0.0f, 0.0f,
952                 0.0f, 0.0f,
953         };
954         float out_data[4 * 3];
955         
956         EffectChainTester tester(NULL, 4, 3);  // Note non-square aspect.
957
958         ImageFormat format;
959         format.color_space = COLORSPACE_sRGB;
960         format.gamma_curve = GAMMA_LINEAR;
961
962         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
963         input1->set_pixel_data(data);
964         
965         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
966         input2->set_pixel_data(data);
967
968         SizeStoringEffect *input_store = new SizeStoringEffect();
969
970         tester.get_chain()->add_input(input1);
971         tester.get_chain()->add_input(input2);
972         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
973         tester.get_chain()->add_effect(input_store);
974         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
975
976         EXPECT_EQ(2, input_store->input_width);
977         EXPECT_EQ(2, input_store->input_height);
978 }
979
980 TEST(EffectChainTest, AspectRatioConversion) {
981         float data1[4 * 3] = {
982                 0.0f, 0.0f, 0.0f, 0.0f,
983                 0.0f, 0.0f, 0.0f, 0.0f,
984                 0.0f, 0.0f, 0.0f, 0.0f,
985         };
986         float data2[7 * 7] = {
987                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
988                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
989                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
990                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
991                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
992                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
993                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
994         };
995
996         // The right conversion here is that the 7x7 image decides the size,
997         // since it is the biggest, so everything is scaled up to 9x7
998         // (keep the height, round the width 9.333 to 9). 
999         float out_data[9 * 7];
1000         
1001         EffectChainTester tester(NULL, 4, 3);
1002
1003         ImageFormat format;
1004         format.color_space = COLORSPACE_sRGB;
1005         format.gamma_curve = GAMMA_LINEAR;
1006
1007         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1008         input1->set_pixel_data(data1);
1009         
1010         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1011         input2->set_pixel_data(data2);
1012
1013         SizeStoringEffect *input_store = new SizeStoringEffect();
1014
1015         tester.get_chain()->add_input(input1);
1016         tester.get_chain()->add_input(input2);
1017         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1018         tester.get_chain()->add_effect(input_store);
1019         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1020
1021         EXPECT_EQ(9, input_store->input_width);
1022         EXPECT_EQ(7, input_store->input_height);
1023 }
1024
1025 // Tests that putting a BlueInput (constant color) into its own pass,
1026 // which creates a phase that doesn't need texture coordinates,
1027 // doesn't mess up a second phase that actually does.
1028 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1029         const int size = 2;
1030         float data[] = {
1031                 1.0f,
1032                 0.0f,
1033         };
1034         float expected_data[] = {
1035                 1.0f, 1.0f, 2.0f, 2.0f,
1036                 0.0f, 0.0f, 1.0f, 2.0f,
1037         };
1038         float out_data[size * 4];
1039         // First say that we have sRGB, linear input.
1040         ImageFormat format;
1041         format.color_space = COLORSPACE_sRGB;
1042         format.gamma_curve = GAMMA_LINEAR;
1043         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1044
1045         input->set_pixel_data(data);
1046         EffectChainTester tester(NULL, 1, size);
1047         tester.get_chain()->add_input(new BlueInput());
1048         Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1049         tester.get_chain()->add_input(input);
1050         tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1051
1052         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1053
1054         expect_equal(expected_data, out_data, 4, size);
1055 }
1056
1057 // An effect that does nothing except changing its output sizes.
1058 class VirtualResizeEffect : public Effect {
1059 public:
1060         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1061                 : width(width),
1062                   height(height),
1063                   virtual_width(virtual_width),
1064                   virtual_height(virtual_height) {}
1065         virtual string effect_type_id() const { return "VirtualResizeEffect"; }
1066         string output_fragment_shader() { return read_file("identity.frag"); }
1067
1068         virtual bool changes_output_size() const { return true; }
1069
1070         virtual void get_output_size(unsigned *width, unsigned *height,
1071                                      unsigned *virtual_width, unsigned *virtual_height) const {
1072                 *width = this->width;
1073                 *height = this->height;
1074                 *virtual_width = this->virtual_width;
1075                 *virtual_height = this->virtual_height;
1076         }
1077
1078 private:
1079         int width, height, virtual_width, virtual_height;
1080 };
1081
1082 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1083         const int size = 2, bigger_size = 3;
1084         float data[size * size] = {
1085                 1.0f, 0.0f,
1086                 0.0f, 1.0f,
1087         };
1088         float out_data[size * size];
1089         
1090         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1091
1092         SizeStoringEffect *size_store = new SizeStoringEffect();
1093
1094         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1095         tester.get_chain()->add_effect(size_store);
1096         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1097         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1098
1099         EXPECT_EQ(bigger_size, size_store->input_width);
1100         EXPECT_EQ(bigger_size, size_store->input_height);
1101
1102         // If the resize is implemented as non-virtual, we'll fail here,
1103         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1104         expect_equal(data, out_data, size, size);
1105 }
1106
1107 // An effect that is like VirtualResizeEffect, but always has virtual and real
1108 // sizes the same (and promises this).
1109 class NonVirtualResizeEffect : public VirtualResizeEffect {
1110 public:
1111         NonVirtualResizeEffect(int width, int height)
1112                 : VirtualResizeEffect(width, height, width, height) {}
1113         virtual string effect_type_id() const { return "NonVirtualResizeEffect"; }
1114         virtual bool sets_virtual_output_size() const { return false; }
1115 };
1116
1117 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1118 class OneToOneEffect : public Effect {
1119 public:
1120         OneToOneEffect() {}
1121         virtual string effect_type_id() const { return "OneToOneEffect"; }
1122         string output_fragment_shader() { return read_file("identity.frag"); }
1123         virtual bool one_to_one_sampling() const { return true; }
1124 };
1125
1126 TEST(EffectChainTest, NoBounceWithOneToOneSampling) {
1127         const int size = 2;
1128         float data[size * size] = {
1129                 1.0f, 0.0f,
1130                 0.0f, 1.0f,
1131         };
1132         float out_data[size * size];
1133
1134         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1135
1136         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1137         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1138
1139         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1140         tester.get_chain()->add_effect(effect1);
1141         tester.get_chain()->add_effect(effect2);
1142         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1143
1144         expect_equal(data, out_data, size, size);
1145
1146         // The first OneToOneEffect should be in the same phase as its input.
1147         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1148         EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1149                   effect1->replaced_node->containing_phase);
1150
1151         // The second OneToOneEffect, too.
1152         EXPECT_EQ(effect1->replaced_node->containing_phase,
1153                   effect2->replaced_node->containing_phase);
1154 }
1155
1156 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1157         const int size = 2;
1158         float data[size * size] = {
1159                 1.0f, 0.0f,
1160                 0.0f, 1.0f,
1161         };
1162         float out_data[size * size];
1163
1164         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1165
1166         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1167         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1168         RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1169         RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1170
1171         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1172         tester.get_chain()->add_effect(effect1);
1173         tester.get_chain()->add_effect(effect2);
1174         tester.get_chain()->add_effect(effect3);
1175         tester.get_chain()->add_effect(effect4);
1176         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1177
1178         expect_equal(data, out_data, size, size);
1179
1180         // The NonVirtualResizeEffect should be in a different phase from
1181         // the IdentityEffect (since the latter is not one-to-one),
1182         // ie., the chain should be broken somewhere between them, but exactly
1183         // where doesn't matter.
1184         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1185         EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1186                   effect3->replaced_node->containing_phase);
1187
1188         // The last OneToOneEffect should also be in the same phase as the
1189         // IdentityEffect (the phase was already broken).
1190         EXPECT_EQ(effect3->replaced_node->containing_phase,
1191                   effect4->replaced_node->containing_phase);
1192 }
1193
1194 // Does not use EffectChainTest, so that it can construct an EffectChain without
1195 // a shared ResourcePool (which is also properly destroyed afterwards).
1196 // Also turns on debugging to test that code path.
1197 TEST(EffectChainTest, IdentityWithOwnPool) {
1198         const int width = 3, height = 2;
1199         float data[] = {
1200                 0.0f, 0.25f, 0.3f,
1201                 0.75f, 1.0f, 1.0f,
1202         };
1203         const float expected_data[] = {
1204                 0.75f, 1.0f, 1.0f,
1205                 0.0f, 0.25f, 0.3f,
1206         };
1207         float out_data[6], temp[6 * 4];
1208
1209         EffectChain chain(width, height);
1210         movit_debug_level = MOVIT_DEBUG_ON;
1211
1212         ImageFormat format;
1213         format.color_space = COLORSPACE_sRGB;
1214         format.gamma_curve = GAMMA_LINEAR;
1215
1216         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1217         input->set_pixel_data(data);
1218         chain.add_input(input);
1219         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1220
1221         GLuint texnum, fbo;
1222         glGenTextures(1, &texnum);
1223         check_error();
1224         glBindTexture(GL_TEXTURE_2D, texnum);
1225         check_error();
1226         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
1227         check_error();
1228
1229         glGenFramebuffers(1, &fbo);
1230         check_error();
1231         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1232         check_error();
1233         glFramebufferTexture2D(
1234                 GL_FRAMEBUFFER,
1235                 GL_COLOR_ATTACHMENT0,
1236                 GL_TEXTURE_2D,
1237                 texnum,
1238                 0);
1239         check_error();
1240         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1241         check_error();
1242
1243         chain.finalize();
1244
1245         chain.render_to_fbo(fbo, width, height);
1246
1247         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1248         check_error();
1249         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1250         check_error();
1251         for (unsigned i = 0; i < 6; ++i) {
1252                 out_data[i] = temp[i * 4];
1253         }
1254
1255         expect_equal(expected_data, out_data, width, height);
1256
1257         // Reset the debug status again.
1258         movit_debug_level = MOVIT_DEBUG_OFF;
1259 }
1260
1261 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1262 class PrintfingBlueEffect : public Effect {
1263 public:
1264         PrintfingBlueEffect() {}
1265         virtual string effect_type_id() const { return "PrintfingBlueEffect"; }
1266         string output_fragment_shader() {
1267                 stringstream ss;
1268                 ss.imbue(locale("C"));
1269                 ss.precision(8);
1270                 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1271                    << 0.0f << ", " << 0.0f << ", "
1272                    << 0.5f << ", " << 1.0f << "); }\n";
1273                 return ss.str();
1274         }
1275 };
1276
1277 TEST(EffectChainTest, StringStreamLocalesWork) {
1278         // An example of a locale with comma instead of period as decimal separator.
1279         // Obviously, if you run on a machine without this locale available,
1280         // the test will always succeed. Note that the OpenGL driver might call
1281         // setlocale() behind-the-scenes, and that might corrupt the returned
1282         // pointer, so we need to take our own copy of it here.
1283         char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1284         if (saved_locale == NULL) {
1285                 // The locale wasn't available.
1286                 return;
1287         }
1288         saved_locale = strdup(saved_locale);
1289         float data[] = {
1290                 0.0f, 0.0f, 0.0f, 0.0f,
1291         };
1292         float expected_data[] = {
1293                 0.0f, 0.0f, 0.5f, 1.0f,
1294         };
1295         float out_data[4];
1296         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1297         tester.get_chain()->add_effect(new PrintfingBlueEffect());
1298         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1299
1300         expect_equal(expected_data, out_data, 4, 1);
1301
1302         setlocale(LC_ALL, saved_locale);
1303         free(saved_locale);
1304 }
1305
1306 TEST(EffectChainTest, sRGBIntermediate) {
1307         float data[] = {
1308                 0.0f, 0.5f, 0.0f, 1.0f,
1309         };
1310         float out_data[4];
1311         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA16F_ARB, GL_SRGB8);
1312         tester.get_chain()->add_effect(new IdentityEffect());
1313         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1314         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1315
1316         EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1317             << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1318         EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1319             << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1320 }
1321
1322 }  // namespace movit