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 "resource_pool.h"
22 #include "test_util.h"
29 TEST(EffectChainTest, EmptyChain) {
35 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
36 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
38 expect_equal(data, out_data, 3, 2);
41 // An effect that does nothing.
42 class IdentityEffect : public Effect {
45 string effect_type_id() const override { return "IdentityEffect"; }
46 string output_fragment_shader() override { return read_file("identity.frag"); }
49 TEST(EffectChainTest, Identity) {
55 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
56 tester.get_chain()->add_effect(new IdentityEffect());
57 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
59 expect_equal(data, out_data, 3, 2);
62 // An effect that does nothing, but requests texture bounce.
63 class BouncingIdentityEffect : public Effect {
65 BouncingIdentityEffect() {}
66 string effect_type_id() const override { return "IdentityEffect"; }
67 string output_fragment_shader() override { return read_file("identity.frag"); }
68 bool needs_texture_bounce() const override { return true; }
69 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
72 TEST(EffectChainTest, TextureBouncePreservesIdentity) {
78 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
79 tester.get_chain()->add_effect(new BouncingIdentityEffect());
80 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
82 expect_equal(data, out_data, 3, 2);
85 TEST(MirrorTest, BasicTest) {
90 float expected_data[6] = {
95 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
96 tester.get_chain()->add_effect(new MirrorEffect());
97 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
99 expect_equal(expected_data, out_data, 3, 2);
102 TEST(EffectChainTest, TopLeftOrigin) {
107 // Note that EffectChainTester assumes bottom-left origin, so by setting
108 // top-left, we will get flipped data back.
109 float expected_data[6] = {
114 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
115 tester.get_chain()->set_output_origin(OUTPUT_ORIGIN_TOP_LEFT);
116 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
118 expect_equal(expected_data, out_data, 3, 2);
121 // A dummy effect that inverts its input.
122 class InvertEffect : public Effect {
125 string effect_type_id() const override { return "InvertEffect"; }
126 string output_fragment_shader() override { return read_file("invert_effect.frag"); }
128 // A real invert would actually care about its alpha,
129 // but in this unit test, it only complicates things.
130 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
133 // Like IdentityEffect, but rewrites itself out of the loop,
134 // splicing in a different effect instead. Also stores the new node,
135 // so we later can check whatever properties we'd like about the graph.
137 class RewritingEffect : public Effect {
139 RewritingEffect() : effect(new T()), replaced_node(nullptr) {}
140 string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
141 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
142 void rewrite_graph(EffectChain *graph, Node *self) override {
143 replaced_node = graph->add_node(effect);
144 graph->replace_receiver(self, replaced_node);
145 graph->replace_sender(self, replaced_node);
146 self->disabled = true;
153 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
158 float expected_data[6] = {
159 1.0f, 0.9771f, 0.9673f,
163 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
164 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
165 tester.get_chain()->add_effect(effect);
166 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
168 Node *node = effect->replaced_node;
169 ASSERT_EQ(1, node->incoming_links.size());
170 ASSERT_EQ(1, node->outgoing_links.size());
171 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
172 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
174 expect_equal(expected_data, out_data, 3, 2);
177 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
178 unsigned char data[] = {
184 float expected_data[] = {
185 1.0000f, 1.0000f, 1.0000f, 1.0000f,
186 0.9771f, 0.9771f, 0.9771f, 1.0000f,
187 0.8983f, 0.8983f, 0.8983f, 1.0000f,
188 0.0000f, 0.0000f, 0.0000f, 1.0000f
190 float out_data[4 * 4];
191 EffectChainTester tester(nullptr, 1, 4);
192 tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
193 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
194 tester.get_chain()->add_effect(effect);
195 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
197 Node *node = effect->replaced_node;
198 ASSERT_EQ(1, node->incoming_links.size());
199 ASSERT_EQ(1, node->outgoing_links.size());
200 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
201 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
203 expect_equal(expected_data, out_data, 4, 4);
206 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
211 float expected_data[6] = {
216 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
217 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
218 tester.get_chain()->add_effect(effect);
219 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
221 Node *node = effect->replaced_node;
222 ASSERT_EQ(1, node->incoming_links.size());
223 ASSERT_EQ(1, node->outgoing_links.size());
224 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
225 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
227 expect_equal(expected_data, out_data, 3, 2);
230 // A fake input that can change its output colorspace and gamma between instantiation
232 class UnknownColorspaceInput : public FlatInput {
234 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
235 : FlatInput(format, pixel_format, type, width, height),
236 overridden_color_space(format.color_space),
237 overridden_gamma_curve(format.gamma_curve) {}
238 string effect_type_id() const override { return "UnknownColorspaceInput"; }
240 void set_color_space(Colorspace colorspace) {
241 overridden_color_space = colorspace;
243 void set_gamma_curve(GammaCurve gamma_curve) {
244 overridden_gamma_curve = gamma_curve;
246 Colorspace get_color_space() const override { return overridden_color_space; }
247 GammaCurve get_gamma_curve() const override { return overridden_gamma_curve; }
250 Colorspace overridden_color_space;
251 GammaCurve overridden_gamma_curve;
254 TEST(EffectChainTest, HandlesInputChangingColorspace) {
263 float out_data[size];
265 EffectChainTester tester(nullptr, 4, 1, FORMAT_GRAYSCALE);
267 // First say that we have sRGB, linear input.
269 format.color_space = COLORSPACE_sRGB;
270 format.gamma_curve = GAMMA_LINEAR;
272 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
273 input->set_pixel_data(data);
274 tester.get_chain()->add_input(input);
276 // Now we change to Rec. 601 input.
277 input->set_color_space(COLORSPACE_REC_601_625);
278 input->set_gamma_curve(GAMMA_REC_601);
280 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
281 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
282 expect_equal(data, out_data, 4, 1);
285 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
290 float expected_data[6] = {
295 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
296 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
297 tester.get_chain()->add_effect(effect);
298 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
300 Node *node = effect->replaced_node;
301 ASSERT_EQ(1, node->incoming_links.size());
302 EXPECT_EQ(0, node->outgoing_links.size());
303 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
305 expect_equal(expected_data, out_data, 3, 2);
308 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
313 float expected_data[6] = {
318 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
319 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
320 tester.get_chain()->add_effect(effect);
321 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
323 Node *node = effect->replaced_node;
324 ASSERT_EQ(1, node->incoming_links.size());
325 EXPECT_EQ(0, node->outgoing_links.size());
326 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
328 expect_equal(expected_data, out_data, 3, 2);
331 // The identity effect needs linear light, and thus will get conversions on both sides.
332 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
333 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
335 for (unsigned i = 0; i < 256; ++i) {
339 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
340 tester.get_chain()->add_effect(new IdentityEffect());
341 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
343 expect_equal(data, out_data, 256, 1);
346 // Same, but uses the forward sRGB table from the GPU.
347 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
348 unsigned char data[256];
349 float expected_data[256];
350 for (unsigned i = 0; i < 256; ++i) {
352 expected_data[i] = i / 255.0;
355 EffectChainTester tester(nullptr, 256, 1);
356 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
357 tester.get_chain()->add_effect(new IdentityEffect());
358 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
360 expect_equal(expected_data, out_data, 256, 1);
363 // Same, for the Rec. 601/709 gamma curve.
364 TEST(EffectChainTest, IdentityThroughRec709) {
366 for (unsigned i = 0; i < 256; ++i) {
370 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
371 tester.get_chain()->add_effect(new IdentityEffect());
372 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
374 expect_equal(data, out_data, 256, 1);
377 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
378 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
380 float data[4 * size] = {
381 0.8f, 0.0f, 0.0f, 0.5f,
382 0.0f, 0.2f, 0.2f, 0.3f,
383 0.1f, 0.0f, 1.0f, 1.0f,
385 float out_data[4 * size];
386 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
387 tester.get_chain()->add_effect(new IdentityEffect());
388 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
390 expect_equal(data, out_data, 4, size);
393 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
395 float data[4 * size] = {
396 0.8f, 0.0f, 0.0f, 0.5f,
397 0.0f, 0.2f, 0.2f, 0.3f,
398 0.1f, 0.0f, 1.0f, 1.0f,
400 float expected_data[4 * size] = {
401 0.1f, 0.0f, 1.0f, 1.0f,
402 0.0f, 0.2f, 0.2f, 0.3f,
403 0.8f, 0.0f, 0.0f, 0.5f,
405 float out_data[4 * size];
406 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
407 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
408 tester.get_chain()->add_effect(effect);
409 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
411 Node *node = effect->replaced_node;
412 ASSERT_EQ(1, node->incoming_links.size());
413 EXPECT_EQ(0, node->outgoing_links.size());
414 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
416 expect_equal(expected_data, out_data, 4, size);
419 // An input that outputs only blue, which has blank alpha.
420 class BlueInput : public Input {
422 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
423 string effect_type_id() const override { return "IdentityEffect"; }
424 string output_fragment_shader() override { return read_file("blue.frag"); }
425 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
426 bool can_output_linear_gamma() const override { return true; }
427 unsigned get_width() const override { return 1; }
428 unsigned get_height() const override { return 1; }
429 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
430 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
436 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
437 // which outputs blank alpha.
438 class RewritingToBlueInput : public Input {
440 RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
441 string effect_type_id() const override { return "RewritingToBlueInput"; }
442 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
443 void rewrite_graph(EffectChain *graph, Node *self) override {
444 Node *blue_node = graph->add_node(new BlueInput());
445 graph->replace_receiver(self, blue_node);
446 graph->replace_sender(self, blue_node);
448 self->disabled = true;
449 this->blue_node = blue_node;
452 // Dummy values that we need to implement because we inherit from Input.
453 // Same as BlueInput.
454 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
455 bool can_output_linear_gamma() const override { return true; }
456 unsigned get_width() const override { return 1; }
457 unsigned get_height() const override { return 1; }
458 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
459 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
467 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
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(nullptr, size, 1);
476 RewritingToBlueInput *input = new RewritingToBlueInput();
477 tester.get_chain()->add_input(input);
478 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
480 Node *node = input->blue_node;
481 EXPECT_EQ(0, node->incoming_links.size());
482 EXPECT_EQ(0, node->outgoing_links.size());
484 expect_equal(data, out_data, 4, size);
487 // An effect that does nothing, and specifies that it preserves blank alpha.
488 class BlankAlphaPreservingEffect : public Effect {
490 BlankAlphaPreservingEffect() {}
491 string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
492 string output_fragment_shader() override { return read_file("identity.frag"); }
493 AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
496 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
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(nullptr, size, 1);
505 tester.get_chain()->add_input(new BlueInput());
506 tester.get_chain()->add_effect(new 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(0, node->outgoing_links.size());
515 expect_equal(data, out_data, 4, size);
518 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
519 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
520 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
521 // with other tests.)
522 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
524 float data[4 * size] = {
525 0.0f, 0.0f, 1.0f, 1.0f,
526 0.0f, 0.0f, 1.0f, 1.0f,
527 0.0f, 0.0f, 1.0f, 1.0f,
529 float out_data[4 * size];
530 EffectChainTester tester(nullptr, size, 1);
531 tester.get_chain()->add_input(new BlueInput());
532 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
533 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
534 tester.get_chain()->add_effect(effect);
535 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
537 Node *node = effect->replaced_node;
538 EXPECT_EQ(1, node->incoming_links.size());
539 EXPECT_EQ(1, node->outgoing_links.size());
540 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
542 expect_equal(data, out_data, 4, size);
545 // Effectively scales down its input linearly by 4x (and repeating it),
546 // which is not attainable without mipmaps.
547 class MipmapNeedingEffect : public Effect {
549 MipmapNeedingEffect() {}
550 bool needs_mipmaps() const override { return true; }
552 // To be allowed to mess with the sampler state.
553 bool needs_texture_bounce() const override { return true; }
555 string effect_type_id() const override { return "MipmapNeedingEffect"; }
556 string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
557 void inform_added(EffectChain *chain) override { this->chain = chain; }
559 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
561 Node *self = chain->find_node_for_effect(this);
562 glActiveTexture(chain->get_input_sampler(self, 0));
564 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
566 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
574 TEST(EffectChainTest, MipmapGenerationWorks) {
575 float data[] = { // In 4x4 blocks.
576 1.0f, 0.0f, 0.0f, 0.0f,
577 0.0f, 0.0f, 0.0f, 0.0f,
578 0.0f, 0.0f, 0.0f, 0.0f,
579 0.0f, 0.0f, 0.0f, 1.0f,
581 0.0f, 0.0f, 0.0f, 0.0f,
582 0.0f, 0.5f, 0.0f, 0.0f,
583 0.0f, 0.0f, 1.0f, 0.0f,
584 0.0f, 0.0f, 0.0f, 0.0f,
586 1.0f, 1.0f, 1.0f, 1.0f,
587 1.0f, 1.0f, 1.0f, 1.0f,
588 1.0f, 1.0f, 1.0f, 1.0f,
589 1.0f, 1.0f, 1.0f, 1.0f,
591 0.0f, 0.0f, 0.0f, 0.0f,
592 0.0f, 1.0f, 1.0f, 0.0f,
593 0.0f, 1.0f, 1.0f, 0.0f,
594 0.0f, 0.0f, 0.0f, 0.0f,
596 float expected_data[] = { // Repeated four times each way.
597 0.125f, 0.125f, 0.125f, 0.125f,
598 0.09375f, 0.09375f, 0.09375f, 0.09375f,
599 1.0f, 1.0f, 1.0f, 1.0f,
600 0.25f, 0.25f, 0.25f, 0.25f,
602 0.125f, 0.125f, 0.125f, 0.125f,
603 0.09375f, 0.09375f, 0.09375f, 0.09375f,
604 1.0f, 1.0f, 1.0f, 1.0f,
605 0.25f, 0.25f, 0.25f, 0.25f,
607 0.125f, 0.125f, 0.125f, 0.125f,
608 0.09375f, 0.09375f, 0.09375f, 0.09375f,
609 1.0f, 1.0f, 1.0f, 1.0f,
610 0.25f, 0.25f, 0.25f, 0.25f,
612 0.125f, 0.125f, 0.125f, 0.125f,
613 0.09375f, 0.09375f, 0.09375f, 0.09375f,
614 1.0f, 1.0f, 1.0f, 1.0f,
615 0.25f, 0.25f, 0.25f, 0.25f,
617 float out_data[4 * 16];
618 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
619 tester.get_chain()->add_effect(new MipmapNeedingEffect());
620 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
622 expect_equal(expected_data, out_data, 4, 16);
625 class NonMipmapCapableInput : public FlatInput {
627 NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
628 : FlatInput(format, pixel_format, type, width, height) {}
630 bool can_supply_mipmaps() const override { return false; }
631 bool set_int(const std::string& key, int value) override {
632 if (key == "needs_mipmaps") {
635 return FlatInput::set_int(key, value);
639 // The same test as MipmapGenerationWorks, but with an input that refuses
640 // to supply mipmaps.
641 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
642 float data[] = { // In 4x4 blocks.
643 1.0f, 0.0f, 0.0f, 0.0f,
644 0.0f, 0.0f, 0.0f, 0.0f,
645 0.0f, 0.0f, 0.0f, 0.0f,
646 0.0f, 0.0f, 0.0f, 1.0f,
648 0.0f, 0.0f, 0.0f, 0.0f,
649 0.0f, 0.5f, 0.0f, 0.0f,
650 0.0f, 0.0f, 1.0f, 0.0f,
651 0.0f, 0.0f, 0.0f, 0.0f,
653 1.0f, 1.0f, 1.0f, 1.0f,
654 1.0f, 1.0f, 1.0f, 1.0f,
655 1.0f, 1.0f, 1.0f, 1.0f,
656 1.0f, 1.0f, 1.0f, 1.0f,
658 0.0f, 0.0f, 0.0f, 0.0f,
659 0.0f, 1.0f, 1.0f, 0.0f,
660 0.0f, 1.0f, 1.0f, 0.0f,
661 0.0f, 0.0f, 0.0f, 0.0f,
663 float expected_data[] = { // Repeated four times each way.
664 0.125f, 0.125f, 0.125f, 0.125f,
665 0.09375f, 0.09375f, 0.09375f, 0.09375f,
666 1.0f, 1.0f, 1.0f, 1.0f,
667 0.25f, 0.25f, 0.25f, 0.25f,
669 0.125f, 0.125f, 0.125f, 0.125f,
670 0.09375f, 0.09375f, 0.09375f, 0.09375f,
671 1.0f, 1.0f, 1.0f, 1.0f,
672 0.25f, 0.25f, 0.25f, 0.25f,
674 0.125f, 0.125f, 0.125f, 0.125f,
675 0.09375f, 0.09375f, 0.09375f, 0.09375f,
676 1.0f, 1.0f, 1.0f, 1.0f,
677 0.25f, 0.25f, 0.25f, 0.25f,
679 0.125f, 0.125f, 0.125f, 0.125f,
680 0.09375f, 0.09375f, 0.09375f, 0.09375f,
681 1.0f, 1.0f, 1.0f, 1.0f,
682 0.25f, 0.25f, 0.25f, 0.25f,
684 float out_data[4 * 16];
685 EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
688 format.color_space = COLORSPACE_sRGB;
689 format.gamma_curve = GAMMA_LINEAR;
691 NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
692 input->set_pixel_data(data);
693 tester.get_chain()->add_input(input);
694 tester.get_chain()->add_effect(new MipmapNeedingEffect());
695 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
697 expect_equal(expected_data, out_data, 4, 16);
700 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
701 float data[] = { // In 4x4 blocks.
702 1.0f, 0.0f, 0.0f, 0.0f,
703 0.0f, 0.0f, 0.0f, 0.0f,
704 0.0f, 0.0f, 0.0f, 0.0f,
705 0.0f, 0.0f, 0.0f, 1.0f,
707 0.0f, 0.0f, 0.0f, 0.0f,
708 0.0f, 0.5f, 0.0f, 0.0f,
709 0.0f, 0.0f, 1.0f, 0.0f,
710 0.0f, 0.0f, 0.0f, 0.0f,
712 1.0f, 1.0f, 1.0f, 1.0f,
713 1.0f, 1.0f, 1.0f, 1.0f,
714 1.0f, 1.0f, 1.0f, 1.0f,
715 1.0f, 1.0f, 1.0f, 1.0f,
717 0.0f, 0.0f, 0.0f, 0.0f,
718 0.0f, 1.0f, 1.0f, 0.0f,
719 0.0f, 1.0f, 1.0f, 0.0f,
720 0.0f, 0.0f, 0.0f, 0.0f,
722 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
723 0.1250f, 0.1250f, 0.1250f, 0.1250f,
724 0.1250f, 0.1250f, 0.1250f, 0.1250f,
725 0.1211f, 0.1211f, 0.1211f, 0.1211f,
726 0.1133f, 0.1133f, 0.1133f, 0.1133f,
727 0.1055f, 0.1055f, 0.1055f, 0.1055f,
728 0.0977f, 0.0977f, 0.0977f, 0.0977f,
729 0.2070f, 0.2070f, 0.2070f, 0.2070f,
730 0.4336f, 0.4336f, 0.4336f, 0.4336f,
731 0.6602f, 0.6602f, 0.6602f, 0.6602f,
732 0.8867f, 0.8867f, 0.8867f, 0.8867f,
733 0.9062f, 0.9062f, 0.9062f, 0.9062f,
734 0.7188f, 0.7188f, 0.7188f, 0.7188f,
735 0.5312f, 0.5312f, 0.5312f, 0.5312f,
736 0.3438f, 0.3438f, 0.3438f, 0.3438f,
737 0.2500f, 0.2500f, 0.2500f, 0.2500f,
738 0.2500f, 0.2500f, 0.2500f, 0.2500f,
740 float out_data[4 * 16];
742 ResizeEffect *downscale = new ResizeEffect();
743 ASSERT_TRUE(downscale->set_int("width", 1));
744 ASSERT_TRUE(downscale->set_int("height", 4));
746 ResizeEffect *upscale = new ResizeEffect();
747 ASSERT_TRUE(upscale->set_int("width", 4));
748 ASSERT_TRUE(upscale->set_int("height", 16));
750 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
751 tester.get_chain()->add_effect(downscale);
752 tester.get_chain()->add_effect(upscale);
753 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
755 expect_equal(expected_data, out_data, 4, 16);
758 // An effect that adds its two inputs together. Used below.
759 class AddEffect : public Effect {
762 string effect_type_id() const override { return "AddEffect"; }
763 string output_fragment_shader() override { return read_file("add.frag"); }
764 unsigned num_inputs() const override { return 2; }
765 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
768 // Constructs the graph
772 // MultiplyEffect MultiplyEffect |
776 // and verifies that it gives the correct output.
777 TEST(EffectChainTest, DiamondGraph) {
782 float expected_data[] = {
786 float out_data[2 * 2];
788 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
789 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
791 MultiplyEffect *mul_half = new MultiplyEffect();
792 ASSERT_TRUE(mul_half->set_vec4("factor", half));
794 MultiplyEffect *mul_two = new MultiplyEffect();
795 ASSERT_TRUE(mul_two->set_vec4("factor", two));
797 EffectChainTester tester(nullptr, 2, 2);
800 format.color_space = COLORSPACE_sRGB;
801 format.gamma_curve = GAMMA_LINEAR;
803 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
804 input->set_pixel_data(data);
806 tester.get_chain()->add_input(input);
807 tester.get_chain()->add_effect(mul_half, input);
808 tester.get_chain()->add_effect(mul_two, input);
809 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
810 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
812 expect_equal(expected_data, out_data, 2, 2);
815 // Constructs the graph
819 // MultiplyEffect MultiplyEffect |
821 // \ BouncingIdentityEffect |
825 // and verifies that it gives the correct output.
826 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
831 float expected_data[] = {
835 float out_data[2 * 2];
837 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
838 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
840 MultiplyEffect *mul_half = new MultiplyEffect();
841 ASSERT_TRUE(mul_half->set_vec4("factor", half));
843 MultiplyEffect *mul_two = new MultiplyEffect();
844 ASSERT_TRUE(mul_two->set_vec4("factor", two));
846 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
848 EffectChainTester tester(nullptr, 2, 2);
851 format.color_space = COLORSPACE_sRGB;
852 format.gamma_curve = GAMMA_LINEAR;
854 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
855 input->set_pixel_data(data);
857 tester.get_chain()->add_input(input);
858 tester.get_chain()->add_effect(mul_half, input);
859 tester.get_chain()->add_effect(mul_two, input);
860 tester.get_chain()->add_effect(bounce, mul_two);
861 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
862 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
864 expect_equal(expected_data, out_data, 2, 2);
867 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
872 float expected_data[] = {
873 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
876 float out_data[2 * 2];
878 EffectChainTester tester(nullptr, 2, 2);
879 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
881 // MirrorEffect does not get linear light, so the conversions will be
882 // inserted after it, not before.
883 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
884 tester.get_chain()->add_effect(effect);
886 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
887 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
888 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
889 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
891 expect_equal(expected_data, out_data, 2, 2);
893 Node *node = effect->replaced_node;
894 ASSERT_EQ(1, node->incoming_links.size());
895 ASSERT_EQ(1, node->outgoing_links.size());
896 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
897 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
900 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
905 float expected_data[] = {
906 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
909 float out_data[2 * 2];
911 EffectChainTester tester(nullptr, 2, 2);
912 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
914 // MirrorEffect does not get linear light, so the conversions will be
915 // inserted after it, not before.
916 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
917 tester.get_chain()->add_effect(effect);
919 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
920 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
921 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
922 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
924 expect_equal(expected_data, out_data, 2, 2);
926 Node *node = effect->replaced_node;
927 ASSERT_EQ(1, node->incoming_links.size());
928 ASSERT_EQ(1, node->outgoing_links.size());
929 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
930 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
933 // An effect that does nothing, but requests texture bounce and stores
935 class SizeStoringEffect : public BouncingIdentityEffect {
937 SizeStoringEffect() : input_width(-1), input_height(-1) {}
938 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
939 assert(input_num == 0);
941 input_height = height;
943 string effect_type_id() const override { return "SizeStoringEffect"; }
945 int input_width, input_height;
948 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
949 float data[2 * 2] = {
953 float out_data[4 * 3];
955 EffectChainTester tester(nullptr, 4, 3); // Note non-square aspect.
958 format.color_space = COLORSPACE_sRGB;
959 format.gamma_curve = GAMMA_LINEAR;
961 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
962 input1->set_pixel_data(data);
964 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
965 input2->set_pixel_data(data);
967 SizeStoringEffect *input_store = new SizeStoringEffect();
969 tester.get_chain()->add_input(input1);
970 tester.get_chain()->add_input(input2);
971 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
972 tester.get_chain()->add_effect(input_store);
973 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
975 EXPECT_EQ(2, input_store->input_width);
976 EXPECT_EQ(2, input_store->input_height);
979 TEST(EffectChainTest, AspectRatioConversion) {
980 float data1[4 * 3] = {
981 0.0f, 0.0f, 0.0f, 0.0f,
982 0.0f, 0.0f, 0.0f, 0.0f,
983 0.0f, 0.0f, 0.0f, 0.0f,
985 float data2[7 * 7] = {
986 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
987 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
988 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
989 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
990 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
991 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
992 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
995 // The right conversion here is that the 7x7 image decides the size,
996 // since it is the biggest, so everything is scaled up to 9x7
997 // (keep the height, round the width 9.333 to 9).
998 float out_data[9 * 7];
1000 EffectChainTester tester(nullptr, 4, 3);
1003 format.color_space = COLORSPACE_sRGB;
1004 format.gamma_curve = GAMMA_LINEAR;
1006 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1007 input1->set_pixel_data(data1);
1009 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1010 input2->set_pixel_data(data2);
1012 SizeStoringEffect *input_store = new SizeStoringEffect();
1014 tester.get_chain()->add_input(input1);
1015 tester.get_chain()->add_input(input2);
1016 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1017 tester.get_chain()->add_effect(input_store);
1018 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1020 EXPECT_EQ(9, input_store->input_width);
1021 EXPECT_EQ(7, input_store->input_height);
1024 // Tests that putting a BlueInput (constant color) into its own pass,
1025 // which creates a phase that doesn't need texture coordinates,
1026 // doesn't mess up a second phase that actually does.
1027 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1033 float expected_data[] = {
1034 1.0f, 1.0f, 2.0f, 2.0f,
1035 0.0f, 0.0f, 1.0f, 2.0f,
1037 float out_data[size * 4];
1038 // First say that we have sRGB, linear input.
1040 format.color_space = COLORSPACE_sRGB;
1041 format.gamma_curve = GAMMA_LINEAR;
1042 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1044 input->set_pixel_data(data);
1045 EffectChainTester tester(nullptr, 1, size);
1046 tester.get_chain()->add_input(new BlueInput());
1047 Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1048 tester.get_chain()->add_input(input);
1049 tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1051 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1053 expect_equal(expected_data, out_data, 4, size);
1056 // An effect that does nothing except changing its output sizes.
1057 class VirtualResizeEffect : public Effect {
1059 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1062 virtual_width(virtual_width),
1063 virtual_height(virtual_height) {}
1064 string effect_type_id() const override { return "VirtualResizeEffect"; }
1065 string output_fragment_shader() override { return read_file("identity.frag"); }
1067 bool changes_output_size() const override { return true; }
1069 void get_output_size(unsigned *width, unsigned *height,
1070 unsigned *virtual_width, unsigned *virtual_height) const override {
1071 *width = this->width;
1072 *height = this->height;
1073 *virtual_width = this->virtual_width;
1074 *virtual_height = this->virtual_height;
1078 int width, height, virtual_width, virtual_height;
1081 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1082 const int size = 2, bigger_size = 3;
1083 float data[size * size] = {
1087 float out_data[size * size];
1089 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1091 SizeStoringEffect *size_store = new SizeStoringEffect();
1093 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1094 tester.get_chain()->add_effect(size_store);
1095 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1096 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1098 EXPECT_EQ(bigger_size, size_store->input_width);
1099 EXPECT_EQ(bigger_size, size_store->input_height);
1101 // If the resize is implemented as non-virtual, we'll fail here,
1102 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1103 expect_equal(data, out_data, size, size);
1106 // An effect that is like VirtualResizeEffect, but always has virtual and real
1107 // sizes the same (and promises this).
1108 class NonVirtualResizeEffect : public VirtualResizeEffect {
1110 NonVirtualResizeEffect(int width, int height)
1111 : VirtualResizeEffect(width, height, width, height) {}
1112 string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1113 bool sets_virtual_output_size() const override { return false; }
1116 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1117 class OneToOneEffect : public Effect {
1120 string effect_type_id() const override { return "OneToOneEffect"; }
1121 string output_fragment_shader() override { return read_file("identity.frag"); }
1122 bool one_to_one_sampling() const override { return true; }
1125 TEST(EffectChainTest, NoBounceWithOneToOneSampling) {
1127 float data[size * size] = {
1131 float out_data[size * size];
1133 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1135 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1136 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1138 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1139 tester.get_chain()->add_effect(effect1);
1140 tester.get_chain()->add_effect(effect2);
1141 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1143 expect_equal(data, out_data, size, size);
1145 // The first OneToOneEffect should be in the same phase as its input.
1146 ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1147 EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1148 effect1->replaced_node->containing_phase);
1150 // The second OneToOneEffect, too.
1151 EXPECT_EQ(effect1->replaced_node->containing_phase,
1152 effect2->replaced_node->containing_phase);
1155 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1157 float data[size * size] = {
1161 float out_data[size * size];
1163 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1165 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1166 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1167 RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1168 RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1170 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1171 tester.get_chain()->add_effect(effect1);
1172 tester.get_chain()->add_effect(effect2);
1173 tester.get_chain()->add_effect(effect3);
1174 tester.get_chain()->add_effect(effect4);
1175 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1177 expect_equal(data, out_data, size, size);
1179 // The NonVirtualResizeEffect should be in a different phase from
1180 // the IdentityEffect (since the latter is not one-to-one),
1181 // ie., the chain should be broken somewhere between them, but exactly
1182 // where doesn't matter.
1183 ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
1184 EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1185 effect3->replaced_node->containing_phase);
1187 // The last OneToOneEffect should also be in the same phase as the
1188 // IdentityEffect (the phase was already broken).
1189 EXPECT_EQ(effect3->replaced_node->containing_phase,
1190 effect4->replaced_node->containing_phase);
1193 // Does not use EffectChainTest, so that it can construct an EffectChain without
1194 // a shared ResourcePool (which is also properly destroyed afterwards).
1195 // Also turns on debugging to test that code path.
1196 TEST(EffectChainTest, IdentityWithOwnPool) {
1197 const int width = 3, height = 2;
1202 const float expected_data[] = {
1206 float out_data[6], temp[6 * 4];
1208 EffectChain chain(width, height);
1209 movit_debug_level = MOVIT_DEBUG_ON;
1212 format.color_space = COLORSPACE_sRGB;
1213 format.gamma_curve = GAMMA_LINEAR;
1215 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1216 input->set_pixel_data(data);
1217 chain.add_input(input);
1218 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1221 glGenTextures(1, &texnum);
1223 glBindTexture(GL_TEXTURE_2D, texnum);
1225 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1228 glGenFramebuffers(1, &fbo);
1230 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1232 glFramebufferTexture2D(
1234 GL_COLOR_ATTACHMENT0,
1239 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1244 chain.render_to_fbo(fbo, width, height);
1246 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1248 glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1250 for (unsigned i = 0; i < 6; ++i) {
1251 out_data[i] = temp[i * 4];
1254 expect_equal(expected_data, out_data, width, height);
1256 // Reset the debug status again.
1257 movit_debug_level = MOVIT_DEBUG_OFF;
1260 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1261 class PrintfingBlueEffect : public Effect {
1263 PrintfingBlueEffect() {}
1264 string effect_type_id() const override { return "PrintfingBlueEffect"; }
1265 string output_fragment_shader() override {
1267 ss.imbue(locale("C"));
1269 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1270 << 0.0f << ", " << 0.0f << ", "
1271 << 0.5f << ", " << 1.0f << "); }\n";
1276 TEST(EffectChainTest, StringStreamLocalesWork) {
1277 // An example of a locale with comma instead of period as decimal separator.
1278 // Obviously, if you run on a machine without this locale available,
1279 // the test will always succeed. Note that the OpenGL driver might call
1280 // setlocale() behind-the-scenes, and that might corrupt the returned
1281 // pointer, so we need to take our own copy of it here.
1282 char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1283 if (saved_locale == nullptr) {
1284 // The locale wasn't available.
1287 saved_locale = strdup(saved_locale);
1289 0.0f, 0.0f, 0.0f, 0.0f,
1291 float expected_data[] = {
1292 0.0f, 0.0f, 0.5f, 1.0f,
1295 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1296 tester.get_chain()->add_effect(new PrintfingBlueEffect());
1297 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1299 expect_equal(expected_data, out_data, 4, 1);
1301 setlocale(LC_ALL, saved_locale);
1305 // An effect that does nothing, but as a compute shader.
1306 class IdentityComputeEffect : public Effect {
1308 IdentityComputeEffect() {}
1309 string effect_type_id() const override { return "IdentityComputeEffect"; }
1310 bool is_compute_shader() const override { return true; }
1311 string output_fragment_shader() override { return read_file("identity.comp"); }
1314 class WithAndWithoutComputeShaderTest : public testing::TestWithParam<string> {
1316 INSTANTIATE_TEST_CASE_P(WithAndWithoutComputeShaderTest,
1317 WithAndWithoutComputeShaderTest,
1318 testing::Values("fragment", "compute"));
1320 TEST(EffectChainTest, sRGBIntermediate) {
1322 0.0f, 0.5f, 0.0f, 1.0f,
1325 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1326 tester.get_chain()->set_intermediate_format(GL_SRGB8);
1327 tester.get_chain()->add_effect(new IdentityEffect());
1328 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1329 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1331 EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1332 << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1333 EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1334 << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1336 // This state should have been preserved.
1337 EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1340 // An effect that is like IdentityEffect, but also does not require linear light.
1341 class PassThroughEffect : public IdentityEffect {
1343 PassThroughEffect() {}
1344 string effect_type_id() const override { return "PassThroughEffect"; }
1345 bool needs_linear_light() const override { return false; }
1346 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1349 // Same, just also bouncing.
1350 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1352 BouncingPassThroughEffect() {}
1353 string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1354 bool needs_linear_light() const override { return false; }
1355 bool needs_texture_bounce() const override { return true; }
1356 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1359 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1360 // Note that we do the comparison in sRGB space, which is what we
1361 // typically would want; however, we do the sRGB conversion ourself
1362 // to avoid compounding errors from shader conversions into the
1364 const int size = 4096; // 12-bit.
1365 float linear_data[size], data[size], out_data[size];
1367 for (int i = 0; i < size; ++i) {
1368 linear_data[i] = i / double(size - 1);
1369 data[i] = srgb_to_linear(linear_data[i]);
1372 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1373 tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1374 tester.get_chain()->add_effect(new IdentityEffect());
1375 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1376 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1378 for (int i = 0; i < size; ++i) {
1379 out_data[i] = linear_to_srgb(out_data[i]);
1382 // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1383 // framebuffer. (Slightly more on NVIDIA cards.)
1384 expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1387 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1388 // Note that we do the comparison in sRGB space, which is what we
1389 // typically would want; however, we do the sRGB conversion ourself
1390 // to avoid compounding errors from shader conversions into the
1392 const int size = 4096; // 12-bit.
1393 float linear_data[size], data[size], out_data[size];
1395 for (int i = 0; i < size; ++i) {
1396 linear_data[i] = i / double(size - 1);
1397 data[i] = srgb_to_linear(linear_data[i]);
1400 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1401 tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1402 if (GetParam() == "compute") {
1403 tester.get_chain()->add_effect(new IdentityComputeEffect());
1405 tester.get_chain()->add_effect(new IdentityEffect());
1407 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1408 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1410 for (int i = 0; i < size; ++i) {
1411 out_data[i] = linear_to_srgb(out_data[i]);
1414 // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1415 // framebuffer (ideal would be 0.5). That is an order of magnitude better
1416 // than in the linear test above. The RMS error is much better, too.
1417 expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1420 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1421 const int size = 256; // 8-bit.
1422 float data[size], out_data[size];
1424 for (int i = 0; i < size; ++i) {
1425 data[i] = i / double(size - 1);
1428 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1429 tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1430 tester.get_chain()->add_effect(new PassThroughEffect());
1431 tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1432 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1434 // The data should be passed through nearly exactly, since there is no effect
1435 // on the path that requires linear light. (Actually, it _is_ exact modulo
1436 // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1437 expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1440 // An effect that stores which program number was last run under.
1441 class RecordingIdentityEffect : public Effect {
1443 RecordingIdentityEffect() {}
1444 string effect_type_id() const override { return "RecordingIdentityEffect"; }
1445 string output_fragment_shader() override { return read_file("identity.frag"); }
1447 GLuint last_glsl_program_num;
1448 void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1450 last_glsl_program_num = glsl_program_num;
1454 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1460 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1461 RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1462 tester.get_chain()->add_effect(effect);
1463 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1465 expect_equal(data, out_data, 3, 2);
1467 ASSERT_NE(0, effect->last_glsl_program_num);
1469 // Now pretend some other effect is using this program number;
1470 // ResourcePool will then need to clone it.
1471 ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1472 GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1473 EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1475 // Re-run should still give the correct data, but it should have run
1476 // with a different program.
1477 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1478 expect_equal(data, out_data, 3, 2);
1479 EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1481 // Release the program, and check one final time.
1482 resource_pool->unuse_glsl_program(master_program_num);
1483 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1484 expect_equal(data, out_data, 3, 2);
1487 TEST(ComputeShaderTest, Identity) {
1493 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1494 if (!movit_compute_shaders_supported) {
1495 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1498 tester.get_chain()->add_effect(new IdentityComputeEffect());
1499 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1501 expect_equal(data, out_data, 3, 2);
1504 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1505 // the very last effect in the chain, which means we can't output it directly
1507 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1508 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1511 TEST(ComputeShaderTest, LastEffectInChain) {
1517 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1518 if (!movit_compute_shaders_supported) {
1519 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1522 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1523 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1525 expect_equal(data, out_data, 3, 2);
1528 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1533 uint8_t out_data[6];
1534 EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1535 if (!movit_compute_shaders_supported) {
1536 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1539 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1540 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1541 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1543 expect_equal(data, out_data, 3, 2);
1546 } // namespace movit