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"
24 TEST(EffectChainTest, EmptyChain) {
30 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
31 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
33 expect_equal(data, out_data, 3, 2);
36 // An effect that does nothing.
37 class IdentityEffect : public Effect {
40 virtual string effect_type_id() const { return "IdentityEffect"; }
41 string output_fragment_shader() { return read_file("identity.frag"); }
44 TEST(EffectChainTest, Identity) {
50 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
51 tester.get_chain()->add_effect(new IdentityEffect());
52 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
54 expect_equal(data, out_data, 3, 2);
57 // An effect that does nothing, but requests texture bounce.
58 class BouncingIdentityEffect : public Effect {
60 BouncingIdentityEffect() {}
61 virtual string effect_type_id() const { return "IdentityEffect"; }
62 string output_fragment_shader() { return read_file("identity.frag"); }
63 bool needs_texture_bounce() const { return true; }
64 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
67 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
73 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
74 tester.get_chain()->add_effect(new BouncingIdentityEffect());
75 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
77 expect_equal(data, out_data, 3, 2);
80 TEST(MirrorTest, BasicTest) {
85 float expected_data[6] = {
90 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
91 tester.get_chain()->add_effect(new MirrorEffect());
92 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
94 expect_equal(expected_data, out_data, 3, 2);
97 // A dummy effect that inverts its input.
98 class InvertEffect : public Effect {
101 virtual string effect_type_id() const { return "InvertEffect"; }
102 string output_fragment_shader() { return read_file("invert_effect.frag"); }
104 // A real invert would actually care about its alpha,
105 // but in this unit test, it only complicates things.
106 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
109 // Like IdentityEffect, but rewrites itself out of the loop,
110 // splicing in a different effect instead. Also stores the new node,
111 // so we later can check whatever properties we'd like about the graph.
113 class RewritingEffect : public Effect {
115 RewritingEffect() : effect(new T()), replaced_node(NULL) {}
116 virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
117 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
118 virtual void rewrite_graph(EffectChain *graph, Node *self) {
119 replaced_node = graph->add_node(effect);
120 graph->replace_receiver(self, replaced_node);
121 graph->replace_sender(self, replaced_node);
122 self->disabled = true;
129 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
134 float expected_data[6] = {
135 1.0f, 0.9771f, 0.9673f,
139 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
140 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
141 tester.get_chain()->add_effect(effect);
142 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
144 Node *node = effect->replaced_node;
145 ASSERT_EQ(1, node->incoming_links.size());
146 ASSERT_EQ(1, node->outgoing_links.size());
147 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
148 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
150 expect_equal(expected_data, out_data, 3, 2);
153 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
154 unsigned char data[] = {
158 float expected_data[4] = {
163 EffectChainTester tester(NULL, 2, 2);
164 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
165 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
166 tester.get_chain()->add_effect(effect);
167 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
169 Node *node = effect->replaced_node;
170 ASSERT_EQ(1, node->incoming_links.size());
171 ASSERT_EQ(1, node->outgoing_links.size());
172 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
173 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
175 expect_equal(expected_data, out_data, 2, 2);
178 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
183 float expected_data[6] = {
188 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
189 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
190 tester.get_chain()->add_effect(effect);
191 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
193 Node *node = effect->replaced_node;
194 ASSERT_EQ(1, node->incoming_links.size());
195 ASSERT_EQ(1, node->outgoing_links.size());
196 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
197 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
199 expect_equal(expected_data, out_data, 3, 2);
202 // A fake input that can change its output colorspace and gamma between instantiation
204 class UnknownColorspaceInput : public FlatInput {
206 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
207 : FlatInput(format, pixel_format, type, width, height),
208 overridden_color_space(format.color_space),
209 overridden_gamma_curve(format.gamma_curve) {}
210 virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
212 void set_color_space(Colorspace colorspace) {
213 overridden_color_space = colorspace;
215 void set_gamma_curve(GammaCurve gamma_curve) {
216 overridden_gamma_curve = gamma_curve;
218 Colorspace get_color_space() const { return overridden_color_space; }
219 GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
222 Colorspace overridden_color_space;
223 GammaCurve overridden_gamma_curve;
226 TEST(EffectChainTester, HandlesInputChangingColorspace) {
235 float out_data[size];
237 EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
239 // First say that we have sRGB, linear input.
241 format.color_space = COLORSPACE_sRGB;
242 format.gamma_curve = GAMMA_LINEAR;
244 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
245 input->set_pixel_data(data);
246 tester.get_chain()->add_input(input);
248 // Now we change to Rec. 601 input.
249 input->set_color_space(COLORSPACE_REC_601_625);
250 input->set_gamma_curve(GAMMA_REC_601);
252 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
253 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
254 expect_equal(data, out_data, 4, 1);
257 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
262 float expected_data[6] = {
267 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
268 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
269 tester.get_chain()->add_effect(effect);
270 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
272 Node *node = effect->replaced_node;
273 ASSERT_EQ(1, node->incoming_links.size());
274 EXPECT_EQ(0, node->outgoing_links.size());
275 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
277 expect_equal(expected_data, out_data, 3, 2);
280 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
285 float expected_data[6] = {
290 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
291 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
292 tester.get_chain()->add_effect(effect);
293 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
295 Node *node = effect->replaced_node;
296 ASSERT_EQ(1, node->incoming_links.size());
297 EXPECT_EQ(0, node->outgoing_links.size());
298 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
300 expect_equal(expected_data, out_data, 3, 2);
303 // The identity effect needs linear light, and thus will get conversions on both sides.
304 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
305 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
307 for (unsigned i = 0; i < 256; ++i) {
311 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
312 tester.get_chain()->add_effect(new IdentityEffect());
313 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
315 expect_equal(data, out_data, 256, 1);
318 // Same, but uses the forward sRGB table from the GPU.
319 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
320 unsigned char data[256];
321 float expected_data[256];
322 for (unsigned i = 0; i < 256; ++i) {
324 expected_data[i] = i / 255.0;
327 EffectChainTester tester(NULL, 256, 1);
328 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
329 tester.get_chain()->add_effect(new IdentityEffect());
330 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
332 expect_equal(expected_data, out_data, 256, 1);
335 // Same, for the Rec. 601/709 gamma curve.
336 TEST(EffectChainTest, IdentityThroughRec709) {
338 for (unsigned i = 0; i < 256; ++i) {
342 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
343 tester.get_chain()->add_effect(new IdentityEffect());
344 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
346 expect_equal(data, out_data, 256, 1);
349 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
350 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
352 float data[4 * size] = {
353 0.8f, 0.0f, 0.0f, 0.5f,
354 0.0f, 0.2f, 0.2f, 0.3f,
355 0.1f, 0.0f, 1.0f, 1.0f,
357 float out_data[4 * size];
358 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
359 tester.get_chain()->add_effect(new IdentityEffect());
360 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
362 expect_equal(data, out_data, 4, size);
365 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
367 float data[4 * size] = {
368 0.8f, 0.0f, 0.0f, 0.5f,
369 0.0f, 0.2f, 0.2f, 0.3f,
370 0.1f, 0.0f, 1.0f, 1.0f,
372 float expected_data[4 * size] = {
373 0.1f, 0.0f, 1.0f, 1.0f,
374 0.0f, 0.2f, 0.2f, 0.3f,
375 0.8f, 0.0f, 0.0f, 0.5f,
377 float out_data[4 * size];
378 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
379 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
380 tester.get_chain()->add_effect(effect);
381 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
383 Node *node = effect->replaced_node;
384 ASSERT_EQ(1, node->incoming_links.size());
385 EXPECT_EQ(0, node->outgoing_links.size());
386 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
388 expect_equal(expected_data, out_data, 4, size);
391 // An input that outputs only blue, which has blank alpha.
392 class BlueInput : public Input {
394 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
395 virtual string effect_type_id() const { return "IdentityEffect"; }
396 string output_fragment_shader() { return read_file("blue.frag"); }
397 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
398 virtual void finalize() {}
399 virtual bool can_output_linear_gamma() const { return true; }
400 virtual unsigned get_width() const { return 1; }
401 virtual unsigned get_height() const { return 1; }
402 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
403 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
409 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
410 // which outputs blank alpha.
411 class RewritingToBlueInput : public Input {
413 RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
414 virtual string effect_type_id() const { return "RewritingToBlueInput"; }
415 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
416 virtual void rewrite_graph(EffectChain *graph, Node *self) {
417 Node *blue_node = graph->add_node(new BlueInput());
418 graph->replace_receiver(self, blue_node);
419 graph->replace_sender(self, blue_node);
421 self->disabled = true;
422 this->blue_node = blue_node;
425 // Dummy values that we need to implement because we inherit from Input.
426 // Same as BlueInput.
427 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
428 virtual void finalize() {}
429 virtual bool can_output_linear_gamma() const { return true; }
430 virtual unsigned get_width() const { return 1; }
431 virtual unsigned get_height() const { return 1; }
432 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
433 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
441 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
443 float data[4 * size] = {
444 0.0f, 0.0f, 1.0f, 1.0f,
445 0.0f, 0.0f, 1.0f, 1.0f,
446 0.0f, 0.0f, 1.0f, 1.0f,
448 float out_data[4 * size];
449 EffectChainTester tester(NULL, size, 1);
450 RewritingToBlueInput *input = new RewritingToBlueInput();
451 tester.get_chain()->add_input(input);
452 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
454 Node *node = input->blue_node;
455 EXPECT_EQ(0, node->incoming_links.size());
456 EXPECT_EQ(0, node->outgoing_links.size());
458 expect_equal(data, out_data, 4, size);
461 // An effect that does nothing, and specifies that it preserves blank alpha.
462 class BlankAlphaPreservingEffect : public Effect {
464 BlankAlphaPreservingEffect() {}
465 virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
466 string output_fragment_shader() { return read_file("identity.frag"); }
467 virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
470 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
472 float data[4 * size] = {
473 0.0f, 0.0f, 1.0f, 1.0f,
474 0.0f, 0.0f, 1.0f, 1.0f,
475 0.0f, 0.0f, 1.0f, 1.0f,
477 float out_data[4 * size];
478 EffectChainTester tester(NULL, size, 1);
479 tester.get_chain()->add_input(new BlueInput());
480 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
481 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
482 tester.get_chain()->add_effect(effect);
483 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
485 Node *node = effect->replaced_node;
486 EXPECT_EQ(1, node->incoming_links.size());
487 EXPECT_EQ(0, node->outgoing_links.size());
489 expect_equal(data, out_data, 4, size);
492 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
493 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
494 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
495 // with other tests.)
496 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
498 float data[4 * size] = {
499 0.0f, 0.0f, 1.0f, 1.0f,
500 0.0f, 0.0f, 1.0f, 1.0f,
501 0.0f, 0.0f, 1.0f, 1.0f,
503 float out_data[4 * size];
504 EffectChainTester tester(NULL, size, 1);
505 tester.get_chain()->add_input(new BlueInput());
506 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
507 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
508 tester.get_chain()->add_effect(effect);
509 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
511 Node *node = effect->replaced_node;
512 EXPECT_EQ(1, node->incoming_links.size());
513 EXPECT_EQ(1, node->outgoing_links.size());
514 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
516 expect_equal(data, out_data, 4, size);
519 // Effectively scales down its input linearly by 4x (and repeating it),
520 // which is not attainable without mipmaps.
521 class MipmapNeedingEffect : public Effect {
523 MipmapNeedingEffect() {}
524 virtual bool needs_mipmaps() const { return true; }
525 virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
526 string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
527 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
529 glActiveTexture(GL_TEXTURE0);
531 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
533 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
538 TEST(EffectChainTest, MipmapGenerationWorks) {
539 float data[] = { // In 4x4 blocks.
540 1.0f, 0.0f, 0.0f, 0.0f,
541 0.0f, 0.0f, 0.0f, 0.0f,
542 0.0f, 0.0f, 0.0f, 0.0f,
543 0.0f, 0.0f, 0.0f, 1.0f,
545 0.0f, 0.0f, 0.0f, 0.0f,
546 0.0f, 0.5f, 0.0f, 0.0f,
547 0.0f, 0.0f, 1.0f, 0.0f,
548 0.0f, 0.0f, 0.0f, 0.0f,
550 1.0f, 1.0f, 1.0f, 1.0f,
551 1.0f, 1.0f, 1.0f, 1.0f,
552 1.0f, 1.0f, 1.0f, 1.0f,
553 1.0f, 1.0f, 1.0f, 1.0f,
555 0.0f, 0.0f, 0.0f, 0.0f,
556 0.0f, 1.0f, 1.0f, 0.0f,
557 0.0f, 1.0f, 1.0f, 0.0f,
558 0.0f, 0.0f, 0.0f, 0.0f,
560 float expected_data[] = { // Repeated four times each way.
561 0.125f, 0.125f, 0.125f, 0.125f,
562 0.09375f, 0.09375f, 0.09375f, 0.09375f,
563 1.0f, 1.0f, 1.0f, 1.0f,
564 0.25f, 0.25f, 0.25f, 0.25f,
566 0.125f, 0.125f, 0.125f, 0.125f,
567 0.09375f, 0.09375f, 0.09375f, 0.09375f,
568 1.0f, 1.0f, 1.0f, 1.0f,
569 0.25f, 0.25f, 0.25f, 0.25f,
571 0.125f, 0.125f, 0.125f, 0.125f,
572 0.09375f, 0.09375f, 0.09375f, 0.09375f,
573 1.0f, 1.0f, 1.0f, 1.0f,
574 0.25f, 0.25f, 0.25f, 0.25f,
576 0.125f, 0.125f, 0.125f, 0.125f,
577 0.09375f, 0.09375f, 0.09375f, 0.09375f,
578 1.0f, 1.0f, 1.0f, 1.0f,
579 0.25f, 0.25f, 0.25f, 0.25f,
581 float out_data[4 * 16];
582 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
583 tester.get_chain()->add_effect(new MipmapNeedingEffect());
584 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
586 expect_equal(expected_data, out_data, 4, 16);
589 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
590 float data[] = { // In 4x4 blocks.
591 1.0f, 0.0f, 0.0f, 0.0f,
592 0.0f, 0.0f, 0.0f, 0.0f,
593 0.0f, 0.0f, 0.0f, 0.0f,
594 0.0f, 0.0f, 0.0f, 1.0f,
596 0.0f, 0.0f, 0.0f, 0.0f,
597 0.0f, 0.5f, 0.0f, 0.0f,
598 0.0f, 0.0f, 1.0f, 0.0f,
599 0.0f, 0.0f, 0.0f, 0.0f,
601 1.0f, 1.0f, 1.0f, 1.0f,
602 1.0f, 1.0f, 1.0f, 1.0f,
603 1.0f, 1.0f, 1.0f, 1.0f,
604 1.0f, 1.0f, 1.0f, 1.0f,
606 0.0f, 0.0f, 0.0f, 0.0f,
607 0.0f, 1.0f, 1.0f, 0.0f,
608 0.0f, 1.0f, 1.0f, 0.0f,
609 0.0f, 0.0f, 0.0f, 0.0f,
611 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
612 0.1250f, 0.1250f, 0.1250f, 0.1250f,
613 0.1250f, 0.1250f, 0.1250f, 0.1250f,
614 0.1211f, 0.1211f, 0.1211f, 0.1211f,
615 0.1133f, 0.1133f, 0.1133f, 0.1133f,
616 0.1055f, 0.1055f, 0.1055f, 0.1055f,
617 0.0977f, 0.0977f, 0.0977f, 0.0977f,
618 0.2070f, 0.2070f, 0.2070f, 0.2070f,
619 0.4336f, 0.4336f, 0.4336f, 0.4336f,
620 0.6602f, 0.6602f, 0.6602f, 0.6602f,
621 0.8867f, 0.8867f, 0.8867f, 0.8867f,
622 0.9062f, 0.9062f, 0.9062f, 0.9062f,
623 0.7188f, 0.7188f, 0.7188f, 0.7188f,
624 0.5312f, 0.5312f, 0.5312f, 0.5312f,
625 0.3438f, 0.3438f, 0.3438f, 0.3438f,
626 0.2500f, 0.2500f, 0.2500f, 0.2500f,
627 0.2500f, 0.2500f, 0.2500f, 0.2500f,
629 float out_data[4 * 16];
631 ResizeEffect *downscale = new ResizeEffect();
632 ASSERT_TRUE(downscale->set_int("width", 1));
633 ASSERT_TRUE(downscale->set_int("height", 4));
635 ResizeEffect *upscale = new ResizeEffect();
636 ASSERT_TRUE(upscale->set_int("width", 4));
637 ASSERT_TRUE(upscale->set_int("height", 16));
639 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
640 tester.get_chain()->add_effect(downscale);
641 tester.get_chain()->add_effect(upscale);
642 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
644 expect_equal(expected_data, out_data, 4, 16);
647 // An effect that adds its two inputs together. Used below.
648 class AddEffect : public Effect {
651 virtual string effect_type_id() const { return "AddEffect"; }
652 string output_fragment_shader() { return read_file("add.frag"); }
653 virtual unsigned num_inputs() const { return 2; }
654 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
657 // Constructs the graph
661 // MultiplyEffect MultiplyEffect |
665 // and verifies that it gives the correct output.
666 TEST(EffectChainTest, DiamondGraph) {
671 float expected_data[] = {
675 float out_data[2 * 2];
677 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
678 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
680 MultiplyEffect *mul_half = new MultiplyEffect();
681 ASSERT_TRUE(mul_half->set_vec4("factor", half));
683 MultiplyEffect *mul_two = new MultiplyEffect();
684 ASSERT_TRUE(mul_two->set_vec4("factor", two));
686 EffectChainTester tester(NULL, 2, 2);
689 format.color_space = COLORSPACE_sRGB;
690 format.gamma_curve = GAMMA_LINEAR;
692 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
693 input->set_pixel_data(data);
695 tester.get_chain()->add_input(input);
696 tester.get_chain()->add_effect(mul_half, input);
697 tester.get_chain()->add_effect(mul_two, input);
698 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
699 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
701 expect_equal(expected_data, out_data, 2, 2);
704 // Constructs the graph
708 // MultiplyEffect MultiplyEffect |
710 // \ BouncingIdentityEffect |
714 // and verifies that it gives the correct output.
715 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
720 float expected_data[] = {
724 float out_data[2 * 2];
726 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
727 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
729 MultiplyEffect *mul_half = new MultiplyEffect();
730 ASSERT_TRUE(mul_half->set_vec4("factor", half));
732 MultiplyEffect *mul_two = new MultiplyEffect();
733 ASSERT_TRUE(mul_two->set_vec4("factor", two));
735 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
737 EffectChainTester tester(NULL, 2, 2);
740 format.color_space = COLORSPACE_sRGB;
741 format.gamma_curve = GAMMA_LINEAR;
743 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
744 input->set_pixel_data(data);
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);
753 expect_equal(expected_data, out_data, 2, 2);
756 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
761 float expected_data[] = {
762 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
765 float out_data[2 * 2];
767 EffectChainTester tester(NULL, 2, 2);
768 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
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);
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);
780 expect_equal(expected_data, out_data, 2, 2);
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());
789 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
794 float expected_data[] = {
795 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
798 float out_data[2 * 2];
800 EffectChainTester tester(NULL, 2, 2);
801 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
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);
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);
813 expect_equal(expected_data, out_data, 2, 2);
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());
822 // An effect that does nothing, but requests texture bounce and stores
824 class SizeStoringEffect : public BouncingIdentityEffect {
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);
830 input_height = height;
832 virtual string effect_type_id() const { return "SizeStoringEffect"; }
834 int input_width, input_height;
837 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
838 float data[2 * 2] = {
842 float out_data[2 * 2];
844 EffectChainTester tester(NULL, 4, 3); // Note non-square aspect.
847 format.color_space = COLORSPACE_sRGB;
848 format.gamma_curve = GAMMA_LINEAR;
850 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
851 input1->set_pixel_data(data);
853 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
854 input2->set_pixel_data(data);
856 SizeStoringEffect *input_store = new SizeStoringEffect();
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);
864 EXPECT_EQ(2, input_store->input_width);
865 EXPECT_EQ(2, input_store->input_height);
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,
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,
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];
889 EffectChainTester tester(NULL, 4, 3);
892 format.color_space = COLORSPACE_sRGB;
893 format.gamma_curve = GAMMA_LINEAR;
895 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
896 input1->set_pixel_data(data1);
898 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
899 input2->set_pixel_data(data2);
901 SizeStoringEffect *input_store = new SizeStoringEffect();
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);
909 EXPECT_EQ(9, input_store->input_width);
910 EXPECT_EQ(7, input_store->input_height);
913 // An effect that does nothing except changing its output sizes.
914 class VirtualResizeEffect : public Effect {
916 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
919 virtual_width(virtual_width),
920 virtual_height(virtual_height) {}
921 virtual string effect_type_id() const { return "VirtualResizeEffect"; }
922 string output_fragment_shader() { return read_file("identity.frag"); }
924 virtual bool changes_output_size() const { return true; }
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;
935 int width, height, virtual_width, virtual_height;
938 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
939 const int size = 2, bigger_size = 3;
940 float data[size * size] = {
944 float out_data[size * size];
946 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
948 SizeStoringEffect *size_store = new SizeStoringEffect();
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);
955 EXPECT_EQ(bigger_size, size_store->input_width);
956 EXPECT_EQ(bigger_size, size_store->input_height);
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);
963 // Does not use EffectChainTest, so that it can construct an EffectChain without
964 // a shared ResourcePool (which is also properly destroyed afterwards).
965 // Also turns on debugging to test that code path.
966 TEST(EffectChainTest, IdentityWithOwnPool) {
967 const int width = 3, height = 2;
972 const float expected_data[] = {
978 EffectChain chain(width, height);
979 movit_debug_level = MOVIT_DEBUG_ON;
982 format.color_space = COLORSPACE_sRGB;
983 format.gamma_curve = GAMMA_LINEAR;
985 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
986 input->set_pixel_data(data);
987 chain.add_input(input);
988 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
991 glGenTextures(1, &texnum);
993 glBindTexture(GL_TEXTURE_2D, texnum);
995 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
998 glGenFramebuffers(1, &fbo);
1000 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1002 glFramebufferTexture2D(
1004 GL_COLOR_ATTACHMENT0,
1009 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1014 chain.render_to_fbo(fbo, width, height);
1016 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1017 glReadPixels(0, 0, width, height, GL_RED, GL_FLOAT, out_data);
1019 expect_equal(expected_data, out_data, width, height);
1021 // Reset the debug status again.
1022 movit_debug_level = MOVIT_DEBUG_OFF;
1025 } // namespace movit