]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
49d431b98db0a067e01076ae5e2f1fcd0550c82c
[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 <GL/glew.h>
6 #include <assert.h>
7 #include <stddef.h>
8
9 #include "effect.h"
10 #include "effect_chain.h"
11 #include "flat_input.h"
12 #include "gtest/gtest.h"
13 #include "input.h"
14 #include "mirror_effect.h"
15 #include "resize_effect.h"
16 #include "test_util.h"
17 #include "util.h"
18
19 TEST(EffectChainTest, EmptyChain) {
20         float data[] = {
21                 0.0f, 0.25f, 0.3f,
22                 0.75f, 1.0f, 1.0f,
23         };
24         float out_data[6];
25         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
26         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
27
28         expect_equal(data, out_data, 3, 2);
29 }
30
31 // An effect that does nothing.
32 class IdentityEffect : public Effect {
33 public:
34         IdentityEffect() {}
35         virtual std::string effect_type_id() const { return "IdentityEffect"; }
36         std::string output_fragment_shader() { return read_file("identity.frag"); }
37 };
38
39 TEST(EffectChainTest, Identity) {
40         float data[] = {
41                 0.0f, 0.25f, 0.3f,
42                 0.75f, 1.0f, 1.0f,
43         };
44         float out_data[6];
45         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
46         tester.get_chain()->add_effect(new IdentityEffect());
47         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
48
49         expect_equal(data, out_data, 3, 2);
50 }
51
52 // An effect that does nothing, but requests texture bounce.
53 class BouncingIdentityEffect : public Effect {
54 public:
55         BouncingIdentityEffect() {}
56         virtual std::string effect_type_id() const { return "IdentityEffect"; }
57         std::string output_fragment_shader() { return read_file("identity.frag"); }
58         bool needs_texture_bounce() const { return true; }
59         AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
60 };
61
62 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
63         float data[] = {
64                 0.0f, 0.25f, 0.3f,
65                 0.75f, 1.0f, 1.0f,
66         };
67         float out_data[6];
68         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
69         tester.get_chain()->add_effect(new BouncingIdentityEffect());
70         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
71
72         expect_equal(data, out_data, 3, 2);
73 }
74
75 TEST(MirrorTest, BasicTest) {
76         float data[] = {
77                 0.0f, 0.25f, 0.3f,
78                 0.75f, 1.0f, 1.0f,
79         };
80         float expected_data[6] = {
81                 0.3f, 0.25f, 0.0f,
82                 1.0f, 1.0f, 0.75f,
83         };
84         float out_data[6];
85         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
86         tester.get_chain()->add_effect(new MirrorEffect());
87         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
88
89         expect_equal(expected_data, out_data, 3, 2);
90 }
91
92 // A dummy effect that inverts its input.
93 class InvertEffect : public Effect {
94 public:
95         InvertEffect() {}
96         virtual std::string effect_type_id() const { return "InvertEffect"; }
97         std::string output_fragment_shader() { return read_file("invert_effect.frag"); }
98
99         // A real invert would actually care about its alpha,
100         // but in this unit test, it only complicates things.
101         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
102 };
103
104 // Like IdentityEffect, but rewrites itself out of the loop,
105 // splicing in a different effect instead. Also stores the new node,
106 // so we later can check whatever properties we'd like about the graph.
107 template<class T>
108 class RewritingEffect : public Effect {
109 public:
110         RewritingEffect() : effect(new T()), replaced_node(NULL) {}
111         virtual std::string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
112         std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
113         virtual void rewrite_graph(EffectChain *graph, Node *self) {
114                 replaced_node = graph->add_node(effect);
115                 graph->replace_receiver(self, replaced_node);
116                 graph->replace_sender(self, replaced_node);
117                 self->disabled = true;
118         }
119
120         T *effect;
121         Node *replaced_node;
122 };
123
124 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
125         float data[] = {
126                 0.0f, 0.25f, 0.3f,
127                 0.75f, 1.0f, 1.0f,
128         };
129         float expected_data[6] = {
130                 1.0f, 0.9771f, 0.9673f,
131                 0.7192f, 0.0f, 0.0f,
132         };
133         float out_data[6];
134         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
135         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
136         tester.get_chain()->add_effect(effect);
137         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
138
139         Node *node = effect->replaced_node;
140         ASSERT_EQ(1, node->incoming_links.size());
141         ASSERT_EQ(1, node->outgoing_links.size());
142         EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
143         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
144
145         expect_equal(expected_data, out_data, 3, 2);
146 }
147
148 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
149         unsigned char data[] = {
150                 0, 64,
151                 128, 255,
152         };
153         float expected_data[4] = {
154                 1.0f, 0.9771f,
155                 0.8983f, 0.0f,
156         };
157         float out_data[4];
158         EffectChainTester tester(NULL, 2, 2);
159         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
160         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
161         tester.get_chain()->add_effect(effect);
162         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
163
164         Node *node = effect->replaced_node;
165         ASSERT_EQ(1, node->incoming_links.size());
166         ASSERT_EQ(1, node->outgoing_links.size());
167         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
168         EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
169
170         expect_equal(expected_data, out_data, 2, 2);
171 }
172
173 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
174         float data[] = {
175                 0.0f, 0.25f, 0.3f,
176                 0.75f, 1.0f, 1.0f,
177         };
178         float expected_data[6] = {
179                 1.0f, 0.75f, 0.7f,
180                 0.25f, 0.0f, 0.0f,
181         };
182         float out_data[6];
183         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
184         RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
185         tester.get_chain()->add_effect(effect);
186         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
187
188         Node *node = effect->replaced_node;
189         ASSERT_EQ(1, node->incoming_links.size());
190         ASSERT_EQ(1, node->outgoing_links.size());
191         EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
192         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
193
194         expect_equal(expected_data, out_data, 3, 2);
195 }
196
197 // A fake input that can change its output colorspace and gamma between instantiation
198 // and finalize.
199 class UnknownColorspaceInput : public FlatInput {
200 public:
201         UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
202             : FlatInput(format, pixel_format, type, width, height),
203               overridden_color_space(format.color_space),
204               overridden_gamma_curve(format.gamma_curve) {}
205         virtual std::string effect_type_id() const { return "UnknownColorspaceInput"; }
206
207         void set_color_space(Colorspace colorspace) {
208                 overridden_color_space = colorspace;
209         }
210         void set_gamma_curve(GammaCurve gamma_curve) {
211                 overridden_gamma_curve = gamma_curve;
212         }
213         Colorspace get_color_space() const { return overridden_color_space; }
214         GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
215
216 private:
217         Colorspace overridden_color_space;
218         GammaCurve overridden_gamma_curve;
219 };
220
221 TEST(EffectChainTester, HandlesInputChangingColorspace) {
222         const int size = 4;
223
224         float data[size] = {
225                 0.0,
226                 0.5,
227                 0.7,
228                 1.0,
229         };
230         float out_data[size];
231
232         EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
233
234         // First say that we have sRGB, linear input.
235         ImageFormat format;
236         format.color_space = COLORSPACE_sRGB;
237         format.gamma_curve = GAMMA_LINEAR;
238
239         UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
240         input->set_pixel_data(data);
241         tester.get_chain()->add_input(input);
242
243         // Now we change to Rec. 601 input.
244         input->set_color_space(COLORSPACE_REC_601_625);
245         input->set_gamma_curve(GAMMA_REC_601);
246
247         // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
248         tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
249         expect_equal(data, out_data, 4, 1);
250 }
251
252 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
253         float data[] = {
254                 0.0f, 0.25f, 0.3f,
255                 0.75f, 1.0f, 1.0f,
256         };
257         float expected_data[6] = {
258                 0.3f, 0.25f, 0.0f,
259                 1.0f, 1.0f, 0.75f,
260         };
261         float out_data[6];
262         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
263         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
264         tester.get_chain()->add_effect(effect);
265         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
266
267         Node *node = effect->replaced_node;
268         ASSERT_EQ(1, node->incoming_links.size());
269         EXPECT_EQ(0, node->outgoing_links.size());
270         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
271
272         expect_equal(expected_data, out_data, 3, 2);
273 }
274
275 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
276         float data[] = {
277                 0.0f, 0.25f, 0.3f,
278                 0.75f, 1.0f, 1.0f,
279         };
280         float expected_data[6] = {
281                 0.3f, 0.25f, 0.0f,
282                 1.0f, 1.0f, 0.75f,
283         };
284         float out_data[6];
285         EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
286         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
287         tester.get_chain()->add_effect(effect);
288         tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
289
290         Node *node = effect->replaced_node;
291         ASSERT_EQ(1, node->incoming_links.size());
292         EXPECT_EQ(0, node->outgoing_links.size());
293         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
294
295         expect_equal(expected_data, out_data, 3, 2);
296 }
297
298 // The identity effect needs linear light, and thus will get conversions on both sides.
299 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
300 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
301         float data[256];
302         for (unsigned i = 0; i < 256; ++i) {
303                 data[i] = i / 255.0;
304         };
305         float out_data[256];
306         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
307         tester.get_chain()->add_effect(new IdentityEffect());
308         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
309
310         expect_equal(data, out_data, 256, 1);
311 }
312
313 // Same, but uses the forward sRGB table from the GPU.
314 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
315         unsigned char data[256];
316         float expected_data[256];
317         for (unsigned i = 0; i < 256; ++i) {
318                 data[i] = i;
319                 expected_data[i] = i / 255.0;
320         };
321         float out_data[256];
322         EffectChainTester tester(NULL, 256, 1);
323         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
324         tester.get_chain()->add_effect(new IdentityEffect());
325         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
326
327         expect_equal(expected_data, out_data, 256, 1);
328 }
329
330 // Same, for the Rec. 601/709 gamma curve.
331 TEST(EffectChainTest, IdentityThroughRec709) {
332         float data[256];
333         for (unsigned i = 0; i < 256; ++i) {
334                 data[i] = i / 255.0;
335         };
336         float out_data[256];
337         EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
338         tester.get_chain()->add_effect(new IdentityEffect());
339         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
340
341         expect_equal(data, out_data, 256, 1);
342 }
343
344 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
345 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
346         const int size = 3;
347         float data[4 * size] = {
348                 0.8f, 0.0f, 0.0f, 0.5f,
349                 0.0f, 0.2f, 0.2f, 0.3f,
350                 0.1f, 0.0f, 1.0f, 1.0f,
351         };
352         float out_data[4 * size];
353         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
354         tester.get_chain()->add_effect(new IdentityEffect());
355         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
356
357         expect_equal(data, out_data, 4, size);
358 }
359
360 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
361         const int size = 3;
362         float data[4 * size] = {
363                 0.8f, 0.0f, 0.0f, 0.5f,
364                 0.0f, 0.2f, 0.2f, 0.3f,
365                 0.1f, 0.0f, 1.0f, 1.0f,
366         };
367         float expected_data[4 * size] = {
368                 0.1f, 0.0f, 1.0f, 1.0f,
369                 0.0f, 0.2f, 0.2f, 0.3f,
370                 0.8f, 0.0f, 0.0f, 0.5f,
371         };
372         float out_data[4 * size];
373         EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
374         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
375         tester.get_chain()->add_effect(effect);
376         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
377
378         Node *node = effect->replaced_node;
379         ASSERT_EQ(1, node->incoming_links.size());
380         EXPECT_EQ(0, node->outgoing_links.size());
381         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
382
383         expect_equal(expected_data, out_data, 4, size);
384 }
385
386 // An input that outputs only blue, which has blank alpha.
387 class BlueInput : public Input {
388 public:
389         BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
390         virtual std::string effect_type_id() const { return "IdentityEffect"; }
391         std::string output_fragment_shader() { return read_file("blue.frag"); }
392         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
393         virtual void finalize() {}
394         virtual bool can_output_linear_gamma() const { return true; }
395         virtual unsigned get_width() const { return 1; }
396         virtual unsigned get_height() const { return 1; }
397         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
398         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
399
400 private:
401         int needs_mipmaps;
402 };
403
404 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
405 // which outputs blank alpha.
406 class RewritingToBlueInput : public Input {
407 public:
408         RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
409         virtual std::string effect_type_id() const { return "RewritingToBlueInput"; }
410         std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
411         virtual void rewrite_graph(EffectChain *graph, Node *self) {
412                 Node *blue_node = graph->add_node(new BlueInput());
413                 graph->replace_receiver(self, blue_node);
414                 graph->replace_sender(self, blue_node);
415
416                 self->disabled = true;
417                 this->blue_node = blue_node;
418         }
419
420         // Dummy values that we need to implement because we inherit from Input.
421         // Same as BlueInput.
422         virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
423         virtual void finalize() {}
424         virtual bool can_output_linear_gamma() const { return true; }
425         virtual unsigned get_width() const { return 1; }
426         virtual unsigned get_height() const { return 1; }
427         virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
428         virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
429
430         Node *blue_node;
431
432 private:
433         int needs_mipmaps;
434 };
435
436 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
437         const int size = 3;
438         float data[4 * size] = {
439                 0.0f, 0.0f, 1.0f, 1.0f,
440                 0.0f, 0.0f, 1.0f, 1.0f,
441                 0.0f, 0.0f, 1.0f, 1.0f,
442         };
443         float out_data[4 * size];
444         EffectChainTester tester(NULL, size, 1);
445         RewritingToBlueInput *input = new RewritingToBlueInput();
446         tester.get_chain()->add_input(input);
447         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
448
449         Node *node = input->blue_node;
450         EXPECT_EQ(0, node->incoming_links.size());
451         EXPECT_EQ(0, node->outgoing_links.size());
452
453         expect_equal(data, out_data, 4, size);
454 }
455
456 // An effect that does nothing, and specifies that it preserves blank alpha.
457 class BlankAlphaPreservingEffect : public Effect {
458 public:
459         BlankAlphaPreservingEffect() {}
460         virtual std::string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
461         std::string output_fragment_shader() { return read_file("identity.frag"); }
462         virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
463 };
464
465 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
466         const int size = 3;
467         float data[4 * size] = {
468                 0.0f, 0.0f, 1.0f, 1.0f,
469                 0.0f, 0.0f, 1.0f, 1.0f,
470                 0.0f, 0.0f, 1.0f, 1.0f,
471         };
472         float out_data[4 * size];
473         EffectChainTester tester(NULL, size, 1);
474         tester.get_chain()->add_input(new BlueInput());
475         tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
476         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
477         tester.get_chain()->add_effect(effect);
478         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
479
480         Node *node = effect->replaced_node;
481         EXPECT_EQ(1, node->incoming_links.size());
482         EXPECT_EQ(0, node->outgoing_links.size());
483
484         expect_equal(data, out_data, 4, size);
485 }
486
487 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
488 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
489 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
490 // with other tests.)
491 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
492         const int size = 3;
493         float data[4 * size] = {
494                 0.0f, 0.0f, 1.0f, 1.0f,
495                 0.0f, 0.0f, 1.0f, 1.0f,
496                 0.0f, 0.0f, 1.0f, 1.0f,
497         };
498         float out_data[4 * size];
499         EffectChainTester tester(NULL, size, 1);
500         tester.get_chain()->add_input(new BlueInput());
501         tester.get_chain()->add_effect(new IdentityEffect());  // Not BlankAlphaPreservingEffect.
502         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
503         tester.get_chain()->add_effect(effect);
504         tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
505
506         Node *node = effect->replaced_node;
507         EXPECT_EQ(1, node->incoming_links.size());
508         EXPECT_EQ(1, node->outgoing_links.size());
509         EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
510
511         expect_equal(data, out_data, 4, size);
512 }
513
514 // Effectively scales down its input linearly by 4x (and repeating it),
515 // which is not attainable without mipmaps.
516 class MipmapNeedingEffect : public Effect {
517 public:
518         MipmapNeedingEffect() {}
519         virtual bool needs_mipmaps() const { return true; }
520         virtual std::string effect_type_id() const { return "MipmapNeedingEffect"; }
521         std::string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
522         void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num)
523         {
524                 glActiveTexture(GL_TEXTURE0);
525                 check_error();
526                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
527                 check_error();
528                 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
529                 check_error();
530         }
531 };
532
533 TEST(EffectChainTest, MipmapGenerationWorks) {
534         float data[] = {  // In 4x4 blocks.
535                 1.0f, 0.0f, 0.0f, 0.0f,
536                 0.0f, 0.0f, 0.0f, 0.0f,
537                 0.0f, 0.0f, 0.0f, 0.0f,
538                 0.0f, 0.0f, 0.0f, 1.0f,
539
540                 0.0f, 0.0f, 0.0f, 0.0f,
541                 0.0f, 0.5f, 0.0f, 0.0f,
542                 0.0f, 0.0f, 1.0f, 0.0f,
543                 0.0f, 0.0f, 0.0f, 0.0f,
544
545                 1.0f, 1.0f, 1.0f, 1.0f,
546                 1.0f, 1.0f, 1.0f, 1.0f,
547                 1.0f, 1.0f, 1.0f, 1.0f,
548                 1.0f, 1.0f, 1.0f, 1.0f,
549
550                 0.0f, 0.0f, 0.0f, 0.0f,
551                 0.0f, 1.0f, 1.0f, 0.0f,
552                 0.0f, 1.0f, 1.0f, 0.0f,
553                 0.0f, 0.0f, 0.0f, 0.0f,
554         };
555         float expected_data[] = {  // Repeated four times each way.
556                 0.125f,   0.125f,   0.125f,   0.125f,
557                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
558                 1.0f,     1.0f,     1.0f,     1.0f,
559                 0.25f,    0.25f,    0.25f,    0.25f,
560
561                 0.125f,   0.125f,   0.125f,   0.125f,
562                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
563                 1.0f,     1.0f,     1.0f,     1.0f,
564                 0.25f,    0.25f,    0.25f,    0.25f,
565
566                 0.125f,   0.125f,   0.125f,   0.125f,
567                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
568                 1.0f,     1.0f,     1.0f,     1.0f,
569                 0.25f,    0.25f,    0.25f,    0.25f,
570
571                 0.125f,   0.125f,   0.125f,   0.125f,
572                 0.09375f, 0.09375f, 0.09375f, 0.09375f,
573                 1.0f,     1.0f,     1.0f,     1.0f,
574                 0.25f,    0.25f,    0.25f,    0.25f,
575         };
576         float out_data[4 * 16];
577         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
578         tester.get_chain()->add_effect(new MipmapNeedingEffect());
579         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
580
581         expect_equal(expected_data, out_data, 4, 16);
582 }
583
584 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
585         float data[] = {  // In 4x4 blocks.
586                 1.0f, 0.0f, 0.0f, 0.0f,
587                 0.0f, 0.0f, 0.0f, 0.0f,
588                 0.0f, 0.0f, 0.0f, 0.0f,
589                 0.0f, 0.0f, 0.0f, 1.0f,
590
591                 0.0f, 0.0f, 0.0f, 0.0f,
592                 0.0f, 0.5f, 0.0f, 0.0f,
593                 0.0f, 0.0f, 1.0f, 0.0f,
594                 0.0f, 0.0f, 0.0f, 0.0f,
595
596                 1.0f, 1.0f, 1.0f, 1.0f,
597                 1.0f, 1.0f, 1.0f, 1.0f,
598                 1.0f, 1.0f, 1.0f, 1.0f,
599                 1.0f, 1.0f, 1.0f, 1.0f,
600
601                 0.0f, 0.0f, 0.0f, 0.0f,
602                 0.0f, 1.0f, 1.0f, 0.0f,
603                 0.0f, 1.0f, 1.0f, 0.0f,
604                 0.0f, 0.0f, 0.0f, 0.0f,
605         };
606         float expected_data[] = {  // Repeated four times horizontaly, interpolated vertically.
607                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
608                 0.1250f, 0.1250f, 0.1250f, 0.1250f,
609                 0.1211f, 0.1211f, 0.1211f, 0.1211f,
610                 0.1133f, 0.1133f, 0.1133f, 0.1133f,
611                 0.1055f, 0.1055f, 0.1055f, 0.1055f,
612                 0.0977f, 0.0977f, 0.0977f, 0.0977f,
613                 0.2070f, 0.2070f, 0.2070f, 0.2070f,
614                 0.4336f, 0.4336f, 0.4336f, 0.4336f,
615                 0.6602f, 0.6602f, 0.6602f, 0.6602f,
616                 0.8867f, 0.8867f, 0.8867f, 0.8867f,
617                 0.9062f, 0.9062f, 0.9062f, 0.9062f,
618                 0.7188f, 0.7188f, 0.7188f, 0.7188f,
619                 0.5312f, 0.5312f, 0.5312f, 0.5312f,
620                 0.3438f, 0.3438f, 0.3438f, 0.3438f,
621                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
622                 0.2500f, 0.2500f, 0.2500f, 0.2500f,
623         };
624         float out_data[4 * 16];
625
626         ResizeEffect *downscale = new ResizeEffect();
627         ASSERT_TRUE(downscale->set_int("width", 1));
628         ASSERT_TRUE(downscale->set_int("height", 4));
629
630         ResizeEffect *upscale = new ResizeEffect();
631         ASSERT_TRUE(upscale->set_int("width", 4));
632         ASSERT_TRUE(upscale->set_int("height", 16));
633
634         EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
635         tester.get_chain()->add_effect(downscale);
636         tester.get_chain()->add_effect(upscale);
637         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
638
639         expect_equal(expected_data, out_data, 4, 16);
640 }
641
642 // An effect that multiplies with a constant. Used below.
643 class MultiplyEffect : public Effect {
644 public:
645         MultiplyEffect() { register_float("factor", &factor); }
646         virtual std::string effect_type_id() const { return "MultiplyEffect"; }
647         std::string output_fragment_shader() { return read_file("multiply.frag"); }
648         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
649
650 private:
651         float factor;
652 };
653
654 // An effect that adds its two inputs together. Used below.
655 class AddEffect : public Effect {
656 public:
657         AddEffect() {}
658         virtual std::string effect_type_id() const { return "AddEffect"; }
659         std::string output_fragment_shader() { return read_file("add.frag"); }
660         virtual unsigned num_inputs() const { return 2; }
661         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
662 };
663
664 // Constructs the graph
665 //
666 //             FlatInput               |
667 //            /         \              |
668 //  MultiplyEffect  MultiplyEffect     |
669 //            \         /              |
670 //             AddEffect               |
671 //
672 // and verifies that it gives the correct output.
673 TEST(EffectChainTest, DiamondGraph) {
674         float data[] = {
675                 1.0f, 1.0f,
676                 1.0f, 0.0f,
677         };
678         float expected_data[] = {
679                 2.5f, 2.5f,
680                 2.5f, 0.0f,
681         };
682         float out_data[2 * 2];
683
684         MultiplyEffect *mul_half = new MultiplyEffect();
685         ASSERT_TRUE(mul_half->set_float("factor", 0.5f));
686         
687         MultiplyEffect *mul_two = new MultiplyEffect();
688         ASSERT_TRUE(mul_two->set_float("factor", 2.0f));
689
690         EffectChainTester tester(NULL, 2, 2);
691
692         ImageFormat format;
693         format.color_space = COLORSPACE_sRGB;
694         format.gamma_curve = GAMMA_LINEAR;
695
696         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
697         input->set_pixel_data(data);
698
699         tester.get_chain()->add_input(input);
700         tester.get_chain()->add_effect(mul_half, input);
701         tester.get_chain()->add_effect(mul_two, input);
702         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
703         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
704
705         expect_equal(expected_data, out_data, 2, 2);
706 }
707
708 // Constructs the graph
709 //
710 //             FlatInput                     |
711 //            /         \                    |
712 //  MultiplyEffect  MultiplyEffect           |
713 //         \             |                   |
714 //          \    BouncingIdentityEffect      |  
715 //            \         /                    |
716 //             AddEffect                     |
717 //
718 // and verifies that it gives the correct output.
719 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
720         float data[] = {
721                 1.0f, 1.0f,
722                 1.0f, 0.0f,
723         };
724         float expected_data[] = {
725                 2.5f, 2.5f,
726                 2.5f, 0.0f,
727         };
728         float out_data[2 * 2];
729
730         MultiplyEffect *mul_half = new MultiplyEffect();
731         ASSERT_TRUE(mul_half->set_float("factor", 0.5f));
732         
733         MultiplyEffect *mul_two = new MultiplyEffect();
734         ASSERT_TRUE(mul_two->set_float("factor", 2.0f));
735         
736         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
737
738         EffectChainTester tester(NULL, 2, 2);
739
740         ImageFormat format;
741         format.color_space = COLORSPACE_sRGB;
742         format.gamma_curve = GAMMA_LINEAR;
743
744         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
745         input->set_pixel_data(data);
746
747         tester.get_chain()->add_input(input);
748         tester.get_chain()->add_effect(mul_half, input);
749         tester.get_chain()->add_effect(mul_two, input);
750         tester.get_chain()->add_effect(bounce, mul_two);
751         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
752         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
753
754         expect_equal(expected_data, out_data, 2, 2);
755 }
756
757 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
758         float data[] = {
759                 0.735f, 0.0f,
760                 0.735f, 0.0f,
761         };
762         float expected_data[] = {
763                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
764                 0.0f, 0.5f,
765         };
766         float out_data[2 * 2];
767         
768         EffectChainTester tester(NULL, 2, 2);
769         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
770
771         // MirrorEffect does not get linear light, so the conversions will be
772         // inserted after it, not before.
773         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
774         tester.get_chain()->add_effect(effect);
775
776         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
777         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
778         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
779         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
780
781         expect_equal(expected_data, out_data, 2, 2);
782
783         Node *node = effect->replaced_node;
784         ASSERT_EQ(1, node->incoming_links.size());
785         ASSERT_EQ(1, node->outgoing_links.size());
786         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
787         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
788 }
789
790 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
791         float data[] = {
792                 0.5f, 0.0f,
793                 0.5f, 0.0f,
794         };
795         float expected_data[] = {
796                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
797                 0.0f, 0.5f,
798         };
799         float out_data[2 * 2];
800         
801         EffectChainTester tester(NULL, 2, 2);
802         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
803
804         // MirrorEffect does not get linear light, so the conversions will be
805         // inserted after it, not before.
806         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
807         tester.get_chain()->add_effect(effect);
808
809         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
810         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
811         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
812         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
813
814         expect_equal(expected_data, out_data, 2, 2);
815
816         Node *node = effect->replaced_node;
817         ASSERT_EQ(1, node->incoming_links.size());
818         ASSERT_EQ(1, node->outgoing_links.size());
819         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
820         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
821 }
822
823 // An effect that does nothing, but requests texture bounce and stores
824 // its input size.
825 class SizeStoringEffect : public BouncingIdentityEffect {
826 public:
827         SizeStoringEffect() : input_width(-1), input_height(-1) {}
828         virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
829                 assert(input_num == 0);
830                 input_width = width;
831                 input_height = height;
832         }
833         virtual std::string effect_type_id() const { return "SizeStoringEffect"; }
834
835         int input_width, input_height;
836 };
837
838 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
839         float data[2 * 2] = {
840                 0.0f, 0.0f,
841                 0.0f, 0.0f,
842         };
843         float out_data[2 * 2];
844         
845         EffectChainTester tester(NULL, 4, 3);  // Note non-square aspect.
846
847         ImageFormat format;
848         format.color_space = COLORSPACE_sRGB;
849         format.gamma_curve = GAMMA_LINEAR;
850
851         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
852         input1->set_pixel_data(data);
853         
854         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
855         input2->set_pixel_data(data);
856
857         SizeStoringEffect *input_store = new SizeStoringEffect();
858
859         tester.get_chain()->add_input(input1);
860         tester.get_chain()->add_input(input2);
861         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
862         tester.get_chain()->add_effect(input_store);
863         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
864
865         EXPECT_EQ(2, input_store->input_width);
866         EXPECT_EQ(2, input_store->input_height);
867 }
868
869 TEST(EffectChainTest, AspectRatioConversion) {
870         float data1[4 * 3] = {
871                 0.0f, 0.0f, 0.0f, 0.0f,
872                 0.0f, 0.0f, 0.0f, 0.0f,
873                 0.0f, 0.0f, 0.0f, 0.0f,
874         };
875         float data2[7 * 7] = {
876                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
877                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
878                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
879                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
880                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
881                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
882                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
883         };
884
885         // The right conversion here is that the 7x7 image decides the size,
886         // since it is the biggest, so everything is scaled up to 9x7
887         // (keep the height, round the width 9.333 to 9). 
888         float out_data[9 * 7];
889         
890         EffectChainTester tester(NULL, 4, 3);
891
892         ImageFormat format;
893         format.color_space = COLORSPACE_sRGB;
894         format.gamma_curve = GAMMA_LINEAR;
895
896         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
897         input1->set_pixel_data(data1);
898         
899         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
900         input2->set_pixel_data(data2);
901
902         SizeStoringEffect *input_store = new SizeStoringEffect();
903
904         tester.get_chain()->add_input(input1);
905         tester.get_chain()->add_input(input2);
906         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
907         tester.get_chain()->add_effect(input_store);
908         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
909
910         EXPECT_EQ(9, input_store->input_width);
911         EXPECT_EQ(7, input_store->input_height);
912 }
913
914 // An effect that does nothing except changing its output sizes.
915 class VirtualResizeEffect : public Effect {
916 public:
917         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
918                 : width(width),
919                   height(height),
920                   virtual_width(virtual_width),
921                   virtual_height(virtual_height) {}
922         virtual std::string effect_type_id() const { return "VirtualResizeEffect"; }
923         std::string output_fragment_shader() { return read_file("identity.frag"); }
924
925         virtual bool changes_output_size() const { return true; }
926
927         virtual void get_output_size(unsigned *width, unsigned *height,
928                                      unsigned *virtual_width, unsigned *virtual_height) const {
929                 *width = this->width;
930                 *height = this->height;
931                 *virtual_width = this->virtual_width;
932                 *virtual_height = this->virtual_height;
933         }
934
935 private:
936         int width, height, virtual_width, virtual_height;
937 };
938
939 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
940         const int size = 2, bigger_size = 3;
941         float data[size * size] = {
942                 1.0f, 0.0f,
943                 0.0f, 1.0f,
944         };
945         float out_data[size * size];
946         
947         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
948
949         SizeStoringEffect *size_store = new SizeStoringEffect();
950
951         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
952         tester.get_chain()->add_effect(size_store);
953         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
954         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
955
956         EXPECT_EQ(bigger_size, size_store->input_width);
957         EXPECT_EQ(bigger_size, size_store->input_height);
958
959         // If the resize is implemented as non-virtual, we'll fail here,
960         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
961         expect_equal(data, out_data, size, size);
962 }