]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
Add some missing shaders (required for running tests) to make dist.
[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         float offset[2] { 0.0f, 0.0f };
795 };
796
797 TEST(EffectChainTest, MipmapChainGetsSplit) {
798         float data[] = {
799                 0.0f, 0.0f, 0.0f, 0.0f,
800                 1.0f, 0.0f, 1.0f, 0.0f,
801                 0.0f, 0.0f, 0.0f, 0.0f,
802                 1.0f, 0.0f, 1.0f, 0.0f,
803         };
804
805         // The intermediate result after the first step looks like this,
806         // assuming there are no mipmaps (the zeros are due to border behavior):
807         //
808         //   0 0 0 0
809         //   0 0 0 0
810         //   1 1 0 0
811         //   1 1 0 0
812         //
813         // so another 2x downscale towards the bottom left will give
814         //
815         //   0 0
816         //   1 0
817         //
818         // with yet more zeros coming in on the top and the right from the border.
819         float expected_data[] = {
820                 0.0f, 0.0f, 0.0f, 0.0f,
821                 0.0f, 0.0f, 0.0f, 0.0f,
822                 0.0f, 0.0f, 0.0f, 0.0f,
823                 1.0f, 0.0f, 0.0f, 0.0f,
824         };
825         float out_data[4 * 4];
826
827         float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
828         RewritingEffect<Downscale2xEffect> *pick_out_bottom_left = new RewritingEffect<Downscale2xEffect>(Effect::CANNOT_ACCEPT_MIPMAPS);
829         ASSERT_TRUE(pick_out_bottom_left->effect->set_vec2("offset", offset));
830
831         RewritingEffect<Downscale2xEffect> *downscale2x = new RewritingEffect<Downscale2xEffect>(Effect::NEEDS_MIPMAPS);
832
833         EffectChainTester tester(data, 4, 4, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
834         tester.get_chain()->add_effect(pick_out_bottom_left);
835         tester.get_chain()->add_effect(downscale2x);
836         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
837
838         EXPECT_NE(pick_out_bottom_left->replaced_node->containing_phase,
839                   downscale2x->replaced_node->containing_phase);
840
841         expect_equal(expected_data, out_data, 4, 4);
842 }
843
844 // An effect that adds its two inputs together. Used below.
845 class AddEffect : public Effect {
846 public:
847         AddEffect() {}
848         string effect_type_id() const override { return "AddEffect"; }
849         string output_fragment_shader() override { return read_file("add.frag"); }
850         unsigned num_inputs() const override { return 2; }
851         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
852 };
853
854 // Constructs the graph
855 //
856 //             FlatInput               |
857 //            /         \              |
858 //  MultiplyEffect  MultiplyEffect     |
859 //            \         /              |
860 //             AddEffect               |
861 //
862 // and verifies that it gives the correct output.
863 TEST(EffectChainTest, DiamondGraph) {
864         float data[] = {
865                 1.0f, 1.0f,
866                 1.0f, 0.0f,
867         };
868         float expected_data[] = {
869                 2.5f, 2.5f,
870                 2.5f, 0.0f,
871         };
872         float out_data[2 * 2];
873
874         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
875         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
876
877         MultiplyEffect *mul_half = new MultiplyEffect();
878         ASSERT_TRUE(mul_half->set_vec4("factor", half));
879         
880         MultiplyEffect *mul_two = new MultiplyEffect();
881         ASSERT_TRUE(mul_two->set_vec4("factor", two));
882
883         EffectChainTester tester(nullptr, 2, 2);
884
885         ImageFormat format;
886         format.color_space = COLORSPACE_sRGB;
887         format.gamma_curve = GAMMA_LINEAR;
888
889         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
890         input->set_pixel_data(data);
891
892         tester.get_chain()->add_input(input);
893         tester.get_chain()->add_effect(mul_half, input);
894         tester.get_chain()->add_effect(mul_two, input);
895         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
896         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
897
898         expect_equal(expected_data, out_data, 2, 2);
899 }
900
901 // Constructs the graph
902 //
903 //             FlatInput                     |
904 //            /         \                    |
905 //  MultiplyEffect  MultiplyEffect           |
906 //         \             |                   |
907 //          \    BouncingIdentityEffect      |  
908 //            \         /                    |
909 //             AddEffect                     |
910 //
911 // and verifies that it gives the correct output.
912 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
913         float data[] = {
914                 1.0f, 1.0f,
915                 1.0f, 0.0f,
916         };
917         float expected_data[] = {
918                 2.5f, 2.5f,
919                 2.5f, 0.0f,
920         };
921         float out_data[2 * 2];
922
923         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
924         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
925
926         MultiplyEffect *mul_half = new MultiplyEffect();
927         ASSERT_TRUE(mul_half->set_vec4("factor", half));
928         
929         MultiplyEffect *mul_two = new MultiplyEffect();
930         ASSERT_TRUE(mul_two->set_vec4("factor", two));
931         
932         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
933
934         EffectChainTester tester(nullptr, 2, 2);
935
936         ImageFormat format;
937         format.color_space = COLORSPACE_sRGB;
938         format.gamma_curve = GAMMA_LINEAR;
939
940         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
941         input->set_pixel_data(data);
942
943         tester.get_chain()->add_input(input);
944         tester.get_chain()->add_effect(mul_half, input);
945         tester.get_chain()->add_effect(mul_two, input);
946         tester.get_chain()->add_effect(bounce, mul_two);
947         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
948         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
949
950         expect_equal(expected_data, out_data, 2, 2);
951 }
952
953 // Constructs the graph
954 //
955 //                        FlatInput                               |
956 //                       /         \                              |
957 //  Downscale2xEffect (mipmaps)  Downscale2xEffect (no mipmaps)   |
958 //                      |           |                             |
959 //  Downscale2xEffect (mipmaps)  Downscale2xEffect (no mipmaps)   |
960 //                       \         /                              |
961 //                        AddEffect                               |
962 //
963 // and verifies that it gives the correct output. Due to the conflicting
964 // mipmap demands, EffectChain needs to make two phases; exactly where it's
965 // split is less important, though (this is a fairly obscure situation that
966 // is unlikely to happen in practice).
967 TEST(EffectChainTest, DiamondGraphWithConflictingMipmaps) {
968         float data[] = {
969                 0.0f, 0.0f, 0.0f, 0.0f,
970                 1.0f, 0.0f, 1.0f, 0.0f,
971                 0.0f, 0.0f, 0.0f, 0.0f,
972                 1.0f, 0.0f, 1.0f, 0.0f,
973         };
974
975         // Same situation as MipmapChainGetsSplit. The output of the two
976         // downscales with no mipmaps looks like this:
977         //
978         //    0 0 0 0
979         //    0 0 0 0
980         //    0 0 0 0
981         //    1 0 0 0
982         //
983         // and the one with mipmaps is 0.25 everywhere. Due to postmultiplied
984         // alpha, we get the average even though we are using AddEffect.
985         float expected_data[] = {
986                 0.125f, 0.125f, 0.125f, 0.125f,
987                 0.125f, 0.125f, 0.125f, 0.125f,
988                 0.125f, 0.125f, 0.125f, 0.125f,
989                 0.625f, 0.125f, 0.125f, 0.125f,
990         };
991         float out_data[4 * 4];
992
993         float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
994         Downscale2xEffect *nomipmap1 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
995         Downscale2xEffect *nomipmap2 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
996         ASSERT_TRUE(nomipmap1->set_vec2("offset", offset));
997         ASSERT_TRUE(nomipmap2->set_vec2("offset", offset));
998
999         Downscale2xEffect *mipmap1 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1000         Downscale2xEffect *mipmap2 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1001
1002         EffectChainTester tester(nullptr, 4, 4);
1003
1004         ImageFormat format;
1005         format.color_space = COLORSPACE_sRGB;
1006         format.gamma_curve = GAMMA_LINEAR;
1007
1008         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 4);
1009         input->set_pixel_data(data);
1010
1011         tester.get_chain()->add_input(input);
1012
1013         tester.get_chain()->add_effect(nomipmap1, input);
1014         tester.get_chain()->add_effect(nomipmap2, nomipmap1);
1015
1016         tester.get_chain()->add_effect(mipmap1, input);
1017         tester.get_chain()->add_effect(mipmap2, mipmap1);
1018
1019         tester.get_chain()->add_effect(new AddEffect(), nomipmap2, mipmap2);
1020         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1021
1022         expect_equal(expected_data, out_data, 4, 4);
1023 }
1024
1025 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
1026         float data[] = {
1027                 0.735f, 0.0f,
1028                 0.735f, 0.0f,
1029         };
1030         float expected_data[] = {
1031                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1032                 0.0f, 0.5f,
1033         };
1034         float out_data[2 * 2];
1035         
1036         EffectChainTester tester(nullptr, 2, 2);
1037         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
1038
1039         // MirrorEffect does not get linear light, so the conversions will be
1040         // inserted after it, not before.
1041         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1042         tester.get_chain()->add_effect(effect);
1043
1044         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1045         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1046         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1047         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1048
1049         expect_equal(expected_data, out_data, 2, 2);
1050
1051         Node *node = effect->replaced_node;
1052         ASSERT_EQ(1u, node->incoming_links.size());
1053         ASSERT_EQ(1u, node->outgoing_links.size());
1054         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1055         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
1056 }
1057
1058 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
1059         float data[] = {
1060                 0.5f, 0.0f,
1061                 0.5f, 0.0f,
1062         };
1063         float expected_data[] = {
1064                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1065                 0.0f, 0.5f,
1066         };
1067         float out_data[2 * 2];
1068         
1069         EffectChainTester tester(nullptr, 2, 2);
1070         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
1071
1072         // MirrorEffect does not get linear light, so the conversions will be
1073         // inserted after it, not before.
1074         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1075         tester.get_chain()->add_effect(effect);
1076
1077         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1078         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1079         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1080         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1081
1082         expect_equal(expected_data, out_data, 2, 2);
1083
1084         Node *node = effect->replaced_node;
1085         ASSERT_EQ(1u, node->incoming_links.size());
1086         ASSERT_EQ(1u, node->outgoing_links.size());
1087         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1088         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
1089 }
1090
1091 // An effect that does nothing, but requests texture bounce and stores
1092 // its input size.
1093 class SizeStoringEffect : public BouncingIdentityEffect {
1094 public:
1095         SizeStoringEffect() : input_width(-1), input_height(-1) {}
1096         void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
1097                 assert(input_num == 0);
1098                 input_width = width;
1099                 input_height = height;
1100         }
1101         string effect_type_id() const override { return "SizeStoringEffect"; }
1102
1103         int input_width, input_height;
1104 };
1105
1106 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
1107         float data[2 * 2] = {
1108                 0.0f, 0.0f,
1109                 0.0f, 0.0f,
1110         };
1111         float out_data[4 * 3];
1112         
1113         EffectChainTester tester(nullptr, 4, 3);  // Note non-square aspect.
1114
1115         ImageFormat format;
1116         format.color_space = COLORSPACE_sRGB;
1117         format.gamma_curve = GAMMA_LINEAR;
1118
1119         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1120         input1->set_pixel_data(data);
1121         
1122         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1123         input2->set_pixel_data(data);
1124
1125         SizeStoringEffect *input_store = new SizeStoringEffect();
1126
1127         tester.get_chain()->add_input(input1);
1128         tester.get_chain()->add_input(input2);
1129         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1130         tester.get_chain()->add_effect(input_store);
1131         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1132
1133         EXPECT_EQ(2, input_store->input_width);
1134         EXPECT_EQ(2, input_store->input_height);
1135 }
1136
1137 TEST(EffectChainTest, AspectRatioConversion) {
1138         float data1[4 * 3] = {
1139                 0.0f, 0.0f, 0.0f, 0.0f,
1140                 0.0f, 0.0f, 0.0f, 0.0f,
1141                 0.0f, 0.0f, 0.0f, 0.0f,
1142         };
1143         float data2[7 * 7] = {
1144                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
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, 1.0f, 0.0f, 0.0f, 0.0f,
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         };
1152
1153         // The right conversion here is that the 7x7 image decides the size,
1154         // since it is the biggest, so everything is scaled up to 9x7
1155         // (keep the height, round the width 9.333 to 9). 
1156         float out_data[9 * 7];
1157         
1158         EffectChainTester tester(nullptr, 4, 3);
1159
1160         ImageFormat format;
1161         format.color_space = COLORSPACE_sRGB;
1162         format.gamma_curve = GAMMA_LINEAR;
1163
1164         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1165         input1->set_pixel_data(data1);
1166         
1167         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1168         input2->set_pixel_data(data2);
1169
1170         SizeStoringEffect *input_store = new SizeStoringEffect();
1171
1172         tester.get_chain()->add_input(input1);
1173         tester.get_chain()->add_input(input2);
1174         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1175         tester.get_chain()->add_effect(input_store);
1176         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1177
1178         EXPECT_EQ(9, input_store->input_width);
1179         EXPECT_EQ(7, input_store->input_height);
1180 }
1181
1182 // Tests that putting a BlueInput (constant color) into its own pass,
1183 // which creates a phase that doesn't need texture coordinates,
1184 // doesn't mess up a second phase that actually does.
1185 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1186         const int size = 2;
1187         float data[] = {
1188                 1.0f,
1189                 0.0f,
1190         };
1191         float expected_data[] = {
1192                 1.0f, 1.0f, 2.0f, 2.0f,
1193                 0.0f, 0.0f, 1.0f, 2.0f,
1194         };
1195         float out_data[size * 4];
1196         // First say that we have sRGB, linear input.
1197         ImageFormat format;
1198         format.color_space = COLORSPACE_sRGB;
1199         format.gamma_curve = GAMMA_LINEAR;
1200         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1201
1202         input->set_pixel_data(data);
1203         EffectChainTester tester(nullptr, 1, size);
1204         tester.get_chain()->add_input(new BlueInput());
1205         Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1206         tester.get_chain()->add_input(input);
1207         tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1208
1209         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1210
1211         expect_equal(expected_data, out_data, 4, size);
1212 }
1213
1214 // An effect that does nothing except changing its output sizes.
1215 class VirtualResizeEffect : public Effect {
1216 public:
1217         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1218                 : width(width),
1219                   height(height),
1220                   virtual_width(virtual_width),
1221                   virtual_height(virtual_height) {}
1222         string effect_type_id() const override { return "VirtualResizeEffect"; }
1223         string output_fragment_shader() override { return read_file("identity.frag"); }
1224
1225         bool changes_output_size() const override { return true; }
1226
1227         void get_output_size(unsigned *width, unsigned *height,
1228                              unsigned *virtual_width, unsigned *virtual_height) const override {
1229                 *width = this->width;
1230                 *height = this->height;
1231                 *virtual_width = this->virtual_width;
1232                 *virtual_height = this->virtual_height;
1233         }
1234
1235 private:
1236         int width, height, virtual_width, virtual_height;
1237 };
1238
1239 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1240         const int size = 2, bigger_size = 3;
1241         float data[size * size] = {
1242                 1.0f, 0.0f,
1243                 0.0f, 1.0f,
1244         };
1245         float out_data[size * size];
1246         
1247         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1248
1249         SizeStoringEffect *size_store = new SizeStoringEffect();
1250
1251         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1252         tester.get_chain()->add_effect(size_store);
1253         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1254         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1255
1256         EXPECT_EQ(bigger_size, size_store->input_width);
1257         EXPECT_EQ(bigger_size, size_store->input_height);
1258
1259         // If the resize is implemented as non-virtual, we'll fail here,
1260         // since bilinear scaling from 2x2 â†’ 3x3 â†’ 2x2 is not very exact.
1261         expect_equal(data, out_data, size, size);
1262 }
1263
1264 // An effect that is like VirtualResizeEffect, but always has virtual and real
1265 // sizes the same (and promises this).
1266 class NonVirtualResizeEffect : public VirtualResizeEffect {
1267 public:
1268         NonVirtualResizeEffect(int width, int height)
1269                 : VirtualResizeEffect(width, height, width, height) {}
1270         string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1271         bool sets_virtual_output_size() const override { return false; }
1272 };
1273
1274 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1275 class OneToOneEffect : public Effect {
1276 public:
1277         OneToOneEffect() {}
1278         string effect_type_id() const override { return "OneToOneEffect"; }
1279         string output_fragment_shader() override { return read_file("identity.frag"); }
1280         bool strong_one_to_one_sampling() const override { return true; }
1281 };
1282
1283 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1284         const int size = 2;
1285         float data[size * size] = {
1286                 1.0f, 0.0f,
1287                 0.0f, 1.0f,
1288         };
1289         float out_data[size * size];
1290
1291         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1292
1293         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1294         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1295
1296         if (GetParam() == "compute") {
1297                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1298         } else {
1299                 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1300         }
1301         tester.get_chain()->add_effect(effect1);
1302         tester.get_chain()->add_effect(effect2);
1303         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1304
1305         expect_equal(data, out_data, size, size);
1306
1307         // The first OneToOneEffect should be in the same phase as its input.
1308         ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1309         EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1310                   effect1->replaced_node->containing_phase);
1311
1312         // The second OneToOneEffect, too.
1313         EXPECT_EQ(effect1->replaced_node->containing_phase,
1314                   effect2->replaced_node->containing_phase);
1315 }
1316
1317 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1318         const int size = 2;
1319         float data[size * size] = {
1320                 1.0f, 0.0f,
1321                 0.0f, 1.0f,
1322         };
1323         float out_data[size * size];
1324
1325         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1326
1327         RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1328         RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1329         RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1330         RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1331
1332         tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1333         tester.get_chain()->add_effect(effect1);
1334         tester.get_chain()->add_effect(effect2);
1335         tester.get_chain()->add_effect(effect3);
1336         tester.get_chain()->add_effect(effect4);
1337         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1338
1339         expect_equal(data, out_data, size, size);
1340
1341         // The NonVirtualResizeEffect should be in a different phase from
1342         // the IdentityEffect (since the latter is not one-to-one),
1343         // ie., the chain should be broken somewhere between them, but exactly
1344         // where doesn't matter.
1345         ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1346         EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1347                   effect3->replaced_node->containing_phase);
1348
1349         // The last OneToOneEffect should also be in the same phase as the
1350         // IdentityEffect (the phase was already broken).
1351         EXPECT_EQ(effect3->replaced_node->containing_phase,
1352                   effect4->replaced_node->containing_phase);
1353 }
1354
1355 // Does not use EffectChainTest, so that it can construct an EffectChain without
1356 // a shared ResourcePool (which is also properly destroyed afterwards).
1357 // Also turns on debugging to test that code path.
1358 TEST(EffectChainTest, IdentityWithOwnPool) {
1359         const int width = 3, height = 2;
1360         float data[] = {
1361                 0.0f, 0.25f, 0.3f,
1362                 0.75f, 1.0f, 1.0f,
1363         };
1364         const float expected_data[] = {
1365                 0.75f, 1.0f, 1.0f,
1366                 0.0f, 0.25f, 0.3f,
1367         };
1368         float out_data[6], temp[6 * 4];
1369
1370         EffectChain chain(width, height);
1371         MovitDebugLevel old_movit_debug_level = movit_debug_level;
1372         movit_debug_level = MOVIT_DEBUG_ON;
1373
1374         ImageFormat format;
1375         format.color_space = COLORSPACE_sRGB;
1376         format.gamma_curve = GAMMA_LINEAR;
1377
1378         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1379         input->set_pixel_data(data);
1380         chain.add_input(input);
1381         chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1382
1383         GLuint texnum, fbo;
1384         glGenTextures(1, &texnum);
1385         check_error();
1386         glBindTexture(GL_TEXTURE_2D, texnum);
1387         check_error();
1388         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1389         check_error();
1390
1391         glGenFramebuffers(1, &fbo);
1392         check_error();
1393         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1394         check_error();
1395         glFramebufferTexture2D(
1396                 GL_FRAMEBUFFER,
1397                 GL_COLOR_ATTACHMENT0,
1398                 GL_TEXTURE_2D,
1399                 texnum,
1400                 0);
1401         check_error();
1402         glBindFramebuffer(GL_FRAMEBUFFER, 0);
1403         check_error();
1404
1405         chain.finalize();
1406
1407         chain.render_to_fbo(fbo, width, height);
1408
1409         glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1410         check_error();
1411         glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1412         check_error();
1413         for (unsigned i = 0; i < 6; ++i) {
1414                 out_data[i] = temp[i * 4];
1415         }
1416
1417         expect_equal(expected_data, out_data, width, height);
1418
1419         // Reset the debug status again.
1420         movit_debug_level = old_movit_debug_level;
1421 }
1422
1423 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1424 class PrintfingBlueEffect : public Effect {
1425 public:
1426         PrintfingBlueEffect() {}
1427         string effect_type_id() const override { return "PrintfingBlueEffect"; }
1428         string output_fragment_shader() override {
1429                 stringstream ss;
1430                 ss.imbue(locale("C"));
1431                 ss.precision(8);
1432                 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1433                    << 0.0f << ", " << 0.0f << ", "
1434                    << 0.5f << ", " << 1.0f << "); }\n";
1435                 return ss.str();
1436         }
1437 };
1438
1439 TEST(EffectChainTest, StringStreamLocalesWork) {
1440         // An example of a locale with comma instead of period as decimal separator.
1441         // Obviously, if you run on a machine without this locale available,
1442         // the test will always succeed. Note that the OpenGL driver might call
1443         // setlocale() behind-the-scenes, and that might corrupt the returned
1444         // pointer, so we need to take our own copy of it here.
1445         char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1446         if (saved_locale == nullptr) {
1447                 // The locale wasn't available.
1448                 return;
1449         }
1450         saved_locale = strdup(saved_locale);
1451         float data[] = {
1452                 0.0f, 0.0f, 0.0f, 0.0f,
1453         };
1454         float expected_data[] = {
1455                 0.0f, 0.0f, 0.5f, 1.0f,
1456         };
1457         float out_data[4];
1458         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1459         tester.get_chain()->add_effect(new PrintfingBlueEffect());
1460         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1461
1462         expect_equal(expected_data, out_data, 4, 1);
1463
1464         setlocale(LC_ALL, saved_locale);
1465         free(saved_locale);
1466 }
1467
1468 TEST(EffectChainTest, sRGBIntermediate) {
1469         float data[] = {
1470                 0.0f, 0.5f, 0.0f, 1.0f,
1471         };
1472         float out_data[4];
1473         EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1474         tester.get_chain()->set_intermediate_format(GL_SRGB8);
1475         tester.get_chain()->add_effect(new IdentityEffect());
1476         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1477         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1478
1479         EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1480             << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1481         EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1482             << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1483
1484         // This state should have been preserved.
1485         EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1486 }
1487
1488 // An effect that is like IdentityEffect, but also does not require linear light.
1489 class PassThroughEffect : public IdentityEffect {
1490 public:
1491         PassThroughEffect() {}
1492         string effect_type_id() const override { return "PassThroughEffect"; }
1493         bool needs_linear_light() const override { return false; }
1494         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1495 };
1496
1497 // Same, just also bouncing.
1498 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1499 public:
1500         BouncingPassThroughEffect() {}
1501         string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1502         bool needs_linear_light() const override { return false; }
1503         bool needs_texture_bounce() const override { return true; }
1504         AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1505 };
1506
1507 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1508         // Note that we do the comparison in sRGB space, which is what we
1509         // typically would want; however, we do the sRGB conversion ourself
1510         // to avoid compounding errors from shader conversions into the
1511         // analysis.
1512         const int size = 4096;  // 12-bit.
1513         float linear_data[size], data[size], out_data[size];
1514
1515         for (int i = 0; i < size; ++i) {
1516                 linear_data[i] = i / double(size - 1);
1517                 data[i] = srgb_to_linear(linear_data[i]);
1518         }
1519
1520         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1521         tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1522         tester.get_chain()->add_effect(new IdentityEffect());
1523         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1524         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1525
1526         for (int i = 0; i < size; ++i) {
1527                 out_data[i] = linear_to_srgb(out_data[i]);
1528         }
1529
1530         // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1531         // framebuffer. (Slightly more on NVIDIA cards.)
1532         expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1533 }
1534
1535 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1536         // Note that we do the comparison in sRGB space, which is what we
1537         // typically would want; however, we do the sRGB conversion ourself
1538         // to avoid compounding errors from shader conversions into the
1539         // analysis.
1540         const int size = 4096;  // 12-bit.
1541         float linear_data[size], data[size], out_data[size];
1542
1543         for (int i = 0; i < size; ++i) {
1544                 linear_data[i] = i / double(size - 1);
1545                 data[i] = srgb_to_linear(linear_data[i]);
1546         }
1547
1548         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1549         tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1550         if (GetParam() == "compute") {
1551                 tester.get_chain()->add_effect(new IdentityComputeEffect());
1552         } else {
1553                 tester.get_chain()->add_effect(new IdentityEffect());
1554         }
1555         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1556         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1557
1558         for (int i = 0; i < size; ++i) {
1559                 out_data[i] = linear_to_srgb(out_data[i]);
1560         }
1561
1562         // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1563         // framebuffer (ideal would be 0.5). That is an order of magnitude better
1564         // than in the linear test above. The RMS error is much better, too.
1565         expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1566 }
1567
1568 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1569         const int size = 256;  // 8-bit.
1570         float data[size], out_data[size];
1571
1572         for (int i = 0; i < size; ++i) {
1573                 data[i] = i / double(size - 1);
1574         }
1575
1576         EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1577         tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1578         tester.get_chain()->add_effect(new PassThroughEffect());
1579         tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1580         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1581
1582         // The data should be passed through nearly exactly, since there is no effect
1583         // on the path that requires linear light. (Actually, it _is_ exact modulo
1584         // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1585         expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1586 }
1587
1588 // An effect that stores which program number was last run under.
1589 class RecordingIdentityEffect : public Effect {
1590 public:
1591         RecordingIdentityEffect() {}
1592         string effect_type_id() const override { return "RecordingIdentityEffect"; }
1593         string output_fragment_shader() override { return read_file("identity.frag"); }
1594
1595         GLuint last_glsl_program_num;
1596         void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1597         {
1598                 last_glsl_program_num = glsl_program_num;
1599         }
1600 };
1601
1602 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1603         float data[] = {
1604                 0.0f, 0.25f, 0.3f,
1605                 0.75f, 1.0f, 1.0f,
1606         };
1607         float out_data[6];
1608         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1609         RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1610         tester.get_chain()->add_effect(effect);
1611         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1612
1613         expect_equal(data, out_data, 3, 2);
1614
1615         ASSERT_NE(0u, effect->last_glsl_program_num);
1616
1617         // Now pretend some other effect is using this program number;
1618         // ResourcePool will then need to clone it.
1619         ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1620         GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1621         EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1622
1623         // Re-run should still give the correct data, but it should have run
1624         // with a different program.
1625         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1626         expect_equal(data, out_data, 3, 2);
1627         EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1628
1629         // Release the program, and check one final time.
1630         resource_pool->unuse_glsl_program(master_program_num);
1631         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1632         expect_equal(data, out_data, 3, 2);
1633 }
1634
1635 TEST(ComputeShaderTest, Identity) {
1636         float data[] = {
1637                 0.0f, 0.25f, 0.3f,
1638                 0.75f, 1.0f, 1.0f,
1639         };
1640         float out_data[6];
1641         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1642         if (!movit_compute_shaders_supported) {
1643                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1644                 return;
1645         }
1646         tester.get_chain()->add_effect(new IdentityComputeEffect());
1647         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1648
1649         expect_equal(data, out_data, 3, 2);
1650 }
1651
1652 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1653 // the very last effect in the chain, which means we can't output it directly
1654 // to the screen.
1655 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1656         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1657 };
1658
1659 TEST(ComputeShaderTest, LastEffectInChain) {
1660         float data[] = {
1661                 0.0f, 0.25f, 0.3f,
1662                 0.75f, 1.0f, 1.0f,
1663         };
1664         float out_data[6];
1665         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1666         if (!movit_compute_shaders_supported) {
1667                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1668                 return;
1669         }
1670         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1671         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1672
1673         expect_equal(data, out_data, 3, 2);
1674 }
1675
1676 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1677         uint8_t data[] = {
1678                 14, 200, 80,
1679                 90, 100, 110,
1680         };
1681         uint8_t out_data[6];
1682         EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1683         if (!movit_compute_shaders_supported) {
1684                 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1685                 return;
1686         }
1687         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1688         tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1689         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1690
1691         expect_equal(data, out_data, 3, 2);
1692 }
1693
1694 // A compute shader to mirror the inputs, in 2x2 blocks.
1695 class MirrorComputeEffect : public Effect {
1696 public:
1697         MirrorComputeEffect() {}
1698         string effect_type_id() const override { return "MirrorComputeEffect"; }
1699         bool is_compute_shader() const override { return true; }
1700         string output_fragment_shader() override { return read_file("mirror.comp"); }
1701         void get_compute_dimensions(unsigned output_width, unsigned output_height,
1702                                     unsigned *x, unsigned *y, unsigned *z) const override {
1703                 *x = output_width / 2;
1704                 *y = output_height / 2;
1705                 *z = 1;
1706         }
1707 };
1708
1709 TEST(ComputeShaderTest, ComputeThenOneToOne) {
1710         float data[] = {
1711                 0.0f, 0.25f, 0.3f, 0.8f,
1712                 0.75f, 1.0f, 1.0f, 0.2f,
1713         };
1714         float expected_data[] = {
1715                 0.8f, 0.3f, 0.25f, 0.0f,
1716                 0.2f, 1.0f, 1.0f, 0.75f,
1717         };
1718         float out_data[8];
1719         EffectChainTester tester(data, 4, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1720         tester.get_chain()->add_effect(new MirrorComputeEffect());
1721         tester.get_chain()->add_effect(new OneToOneEffect());
1722         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1723
1724         expect_equal(expected_data, out_data, 4, 2);
1725 }
1726
1727 // A compute shader that also resizes its input, taking the upper-left pixel
1728 // of every 2x2 group. (The shader is hard-coded to 4x2 input for simplicity.)
1729 class Downscale2xComputeEffect : public Effect {
1730 public:
1731         Downscale2xComputeEffect() {}
1732         string effect_type_id() const override { return "Downscale2xComputeEffect"; }
1733         bool is_compute_shader() const override { return true; }
1734         string output_fragment_shader() override { return read_file("downscale2x.comp"); }
1735         bool changes_output_size() const override { return true; }
1736         void inform_input_size(unsigned input_num, unsigned width, unsigned height) override
1737         {
1738                 this->width = width;
1739                 this->height = height;
1740         }
1741         void get_output_size(unsigned *width, unsigned *height,
1742                              unsigned *virtual_width, unsigned *virtual_height) const override {
1743                 *width = *virtual_width = this->width / 2;
1744                 *height = *virtual_height = this->height / 2;
1745         }
1746
1747 private:
1748         unsigned width, height;
1749 };
1750
1751 // Even if the compute shader is not the last effect, it's the one that should decide
1752 // the output size of the phase.
1753 TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
1754         float data[] = {
1755                 0.0f, 0.25f, 0.3f, 0.8f,
1756                 0.75f, 1.0f, 1.0f, 0.2f,
1757         };
1758         float expected_data[] = {
1759                 0.0f, 0.3f,
1760         };
1761         float out_data[2];
1762         EffectChainTester tester(nullptr, 2, 1);
1763         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 4, 2);
1764
1765         RewritingEffect<Downscale2xComputeEffect> *downscale_effect = new RewritingEffect<Downscale2xComputeEffect>();
1766         tester.get_chain()->add_effect(downscale_effect);
1767         tester.get_chain()->add_effect(new OneToOneEffect());
1768         tester.get_chain()->add_effect(new BouncingIdentityEffect());
1769         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1770
1771         expect_equal(expected_data, out_data, 2, 1);
1772
1773         Phase *phase = downscale_effect->replaced_node->containing_phase;
1774         EXPECT_EQ(2u, phase->output_width);
1775         EXPECT_EQ(1u, phase->output_height);
1776 }
1777
1778 class StrongOneToOneAddEffect : public AddEffect {
1779 public:
1780         StrongOneToOneAddEffect() {}
1781         string effect_type_id() const override { return "StrongOneToOneAddEffect"; }
1782         bool strong_one_to_one_sampling() const override { return true; }
1783 };
1784
1785 TEST(ComputeShaderTest, NoTwoComputeInSamePhase) {
1786         float data[] = {
1787                 0.0f, 0.25f, 0.3f, 0.8f,
1788                 0.75f, 1.0f, 1.0f, 0.2f,
1789         };
1790         float expected_data[] = {
1791                 0.0f, 0.3f,
1792         };
1793         float out_data[2];
1794
1795         ImageFormat format;
1796         format.color_space = COLORSPACE_sRGB;
1797         format.gamma_curve = GAMMA_LINEAR;
1798
1799         EffectChainTester tester(nullptr, 2, 1);
1800
1801         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1802         input1->set_pixel_data(data);
1803         tester.get_chain()->add_input(input1);
1804         Effect *downscale1 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1805
1806         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1807         input2->set_pixel_data(data);
1808         tester.get_chain()->add_input(input2);
1809         Effect *downscale2 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1810
1811         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), downscale1, downscale2);
1812         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1813         expect_equal(expected_data, out_data, 2, 1);
1814 }
1815
1816 // Like the previous test, but the adder effect is not directly connected
1817 // to the compute shaders (so the status has to be propagated through those effects).
1818 TEST(ComputeShaderTest, NoTwoComputeInSamePhaseIndirect) {
1819         float data[] = {
1820                 0.0f, 0.25f, 0.3f, 0.8f,
1821                 0.75f, 1.0f, 1.0f, 0.2f,
1822         };
1823         float expected_data[] = {
1824                 0.0f, 0.3f,
1825         };
1826         float out_data[2];
1827
1828         ImageFormat format;
1829         format.color_space = COLORSPACE_sRGB;
1830         format.gamma_curve = GAMMA_LINEAR;
1831
1832         EffectChainTester tester(nullptr, 2, 1);
1833
1834         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1835         input1->set_pixel_data(data);
1836         tester.get_chain()->add_input(input1);
1837         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1838         Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1839
1840         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1841         input2->set_pixel_data(data);
1842         tester.get_chain()->add_input(input2);
1843         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1844         Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1845
1846         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), identity1, identity2);
1847         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1848         expect_equal(expected_data, out_data, 2, 1);
1849 }
1850
1851 // Like the previous test, but the adder is not strong one-to-one
1852 // (so there are two different compute shader inputs, but none of them
1853 // are in the same phase).
1854 TEST(ComputeShaderTest, BounceTextureFromTwoComputeShaders) {
1855         float data[] = {
1856                 0.0f, 0.25f, 0.3f, 0.8f,
1857                 0.75f, 1.0f, 1.0f, 0.2f,
1858         };
1859         float expected_data[] = {
1860                 0.0f, 0.3f,
1861         };
1862         float out_data[2];
1863
1864         ImageFormat format;
1865         format.color_space = COLORSPACE_sRGB;
1866         format.gamma_curve = GAMMA_LINEAR;
1867
1868         EffectChainTester tester(nullptr, 2, 1);
1869
1870         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1871         input1->set_pixel_data(data);
1872         tester.get_chain()->add_input(input1);
1873         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1874         Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1875
1876         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1877         input2->set_pixel_data(data);
1878         tester.get_chain()->add_input(input2);
1879         tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1880         Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1881
1882         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1883         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1884         expect_equal(expected_data, out_data, 2, 1);
1885 }
1886
1887 // Requires mipmaps, but is otherwise like IdentityEffect.
1888 class MipmapNeedingIdentityEffect : public IdentityEffect {
1889 public:
1890         MipmapNeedingIdentityEffect() {}
1891         MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
1892         string effect_type_id() const override { return "MipmapNeedingIdentityEffect"; }
1893         bool strong_one_to_one_sampling() const override { return true; }
1894 };
1895
1896 TEST(ComputeShaderTest, StrongOneToOneButStillNotChained) {
1897         float data[] = {
1898                 0.0f, 0.25f, 0.3f, 0.8f,
1899                 0.75f, 1.0f, 1.0f, 0.2f,
1900         };
1901         float out_data[8];
1902
1903         ImageFormat format;
1904         format.color_space = COLORSPACE_sRGB;
1905         format.gamma_curve = GAMMA_LINEAR;
1906
1907         EffectChainTester tester(nullptr, 4, 2);
1908
1909         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1910         input1->set_pixel_data(data);
1911         tester.get_chain()->add_input(input1);
1912         Effect *compute_effect = tester.get_chain()->add_effect(new IdentityComputeEffect());
1913
1914         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1915         input2->set_pixel_data(data);
1916         tester.get_chain()->add_input(input2);
1917
1918         // Not chained with the compute shader because MipmapNeedingIdentityEffect comes in
1919         // the same phase, and compute shaders cannot supply mipmaps.
1920         tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), compute_effect, input2);
1921         tester.get_chain()->add_effect(new MipmapNeedingIdentityEffect());
1922
1923         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1924         expect_equal(data, out_data, 4, 2);
1925 }
1926
1927 TEST(EffectChainTest, BounceResetsMipmapNeeds) {
1928         float data[] = {
1929                 0.0f, 0.25f,
1930                 0.75f, 1.0f,
1931         };
1932         float out_data[1];
1933
1934         ImageFormat format;
1935         format.color_space = COLORSPACE_sRGB;
1936         format.gamma_curve = GAMMA_LINEAR;
1937
1938         NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1939         input->set_pixel_data(data);
1940
1941         RewritingEffect<IdentityEffect> *identity = new RewritingEffect<IdentityEffect>();
1942
1943         RewritingEffect<ResizeEffect> *downscale = new RewritingEffect<ResizeEffect>();  // Needs mipmaps.
1944         ASSERT_TRUE(downscale->effect->set_int("width", 1));
1945         ASSERT_TRUE(downscale->effect->set_int("height", 1));
1946
1947         EffectChainTester tester(nullptr, 1, 1);
1948         tester.get_chain()->add_input(input);
1949         tester.get_chain()->add_effect(identity);
1950         tester.get_chain()->add_effect(downscale);
1951         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1952
1953         Node *input_node = identity->replaced_node->incoming_links[0];
1954
1955         // The ResizeEffect needs mipmaps. Normally, that would mean that it should
1956         // propagate this tatus down through the IdentityEffect. However, since we
1957         // bounce (due to the resize), the dependency breaks there, and we don't
1958         // need to bounce again between the input and the IdentityEffect.
1959         EXPECT_EQ(input_node->containing_phase,
1960                   identity->replaced_node->containing_phase);
1961         EXPECT_NE(identity->replaced_node->containing_phase,
1962                   downscale->replaced_node->containing_phase);
1963 }
1964
1965 }  // namespace movit