551cdbcaabd607d1cc50f762f00190e1b99a3816
[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         string effect_type_id() const override { return "IdentityEffect"; }
46         string output_fragment_shader() override { 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         string effect_type_id() const override { return "IdentityEffect"; }
67         string output_fragment_shader() override { return read_file("identity.frag"); }
68         bool needs_texture_bounce() const override { return true; }
69         AlphaHandling alpha_handling() const override { 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         string effect_type_id() const override { return "InvertEffect"; }
126         string output_fragment_shader() override { 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         AlphaHandling alpha_handling() const override { 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         string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
141         string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
142         void rewrite_graph(EffectChain *graph, Node *self) override {
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         string effect_type_id() const override { 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 override { return overridden_color_space; }
247         GammaCurve get_gamma_curve() const override { 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         string effect_type_id() const override { return "IdentityEffect"; }
424         string output_fragment_shader() override { return read_file("blue.frag"); }
425         AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
426         bool can_output_linear_gamma() const override { return true; }
427         unsigned get_width() const override { return 1; }
428         unsigned get_height() const override { return 1; }
429         Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
430         GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
431
432 private:
433         int needs_mipmaps;
434 };
435
436 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
437 // which outputs blank alpha.
438 class RewritingToBlueInput : public Input {
439 public:
440         RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
441         string effect_type_id() const override { return "RewritingToBlueInput"; }
442         string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
443         void rewrite_graph(EffectChain *graph, Node *self) override {
444                 Node *blue_node = graph->add_node(new BlueInput());
445                 graph->replace_receiver(self, blue_node);
446                 graph->replace_sender(self, blue_node);
447
448                 self->disabled = true;
449                 this->blue_node = blue_node;
450         }
451
452         // Dummy values that we need to implement because we inherit from Input.
453         // Same as BlueInput.
454         AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
455         bool can_output_linear_gamma() const override { return true; }
456         unsigned get_width() const override { return 1; }
457         unsigned get_height() const override { return 1; }
458         Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
459         GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
460
461         Node *blue_node;
462
463 private:
464         int needs_mipmaps;
465 };
466
467 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
468         const int size = 3;
469         float data[4 * size] = {
470                 0.0f, 0.0f, 1.0f, 1.0f,
471                 0.0f, 0.0f, 1.0f, 1.0f,
472                 0.0f, 0.0f, 1.0f, 1.0f,
473         };
474         float out_data[4 * size];
475         EffectChainTester tester(nullptr, size, 1);
476         RewritingToBlueInput *input = new RewritingToBlueInput();
477         tester.get_chain()->add_input(input);
478         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
479
480         Node *node = input->blue_node;
481         EXPECT_EQ(0, node->incoming_links.size());
482         EXPECT_EQ(0, node->outgoing_links.size());
483
484         expect_equal(data, out_data, 4, size);
485 }
486
487 // An effect that does nothing, and specifies that it preserves blank alpha.
488 class BlankAlphaPreservingEffect : public Effect {
489 public:
490         BlankAlphaPreservingEffect() {}
491         string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
492         string output_fragment_shader() override { return read_file("identity.frag"); }
493         AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
494 };
495
496 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
497         const int size = 3;
498         float data[4 * size] = {
499                 0.0f, 0.0f, 1.0f, 1.0f,
500                 0.0f, 0.0f, 1.0f, 1.0f,
501                 0.0f, 0.0f, 1.0f, 1.0f,
502         };
503         float out_data[4 * size];
504         EffectChainTester tester(nullptr, size, 1);
505         tester.get_chain()->add_input(new BlueInput());
506         tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
507         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
508         tester.get_chain()->add_effect(effect);
509         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
510
511         Node *node = effect->replaced_node;
512         EXPECT_EQ(1, node->incoming_links.size());
513         EXPECT_EQ(0, node->outgoing_links.size());
514
515         expect_equal(data, out_data, 4, size);
516 }
517
518 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
519 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
520 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
521 // with other tests.)
522 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
523         const int size = 3;
524         float data[4 * size] = {
525                 0.0f, 0.0f, 1.0f, 1.0f,
526                 0.0f, 0.0f, 1.0f, 1.0f,
527                 0.0f, 0.0f, 1.0f, 1.0f,
528         };
529         float out_data[4 * size];
530         EffectChainTester tester(nullptr, size, 1);
531         tester.get_chain()->add_input(new BlueInput());
532         tester.get_chain()->add_effect(new IdentityEffect());  // Not BlankAlphaPreservingEffect.
533         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
534         tester.get_chain()->add_effect(effect);
535         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
536
537         Node *node = effect->replaced_node;
538         EXPECT_EQ(1, node->incoming_links.size());
539         EXPECT_EQ(1, node->outgoing_links.size());
540         EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
541
542         expect_equal(data, out_data, 4, size);
543 }
544
545 // Effectively scales down its input linearly by 4x (and repeating it),
546 // which is not attainable without mipmaps.
547 class MipmapNeedingEffect : public Effect {
548 public:
549         MipmapNeedingEffect() {}
550         bool needs_mipmaps() const override { return true; }
551
552         // To be allowed to mess with the sampler state.
553         bool needs_texture_bounce() const override { return true; }
554
555         string effect_type_id() const override { return "MipmapNeedingEffect"; }
556         string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
557         void inform_added(EffectChain *chain) override { this->chain = chain; }
558
559         void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
560         {
561                 Node *self = chain->find_node_for_effect(this);
562                 glActiveTexture(chain->get_input_sampler(self, 0));
563                 check_error();
564                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
565                 check_error();
566                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
567                 check_error();
568         }
569
570 private:
571         EffectChain *chain;
572 };
573
574 TEST(EffectChainTest, MipmapGenerationWorks) {
575         float data[] = {  // In 4x4 blocks.
576                 1.0f, 0.0f, 0.0f, 0.0f,
577                 0.0f, 0.0f, 0.0f, 0.0f,
578                 0.0f, 0.0f, 0.0f, 0.0f,
579                 0.0f, 0.0f, 0.0f, 1.0f,
580
581                 0.0f, 0.0f, 0.0f, 0.0f,
582                 0.0f, 0.5f, 0.0f, 0.0f,
583                 0.0f, 0.0f, 1.0f, 0.0f,
584                 0.0f, 0.0f, 0.0f, 0.0f,
585
586                 1.0f, 1.0f, 1.0f, 1.0f,
587                 1.0f, 1.0f, 1.0f, 1.0f,
588                 1.0f, 1.0f, 1.0f, 1.0f,
589                 1.0f, 1.0f, 1.0f, 1.0f,
590
591                 0.0f, 0.0f, 0.0f, 0.0f,
592                 0.0f, 1.0f, 1.0f, 0.0f,
593                 0.0f, 1.0f, 1.0f, 0.0f,
594                 0.0f, 0.0f, 0.0f, 0.0f,
595         };
596         float expected_data[] = {  // Repeated four times each way.
597                 0.125f,   0.125f,   0.125f,   0.125f,
598                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
599                 1.0f,     1.0f,     1.0f,     1.0f,
600                 0.25f,    0.25f,    0.25f,    0.25f,
601
602                 0.125f,   0.125f,   0.125f,   0.125f,
603                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
604                 1.0f,     1.0f,     1.0f,     1.0f,
605                 0.25f,    0.25f,    0.25f,    0.25f,
606
607                 0.125f,   0.125f,   0.125f,   0.125f,
608                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
609                 1.0f,     1.0f,     1.0f,     1.0f,
610                 0.25f,    0.25f,    0.25f,    0.25f,
611
612                 0.125f,   0.125f,   0.125f,   0.125f,
613                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
614                 1.0f,     1.0f,     1.0f,     1.0f,
615                 0.25f,    0.25f,    0.25f,    0.25f,
616         };
617         float out_data[4 * 16];
618         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
619         tester.get_chain()->add_effect(new MipmapNeedingEffect());
620         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
621
622         expect_equal(expected_data, out_data, 4, 16);
623 }
624
625 class NonMipmapCapableInput : public FlatInput {
626 public:
627         NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
628                 : FlatInput(format, pixel_format, type, width, height) {}
629
630         bool can_supply_mipmaps() const override { return false; }
631         bool set_int(const std::string& key, int value) override {
632                 if (key == "needs_mipmaps") {
633                         assert(value == 0);
634                 }
635                 return FlatInput::set_int(key, value);
636         }
637 };
638
639 // The same test as MipmapGenerationWorks, but with an input that refuses
640 // to supply mipmaps.
641 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
642         float data[] = {  // In 4x4 blocks.
643                 1.0f, 0.0f, 0.0f, 0.0f,
644                 0.0f, 0.0f, 0.0f, 0.0f,
645                 0.0f, 0.0f, 0.0f, 0.0f,
646                 0.0f, 0.0f, 0.0f, 1.0f,
647
648                 0.0f, 0.0f, 0.0f, 0.0f,
649                 0.0f, 0.5f, 0.0f, 0.0f,
650                 0.0f, 0.0f, 1.0f, 0.0f,
651                 0.0f, 0.0f, 0.0f, 0.0f,
652
653                 1.0f, 1.0f, 1.0f, 1.0f,
654                 1.0f, 1.0f, 1.0f, 1.0f,
655                 1.0f, 1.0f, 1.0f, 1.0f,
656                 1.0f, 1.0f, 1.0f, 1.0f,
657
658                 0.0f, 0.0f, 0.0f, 0.0f,
659                 0.0f, 1.0f, 1.0f, 0.0f,
660                 0.0f, 1.0f, 1.0f, 0.0f,
661                 0.0f, 0.0f, 0.0f, 0.0f,
662         };
663         float expected_data[] = {  // Repeated four times each way.
664                 0.125f,   0.125f,   0.125f,   0.125f,
665                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
666                 1.0f,     1.0f,     1.0f,     1.0f,
667                 0.25f,    0.25f,    0.25f,    0.25f,
668
669                 0.125f,   0.125f,   0.125f,   0.125f,
670                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
671                 1.0f,     1.0f,     1.0f,     1.0f,
672                 0.25f,    0.25f,    0.25f,    0.25f,
673
674                 0.125f,   0.125f,   0.125f,   0.125f,
675                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
676                 1.0f,     1.0f,     1.0f,     1.0f,
677                 0.25f,    0.25f,    0.25f,    0.25f,
678
679                 0.125f,   0.125f,   0.125f,   0.125f,
680                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
681                 1.0f,     1.0f,     1.0f,     1.0f,
682                 0.25f,    0.25f,    0.25f,    0.25f,
683         };
684         float out_data[4 * 16];
685         EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
686
687         ImageFormat format;
688         format.color_space = COLORSPACE_sRGB;
689         format.gamma_curve = GAMMA_LINEAR;
690
691         NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
692         input->set_pixel_data(data);
693         tester.get_chain()->add_input(input);
694         tester.get_chain()->add_effect(new MipmapNeedingEffect());
695         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
696
697         expect_equal(expected_data, out_data, 4, 16);
698 }
699
700 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
701         float data[] = {  // In 4x4 blocks.
702                 1.0f, 0.0f, 0.0f, 0.0f,
703                 0.0f, 0.0f, 0.0f, 0.0f,
704                 0.0f, 0.0f, 0.0f, 0.0f,
705                 0.0f, 0.0f, 0.0f, 1.0f,
706
707                 0.0f, 0.0f, 0.0f, 0.0f,
708                 0.0f, 0.5f, 0.0f, 0.0f,
709                 0.0f, 0.0f, 1.0f, 0.0f,
710                 0.0f, 0.0f, 0.0f, 0.0f,
711
712                 1.0f, 1.0f, 1.0f, 1.0f,
713                 1.0f, 1.0f, 1.0f, 1.0f,
714                 1.0f, 1.0f, 1.0f, 1.0f,
715                 1.0f, 1.0f, 1.0f, 1.0f,
716
717                 0.0f, 0.0f, 0.0f, 0.0f,
718                 0.0f, 1.0f, 1.0f, 0.0f,
719                 0.0f, 1.0f, 1.0f, 0.0f,
720                 0.0f, 0.0f, 0.0f, 0.0f,
721         };
722         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
723                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
724                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
725                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
726                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
727                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
728                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
729                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
730                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
731                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
732                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
733                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
734                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
735                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
736                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
737                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
738                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
739         };
740         float out_data[4 * 16];
741
742         ResizeEffect *downscale = new ResizeEffect();
743         ASSERT_TRUE(downscale->set_int("width", 1));
744         ASSERT_TRUE(downscale->set_int("height", 4));
745
746         ResizeEffect *upscale = new ResizeEffect();
747         ASSERT_TRUE(upscale->set_int("width", 4));
748         ASSERT_TRUE(upscale->set_int("height", 16));
749
750         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
751         tester.get_chain()->add_effect(downscale);
752         tester.get_chain()->add_effect(upscale);
753         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
754
755         expect_equal(expected_data, out_data, 4, 16);
756 }
757
758 // An effect that adds its two inputs together. Used below.
759 class AddEffect : public Effect {
760 public:
761         AddEffect() {}
762         string effect_type_id() const override { return "AddEffect"; }
763         string output_fragment_shader() override { return read_file("add.frag"); }
764         unsigned num_inputs() const override { return 2; }
765         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
766 };
767
768 // Constructs the graph
769 //
770 //             FlatInput               |
771 //            /         \              |
772 //  MultiplyEffect  MultiplyEffect     |
773 //            \         /              |
774 //             AddEffect               |
775 //
776 // and verifies that it gives the correct output.
777 TEST(EffectChainTest, DiamondGraph) {
778         float data[] = {
779                 1.0f, 1.0f,
780                 1.0f, 0.0f,
781         };
782         float expected_data[] = {
783                 2.5f, 2.5f,
784                 2.5f, 0.0f,
785         };
786         float out_data[2 * 2];
787
788         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
789         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
790
791         MultiplyEffect *mul_half = new MultiplyEffect();
792         ASSERT_TRUE(mul_half->set_vec4("factor", half));
793         
794         MultiplyEffect *mul_two = new MultiplyEffect();
795         ASSERT_TRUE(mul_two->set_vec4("factor", two));
796
797         EffectChainTester tester(nullptr, 2, 2);
798
799         ImageFormat format;
800         format.color_space = COLORSPACE_sRGB;
801         format.gamma_curve = GAMMA_LINEAR;
802
803         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
804         input->set_pixel_data(data);
805
806         tester.get_chain()->add_input(input);
807         tester.get_chain()->add_effect(mul_half, input);
808         tester.get_chain()->add_effect(mul_two, input);
809         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
810         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
811
812         expect_equal(expected_data, out_data, 2, 2);
813 }
814
815 // Constructs the graph
816 //
817 //             FlatInput                     |
818 //            /         \                    |
819 //  MultiplyEffect  MultiplyEffect           |
820 //         \             |                   |
821 //          \    BouncingIdentityEffect      |  
822 //            \         /                    |
823 //             AddEffect                     |
824 //
825 // and verifies that it gives the correct output.
826 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
827         float data[] = {
828                 1.0f, 1.0f,
829                 1.0f, 0.0f,
830         };
831         float expected_data[] = {
832                 2.5f, 2.5f,
833                 2.5f, 0.0f,
834         };
835         float out_data[2 * 2];
836
837         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
838         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
839
840         MultiplyEffect *mul_half = new MultiplyEffect();
841         ASSERT_TRUE(mul_half->set_vec4("factor", half));
842         
843         MultiplyEffect *mul_two = new MultiplyEffect();
844         ASSERT_TRUE(mul_two->set_vec4("factor", two));
845         
846         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
847
848         EffectChainTester tester(nullptr, 2, 2);
849
850         ImageFormat format;
851         format.color_space = COLORSPACE_sRGB;
852         format.gamma_curve = GAMMA_LINEAR;
853
854         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
855         input->set_pixel_data(data);
856
857         tester.get_chain()->add_input(input);
858         tester.get_chain()->add_effect(mul_half, input);
859         tester.get_chain()->add_effect(mul_two, input);
860         tester.get_chain()->add_effect(bounce, mul_two);
861         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
862         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
863
864         expect_equal(expected_data, out_data, 2, 2);
865 }
866
867 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
868         float data[] = {
869                 0.735f, 0.0f,
870                 0.735f, 0.0f,
871         };
872         float expected_data[] = {
873                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
874                 0.0f, 0.5f,
875         };
876         float out_data[2 * 2];
877         
878         EffectChainTester tester(nullptr, 2, 2);
879         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
880
881         // MirrorEffect does not get linear light, so the conversions will be
882         // inserted after it, not before.
883         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
884         tester.get_chain()->add_effect(effect);
885
886         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
887         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
888         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
889         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
890
891         expect_equal(expected_data, out_data, 2, 2);
892
893         Node *node = effect->replaced_node;
894         ASSERT_EQ(1, node->incoming_links.size());
895         ASSERT_EQ(1, node->outgoing_links.size());
896         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
897         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
898 }
899
900 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
901         float data[] = {
902                 0.5f, 0.0f,
903                 0.5f, 0.0f,
904         };
905         float expected_data[] = {
906                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
907                 0.0f, 0.5f,
908         };
909         float out_data[2 * 2];
910         
911         EffectChainTester tester(nullptr, 2, 2);
912         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
913
914         // MirrorEffect does not get linear light, so the conversions will be
915         // inserted after it, not before.
916         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
917         tester.get_chain()->add_effect(effect);
918
919         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
920         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
921         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
922         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
923
924         expect_equal(expected_data, out_data, 2, 2);
925
926         Node *node = effect->replaced_node;
927         ASSERT_EQ(1, node->incoming_links.size());
928         ASSERT_EQ(1, node->outgoing_links.size());
929         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
930         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
931 }
932
933 // An effect that does nothing, but requests texture bounce and stores
934 // its input size.
935 class SizeStoringEffect : public BouncingIdentityEffect {
936 public:
937         SizeStoringEffect() : input_width(-1), input_height(-1) {}
938         void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
939                 assert(input_num == 0);
940                 input_width = width;
941                 input_height = height;
942         }
943         string effect_type_id() const override { return "SizeStoringEffect"; }
944
945         int input_width, input_height;
946 };
947
948 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
949         float data[2 * 2] = {
950                 0.0f, 0.0f,
951                 0.0f, 0.0f,
952         };
953         float out_data[4 * 3];
954         
955         EffectChainTester tester(nullptr, 4, 3);  // Note non-square aspect.
956
957         ImageFormat format;
958         format.color_space = COLORSPACE_sRGB;
959         format.gamma_curve = GAMMA_LINEAR;
960
961         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
962         input1->set_pixel_data(data);
963         
964         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
965         input2->set_pixel_data(data);
966
967         SizeStoringEffect *input_store = new SizeStoringEffect();
968
969         tester.get_chain()->add_input(input1);
970         tester.get_chain()->add_input(input2);
971         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
972         tester.get_chain()->add_effect(input_store);
973         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
974
975         EXPECT_EQ(2, input_store->input_width);
976         EXPECT_EQ(2, input_store->input_height);
977 }
978
979 TEST(EffectChainTest, AspectRatioConversion) {
980         float data1[4 * 3] = {
981                 0.0f, 0.0f, 0.0f, 0.0f,
982                 0.0f, 0.0f, 0.0f, 0.0f,
983                 0.0f, 0.0f, 0.0f, 0.0f,
984         };
985         float data2[7 * 7] = {
986                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
987                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
988                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
989                 0.0f, 0.0f, 0.0f, 1.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, 0.0f, 0.0f, 0.0f, 0.0f,
992                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
993         };
994
995         // The right conversion here is that the 7x7 image decides the size,
996         // since it is the biggest, so everything is scaled up to 9x7
997         // (keep the height, round the width 9.333 to 9). 
998         float out_data[9 * 7];
999         
1000         EffectChainTester tester(nullptr, 4, 3);
1001
1002         ImageFormat format;
1003         format.color_space = COLORSPACE_sRGB;
1004         format.gamma_curve = GAMMA_LINEAR;
1005
1006         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1007         input1->set_pixel_data(data1);
1008         
1009         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1010         input2->set_pixel_data(data2);
1011
1012         SizeStoringEffect *input_store = new SizeStoringEffect();
1013
1014         tester.get_chain()->add_input(input1);
1015         tester.get_chain()->add_input(input2);
1016         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1017         tester.get_chain()->add_effect(input_store);
1018         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1019
1020         EXPECT_EQ(9, input_store->input_width);
1021         EXPECT_EQ(7, input_store->input_height);
1022 }
1023
1024 // Tests that putting a BlueInput (constant color) into its own pass,
1025 // which creates a phase that doesn't need texture coordinates,
1026 // doesn't mess up a second phase that actually does.
1027 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1028         const int size = 2;
1029         float data[] = {
1030                 1.0f,
1031                 0.0f,
1032         };
1033         float expected_data[] = {
1034                 1.0f, 1.0f, 2.0f, 2.0f,
1035                 0.0f, 0.0f, 1.0f, 2.0f,
1036         };
1037         float out_data[size * 4];
1038         // First say that we have sRGB, linear input.
1039         ImageFormat format;
1040         format.color_space = COLORSPACE_sRGB;
1041         format.gamma_curve = GAMMA_LINEAR;
1042         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1043
1044         input->set_pixel_data(data);
1045         EffectChainTester tester(nullptr, 1, size);
1046         tester.get_chain()->add_input(new BlueInput());
1047         Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1048         tester.get_chain()->add_input(input);
1049         tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1050
1051         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1052
1053         expect_equal(expected_data, out_data, 4, size);
1054 }
1055
1056 // An effect that does nothing except changing its output sizes.
1057 class VirtualResizeEffect : public Effect {
1058 public:
1059         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1060                 : width(width),
1061                   height(height),
1062                   virtual_width(virtual_width),
1063                   virtual_height(virtual_height) {}
1064         string effect_type_id() const override { return "VirtualResizeEffect"; }
1065         string output_fragment_shader() override { return read_file("identity.frag"); }
1066
1067         bool changes_output_size() const override { return true; }
1068
1069         void get_output_size(unsigned *width, unsigned *height,
1070                              unsigned *virtual_width, unsigned *virtual_height) const override {
1071                 *width = this->width;
1072                 *height = this->height;
1073                 *virtual_width = this->virtual_width;
1074                 *virtual_height = this->virtual_height;
1075         }
1076
1077 private:
1078         int width, height, virtual_width, virtual_height;
1079 };
1080
1081 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1082         const int size = 2, bigger_size = 3;
1083         float data[size * size] = {
1084                 1.0f, 0.0f,
1085                 0.0f, 1.0f,
1086         };
1087         float out_data[size * size];
1088         
1089         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1090
1091         SizeStoringEffect *size_store = new SizeStoringEffect();
1092
1093         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1094         tester.get_chain()->add_effect(size_store);
1095         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1096         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1097
1098         EXPECT_EQ(bigger_size, size_store->input_width);
1099         EXPECT_EQ(bigger_size, size_store->input_height);
1100
1101         // If the resize is implemented as non-virtual, we'll fail here,
1102         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1103         expect_equal(data, out_data, size, size);
1104 }
1105
1106 // An effect that is like VirtualResizeEffect, but always has virtual and real
1107 // sizes the same (and promises this).
1108 class NonVirtualResizeEffect : public VirtualResizeEffect {
1109 public:
1110         NonVirtualResizeEffect(int width, int height)
1111                 : VirtualResizeEffect(width, height, width, height) {}
1112         string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1113         bool sets_virtual_output_size() const override { return false; }
1114 };
1115
1116 class WithAndWithoutComputeShaderTest : public testing::TestWithParam<string> {
1117 };
1118 INSTANTIATE_TEST_CASE_P(WithAndWithoutComputeShaderTest,
1119                         WithAndWithoutComputeShaderTest,
1120                         testing::Values("fragment", "compute"));
1121
1122 // An effect that does nothing, but as a compute shader.
1123 class IdentityComputeEffect : public Effect {
1124 public:
1125         IdentityComputeEffect() {}
1126         virtual string effect_type_id() const { return "IdentityComputeEffect"; }
1127         virtual bool is_compute_shader() const { return true; }
1128         string output_fragment_shader() { return read_file("identity.comp"); }
1129 };
1130
1131 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1132 class OneToOneEffect : public Effect {
1133 public:
1134         OneToOneEffect() {}
1135         string effect_type_id() const override { return "OneToOneEffect"; }
1136         string output_fragment_shader() override { return read_file("identity.frag"); }
1137         bool strong_one_to_one_sampling() const override { return true; }
1138 };
1139
1140 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1141         const int size = 2;
1142         float data[size * size] = {
1143                 1.0f, 0.0f,
1144                 0.0f, 1.0f,
1145         };
1146         float out_data[size * size];
1147
1148         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1149
1150         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1151         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1152
1153         if (GetParam() == "compute") {
1154                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1155         } else {
1156                 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1157         }
1158         tester.get_chain()->add_effect(effect1);
1159         tester.get_chain()->add_effect(effect2);
1160         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1161
1162         expect_equal(data, out_data, size, size);
1163
1164         // The first OneToOneEffect should be in the same phase as its input.
1165         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1166         EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1167                   effect1->replaced_node->containing_phase);
1168
1169         // The second OneToOneEffect, too.
1170         EXPECT_EQ(effect1->replaced_node->containing_phase,
1171                   effect2->replaced_node->containing_phase);
1172 }
1173
1174 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1175         const int size = 2;
1176         float data[size * size] = {
1177                 1.0f, 0.0f,
1178                 0.0f, 1.0f,
1179         };
1180         float out_data[size * size];
1181
1182         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1183
1184         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1185         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1186         RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1187         RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1188
1189         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1190         tester.get_chain()->add_effect(effect1);
1191         tester.get_chain()->add_effect(effect2);
1192         tester.get_chain()->add_effect(effect3);
1193         tester.get_chain()->add_effect(effect4);
1194         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1195
1196         expect_equal(data, out_data, size, size);
1197
1198         // The NonVirtualResizeEffect should be in a different phase from
1199         // the IdentityEffect (since the latter is not one-to-one),
1200         // ie., the chain should be broken somewhere between them, but exactly
1201         // where doesn't matter.
1202         ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1203         EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1204                   effect3->replaced_node->containing_phase);
1205
1206         // The last OneToOneEffect should also be in the same phase as the
1207         // IdentityEffect (the phase was already broken).
1208         EXPECT_EQ(effect3->replaced_node->containing_phase,
1209                   effect4->replaced_node->containing_phase);
1210 }
1211
1212 // Does not use EffectChainTest, so that it can construct an EffectChain without
1213 // a shared ResourcePool (which is also properly destroyed afterwards).
1214 // Also turns on debugging to test that code path.
1215 TEST(EffectChainTest, IdentityWithOwnPool) {
1216         const int width = 3, height = 2;
1217         float data[] = {
1218                 0.0f, 0.25f, 0.3f,
1219                 0.75f, 1.0f, 1.0f,
1220         };
1221         const float expected_data[] = {
1222                 0.75f, 1.0f, 1.0f,
1223                 0.0f, 0.25f, 0.3f,
1224         };
1225         float out_data[6], temp[6 * 4];
1226
1227         EffectChain chain(width, height);
1228         movit_debug_level = MOVIT_DEBUG_ON;
1229
1230         ImageFormat format;
1231         format.color_space = COLORSPACE_sRGB;
1232         format.gamma_curve = GAMMA_LINEAR;
1233
1234         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1235         input->set_pixel_data(data);
1236         chain.add_input(input);
1237         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1238
1239         GLuint texnum, fbo;
1240         glGenTextures(1, &texnum);
1241         check_error();
1242         glBindTexture(GL_TEXTURE_2D, texnum);
1243         check_error();
1244         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1245         check_error();
1246
1247         glGenFramebuffers(1, &fbo);
1248         check_error();
1249         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1250         check_error();
1251         glFramebufferTexture2D(
1252                 GL_FRAMEBUFFER,
1253                 GL_COLOR_ATTACHMENT0,
1254                 GL_TEXTURE_2D,
1255                 texnum,
1256                 0);
1257         check_error();
1258         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1259         check_error();
1260
1261         chain.finalize();
1262
1263         chain.render_to_fbo(fbo, width, height);
1264
1265         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1266         check_error();
1267         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1268         check_error();
1269         for (unsigned i = 0; i < 6; ++i) {
1270                 out_data[i] = temp[i * 4];
1271         }
1272
1273         expect_equal(expected_data, out_data, width, height);
1274
1275         // Reset the debug status again.
1276         movit_debug_level = MOVIT_DEBUG_OFF;
1277 }
1278
1279 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1280 class PrintfingBlueEffect : public Effect {
1281 public:
1282         PrintfingBlueEffect() {}
1283         string effect_type_id() const override { return "PrintfingBlueEffect"; }
1284         string output_fragment_shader() override {
1285                 stringstream ss;
1286                 ss.imbue(locale("C"));
1287                 ss.precision(8);
1288                 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1289                    << 0.0f << ", " << 0.0f << ", "
1290                    << 0.5f << ", " << 1.0f << "); }\n";
1291                 return ss.str();
1292         }
1293 };
1294
1295 TEST(EffectChainTest, StringStreamLocalesWork) {
1296         // An example of a locale with comma instead of period as decimal separator.
1297         // Obviously, if you run on a machine without this locale available,
1298         // the test will always succeed. Note that the OpenGL driver might call
1299         // setlocale() behind-the-scenes, and that might corrupt the returned
1300         // pointer, so we need to take our own copy of it here.
1301         char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1302         if (saved_locale == nullptr) {
1303                 // The locale wasn't available.
1304                 return;
1305         }
1306         saved_locale = strdup(saved_locale);
1307         float data[] = {
1308                 0.0f, 0.0f, 0.0f, 0.0f,
1309         };
1310         float expected_data[] = {
1311                 0.0f, 0.0f, 0.5f, 1.0f,
1312         };
1313         float out_data[4];
1314         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1315         tester.get_chain()->add_effect(new PrintfingBlueEffect());
1316         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1317
1318         expect_equal(expected_data, out_data, 4, 1);
1319
1320         setlocale(LC_ALL, saved_locale);
1321         free(saved_locale);
1322 }
1323
1324 TEST(EffectChainTest, sRGBIntermediate) {
1325         float data[] = {
1326                 0.0f, 0.5f, 0.0f, 1.0f,
1327         };
1328         float out_data[4];
1329         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1330         tester.get_chain()->set_intermediate_format(GL_SRGB8);
1331         tester.get_chain()->add_effect(new IdentityEffect());
1332         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1333         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1334
1335         EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1336             << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1337         EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1338             << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1339
1340         // This state should have been preserved.
1341         EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1342 }
1343
1344 // An effect that is like IdentityEffect, but also does not require linear light.
1345 class PassThroughEffect : public IdentityEffect {
1346 public:
1347         PassThroughEffect() {}
1348         string effect_type_id() const override { return "PassThroughEffect"; }
1349         bool needs_linear_light() const override { return false; }
1350         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1351 };
1352
1353 // Same, just also bouncing.
1354 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1355 public:
1356         BouncingPassThroughEffect() {}
1357         string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1358         bool needs_linear_light() const override { return false; }
1359         bool needs_texture_bounce() const override { return true; }
1360         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1361 };
1362
1363 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1364         // Note that we do the comparison in sRGB space, which is what we
1365         // typically would want; however, we do the sRGB conversion ourself
1366         // to avoid compounding errors from shader conversions into the
1367         // analysis.
1368         const int size = 4096;  // 12-bit.
1369         float linear_data[size], data[size], out_data[size];
1370
1371         for (int i = 0; i < size; ++i) {
1372                 linear_data[i] = i / double(size - 1);
1373                 data[i] = srgb_to_linear(linear_data[i]);
1374         }
1375
1376         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1377         tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1378         tester.get_chain()->add_effect(new IdentityEffect());
1379         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1380         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1381
1382         for (int i = 0; i < size; ++i) {
1383                 out_data[i] = linear_to_srgb(out_data[i]);
1384         }
1385
1386         // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1387         // framebuffer. (Slightly more on NVIDIA cards.)
1388         expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1389 }
1390
1391 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1392         // Note that we do the comparison in sRGB space, which is what we
1393         // typically would want; however, we do the sRGB conversion ourself
1394         // to avoid compounding errors from shader conversions into the
1395         // analysis.
1396         const int size = 4096;  // 12-bit.
1397         float linear_data[size], data[size], out_data[size];
1398
1399         for (int i = 0; i < size; ++i) {
1400                 linear_data[i] = i / double(size - 1);
1401                 data[i] = srgb_to_linear(linear_data[i]);
1402         }
1403
1404         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1405         tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1406         if (GetParam() == "compute") {
1407                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1408         } else {
1409                 tester.get_chain()->add_effect(new IdentityEffect());
1410         }
1411         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1412         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1413
1414         for (int i = 0; i < size; ++i) {
1415                 out_data[i] = linear_to_srgb(out_data[i]);
1416         }
1417
1418         // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1419         // framebuffer (ideal would be 0.5). That is an order of magnitude better
1420         // than in the linear test above. The RMS error is much better, too.
1421         expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1422 }
1423
1424 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1425         const int size = 256;  // 8-bit.
1426         float data[size], out_data[size];
1427
1428         for (int i = 0; i < size; ++i) {
1429                 data[i] = i / double(size - 1);
1430         }
1431
1432         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1433         tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1434         tester.get_chain()->add_effect(new PassThroughEffect());
1435         tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1436         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1437
1438         // The data should be passed through nearly exactly, since there is no effect
1439         // on the path that requires linear light. (Actually, it _is_ exact modulo
1440         // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1441         expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1442 }
1443
1444 // An effect that stores which program number was last run under.
1445 class RecordingIdentityEffect : public Effect {
1446 public:
1447         RecordingIdentityEffect() {}
1448         string effect_type_id() const override { return "RecordingIdentityEffect"; }
1449         string output_fragment_shader() override { return read_file("identity.frag"); }
1450
1451         GLuint last_glsl_program_num;
1452         void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1453         {
1454                 last_glsl_program_num = glsl_program_num;
1455         }
1456 };
1457
1458 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1459         float data[] = {
1460                 0.0f, 0.25f, 0.3f,
1461                 0.75f, 1.0f, 1.0f,
1462         };
1463         float out_data[6];
1464         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1465         RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1466         tester.get_chain()->add_effect(effect);
1467         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1468
1469         expect_equal(data, out_data, 3, 2);
1470
1471         ASSERT_NE(0, effect->last_glsl_program_num);
1472
1473         // Now pretend some other effect is using this program number;
1474         // ResourcePool will then need to clone it.
1475         ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1476         GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1477         EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1478
1479         // Re-run should still give the correct data, but it should have run
1480         // with a different program.
1481         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1482         expect_equal(data, out_data, 3, 2);
1483         EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1484
1485         // Release the program, and check one final time.
1486         resource_pool->unuse_glsl_program(master_program_num);
1487         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1488         expect_equal(data, out_data, 3, 2);
1489 }
1490
1491 TEST(ComputeShaderTest, Identity) {
1492         float data[] = {
1493                 0.0f, 0.25f, 0.3f,
1494                 0.75f, 1.0f, 1.0f,
1495         };
1496         float out_data[6];
1497         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1498         if (!movit_compute_shaders_supported) {
1499                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1500                 return;
1501         }
1502         tester.get_chain()->add_effect(new IdentityComputeEffect());
1503         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1504
1505         expect_equal(data, out_data, 3, 2);
1506 }
1507
1508 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1509 // the very last effect in the chain, which means we can't output it directly
1510 // to the screen.
1511 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1512         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1513 };
1514
1515 TEST(ComputeShaderTest, LastEffectInChain) {
1516         float data[] = {
1517                 0.0f, 0.25f, 0.3f,
1518                 0.75f, 1.0f, 1.0f,
1519         };
1520         float out_data[6];
1521         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1522         if (!movit_compute_shaders_supported) {
1523                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1524                 return;
1525         }
1526         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1527         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1528
1529         expect_equal(data, out_data, 3, 2);
1530 }
1531
1532 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1533         uint8_t data[] = {
1534                 14, 200, 80,
1535                 90, 100, 110,
1536         };
1537         uint8_t out_data[6];
1538         EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1539         if (!movit_compute_shaders_supported) {
1540                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1541                 return;
1542         }
1543         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1544         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1545         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1546
1547         expect_equal(data, out_data, 3, 2);
1548 }
1549
1550 }  // namespace movit