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