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