1 // Unit tests for EffectChain.
3 // Note that this also contains the tests for some of the simpler effects.
7 #include "effect_chain.h"
8 #include "flat_input.h"
9 #include "gtest/gtest.h"
10 #include "mirror_effect.h"
11 #include "resize_effect.h"
12 #include "test_util.h"
14 TEST(EffectChainTest, EmptyChain) {
20 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
21 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
23 expect_equal(data, out_data, 3, 2);
26 // An effect that does nothing.
27 class IdentityEffect : public Effect {
30 virtual std::string effect_type_id() const { return "IdentityEffect"; }
31 std::string output_fragment_shader() { return read_file("identity.frag"); }
34 TEST(EffectChainTest, Identity) {
40 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
41 tester.get_chain()->add_effect(new IdentityEffect());
42 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
44 expect_equal(data, out_data, 3, 2);
47 // An effect that does nothing, but requests texture bounce.
48 class BouncingIdentityEffect : public Effect {
50 BouncingIdentityEffect() {}
51 virtual std::string effect_type_id() const { return "IdentityEffect"; }
52 std::string output_fragment_shader() { return read_file("identity.frag"); }
53 bool needs_texture_bounce() const { return true; }
56 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
62 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
63 tester.get_chain()->add_effect(new BouncingIdentityEffect());
64 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
66 expect_equal(data, out_data, 3, 2);
69 TEST(MirrorTest, BasicTest) {
74 float expected_data[6] = {
79 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
80 tester.get_chain()->add_effect(new MirrorEffect());
81 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
83 expect_equal(expected_data, out_data, 3, 2);
86 // A dummy effect that inverts its input.
87 class InvertEffect : public Effect {
90 virtual std::string effect_type_id() const { return "InvertEffect"; }
91 std::string output_fragment_shader() { return read_file("invert_effect.frag"); }
93 // A real invert would actually care about its alpha,
94 // but in this unit test, it only complicates things.
95 virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
98 // Like IdentityEffect, but rewrites itself out of the loop,
99 // splicing in a InvertEffect instead. Also stores the new node,
100 // so we later can check that there are gamma conversion effects
102 class RewritingToInvertEffect : public Effect {
104 RewritingToInvertEffect() {}
105 virtual std::string effect_type_id() const { return "RewritingToInvertEffect"; }
106 std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
107 virtual void rewrite_graph(EffectChain *graph, Node *self) {
108 Node *invert_node = graph->add_node(new InvertEffect());
109 graph->replace_receiver(self, invert_node);
110 graph->replace_sender(self, invert_node);
112 self->disabled = true;
113 this->invert_node = invert_node;
119 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
124 float expected_data[6] = {
125 1.0f, 0.9771f, 0.9673f,
129 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
130 RewritingToInvertEffect *effect = new RewritingToInvertEffect();
131 tester.get_chain()->add_effect(effect);
132 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
134 Node *node = effect->invert_node;
135 ASSERT_EQ(1, node->incoming_links.size());
136 ASSERT_EQ(1, node->outgoing_links.size());
137 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
138 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
140 expect_equal(expected_data, out_data, 3, 2);
143 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
144 unsigned char data[] = {
148 float expected_data[4] = {
153 EffectChainTester tester(NULL, 2, 2);
154 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
155 RewritingToInvertEffect *effect = new RewritingToInvertEffect();
156 tester.get_chain()->add_effect(effect);
157 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
159 Node *node = effect->invert_node;
160 ASSERT_EQ(1, node->incoming_links.size());
161 ASSERT_EQ(1, node->outgoing_links.size());
162 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
163 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
165 expect_equal(expected_data, out_data, 2, 2);
168 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
173 float expected_data[6] = {
178 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
179 RewritingToInvertEffect *effect = new RewritingToInvertEffect();
180 tester.get_chain()->add_effect(effect);
181 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
183 Node *node = effect->invert_node;
184 ASSERT_EQ(1, node->incoming_links.size());
185 ASSERT_EQ(1, node->outgoing_links.size());
186 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
187 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
189 expect_equal(expected_data, out_data, 3, 2);
192 // A fake input that can change its output colorspace and gamma between instantiation
194 class UnknownColorspaceInput : public FlatInput {
196 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
197 : FlatInput(format, pixel_format, type, width, height),
198 overridden_color_space(format.color_space),
199 overridden_gamma_curve(format.gamma_curve) {}
200 virtual std::string effect_type_id() const { return "UnknownColorspaceInput"; }
202 void set_color_space(Colorspace colorspace) {
203 overridden_color_space = colorspace;
205 void set_gamma_curve(GammaCurve gamma_curve) {
206 overridden_gamma_curve = gamma_curve;
208 Colorspace get_color_space() const { return overridden_color_space; }
209 GammaCurve get_gamma_curve() const { return overridden_gamma_curve; }
212 Colorspace overridden_color_space;
213 GammaCurve overridden_gamma_curve;
216 TEST(EffectChainTester, HandlesInputChangingColorspace) {
225 float out_data[size];
227 EffectChainTester tester(NULL, 4, 1, FORMAT_GRAYSCALE);
229 // First say that we have sRGB, linear input.
231 format.color_space = COLORSPACE_sRGB;
232 format.gamma_curve = GAMMA_LINEAR;
234 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
235 input->set_pixel_data(data);
236 tester.get_chain()->add_input(input);
238 // Now we change to Rec. 601 input.
239 input->set_color_space(COLORSPACE_REC_601_625);
240 input->set_gamma_curve(GAMMA_REC_601);
242 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
243 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
244 expect_equal(data, out_data, 4, 1);
247 // Like RewritingToInvertEffect, but splicing in a MirrorEffect instead,
248 // which does not need linear light or sRGB primaries.
249 class RewritingToMirrorEffect : public Effect {
251 RewritingToMirrorEffect() {}
252 virtual std::string effect_type_id() const { return "RewritingToMirrorEffect"; }
253 std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
254 virtual void rewrite_graph(EffectChain *graph, Node *self) {
255 Node *mirror_node = graph->add_node(new MirrorEffect());
256 graph->replace_receiver(self, mirror_node);
257 graph->replace_sender(self, mirror_node);
259 self->disabled = true;
260 this->mirror_node = mirror_node;
266 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
271 float expected_data[6] = {
276 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
277 RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
278 tester.get_chain()->add_effect(effect);
279 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
281 Node *node = effect->mirror_node;
282 ASSERT_EQ(1, node->incoming_links.size());
283 EXPECT_EQ(0, node->outgoing_links.size());
284 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
286 expect_equal(expected_data, out_data, 3, 2);
289 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
294 float expected_data[6] = {
299 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
300 RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
301 tester.get_chain()->add_effect(effect);
302 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
304 Node *node = effect->mirror_node;
305 ASSERT_EQ(1, node->incoming_links.size());
306 EXPECT_EQ(0, node->outgoing_links.size());
307 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
309 expect_equal(expected_data, out_data, 3, 2);
312 // The identity effect needs linear light, and thus will get conversions on both sides.
313 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
314 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
316 for (unsigned i = 0; i < 256; ++i) {
320 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
321 tester.get_chain()->add_effect(new IdentityEffect());
322 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
324 expect_equal(data, out_data, 256, 1);
327 // Same, but uses the forward sRGB table from the GPU.
328 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
329 unsigned char data[256];
330 float expected_data[256];
331 for (unsigned i = 0; i < 256; ++i) {
333 expected_data[i] = i / 255.0;
336 EffectChainTester tester(NULL, 256, 1);
337 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
338 tester.get_chain()->add_effect(new IdentityEffect());
339 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
341 expect_equal(expected_data, out_data, 256, 1);
344 // Same, for the Rec. 601/709 gamma curve.
345 TEST(EffectChainTest, IdentityThroughRec709) {
347 for (unsigned i = 0; i < 256; ++i) {
351 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
352 tester.get_chain()->add_effect(new IdentityEffect());
353 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
355 expect_equal(data, out_data, 256, 1);
358 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
359 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
361 float data[4 * size] = {
362 0.8f, 0.0f, 0.0f, 0.5f,
363 0.0f, 0.2f, 0.2f, 0.3f,
364 0.1f, 0.0f, 1.0f, 1.0f,
367 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
368 tester.get_chain()->add_effect(new IdentityEffect());
369 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
371 expect_equal(data, out_data, 4, size);
374 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
376 float data[4 * size] = {
377 0.8f, 0.0f, 0.0f, 0.5f,
378 0.0f, 0.2f, 0.2f, 0.3f,
379 0.1f, 0.0f, 1.0f, 1.0f,
381 float expected_data[4 * size] = {
382 0.1f, 0.0f, 1.0f, 1.0f,
383 0.0f, 0.2f, 0.2f, 0.3f,
384 0.8f, 0.0f, 0.0f, 0.5f,
386 float out_data[4 * size];
387 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
388 RewritingToMirrorEffect *effect = new RewritingToMirrorEffect();
389 tester.get_chain()->add_effect(effect);
390 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
392 Node *node = effect->mirror_node;
393 ASSERT_EQ(1, node->incoming_links.size());
394 EXPECT_EQ(0, node->outgoing_links.size());
395 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
397 expect_equal(expected_data, out_data, 4, size);
400 // An input that outputs only blue, which has blank alpha.
401 class BlueInput : public Input {
403 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
404 virtual std::string effect_type_id() const { return "IdentityEffect"; }
405 std::string output_fragment_shader() { return read_file("blue.frag"); }
406 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
407 virtual void finalize() {}
408 virtual bool can_output_linear_gamma() const { return true; }
409 virtual unsigned get_width() const { return 1; }
410 virtual unsigned get_height() const { return 1; }
411 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
412 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
418 // Like RewritingToInvertEffect, but splicing in a BlueInput instead,
419 // which outputs blank alpha.
420 class RewritingToBlueInput : public Input {
422 RewritingToBlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
423 virtual std::string effect_type_id() const { return "RewritingToBlueInput"; }
424 std::string output_fragment_shader() { EXPECT_TRUE(false); return read_file("identity.frag"); }
425 virtual void rewrite_graph(EffectChain *graph, Node *self) {
426 Node *blue_node = graph->add_node(new BlueInput());
427 graph->replace_receiver(self, blue_node);
428 graph->replace_sender(self, blue_node);
430 self->disabled = true;
431 this->blue_node = blue_node;
434 // Dummy values that we need to implement because we inherit from Input.
435 // Same as BlueInput.
436 virtual AlphaHandling alpha_handling() const { return OUTPUT_BLANK_ALPHA; }
437 virtual void finalize() {}
438 virtual bool can_output_linear_gamma() const { return true; }
439 virtual unsigned get_width() const { return 1; }
440 virtual unsigned get_height() const { return 1; }
441 virtual Colorspace get_color_space() const { return COLORSPACE_sRGB; }
442 virtual GammaCurve get_gamma_curve() const { return GAMMA_LINEAR; }
450 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
452 float data[4 * size] = {
453 0.0f, 0.0f, 1.0f, 1.0f,
454 0.0f, 0.0f, 1.0f, 1.0f,
455 0.0f, 0.0f, 1.0f, 1.0f,
457 float out_data[4 * size];
458 EffectChainTester tester(NULL, size, 1);
459 RewritingToBlueInput *input = new RewritingToBlueInput();
460 tester.get_chain()->add_input(input);
461 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_PREMULTIPLIED);
463 Node *node = input->blue_node;
464 EXPECT_EQ(0, node->incoming_links.size());
465 EXPECT_EQ(0, node->outgoing_links.size());
467 expect_equal(data, out_data, 4, size);
470 // Effectively scales down its input linearly by 4x (and repeating it),
471 // which is not attainable without mipmaps.
472 class MipmapNeedingEffect : public Effect {
474 MipmapNeedingEffect() {}
475 virtual bool needs_mipmaps() const { return true; }
476 virtual std::string effect_type_id() const { return "MipmapNeedingEffect"; }
477 std::string output_fragment_shader() { return read_file("mipmap_needing_effect.frag"); }
478 void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num)
480 glActiveTexture(GL_TEXTURE0);
482 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
484 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
489 TEST(EffectChainTest, MipmapGenerationWorks) {
490 float data[] = { // In 4x4 blocks.
491 1.0f, 0.0f, 0.0f, 0.0f,
492 0.0f, 0.0f, 0.0f, 0.0f,
493 0.0f, 0.0f, 0.0f, 0.0f,
494 0.0f, 0.0f, 0.0f, 1.0f,
496 0.0f, 0.0f, 0.0f, 0.0f,
497 0.0f, 0.5f, 0.0f, 0.0f,
498 0.0f, 0.0f, 1.0f, 0.0f,
499 0.0f, 0.0f, 0.0f, 0.0f,
501 1.0f, 1.0f, 1.0f, 1.0f,
502 1.0f, 1.0f, 1.0f, 1.0f,
503 1.0f, 1.0f, 1.0f, 1.0f,
504 1.0f, 1.0f, 1.0f, 1.0f,
506 0.0f, 0.0f, 0.0f, 0.0f,
507 0.0f, 1.0f, 1.0f, 0.0f,
508 0.0f, 1.0f, 1.0f, 0.0f,
509 0.0f, 0.0f, 0.0f, 0.0f,
511 float expected_data[] = { // Repeated four times each way.
512 0.125f, 0.125f, 0.125f, 0.125f,
513 0.09375f, 0.09375f, 0.09375f, 0.09375f,
514 1.0f, 1.0f, 1.0f, 1.0f,
515 0.25f, 0.25f, 0.25f, 0.25f,
517 0.125f, 0.125f, 0.125f, 0.125f,
518 0.09375f, 0.09375f, 0.09375f, 0.09375f,
519 1.0f, 1.0f, 1.0f, 1.0f,
520 0.25f, 0.25f, 0.25f, 0.25f,
522 0.125f, 0.125f, 0.125f, 0.125f,
523 0.09375f, 0.09375f, 0.09375f, 0.09375f,
524 1.0f, 1.0f, 1.0f, 1.0f,
525 0.25f, 0.25f, 0.25f, 0.25f,
527 0.125f, 0.125f, 0.125f, 0.125f,
528 0.09375f, 0.09375f, 0.09375f, 0.09375f,
529 1.0f, 1.0f, 1.0f, 1.0f,
530 0.25f, 0.25f, 0.25f, 0.25f,
532 float out_data[4 * 16];
533 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
534 tester.get_chain()->add_effect(new MipmapNeedingEffect());
535 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
537 expect_equal(expected_data, out_data, 4, 16);
540 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
541 float data[] = { // In 4x4 blocks.
542 1.0f, 0.0f, 0.0f, 0.0f,
543 0.0f, 0.0f, 0.0f, 0.0f,
544 0.0f, 0.0f, 0.0f, 0.0f,
545 0.0f, 0.0f, 0.0f, 1.0f,
547 0.0f, 0.0f, 0.0f, 0.0f,
548 0.0f, 0.5f, 0.0f, 0.0f,
549 0.0f, 0.0f, 1.0f, 0.0f,
550 0.0f, 0.0f, 0.0f, 0.0f,
552 1.0f, 1.0f, 1.0f, 1.0f,
553 1.0f, 1.0f, 1.0f, 1.0f,
554 1.0f, 1.0f, 1.0f, 1.0f,
555 1.0f, 1.0f, 1.0f, 1.0f,
557 0.0f, 0.0f, 0.0f, 0.0f,
558 0.0f, 1.0f, 1.0f, 0.0f,
559 0.0f, 1.0f, 1.0f, 0.0f,
560 0.0f, 0.0f, 0.0f, 0.0f,
562 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
563 0.1250f, 0.1250f, 0.1250f, 0.1250f,
564 0.1250f, 0.1250f, 0.1250f, 0.1250f,
565 0.1211f, 0.1211f, 0.1211f, 0.1211f,
566 0.1133f, 0.1133f, 0.1133f, 0.1133f,
567 0.1055f, 0.1055f, 0.1055f, 0.1055f,
568 0.0977f, 0.0977f, 0.0977f, 0.0977f,
569 0.2070f, 0.2070f, 0.2070f, 0.2070f,
570 0.4336f, 0.4336f, 0.4336f, 0.4336f,
571 0.6602f, 0.6602f, 0.6602f, 0.6602f,
572 0.8867f, 0.8867f, 0.8867f, 0.8867f,
573 0.9062f, 0.9062f, 0.9062f, 0.9062f,
574 0.7188f, 0.7188f, 0.7188f, 0.7188f,
575 0.5312f, 0.5312f, 0.5312f, 0.5312f,
576 0.3438f, 0.3438f, 0.3438f, 0.3438f,
577 0.2500f, 0.2500f, 0.2500f, 0.2500f,
578 0.2500f, 0.2500f, 0.2500f, 0.2500f,
580 float out_data[4 * 16];
582 ResizeEffect *downscale = new ResizeEffect();
583 ASSERT_TRUE(downscale->set_int("width", 1));
584 ASSERT_TRUE(downscale->set_int("height", 4));
586 ResizeEffect *upscale = new ResizeEffect();
587 ASSERT_TRUE(upscale->set_int("width", 4));
588 ASSERT_TRUE(upscale->set_int("height", 16));
590 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
591 tester.get_chain()->add_effect(downscale);
592 tester.get_chain()->add_effect(upscale);
593 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
595 expect_equal(expected_data, out_data, 4, 16);