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