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