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"
13 #include "mirror_effect.h"
14 #include "multiply_effect.h"
15 #include "resize_effect.h"
16 #include "test_util.h"
21 TEST(EffectChainTest, EmptyChain) {
27 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
28 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
30 expect_equal(data, out_data, 3, 2);
33 // An effect that does nothing.
34 class IdentityEffect : public Effect {
37 virtual string effect_type_id() const { return "IdentityEffect"; }
38 string output_fragment_shader() { return read_file("identity.frag"); }
41 TEST(EffectChainTest, Identity) {
47 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
48 tester.get_chain()->add_effect(new IdentityEffect());
49 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
51 expect_equal(data, out_data, 3, 2);
54 // An effect that does nothing, but requests texture bounce.
55 class BouncingIdentityEffect : public Effect {
57 BouncingIdentityEffect() {}
58 virtual string effect_type_id() const { return "IdentityEffect"; }
59 string output_fragment_shader() { return read_file("identity.frag"); }
60 bool needs_texture_bounce() const { return true; }
61 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
64 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
70 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
71 tester.get_chain()->add_effect(new BouncingIdentityEffect());
72 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
74 expect_equal(data, out_data, 3, 2);
77 TEST(MirrorTest, BasicTest) {
82 float expected_data[6] = {
87 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
88 tester.get_chain()->add_effect(new MirrorEffect());
89 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
91 expect_equal(expected_data, out_data, 3, 2);
94 // A dummy effect that inverts its input.
95 class InvertEffect : public Effect {
98 virtual string effect_type_id() const { return "InvertEffect"; }
99 string output_fragment_shader() { return read_file("invert_effect.frag"); }
101 // A real invert would actually care about its alpha,
102 // but in this unit test, it only complicates things.
103 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
106 // Like IdentityEffect, but rewrites itself out of the loop,
107 // splicing in a different effect instead. Also stores the new node,
108 // so we later can check whatever properties we'd like about the graph.
110 class RewritingEffect : public Effect {
112 RewritingEffect() : effect(new T()), replaced_node(NULL) {}
113 virtual string effect_type_id() const { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
114 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
115 virtual void rewrite_graph(EffectChain *graph, Node *self) {
116 replaced_node = graph->add_node(effect);
117 graph->replace_receiver(self, replaced_node);
118 graph->replace_sender(self, replaced_node);
119 self->disabled = true;
126 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
131 float expected_data[6] = {
132 1.0f, 0.9771f, 0.9673f,
136 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
137 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
138 tester.get_chain()->add_effect(effect);
139 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
141 Node *node = effect->replaced_node;
142 ASSERT_EQ(1, node->incoming_links.size());
143 ASSERT_EQ(1, node->outgoing_links.size());
144 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
145 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
147 expect_equal(expected_data, out_data, 3, 2);
150 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
151 unsigned char data[] = {
155 float expected_data[4] = {
160 EffectChainTester tester(NULL, 2, 2);
161 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
162 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
163 tester.get_chain()->add_effect(effect);
164 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
166 Node *node = effect->replaced_node;
167 ASSERT_EQ(1, node->incoming_links.size());
168 ASSERT_EQ(1, node->outgoing_links.size());
169 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
170 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
172 expect_equal(expected_data, out_data, 2, 2);
175 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
180 float expected_data[6] = {
185 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
186 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
187 tester.get_chain()->add_effect(effect);
188 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
190 Node *node = effect->replaced_node;
191 ASSERT_EQ(1, node->incoming_links.size());
192 ASSERT_EQ(1, node->outgoing_links.size());
193 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
194 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
196 expect_equal(expected_data, out_data, 3, 2);
199 // A fake input that can change its output colorspace and gamma between instantiation
201 class UnknownColorspaceInput : public FlatInput {
203 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
204 : FlatInput(format, pixel_format, type, width, height),
205 overridden_color_space(format.color_space),
206 overridden_gamma_curve(format.gamma_curve) {}
207 virtual string effect_type_id() const { return "UnknownColorspaceInput"; }
209 void set_color_space(Colorspace colorspace) {
210 overridden_color_space = colorspace;
212 void set_gamma_curve(GammaCurve gamma_curve) {
213 overridden_gamma_curve = gamma_curve;
215 Colorspace get_color_space() const { return overridden_color_space; }
216 GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
219 Colorspace overridden_color_space;
220 GammaCurve overridden_gamma_curve;
223 TEST(EffectChainTester, HandlesInputChangingColorspace) {
232 float out_data[size];
234 EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
236 // First say that we have sRGB, linear input.
238 format.color_space = COLORSPACE_sRGB;
239 format.gamma_curve = GAMMA_LINEAR;
241 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
242 input->set_pixel_data(data);
243 tester.get_chain()->add_input(input);
245 // Now we change to Rec. 601 input.
246 input->set_color_space(COLORSPACE_REC_601_625);
247 input->set_gamma_curve(GAMMA_REC_601);
249 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
250 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
251 expect_equal(data, out_data, 4, 1);
254 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
259 float expected_data[6] = {
264 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
265 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
266 tester.get_chain()->add_effect(effect);
267 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
269 Node *node = effect->replaced_node;
270 ASSERT_EQ(1, node->incoming_links.size());
271 EXPECT_EQ(0, node->outgoing_links.size());
272 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
274 expect_equal(expected_data, out_data, 3, 2);
277 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
282 float expected_data[6] = {
287 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
288 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
289 tester.get_chain()->add_effect(effect);
290 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
292 Node *node = effect->replaced_node;
293 ASSERT_EQ(1, node->incoming_links.size());
294 EXPECT_EQ(0, node->outgoing_links.size());
295 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
297 expect_equal(expected_data, out_data, 3, 2);
300 // The identity effect needs linear light, and thus will get conversions on both sides.
301 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
302 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
304 for (unsigned i = 0; i < 256; ++i) {
308 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
309 tester.get_chain()->add_effect(new IdentityEffect());
310 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
312 expect_equal(data, out_data, 256, 1);
315 // Same, but uses the forward sRGB table from the GPU.
316 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
317 unsigned char data[256];
318 float expected_data[256];
319 for (unsigned i = 0; i < 256; ++i) {
321 expected_data[i] = i / 255.0;
324 EffectChainTester tester(NULL, 256, 1);
325 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
326 tester.get_chain()->add_effect(new IdentityEffect());
327 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
329 expect_equal(expected_data, out_data, 256, 1);
332 // Same, for the Rec. 601/709 gamma curve.
333 TEST(EffectChainTest, IdentityThroughRec709) {
335 for (unsigned i = 0; i < 256; ++i) {
339 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
340 tester.get_chain()->add_effect(new IdentityEffect());
341 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
343 expect_equal(data, out_data, 256, 1);
346 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
347 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
349 float data[4 * size] = {
350 0.8f, 0.0f, 0.0f, 0.5f,
351 0.0f, 0.2f, 0.2f, 0.3f,
352 0.1f, 0.0f, 1.0f, 1.0f,
354 float out_data[4 * size];
355 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
356 tester.get_chain()->add_effect(new IdentityEffect());
357 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
359 expect_equal(data, out_data, 4, size);
362 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
364 float data[4 * size] = {
365 0.8f, 0.0f, 0.0f, 0.5f,
366 0.0f, 0.2f, 0.2f, 0.3f,
367 0.1f, 0.0f, 1.0f, 1.0f,
369 float expected_data[4 * size] = {
370 0.1f, 0.0f, 1.0f, 1.0f,
371 0.0f, 0.2f, 0.2f, 0.3f,
372 0.8f, 0.0f, 0.0f, 0.5f,
374 float out_data[4 * size];
375 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
376 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
377 tester.get_chain()->add_effect(effect);
378 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
380 Node *node = effect->replaced_node;
381 ASSERT_EQ(1, node->incoming_links.size());
382 EXPECT_EQ(0, node->outgoing_links.size());
383 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
385 expect_equal(expected_data, out_data, 4, size);
388 // An input that outputs only blue, which has blank alpha.
389 class BlueInput : public Input {
391 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
392 virtual string effect_type_id() const { return "IdentityEffect"; }
393 string output_fragment_shader() { return read_file("blue.frag"); }
394 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
395 virtual void finalize() {}
396 virtual bool can_output_linear_gamma() const { return true; }
397 virtual unsigned get_width() const { return 1; }
398 virtual unsigned get_height() const { return 1; }
399 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
400 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
406 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
407 // which outputs blank alpha.
408 class RewritingToBlueInput : public Input {
410 RewritingToBlueInput() : blue_node(NULL) { register_int("needs_mipmaps", &needs_mipmaps); }
411 virtual string effect_type_id() const { return "RewritingToBlueInput"; }
412 string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
413 virtual void rewrite_graph(EffectChain *graph, Node *self) {
414 Node *blue_node = graph->add_node(new BlueInput());
415 graph->replace_receiver(self, blue_node);
416 graph->replace_sender(self, blue_node);
418 self->disabled = true;
419 this->blue_node = blue_node;
422 // Dummy values that we need to implement because we inherit from Input.
423 // Same as BlueInput.
424 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
425 virtual void finalize() {}
426 virtual bool can_output_linear_gamma() const { return true; }
427 virtual unsigned get_width() const { return 1; }
428 virtual unsigned get_height() const { return 1; }
429 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
430 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
438 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
440 float data[4 * size] = {
441 0.0f, 0.0f, 1.0f, 1.0f,
442 0.0f, 0.0f, 1.0f, 1.0f,
443 0.0f, 0.0f, 1.0f, 1.0f,
445 float out_data[4 * size];
446 EffectChainTester tester(NULL, size, 1);
447 RewritingToBlueInput *input = new RewritingToBlueInput();
448 tester.get_chain()->add_input(input);
449 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
451 Node *node = input->blue_node;
452 EXPECT_EQ(0, node->incoming_links.size());
453 EXPECT_EQ(0, node->outgoing_links.size());
455 expect_equal(data, out_data, 4, size);
458 // An effect that does nothing, and specifies that it preserves blank alpha.
459 class BlankAlphaPreservingEffect : public Effect {
461 BlankAlphaPreservingEffect() {}
462 virtual string effect_type_id() const { return "BlankAlphaPreservingEffect"; }
463 string output_fragment_shader() { return read_file("identity.frag"); }
464 virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
467 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
469 float data[4 * size] = {
470 0.0f, 0.0f, 1.0f, 1.0f,
471 0.0f, 0.0f, 1.0f, 1.0f,
472 0.0f, 0.0f, 1.0f, 1.0f,
474 float out_data[4 * size];
475 EffectChainTester tester(NULL, size, 1);
476 tester.get_chain()->add_input(new BlueInput());
477 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
478 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
479 tester.get_chain()->add_effect(effect);
480 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
482 Node *node = effect->replaced_node;
483 EXPECT_EQ(1, node->incoming_links.size());
484 EXPECT_EQ(0, node->outgoing_links.size());
486 expect_equal(data, out_data, 4, size);
489 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
490 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
491 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
492 // with other tests.)
493 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
495 float data[4 * size] = {
496 0.0f, 0.0f, 1.0f, 1.0f,
497 0.0f, 0.0f, 1.0f, 1.0f,
498 0.0f, 0.0f, 1.0f, 1.0f,
500 float out_data[4 * size];
501 EffectChainTester tester(NULL, size, 1);
502 tester.get_chain()->add_input(new BlueInput());
503 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
504 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
505 tester.get_chain()->add_effect(effect);
506 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
508 Node *node = effect->replaced_node;
509 EXPECT_EQ(1, node->incoming_links.size());
510 EXPECT_EQ(1, node->outgoing_links.size());
511 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
513 expect_equal(data, out_data, 4, size);
516 // Effectively scales down its input linearly by 4x (and repeating it),
517 // which is not attainable without mipmaps.
518 class MipmapNeedingEffect : public Effect {
520 MipmapNeedingEffect() {}
521 virtual bool needs_mipmaps() const { return true; }
522 virtual string effect_type_id() const { return "MipmapNeedingEffect"; }
523 string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
524 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num)
526 glActiveTexture(GL_TEXTURE0);
528 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
530 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
535 TEST(EffectChainTest, MipmapGenerationWorks) {
536 float data[] = { // In 4x4 blocks.
537 1.0f, 0.0f, 0.0f, 0.0f,
538 0.0f, 0.0f, 0.0f, 0.0f,
539 0.0f, 0.0f, 0.0f, 0.0f,
540 0.0f, 0.0f, 0.0f, 1.0f,
542 0.0f, 0.0f, 0.0f, 0.0f,
543 0.0f, 0.5f, 0.0f, 0.0f,
544 0.0f, 0.0f, 1.0f, 0.0f,
545 0.0f, 0.0f, 0.0f, 0.0f,
547 1.0f, 1.0f, 1.0f, 1.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,
552 0.0f, 0.0f, 0.0f, 0.0f,
553 0.0f, 1.0f, 1.0f, 0.0f,
554 0.0f, 1.0f, 1.0f, 0.0f,
555 0.0f, 0.0f, 0.0f, 0.0f,
557 float expected_data[] = { // Repeated four times each way.
558 0.125f, 0.125f, 0.125f, 0.125f,
559 0.09375f, 0.09375f, 0.09375f, 0.09375f,
560 1.0f, 1.0f, 1.0f, 1.0f,
561 0.25f, 0.25f, 0.25f, 0.25f,
563 0.125f, 0.125f, 0.125f, 0.125f,
564 0.09375f, 0.09375f, 0.09375f, 0.09375f,
565 1.0f, 1.0f, 1.0f, 1.0f,
566 0.25f, 0.25f, 0.25f, 0.25f,
568 0.125f, 0.125f, 0.125f, 0.125f,
569 0.09375f, 0.09375f, 0.09375f, 0.09375f,
570 1.0f, 1.0f, 1.0f, 1.0f,
571 0.25f, 0.25f, 0.25f, 0.25f,
573 0.125f, 0.125f, 0.125f, 0.125f,
574 0.09375f, 0.09375f, 0.09375f, 0.09375f,
575 1.0f, 1.0f, 1.0f, 1.0f,
576 0.25f, 0.25f, 0.25f, 0.25f,
578 float out_data[4 * 16];
579 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
580 tester.get_chain()->add_effect(new MipmapNeedingEffect());
581 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
583 expect_equal(expected_data, out_data, 4, 16);
586 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
587 float data[] = { // In 4x4 blocks.
588 1.0f, 0.0f, 0.0f, 0.0f,
589 0.0f, 0.0f, 0.0f, 0.0f,
590 0.0f, 0.0f, 0.0f, 0.0f,
591 0.0f, 0.0f, 0.0f, 1.0f,
593 0.0f, 0.0f, 0.0f, 0.0f,
594 0.0f, 0.5f, 0.0f, 0.0f,
595 0.0f, 0.0f, 1.0f, 0.0f,
596 0.0f, 0.0f, 0.0f, 0.0f,
598 1.0f, 1.0f, 1.0f, 1.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,
603 0.0f, 0.0f, 0.0f, 0.0f,
604 0.0f, 1.0f, 1.0f, 0.0f,
605 0.0f, 1.0f, 1.0f, 0.0f,
606 0.0f, 0.0f, 0.0f, 0.0f,
608 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
609 0.1250f, 0.1250f, 0.1250f, 0.1250f,
610 0.1250f, 0.1250f, 0.1250f, 0.1250f,
611 0.1211f, 0.1211f, 0.1211f, 0.1211f,
612 0.1133f, 0.1133f, 0.1133f, 0.1133f,
613 0.1055f, 0.1055f, 0.1055f, 0.1055f,
614 0.0977f, 0.0977f, 0.0977f, 0.0977f,
615 0.2070f, 0.2070f, 0.2070f, 0.2070f,
616 0.4336f, 0.4336f, 0.4336f, 0.4336f,
617 0.6602f, 0.6602f, 0.6602f, 0.6602f,
618 0.8867f, 0.8867f, 0.8867f, 0.8867f,
619 0.9062f, 0.9062f, 0.9062f, 0.9062f,
620 0.7188f, 0.7188f, 0.7188f, 0.7188f,
621 0.5312f, 0.5312f, 0.5312f, 0.5312f,
622 0.3438f, 0.3438f, 0.3438f, 0.3438f,
623 0.2500f, 0.2500f, 0.2500f, 0.2500f,
624 0.2500f, 0.2500f, 0.2500f, 0.2500f,
626 float out_data[4 * 16];
628 ResizeEffect *downscale = new ResizeEffect();
629 ASSERT_TRUE(downscale->set_int("width", 1));
630 ASSERT_TRUE(downscale->set_int("height", 4));
632 ResizeEffect *upscale = new ResizeEffect();
633 ASSERT_TRUE(upscale->set_int("width", 4));
634 ASSERT_TRUE(upscale->set_int("height", 16));
636 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
637 tester.get_chain()->add_effect(downscale);
638 tester.get_chain()->add_effect(upscale);
639 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
641 expect_equal(expected_data, out_data, 4, 16);
644 // An effect that adds its two inputs together. Used below.
645 class AddEffect : public Effect {
648 virtual string effect_type_id() const { return "AddEffect"; }
649 string output_fragment_shader() { return read_file("add.frag"); }
650 virtual unsigned num_inputs() const { return 2; }
651 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
654 // Constructs the graph
658 // MultiplyEffect MultiplyEffect |
662 // and verifies that it gives the correct output.
663 TEST(EffectChainTest, DiamondGraph) {
668 float expected_data[] = {
672 float out_data[2 * 2];
674 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
675 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
677 MultiplyEffect *mul_half = new MultiplyEffect();
678 ASSERT_TRUE(mul_half->set_vec4("factor", half));
680 MultiplyEffect *mul_two = new MultiplyEffect();
681 ASSERT_TRUE(mul_two->set_vec4("factor", two));
683 EffectChainTester tester(NULL, 2, 2);
686 format.color_space = COLORSPACE_sRGB;
687 format.gamma_curve = GAMMA_LINEAR;
689 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
690 input->set_pixel_data(data);
692 tester.get_chain()->add_input(input);
693 tester.get_chain()->add_effect(mul_half, input);
694 tester.get_chain()->add_effect(mul_two, input);
695 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
696 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
698 expect_equal(expected_data, out_data, 2, 2);
701 // Constructs the graph
705 // MultiplyEffect MultiplyEffect |
707 // \ BouncingIdentityEffect |
711 // and verifies that it gives the correct output.
712 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
717 float expected_data[] = {
721 float out_data[2 * 2];
723 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
724 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
726 MultiplyEffect *mul_half = new MultiplyEffect();
727 ASSERT_TRUE(mul_half->set_vec4("factor", half));
729 MultiplyEffect *mul_two = new MultiplyEffect();
730 ASSERT_TRUE(mul_two->set_vec4("factor", two));
732 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
734 EffectChainTester tester(NULL, 2, 2);
737 format.color_space = COLORSPACE_sRGB;
738 format.gamma_curve = GAMMA_LINEAR;
740 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
741 input->set_pixel_data(data);
743 tester.get_chain()->add_input(input);
744 tester.get_chain()->add_effect(mul_half, input);
745 tester.get_chain()->add_effect(mul_two, input);
746 tester.get_chain()->add_effect(bounce, mul_two);
747 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
748 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
750 expect_equal(expected_data, out_data, 2, 2);
753 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
758 float expected_data[] = {
759 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
762 float out_data[2 * 2];
764 EffectChainTester tester(NULL, 2, 2);
765 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
767 // MirrorEffect does not get linear light, so the conversions will be
768 // inserted after it, not before.
769 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
770 tester.get_chain()->add_effect(effect);
772 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
773 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
774 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
775 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
777 expect_equal(expected_data, out_data, 2, 2);
779 Node *node = effect->replaced_node;
780 ASSERT_EQ(1, node->incoming_links.size());
781 ASSERT_EQ(1, node->outgoing_links.size());
782 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
783 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
786 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
791 float expected_data[] = {
792 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
795 float out_data[2 * 2];
797 EffectChainTester tester(NULL, 2, 2);
798 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
800 // MirrorEffect does not get linear light, so the conversions will be
801 // inserted after it, not before.
802 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
803 tester.get_chain()->add_effect(effect);
805 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
806 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
807 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
808 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
810 expect_equal(expected_data, out_data, 2, 2);
812 Node *node = effect->replaced_node;
813 ASSERT_EQ(1, node->incoming_links.size());
814 ASSERT_EQ(1, node->outgoing_links.size());
815 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
816 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
819 // An effect that does nothing, but requests texture bounce and stores
821 class SizeStoringEffect : public BouncingIdentityEffect {
823 SizeStoringEffect() : input_width(-1), input_height(-1) {}
824 virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height) {
825 assert(input_num == 0);
827 input_height = height;
829 virtual string effect_type_id() const { return "SizeStoringEffect"; }
831 int input_width, input_height;
834 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
835 float data[2 * 2] = {
839 float out_data[2 * 2];
841 EffectChainTester tester(NULL, 4, 3); // Note non-square aspect.
844 format.color_space = COLORSPACE_sRGB;
845 format.gamma_curve = GAMMA_LINEAR;
847 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
848 input1->set_pixel_data(data);
850 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
851 input2->set_pixel_data(data);
853 SizeStoringEffect *input_store = new SizeStoringEffect();
855 tester.get_chain()->add_input(input1);
856 tester.get_chain()->add_input(input2);
857 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
858 tester.get_chain()->add_effect(input_store);
859 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
861 EXPECT_EQ(2, input_store->input_width);
862 EXPECT_EQ(2, input_store->input_height);
865 TEST(EffectChainTest, AspectRatioConversion) {
866 float data1[4 * 3] = {
867 0.0f, 0.0f, 0.0f, 0.0f,
868 0.0f, 0.0f, 0.0f, 0.0f,
869 0.0f, 0.0f, 0.0f, 0.0f,
871 float data2[7 * 7] = {
872 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
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, 1.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, 0.0f, 0.0f, 0.0f, 0.0f,
881 // The right conversion here is that the 7x7 image decides the size,
882 // since it is the biggest, so everything is scaled up to 9x7
883 // (keep the height, round the width 9.333 to 9).
884 float out_data[9 * 7];
886 EffectChainTester tester(NULL, 4, 3);
889 format.color_space = COLORSPACE_sRGB;
890 format.gamma_curve = GAMMA_LINEAR;
892 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
893 input1->set_pixel_data(data1);
895 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
896 input2->set_pixel_data(data2);
898 SizeStoringEffect *input_store = new SizeStoringEffect();
900 tester.get_chain()->add_input(input1);
901 tester.get_chain()->add_input(input2);
902 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
903 tester.get_chain()->add_effect(input_store);
904 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
906 EXPECT_EQ(9, input_store->input_width);
907 EXPECT_EQ(7, input_store->input_height);
910 // An effect that does nothing except changing its output sizes.
911 class VirtualResizeEffect : public Effect {
913 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
916 virtual_width(virtual_width),
917 virtual_height(virtual_height) {}
918 virtual string effect_type_id() const { return "VirtualResizeEffect"; }
919 string output_fragment_shader() { return read_file("identity.frag"); }
921 virtual bool changes_output_size() const { return true; }
923 virtual void get_output_size(unsigned *width, unsigned *height,
924 unsigned *virtual_width, unsigned *virtual_height) const {
925 *width = this->width;
926 *height = this->height;
927 *virtual_width = this->virtual_width;
928 *virtual_height = this->virtual_height;
932 int width, height, virtual_width, virtual_height;
935 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
936 const int size = 2, bigger_size = 3;
937 float data[size * size] = {
941 float out_data[size * size];
943 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
945 SizeStoringEffect *size_store = new SizeStoringEffect();
947 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
948 tester.get_chain()->add_effect(size_store);
949 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
950 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
952 EXPECT_EQ(bigger_size, size_store->input_width);
953 EXPECT_EQ(bigger_size, size_store->input_height);
955 // If the resize is implemented as non-virtual, we'll fail here,
956 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
957 expect_equal(data, out_data, size, size);
960 extern bool movit_initialized;
962 // Does not use EffectChainTest, so that it can construct an EffectChain without
963 // a shared ResourcePool (which is also properly destroyed afterwards).
964 // Also turns on debugging to test that code path.
965 TEST(EffectChainTest, IdentityWithOwnPool) {
966 const int width = 3, height = 2;
971 const float expected_data[] = {
977 EffectChain chain(width, height);
978 movit_initialized = false;
979 init_movit(".", 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_initialized = false;
1023 init_movit(".", MOVIT_DEBUG_OFF);