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