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