]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
Disable dither explicitly per frame; fixes some weird artifacts I found.
[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[2 * 2];
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 // An effect that does nothing except changing its output sizes.
1026 class VirtualResizeEffect : public Effect {
1027 public:
1028         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1029                 : width(width),
1030                   height(height),
1031                   virtual_width(virtual_width),
1032                   virtual_height(virtual_height) {}
1033         virtual string effect_type_id() const { return "VirtualResizeEffect"; }
1034         string output_fragment_shader() { return read_file("identity.frag"); }
1035
1036         virtual bool changes_output_size() const { return true; }
1037
1038         virtual void get_output_size(unsigned *width, unsigned *height,
1039                                      unsigned *virtual_width, unsigned *virtual_height) const {
1040                 *width = this->width;
1041                 *height = this->height;
1042                 *virtual_width = this->virtual_width;
1043                 *virtual_height = this->virtual_height;
1044         }
1045
1046 private:
1047         int width, height, virtual_width, virtual_height;
1048 };
1049
1050 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1051         const int size = 2, bigger_size = 3;
1052         float data[size * size] = {
1053                 1.0f, 0.0f,
1054                 0.0f, 1.0f,
1055         };
1056         float out_data[size * size];
1057         
1058         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1059
1060         SizeStoringEffect *size_store = new SizeStoringEffect();
1061
1062         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1063         tester.get_chain()->add_effect(size_store);
1064         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1065         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1066
1067         EXPECT_EQ(bigger_size, size_store->input_width);
1068         EXPECT_EQ(bigger_size, size_store->input_height);
1069
1070         // If the resize is implemented as non-virtual, we'll fail here,
1071         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1072         expect_equal(data, out_data, size, size);
1073 }
1074
1075 // An effect that is like VirtualResizeEffect, but always has virtual and real
1076 // sizes the same (and promises this).
1077 class NonVirtualResizeEffect : public VirtualResizeEffect {
1078 public:
1079         NonVirtualResizeEffect(int width, int height)
1080                 : VirtualResizeEffect(width, height, width, height) {}
1081         virtual string effect_type_id() const { return "NonVirtualResizeEffect"; }
1082         virtual bool sets_virtual_output_size() const { return false; }
1083 };
1084
1085 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1086 class OneToOneEffect : public Effect {
1087 public:
1088         OneToOneEffect() {}
1089         virtual string effect_type_id() const { return "OneToOneEffect"; }
1090         string output_fragment_shader() { return read_file("identity.frag"); }
1091         virtual bool one_to_one_sampling() const { return true; }
1092 };
1093
1094 TEST(EffectChainTest, NoBounceWithOneToOneSampling) {
1095         const int size = 2;
1096         float data[size * size] = {
1097                 1.0f, 0.0f,
1098                 0.0f, 1.0f,
1099         };
1100         float out_data[size * size];
1101
1102         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1103
1104         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1105         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1106
1107         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1108         tester.get_chain()->add_effect(effect1);
1109         tester.get_chain()->add_effect(effect2);
1110         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1111
1112         expect_equal(data, out_data, size, size);
1113
1114         // The first OneToOneEffect should be in the same phase as its input.
1115         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1116         EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1117                   effect1->replaced_node->containing_phase);
1118
1119         // The second OneToOneEffect, too.
1120         EXPECT_EQ(effect1->replaced_node->containing_phase,
1121                   effect2->replaced_node->containing_phase);
1122 }
1123
1124 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1125         const int size = 2;
1126         float data[size * size] = {
1127                 1.0f, 0.0f,
1128                 0.0f, 1.0f,
1129         };
1130         float out_data[size * size];
1131
1132         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1133
1134         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1135         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1136         RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1137         RewritingEffect<OneToOneEffect> *effect4 = 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.get_chain()->add_effect(effect3);
1143         tester.get_chain()->add_effect(effect4);
1144         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1145
1146         expect_equal(data, out_data, size, size);
1147
1148         // The NonVirtualResizeEffect should be in a different phase from
1149         // the IdentityEffect (since the latter is not one-to-one),
1150         // ie., the chain should be broken somewhere between them, but exactly
1151         // where doesn't matter.
1152         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1153         EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1154                   effect3->replaced_node->containing_phase);
1155
1156         // The last OneToOneEffect should also be in the same phase as the
1157         // IdentityEffect (the phase was already broken).
1158         EXPECT_EQ(effect3->replaced_node->containing_phase,
1159                   effect4->replaced_node->containing_phase);
1160 }
1161
1162 // Does not use EffectChainTest, so that it can construct an EffectChain without
1163 // a shared ResourcePool (which is also properly destroyed afterwards).
1164 // Also turns on debugging to test that code path.
1165 TEST(EffectChainTest, IdentityWithOwnPool) {
1166         const int width = 3, height = 2;
1167         float data[] = {
1168                 0.0f, 0.25f, 0.3f,
1169                 0.75f, 1.0f, 1.0f,
1170         };
1171         const float expected_data[] = {
1172                 0.75f, 1.0f, 1.0f,
1173                 0.0f, 0.25f, 0.3f,
1174         };
1175         float out_data[6], temp[6 * 4];
1176
1177         EffectChain chain(width, height);
1178         movit_debug_level = MOVIT_DEBUG_ON;
1179
1180         ImageFormat format;
1181         format.color_space = COLORSPACE_sRGB;
1182         format.gamma_curve = GAMMA_LINEAR;
1183
1184         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1185         input->set_pixel_data(data);
1186         chain.add_input(input);
1187         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1188
1189         GLuint texnum, fbo;
1190         glGenTextures(1, &texnum);
1191         check_error();
1192         glBindTexture(GL_TEXTURE_2D, texnum);
1193         check_error();
1194         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
1195         check_error();
1196
1197         glGenFramebuffers(1, &fbo);
1198         check_error();
1199         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1200         check_error();
1201         glFramebufferTexture2D(
1202                 GL_FRAMEBUFFER,
1203                 GL_COLOR_ATTACHMENT0,
1204                 GL_TEXTURE_2D,
1205                 texnum,
1206                 0);
1207         check_error();
1208         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1209         check_error();
1210
1211         chain.finalize();
1212
1213         chain.render_to_fbo(fbo, width, height);
1214
1215         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1216         check_error();
1217         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1218         check_error();
1219         for (unsigned i = 0; i < 6; ++i) {
1220                 out_data[i] = temp[i * 4];
1221         }
1222
1223         expect_equal(expected_data, out_data, width, height);
1224
1225         // Reset the debug status again.
1226         movit_debug_level = MOVIT_DEBUG_OFF;
1227 }
1228
1229 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1230 class PrintfingBlueEffect : public Effect {
1231 public:
1232         PrintfingBlueEffect() {}
1233         virtual string effect_type_id() const { return "PrintfingBlueEffect"; }
1234         string output_fragment_shader() {
1235                 stringstream ss;
1236                 ss.imbue(locale("C"));
1237                 ss.precision(8);
1238                 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1239                    << 0.0f << ", " << 0.0f << ", "
1240                    << 0.5f << ", " << 1.0f << "); }\n";
1241                 return ss.str();
1242         }
1243 };
1244
1245 TEST(EffectChainTest, StringStreamLocalesWork) {
1246         // An example of a locale with comma instead of period as decimal separator.
1247         // Obviously, if you run on a machine without this locale available,
1248         // the test will always succeed. Note that the OpenGL driver might call
1249         // setlocale() behind-the-scenes, and that might corrupt the returned
1250         // pointer, so we need to take our own copy of it here.
1251         char *saved_locale = strdup(setlocale(LC_ALL, "nb_NO.UTF_8"));
1252         float data[] = {
1253                 0.0f, 0.0f, 0.0f, 0.0f,
1254         };
1255         float expected_data[] = {
1256                 0.0f, 0.0f, 0.5f, 1.0f,
1257         };
1258         float out_data[4];
1259         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1260         tester.get_chain()->add_effect(new PrintfingBlueEffect());
1261         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1262
1263         expect_equal(expected_data, out_data, 4, 1);
1264
1265         setlocale(LC_ALL, saved_locale);
1266         free(saved_locale);
1267 }
1268
1269
1270 }  // namespace movit