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