1 // Unit tests for EffectChain.
3 // Note that this also contains the tests for some of the simpler effects.
9 #include "effect_chain.h"
10 #include "flat_input.h"
11 #include "gtest/gtest.h"
14 #include "mirror_effect.h"
15 #include "multiply_effect.h"
16 #include "resize_effect.h"
17 #include "test_util.h"
22 TEST(EffectChainTest, EmptyChain) {
28 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
29 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
31 expect_equal(data, out_data, 3, 2);
34 // An effect that does nothing.
35 class IdentityEffect : public Effect {
38 virtual string effect_type_id() const { return "IdentityEffect"; }
39 string output_fragment_shader() { return read_file("identity.frag"); }
42 TEST(EffectChainTest, Identity) {
48 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
49 tester.get_chain()->add_effect(new IdentityEffect());
50 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
52 expect_equal(data, out_data, 3, 2);
55 // An effect that does nothing, but requests texture bounce.
56 class BouncingIdentityEffect : public Effect {
58 BouncingIdentityEffect() {}
59 virtual string effect_type_id() const { return "IdentityEffect"; }
60 string output_fragment_shader() { return read_file("identity.frag"); }
61 bool needs_texture_bounce() const { return true; }
62 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
65 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
71 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
72 tester.get_chain()->add_effect(new BouncingIdentityEffect());
73 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
75 expect_equal(data, out_data, 3, 2);
78 TEST(MirrorTest, BasicTest) {
83 float expected_data[6] = {
88 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
89 tester.get_chain()->add_effect(new MirrorEffect());
90 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
92 expect_equal(expected_data, out_data, 3, 2);
95 // A dummy effect that inverts its input.
96 class InvertEffect : public Effect {
99 virtual string effect_type_id() const { return "InvertEffect"; }
100 string output_fragment_shader() { return read_file("invert_effect.frag"); }
102 // A real invert would actually care about its alpha,
103 // but in this unit test, it only complicates things.
104 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
107 // Like IdentityEffect, but rewrites itself out of the loop,
108 // splicing in a different effect instead. Also stores the new node,
109 // so we later can check whatever properties we'd like about the graph.
111 class RewritingEffect : public Effect {
113 RewritingEffect() : effect(new T()), replaced_node(NULL) {}
114 virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
115 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
116 virtual void rewrite_graph(EffectChain *graph, Node *self) {
117 replaced_node = graph->add_node(effect);
118 graph->replace_receiver(self, replaced_node);
119 graph->replace_sender(self, replaced_node);
120 self->disabled = true;
127 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
132 float expected_data[6] = {
133 1.0f, 0.9771f, 0.9673f,
137 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
138 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
139 tester.get_chain()->add_effect(effect);
140 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
142 Node *node = effect->replaced_node;
143 ASSERT_EQ(1, node->incoming_links.size());
144 ASSERT_EQ(1, node->outgoing_links.size());
145 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
146 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
148 expect_equal(expected_data, out_data, 3, 2);
151 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
152 unsigned char data[] = {
156 float expected_data[4] = {
161 EffectChainTester tester(NULL, 2, 2);
162 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
163 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
164 tester.get_chain()->add_effect(effect);
165 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
167 Node *node = effect->replaced_node;
168 ASSERT_EQ(1, node->incoming_links.size());
169 ASSERT_EQ(1, node->outgoing_links.size());
170 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
171 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
173 expect_equal(expected_data, out_data, 2, 2);
176 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
181 float expected_data[6] = {
186 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
187 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
188 tester.get_chain()->add_effect(effect);
189 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
191 Node *node = effect->replaced_node;
192 ASSERT_EQ(1, node->incoming_links.size());
193 ASSERT_EQ(1, node->outgoing_links.size());
194 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
195 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
197 expect_equal(expected_data, out_data, 3, 2);
200 // A fake input that can change its output colorspace and gamma between instantiation
202 class UnknownColorspaceInput : public FlatInput {
204 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
205 : FlatInput(format, pixel_format, type, width, height),
206 overridden_color_space(format.color_space),
207 overridden_gamma_curve(format.gamma_curve) {}
208 virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
210 void set_color_space(Colorspace colorspace) {
211 overridden_color_space = colorspace;
213 void set_gamma_curve(GammaCurve gamma_curve) {
214 overridden_gamma_curve = gamma_curve;
216 Colorspace get_color_space() const { return overridden_color_space; }
217 GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
220 Colorspace overridden_color_space;
221 GammaCurve overridden_gamma_curve;
224 TEST(EffectChainTester, HandlesInputChangingColorspace) {
233 float out_data[size];
235 EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
237 // First say that we have sRGB, linear input.
239 format.color_space = COLORSPACE_sRGB;
240 format.gamma_curve = GAMMA_LINEAR;
242 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
243 input->set_pixel_data(data);
244 tester.get_chain()->add_input(input);
246 // Now we change to Rec. 601 input.
247 input->set_color_space(COLORSPACE_REC_601_625);
248 input->set_gamma_curve(GAMMA_REC_601);
250 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
251 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
252 expect_equal(data, out_data, 4, 1);
255 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
260 float expected_data[6] = {
265 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
266 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
267 tester.get_chain()->add_effect(effect);
268 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
270 Node *node = effect->replaced_node;
271 ASSERT_EQ(1, node->incoming_links.size());
272 EXPECT_EQ(0, node->outgoing_links.size());
273 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
275 expect_equal(expected_data, out_data, 3, 2);
278 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
283 float expected_data[6] = {
288 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
289 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
290 tester.get_chain()->add_effect(effect);
291 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
293 Node *node = effect->replaced_node;
294 ASSERT_EQ(1, node->incoming_links.size());
295 EXPECT_EQ(0, node->outgoing_links.size());
296 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
298 expect_equal(expected_data, out_data, 3, 2);
301 // The identity effect needs linear light, and thus will get conversions on both sides.
302 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
303 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
305 for (unsigned i = 0; i < 256; ++i) {
309 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
310 tester.get_chain()->add_effect(new IdentityEffect());
311 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
313 expect_equal(data, out_data, 256, 1);
316 // Same, but uses the forward sRGB table from the GPU.
317 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
318 unsigned char data[256];
319 float expected_data[256];
320 for (unsigned i = 0; i < 256; ++i) {
322 expected_data[i] = i / 255.0;
325 EffectChainTester tester(NULL, 256, 1);
326 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
327 tester.get_chain()->add_effect(new IdentityEffect());
328 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
330 expect_equal(expected_data, out_data, 256, 1);
333 // Same, for the Rec. 601/709 gamma curve.
334 TEST(EffectChainTest, IdentityThroughRec709) {
336 for (unsigned i = 0; i < 256; ++i) {
340 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
341 tester.get_chain()->add_effect(new IdentityEffect());
342 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
344 expect_equal(data, out_data, 256, 1);
347 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
348 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
350 float data[4 * size] = {
351 0.8f, 0.0f, 0.0f, 0.5f,
352 0.0f, 0.2f, 0.2f, 0.3f,
353 0.1f, 0.0f, 1.0f, 1.0f,
355 float out_data[4 * size];
356 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
357 tester.get_chain()->add_effect(new IdentityEffect());
358 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
360 expect_equal(data, out_data, 4, size);
363 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
365 float data[4 * size] = {
366 0.8f, 0.0f, 0.0f, 0.5f,
367 0.0f, 0.2f, 0.2f, 0.3f,
368 0.1f, 0.0f, 1.0f, 1.0f,
370 float expected_data[4 * size] = {
371 0.1f, 0.0f, 1.0f, 1.0f,
372 0.0f, 0.2f, 0.2f, 0.3f,
373 0.8f, 0.0f, 0.0f, 0.5f,
375 float out_data[4 * size];
376 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
377 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
378 tester.get_chain()->add_effect(effect);
379 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
381 Node *node = effect->replaced_node;
382 ASSERT_EQ(1, node->incoming_links.size());
383 EXPECT_EQ(0, node->outgoing_links.size());
384 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
386 expect_equal(expected_data, out_data, 4, size);
389 // An input that outputs only blue, which has blank alpha.
390 class BlueInput : public Input {
392 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
393 virtual string effect_type_id() const { return "IdentityEffect"; }
394 string output_fragment_shader() { return read_file("blue.frag"); }
395 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
396 virtual void finalize() {}
397 virtual bool can_output_linear_gamma() const { return true; }
398 virtual unsigned get_width() const { return 1; }
399 virtual unsigned get_height() const { return 1; }
400 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
401 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
407 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
408 // which outputs blank alpha.
409 class RewritingToBlueInput : public Input {
411 RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
412 virtual string effect_type_id() const { return "RewritingToBlueInput"; }
413 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
414 virtual void rewrite_graph(EffectChain *graph, Node *self) {
415 Node *blue_node = graph->add_node(new BlueInput());
416 graph->replace_receiver(self, blue_node);
417 graph->replace_sender(self, blue_node);
419 self->disabled = true;
420 this->blue_node = blue_node;
423 // Dummy values that we need to implement because we inherit from Input.
424 // Same as BlueInput.
425 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
426 virtual void finalize() {}
427 virtual bool can_output_linear_gamma() const { return true; }
428 virtual unsigned get_width() const { return 1; }
429 virtual unsigned get_height() const { return 1; }
430 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
431 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
439 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
441 float data[4 * size] = {
442 0.0f, 0.0f, 1.0f, 1.0f,
443 0.0f, 0.0f, 1.0f, 1.0f,
444 0.0f, 0.0f, 1.0f, 1.0f,
446 float out_data[4 * size];
447 EffectChainTester tester(NULL, size, 1);
448 RewritingToBlueInput *input = new RewritingToBlueInput();
449 tester.get_chain()->add_input(input);
450 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
452 Node *node = input->blue_node;
453 EXPECT_EQ(0, node->incoming_links.size());
454 EXPECT_EQ(0, node->outgoing_links.size());
456 expect_equal(data, out_data, 4, size);
459 // An effect that does nothing, and specifies that it preserves blank alpha.
460 class BlankAlphaPreservingEffect : public Effect {
462 BlankAlphaPreservingEffect() {}
463 virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
464 string output_fragment_shader() { return read_file("identity.frag"); }
465 virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
468 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
470 float data[4 * size] = {
471 0.0f, 0.0f, 1.0f, 1.0f,
472 0.0f, 0.0f, 1.0f, 1.0f,
473 0.0f, 0.0f, 1.0f, 1.0f,
475 float out_data[4 * size];
476 EffectChainTester tester(NULL, size, 1);
477 tester.get_chain()->add_input(new BlueInput());
478 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
479 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
480 tester.get_chain()->add_effect(effect);
481 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
483 Node *node = effect->replaced_node;
484 EXPECT_EQ(1, node->incoming_links.size());
485 EXPECT_EQ(0, node->outgoing_links.size());
487 expect_equal(data, out_data, 4, size);
490 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
491 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
492 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
493 // with other tests.)
494 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
496 float data[4 * size] = {
497 0.0f, 0.0f, 1.0f, 1.0f,
498 0.0f, 0.0f, 1.0f, 1.0f,
499 0.0f, 0.0f, 1.0f, 1.0f,
501 float out_data[4 * size];
502 EffectChainTester tester(NULL, size, 1);
503 tester.get_chain()->add_input(new BlueInput());
504 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
505 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
506 tester.get_chain()->add_effect(effect);
507 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
509 Node *node = effect->replaced_node;
510 EXPECT_EQ(1, node->incoming_links.size());
511 EXPECT_EQ(1, node->outgoing_links.size());
512 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
514 expect_equal(data, out_data, 4, size);
517 // Effectively scales down its input linearly by 4x (and repeating it),
518 // which is not attainable without mipmaps.
519 class MipmapNeedingEffect : public Effect {
521 MipmapNeedingEffect() {}
522 virtual bool needs_mipmaps() const { return true; }
523 virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
524 string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
525 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
527 glActiveTexture(GL_TEXTURE0);
529 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
531 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
536 TEST(EffectChainTest, MipmapGenerationWorks) {
537 float data[] = { // In 4x4 blocks.
538 1.0f, 0.0f, 0.0f, 0.0f,
539 0.0f, 0.0f, 0.0f, 0.0f,
540 0.0f, 0.0f, 0.0f, 0.0f,
541 0.0f, 0.0f, 0.0f, 1.0f,
543 0.0f, 0.0f, 0.0f, 0.0f,
544 0.0f, 0.5f, 0.0f, 0.0f,
545 0.0f, 0.0f, 1.0f, 0.0f,
546 0.0f, 0.0f, 0.0f, 0.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 1.0f, 1.0f, 1.0f, 1.0f,
553 0.0f, 0.0f, 0.0f, 0.0f,
554 0.0f, 1.0f, 1.0f, 0.0f,
555 0.0f, 1.0f, 1.0f, 0.0f,
556 0.0f, 0.0f, 0.0f, 0.0f,
558 float expected_data[] = { // Repeated four times each way.
559 0.125f, 0.125f, 0.125f, 0.125f,
560 0.09375f, 0.09375f, 0.09375f, 0.09375f,
561 1.0f, 1.0f, 1.0f, 1.0f,
562 0.25f, 0.25f, 0.25f, 0.25f,
564 0.125f, 0.125f, 0.125f, 0.125f,
565 0.09375f, 0.09375f, 0.09375f, 0.09375f,
566 1.0f, 1.0f, 1.0f, 1.0f,
567 0.25f, 0.25f, 0.25f, 0.25f,
569 0.125f, 0.125f, 0.125f, 0.125f,
570 0.09375f, 0.09375f, 0.09375f, 0.09375f,
571 1.0f, 1.0f, 1.0f, 1.0f,
572 0.25f, 0.25f, 0.25f, 0.25f,
574 0.125f, 0.125f, 0.125f, 0.125f,
575 0.09375f, 0.09375f, 0.09375f, 0.09375f,
576 1.0f, 1.0f, 1.0f, 1.0f,
577 0.25f, 0.25f, 0.25f, 0.25f,
579 float out_data[4 * 16];
580 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
581 tester.get_chain()->add_effect(new MipmapNeedingEffect());
582 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
584 expect_equal(expected_data, out_data, 4, 16);
587 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
588 float data[] = { // In 4x4 blocks.
589 1.0f, 0.0f, 0.0f, 0.0f,
590 0.0f, 0.0f, 0.0f, 0.0f,
591 0.0f, 0.0f, 0.0f, 0.0f,
592 0.0f, 0.0f, 0.0f, 1.0f,
594 0.0f, 0.0f, 0.0f, 0.0f,
595 0.0f, 0.5f, 0.0f, 0.0f,
596 0.0f, 0.0f, 1.0f, 0.0f,
597 0.0f, 0.0f, 0.0f, 0.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 1.0f, 1.0f, 1.0f, 1.0f,
604 0.0f, 0.0f, 0.0f, 0.0f,
605 0.0f, 1.0f, 1.0f, 0.0f,
606 0.0f, 1.0f, 1.0f, 0.0f,
607 0.0f, 0.0f, 0.0f, 0.0f,
609 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
610 0.1250f, 0.1250f, 0.1250f, 0.1250f,
611 0.1250f, 0.1250f, 0.1250f, 0.1250f,
612 0.1211f, 0.1211f, 0.1211f, 0.1211f,
613 0.1133f, 0.1133f, 0.1133f, 0.1133f,
614 0.1055f, 0.1055f, 0.1055f, 0.1055f,
615 0.0977f, 0.0977f, 0.0977f, 0.0977f,
616 0.2070f, 0.2070f, 0.2070f, 0.2070f,
617 0.4336f, 0.4336f, 0.4336f, 0.4336f,
618 0.6602f, 0.6602f, 0.6602f, 0.6602f,
619 0.8867f, 0.8867f, 0.8867f, 0.8867f,
620 0.9062f, 0.9062f, 0.9062f, 0.9062f,
621 0.7188f, 0.7188f, 0.7188f, 0.7188f,
622 0.5312f, 0.5312f, 0.5312f, 0.5312f,
623 0.3438f, 0.3438f, 0.3438f, 0.3438f,
624 0.2500f, 0.2500f, 0.2500f, 0.2500f,
625 0.2500f, 0.2500f, 0.2500f, 0.2500f,
627 float out_data[4 * 16];
629 ResizeEffect *downscale = new ResizeEffect();
630 ASSERT_TRUE(downscale->set_int("width", 1));
631 ASSERT_TRUE(downscale->set_int("height", 4));
633 ResizeEffect *upscale = new ResizeEffect();
634 ASSERT_TRUE(upscale->set_int("width", 4));
635 ASSERT_TRUE(upscale->set_int("height", 16));
637 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
638 tester.get_chain()->add_effect(downscale);
639 tester.get_chain()->add_effect(upscale);
640 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
642 expect_equal(expected_data, out_data, 4, 16);
645 // An effect that adds its two inputs together. Used below.
646 class AddEffect : public Effect {
649 virtual string effect_type_id() const { return "AddEffect"; }
650 string output_fragment_shader() { return read_file("add.frag"); }
651 virtual unsigned num_inputs() const { return 2; }
652 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
655 // Constructs the graph
659 // MultiplyEffect MultiplyEffect |
663 // and verifies that it gives the correct output.
664 TEST(EffectChainTest, DiamondGraph) {
669 float expected_data[] = {
673 float out_data[2 * 2];
675 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
676 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
678 MultiplyEffect *mul_half = new MultiplyEffect();
679 ASSERT_TRUE(mul_half->set_vec4("factor", half));
681 MultiplyEffect *mul_two = new MultiplyEffect();
682 ASSERT_TRUE(mul_two->set_vec4("factor", two));
684 EffectChainTester tester(NULL, 2, 2);
687 format.color_space = COLORSPACE_sRGB;
688 format.gamma_curve = GAMMA_LINEAR;
690 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
691 input->set_pixel_data(data);
693 tester.get_chain()->add_input(input);
694 tester.get_chain()->add_effect(mul_half, input);
695 tester.get_chain()->add_effect(mul_two, input);
696 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
697 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
699 expect_equal(expected_data, out_data, 2, 2);
702 // Constructs the graph
706 // MultiplyEffect MultiplyEffect |
708 // \ BouncingIdentityEffect |
712 // and verifies that it gives the correct output.
713 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
718 float expected_data[] = {
722 float out_data[2 * 2];
724 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
725 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
727 MultiplyEffect *mul_half = new MultiplyEffect();
728 ASSERT_TRUE(mul_half->set_vec4("factor", half));
730 MultiplyEffect *mul_two = new MultiplyEffect();
731 ASSERT_TRUE(mul_two->set_vec4("factor", two));
733 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
735 EffectChainTester tester(NULL, 2, 2);
738 format.color_space = COLORSPACE_sRGB;
739 format.gamma_curve = GAMMA_LINEAR;
741 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
742 input->set_pixel_data(data);
744 tester.get_chain()->add_input(input);
745 tester.get_chain()->add_effect(mul_half, input);
746 tester.get_chain()->add_effect(mul_two, input);
747 tester.get_chain()->add_effect(bounce, mul_two);
748 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
749 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
751 expect_equal(expected_data, out_data, 2, 2);
754 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
759 float expected_data[] = {
760 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
763 float out_data[2 * 2];
765 EffectChainTester tester(NULL, 2, 2);
766 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
768 // MirrorEffect does not get linear light, so the conversions will be
769 // inserted after it, not before.
770 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
771 tester.get_chain()->add_effect(effect);
773 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
774 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
775 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
776 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
778 expect_equal(expected_data, out_data, 2, 2);
780 Node *node = effect->replaced_node;
781 ASSERT_EQ(1, node->incoming_links.size());
782 ASSERT_EQ(1, node->outgoing_links.size());
783 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
784 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
787 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
792 float expected_data[] = {
793 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
796 float out_data[2 * 2];
798 EffectChainTester tester(NULL, 2, 2);
799 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
801 // MirrorEffect does not get linear light, so the conversions will be
802 // inserted after it, not before.
803 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
804 tester.get_chain()->add_effect(effect);
806 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
807 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
808 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
809 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
811 expect_equal(expected_data, out_data, 2, 2);
813 Node *node = effect->replaced_node;
814 ASSERT_EQ(1, node->incoming_links.size());
815 ASSERT_EQ(1, node->outgoing_links.size());
816 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
817 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
820 // An effect that does nothing, but requests texture bounce and stores
822 class SizeStoringEffect : public BouncingIdentityEffect {
824 SizeStoringEffect() : input_width(-1), input_height(-1) {}
825 virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
826 assert(input_num == 0);
828 input_height = height;
830 virtual string effect_type_id() const { return "SizeStoringEffect"; }
832 int input_width, input_height;
835 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
836 float data[2 * 2] = {
840 float out_data[2 * 2];
842 EffectChainTester tester(NULL, 4, 3); // Note non-square aspect.
845 format.color_space = COLORSPACE_sRGB;
846 format.gamma_curve = GAMMA_LINEAR;
848 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
849 input1->set_pixel_data(data);
851 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
852 input2->set_pixel_data(data);
854 SizeStoringEffect *input_store = new SizeStoringEffect();
856 tester.get_chain()->add_input(input1);
857 tester.get_chain()->add_input(input2);
858 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
859 tester.get_chain()->add_effect(input_store);
860 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
862 EXPECT_EQ(2, input_store->input_width);
863 EXPECT_EQ(2, input_store->input_height);
866 TEST(EffectChainTest, AspectRatioConversion) {
867 float data1[4 * 3] = {
868 0.0f, 0.0f, 0.0f, 0.0f,
869 0.0f, 0.0f, 0.0f, 0.0f,
870 0.0f, 0.0f, 0.0f, 0.0f,
872 float data2[7 * 7] = {
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, 0.0f, 0.0f, 0.0f, 0.0f,
876 0.0f, 0.0f, 0.0f, 1.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 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
882 // The right conversion here is that the 7x7 image decides the size,
883 // since it is the biggest, so everything is scaled up to 9x7
884 // (keep the height, round the width 9.333 to 9).
885 float out_data[9 * 7];
887 EffectChainTester tester(NULL, 4, 3);
890 format.color_space = COLORSPACE_sRGB;
891 format.gamma_curve = GAMMA_LINEAR;
893 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
894 input1->set_pixel_data(data1);
896 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
897 input2->set_pixel_data(data2);
899 SizeStoringEffect *input_store = new SizeStoringEffect();
901 tester.get_chain()->add_input(input1);
902 tester.get_chain()->add_input(input2);
903 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
904 tester.get_chain()->add_effect(input_store);
905 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
907 EXPECT_EQ(9, input_store->input_width);
908 EXPECT_EQ(7, input_store->input_height);
911 // An effect that does nothing except changing its output sizes.
912 class VirtualResizeEffect : public Effect {
914 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
917 virtual_width(virtual_width),
918 virtual_height(virtual_height) {}
919 virtual string effect_type_id() const { return "VirtualResizeEffect"; }
920 string output_fragment_shader() { return read_file("identity.frag"); }
922 virtual bool changes_output_size() const { return true; }
924 virtual void get_output_size(unsigned *width, unsigned *height,
925 unsigned *virtual_width, unsigned *virtual_height) const {
926 *width = this->width;
927 *height = this->height;
928 *virtual_width = this->virtual_width;
929 *virtual_height = this->virtual_height;
933 int width, height, virtual_width, virtual_height;
936 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
937 const int size = 2, bigger_size = 3;
938 float data[size * size] = {
942 float out_data[size * size];
944 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
946 SizeStoringEffect *size_store = new SizeStoringEffect();
948 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
949 tester.get_chain()->add_effect(size_store);
950 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
951 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
953 EXPECT_EQ(bigger_size, size_store->input_width);
954 EXPECT_EQ(bigger_size, size_store->input_height);
956 // If the resize is implemented as non-virtual, we'll fail here,
957 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
958 expect_equal(data, out_data, size, size);
961 // Does not use EffectChainTest, so that it can construct an EffectChain without
962 // a shared ResourcePool (which is also properly destroyed afterwards).
963 // Also turns on debugging to test that code path.
964 TEST(EffectChainTest, IdentityWithOwnPool) {
965 const int width = 3, height = 2;
970 const float expected_data[] = {
976 EffectChain chain(width, height);
977 movit_debug_level = MOVIT_DEBUG_ON;
980 format.color_space = COLORSPACE_sRGB;
981 format.gamma_curve = GAMMA_LINEAR;
983 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
984 input->set_pixel_data(data);
985 chain.add_input(input);
986 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
989 glGenTextures(1, &texnum);
991 glBindTexture(GL_TEXTURE_2D, texnum);
993 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
996 glGenFramebuffers(1, &fbo);
998 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1000 glFramebufferTexture2D(
1002 GL_COLOR_ATTACHMENT0,
1007 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1012 chain.render_to_fbo(fbo, width, height);
1014 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1015 glReadPixels(0, 0, width, height, GL_RED, GL_FLOAT, out_data);
1017 expect_equal(expected_data, out_data, width, height);
1019 // Reset the debug status again.
1020 movit_debug_level = MOVIT_DEBUG_OFF;