1 // Unit tests for EffectChain.
3 // Note that this also contains the tests for some of the simpler effects.
13 #include "effect_chain.h"
14 #include "flat_input.h"
15 #include "gtest/gtest.h"
18 #include "mirror_effect.h"
19 #include "multiply_effect.h"
20 #include "resize_effect.h"
21 #include "test_util.h"
28 TEST(EffectChainTest, EmptyChain) {
34 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
35 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
37 expect_equal(data, out_data, 3, 2);
40 // An effect that does nothing.
41 class IdentityEffect : public Effect {
44 virtual string effect_type_id() const { return "IdentityEffect"; }
45 string output_fragment_shader() { return read_file("identity.frag"); }
48 TEST(EffectChainTest, Identity) {
54 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
55 tester.get_chain()->add_effect(new IdentityEffect());
56 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
58 expect_equal(data, out_data, 3, 2);
61 // An effect that does nothing, but requests texture bounce.
62 class BouncingIdentityEffect : public Effect {
64 BouncingIdentityEffect() {}
65 virtual string effect_type_id() const { return "IdentityEffect"; }
66 string output_fragment_shader() { return read_file("identity.frag"); }
67 bool needs_texture_bounce() const { return true; }
68 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
71 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
77 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
78 tester.get_chain()->add_effect(new BouncingIdentityEffect());
79 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
81 expect_equal(data, out_data, 3, 2);
84 TEST(MirrorTest, BasicTest) {
89 float expected_data[6] = {
94 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
95 tester.get_chain()->add_effect(new MirrorEffect());
96 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
98 expect_equal(expected_data, out_data, 3, 2);
101 // A dummy effect that inverts its input.
102 class InvertEffect : public Effect {
105 virtual string effect_type_id() const { return "InvertEffect"; }
106 string output_fragment_shader() { return read_file("invert_effect.frag"); }
108 // A real invert would actually care about its alpha,
109 // but in this unit test, it only complicates things.
110 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
113 // Like IdentityEffect, but rewrites itself out of the loop,
114 // splicing in a different effect instead. Also stores the new node,
115 // so we later can check whatever properties we'd like about the graph.
117 class RewritingEffect : public Effect {
119 RewritingEffect() : effect(new T()), replaced_node(NULL) {}
120 virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
121 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
122 virtual void rewrite_graph(EffectChain *graph, Node *self) {
123 replaced_node = graph->add_node(effect);
124 graph->replace_receiver(self, replaced_node);
125 graph->replace_sender(self, replaced_node);
126 self->disabled = true;
133 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
138 float expected_data[6] = {
139 1.0f, 0.9771f, 0.9673f,
143 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
144 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
145 tester.get_chain()->add_effect(effect);
146 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
148 Node *node = effect->replaced_node;
149 ASSERT_EQ(1, node->incoming_links.size());
150 ASSERT_EQ(1, node->outgoing_links.size());
151 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
152 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
154 expect_equal(expected_data, out_data, 3, 2);
157 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
158 unsigned char data[] = {
164 float expected_data[] = {
165 1.0000f, 1.0000f, 1.0000f, 1.0000f,
166 0.9771f, 0.9771f, 0.9771f, 1.0000f,
167 0.8983f, 0.8983f, 0.8983f, 1.0000f,
168 0.0000f, 0.0000f, 0.0000f, 1.0000f
170 float out_data[4 * 4];
171 EffectChainTester tester(NULL, 1, 4);
172 tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
173 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
174 tester.get_chain()->add_effect(effect);
175 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
177 Node *node = effect->replaced_node;
178 ASSERT_EQ(1, node->incoming_links.size());
179 ASSERT_EQ(1, node->outgoing_links.size());
180 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
181 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
183 expect_equal(expected_data, out_data, 4, 4);
186 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
191 float expected_data[6] = {
196 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
197 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
198 tester.get_chain()->add_effect(effect);
199 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
201 Node *node = effect->replaced_node;
202 ASSERT_EQ(1, node->incoming_links.size());
203 ASSERT_EQ(1, node->outgoing_links.size());
204 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
205 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
207 expect_equal(expected_data, out_data, 3, 2);
210 // A fake input that can change its output colorspace and gamma between instantiation
212 class UnknownColorspaceInput : public FlatInput {
214 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
215 : FlatInput(format, pixel_format, type, width, height),
216 overridden_color_space(format.color_space),
217 overridden_gamma_curve(format.gamma_curve) {}
218 virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
220 void set_color_space(Colorspace colorspace) {
221 overridden_color_space = colorspace;
223 void set_gamma_curve(GammaCurve gamma_curve) {
224 overridden_gamma_curve = gamma_curve;
226 Colorspace get_color_space() const { return overridden_color_space; }
227 GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
230 Colorspace overridden_color_space;
231 GammaCurve overridden_gamma_curve;
234 TEST(EffectChainTest, HandlesInputChangingColorspace) {
243 float out_data[size];
245 EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
247 // First say that we have sRGB, linear input.
249 format.color_space = COLORSPACE_sRGB;
250 format.gamma_curve = GAMMA_LINEAR;
252 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
253 input->set_pixel_data(data);
254 tester.get_chain()->add_input(input);
256 // Now we change to Rec. 601 input.
257 input->set_color_space(COLORSPACE_REC_601_625);
258 input->set_gamma_curve(GAMMA_REC_601);
260 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
261 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
262 expect_equal(data, out_data, 4, 1);
265 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
270 float expected_data[6] = {
275 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
276 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
277 tester.get_chain()->add_effect(effect);
278 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
280 Node *node = effect->replaced_node;
281 ASSERT_EQ(1, node->incoming_links.size());
282 EXPECT_EQ(0, node->outgoing_links.size());
283 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
285 expect_equal(expected_data, out_data, 3, 2);
288 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
293 float expected_data[6] = {
298 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
299 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
300 tester.get_chain()->add_effect(effect);
301 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
303 Node *node = effect->replaced_node;
304 ASSERT_EQ(1, node->incoming_links.size());
305 EXPECT_EQ(0, node->outgoing_links.size());
306 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
308 expect_equal(expected_data, out_data, 3, 2);
311 // The identity effect needs linear light, and thus will get conversions on both sides.
312 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
313 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
315 for (unsigned i = 0; i < 256; ++i) {
319 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
320 tester.get_chain()->add_effect(new IdentityEffect());
321 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
323 expect_equal(data, out_data, 256, 1);
326 // Same, but uses the forward sRGB table from the GPU.
327 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
328 unsigned char data[256];
329 float expected_data[256];
330 for (unsigned i = 0; i < 256; ++i) {
332 expected_data[i] = i / 255.0;
335 EffectChainTester tester(NULL, 256, 1);
336 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
337 tester.get_chain()->add_effect(new IdentityEffect());
338 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
340 expect_equal(expected_data, out_data, 256, 1);
343 // Same, for the Rec. 601/709 gamma curve.
344 TEST(EffectChainTest, IdentityThroughRec709) {
346 for (unsigned i = 0; i < 256; ++i) {
350 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
351 tester.get_chain()->add_effect(new IdentityEffect());
352 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
354 expect_equal(data, out_data, 256, 1);
357 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
358 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
360 float data[4 * size] = {
361 0.8f, 0.0f, 0.0f, 0.5f,
362 0.0f, 0.2f, 0.2f, 0.3f,
363 0.1f, 0.0f, 1.0f, 1.0f,
365 float out_data[4 * size];
366 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
367 tester.get_chain()->add_effect(new IdentityEffect());
368 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
370 expect_equal(data, out_data, 4, size);
373 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
375 float data[4 * size] = {
376 0.8f, 0.0f, 0.0f, 0.5f,
377 0.0f, 0.2f, 0.2f, 0.3f,
378 0.1f, 0.0f, 1.0f, 1.0f,
380 float expected_data[4 * size] = {
381 0.1f, 0.0f, 1.0f, 1.0f,
382 0.0f, 0.2f, 0.2f, 0.3f,
383 0.8f, 0.0f, 0.0f, 0.5f,
385 float out_data[4 * size];
386 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
387 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
388 tester.get_chain()->add_effect(effect);
389 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
391 Node *node = effect->replaced_node;
392 ASSERT_EQ(1, node->incoming_links.size());
393 EXPECT_EQ(0, node->outgoing_links.size());
394 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
396 expect_equal(expected_data, out_data, 4, size);
399 // An input that outputs only blue, which has blank alpha.
400 class BlueInput : public Input {
402 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
403 virtual string effect_type_id() const { return "IdentityEffect"; }
404 string output_fragment_shader() { return read_file("blue.frag"); }
405 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
406 virtual void finalize() {}
407 virtual bool can_output_linear_gamma() const { return true; }
408 virtual unsigned get_width() const { return 1; }
409 virtual unsigned get_height() const { return 1; }
410 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
411 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
417 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
418 // which outputs blank alpha.
419 class RewritingToBlueInput : public Input {
421 RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
422 virtual string effect_type_id() const { return "RewritingToBlueInput"; }
423 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
424 virtual void rewrite_graph(EffectChain *graph, Node *self) {
425 Node *blue_node = graph->add_node(new BlueInput());
426 graph->replace_receiver(self, blue_node);
427 graph->replace_sender(self, blue_node);
429 self->disabled = true;
430 this->blue_node = blue_node;
433 // Dummy values that we need to implement because we inherit from Input.
434 // Same as BlueInput.
435 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
436 virtual void finalize() {}
437 virtual bool can_output_linear_gamma() const { return true; }
438 virtual unsigned get_width() const { return 1; }
439 virtual unsigned get_height() const { return 1; }
440 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
441 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
449 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
451 float data[4 * size] = {
452 0.0f, 0.0f, 1.0f, 1.0f,
453 0.0f, 0.0f, 1.0f, 1.0f,
454 0.0f, 0.0f, 1.0f, 1.0f,
456 float out_data[4 * size];
457 EffectChainTester tester(NULL, size, 1);
458 RewritingToBlueInput *input = new RewritingToBlueInput();
459 tester.get_chain()->add_input(input);
460 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
462 Node *node = input->blue_node;
463 EXPECT_EQ(0, node->incoming_links.size());
464 EXPECT_EQ(0, node->outgoing_links.size());
466 expect_equal(data, out_data, 4, size);
469 // An effect that does nothing, and specifies that it preserves blank alpha.
470 class BlankAlphaPreservingEffect : public Effect {
472 BlankAlphaPreservingEffect() {}
473 virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
474 string output_fragment_shader() { return read_file("identity.frag"); }
475 virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
478 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
480 float data[4 * size] = {
481 0.0f, 0.0f, 1.0f, 1.0f,
482 0.0f, 0.0f, 1.0f, 1.0f,
483 0.0f, 0.0f, 1.0f, 1.0f,
485 float out_data[4 * size];
486 EffectChainTester tester(NULL, size, 1);
487 tester.get_chain()->add_input(new BlueInput());
488 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
489 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
490 tester.get_chain()->add_effect(effect);
491 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
493 Node *node = effect->replaced_node;
494 EXPECT_EQ(1, node->incoming_links.size());
495 EXPECT_EQ(0, node->outgoing_links.size());
497 expect_equal(data, out_data, 4, size);
500 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
501 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
502 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
503 // with other tests.)
504 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
506 float data[4 * size] = {
507 0.0f, 0.0f, 1.0f, 1.0f,
508 0.0f, 0.0f, 1.0f, 1.0f,
509 0.0f, 0.0f, 1.0f, 1.0f,
511 float out_data[4 * size];
512 EffectChainTester tester(NULL, size, 1);
513 tester.get_chain()->add_input(new BlueInput());
514 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
515 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
516 tester.get_chain()->add_effect(effect);
517 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
519 Node *node = effect->replaced_node;
520 EXPECT_EQ(1, node->incoming_links.size());
521 EXPECT_EQ(1, node->outgoing_links.size());
522 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
524 expect_equal(data, out_data, 4, size);
527 // Effectively scales down its input linearly by 4x (and repeating it),
528 // which is not attainable without mipmaps.
529 class MipmapNeedingEffect : public Effect {
531 MipmapNeedingEffect() {}
532 virtual bool needs_mipmaps() const { return true; }
534 // To be allowed to mess with the sampler state.
535 virtual bool needs_texture_bounce() const { return true; }
537 virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
538 string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
539 virtual void inform_added(EffectChain *chain) { this->chain = chain; }
541 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
543 Node *self = chain->find_node_for_effect(this);
544 glActiveTexture(chain->get_input_sampler(self, 0));
546 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
548 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
556 TEST(EffectChainTest, MipmapGenerationWorks) {
557 float data[] = { // In 4x4 blocks.
558 1.0f, 0.0f, 0.0f, 0.0f,
559 0.0f, 0.0f, 0.0f, 0.0f,
560 0.0f, 0.0f, 0.0f, 0.0f,
561 0.0f, 0.0f, 0.0f, 1.0f,
563 0.0f, 0.0f, 0.0f, 0.0f,
564 0.0f, 0.5f, 0.0f, 0.0f,
565 0.0f, 0.0f, 1.0f, 0.0f,
566 0.0f, 0.0f, 0.0f, 0.0f,
568 1.0f, 1.0f, 1.0f, 1.0f,
569 1.0f, 1.0f, 1.0f, 1.0f,
570 1.0f, 1.0f, 1.0f, 1.0f,
571 1.0f, 1.0f, 1.0f, 1.0f,
573 0.0f, 0.0f, 0.0f, 0.0f,
574 0.0f, 1.0f, 1.0f, 0.0f,
575 0.0f, 1.0f, 1.0f, 0.0f,
576 0.0f, 0.0f, 0.0f, 0.0f,
578 float expected_data[] = { // Repeated four times each way.
579 0.125f, 0.125f, 0.125f, 0.125f,
580 0.09375f, 0.09375f, 0.09375f, 0.09375f,
581 1.0f, 1.0f, 1.0f, 1.0f,
582 0.25f, 0.25f, 0.25f, 0.25f,
584 0.125f, 0.125f, 0.125f, 0.125f,
585 0.09375f, 0.09375f, 0.09375f, 0.09375f,
586 1.0f, 1.0f, 1.0f, 1.0f,
587 0.25f, 0.25f, 0.25f, 0.25f,
589 0.125f, 0.125f, 0.125f, 0.125f,
590 0.09375f, 0.09375f, 0.09375f, 0.09375f,
591 1.0f, 1.0f, 1.0f, 1.0f,
592 0.25f, 0.25f, 0.25f, 0.25f,
594 0.125f, 0.125f, 0.125f, 0.125f,
595 0.09375f, 0.09375f, 0.09375f, 0.09375f,
596 1.0f, 1.0f, 1.0f, 1.0f,
597 0.25f, 0.25f, 0.25f, 0.25f,
599 float out_data[4 * 16];
600 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
601 tester.get_chain()->add_effect(new MipmapNeedingEffect());
602 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
604 expect_equal(expected_data, out_data, 4, 16);
607 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
608 float data[] = { // In 4x4 blocks.
609 1.0f, 0.0f, 0.0f, 0.0f,
610 0.0f, 0.0f, 0.0f, 0.0f,
611 0.0f, 0.0f, 0.0f, 0.0f,
612 0.0f, 0.0f, 0.0f, 1.0f,
614 0.0f, 0.0f, 0.0f, 0.0f,
615 0.0f, 0.5f, 0.0f, 0.0f,
616 0.0f, 0.0f, 1.0f, 0.0f,
617 0.0f, 0.0f, 0.0f, 0.0f,
619 1.0f, 1.0f, 1.0f, 1.0f,
620 1.0f, 1.0f, 1.0f, 1.0f,
621 1.0f, 1.0f, 1.0f, 1.0f,
622 1.0f, 1.0f, 1.0f, 1.0f,
624 0.0f, 0.0f, 0.0f, 0.0f,
625 0.0f, 1.0f, 1.0f, 0.0f,
626 0.0f, 1.0f, 1.0f, 0.0f,
627 0.0f, 0.0f, 0.0f, 0.0f,
629 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
630 0.1250f, 0.1250f, 0.1250f, 0.1250f,
631 0.1250f, 0.1250f, 0.1250f, 0.1250f,
632 0.1211f, 0.1211f, 0.1211f, 0.1211f,
633 0.1133f, 0.1133f, 0.1133f, 0.1133f,
634 0.1055f, 0.1055f, 0.1055f, 0.1055f,
635 0.0977f, 0.0977f, 0.0977f, 0.0977f,
636 0.2070f, 0.2070f, 0.2070f, 0.2070f,
637 0.4336f, 0.4336f, 0.4336f, 0.4336f,
638 0.6602f, 0.6602f, 0.6602f, 0.6602f,
639 0.8867f, 0.8867f, 0.8867f, 0.8867f,
640 0.9062f, 0.9062f, 0.9062f, 0.9062f,
641 0.7188f, 0.7188f, 0.7188f, 0.7188f,
642 0.5312f, 0.5312f, 0.5312f, 0.5312f,
643 0.3438f, 0.3438f, 0.3438f, 0.3438f,
644 0.2500f, 0.2500f, 0.2500f, 0.2500f,
645 0.2500f, 0.2500f, 0.2500f, 0.2500f,
647 float out_data[4 * 16];
649 ResizeEffect *downscale = new ResizeEffect();
650 ASSERT_TRUE(downscale->set_int("width", 1));
651 ASSERT_TRUE(downscale->set_int("height", 4));
653 ResizeEffect *upscale = new ResizeEffect();
654 ASSERT_TRUE(upscale->set_int("width", 4));
655 ASSERT_TRUE(upscale->set_int("height", 16));
657 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
658 tester.get_chain()->add_effect(downscale);
659 tester.get_chain()->add_effect(upscale);
660 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
662 expect_equal(expected_data, out_data, 4, 16);
665 // An effect that adds its two inputs together. Used below.
666 class AddEffect : public Effect {
669 virtual string effect_type_id() const { return "AddEffect"; }
670 string output_fragment_shader() { return read_file("add.frag"); }
671 virtual unsigned num_inputs() const { return 2; }
672 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
675 // Constructs the graph
679 // MultiplyEffect MultiplyEffect |
683 // and verifies that it gives the correct output.
684 TEST(EffectChainTest, DiamondGraph) {
689 float expected_data[] = {
693 float out_data[2 * 2];
695 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
696 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
698 MultiplyEffect *mul_half = new MultiplyEffect();
699 ASSERT_TRUE(mul_half->set_vec4("factor", half));
701 MultiplyEffect *mul_two = new MultiplyEffect();
702 ASSERT_TRUE(mul_two->set_vec4("factor", two));
704 EffectChainTester tester(NULL, 2, 2);
707 format.color_space = COLORSPACE_sRGB;
708 format.gamma_curve = GAMMA_LINEAR;
710 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
711 input->set_pixel_data(data);
713 tester.get_chain()->add_input(input);
714 tester.get_chain()->add_effect(mul_half, input);
715 tester.get_chain()->add_effect(mul_two, input);
716 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
717 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
719 expect_equal(expected_data, out_data, 2, 2);
722 // Constructs the graph
726 // MultiplyEffect MultiplyEffect |
728 // \ BouncingIdentityEffect |
732 // and verifies that it gives the correct output.
733 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
738 float expected_data[] = {
742 float out_data[2 * 2];
744 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
745 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
747 MultiplyEffect *mul_half = new MultiplyEffect();
748 ASSERT_TRUE(mul_half->set_vec4("factor", half));
750 MultiplyEffect *mul_two = new MultiplyEffect();
751 ASSERT_TRUE(mul_two->set_vec4("factor", two));
753 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
755 EffectChainTester tester(NULL, 2, 2);
758 format.color_space = COLORSPACE_sRGB;
759 format.gamma_curve = GAMMA_LINEAR;
761 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
762 input->set_pixel_data(data);
764 tester.get_chain()->add_input(input);
765 tester.get_chain()->add_effect(mul_half, input);
766 tester.get_chain()->add_effect(mul_two, input);
767 tester.get_chain()->add_effect(bounce, mul_two);
768 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
769 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
771 expect_equal(expected_data, out_data, 2, 2);
774 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
779 float expected_data[] = {
780 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
783 float out_data[2 * 2];
785 EffectChainTester tester(NULL, 2, 2);
786 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
788 // MirrorEffect does not get linear light, so the conversions will be
789 // inserted after it, not before.
790 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
791 tester.get_chain()->add_effect(effect);
793 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
794 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
795 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
796 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
798 expect_equal(expected_data, out_data, 2, 2);
800 Node *node = effect->replaced_node;
801 ASSERT_EQ(1, node->incoming_links.size());
802 ASSERT_EQ(1, node->outgoing_links.size());
803 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
804 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
807 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
812 float expected_data[] = {
813 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
816 float out_data[2 * 2];
818 EffectChainTester tester(NULL, 2, 2);
819 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
821 // MirrorEffect does not get linear light, so the conversions will be
822 // inserted after it, not before.
823 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
824 tester.get_chain()->add_effect(effect);
826 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
827 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
828 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
829 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
831 expect_equal(expected_data, out_data, 2, 2);
833 Node *node = effect->replaced_node;
834 ASSERT_EQ(1, node->incoming_links.size());
835 ASSERT_EQ(1, node->outgoing_links.size());
836 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
837 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
840 // An effect that does nothing, but requests texture bounce and stores
842 class SizeStoringEffect : public BouncingIdentityEffect {
844 SizeStoringEffect() : input_width(-1), input_height(-1) {}
845 virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
846 assert(input_num == 0);
848 input_height = height;
850 virtual string effect_type_id() const { return "SizeStoringEffect"; }
852 int input_width, input_height;
855 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
856 float data[2 * 2] = {
860 float out_data[2 * 2];
862 EffectChainTester tester(NULL, 4, 3); // Note non-square aspect.
865 format.color_space = COLORSPACE_sRGB;
866 format.gamma_curve = GAMMA_LINEAR;
868 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
869 input1->set_pixel_data(data);
871 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
872 input2->set_pixel_data(data);
874 SizeStoringEffect *input_store = new SizeStoringEffect();
876 tester.get_chain()->add_input(input1);
877 tester.get_chain()->add_input(input2);
878 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
879 tester.get_chain()->add_effect(input_store);
880 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
882 EXPECT_EQ(2, input_store->input_width);
883 EXPECT_EQ(2, input_store->input_height);
886 TEST(EffectChainTest, AspectRatioConversion) {
887 float data1[4 * 3] = {
888 0.0f, 0.0f, 0.0f, 0.0f,
889 0.0f, 0.0f, 0.0f, 0.0f,
890 0.0f, 0.0f, 0.0f, 0.0f,
892 float data2[7 * 7] = {
893 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
894 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
895 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
896 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
897 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
898 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
899 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
902 // The right conversion here is that the 7x7 image decides the size,
903 // since it is the biggest, so everything is scaled up to 9x7
904 // (keep the height, round the width 9.333 to 9).
905 float out_data[9 * 7];
907 EffectChainTester tester(NULL, 4, 3);
910 format.color_space = COLORSPACE_sRGB;
911 format.gamma_curve = GAMMA_LINEAR;
913 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
914 input1->set_pixel_data(data1);
916 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
917 input2->set_pixel_data(data2);
919 SizeStoringEffect *input_store = new SizeStoringEffect();
921 tester.get_chain()->add_input(input1);
922 tester.get_chain()->add_input(input2);
923 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
924 tester.get_chain()->add_effect(input_store);
925 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
927 EXPECT_EQ(9, input_store->input_width);
928 EXPECT_EQ(7, input_store->input_height);
931 // An effect that does nothing except changing its output sizes.
932 class VirtualResizeEffect : public Effect {
934 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
937 virtual_width(virtual_width),
938 virtual_height(virtual_height) {}
939 virtual string effect_type_id() const { return "VirtualResizeEffect"; }
940 string output_fragment_shader() { return read_file("identity.frag"); }
942 virtual bool changes_output_size() const { return true; }
944 virtual void get_output_size(unsigned *width, unsigned *height,
945 unsigned *virtual_width, unsigned *virtual_height) const {
946 *width = this->width;
947 *height = this->height;
948 *virtual_width = this->virtual_width;
949 *virtual_height = this->virtual_height;
953 int width, height, virtual_width, virtual_height;
956 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
957 const int size = 2, bigger_size = 3;
958 float data[size * size] = {
962 float out_data[size * size];
964 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
966 SizeStoringEffect *size_store = new SizeStoringEffect();
968 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
969 tester.get_chain()->add_effect(size_store);
970 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
971 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
973 EXPECT_EQ(bigger_size, size_store->input_width);
974 EXPECT_EQ(bigger_size, size_store->input_height);
976 // If the resize is implemented as non-virtual, we'll fail here,
977 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
978 expect_equal(data, out_data, size, size);
981 // Does not use EffectChainTest, so that it can construct an EffectChain without
982 // a shared ResourcePool (which is also properly destroyed afterwards).
983 // Also turns on debugging to test that code path.
984 TEST(EffectChainTest, IdentityWithOwnPool) {
985 const int width = 3, height = 2;
990 const float expected_data[] = {
994 float out_data[6], temp[6 * 4];
996 EffectChain chain(width, height);
997 movit_debug_level = MOVIT_DEBUG_ON;
1000 format.color_space = COLORSPACE_sRGB;
1001 format.gamma_curve = GAMMA_LINEAR;
1003 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1004 input->set_pixel_data(data);
1005 chain.add_input(input);
1006 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1009 glGenTextures(1, &texnum);
1011 glBindTexture(GL_TEXTURE_2D, texnum);
1013 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
1016 glGenFramebuffers(1, &fbo);
1018 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1020 glFramebufferTexture2D(
1022 GL_COLOR_ATTACHMENT0,
1027 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1032 chain.render_to_fbo(fbo, width, height);
1034 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1036 glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1038 for (unsigned i = 0; i < 6; ++i) {
1039 out_data[i] = temp[i * 4];
1042 expect_equal(expected_data, out_data, width, height);
1044 // Reset the debug status again.
1045 movit_debug_level = MOVIT_DEBUG_OFF;
1048 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1049 class PrintfingBlueEffect : public Effect {
1051 PrintfingBlueEffect() {}
1052 virtual string effect_type_id() const { return "PrintfingBlueEffect"; }
1053 string output_fragment_shader() {
1055 ss.imbue(locale("C"));
1057 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1058 << 0.0f << ", " << 0.0f << ", "
1059 << 0.5f << ", " << 1.0f << "); }\n";
1064 TEST(EffectChainTest, StringStreamLocalesWork) {
1065 // An example of a locale with comma instead of period as decimal separator.
1066 // Obviously, if you run on a machine without this locale available,
1067 // the test will always succeed. Note that the OpenGL driver might call
1068 // setlocale() behind-the-scenes, and that might corrupt the returned
1069 // pointer, so we need to take our own copy of it here.
1070 char *saved_locale = strdup(setlocale(LC_ALL, "nb_NO.UTF_8"));
1072 0.0f, 0.0f, 0.0f, 0.0f,
1074 float expected_data[] = {
1075 0.0f, 0.0f, 0.5f, 1.0f,
1078 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1079 tester.get_chain()->add_effect(new PrintfingBlueEffect());
1080 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1082 expect_equal(expected_data, out_data, 4, 1);
1084 setlocale(LC_ALL, saved_locale);
1089 } // namespace movit