Skip all the compute shader tests if there is no compute support (it was done before...
[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 class WithAndWithoutComputeShaderTest : public testing::TestWithParam<string> {
103 };
104 INSTANTIATE_TEST_CASE_P(WithAndWithoutComputeShaderTest,
105                         WithAndWithoutComputeShaderTest,
106                         testing::Values("fragment", "compute"));
107
108 // An effect that does nothing, but as a compute shader.
109 class IdentityComputeEffect : public Effect {
110 public:
111         IdentityComputeEffect() {}
112         virtual string effect_type_id() const { return "IdentityComputeEffect"; }
113         virtual bool is_compute_shader() const { return true; }
114         string output_fragment_shader() { return read_file("identity.comp"); }
115 };
116
117 TEST_P(WithAndWithoutComputeShaderTest, TopLeftOrigin) {
118         float data[] = {
119                 0.0f, 0.25f, 0.3f,
120                 0.75f, 1.0f, 1.0f,
121         };
122         // Note that EffectChainTester assumes bottom-left origin, so by setting
123         // top-left, we will get flipped data back.
124         float expected_data[6] = {
125                 0.75f, 1.0f, 1.0f,
126                 0.0f, 0.25f, 0.3f,
127         };
128         float out_data[6];
129         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
130         tester.get_chain()->set_output_origin(OUTPUT_ORIGIN_TOP_LEFT);
131         if (GetParam() == "compute") {
132                 if (!movit_compute_shaders_supported) {
133                         fprintf(stderr, "Skipping test; no support for compile shaders.\n");
134                         return;
135                 }
136                 tester.get_chain()->add_effect(new IdentityComputeEffect());
137         }
138         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
139
140         expect_equal(expected_data, out_data, 3, 2);
141 }
142
143 // A dummy effect that inverts its input.
144 class InvertEffect : public Effect {
145 public:
146         InvertEffect() {}
147         string effect_type_id() const override { return "InvertEffect"; }
148         string output_fragment_shader() override { return read_file("invert_effect.frag"); }
149
150         // A real invert would actually care about its alpha,
151         // but in this unit test, it only complicates things.
152         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
153 };
154
155 // Like IdentityEffect, but rewrites itself out of the loop,
156 // splicing in a different effect instead. Also stores the new node,
157 // so we later can check whatever properties we'd like about the graph.
158 template<class T>
159 class RewritingEffect : public Effect {
160 public:
161         template<class... Args>
162         RewritingEffect(Args &&... args) : effect(new T(std::forward<Args>(args)...)), replaced_node(nullptr) {}
163         string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
164         string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
165         void rewrite_graph(EffectChain *graph, Node *self) override {
166                 replaced_node = graph->add_node(effect);
167                 graph->replace_receiver(self, replaced_node);
168                 graph->replace_sender(self, replaced_node);
169                 self->disabled = true;
170         }
171
172         T *effect;
173         Node *replaced_node;
174 };
175
176 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
177         float data[] = {
178                 0.0f, 0.25f, 0.3f,
179                 0.75f, 1.0f, 1.0f,
180         };
181         float expected_data[6] = {
182                 1.0f, 0.9771f, 0.9673f,
183                 0.7192f, 0.0f, 0.0f,
184         };
185         float out_data[6];
186         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
187         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
188         tester.get_chain()->add_effect(effect);
189         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
190
191         Node *node = effect->replaced_node;
192         ASSERT_EQ(1u, node->incoming_links.size());
193         ASSERT_EQ(1u, node->outgoing_links.size());
194         EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
195         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
196
197         expect_equal(expected_data, out_data, 3, 2);
198 }
199
200 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
201         unsigned char data[] = {
202                   0,   0,   0, 255,
203                  64,  64,  64, 255,
204                 128, 128, 128, 255,
205                 255, 255, 255, 255,
206         };
207         float expected_data[] = {
208                 1.0000f, 1.0000f, 1.0000f, 1.0000f,
209                 0.9771f, 0.9771f, 0.9771f, 1.0000f,
210                 0.8983f, 0.8983f, 0.8983f, 1.0000f,
211                 0.0000f, 0.0000f, 0.0000f, 1.0000f
212         };
213         float out_data[4 * 4];
214         EffectChainTester tester(nullptr, 1, 4);
215         tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
216         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
217         tester.get_chain()->add_effect(effect);
218         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
219
220         Node *node = effect->replaced_node;
221         ASSERT_EQ(1u, node->incoming_links.size());
222         ASSERT_EQ(1u, node->outgoing_links.size());
223         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
224         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
225
226         expect_equal(expected_data, out_data, 4, 4);
227 }
228
229 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
230         float data[] = {
231                 0.0f, 0.25f, 0.3f,
232                 0.75f, 1.0f, 1.0f,
233         };
234         float expected_data[6] = {
235                 1.0f, 0.75f, 0.7f,
236                 0.25f, 0.0f, 0.0f,
237         };
238         float out_data[6];
239         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
240         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
241         tester.get_chain()->add_effect(effect);
242         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
243
244         Node *node = effect->replaced_node;
245         ASSERT_EQ(1u, node->incoming_links.size());
246         ASSERT_EQ(1u, node->outgoing_links.size());
247         EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
248         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
249
250         expect_equal(expected_data, out_data, 3, 2);
251 }
252
253 // A fake input that can change its output colorspace and gamma between instantiation
254 // and finalize.
255 class UnknownColorspaceInput : public FlatInput {
256 public:
257         UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
258             : FlatInput(format, pixel_format, type, width, height),
259               overridden_color_space(format.color_space),
260               overridden_gamma_curve(format.gamma_curve) {}
261         string effect_type_id() const override { return "UnknownColorspaceInput"; }
262
263         void set_color_space(Colorspace colorspace) {
264                 overridden_color_space = colorspace;
265         }
266         void set_gamma_curve(GammaCurve gamma_curve) {
267                 overridden_gamma_curve = gamma_curve;
268         }
269         Colorspace get_color_space() const override { return overridden_color_space; }
270         GammaCurve get_gamma_curve() const override { return overridden_gamma_curve; }
271
272 private:
273         Colorspace overridden_color_space;
274         GammaCurve overridden_gamma_curve;
275 };
276
277 TEST(EffectChainTest, HandlesInputChangingColorspace) {
278         const int size = 4;
279
280         float data[size] = {
281                 0.0,
282                 0.5,
283                 0.7,
284                 1.0,
285         };
286         float out_data[size];
287
288         EffectChainTester tester(nullptr, 4, 1, FORMAT_GRAYSCALE);
289
290         // First say that we have sRGB, linear input.
291         ImageFormat format;
292         format.color_space = COLORSPACE_sRGB;
293         format.gamma_curve = GAMMA_LINEAR;
294
295         UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
296         input->set_pixel_data(data);
297         tester.get_chain()->add_input(input);
298
299         // Now we change to Rec. 601 input.
300         input->set_color_space(COLORSPACE_REC_601_625);
301         input->set_gamma_curve(GAMMA_REC_601);
302
303         // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
304         tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
305         expect_equal(data, out_data, 4, 1);
306 }
307
308 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
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_sRGB, GAMMA_sRGB);
319         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
320         tester.get_chain()->add_effect(effect);
321         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
322
323         Node *node = effect->replaced_node;
324         ASSERT_EQ(1u, node->incoming_links.size());
325         EXPECT_EQ(0u, 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 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
332         float data[] = {
333                 0.0f, 0.25f, 0.3f,
334                 0.75f, 1.0f, 1.0f,
335         };
336         float expected_data[6] = {
337                 0.3f, 0.25f, 0.0f,
338                 1.0f, 1.0f, 0.75f,
339         };
340         float out_data[6];
341         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
342         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
343         tester.get_chain()->add_effect(effect);
344         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
345
346         Node *node = effect->replaced_node;
347         ASSERT_EQ(1u, node->incoming_links.size());
348         EXPECT_EQ(0u, node->outgoing_links.size());
349         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
350
351         expect_equal(expected_data, out_data, 3, 2);
352 }
353
354 // The identity effect needs linear light, and thus will get conversions on both sides.
355 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
356 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
357         float data[256];
358         for (unsigned i = 0; i < 256; ++i) {
359                 data[i] = i / 255.0;
360         };
361         float out_data[256];
362         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
363         tester.get_chain()->add_effect(new IdentityEffect());
364         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
365
366         expect_equal(data, out_data, 256, 1);
367 }
368
369 // Same, but uses the forward sRGB table from the GPU.
370 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
371         unsigned char data[256];
372         float expected_data[256];
373         for (unsigned i = 0; i < 256; ++i) {
374                 data[i] = i;
375                 expected_data[i] = i / 255.0;
376         };
377         float out_data[256];
378         EffectChainTester tester(nullptr, 256, 1);
379         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
380         tester.get_chain()->add_effect(new IdentityEffect());
381         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
382
383         expect_equal(expected_data, out_data, 256, 1);
384 }
385
386 // Same, for the Rec. 601/709 gamma curve.
387 TEST(EffectChainTest, IdentityThroughRec709) {
388         float data[256];
389         for (unsigned i = 0; i < 256; ++i) {
390                 data[i] = i / 255.0;
391         };
392         float out_data[256];
393         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
394         tester.get_chain()->add_effect(new IdentityEffect());
395         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
396
397         expect_equal(data, out_data, 256, 1);
398 }
399
400 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
401 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
402         const int size = 3;
403         float data[4 * size] = {
404                 0.8f, 0.0f, 0.0f, 0.5f,
405                 0.0f, 0.2f, 0.2f, 0.3f,
406                 0.1f, 0.0f, 1.0f, 1.0f,
407         };
408         float out_data[4 * size];
409         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
410         tester.get_chain()->add_effect(new IdentityEffect());
411         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
412
413         expect_equal(data, out_data, 4, size);
414 }
415
416 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
417         const int size = 3;
418         float data[4 * size] = {
419                 0.8f, 0.0f, 0.0f, 0.5f,
420                 0.0f, 0.2f, 0.2f, 0.3f,
421                 0.1f, 0.0f, 1.0f, 1.0f,
422         };
423         float expected_data[4 * size] = {
424                 0.1f, 0.0f, 1.0f, 1.0f,
425                 0.0f, 0.2f, 0.2f, 0.3f,
426                 0.8f, 0.0f, 0.0f, 0.5f,
427         };
428         float out_data[4 * size];
429         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
430         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
431         tester.get_chain()->add_effect(effect);
432         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
433
434         Node *node = effect->replaced_node;
435         ASSERT_EQ(1u, node->incoming_links.size());
436         EXPECT_EQ(0u, node->outgoing_links.size());
437         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
438
439         expect_equal(expected_data, out_data, 4, size);
440 }
441
442 // An input that outputs only blue, which has blank alpha.
443 class BlueInput : public Input {
444 public:
445         BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
446         string effect_type_id() const override { return "IdentityEffect"; }
447         string output_fragment_shader() override { return read_file("blue.frag"); }
448         AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
449         bool can_output_linear_gamma() const override { return true; }
450         unsigned get_width() const override { return 1; }
451         unsigned get_height() const override { return 1; }
452         Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
453         GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
454
455 private:
456         int needs_mipmaps;
457 };
458
459 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
460 // which outputs blank alpha.
461 class RewritingToBlueInput : public Input {
462 public:
463         RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
464         string effect_type_id() const override { return "RewritingToBlueInput"; }
465         string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
466         void rewrite_graph(EffectChain *graph, Node *self) override {
467                 Node *blue_node = graph->add_node(new BlueInput());
468                 graph->replace_receiver(self, blue_node);
469                 graph->replace_sender(self, blue_node);
470
471                 self->disabled = true;
472                 this->blue_node = blue_node;
473         }
474
475         // Dummy values that we need to implement because we inherit from Input.
476         // Same as BlueInput.
477         AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
478         bool can_output_linear_gamma() const override { return true; }
479         unsigned get_width() const override { return 1; }
480         unsigned get_height() const override { return 1; }
481         Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
482         GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
483
484         Node *blue_node;
485
486 private:
487         int needs_mipmaps;
488 };
489
490 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
491         const int size = 3;
492         float data[4 * size] = {
493                 0.0f, 0.0f, 1.0f, 1.0f,
494                 0.0f, 0.0f, 1.0f, 1.0f,
495                 0.0f, 0.0f, 1.0f, 1.0f,
496         };
497         float out_data[4 * size];
498         EffectChainTester tester(nullptr, size, 1);
499         RewritingToBlueInput *input = new RewritingToBlueInput();
500         tester.get_chain()->add_input(input);
501         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
502
503         Node *node = input->blue_node;
504         EXPECT_EQ(0u, node->incoming_links.size());
505         EXPECT_EQ(0u, node->outgoing_links.size());
506
507         expect_equal(data, out_data, 4, size);
508 }
509
510 // An effect that does nothing, and specifies that it preserves blank alpha.
511 class BlankAlphaPreservingEffect : public Effect {
512 public:
513         BlankAlphaPreservingEffect() {}
514         string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
515         string output_fragment_shader() override { return read_file("identity.frag"); }
516         AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
517 };
518
519 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
520         const int size = 3;
521         float data[4 * size] = {
522                 0.0f, 0.0f, 1.0f, 1.0f,
523                 0.0f, 0.0f, 1.0f, 1.0f,
524                 0.0f, 0.0f, 1.0f, 1.0f,
525         };
526         float out_data[4 * size];
527         EffectChainTester tester(nullptr, size, 1);
528         tester.get_chain()->add_input(new BlueInput());
529         tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
530         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
531         tester.get_chain()->add_effect(effect);
532         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
533
534         Node *node = effect->replaced_node;
535         EXPECT_EQ(1u, node->incoming_links.size());
536         EXPECT_EQ(0u, node->outgoing_links.size());
537
538         expect_equal(data, out_data, 4, size);
539 }
540
541 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
542 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
543 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
544 // with other tests.)
545 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
546         const int size = 3;
547         float data[4 * size] = {
548                 0.0f, 0.0f, 1.0f, 1.0f,
549                 0.0f, 0.0f, 1.0f, 1.0f,
550                 0.0f, 0.0f, 1.0f, 1.0f,
551         };
552         float out_data[4 * size];
553         EffectChainTester tester(nullptr, size, 1);
554         tester.get_chain()->add_input(new BlueInput());
555         tester.get_chain()->add_effect(new IdentityEffect());  // Not BlankAlphaPreservingEffect.
556         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
557         tester.get_chain()->add_effect(effect);
558         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
559
560         Node *node = effect->replaced_node;
561         EXPECT_EQ(1u, node->incoming_links.size());
562         EXPECT_EQ(1u, node->outgoing_links.size());
563         EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
564
565         expect_equal(data, out_data, 4, size);
566 }
567
568 // Effectively scales down its input linearly by 4x (and repeating it),
569 // which is not attainable without mipmaps.
570 class MipmapNeedingEffect : public Effect {
571 public:
572         MipmapNeedingEffect() {}
573         MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
574
575         // To be allowed to mess with the sampler state.
576         bool needs_texture_bounce() const override { return true; }
577
578         string effect_type_id() const override { return "MipmapNeedingEffect"; }
579         string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
580         void inform_added(EffectChain *chain) override { this->chain = chain; }
581
582         void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
583         {
584                 Node *self = chain->find_node_for_effect(this);
585                 glActiveTexture(chain->get_input_sampler(self, 0));
586                 check_error();
587                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
588                 check_error();
589                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
590                 check_error();
591         }
592
593 private:
594         EffectChain *chain;
595 };
596
597 TEST(EffectChainTest, MipmapGenerationWorks) {
598         float data[] = {  // In 4x4 blocks.
599                 1.0f, 0.0f, 0.0f, 0.0f,
600                 0.0f, 0.0f, 0.0f, 0.0f,
601                 0.0f, 0.0f, 0.0f, 0.0f,
602                 0.0f, 0.0f, 0.0f, 1.0f,
603
604                 0.0f, 0.0f, 0.0f, 0.0f,
605                 0.0f, 0.5f, 0.0f, 0.0f,
606                 0.0f, 0.0f, 1.0f, 0.0f,
607                 0.0f, 0.0f, 0.0f, 0.0f,
608
609                 1.0f, 1.0f, 1.0f, 1.0f,
610                 1.0f, 1.0f, 1.0f, 1.0f,
611                 1.0f, 1.0f, 1.0f, 1.0f,
612                 1.0f, 1.0f, 1.0f, 1.0f,
613
614                 0.0f, 0.0f, 0.0f, 0.0f,
615                 0.0f, 1.0f, 1.0f, 0.0f,
616                 0.0f, 1.0f, 1.0f, 0.0f,
617                 0.0f, 0.0f, 0.0f, 0.0f,
618         };
619         float expected_data[] = {  // Repeated four times each way.
620                 0.125f,   0.125f,   0.125f,   0.125f,
621                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
622                 1.0f,     1.0f,     1.0f,     1.0f,
623                 0.25f,    0.25f,    0.25f,    0.25f,
624
625                 0.125f,   0.125f,   0.125f,   0.125f,
626                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
627                 1.0f,     1.0f,     1.0f,     1.0f,
628                 0.25f,    0.25f,    0.25f,    0.25f,
629
630                 0.125f,   0.125f,   0.125f,   0.125f,
631                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
632                 1.0f,     1.0f,     1.0f,     1.0f,
633                 0.25f,    0.25f,    0.25f,    0.25f,
634
635                 0.125f,   0.125f,   0.125f,   0.125f,
636                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
637                 1.0f,     1.0f,     1.0f,     1.0f,
638                 0.25f,    0.25f,    0.25f,    0.25f,
639         };
640         float out_data[4 * 16];
641         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
642         tester.get_chain()->add_effect(new MipmapNeedingEffect());
643         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
644
645         expect_equal(expected_data, out_data, 4, 16);
646 }
647
648 class NonMipmapCapableInput : public FlatInput {
649 public:
650         NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
651                 : FlatInput(format, pixel_format, type, width, height) {}
652
653         bool can_supply_mipmaps() const override { return false; }
654         bool set_int(const std::string& key, int value) override {
655                 if (key == "needs_mipmaps") {
656                         assert(value == 0);
657                 }
658                 return FlatInput::set_int(key, value);
659         }
660 };
661
662 // The same test as MipmapGenerationWorks, but with an input that refuses
663 // to supply mipmaps.
664 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
665         float data[] = {  // In 4x4 blocks.
666                 1.0f, 0.0f, 0.0f, 0.0f,
667                 0.0f, 0.0f, 0.0f, 0.0f,
668                 0.0f, 0.0f, 0.0f, 0.0f,
669                 0.0f, 0.0f, 0.0f, 1.0f,
670
671                 0.0f, 0.0f, 0.0f, 0.0f,
672                 0.0f, 0.5f, 0.0f, 0.0f,
673                 0.0f, 0.0f, 1.0f, 0.0f,
674                 0.0f, 0.0f, 0.0f, 0.0f,
675
676                 1.0f, 1.0f, 1.0f, 1.0f,
677                 1.0f, 1.0f, 1.0f, 1.0f,
678                 1.0f, 1.0f, 1.0f, 1.0f,
679                 1.0f, 1.0f, 1.0f, 1.0f,
680
681                 0.0f, 0.0f, 0.0f, 0.0f,
682                 0.0f, 1.0f, 1.0f, 0.0f,
683                 0.0f, 1.0f, 1.0f, 0.0f,
684                 0.0f, 0.0f, 0.0f, 0.0f,
685         };
686         float expected_data[] = {  // Repeated four times each way.
687                 0.125f,   0.125f,   0.125f,   0.125f,
688                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
689                 1.0f,     1.0f,     1.0f,     1.0f,
690                 0.25f,    0.25f,    0.25f,    0.25f,
691
692                 0.125f,   0.125f,   0.125f,   0.125f,
693                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
694                 1.0f,     1.0f,     1.0f,     1.0f,
695                 0.25f,    0.25f,    0.25f,    0.25f,
696
697                 0.125f,   0.125f,   0.125f,   0.125f,
698                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
699                 1.0f,     1.0f,     1.0f,     1.0f,
700                 0.25f,    0.25f,    0.25f,    0.25f,
701
702                 0.125f,   0.125f,   0.125f,   0.125f,
703                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
704                 1.0f,     1.0f,     1.0f,     1.0f,
705                 0.25f,    0.25f,    0.25f,    0.25f,
706         };
707         float out_data[4 * 16];
708         EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
709
710         ImageFormat format;
711         format.color_space = COLORSPACE_sRGB;
712         format.gamma_curve = GAMMA_LINEAR;
713
714         NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
715         input->set_pixel_data(data);
716         tester.get_chain()->add_input(input);
717         tester.get_chain()->add_effect(new MipmapNeedingEffect());
718         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
719
720         expect_equal(expected_data, out_data, 4, 16);
721 }
722
723 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
724         float data[] = {  // In 4x4 blocks.
725                 1.0f, 0.0f, 0.0f, 0.0f,
726                 0.0f, 0.0f, 0.0f, 0.0f,
727                 0.0f, 0.0f, 0.0f, 0.0f,
728                 0.0f, 0.0f, 0.0f, 1.0f,
729
730                 0.0f, 0.0f, 0.0f, 0.0f,
731                 0.0f, 0.5f, 0.0f, 0.0f,
732                 0.0f, 0.0f, 1.0f, 0.0f,
733                 0.0f, 0.0f, 0.0f, 0.0f,
734
735                 1.0f, 1.0f, 1.0f, 1.0f,
736                 1.0f, 1.0f, 1.0f, 1.0f,
737                 1.0f, 1.0f, 1.0f, 1.0f,
738                 1.0f, 1.0f, 1.0f, 1.0f,
739
740                 0.0f, 0.0f, 0.0f, 0.0f,
741                 0.0f, 1.0f, 1.0f, 0.0f,
742                 0.0f, 1.0f, 1.0f, 0.0f,
743                 0.0f, 0.0f, 0.0f, 0.0f,
744         };
745         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
746                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
747                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
748                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
749                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
750                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
751                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
752                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
753                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
754                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
755                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
756                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
757                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
758                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
759                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
760                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
761                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
762         };
763         float out_data[4 * 16];
764
765         ResizeEffect *downscale = new ResizeEffect();
766         ASSERT_TRUE(downscale->set_int("width", 1));
767         ASSERT_TRUE(downscale->set_int("height", 4));
768
769         ResizeEffect *upscale = new ResizeEffect();
770         ASSERT_TRUE(upscale->set_int("width", 4));
771         ASSERT_TRUE(upscale->set_int("height", 16));
772
773         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
774         tester.get_chain()->add_effect(downscale);
775         tester.get_chain()->add_effect(upscale);
776         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
777
778         expect_equal(expected_data, out_data, 4, 16);
779 }
780
781 // An effect to verify that you can turn off mipmaps; it downscales by two,
782 // which gives blur with mipmaps and aliasing (picks out every other pixel)
783 // without.
784 class Downscale2xEffect : public Effect {
785 public:
786         explicit Downscale2xEffect(MipmapRequirements mipmap_requirements)
787                 : mipmap_requirements(mipmap_requirements)
788         {
789                 register_vec2("offset", offset);
790         }
791         MipmapRequirements needs_mipmaps() const override { return mipmap_requirements; }
792
793         string effect_type_id() const override { return "Downscale2xEffect"; }
794         string output_fragment_shader() override { return read_file("downscale2x.frag"); }
795
796 private:
797         const MipmapRequirements mipmap_requirements;
798         float offset[2] { 0.0f, 0.0f };
799 };
800
801 TEST(EffectChainTest, MipmapChainGetsSplit) {
802         float data[] = {
803                 0.0f, 0.0f, 0.0f, 0.0f,
804                 1.0f, 0.0f, 1.0f, 0.0f,
805                 0.0f, 0.0f, 0.0f, 0.0f,
806                 1.0f, 0.0f, 1.0f, 0.0f,
807         };
808
809         // The intermediate result after the first step looks like this,
810         // assuming there are no mipmaps (the zeros are due to border behavior):
811         //
812         //   0 0 0 0
813         //   0 0 0 0
814         //   1 1 0 0
815         //   1 1 0 0
816         //
817         // so another 2x downscale towards the bottom left will give
818         //
819         //   0 0
820         //   1 0
821         //
822         // with yet more zeros coming in on the top and the right from the border.
823         float expected_data[] = {
824                 0.0f, 0.0f, 0.0f, 0.0f,
825                 0.0f, 0.0f, 0.0f, 0.0f,
826                 0.0f, 0.0f, 0.0f, 0.0f,
827                 1.0f, 0.0f, 0.0f, 0.0f,
828         };
829         float out_data[4 * 4];
830
831         float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
832         RewritingEffect<Downscale2xEffect> *pick_out_bottom_left = new RewritingEffect<Downscale2xEffect>(Effect::CANNOT_ACCEPT_MIPMAPS);
833         ASSERT_TRUE(pick_out_bottom_left->effect->set_vec2("offset", offset));
834
835         RewritingEffect<Downscale2xEffect> *downscale2x = new RewritingEffect<Downscale2xEffect>(Effect::NEEDS_MIPMAPS);
836
837         EffectChainTester tester(data, 4, 4, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
838         tester.get_chain()->add_effect(pick_out_bottom_left);
839         tester.get_chain()->add_effect(downscale2x);
840         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
841
842         EXPECT_NE(pick_out_bottom_left->replaced_node->containing_phase,
843                   downscale2x->replaced_node->containing_phase);
844
845         expect_equal(expected_data, out_data, 4, 4);
846 }
847
848 // An effect that adds its two inputs together. Used below.
849 class AddEffect : public Effect {
850 public:
851         AddEffect() {}
852         string effect_type_id() const override { return "AddEffect"; }
853         string output_fragment_shader() override { return read_file("add.frag"); }
854         unsigned num_inputs() const override { return 2; }
855         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
856 };
857
858 // Constructs the graph
859 //
860 //             FlatInput               |
861 //            /         \              |
862 //  MultiplyEffect  MultiplyEffect     |
863 //            \         /              |
864 //             AddEffect               |
865 //
866 // and verifies that it gives the correct output.
867 TEST(EffectChainTest, DiamondGraph) {
868         float data[] = {
869                 1.0f, 1.0f,
870                 1.0f, 0.0f,
871         };
872         float expected_data[] = {
873                 2.5f, 2.5f,
874                 2.5f, 0.0f,
875         };
876         float out_data[2 * 2];
877
878         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
879         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
880
881         MultiplyEffect *mul_half = new MultiplyEffect();
882         ASSERT_TRUE(mul_half->set_vec4("factor", half));
883         
884         MultiplyEffect *mul_two = new MultiplyEffect();
885         ASSERT_TRUE(mul_two->set_vec4("factor", two));
886
887         EffectChainTester tester(nullptr, 2, 2);
888
889         ImageFormat format;
890         format.color_space = COLORSPACE_sRGB;
891         format.gamma_curve = GAMMA_LINEAR;
892
893         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
894         input->set_pixel_data(data);
895
896         tester.get_chain()->add_input(input);
897         tester.get_chain()->add_effect(mul_half, input);
898         tester.get_chain()->add_effect(mul_two, input);
899         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
900         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
901
902         expect_equal(expected_data, out_data, 2, 2);
903 }
904
905 // Constructs the graph
906 //
907 //             FlatInput                     |
908 //            /         \                    |
909 //  MultiplyEffect  MultiplyEffect           |
910 //         \             |                   |
911 //          \    BouncingIdentityEffect      |  
912 //            \         /                    |
913 //             AddEffect                     |
914 //
915 // and verifies that it gives the correct output.
916 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
917         float data[] = {
918                 1.0f, 1.0f,
919                 1.0f, 0.0f,
920         };
921         float expected_data[] = {
922                 2.5f, 2.5f,
923                 2.5f, 0.0f,
924         };
925         float out_data[2 * 2];
926
927         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
928         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
929
930         MultiplyEffect *mul_half = new MultiplyEffect();
931         ASSERT_TRUE(mul_half->set_vec4("factor", half));
932         
933         MultiplyEffect *mul_two = new MultiplyEffect();
934         ASSERT_TRUE(mul_two->set_vec4("factor", two));
935         
936         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
937
938         EffectChainTester tester(nullptr, 2, 2);
939
940         ImageFormat format;
941         format.color_space = COLORSPACE_sRGB;
942         format.gamma_curve = GAMMA_LINEAR;
943
944         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
945         input->set_pixel_data(data);
946
947         tester.get_chain()->add_input(input);
948         tester.get_chain()->add_effect(mul_half, input);
949         tester.get_chain()->add_effect(mul_two, input);
950         tester.get_chain()->add_effect(bounce, mul_two);
951         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
952         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
953
954         expect_equal(expected_data, out_data, 2, 2);
955 }
956
957 // Constructs the graph
958 //
959 //                        FlatInput                               |
960 //                       /         \                              |
961 //  Downscale2xEffect (mipmaps)  Downscale2xEffect (no mipmaps)   |
962 //                      |           |                             |
963 //  Downscale2xEffect (mipmaps)  Downscale2xEffect (no mipmaps)   |
964 //                       \         /                              |
965 //                        AddEffect                               |
966 //
967 // and verifies that it gives the correct output. Due to the conflicting
968 // mipmap demands, EffectChain needs to make two phases; exactly where it's
969 // split is less important, though (this is a fairly obscure situation that
970 // is unlikely to happen in practice).
971 TEST(EffectChainTest, DiamondGraphWithConflictingMipmaps) {
972         float data[] = {
973                 0.0f, 0.0f, 0.0f, 0.0f,
974                 1.0f, 0.0f, 1.0f, 0.0f,
975                 0.0f, 0.0f, 0.0f, 0.0f,
976                 1.0f, 0.0f, 1.0f, 0.0f,
977         };
978
979         // Same situation as MipmapChainGetsSplit. The output of the two
980         // downscales with no mipmaps looks like this:
981         //
982         //    0 0 0 0
983         //    0 0 0 0
984         //    0 0 0 0
985         //    1 0 0 0
986         //
987         // and the one with mipmaps is 0.25 everywhere. Due to postmultiplied
988         // alpha, we get the average even though we are using AddEffect.
989         float expected_data[] = {
990                 0.125f, 0.125f, 0.125f, 0.125f,
991                 0.125f, 0.125f, 0.125f, 0.125f,
992                 0.125f, 0.125f, 0.125f, 0.125f,
993                 0.625f, 0.125f, 0.125f, 0.125f,
994         };
995         float out_data[4 * 4];
996
997         float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
998         Downscale2xEffect *nomipmap1 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
999         Downscale2xEffect *nomipmap2 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
1000         ASSERT_TRUE(nomipmap1->set_vec2("offset", offset));
1001         ASSERT_TRUE(nomipmap2->set_vec2("offset", offset));
1002
1003         Downscale2xEffect *mipmap1 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1004         Downscale2xEffect *mipmap2 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1005
1006         EffectChainTester tester(nullptr, 4, 4);
1007
1008         ImageFormat format;
1009         format.color_space = COLORSPACE_sRGB;
1010         format.gamma_curve = GAMMA_LINEAR;
1011
1012         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 4);
1013         input->set_pixel_data(data);
1014
1015         tester.get_chain()->add_input(input);
1016
1017         tester.get_chain()->add_effect(nomipmap1, input);
1018         tester.get_chain()->add_effect(nomipmap2, nomipmap1);
1019
1020         tester.get_chain()->add_effect(mipmap1, input);
1021         tester.get_chain()->add_effect(mipmap2, mipmap1);
1022
1023         tester.get_chain()->add_effect(new AddEffect(), nomipmap2, mipmap2);
1024         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1025
1026         expect_equal(expected_data, out_data, 4, 4);
1027 }
1028
1029 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
1030         float data[] = {
1031                 0.735f, 0.0f,
1032                 0.735f, 0.0f,
1033         };
1034         float expected_data[] = {
1035                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1036                 0.0f, 0.5f,
1037         };
1038         float out_data[2 * 2];
1039         
1040         EffectChainTester tester(nullptr, 2, 2);
1041         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
1042
1043         // MirrorEffect does not get linear light, so the conversions will be
1044         // inserted after it, not before.
1045         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1046         tester.get_chain()->add_effect(effect);
1047
1048         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1049         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1050         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1051         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1052
1053         expect_equal(expected_data, out_data, 2, 2);
1054
1055         Node *node = effect->replaced_node;
1056         ASSERT_EQ(1u, node->incoming_links.size());
1057         ASSERT_EQ(1u, node->outgoing_links.size());
1058         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1059         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
1060 }
1061
1062 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
1063         float data[] = {
1064                 0.5f, 0.0f,
1065                 0.5f, 0.0f,
1066         };
1067         float expected_data[] = {
1068                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1069                 0.0f, 0.5f,
1070         };
1071         float out_data[2 * 2];
1072         
1073         EffectChainTester tester(nullptr, 2, 2);
1074         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
1075
1076         // MirrorEffect does not get linear light, so the conversions will be
1077         // inserted after it, not before.
1078         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1079         tester.get_chain()->add_effect(effect);
1080
1081         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1082         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1083         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1084         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1085
1086         expect_equal(expected_data, out_data, 2, 2);
1087
1088         Node *node = effect->replaced_node;
1089         ASSERT_EQ(1u, node->incoming_links.size());
1090         ASSERT_EQ(1u, node->outgoing_links.size());
1091         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1092         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
1093 }
1094
1095 // An effect that does nothing, but requests texture bounce and stores
1096 // its input size.
1097 class SizeStoringEffect : public BouncingIdentityEffect {
1098 public:
1099         SizeStoringEffect() : input_width(-1), input_height(-1) {}
1100         void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
1101                 assert(input_num == 0);
1102                 input_width = width;
1103                 input_height = height;
1104         }
1105         string effect_type_id() const override { return "SizeStoringEffect"; }
1106
1107         int input_width, input_height;
1108 };
1109
1110 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
1111         float data[2 * 2] = {
1112                 0.0f, 0.0f,
1113                 0.0f, 0.0f,
1114         };
1115         float out_data[4 * 3];
1116         
1117         EffectChainTester tester(nullptr, 4, 3);  // Note non-square aspect.
1118
1119         ImageFormat format;
1120         format.color_space = COLORSPACE_sRGB;
1121         format.gamma_curve = GAMMA_LINEAR;
1122
1123         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1124         input1->set_pixel_data(data);
1125         
1126         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1127         input2->set_pixel_data(data);
1128
1129         SizeStoringEffect *input_store = new SizeStoringEffect();
1130
1131         tester.get_chain()->add_input(input1);
1132         tester.get_chain()->add_input(input2);
1133         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1134         tester.get_chain()->add_effect(input_store);
1135         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1136
1137         EXPECT_EQ(2, input_store->input_width);
1138         EXPECT_EQ(2, input_store->input_height);
1139 }
1140
1141 TEST(EffectChainTest, AspectRatioConversion) {
1142         float data1[4 * 3] = {
1143                 0.0f, 0.0f, 0.0f, 0.0f,
1144                 0.0f, 0.0f, 0.0f, 0.0f,
1145                 0.0f, 0.0f, 0.0f, 0.0f,
1146         };
1147         float data2[7 * 7] = {
1148                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1149                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1150                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1151                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
1152                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1153                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1154                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1155         };
1156
1157         // The right conversion here is that the 7x7 image decides the size,
1158         // since it is the biggest, so everything is scaled up to 9x7
1159         // (keep the height, round the width 9.333 to 9). 
1160         float out_data[9 * 7];
1161         
1162         EffectChainTester tester(nullptr, 4, 3);
1163
1164         ImageFormat format;
1165         format.color_space = COLORSPACE_sRGB;
1166         format.gamma_curve = GAMMA_LINEAR;
1167
1168         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1169         input1->set_pixel_data(data1);
1170         
1171         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1172         input2->set_pixel_data(data2);
1173
1174         SizeStoringEffect *input_store = new SizeStoringEffect();
1175
1176         tester.get_chain()->add_input(input1);
1177         tester.get_chain()->add_input(input2);
1178         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1179         tester.get_chain()->add_effect(input_store);
1180         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1181
1182         EXPECT_EQ(9, input_store->input_width);
1183         EXPECT_EQ(7, input_store->input_height);
1184 }
1185
1186 // Tests that putting a BlueInput (constant color) into its own pass,
1187 // which creates a phase that doesn't need texture coordinates,
1188 // doesn't mess up a second phase that actually does.
1189 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1190         const int size = 2;
1191         float data[] = {
1192                 1.0f,
1193                 0.0f,
1194         };
1195         float expected_data[] = {
1196                 1.0f, 1.0f, 2.0f, 2.0f,
1197                 0.0f, 0.0f, 1.0f, 2.0f,
1198         };
1199         float out_data[size * 4];
1200         // First say that we have sRGB, linear input.
1201         ImageFormat format;
1202         format.color_space = COLORSPACE_sRGB;
1203         format.gamma_curve = GAMMA_LINEAR;
1204         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1205
1206         input->set_pixel_data(data);
1207         EffectChainTester tester(nullptr, 1, size);
1208         tester.get_chain()->add_input(new BlueInput());
1209         Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1210         tester.get_chain()->add_input(input);
1211         tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1212
1213         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1214
1215         expect_equal(expected_data, out_data, 4, size);
1216 }
1217
1218 // An effect that does nothing except changing its output sizes.
1219 class VirtualResizeEffect : public Effect {
1220 public:
1221         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1222                 : width(width),
1223                   height(height),
1224                   virtual_width(virtual_width),
1225                   virtual_height(virtual_height) {}
1226         string effect_type_id() const override { return "VirtualResizeEffect"; }
1227         string output_fragment_shader() override { return read_file("identity.frag"); }
1228
1229         bool changes_output_size() const override { return true; }
1230
1231         void get_output_size(unsigned *width, unsigned *height,
1232                              unsigned *virtual_width, unsigned *virtual_height) const override {
1233                 *width = this->width;
1234                 *height = this->height;
1235                 *virtual_width = this->virtual_width;
1236                 *virtual_height = this->virtual_height;
1237         }
1238
1239 private:
1240         int width, height, virtual_width, virtual_height;
1241 };
1242
1243 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1244         const int size = 2, bigger_size = 3;
1245         float data[size * size] = {
1246                 1.0f, 0.0f,
1247                 0.0f, 1.0f,
1248         };
1249         float out_data[size * size];
1250         
1251         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1252
1253         SizeStoringEffect *size_store = new SizeStoringEffect();
1254
1255         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1256         tester.get_chain()->add_effect(size_store);
1257         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1258         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1259
1260         EXPECT_EQ(bigger_size, size_store->input_width);
1261         EXPECT_EQ(bigger_size, size_store->input_height);
1262
1263         // If the resize is implemented as non-virtual, we'll fail here,
1264         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1265         expect_equal(data, out_data, size, size);
1266 }
1267
1268 // An effect that is like VirtualResizeEffect, but always has virtual and real
1269 // sizes the same (and promises this).
1270 class NonVirtualResizeEffect : public VirtualResizeEffect {
1271 public:
1272         NonVirtualResizeEffect(int width, int height)
1273                 : VirtualResizeEffect(width, height, width, height) {}
1274         string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1275         bool sets_virtual_output_size() const override { return false; }
1276 };
1277
1278 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1279 class OneToOneEffect : public Effect {
1280 public:
1281         OneToOneEffect() {}
1282         string effect_type_id() const override { return "OneToOneEffect"; }
1283         string output_fragment_shader() override { return read_file("identity.frag"); }
1284         bool strong_one_to_one_sampling() const override { return true; }
1285 };
1286
1287 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1288         const int size = 2;
1289         float data[size * size] = {
1290                 1.0f, 0.0f,
1291                 0.0f, 1.0f,
1292         };
1293         float out_data[size * size];
1294
1295         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1296
1297         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1298         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1299
1300         if (GetParam() == "compute") {
1301                 if (!movit_compute_shaders_supported) {
1302                         fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1303                         return;
1304                 }
1305                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1306         } else {
1307                 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1308         }
1309         tester.get_chain()->add_effect(effect1);
1310         tester.get_chain()->add_effect(effect2);
1311         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1312
1313         expect_equal(data, out_data, size, size);
1314
1315         // The first OneToOneEffect should be in the same phase as its input.
1316         ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1317         EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1318                   effect1->replaced_node->containing_phase);
1319
1320         // The second OneToOneEffect, too.
1321         EXPECT_EQ(effect1->replaced_node->containing_phase,
1322                   effect2->replaced_node->containing_phase);
1323 }
1324
1325 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1326         const int size = 2;
1327         float data[size * size] = {
1328                 1.0f, 0.0f,
1329                 0.0f, 1.0f,
1330         };
1331         float out_data[size * size];
1332
1333         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1334
1335         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1336         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1337         RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1338         RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1339
1340         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1341         tester.get_chain()->add_effect(effect1);
1342         tester.get_chain()->add_effect(effect2);
1343         tester.get_chain()->add_effect(effect3);
1344         tester.get_chain()->add_effect(effect4);
1345         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1346
1347         expect_equal(data, out_data, size, size);
1348
1349         // The NonVirtualResizeEffect should be in a different phase from
1350         // the IdentityEffect (since the latter is not one-to-one),
1351         // ie., the chain should be broken somewhere between them, but exactly
1352         // where doesn't matter.
1353         ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1354         EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1355                   effect3->replaced_node->containing_phase);
1356
1357         // The last OneToOneEffect should also be in the same phase as the
1358         // IdentityEffect (the phase was already broken).
1359         EXPECT_EQ(effect3->replaced_node->containing_phase,
1360                   effect4->replaced_node->containing_phase);
1361 }
1362
1363 // Does not use EffectChainTest, so that it can construct an EffectChain without
1364 // a shared ResourcePool (which is also properly destroyed afterwards).
1365 // Also turns on debugging to test that code path.
1366 TEST(EffectChainTest, IdentityWithOwnPool) {
1367         const int width = 3, height = 2;
1368         float data[] = {
1369                 0.0f, 0.25f, 0.3f,
1370                 0.75f, 1.0f, 1.0f,
1371         };
1372         const float expected_data[] = {
1373                 0.75f, 1.0f, 1.0f,
1374                 0.0f, 0.25f, 0.3f,
1375         };
1376         float out_data[6], temp[6 * 4];
1377
1378         EffectChain chain(width, height);
1379         MovitDebugLevel old_movit_debug_level = movit_debug_level;
1380         movit_debug_level = MOVIT_DEBUG_ON;
1381
1382         ImageFormat format;
1383         format.color_space = COLORSPACE_sRGB;
1384         format.gamma_curve = GAMMA_LINEAR;
1385
1386         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1387         input->set_pixel_data(data);
1388         chain.add_input(input);
1389         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1390
1391         GLuint texnum, fbo;
1392         glGenTextures(1, &texnum);
1393         check_error();
1394         glBindTexture(GL_TEXTURE_2D, texnum);
1395         check_error();
1396         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1397         check_error();
1398
1399         glGenFramebuffers(1, &fbo);
1400         check_error();
1401         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1402         check_error();
1403         glFramebufferTexture2D(
1404                 GL_FRAMEBUFFER,
1405                 GL_COLOR_ATTACHMENT0,
1406                 GL_TEXTURE_2D,
1407                 texnum,
1408                 0);
1409         check_error();
1410         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1411         check_error();
1412
1413         chain.finalize();
1414
1415         chain.render_to_fbo(fbo, width, height);
1416
1417         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1418         check_error();
1419         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1420         check_error();
1421         for (unsigned i = 0; i < 6; ++i) {
1422                 out_data[i] = temp[i * 4];
1423         }
1424
1425         expect_equal(expected_data, out_data, width, height);
1426
1427         // Reset the debug status again.
1428         movit_debug_level = old_movit_debug_level;
1429 }
1430
1431 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1432 class PrintfingBlueEffect : public Effect {
1433 public:
1434         PrintfingBlueEffect() {}
1435         string effect_type_id() const override { return "PrintfingBlueEffect"; }
1436         string output_fragment_shader() override {
1437                 stringstream ss;
1438                 ss.imbue(locale("C"));
1439                 ss.precision(8);
1440                 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1441                    << 0.0f << ", " << 0.0f << ", "
1442                    << 0.5f << ", " << 1.0f << "); }\n";
1443                 return ss.str();
1444         }
1445 };
1446
1447 TEST(EffectChainTest, StringStreamLocalesWork) {
1448         // An example of a locale with comma instead of period as decimal separator.
1449         // Obviously, if you run on a machine without this locale available,
1450         // the test will always succeed. Note that the OpenGL driver might call
1451         // setlocale() behind-the-scenes, and that might corrupt the returned
1452         // pointer, so we need to take our own copy of it here.
1453         char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1454         if (saved_locale == nullptr) {
1455                 // The locale wasn't available.
1456                 return;
1457         }
1458         saved_locale = strdup(saved_locale);
1459         float data[] = {
1460                 0.0f, 0.0f, 0.0f, 0.0f,
1461         };
1462         float expected_data[] = {
1463                 0.0f, 0.0f, 0.5f, 1.0f,
1464         };
1465         float out_data[4];
1466         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1467         tester.get_chain()->add_effect(new PrintfingBlueEffect());
1468         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1469
1470         expect_equal(expected_data, out_data, 4, 1);
1471
1472         setlocale(LC_ALL, saved_locale);
1473         free(saved_locale);
1474 }
1475
1476 TEST(EffectChainTest, sRGBIntermediate) {
1477         float data[] = {
1478                 0.0f, 0.5f, 0.0f, 1.0f,
1479         };
1480         float out_data[4];
1481         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1482         tester.get_chain()->set_intermediate_format(GL_SRGB8);
1483         tester.get_chain()->add_effect(new IdentityEffect());
1484         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1485         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1486
1487         EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1488             << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1489         EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1490             << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1491
1492         // This state should have been preserved.
1493         EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1494 }
1495
1496 // An effect that is like IdentityEffect, but also does not require linear light.
1497 class PassThroughEffect : public IdentityEffect {
1498 public:
1499         PassThroughEffect() {}
1500         string effect_type_id() const override { return "PassThroughEffect"; }
1501         bool needs_linear_light() const override { return false; }
1502         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1503 };
1504
1505 // Same, just also bouncing.
1506 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1507 public:
1508         BouncingPassThroughEffect() {}
1509         string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1510         bool needs_linear_light() const override { return false; }
1511         bool needs_texture_bounce() const override { return true; }
1512         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1513 };
1514
1515 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1516         // Note that we do the comparison in sRGB space, which is what we
1517         // typically would want; however, we do the sRGB conversion ourself
1518         // to avoid compounding errors from shader conversions into the
1519         // analysis.
1520         const int size = 4096;  // 12-bit.
1521         float linear_data[size], data[size], out_data[size];
1522
1523         for (int i = 0; i < size; ++i) {
1524                 linear_data[i] = i / double(size - 1);
1525                 data[i] = srgb_to_linear(linear_data[i]);
1526         }
1527
1528         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1529         tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1530         tester.get_chain()->add_effect(new IdentityEffect());
1531         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1532         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1533
1534         for (int i = 0; i < size; ++i) {
1535                 out_data[i] = linear_to_srgb(out_data[i]);
1536         }
1537
1538         // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1539         // framebuffer. (Slightly more on NVIDIA cards.)
1540         expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1541 }
1542
1543 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1544         // Note that we do the comparison in sRGB space, which is what we
1545         // typically would want; however, we do the sRGB conversion ourself
1546         // to avoid compounding errors from shader conversions into the
1547         // analysis.
1548         const int size = 4096;  // 12-bit.
1549         float linear_data[size], data[size], out_data[size];
1550
1551         for (int i = 0; i < size; ++i) {
1552                 linear_data[i] = i / double(size - 1);
1553                 data[i] = srgb_to_linear(linear_data[i]);
1554         }
1555
1556         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1557         tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1558         if (GetParam() == "compute") {
1559                 if (!movit_compute_shaders_supported) {
1560                         fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1561                         return;
1562                 }
1563                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1564         } else {
1565                 tester.get_chain()->add_effect(new IdentityEffect());
1566         }
1567         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1568         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1569
1570         for (int i = 0; i < size; ++i) {
1571                 out_data[i] = linear_to_srgb(out_data[i]);
1572         }
1573
1574         // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1575         // framebuffer (ideal would be 0.5). That is an order of magnitude better
1576         // than in the linear test above. The RMS error is much better, too.
1577         expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1578 }
1579
1580 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1581         const int size = 256;  // 8-bit.
1582         float data[size], out_data[size];
1583
1584         for (int i = 0; i < size; ++i) {
1585                 data[i] = i / double(size - 1);
1586         }
1587
1588         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1589         tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1590         tester.get_chain()->add_effect(new PassThroughEffect());
1591         tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1592         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1593
1594         // The data should be passed through nearly exactly, since there is no effect
1595         // on the path that requires linear light. (Actually, it _is_ exact modulo
1596         // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1597         expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1598 }
1599
1600 // An effect that stores which program number was last run under.
1601 class RecordingIdentityEffect : public Effect {
1602 public:
1603         RecordingIdentityEffect() {}
1604         string effect_type_id() const override { return "RecordingIdentityEffect"; }
1605         string output_fragment_shader() override { return read_file("identity.frag"); }
1606
1607         GLuint last_glsl_program_num;
1608         void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1609         {
1610                 last_glsl_program_num = glsl_program_num;
1611         }
1612 };
1613
1614 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1615         float data[] = {
1616                 0.0f, 0.25f, 0.3f,
1617                 0.75f, 1.0f, 1.0f,
1618         };
1619         float out_data[6];
1620         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1621         RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1622         tester.get_chain()->add_effect(effect);
1623         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1624
1625         expect_equal(data, out_data, 3, 2);
1626
1627         ASSERT_NE(0u, effect->last_glsl_program_num);
1628
1629         // Now pretend some other effect is using this program number;
1630         // ResourcePool will then need to clone it.
1631         ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1632         GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1633         EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1634
1635         // Re-run should still give the correct data, but it should have run
1636         // with a different program.
1637         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1638         expect_equal(data, out_data, 3, 2);
1639         EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1640
1641         // Release the program, and check one final time.
1642         resource_pool->unuse_glsl_program(master_program_num);
1643         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1644         expect_equal(data, out_data, 3, 2);
1645 }
1646
1647 TEST(ComputeShaderTest, Identity) {
1648         float data[] = {
1649                 0.0f, 0.25f, 0.3f,
1650                 0.75f, 1.0f, 1.0f,
1651         };
1652         float out_data[6];
1653         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1654         if (!movit_compute_shaders_supported) {
1655                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1656                 return;
1657         }
1658         tester.get_chain()->add_effect(new IdentityComputeEffect());
1659         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1660
1661         expect_equal(data, out_data, 3, 2);
1662 }
1663
1664 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1665 // the very last effect in the chain, which means we can't output it directly
1666 // to the screen.
1667 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1668         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1669 };
1670
1671 TEST(ComputeShaderTest, LastEffectInChain) {
1672         float data[] = {
1673                 0.0f, 0.25f, 0.3f,
1674                 0.75f, 1.0f, 1.0f,
1675         };
1676         float out_data[6];
1677         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1678         if (!movit_compute_shaders_supported) {
1679                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1680                 return;
1681         }
1682         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1683         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1684
1685         expect_equal(data, out_data, 3, 2);
1686 }
1687
1688 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1689         uint8_t data[] = {
1690                 14, 200, 80,
1691                 90, 100, 110,
1692         };
1693         uint8_t out_data[6];
1694         EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1695         if (!movit_compute_shaders_supported) {
1696                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1697                 return;
1698         }
1699         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1700         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1701         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1702
1703         expect_equal(data, out_data, 3, 2);
1704 }
1705
1706 // A compute shader to mirror the inputs, in 2x2 blocks.
1707 class MirrorComputeEffect : public Effect {
1708 public:
1709         MirrorComputeEffect() {}
1710         string effect_type_id() const override { return "MirrorComputeEffect"; }
1711         bool is_compute_shader() const override { return true; }
1712         string output_fragment_shader() override { return read_file("mirror.comp"); }
1713         void get_compute_dimensions(unsigned output_width, unsigned output_height,
1714                                     unsigned *x, unsigned *y, unsigned *z) const override {
1715                 *x = output_width / 2;
1716                 *y = output_height / 2;
1717                 *z = 1;
1718         }
1719 };
1720
1721 TEST(ComputeShaderTest, ComputeThenOneToOne) {
1722         float data[] = {
1723                 0.0f, 0.25f, 0.3f, 0.8f,
1724                 0.75f, 1.0f, 1.0f, 0.2f,
1725         };
1726         float expected_data[] = {
1727                 0.8f, 0.3f, 0.25f, 0.0f,
1728                 0.2f, 1.0f, 1.0f, 0.75f,
1729         };
1730         float out_data[8];
1731         EffectChainTester tester(data, 4, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1732         if (!movit_compute_shaders_supported) {
1733                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1734                 return;
1735         }
1736         tester.get_chain()->add_effect(new MirrorComputeEffect());
1737         tester.get_chain()->add_effect(new OneToOneEffect());
1738         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1739
1740         expect_equal(expected_data, out_data, 4, 2);
1741 }
1742
1743 // A compute shader that also resizes its input, taking the upper-left pixel
1744 // of every 2x2 group. (The shader is hard-coded to 4x2 input for simplicity.)
1745 class Downscale2xComputeEffect : public Effect {
1746 public:
1747         Downscale2xComputeEffect() {}
1748         string effect_type_id() const override { return "Downscale2xComputeEffect"; }
1749         bool is_compute_shader() const override { return true; }
1750         string output_fragment_shader() override { return read_file("downscale2x.comp"); }
1751         bool changes_output_size() const override { return true; }
1752         void inform_input_size(unsigned input_num, unsigned width, unsigned height) override
1753         {
1754                 this->width = width;
1755                 this->height = height;
1756         }
1757         void get_output_size(unsigned *width, unsigned *height,
1758                              unsigned *virtual_width, unsigned *virtual_height) const override {
1759                 *width = *virtual_width = this->width / 2;
1760                 *height = *virtual_height = this->height / 2;
1761         }
1762
1763 private:
1764         unsigned width, height;
1765 };
1766
1767 // Even if the compute shader is not the last effect, it's the one that should decide
1768 // the output size of the phase.
1769 TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
1770         float data[] = {
1771                 0.0f, 0.25f, 0.3f, 0.8f,
1772                 0.75f, 1.0f, 1.0f, 0.2f,
1773         };
1774         float expected_data[] = {
1775                 0.0f, 0.3f,
1776         };
1777         float out_data[2];
1778         EffectChainTester tester(nullptr, 2, 1);
1779         if (!movit_compute_shaders_supported) {
1780                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1781                 return;
1782         }
1783         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 4, 2);
1784
1785         RewritingEffect<Downscale2xComputeEffect> *downscale_effect = new RewritingEffect<Downscale2xComputeEffect>();
1786         tester.get_chain()->add_effect(downscale_effect);
1787         tester.get_chain()->add_effect(new OneToOneEffect());
1788         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1789         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1790
1791         expect_equal(expected_data, out_data, 2, 1);
1792
1793         Phase *phase = downscale_effect->replaced_node->containing_phase;
1794         EXPECT_EQ(2u, phase->output_width);
1795         EXPECT_EQ(1u, phase->output_height);
1796 }
1797
1798 class StrongOneToOneAddEffect : public AddEffect {
1799 public:
1800         StrongOneToOneAddEffect() {}
1801         string effect_type_id() const override { return "StrongOneToOneAddEffect"; }
1802         bool strong_one_to_one_sampling() const override { return true; }
1803 };
1804
1805 TEST(ComputeShaderTest, NoTwoComputeInSamePhase) {
1806         float data[] = {
1807                 0.0f, 0.25f, 0.3f, 0.8f,
1808                 0.75f, 1.0f, 1.0f, 0.2f,
1809         };
1810         float expected_data[] = {
1811                 0.0f, 0.3f,
1812         };
1813         float out_data[2];
1814
1815         ImageFormat format;
1816         format.color_space = COLORSPACE_sRGB;
1817         format.gamma_curve = GAMMA_LINEAR;
1818
1819         EffectChainTester tester(nullptr, 2, 1);
1820         if (!movit_compute_shaders_supported) {
1821                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1822                 return;
1823         }
1824
1825         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1826         input1->set_pixel_data(data);
1827         tester.get_chain()->add_input(input1);
1828         Effect *downscale1 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1829
1830         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1831         input2->set_pixel_data(data);
1832         tester.get_chain()->add_input(input2);
1833         Effect *downscale2 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1834
1835         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), downscale1, downscale2);
1836         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1837         expect_equal(expected_data, out_data, 2, 1);
1838 }
1839
1840 // Like the previous test, but the adder effect is not directly connected
1841 // to the compute shaders (so the status has to be propagated through those effects).
1842 TEST(ComputeShaderTest, NoTwoComputeInSamePhaseIndirect) {
1843         float data[] = {
1844                 0.0f, 0.25f, 0.3f, 0.8f,
1845                 0.75f, 1.0f, 1.0f, 0.2f,
1846         };
1847         float expected_data[] = {
1848                 0.0f, 0.3f,
1849         };
1850         float out_data[2];
1851
1852         ImageFormat format;
1853         format.color_space = COLORSPACE_sRGB;
1854         format.gamma_curve = GAMMA_LINEAR;
1855
1856         EffectChainTester tester(nullptr, 2, 1);
1857         if (!movit_compute_shaders_supported) {
1858                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1859                 return;
1860         }
1861
1862         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1863         input1->set_pixel_data(data);
1864         tester.get_chain()->add_input(input1);
1865         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1866         Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1867
1868         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1869         input2->set_pixel_data(data);
1870         tester.get_chain()->add_input(input2);
1871         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1872         Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1873
1874         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), identity1, identity2);
1875         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1876         expect_equal(expected_data, out_data, 2, 1);
1877 }
1878
1879 // Like the previous test, but the adder is not strong one-to-one
1880 // (so there are two different compute shader inputs, but none of them
1881 // are in the same phase).
1882 TEST(ComputeShaderTest, BounceTextureFromTwoComputeShaders) {
1883         float data[] = {
1884                 0.0f, 0.25f, 0.3f, 0.8f,
1885                 0.75f, 1.0f, 1.0f, 0.2f,
1886         };
1887         float expected_data[] = {
1888                 0.0f, 0.3f,
1889         };
1890         float out_data[2];
1891
1892         ImageFormat format;
1893         format.color_space = COLORSPACE_sRGB;
1894         format.gamma_curve = GAMMA_LINEAR;
1895
1896         EffectChainTester tester(nullptr, 2, 1);
1897         if (!movit_compute_shaders_supported) {
1898                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1899                 return;
1900         }
1901
1902         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1903         input1->set_pixel_data(data);
1904         tester.get_chain()->add_input(input1);
1905         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1906         Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1907
1908         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1909         input2->set_pixel_data(data);
1910         tester.get_chain()->add_input(input2);
1911         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1912         Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1913
1914         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1915         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1916         expect_equal(expected_data, out_data, 2, 1);
1917 }
1918
1919 // Requires mipmaps, but is otherwise like IdentityEffect.
1920 class MipmapNeedingIdentityEffect : public IdentityEffect {
1921 public:
1922         MipmapNeedingIdentityEffect() {}
1923         MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
1924         string effect_type_id() const override { return "MipmapNeedingIdentityEffect"; }
1925         bool strong_one_to_one_sampling() const override { return true; }
1926 };
1927
1928 TEST(ComputeShaderTest, StrongOneToOneButStillNotChained) {
1929         float data[] = {
1930                 0.0f, 0.25f, 0.3f, 0.8f,
1931                 0.75f, 1.0f, 1.0f, 0.2f,
1932         };
1933         float out_data[8];
1934
1935         ImageFormat format;
1936         format.color_space = COLORSPACE_sRGB;
1937         format.gamma_curve = GAMMA_LINEAR;
1938
1939         EffectChainTester tester(nullptr, 4, 2);
1940         if (!movit_compute_shaders_supported) {
1941                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1942                 return;
1943         }
1944
1945         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1946         input1->set_pixel_data(data);
1947         tester.get_chain()->add_input(input1);
1948         Effect *compute_effect = tester.get_chain()->add_effect(new IdentityComputeEffect());
1949
1950         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1951         input2->set_pixel_data(data);
1952         tester.get_chain()->add_input(input2);
1953
1954         // Not chained with the compute shader because MipmapNeedingIdentityEffect comes in
1955         // the same phase, and compute shaders cannot supply mipmaps.
1956         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), compute_effect, input2);
1957         tester.get_chain()->add_effect(new MipmapNeedingIdentityEffect());
1958
1959         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1960         expect_equal(data, out_data, 4, 2);
1961 }
1962
1963 TEST(EffectChainTest, BounceResetsMipmapNeeds) {
1964         float data[] = {
1965                 0.0f, 0.25f,
1966                 0.75f, 1.0f,
1967         };
1968         float out_data[1];
1969
1970         ImageFormat format;
1971         format.color_space = COLORSPACE_sRGB;
1972         format.gamma_curve = GAMMA_LINEAR;
1973
1974         NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1975         input->set_pixel_data(data);
1976
1977         RewritingEffect<IdentityEffect> *identity = new RewritingEffect<IdentityEffect>();
1978
1979         RewritingEffect<ResizeEffect> *downscale = new RewritingEffect<ResizeEffect>();  // Needs mipmaps.
1980         ASSERT_TRUE(downscale->effect->set_int("width", 1));
1981         ASSERT_TRUE(downscale->effect->set_int("height", 1));
1982
1983         EffectChainTester tester(nullptr, 1, 1);
1984         tester.get_chain()->add_input(input);
1985         tester.get_chain()->add_effect(identity);
1986         tester.get_chain()->add_effect(downscale);
1987         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1988
1989         Node *input_node = identity->replaced_node->incoming_links[0];
1990
1991         // The ResizeEffect needs mipmaps. Normally, that would mean that it should
1992         // propagate this tatus down through the IdentityEffect. However, since we
1993         // bounce (due to the resize), the dependency breaks there, and we don't
1994         // need to bounce again between the input and the IdentityEffect.
1995         EXPECT_EQ(input_node->containing_phase,
1996                   identity->replaced_node->containing_phase);
1997         EXPECT_NE(identity->replaced_node->containing_phase,
1998                   downscale->replaced_node->containing_phase);
1999 }
2000
2001 }  // namespace movit