]> git.sesse.net Git - movit/blob - effect_chain_test.cpp
Comment and README updates about Rec. 2020 in light of the accuracy test results.
[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
8 #include "effect.h"
9 #include "effect_chain.h"
10 #include "flat_input.h"
11 #include "gtest/gtest.h"
12 #include "input.h"
13 #include "mirror_effect.h"
14 #include "multiply_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 adds its two inputs together. Used below.
643 class AddEffect : public Effect {
644 public:
645         AddEffect() {}
646         virtual std::string effect_type_id() const { return "AddEffect"; }
647         std::string output_fragment_shader() { return read_file("add.frag"); }
648         virtual unsigned num_inputs() const { return 2; }
649         virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
650 };
651
652 // Constructs the graph
653 //
654 //             FlatInput               |
655 //            /         \              |
656 //  MultiplyEffect  MultiplyEffect     |
657 //            \         /              |
658 //             AddEffect               |
659 //
660 // and verifies that it gives the correct output.
661 TEST(EffectChainTest, DiamondGraph) {
662         float data[] = {
663                 1.0f, 1.0f,
664                 1.0f, 0.0f,
665         };
666         float expected_data[] = {
667                 2.5f, 2.5f,
668                 2.5f, 0.0f,
669         };
670         float out_data[2 * 2];
671
672         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
673         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
674
675         MultiplyEffect *mul_half = new MultiplyEffect();
676         ASSERT_TRUE(mul_half->set_vec4("factor", half));
677         
678         MultiplyEffect *mul_two = new MultiplyEffect();
679         ASSERT_TRUE(mul_two->set_vec4("factor", two));
680
681         EffectChainTester tester(NULL, 2, 2);
682
683         ImageFormat format;
684         format.color_space = COLORSPACE_sRGB;
685         format.gamma_curve = GAMMA_LINEAR;
686
687         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
688         input->set_pixel_data(data);
689
690         tester.get_chain()->add_input(input);
691         tester.get_chain()->add_effect(mul_half, input);
692         tester.get_chain()->add_effect(mul_two, input);
693         tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
694         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
695
696         expect_equal(expected_data, out_data, 2, 2);
697 }
698
699 // Constructs the graph
700 //
701 //             FlatInput                     |
702 //            /         \                    |
703 //  MultiplyEffect  MultiplyEffect           |
704 //         \             |                   |
705 //          \    BouncingIdentityEffect      |  
706 //            \         /                    |
707 //             AddEffect                     |
708 //
709 // and verifies that it gives the correct output.
710 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
711         float data[] = {
712                 1.0f, 1.0f,
713                 1.0f, 0.0f,
714         };
715         float expected_data[] = {
716                 2.5f, 2.5f,
717                 2.5f, 0.0f,
718         };
719         float out_data[2 * 2];
720
721         const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
722         const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
723
724         MultiplyEffect *mul_half = new MultiplyEffect();
725         ASSERT_TRUE(mul_half->set_vec4("factor", half));
726         
727         MultiplyEffect *mul_two = new MultiplyEffect();
728         ASSERT_TRUE(mul_two->set_vec4("factor", two));
729         
730         BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
731
732         EffectChainTester tester(NULL, 2, 2);
733
734         ImageFormat format;
735         format.color_space = COLORSPACE_sRGB;
736         format.gamma_curve = GAMMA_LINEAR;
737
738         FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
739         input->set_pixel_data(data);
740
741         tester.get_chain()->add_input(input);
742         tester.get_chain()->add_effect(mul_half, input);
743         tester.get_chain()->add_effect(mul_two, input);
744         tester.get_chain()->add_effect(bounce, mul_two);
745         tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
746         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
747
748         expect_equal(expected_data, out_data, 2, 2);
749 }
750
751 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
752         float data[] = {
753                 0.735f, 0.0f,
754                 0.735f, 0.0f,
755         };
756         float expected_data[] = {
757                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
758                 0.0f, 0.5f,
759         };
760         float out_data[2 * 2];
761         
762         EffectChainTester tester(NULL, 2, 2);
763         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
764
765         // MirrorEffect does not get linear light, so the conversions will be
766         // inserted after it, not before.
767         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
768         tester.get_chain()->add_effect(effect);
769
770         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
771         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
772         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
773         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
774
775         expect_equal(expected_data, out_data, 2, 2);
776
777         Node *node = effect->replaced_node;
778         ASSERT_EQ(1, node->incoming_links.size());
779         ASSERT_EQ(1, node->outgoing_links.size());
780         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
781         EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
782 }
783
784 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
785         float data[] = {
786                 0.5f, 0.0f,
787                 0.5f, 0.0f,
788         };
789         float expected_data[] = {
790                 0.0f, 0.5f,  // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
791                 0.0f, 0.5f,
792         };
793         float out_data[2 * 2];
794         
795         EffectChainTester tester(NULL, 2, 2);
796         tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
797
798         // MirrorEffect does not get linear light, so the conversions will be
799         // inserted after it, not before.
800         RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
801         tester.get_chain()->add_effect(effect);
802
803         Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
804         Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
805         tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
806         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
807
808         expect_equal(expected_data, out_data, 2, 2);
809
810         Node *node = effect->replaced_node;
811         ASSERT_EQ(1, node->incoming_links.size());
812         ASSERT_EQ(1, node->outgoing_links.size());
813         EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
814         EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
815 }
816
817 // An effect that does nothing, but requests texture bounce and stores
818 // its input size.
819 class SizeStoringEffect : public BouncingIdentityEffect {
820 public:
821         SizeStoringEffect() : input_width(-1), input_height(-1) {}
822         virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
823                 assert(input_num == 0);
824                 input_width = width;
825                 input_height = height;
826         }
827         virtual std::string effect_type_id() const { return "SizeStoringEffect"; }
828
829         int input_width, input_height;
830 };
831
832 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
833         float data[2 * 2] = {
834                 0.0f, 0.0f,
835                 0.0f, 0.0f,
836         };
837         float out_data[2 * 2];
838         
839         EffectChainTester tester(NULL, 4, 3);  // Note non-square aspect.
840
841         ImageFormat format;
842         format.color_space = COLORSPACE_sRGB;
843         format.gamma_curve = GAMMA_LINEAR;
844
845         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
846         input1->set_pixel_data(data);
847         
848         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
849         input2->set_pixel_data(data);
850
851         SizeStoringEffect *input_store = new SizeStoringEffect();
852
853         tester.get_chain()->add_input(input1);
854         tester.get_chain()->add_input(input2);
855         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
856         tester.get_chain()->add_effect(input_store);
857         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
858
859         EXPECT_EQ(2, input_store->input_width);
860         EXPECT_EQ(2, input_store->input_height);
861 }
862
863 TEST(EffectChainTest, AspectRatioConversion) {
864         float data1[4 * 3] = {
865                 0.0f, 0.0f, 0.0f, 0.0f,
866                 0.0f, 0.0f, 0.0f, 0.0f,
867                 0.0f, 0.0f, 0.0f, 0.0f,
868         };
869         float data2[7 * 7] = {
870                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
871                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
872                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
873                 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
874                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
875                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
876                 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
877         };
878
879         // The right conversion here is that the 7x7 image decides the size,
880         // since it is the biggest, so everything is scaled up to 9x7
881         // (keep the height, round the width 9.333 to 9). 
882         float out_data[9 * 7];
883         
884         EffectChainTester tester(NULL, 4, 3);
885
886         ImageFormat format;
887         format.color_space = COLORSPACE_sRGB;
888         format.gamma_curve = GAMMA_LINEAR;
889
890         FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
891         input1->set_pixel_data(data1);
892         
893         FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
894         input2->set_pixel_data(data2);
895
896         SizeStoringEffect *input_store = new SizeStoringEffect();
897
898         tester.get_chain()->add_input(input1);
899         tester.get_chain()->add_input(input2);
900         tester.get_chain()->add_effect(new AddEffect(), input1, input2);
901         tester.get_chain()->add_effect(input_store);
902         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
903
904         EXPECT_EQ(9, input_store->input_width);
905         EXPECT_EQ(7, input_store->input_height);
906 }
907
908 // An effect that does nothing except changing its output sizes.
909 class VirtualResizeEffect : public Effect {
910 public:
911         VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
912                 : width(width),
913                   height(height),
914                   virtual_width(virtual_width),
915                   virtual_height(virtual_height) {}
916         virtual std::string effect_type_id() const { return "VirtualResizeEffect"; }
917         std::string output_fragment_shader() { return read_file("identity.frag"); }
918
919         virtual bool changes_output_size() const { return true; }
920
921         virtual void get_output_size(unsigned *width, unsigned *height,
922                                      unsigned *virtual_width, unsigned *virtual_height) const {
923                 *width = this->width;
924                 *height = this->height;
925                 *virtual_width = this->virtual_width;
926                 *virtual_height = this->virtual_height;
927         }
928
929 private:
930         int width, height, virtual_width, virtual_height;
931 };
932
933 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
934         const int size = 2, bigger_size = 3;
935         float data[size * size] = {
936                 1.0f, 0.0f,
937                 0.0f, 1.0f,
938         };
939         float out_data[size * size];
940         
941         EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
942
943         SizeStoringEffect *size_store = new SizeStoringEffect();
944
945         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
946         tester.get_chain()->add_effect(size_store);
947         tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
948         tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
949
950         EXPECT_EQ(bigger_size, size_store->input_width);
951         EXPECT_EQ(bigger_size, size_store->input_height);
952
953         // If the resize is implemented as non-virtual, we'll fail here,
954         // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
955         expect_equal(data, out_data, size, size);
956 }