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 class WithAndWithoutComputeShaderTest : public testing::TestWithParam<string> {
104 INSTANTIATE_TEST_CASE_P(WithAndWithoutComputeShaderTest,
105 WithAndWithoutComputeShaderTest,
106 testing::Values("fragment", "compute"));
108 // An effect that does nothing, but as a compute shader.
109 class IdentityComputeEffect : public Effect {
111 IdentityComputeEffect() {}
112 virtual string effect_type_id() const { return "IdentityComputeEffect"; }
113 virtual bool is_compute_shader() const { return true; }
114 string output_fragment_shader() { return read_file("identity.comp"); }
117 TEST_P(WithAndWithoutComputeShaderTest, TopLeftOrigin) {
122 // Note that EffectChainTester assumes bottom-left origin, so by setting
123 // top-left, we will get flipped data back.
124 float expected_data[6] = {
129 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
130 tester.get_chain()->set_output_origin(OUTPUT_ORIGIN_TOP_LEFT);
131 if (GetParam() == "compute") {
132 tester.get_chain()->add_effect(new IdentityComputeEffect());
134 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
136 expect_equal(expected_data, out_data, 3, 2);
139 // A dummy effect that inverts its input.
140 class InvertEffect : public Effect {
143 string effect_type_id() const override { return "InvertEffect"; }
144 string output_fragment_shader() override { return read_file("invert_effect.frag"); }
146 // A real invert would actually care about its alpha,
147 // but in this unit test, it only complicates things.
148 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
151 // Like IdentityEffect, but rewrites itself out of the loop,
152 // splicing in a different effect instead. Also stores the new node,
153 // so we later can check whatever properties we'd like about the graph.
155 class RewritingEffect : public Effect {
157 RewritingEffect() : effect(new T()), replaced_node(nullptr) {}
158 string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
159 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
160 void rewrite_graph(EffectChain *graph, Node *self) override {
161 replaced_node = graph->add_node(effect);
162 graph->replace_receiver(self, replaced_node);
163 graph->replace_sender(self, replaced_node);
164 self->disabled = true;
171 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
176 float expected_data[6] = {
177 1.0f, 0.9771f, 0.9673f,
181 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
182 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
183 tester.get_chain()->add_effect(effect);
184 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
186 Node *node = effect->replaced_node;
187 ASSERT_EQ(1u, node->incoming_links.size());
188 ASSERT_EQ(1u, node->outgoing_links.size());
189 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
190 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
192 expect_equal(expected_data, out_data, 3, 2);
195 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
196 unsigned char data[] = {
202 float expected_data[] = {
203 1.0000f, 1.0000f, 1.0000f, 1.0000f,
204 0.9771f, 0.9771f, 0.9771f, 1.0000f,
205 0.8983f, 0.8983f, 0.8983f, 1.0000f,
206 0.0000f, 0.0000f, 0.0000f, 1.0000f
208 float out_data[4 * 4];
209 EffectChainTester tester(nullptr, 1, 4);
210 tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
211 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
212 tester.get_chain()->add_effect(effect);
213 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
215 Node *node = effect->replaced_node;
216 ASSERT_EQ(1u, node->incoming_links.size());
217 ASSERT_EQ(1u, node->outgoing_links.size());
218 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
219 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
221 expect_equal(expected_data, out_data, 4, 4);
224 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
229 float expected_data[6] = {
234 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
235 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
236 tester.get_chain()->add_effect(effect);
237 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
239 Node *node = effect->replaced_node;
240 ASSERT_EQ(1u, node->incoming_links.size());
241 ASSERT_EQ(1u, node->outgoing_links.size());
242 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
243 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
245 expect_equal(expected_data, out_data, 3, 2);
248 // A fake input that can change its output colorspace and gamma between instantiation
250 class UnknownColorspaceInput : public FlatInput {
252 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
253 : FlatInput(format, pixel_format, type, width, height),
254 overridden_color_space(format.color_space),
255 overridden_gamma_curve(format.gamma_curve) {}
256 string effect_type_id() const override { return "UnknownColorspaceInput"; }
258 void set_color_space(Colorspace colorspace) {
259 overridden_color_space = colorspace;
261 void set_gamma_curve(GammaCurve gamma_curve) {
262 overridden_gamma_curve = gamma_curve;
264 Colorspace get_color_space() const override { return overridden_color_space; }
265 GammaCurve get_gamma_curve() const override { return overridden_gamma_curve; }
268 Colorspace overridden_color_space;
269 GammaCurve overridden_gamma_curve;
272 TEST(EffectChainTest, HandlesInputChangingColorspace) {
281 float out_data[size];
283 EffectChainTester tester(nullptr, 4, 1, FORMAT_GRAYSCALE);
285 // First say that we have sRGB, linear input.
287 format.color_space = COLORSPACE_sRGB;
288 format.gamma_curve = GAMMA_LINEAR;
290 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
291 input->set_pixel_data(data);
292 tester.get_chain()->add_input(input);
294 // Now we change to Rec. 601 input.
295 input->set_color_space(COLORSPACE_REC_601_625);
296 input->set_gamma_curve(GAMMA_REC_601);
298 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
299 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
300 expect_equal(data, out_data, 4, 1);
303 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
308 float expected_data[6] = {
313 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
314 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
315 tester.get_chain()->add_effect(effect);
316 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
318 Node *node = effect->replaced_node;
319 ASSERT_EQ(1u, node->incoming_links.size());
320 EXPECT_EQ(0u, node->outgoing_links.size());
321 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
323 expect_equal(expected_data, out_data, 3, 2);
326 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
331 float expected_data[6] = {
336 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
337 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
338 tester.get_chain()->add_effect(effect);
339 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
341 Node *node = effect->replaced_node;
342 ASSERT_EQ(1u, node->incoming_links.size());
343 EXPECT_EQ(0u, node->outgoing_links.size());
344 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
346 expect_equal(expected_data, out_data, 3, 2);
349 // The identity effect needs linear light, and thus will get conversions on both sides.
350 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
351 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
353 for (unsigned i = 0; i < 256; ++i) {
357 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
358 tester.get_chain()->add_effect(new IdentityEffect());
359 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
361 expect_equal(data, out_data, 256, 1);
364 // Same, but uses the forward sRGB table from the GPU.
365 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
366 unsigned char data[256];
367 float expected_data[256];
368 for (unsigned i = 0; i < 256; ++i) {
370 expected_data[i] = i / 255.0;
373 EffectChainTester tester(nullptr, 256, 1);
374 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
375 tester.get_chain()->add_effect(new IdentityEffect());
376 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
378 expect_equal(expected_data, out_data, 256, 1);
381 // Same, for the Rec. 601/709 gamma curve.
382 TEST(EffectChainTest, IdentityThroughRec709) {
384 for (unsigned i = 0; i < 256; ++i) {
388 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
389 tester.get_chain()->add_effect(new IdentityEffect());
390 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
392 expect_equal(data, out_data, 256, 1);
395 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
396 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
398 float data[4 * size] = {
399 0.8f, 0.0f, 0.0f, 0.5f,
400 0.0f, 0.2f, 0.2f, 0.3f,
401 0.1f, 0.0f, 1.0f, 1.0f,
403 float out_data[4 * size];
404 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
405 tester.get_chain()->add_effect(new IdentityEffect());
406 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
408 expect_equal(data, out_data, 4, size);
411 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
413 float data[4 * size] = {
414 0.8f, 0.0f, 0.0f, 0.5f,
415 0.0f, 0.2f, 0.2f, 0.3f,
416 0.1f, 0.0f, 1.0f, 1.0f,
418 float expected_data[4 * size] = {
419 0.1f, 0.0f, 1.0f, 1.0f,
420 0.0f, 0.2f, 0.2f, 0.3f,
421 0.8f, 0.0f, 0.0f, 0.5f,
423 float out_data[4 * size];
424 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
425 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
426 tester.get_chain()->add_effect(effect);
427 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
429 Node *node = effect->replaced_node;
430 ASSERT_EQ(1u, node->incoming_links.size());
431 EXPECT_EQ(0u, node->outgoing_links.size());
432 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
434 expect_equal(expected_data, out_data, 4, size);
437 // An input that outputs only blue, which has blank alpha.
438 class BlueInput : public Input {
440 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
441 string effect_type_id() const override { return "IdentityEffect"; }
442 string output_fragment_shader() override { return read_file("blue.frag"); }
443 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
444 bool can_output_linear_gamma() const override { return true; }
445 unsigned get_width() const override { return 1; }
446 unsigned get_height() const override { return 1; }
447 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
448 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
454 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
455 // which outputs blank alpha.
456 class RewritingToBlueInput : public Input {
458 RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
459 string effect_type_id() const override { return "RewritingToBlueInput"; }
460 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
461 void rewrite_graph(EffectChain *graph, Node *self) override {
462 Node *blue_node = graph->add_node(new BlueInput());
463 graph->replace_receiver(self, blue_node);
464 graph->replace_sender(self, blue_node);
466 self->disabled = true;
467 this->blue_node = blue_node;
470 // Dummy values that we need to implement because we inherit from Input.
471 // Same as BlueInput.
472 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
473 bool can_output_linear_gamma() const override { return true; }
474 unsigned get_width() const override { return 1; }
475 unsigned get_height() const override { return 1; }
476 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
477 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
485 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
487 float data[4 * size] = {
488 0.0f, 0.0f, 1.0f, 1.0f,
489 0.0f, 0.0f, 1.0f, 1.0f,
490 0.0f, 0.0f, 1.0f, 1.0f,
492 float out_data[4 * size];
493 EffectChainTester tester(nullptr, size, 1);
494 RewritingToBlueInput *input = new RewritingToBlueInput();
495 tester.get_chain()->add_input(input);
496 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
498 Node *node = input->blue_node;
499 EXPECT_EQ(0u, node->incoming_links.size());
500 EXPECT_EQ(0u, node->outgoing_links.size());
502 expect_equal(data, out_data, 4, size);
505 // An effect that does nothing, and specifies that it preserves blank alpha.
506 class BlankAlphaPreservingEffect : public Effect {
508 BlankAlphaPreservingEffect() {}
509 string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
510 string output_fragment_shader() override { return read_file("identity.frag"); }
511 AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
514 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
516 float data[4 * size] = {
517 0.0f, 0.0f, 1.0f, 1.0f,
518 0.0f, 0.0f, 1.0f, 1.0f,
519 0.0f, 0.0f, 1.0f, 1.0f,
521 float out_data[4 * size];
522 EffectChainTester tester(nullptr, size, 1);
523 tester.get_chain()->add_input(new BlueInput());
524 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
525 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
526 tester.get_chain()->add_effect(effect);
527 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
529 Node *node = effect->replaced_node;
530 EXPECT_EQ(1u, node->incoming_links.size());
531 EXPECT_EQ(0u, node->outgoing_links.size());
533 expect_equal(data, out_data, 4, size);
536 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
537 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
538 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
539 // with other tests.)
540 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
542 float data[4 * size] = {
543 0.0f, 0.0f, 1.0f, 1.0f,
544 0.0f, 0.0f, 1.0f, 1.0f,
545 0.0f, 0.0f, 1.0f, 1.0f,
547 float out_data[4 * size];
548 EffectChainTester tester(nullptr, size, 1);
549 tester.get_chain()->add_input(new BlueInput());
550 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
551 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
552 tester.get_chain()->add_effect(effect);
553 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
555 Node *node = effect->replaced_node;
556 EXPECT_EQ(1u, node->incoming_links.size());
557 EXPECT_EQ(1u, node->outgoing_links.size());
558 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
560 expect_equal(data, out_data, 4, size);
563 // Effectively scales down its input linearly by 4x (and repeating it),
564 // which is not attainable without mipmaps.
565 class MipmapNeedingEffect : public Effect {
567 MipmapNeedingEffect() {}
568 bool needs_mipmaps() const override { return true; }
570 // To be allowed to mess with the sampler state.
571 bool needs_texture_bounce() const override { return true; }
573 string effect_type_id() const override { return "MipmapNeedingEffect"; }
574 string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
575 void inform_added(EffectChain *chain) override { this->chain = chain; }
577 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
579 Node *self = chain->find_node_for_effect(this);
580 glActiveTexture(chain->get_input_sampler(self, 0));
582 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
584 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
592 TEST(EffectChainTest, MipmapGenerationWorks) {
593 float data[] = { // In 4x4 blocks.
594 1.0f, 0.0f, 0.0f, 0.0f,
595 0.0f, 0.0f, 0.0f, 0.0f,
596 0.0f, 0.0f, 0.0f, 0.0f,
597 0.0f, 0.0f, 0.0f, 1.0f,
599 0.0f, 0.0f, 0.0f, 0.0f,
600 0.0f, 0.5f, 0.0f, 0.0f,
601 0.0f, 0.0f, 1.0f, 0.0f,
602 0.0f, 0.0f, 0.0f, 0.0f,
604 1.0f, 1.0f, 1.0f, 1.0f,
605 1.0f, 1.0f, 1.0f, 1.0f,
606 1.0f, 1.0f, 1.0f, 1.0f,
607 1.0f, 1.0f, 1.0f, 1.0f,
609 0.0f, 0.0f, 0.0f, 0.0f,
610 0.0f, 1.0f, 1.0f, 0.0f,
611 0.0f, 1.0f, 1.0f, 0.0f,
612 0.0f, 0.0f, 0.0f, 0.0f,
614 float expected_data[] = { // Repeated four times each way.
615 0.125f, 0.125f, 0.125f, 0.125f,
616 0.09375f, 0.09375f, 0.09375f, 0.09375f,
617 1.0f, 1.0f, 1.0f, 1.0f,
618 0.25f, 0.25f, 0.25f, 0.25f,
620 0.125f, 0.125f, 0.125f, 0.125f,
621 0.09375f, 0.09375f, 0.09375f, 0.09375f,
622 1.0f, 1.0f, 1.0f, 1.0f,
623 0.25f, 0.25f, 0.25f, 0.25f,
625 0.125f, 0.125f, 0.125f, 0.125f,
626 0.09375f, 0.09375f, 0.09375f, 0.09375f,
627 1.0f, 1.0f, 1.0f, 1.0f,
628 0.25f, 0.25f, 0.25f, 0.25f,
630 0.125f, 0.125f, 0.125f, 0.125f,
631 0.09375f, 0.09375f, 0.09375f, 0.09375f,
632 1.0f, 1.0f, 1.0f, 1.0f,
633 0.25f, 0.25f, 0.25f, 0.25f,
635 float out_data[4 * 16];
636 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
637 tester.get_chain()->add_effect(new MipmapNeedingEffect());
638 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
640 expect_equal(expected_data, out_data, 4, 16);
643 class NonMipmapCapableInput : public FlatInput {
645 NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
646 : FlatInput(format, pixel_format, type, width, height) {}
648 bool can_supply_mipmaps() const override { return false; }
649 bool set_int(const std::string& key, int value) override {
650 if (key == "needs_mipmaps") {
653 return FlatInput::set_int(key, value);
657 // The same test as MipmapGenerationWorks, but with an input that refuses
658 // to supply mipmaps.
659 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
660 float data[] = { // In 4x4 blocks.
661 1.0f, 0.0f, 0.0f, 0.0f,
662 0.0f, 0.0f, 0.0f, 0.0f,
663 0.0f, 0.0f, 0.0f, 0.0f,
664 0.0f, 0.0f, 0.0f, 1.0f,
666 0.0f, 0.0f, 0.0f, 0.0f,
667 0.0f, 0.5f, 0.0f, 0.0f,
668 0.0f, 0.0f, 1.0f, 0.0f,
669 0.0f, 0.0f, 0.0f, 0.0f,
671 1.0f, 1.0f, 1.0f, 1.0f,
672 1.0f, 1.0f, 1.0f, 1.0f,
673 1.0f, 1.0f, 1.0f, 1.0f,
674 1.0f, 1.0f, 1.0f, 1.0f,
676 0.0f, 0.0f, 0.0f, 0.0f,
677 0.0f, 1.0f, 1.0f, 0.0f,
678 0.0f, 1.0f, 1.0f, 0.0f,
679 0.0f, 0.0f, 0.0f, 0.0f,
681 float expected_data[] = { // Repeated four times each way.
682 0.125f, 0.125f, 0.125f, 0.125f,
683 0.09375f, 0.09375f, 0.09375f, 0.09375f,
684 1.0f, 1.0f, 1.0f, 1.0f,
685 0.25f, 0.25f, 0.25f, 0.25f,
687 0.125f, 0.125f, 0.125f, 0.125f,
688 0.09375f, 0.09375f, 0.09375f, 0.09375f,
689 1.0f, 1.0f, 1.0f, 1.0f,
690 0.25f, 0.25f, 0.25f, 0.25f,
692 0.125f, 0.125f, 0.125f, 0.125f,
693 0.09375f, 0.09375f, 0.09375f, 0.09375f,
694 1.0f, 1.0f, 1.0f, 1.0f,
695 0.25f, 0.25f, 0.25f, 0.25f,
697 0.125f, 0.125f, 0.125f, 0.125f,
698 0.09375f, 0.09375f, 0.09375f, 0.09375f,
699 1.0f, 1.0f, 1.0f, 1.0f,
700 0.25f, 0.25f, 0.25f, 0.25f,
702 float out_data[4 * 16];
703 EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
706 format.color_space = COLORSPACE_sRGB;
707 format.gamma_curve = GAMMA_LINEAR;
709 NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
710 input->set_pixel_data(data);
711 tester.get_chain()->add_input(input);
712 tester.get_chain()->add_effect(new MipmapNeedingEffect());
713 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
715 expect_equal(expected_data, out_data, 4, 16);
718 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
719 float data[] = { // In 4x4 blocks.
720 1.0f, 0.0f, 0.0f, 0.0f,
721 0.0f, 0.0f, 0.0f, 0.0f,
722 0.0f, 0.0f, 0.0f, 0.0f,
723 0.0f, 0.0f, 0.0f, 1.0f,
725 0.0f, 0.0f, 0.0f, 0.0f,
726 0.0f, 0.5f, 0.0f, 0.0f,
727 0.0f, 0.0f, 1.0f, 0.0f,
728 0.0f, 0.0f, 0.0f, 0.0f,
730 1.0f, 1.0f, 1.0f, 1.0f,
731 1.0f, 1.0f, 1.0f, 1.0f,
732 1.0f, 1.0f, 1.0f, 1.0f,
733 1.0f, 1.0f, 1.0f, 1.0f,
735 0.0f, 0.0f, 0.0f, 0.0f,
736 0.0f, 1.0f, 1.0f, 0.0f,
737 0.0f, 1.0f, 1.0f, 0.0f,
738 0.0f, 0.0f, 0.0f, 0.0f,
740 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
741 0.1250f, 0.1250f, 0.1250f, 0.1250f,
742 0.1250f, 0.1250f, 0.1250f, 0.1250f,
743 0.1211f, 0.1211f, 0.1211f, 0.1211f,
744 0.1133f, 0.1133f, 0.1133f, 0.1133f,
745 0.1055f, 0.1055f, 0.1055f, 0.1055f,
746 0.0977f, 0.0977f, 0.0977f, 0.0977f,
747 0.2070f, 0.2070f, 0.2070f, 0.2070f,
748 0.4336f, 0.4336f, 0.4336f, 0.4336f,
749 0.6602f, 0.6602f, 0.6602f, 0.6602f,
750 0.8867f, 0.8867f, 0.8867f, 0.8867f,
751 0.9062f, 0.9062f, 0.9062f, 0.9062f,
752 0.7188f, 0.7188f, 0.7188f, 0.7188f,
753 0.5312f, 0.5312f, 0.5312f, 0.5312f,
754 0.3438f, 0.3438f, 0.3438f, 0.3438f,
755 0.2500f, 0.2500f, 0.2500f, 0.2500f,
756 0.2500f, 0.2500f, 0.2500f, 0.2500f,
758 float out_data[4 * 16];
760 ResizeEffect *downscale = new ResizeEffect();
761 ASSERT_TRUE(downscale->set_int("width", 1));
762 ASSERT_TRUE(downscale->set_int("height", 4));
764 ResizeEffect *upscale = new ResizeEffect();
765 ASSERT_TRUE(upscale->set_int("width", 4));
766 ASSERT_TRUE(upscale->set_int("height", 16));
768 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
769 tester.get_chain()->add_effect(downscale);
770 tester.get_chain()->add_effect(upscale);
771 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
773 expect_equal(expected_data, out_data, 4, 16);
776 // An effect that adds its two inputs together. Used below.
777 class AddEffect : public Effect {
780 string effect_type_id() const override { return "AddEffect"; }
781 string output_fragment_shader() override { return read_file("add.frag"); }
782 unsigned num_inputs() const override { return 2; }
783 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
786 // Constructs the graph
790 // MultiplyEffect MultiplyEffect |
794 // and verifies that it gives the correct output.
795 TEST(EffectChainTest, DiamondGraph) {
800 float expected_data[] = {
804 float out_data[2 * 2];
806 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
807 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
809 MultiplyEffect *mul_half = new MultiplyEffect();
810 ASSERT_TRUE(mul_half->set_vec4("factor", half));
812 MultiplyEffect *mul_two = new MultiplyEffect();
813 ASSERT_TRUE(mul_two->set_vec4("factor", two));
815 EffectChainTester tester(nullptr, 2, 2);
818 format.color_space = COLORSPACE_sRGB;
819 format.gamma_curve = GAMMA_LINEAR;
821 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
822 input->set_pixel_data(data);
824 tester.get_chain()->add_input(input);
825 tester.get_chain()->add_effect(mul_half, input);
826 tester.get_chain()->add_effect(mul_two, input);
827 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
828 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
830 expect_equal(expected_data, out_data, 2, 2);
833 // Constructs the graph
837 // MultiplyEffect MultiplyEffect |
839 // \ BouncingIdentityEffect |
843 // and verifies that it gives the correct output.
844 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
849 float expected_data[] = {
853 float out_data[2 * 2];
855 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
856 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
858 MultiplyEffect *mul_half = new MultiplyEffect();
859 ASSERT_TRUE(mul_half->set_vec4("factor", half));
861 MultiplyEffect *mul_two = new MultiplyEffect();
862 ASSERT_TRUE(mul_two->set_vec4("factor", two));
864 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
866 EffectChainTester tester(nullptr, 2, 2);
869 format.color_space = COLORSPACE_sRGB;
870 format.gamma_curve = GAMMA_LINEAR;
872 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
873 input->set_pixel_data(data);
875 tester.get_chain()->add_input(input);
876 tester.get_chain()->add_effect(mul_half, input);
877 tester.get_chain()->add_effect(mul_two, input);
878 tester.get_chain()->add_effect(bounce, mul_two);
879 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
880 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
882 expect_equal(expected_data, out_data, 2, 2);
885 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
890 float expected_data[] = {
891 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
894 float out_data[2 * 2];
896 EffectChainTester tester(nullptr, 2, 2);
897 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
899 // MirrorEffect does not get linear light, so the conversions will be
900 // inserted after it, not before.
901 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
902 tester.get_chain()->add_effect(effect);
904 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
905 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
906 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
907 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
909 expect_equal(expected_data, out_data, 2, 2);
911 Node *node = effect->replaced_node;
912 ASSERT_EQ(1u, node->incoming_links.size());
913 ASSERT_EQ(1u, node->outgoing_links.size());
914 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
915 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
918 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
923 float expected_data[] = {
924 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
927 float out_data[2 * 2];
929 EffectChainTester tester(nullptr, 2, 2);
930 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
932 // MirrorEffect does not get linear light, so the conversions will be
933 // inserted after it, not before.
934 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
935 tester.get_chain()->add_effect(effect);
937 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
938 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
939 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
940 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
942 expect_equal(expected_data, out_data, 2, 2);
944 Node *node = effect->replaced_node;
945 ASSERT_EQ(1u, node->incoming_links.size());
946 ASSERT_EQ(1u, node->outgoing_links.size());
947 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
948 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
951 // An effect that does nothing, but requests texture bounce and stores
953 class SizeStoringEffect : public BouncingIdentityEffect {
955 SizeStoringEffect() : input_width(-1), input_height(-1) {}
956 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
957 assert(input_num == 0);
959 input_height = height;
961 string effect_type_id() const override { return "SizeStoringEffect"; }
963 int input_width, input_height;
966 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
967 float data[2 * 2] = {
971 float out_data[4 * 3];
973 EffectChainTester tester(nullptr, 4, 3); // Note non-square aspect.
976 format.color_space = COLORSPACE_sRGB;
977 format.gamma_curve = GAMMA_LINEAR;
979 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
980 input1->set_pixel_data(data);
982 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
983 input2->set_pixel_data(data);
985 SizeStoringEffect *input_store = new SizeStoringEffect();
987 tester.get_chain()->add_input(input1);
988 tester.get_chain()->add_input(input2);
989 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
990 tester.get_chain()->add_effect(input_store);
991 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
993 EXPECT_EQ(2, input_store->input_width);
994 EXPECT_EQ(2, input_store->input_height);
997 TEST(EffectChainTest, AspectRatioConversion) {
998 float data1[4 * 3] = {
999 0.0f, 0.0f, 0.0f, 0.0f,
1000 0.0f, 0.0f, 0.0f, 0.0f,
1001 0.0f, 0.0f, 0.0f, 0.0f,
1003 float data2[7 * 7] = {
1004 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1005 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1006 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1007 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
1008 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1009 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1010 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1013 // The right conversion here is that the 7x7 image decides the size,
1014 // since it is the biggest, so everything is scaled up to 9x7
1015 // (keep the height, round the width 9.333 to 9).
1016 float out_data[9 * 7];
1018 EffectChainTester tester(nullptr, 4, 3);
1021 format.color_space = COLORSPACE_sRGB;
1022 format.gamma_curve = GAMMA_LINEAR;
1024 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1025 input1->set_pixel_data(data1);
1027 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1028 input2->set_pixel_data(data2);
1030 SizeStoringEffect *input_store = new SizeStoringEffect();
1032 tester.get_chain()->add_input(input1);
1033 tester.get_chain()->add_input(input2);
1034 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1035 tester.get_chain()->add_effect(input_store);
1036 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1038 EXPECT_EQ(9, input_store->input_width);
1039 EXPECT_EQ(7, input_store->input_height);
1042 // Tests that putting a BlueInput (constant color) into its own pass,
1043 // which creates a phase that doesn't need texture coordinates,
1044 // doesn't mess up a second phase that actually does.
1045 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1051 float expected_data[] = {
1052 1.0f, 1.0f, 2.0f, 2.0f,
1053 0.0f, 0.0f, 1.0f, 2.0f,
1055 float out_data[size * 4];
1056 // First say that we have sRGB, linear input.
1058 format.color_space = COLORSPACE_sRGB;
1059 format.gamma_curve = GAMMA_LINEAR;
1060 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1062 input->set_pixel_data(data);
1063 EffectChainTester tester(nullptr, 1, size);
1064 tester.get_chain()->add_input(new BlueInput());
1065 Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1066 tester.get_chain()->add_input(input);
1067 tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1069 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1071 expect_equal(expected_data, out_data, 4, size);
1074 // An effect that does nothing except changing its output sizes.
1075 class VirtualResizeEffect : public Effect {
1077 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1080 virtual_width(virtual_width),
1081 virtual_height(virtual_height) {}
1082 string effect_type_id() const override { return "VirtualResizeEffect"; }
1083 string output_fragment_shader() override { return read_file("identity.frag"); }
1085 bool changes_output_size() const override { return true; }
1087 void get_output_size(unsigned *width, unsigned *height,
1088 unsigned *virtual_width, unsigned *virtual_height) const override {
1089 *width = this->width;
1090 *height = this->height;
1091 *virtual_width = this->virtual_width;
1092 *virtual_height = this->virtual_height;
1096 int width, height, virtual_width, virtual_height;
1099 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1100 const int size = 2, bigger_size = 3;
1101 float data[size * size] = {
1105 float out_data[size * size];
1107 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1109 SizeStoringEffect *size_store = new SizeStoringEffect();
1111 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1112 tester.get_chain()->add_effect(size_store);
1113 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1114 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1116 EXPECT_EQ(bigger_size, size_store->input_width);
1117 EXPECT_EQ(bigger_size, size_store->input_height);
1119 // If the resize is implemented as non-virtual, we'll fail here,
1120 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1121 expect_equal(data, out_data, size, size);
1124 // An effect that is like VirtualResizeEffect, but always has virtual and real
1125 // sizes the same (and promises this).
1126 class NonVirtualResizeEffect : public VirtualResizeEffect {
1128 NonVirtualResizeEffect(int width, int height)
1129 : VirtualResizeEffect(width, height, width, height) {}
1130 string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1131 bool sets_virtual_output_size() const override { return false; }
1134 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1135 class OneToOneEffect : public Effect {
1138 string effect_type_id() const override { return "OneToOneEffect"; }
1139 string output_fragment_shader() override { return read_file("identity.frag"); }
1140 bool strong_one_to_one_sampling() const override { return true; }
1143 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1145 float data[size * size] = {
1149 float out_data[size * size];
1151 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1153 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1154 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1156 if (GetParam() == "compute") {
1157 tester.get_chain()->add_effect(new IdentityComputeEffect());
1159 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1161 tester.get_chain()->add_effect(effect1);
1162 tester.get_chain()->add_effect(effect2);
1163 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1165 expect_equal(data, out_data, size, size);
1167 // The first OneToOneEffect should be in the same phase as its input.
1168 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1169 EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1170 effect1->replaced_node->containing_phase);
1172 // The second OneToOneEffect, too.
1173 EXPECT_EQ(effect1->replaced_node->containing_phase,
1174 effect2->replaced_node->containing_phase);
1177 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1179 float data[size * size] = {
1183 float out_data[size * size];
1185 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1187 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1188 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1189 RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1190 RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1192 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1193 tester.get_chain()->add_effect(effect1);
1194 tester.get_chain()->add_effect(effect2);
1195 tester.get_chain()->add_effect(effect3);
1196 tester.get_chain()->add_effect(effect4);
1197 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1199 expect_equal(data, out_data, size, size);
1201 // The NonVirtualResizeEffect should be in a different phase from
1202 // the IdentityEffect (since the latter is not one-to-one),
1203 // ie., the chain should be broken somewhere between them, but exactly
1204 // where doesn't matter.
1205 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1206 EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1207 effect3->replaced_node->containing_phase);
1209 // The last OneToOneEffect should also be in the same phase as the
1210 // IdentityEffect (the phase was already broken).
1211 EXPECT_EQ(effect3->replaced_node->containing_phase,
1212 effect4->replaced_node->containing_phase);
1215 // Does not use EffectChainTest, so that it can construct an EffectChain without
1216 // a shared ResourcePool (which is also properly destroyed afterwards).
1217 // Also turns on debugging to test that code path.
1218 TEST(EffectChainTest, IdentityWithOwnPool) {
1219 const int width = 3, height = 2;
1224 const float expected_data[] = {
1228 float out_data[6], temp[6 * 4];
1230 EffectChain chain(width, height);
1231 MovitDebugLevel old_movit_debug_level = movit_debug_level;
1232 movit_debug_level = MOVIT_DEBUG_ON;
1235 format.color_space = COLORSPACE_sRGB;
1236 format.gamma_curve = GAMMA_LINEAR;
1238 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1239 input->set_pixel_data(data);
1240 chain.add_input(input);
1241 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1244 glGenTextures(1, &texnum);
1246 glBindTexture(GL_TEXTURE_2D, texnum);
1248 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1251 glGenFramebuffers(1, &fbo);
1253 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1255 glFramebufferTexture2D(
1257 GL_COLOR_ATTACHMENT0,
1262 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1267 chain.render_to_fbo(fbo, width, height);
1269 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1271 glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1273 for (unsigned i = 0; i < 6; ++i) {
1274 out_data[i] = temp[i * 4];
1277 expect_equal(expected_data, out_data, width, height);
1279 // Reset the debug status again.
1280 movit_debug_level = old_movit_debug_level;
1283 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1284 class PrintfingBlueEffect : public Effect {
1286 PrintfingBlueEffect() {}
1287 string effect_type_id() const override { return "PrintfingBlueEffect"; }
1288 string output_fragment_shader() override {
1290 ss.imbue(locale("C"));
1292 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1293 << 0.0f << ", " << 0.0f << ", "
1294 << 0.5f << ", " << 1.0f << "); }\n";
1299 TEST(EffectChainTest, StringStreamLocalesWork) {
1300 // An example of a locale with comma instead of period as decimal separator.
1301 // Obviously, if you run on a machine without this locale available,
1302 // the test will always succeed. Note that the OpenGL driver might call
1303 // setlocale() behind-the-scenes, and that might corrupt the returned
1304 // pointer, so we need to take our own copy of it here.
1305 char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1306 if (saved_locale == nullptr) {
1307 // The locale wasn't available.
1310 saved_locale = strdup(saved_locale);
1312 0.0f, 0.0f, 0.0f, 0.0f,
1314 float expected_data[] = {
1315 0.0f, 0.0f, 0.5f, 1.0f,
1318 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1319 tester.get_chain()->add_effect(new PrintfingBlueEffect());
1320 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1322 expect_equal(expected_data, out_data, 4, 1);
1324 setlocale(LC_ALL, saved_locale);
1328 TEST(EffectChainTest, sRGBIntermediate) {
1330 0.0f, 0.5f, 0.0f, 1.0f,
1333 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1334 tester.get_chain()->set_intermediate_format(GL_SRGB8);
1335 tester.get_chain()->add_effect(new IdentityEffect());
1336 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1337 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1339 EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1340 << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1341 EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1342 << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1344 // This state should have been preserved.
1345 EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1348 // An effect that is like IdentityEffect, but also does not require linear light.
1349 class PassThroughEffect : public IdentityEffect {
1351 PassThroughEffect() {}
1352 string effect_type_id() const override { return "PassThroughEffect"; }
1353 bool needs_linear_light() const override { return false; }
1354 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1357 // Same, just also bouncing.
1358 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1360 BouncingPassThroughEffect() {}
1361 string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1362 bool needs_linear_light() const override { return false; }
1363 bool needs_texture_bounce() const override { return true; }
1364 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1367 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1368 // Note that we do the comparison in sRGB space, which is what we
1369 // typically would want; however, we do the sRGB conversion ourself
1370 // to avoid compounding errors from shader conversions into the
1372 const int size = 4096; // 12-bit.
1373 float linear_data[size], data[size], out_data[size];
1375 for (int i = 0; i < size; ++i) {
1376 linear_data[i] = i / double(size - 1);
1377 data[i] = srgb_to_linear(linear_data[i]);
1380 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1381 tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1382 tester.get_chain()->add_effect(new IdentityEffect());
1383 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1384 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1386 for (int i = 0; i < size; ++i) {
1387 out_data[i] = linear_to_srgb(out_data[i]);
1390 // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1391 // framebuffer. (Slightly more on NVIDIA cards.)
1392 expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1395 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1396 // Note that we do the comparison in sRGB space, which is what we
1397 // typically would want; however, we do the sRGB conversion ourself
1398 // to avoid compounding errors from shader conversions into the
1400 const int size = 4096; // 12-bit.
1401 float linear_data[size], data[size], out_data[size];
1403 for (int i = 0; i < size; ++i) {
1404 linear_data[i] = i / double(size - 1);
1405 data[i] = srgb_to_linear(linear_data[i]);
1408 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1409 tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1410 if (GetParam() == "compute") {
1411 tester.get_chain()->add_effect(new IdentityComputeEffect());
1413 tester.get_chain()->add_effect(new IdentityEffect());
1415 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1416 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1418 for (int i = 0; i < size; ++i) {
1419 out_data[i] = linear_to_srgb(out_data[i]);
1422 // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1423 // framebuffer (ideal would be 0.5). That is an order of magnitude better
1424 // than in the linear test above. The RMS error is much better, too.
1425 expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1428 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1429 const int size = 256; // 8-bit.
1430 float data[size], out_data[size];
1432 for (int i = 0; i < size; ++i) {
1433 data[i] = i / double(size - 1);
1436 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1437 tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1438 tester.get_chain()->add_effect(new PassThroughEffect());
1439 tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1440 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1442 // The data should be passed through nearly exactly, since there is no effect
1443 // on the path that requires linear light. (Actually, it _is_ exact modulo
1444 // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1445 expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1448 // An effect that stores which program number was last run under.
1449 class RecordingIdentityEffect : public Effect {
1451 RecordingIdentityEffect() {}
1452 string effect_type_id() const override { return "RecordingIdentityEffect"; }
1453 string output_fragment_shader() override { return read_file("identity.frag"); }
1455 GLuint last_glsl_program_num;
1456 void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1458 last_glsl_program_num = glsl_program_num;
1462 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1468 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1469 RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1470 tester.get_chain()->add_effect(effect);
1471 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1473 expect_equal(data, out_data, 3, 2);
1475 ASSERT_NE(0u, effect->last_glsl_program_num);
1477 // Now pretend some other effect is using this program number;
1478 // ResourcePool will then need to clone it.
1479 ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1480 GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1481 EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1483 // Re-run should still give the correct data, but it should have run
1484 // with a different program.
1485 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1486 expect_equal(data, out_data, 3, 2);
1487 EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1489 // Release the program, and check one final time.
1490 resource_pool->unuse_glsl_program(master_program_num);
1491 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1492 expect_equal(data, out_data, 3, 2);
1495 TEST(ComputeShaderTest, Identity) {
1501 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1502 if (!movit_compute_shaders_supported) {
1503 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1506 tester.get_chain()->add_effect(new IdentityComputeEffect());
1507 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1509 expect_equal(data, out_data, 3, 2);
1512 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1513 // the very last effect in the chain, which means we can't output it directly
1515 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1516 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1519 TEST(ComputeShaderTest, LastEffectInChain) {
1525 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1526 if (!movit_compute_shaders_supported) {
1527 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1530 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1531 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1533 expect_equal(data, out_data, 3, 2);
1536 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1541 uint8_t out_data[6];
1542 EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1543 if (!movit_compute_shaders_supported) {
1544 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1547 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1548 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1549 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1551 expect_equal(data, out_data, 3, 2);
1554 // A compute shader to mirror the inputs, in 2x2 blocks.
1555 class MirrorComputeEffect : public Effect {
1557 MirrorComputeEffect() {}
1558 string effect_type_id() const override { return "MirrorComputeEffect"; }
1559 bool is_compute_shader() const override { return true; }
1560 string output_fragment_shader() override { return read_file("mirror.comp"); }
1561 void get_compute_dimensions(unsigned output_width, unsigned output_height,
1562 unsigned *x, unsigned *y, unsigned *z) const override {
1563 *x = output_width / 2;
1564 *y = output_height / 2;
1569 TEST(ComputeShaderTest, ComputeThenOneToOne) {
1571 0.0f, 0.25f, 0.3f, 0.8f,
1572 0.75f, 1.0f, 1.0f, 0.2f,
1574 float expected_data[] = {
1575 0.8f, 0.3f, 0.25f, 0.0f,
1576 0.2f, 1.0f, 1.0f, 0.75f,
1579 EffectChainTester tester(data, 4, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1580 tester.get_chain()->add_effect(new MirrorComputeEffect());
1581 tester.get_chain()->add_effect(new OneToOneEffect());
1582 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1584 expect_equal(expected_data, out_data, 4, 2);
1587 // A compute shader that also resizes its input, taking the upper-left pixel
1588 // of every 2x2 group. (The shader is hard-coded to 4x2 input for simplicity.)
1589 class Downscale2xComputeEffect : public Effect {
1591 Downscale2xComputeEffect() {}
1592 string effect_type_id() const override { return "Downscale2xComputeEffect"; }
1593 bool is_compute_shader() const override { return true; }
1594 string output_fragment_shader() override { return read_file("downscale2x.comp"); }
1595 bool changes_output_size() const override { return true; }
1596 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override
1598 this->width = width;
1599 this->height = height;
1601 void get_output_size(unsigned *width, unsigned *height,
1602 unsigned *virtual_width, unsigned *virtual_height) const override {
1603 *width = *virtual_width = this->width / 2;
1604 *height = *virtual_height = this->height / 2;
1608 unsigned width, height;
1611 // Even if the compute shader is not the last effect, it's the one that should decide
1612 // the output size of the phase.
1613 TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
1615 0.0f, 0.25f, 0.3f, 0.8f,
1616 0.75f, 1.0f, 1.0f, 0.2f,
1618 float expected_data[] = {
1622 EffectChainTester tester(nullptr, 2, 1);
1623 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 4, 2);
1625 RewritingEffect<Downscale2xComputeEffect> *downscale_effect = new RewritingEffect<Downscale2xComputeEffect>();
1626 tester.get_chain()->add_effect(downscale_effect);
1627 tester.get_chain()->add_effect(new OneToOneEffect());
1628 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1629 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1631 expect_equal(expected_data, out_data, 2, 1);
1633 Phase *phase = downscale_effect->replaced_node->containing_phase;
1634 EXPECT_EQ(2u, phase->output_width);
1635 EXPECT_EQ(1u, phase->output_height);
1638 } // namespace movit