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 if (!movit_compute_shaders_supported) {
133 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
136 tester.get_chain()->add_effect(new IdentityComputeEffect());
138 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
140 expect_equal(expected_data, out_data, 3, 2);
143 // A dummy effect that inverts its input.
144 class InvertEffect : public Effect {
147 string effect_type_id() const override { return "InvertEffect"; }
148 string output_fragment_shader() override { return read_file("invert_effect.frag"); }
150 // A real invert would actually care about its alpha,
151 // but in this unit test, it only complicates things.
152 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
155 // Like IdentityEffect, but rewrites itself out of the loop,
156 // splicing in a different effect instead. Also stores the new node,
157 // so we later can check whatever properties we'd like about the graph.
159 class RewritingEffect : public Effect {
161 template<class... Args>
162 RewritingEffect(Args &&... args) : effect(new T(std::forward<Args>(args)...)), replaced_node(nullptr) {}
163 string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
164 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
165 void rewrite_graph(EffectChain *graph, Node *self) override {
166 replaced_node = graph->add_node(effect);
167 graph->replace_receiver(self, replaced_node);
168 graph->replace_sender(self, replaced_node);
169 self->disabled = true;
176 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
181 float expected_data[6] = {
182 1.0f, 0.9771f, 0.9673f,
186 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
187 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
188 tester.get_chain()->add_effect(effect);
189 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
191 Node *node = effect->replaced_node;
192 ASSERT_EQ(1u, node->incoming_links.size());
193 ASSERT_EQ(1u, node->outgoing_links.size());
194 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
195 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
197 expect_equal(expected_data, out_data, 3, 2);
200 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
201 unsigned char data[] = {
207 float expected_data[] = {
208 1.0000f, 1.0000f, 1.0000f, 1.0000f,
209 0.9771f, 0.9771f, 0.9771f, 1.0000f,
210 0.8983f, 0.8983f, 0.8983f, 1.0000f,
211 0.0000f, 0.0000f, 0.0000f, 1.0000f
213 float out_data[4 * 4];
214 EffectChainTester tester(nullptr, 1, 4);
215 tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
216 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
217 tester.get_chain()->add_effect(effect);
218 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
220 Node *node = effect->replaced_node;
221 ASSERT_EQ(1u, node->incoming_links.size());
222 ASSERT_EQ(1u, node->outgoing_links.size());
223 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
224 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
226 expect_equal(expected_data, out_data, 4, 4);
229 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
234 float expected_data[6] = {
239 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
240 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
241 tester.get_chain()->add_effect(effect);
242 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
244 Node *node = effect->replaced_node;
245 ASSERT_EQ(1u, node->incoming_links.size());
246 ASSERT_EQ(1u, node->outgoing_links.size());
247 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
248 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
250 expect_equal(expected_data, out_data, 3, 2);
253 // A fake input that can change its output colorspace and gamma between instantiation
255 class UnknownColorspaceInput : public FlatInput {
257 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
258 : FlatInput(format, pixel_format, type, width, height),
259 overridden_color_space(format.color_space),
260 overridden_gamma_curve(format.gamma_curve) {}
261 string effect_type_id() const override { return "UnknownColorspaceInput"; }
263 void set_color_space(Colorspace colorspace) {
264 overridden_color_space = colorspace;
266 void set_gamma_curve(GammaCurve gamma_curve) {
267 overridden_gamma_curve = gamma_curve;
269 Colorspace get_color_space() const override { return overridden_color_space; }
270 GammaCurve get_gamma_curve() const override { return overridden_gamma_curve; }
273 Colorspace overridden_color_space;
274 GammaCurve overridden_gamma_curve;
277 TEST(EffectChainTest, HandlesInputChangingColorspace) {
286 float out_data[size];
288 EffectChainTester tester(nullptr, 4, 1, FORMAT_GRAYSCALE);
290 // First say that we have sRGB, linear input.
292 format.color_space = COLORSPACE_sRGB;
293 format.gamma_curve = GAMMA_LINEAR;
295 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
296 input->set_pixel_data(data);
297 tester.get_chain()->add_input(input);
299 // Now we change to Rec. 601 input.
300 input->set_color_space(COLORSPACE_REC_601_625);
301 input->set_gamma_curve(GAMMA_REC_601);
303 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
304 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
305 expect_equal(data, out_data, 4, 1);
308 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
313 float expected_data[6] = {
318 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
319 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
320 tester.get_chain()->add_effect(effect);
321 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
323 Node *node = effect->replaced_node;
324 ASSERT_EQ(1u, node->incoming_links.size());
325 EXPECT_EQ(0u, 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 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
336 float expected_data[6] = {
341 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
342 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
343 tester.get_chain()->add_effect(effect);
344 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
346 Node *node = effect->replaced_node;
347 ASSERT_EQ(1u, node->incoming_links.size());
348 EXPECT_EQ(0u, node->outgoing_links.size());
349 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
351 expect_equal(expected_data, out_data, 3, 2);
354 // The identity effect needs linear light, and thus will get conversions on both sides.
355 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
356 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
358 for (unsigned i = 0; i < 256; ++i) {
362 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
363 tester.get_chain()->add_effect(new IdentityEffect());
364 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
366 expect_equal(data, out_data, 256, 1);
369 // Same, but uses the forward sRGB table from the GPU.
370 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
371 unsigned char data[256];
372 float expected_data[256];
373 for (unsigned i = 0; i < 256; ++i) {
375 expected_data[i] = i / 255.0;
378 EffectChainTester tester(nullptr, 256, 1);
379 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
380 tester.get_chain()->add_effect(new IdentityEffect());
381 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
383 expect_equal(expected_data, out_data, 256, 1);
386 // Same, for the Rec. 601/709 gamma curve.
387 TEST(EffectChainTest, IdentityThroughRec709) {
389 for (unsigned i = 0; i < 256; ++i) {
393 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
394 tester.get_chain()->add_effect(new IdentityEffect());
395 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
397 expect_equal(data, out_data, 256, 1);
400 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
401 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
403 float data[4 * size] = {
404 0.8f, 0.0f, 0.0f, 0.5f,
405 0.0f, 0.2f, 0.2f, 0.3f,
406 0.1f, 0.0f, 1.0f, 1.0f,
408 float out_data[4 * size];
409 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
410 tester.get_chain()->add_effect(new IdentityEffect());
411 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
413 expect_equal(data, out_data, 4, size);
416 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
418 float data[4 * size] = {
419 0.8f, 0.0f, 0.0f, 0.5f,
420 0.0f, 0.2f, 0.2f, 0.3f,
421 0.1f, 0.0f, 1.0f, 1.0f,
423 float expected_data[4 * size] = {
424 0.1f, 0.0f, 1.0f, 1.0f,
425 0.0f, 0.2f, 0.2f, 0.3f,
426 0.8f, 0.0f, 0.0f, 0.5f,
428 float out_data[4 * size];
429 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
430 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
431 tester.get_chain()->add_effect(effect);
432 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
434 Node *node = effect->replaced_node;
435 ASSERT_EQ(1u, node->incoming_links.size());
436 EXPECT_EQ(0u, node->outgoing_links.size());
437 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
439 expect_equal(expected_data, out_data, 4, size);
442 // An input that outputs only blue, which has blank alpha.
443 class BlueInput : public Input {
445 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
446 string effect_type_id() const override { return "IdentityEffect"; }
447 string output_fragment_shader() override { return read_file("blue.frag"); }
448 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
449 bool can_output_linear_gamma() const override { return true; }
450 unsigned get_width() const override { return 1; }
451 unsigned get_height() const override { return 1; }
452 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
453 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
459 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
460 // which outputs blank alpha.
461 class RewritingToBlueInput : public Input {
463 RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
464 string effect_type_id() const override { return "RewritingToBlueInput"; }
465 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
466 void rewrite_graph(EffectChain *graph, Node *self) override {
467 Node *blue_node = graph->add_node(new BlueInput());
468 graph->replace_receiver(self, blue_node);
469 graph->replace_sender(self, blue_node);
471 self->disabled = true;
472 this->blue_node = blue_node;
475 // Dummy values that we need to implement because we inherit from Input.
476 // Same as BlueInput.
477 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
478 bool can_output_linear_gamma() const override { return true; }
479 unsigned get_width() const override { return 1; }
480 unsigned get_height() const override { return 1; }
481 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
482 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
490 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
492 float data[4 * size] = {
493 0.0f, 0.0f, 1.0f, 1.0f,
494 0.0f, 0.0f, 1.0f, 1.0f,
495 0.0f, 0.0f, 1.0f, 1.0f,
497 float out_data[4 * size];
498 EffectChainTester tester(nullptr, size, 1);
499 RewritingToBlueInput *input = new RewritingToBlueInput();
500 tester.get_chain()->add_input(input);
501 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
503 Node *node = input->blue_node;
504 EXPECT_EQ(0u, node->incoming_links.size());
505 EXPECT_EQ(0u, node->outgoing_links.size());
507 expect_equal(data, out_data, 4, size);
510 // An effect that does nothing, and specifies that it preserves blank alpha.
511 class BlankAlphaPreservingEffect : public Effect {
513 BlankAlphaPreservingEffect() {}
514 string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
515 string output_fragment_shader() override { return read_file("identity.frag"); }
516 AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
519 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
521 float data[4 * size] = {
522 0.0f, 0.0f, 1.0f, 1.0f,
523 0.0f, 0.0f, 1.0f, 1.0f,
524 0.0f, 0.0f, 1.0f, 1.0f,
526 float out_data[4 * size];
527 EffectChainTester tester(nullptr, size, 1);
528 tester.get_chain()->add_input(new BlueInput());
529 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
530 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
531 tester.get_chain()->add_effect(effect);
532 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
534 Node *node = effect->replaced_node;
535 EXPECT_EQ(1u, node->incoming_links.size());
536 EXPECT_EQ(0u, node->outgoing_links.size());
538 expect_equal(data, out_data, 4, size);
541 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
542 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
543 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
544 // with other tests.)
545 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
547 float data[4 * size] = {
548 0.0f, 0.0f, 1.0f, 1.0f,
549 0.0f, 0.0f, 1.0f, 1.0f,
550 0.0f, 0.0f, 1.0f, 1.0f,
552 float out_data[4 * size];
553 EffectChainTester tester(nullptr, size, 1);
554 tester.get_chain()->add_input(new BlueInput());
555 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
556 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
557 tester.get_chain()->add_effect(effect);
558 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
560 Node *node = effect->replaced_node;
561 EXPECT_EQ(1u, node->incoming_links.size());
562 EXPECT_EQ(1u, node->outgoing_links.size());
563 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
565 expect_equal(data, out_data, 4, size);
568 // Effectively scales down its input linearly by 4x (and repeating it),
569 // which is not attainable without mipmaps.
570 class MipmapNeedingEffect : public Effect {
572 MipmapNeedingEffect() {}
573 MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
575 // To be allowed to mess with the sampler state.
576 bool needs_texture_bounce() const override { return true; }
578 string effect_type_id() const override { return "MipmapNeedingEffect"; }
579 string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
580 void inform_added(EffectChain *chain) override { this->chain = chain; }
582 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
584 Node *self = chain->find_node_for_effect(this);
585 glActiveTexture(chain->get_input_sampler(self, 0));
587 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
589 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
597 TEST(EffectChainTest, MipmapGenerationWorks) {
598 float data[] = { // In 4x4 blocks.
599 1.0f, 0.0f, 0.0f, 0.0f,
600 0.0f, 0.0f, 0.0f, 0.0f,
601 0.0f, 0.0f, 0.0f, 0.0f,
602 0.0f, 0.0f, 0.0f, 1.0f,
604 0.0f, 0.0f, 0.0f, 0.0f,
605 0.0f, 0.5f, 0.0f, 0.0f,
606 0.0f, 0.0f, 1.0f, 0.0f,
607 0.0f, 0.0f, 0.0f, 0.0f,
609 1.0f, 1.0f, 1.0f, 1.0f,
610 1.0f, 1.0f, 1.0f, 1.0f,
611 1.0f, 1.0f, 1.0f, 1.0f,
612 1.0f, 1.0f, 1.0f, 1.0f,
614 0.0f, 0.0f, 0.0f, 0.0f,
615 0.0f, 1.0f, 1.0f, 0.0f,
616 0.0f, 1.0f, 1.0f, 0.0f,
617 0.0f, 0.0f, 0.0f, 0.0f,
619 float expected_data[] = { // Repeated four times each way.
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 0.125f, 0.125f, 0.125f, 0.125f,
636 0.09375f, 0.09375f, 0.09375f, 0.09375f,
637 1.0f, 1.0f, 1.0f, 1.0f,
638 0.25f, 0.25f, 0.25f, 0.25f,
640 float out_data[4 * 16];
641 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
642 tester.get_chain()->add_effect(new MipmapNeedingEffect());
643 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
645 expect_equal(expected_data, out_data, 4, 16);
648 class NonMipmapCapableInput : public FlatInput {
650 NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
651 : FlatInput(format, pixel_format, type, width, height) {}
653 bool can_supply_mipmaps() const override { return false; }
654 bool set_int(const std::string& key, int value) override {
655 if (key == "needs_mipmaps") {
658 return FlatInput::set_int(key, value);
662 // The same test as MipmapGenerationWorks, but with an input that refuses
663 // to supply mipmaps.
664 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
665 float data[] = { // In 4x4 blocks.
666 1.0f, 0.0f, 0.0f, 0.0f,
667 0.0f, 0.0f, 0.0f, 0.0f,
668 0.0f, 0.0f, 0.0f, 0.0f,
669 0.0f, 0.0f, 0.0f, 1.0f,
671 0.0f, 0.0f, 0.0f, 0.0f,
672 0.0f, 0.5f, 0.0f, 0.0f,
673 0.0f, 0.0f, 1.0f, 0.0f,
674 0.0f, 0.0f, 0.0f, 0.0f,
676 1.0f, 1.0f, 1.0f, 1.0f,
677 1.0f, 1.0f, 1.0f, 1.0f,
678 1.0f, 1.0f, 1.0f, 1.0f,
679 1.0f, 1.0f, 1.0f, 1.0f,
681 0.0f, 0.0f, 0.0f, 0.0f,
682 0.0f, 1.0f, 1.0f, 0.0f,
683 0.0f, 1.0f, 1.0f, 0.0f,
684 0.0f, 0.0f, 0.0f, 0.0f,
686 float expected_data[] = { // Repeated four times each way.
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 0.125f, 0.125f, 0.125f, 0.125f,
703 0.09375f, 0.09375f, 0.09375f, 0.09375f,
704 1.0f, 1.0f, 1.0f, 1.0f,
705 0.25f, 0.25f, 0.25f, 0.25f,
707 float out_data[4 * 16];
708 EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
711 format.color_space = COLORSPACE_sRGB;
712 format.gamma_curve = GAMMA_LINEAR;
714 NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
715 input->set_pixel_data(data);
716 tester.get_chain()->add_input(input);
717 tester.get_chain()->add_effect(new MipmapNeedingEffect());
718 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
720 expect_equal(expected_data, out_data, 4, 16);
723 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
724 float data[] = { // In 4x4 blocks.
725 1.0f, 0.0f, 0.0f, 0.0f,
726 0.0f, 0.0f, 0.0f, 0.0f,
727 0.0f, 0.0f, 0.0f, 0.0f,
728 0.0f, 0.0f, 0.0f, 1.0f,
730 0.0f, 0.0f, 0.0f, 0.0f,
731 0.0f, 0.5f, 0.0f, 0.0f,
732 0.0f, 0.0f, 1.0f, 0.0f,
733 0.0f, 0.0f, 0.0f, 0.0f,
735 1.0f, 1.0f, 1.0f, 1.0f,
736 1.0f, 1.0f, 1.0f, 1.0f,
737 1.0f, 1.0f, 1.0f, 1.0f,
738 1.0f, 1.0f, 1.0f, 1.0f,
740 0.0f, 0.0f, 0.0f, 0.0f,
741 0.0f, 1.0f, 1.0f, 0.0f,
742 0.0f, 1.0f, 1.0f, 0.0f,
743 0.0f, 0.0f, 0.0f, 0.0f,
745 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
746 0.1250f, 0.1250f, 0.1250f, 0.1250f,
747 0.1250f, 0.1250f, 0.1250f, 0.1250f,
748 0.1211f, 0.1211f, 0.1211f, 0.1211f,
749 0.1133f, 0.1133f, 0.1133f, 0.1133f,
750 0.1055f, 0.1055f, 0.1055f, 0.1055f,
751 0.0977f, 0.0977f, 0.0977f, 0.0977f,
752 0.2070f, 0.2070f, 0.2070f, 0.2070f,
753 0.4336f, 0.4336f, 0.4336f, 0.4336f,
754 0.6602f, 0.6602f, 0.6602f, 0.6602f,
755 0.8867f, 0.8867f, 0.8867f, 0.8867f,
756 0.9062f, 0.9062f, 0.9062f, 0.9062f,
757 0.7188f, 0.7188f, 0.7188f, 0.7188f,
758 0.5312f, 0.5312f, 0.5312f, 0.5312f,
759 0.3438f, 0.3438f, 0.3438f, 0.3438f,
760 0.2500f, 0.2500f, 0.2500f, 0.2500f,
761 0.2500f, 0.2500f, 0.2500f, 0.2500f,
763 float out_data[4 * 16];
765 ResizeEffect *downscale = new ResizeEffect();
766 ASSERT_TRUE(downscale->set_int("width", 1));
767 ASSERT_TRUE(downscale->set_int("height", 4));
769 ResizeEffect *upscale = new ResizeEffect();
770 ASSERT_TRUE(upscale->set_int("width", 4));
771 ASSERT_TRUE(upscale->set_int("height", 16));
773 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
774 tester.get_chain()->add_effect(downscale);
775 tester.get_chain()->add_effect(upscale);
776 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
778 expect_equal(expected_data, out_data, 4, 16);
781 // An effect to verify that you can turn off mipmaps; it downscales by two,
782 // which gives blur with mipmaps and aliasing (picks out every other pixel)
784 class Downscale2xEffect : public Effect {
786 explicit Downscale2xEffect(MipmapRequirements mipmap_requirements)
787 : mipmap_requirements(mipmap_requirements)
789 register_vec2("offset", offset);
791 MipmapRequirements needs_mipmaps() const override { return mipmap_requirements; }
793 string effect_type_id() const override { return "Downscale2xEffect"; }
794 string output_fragment_shader() override { return read_file("downscale2x.frag"); }
797 const MipmapRequirements mipmap_requirements;
798 float offset[2] { 0.0f, 0.0f };
801 TEST(EffectChainTest, MipmapChainGetsSplit) {
803 0.0f, 0.0f, 0.0f, 0.0f,
804 1.0f, 0.0f, 1.0f, 0.0f,
805 0.0f, 0.0f, 0.0f, 0.0f,
806 1.0f, 0.0f, 1.0f, 0.0f,
809 // The intermediate result after the first step looks like this,
810 // assuming there are no mipmaps (the zeros are due to border behavior):
817 // so another 2x downscale towards the bottom left will give
822 // with yet more zeros coming in on the top and the right from the border.
823 float expected_data[] = {
824 0.0f, 0.0f, 0.0f, 0.0f,
825 0.0f, 0.0f, 0.0f, 0.0f,
826 0.0f, 0.0f, 0.0f, 0.0f,
827 1.0f, 0.0f, 0.0f, 0.0f,
829 float out_data[4 * 4];
831 float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
832 RewritingEffect<Downscale2xEffect> *pick_out_bottom_left = new RewritingEffect<Downscale2xEffect>(Effect::CANNOT_ACCEPT_MIPMAPS);
833 ASSERT_TRUE(pick_out_bottom_left->effect->set_vec2("offset", offset));
835 RewritingEffect<Downscale2xEffect> *downscale2x = new RewritingEffect<Downscale2xEffect>(Effect::NEEDS_MIPMAPS);
837 EffectChainTester tester(data, 4, 4, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
838 tester.get_chain()->add_effect(pick_out_bottom_left);
839 tester.get_chain()->add_effect(downscale2x);
840 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
842 EXPECT_NE(pick_out_bottom_left->replaced_node->containing_phase,
843 downscale2x->replaced_node->containing_phase);
845 expect_equal(expected_data, out_data, 4, 4);
848 // An effect that adds its two inputs together. Used below.
849 class AddEffect : public Effect {
852 string effect_type_id() const override { return "AddEffect"; }
853 string output_fragment_shader() override { return read_file("add.frag"); }
854 unsigned num_inputs() const override { return 2; }
855 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
858 // Constructs the graph
862 // MultiplyEffect MultiplyEffect |
866 // and verifies that it gives the correct output.
867 TEST(EffectChainTest, DiamondGraph) {
872 float expected_data[] = {
876 float out_data[2 * 2];
878 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
879 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
881 MultiplyEffect *mul_half = new MultiplyEffect();
882 ASSERT_TRUE(mul_half->set_vec4("factor", half));
884 MultiplyEffect *mul_two = new MultiplyEffect();
885 ASSERT_TRUE(mul_two->set_vec4("factor", two));
887 EffectChainTester tester(nullptr, 2, 2);
890 format.color_space = COLORSPACE_sRGB;
891 format.gamma_curve = GAMMA_LINEAR;
893 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
894 input->set_pixel_data(data);
896 tester.get_chain()->add_input(input);
897 tester.get_chain()->add_effect(mul_half, input);
898 tester.get_chain()->add_effect(mul_two, input);
899 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
900 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
902 expect_equal(expected_data, out_data, 2, 2);
905 // Constructs the graph
909 // MultiplyEffect MultiplyEffect |
911 // \ BouncingIdentityEffect |
915 // and verifies that it gives the correct output.
916 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
921 float expected_data[] = {
925 float out_data[2 * 2];
927 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
928 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
930 MultiplyEffect *mul_half = new MultiplyEffect();
931 ASSERT_TRUE(mul_half->set_vec4("factor", half));
933 MultiplyEffect *mul_two = new MultiplyEffect();
934 ASSERT_TRUE(mul_two->set_vec4("factor", two));
936 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
938 EffectChainTester tester(nullptr, 2, 2);
941 format.color_space = COLORSPACE_sRGB;
942 format.gamma_curve = GAMMA_LINEAR;
944 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
945 input->set_pixel_data(data);
947 tester.get_chain()->add_input(input);
948 tester.get_chain()->add_effect(mul_half, input);
949 tester.get_chain()->add_effect(mul_two, input);
950 tester.get_chain()->add_effect(bounce, mul_two);
951 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
952 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
954 expect_equal(expected_data, out_data, 2, 2);
957 // Constructs the graph
961 // Downscale2xEffect (mipmaps) Downscale2xEffect (no mipmaps) |
963 // Downscale2xEffect (mipmaps) Downscale2xEffect (no mipmaps) |
967 // and verifies that it gives the correct output. Due to the conflicting
968 // mipmap demands, EffectChain needs to make two phases; exactly where it's
969 // split is less important, though (this is a fairly obscure situation that
970 // is unlikely to happen in practice).
971 TEST(EffectChainTest, DiamondGraphWithConflictingMipmaps) {
973 0.0f, 0.0f, 0.0f, 0.0f,
974 1.0f, 0.0f, 1.0f, 0.0f,
975 0.0f, 0.0f, 0.0f, 0.0f,
976 1.0f, 0.0f, 1.0f, 0.0f,
979 // Same situation as MipmapChainGetsSplit. The output of the two
980 // downscales with no mipmaps looks like this:
987 // and the one with mipmaps is 0.25 everywhere. Due to postmultiplied
988 // alpha, we get the average even though we are using AddEffect.
989 float expected_data[] = {
990 0.125f, 0.125f, 0.125f, 0.125f,
991 0.125f, 0.125f, 0.125f, 0.125f,
992 0.125f, 0.125f, 0.125f, 0.125f,
993 0.625f, 0.125f, 0.125f, 0.125f,
995 float out_data[4 * 4];
997 float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
998 Downscale2xEffect *nomipmap1 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
999 Downscale2xEffect *nomipmap2 = new Downscale2xEffect(Effect::CANNOT_ACCEPT_MIPMAPS);
1000 ASSERT_TRUE(nomipmap1->set_vec2("offset", offset));
1001 ASSERT_TRUE(nomipmap2->set_vec2("offset", offset));
1003 Downscale2xEffect *mipmap1 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1004 Downscale2xEffect *mipmap2 = new Downscale2xEffect(Effect::NEEDS_MIPMAPS);
1006 EffectChainTester tester(nullptr, 4, 4);
1009 format.color_space = COLORSPACE_sRGB;
1010 format.gamma_curve = GAMMA_LINEAR;
1012 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 4);
1013 input->set_pixel_data(data);
1015 tester.get_chain()->add_input(input);
1017 tester.get_chain()->add_effect(nomipmap1, input);
1018 tester.get_chain()->add_effect(nomipmap2, nomipmap1);
1020 tester.get_chain()->add_effect(mipmap1, input);
1021 tester.get_chain()->add_effect(mipmap2, mipmap1);
1023 tester.get_chain()->add_effect(new AddEffect(), nomipmap2, mipmap2);
1024 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1026 expect_equal(expected_data, out_data, 4, 4);
1029 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
1034 float expected_data[] = {
1035 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1038 float out_data[2 * 2];
1040 EffectChainTester tester(nullptr, 2, 2);
1041 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
1043 // MirrorEffect does not get linear light, so the conversions will be
1044 // inserted after it, not before.
1045 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1046 tester.get_chain()->add_effect(effect);
1048 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1049 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1050 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1051 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1053 expect_equal(expected_data, out_data, 2, 2);
1055 Node *node = effect->replaced_node;
1056 ASSERT_EQ(1u, node->incoming_links.size());
1057 ASSERT_EQ(1u, node->outgoing_links.size());
1058 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1059 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
1062 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
1067 float expected_data[] = {
1068 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
1071 float out_data[2 * 2];
1073 EffectChainTester tester(nullptr, 2, 2);
1074 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
1076 // MirrorEffect does not get linear light, so the conversions will be
1077 // inserted after it, not before.
1078 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1079 tester.get_chain()->add_effect(effect);
1081 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1082 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1083 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1084 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1086 expect_equal(expected_data, out_data, 2, 2);
1088 Node *node = effect->replaced_node;
1089 ASSERT_EQ(1u, node->incoming_links.size());
1090 ASSERT_EQ(1u, node->outgoing_links.size());
1091 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1092 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
1095 // An effect that does nothing, but requests texture bounce and stores
1097 class SizeStoringEffect : public BouncingIdentityEffect {
1099 SizeStoringEffect() : input_width(-1), input_height(-1) {}
1100 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
1101 assert(input_num == 0);
1102 input_width = width;
1103 input_height = height;
1105 string effect_type_id() const override { return "SizeStoringEffect"; }
1107 int input_width, input_height;
1110 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
1111 float data[2 * 2] = {
1115 float out_data[4 * 3];
1117 EffectChainTester tester(nullptr, 4, 3); // Note non-square aspect.
1120 format.color_space = COLORSPACE_sRGB;
1121 format.gamma_curve = GAMMA_LINEAR;
1123 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1124 input1->set_pixel_data(data);
1126 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1127 input2->set_pixel_data(data);
1129 SizeStoringEffect *input_store = new SizeStoringEffect();
1131 tester.get_chain()->add_input(input1);
1132 tester.get_chain()->add_input(input2);
1133 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1134 tester.get_chain()->add_effect(input_store);
1135 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1137 EXPECT_EQ(2, input_store->input_width);
1138 EXPECT_EQ(2, input_store->input_height);
1141 TEST(EffectChainTest, AspectRatioConversion) {
1142 float data1[4 * 3] = {
1143 0.0f, 0.0f, 0.0f, 0.0f,
1144 0.0f, 0.0f, 0.0f, 0.0f,
1145 0.0f, 0.0f, 0.0f, 0.0f,
1147 float data2[7 * 7] = {
1148 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1149 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1150 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1151 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
1152 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1153 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1154 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1157 // The right conversion here is that the 7x7 image decides the size,
1158 // since it is the biggest, so everything is scaled up to 9x7
1159 // (keep the height, round the width 9.333 to 9).
1160 float out_data[9 * 7];
1162 EffectChainTester tester(nullptr, 4, 3);
1165 format.color_space = COLORSPACE_sRGB;
1166 format.gamma_curve = GAMMA_LINEAR;
1168 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1169 input1->set_pixel_data(data1);
1171 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1172 input2->set_pixel_data(data2);
1174 SizeStoringEffect *input_store = new SizeStoringEffect();
1176 tester.get_chain()->add_input(input1);
1177 tester.get_chain()->add_input(input2);
1178 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1179 tester.get_chain()->add_effect(input_store);
1180 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1182 EXPECT_EQ(9, input_store->input_width);
1183 EXPECT_EQ(7, input_store->input_height);
1186 // Tests that putting a BlueInput (constant color) into its own pass,
1187 // which creates a phase that doesn't need texture coordinates,
1188 // doesn't mess up a second phase that actually does.
1189 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1195 float expected_data[] = {
1196 1.0f, 1.0f, 2.0f, 2.0f,
1197 0.0f, 0.0f, 1.0f, 2.0f,
1199 float out_data[size * 4];
1200 // First say that we have sRGB, linear input.
1202 format.color_space = COLORSPACE_sRGB;
1203 format.gamma_curve = GAMMA_LINEAR;
1204 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1206 input->set_pixel_data(data);
1207 EffectChainTester tester(nullptr, 1, size);
1208 tester.get_chain()->add_input(new BlueInput());
1209 Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1210 tester.get_chain()->add_input(input);
1211 tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1213 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1215 expect_equal(expected_data, out_data, 4, size);
1218 // An effect that does nothing except changing its output sizes.
1219 class VirtualResizeEffect : public Effect {
1221 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1224 virtual_width(virtual_width),
1225 virtual_height(virtual_height) {}
1226 string effect_type_id() const override { return "VirtualResizeEffect"; }
1227 string output_fragment_shader() override { return read_file("identity.frag"); }
1229 bool changes_output_size() const override { return true; }
1231 void get_output_size(unsigned *width, unsigned *height,
1232 unsigned *virtual_width, unsigned *virtual_height) const override {
1233 *width = this->width;
1234 *height = this->height;
1235 *virtual_width = this->virtual_width;
1236 *virtual_height = this->virtual_height;
1240 int width, height, virtual_width, virtual_height;
1243 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1244 const int size = 2, bigger_size = 3;
1245 float data[size * size] = {
1249 float out_data[size * size];
1251 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1253 SizeStoringEffect *size_store = new SizeStoringEffect();
1255 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1256 tester.get_chain()->add_effect(size_store);
1257 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1258 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1260 EXPECT_EQ(bigger_size, size_store->input_width);
1261 EXPECT_EQ(bigger_size, size_store->input_height);
1263 // If the resize is implemented as non-virtual, we'll fail here,
1264 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1265 expect_equal(data, out_data, size, size);
1268 // An effect that is like VirtualResizeEffect, but always has virtual and real
1269 // sizes the same (and promises this).
1270 class NonVirtualResizeEffect : public VirtualResizeEffect {
1272 NonVirtualResizeEffect(int width, int height)
1273 : VirtualResizeEffect(width, height, width, height) {}
1274 string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1275 bool sets_virtual_output_size() const override { return false; }
1278 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1279 class OneToOneEffect : public Effect {
1282 string effect_type_id() const override { return "OneToOneEffect"; }
1283 string output_fragment_shader() override { return read_file("identity.frag"); }
1284 bool strong_one_to_one_sampling() const override { return true; }
1287 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1289 float data[size * size] = {
1293 float out_data[size * size];
1295 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1297 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1298 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1300 if (GetParam() == "compute") {
1301 if (!movit_compute_shaders_supported) {
1302 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1305 tester.get_chain()->add_effect(new IdentityComputeEffect());
1307 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1309 tester.get_chain()->add_effect(effect1);
1310 tester.get_chain()->add_effect(effect2);
1311 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1313 expect_equal(data, out_data, size, size);
1315 // The first OneToOneEffect should be in the same phase as its input.
1316 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1317 EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1318 effect1->replaced_node->containing_phase);
1320 // The second OneToOneEffect, too.
1321 EXPECT_EQ(effect1->replaced_node->containing_phase,
1322 effect2->replaced_node->containing_phase);
1325 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1327 float data[size * size] = {
1331 float out_data[size * size];
1333 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1335 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1336 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1337 RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1338 RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1340 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1341 tester.get_chain()->add_effect(effect1);
1342 tester.get_chain()->add_effect(effect2);
1343 tester.get_chain()->add_effect(effect3);
1344 tester.get_chain()->add_effect(effect4);
1345 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1347 expect_equal(data, out_data, size, size);
1349 // The NonVirtualResizeEffect should be in a different phase from
1350 // the IdentityEffect (since the latter is not one-to-one),
1351 // ie., the chain should be broken somewhere between them, but exactly
1352 // where doesn't matter.
1353 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1354 EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1355 effect3->replaced_node->containing_phase);
1357 // The last OneToOneEffect should also be in the same phase as the
1358 // IdentityEffect (the phase was already broken).
1359 EXPECT_EQ(effect3->replaced_node->containing_phase,
1360 effect4->replaced_node->containing_phase);
1363 // Does not use EffectChainTest, so that it can construct an EffectChain without
1364 // a shared ResourcePool (which is also properly destroyed afterwards).
1365 // Also turns on debugging to test that code path.
1366 TEST(EffectChainTest, IdentityWithOwnPool) {
1367 const int width = 3, height = 2;
1372 const float expected_data[] = {
1376 float out_data[6], temp[6 * 4];
1378 EffectChain chain(width, height);
1379 MovitDebugLevel old_movit_debug_level = movit_debug_level;
1380 movit_debug_level = MOVIT_DEBUG_ON;
1383 format.color_space = COLORSPACE_sRGB;
1384 format.gamma_curve = GAMMA_LINEAR;
1386 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1387 input->set_pixel_data(data);
1388 chain.add_input(input);
1389 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1392 glGenTextures(1, &texnum);
1394 glBindTexture(GL_TEXTURE_2D, texnum);
1396 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1399 glGenFramebuffers(1, &fbo);
1401 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1403 glFramebufferTexture2D(
1405 GL_COLOR_ATTACHMENT0,
1410 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1415 chain.render_to_fbo(fbo, width, height);
1417 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1419 glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1421 for (unsigned i = 0; i < 6; ++i) {
1422 out_data[i] = temp[i * 4];
1425 expect_equal(expected_data, out_data, width, height);
1427 // Reset the debug status again.
1428 movit_debug_level = old_movit_debug_level;
1431 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1432 class PrintfingBlueEffect : public Effect {
1434 PrintfingBlueEffect() {}
1435 string effect_type_id() const override { return "PrintfingBlueEffect"; }
1436 string output_fragment_shader() override {
1438 ss.imbue(locale("C"));
1440 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1441 << 0.0f << ", " << 0.0f << ", "
1442 << 0.5f << ", " << 1.0f << "); }\n";
1447 TEST(EffectChainTest, StringStreamLocalesWork) {
1448 // An example of a locale with comma instead of period as decimal separator.
1449 // Obviously, if you run on a machine without this locale available,
1450 // the test will always succeed. Note that the OpenGL driver might call
1451 // setlocale() behind-the-scenes, and that might corrupt the returned
1452 // pointer, so we need to take our own copy of it here.
1453 char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1454 if (saved_locale == nullptr) {
1455 // The locale wasn't available.
1458 saved_locale = strdup(saved_locale);
1460 0.0f, 0.0f, 0.0f, 0.0f,
1462 float expected_data[] = {
1463 0.0f, 0.0f, 0.5f, 1.0f,
1466 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1467 tester.get_chain()->add_effect(new PrintfingBlueEffect());
1468 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1470 expect_equal(expected_data, out_data, 4, 1);
1472 setlocale(LC_ALL, saved_locale);
1476 TEST(EffectChainTest, sRGBIntermediate) {
1478 0.0f, 0.5f, 0.0f, 1.0f,
1481 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1482 tester.get_chain()->set_intermediate_format(GL_SRGB8);
1483 tester.get_chain()->add_effect(new IdentityEffect());
1484 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1485 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1487 EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1488 << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1489 EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1490 << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1492 // This state should have been preserved.
1493 EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1496 // An effect that is like IdentityEffect, but also does not require linear light.
1497 class PassThroughEffect : public IdentityEffect {
1499 PassThroughEffect() {}
1500 string effect_type_id() const override { return "PassThroughEffect"; }
1501 bool needs_linear_light() const override { return false; }
1502 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1505 // Same, just also bouncing.
1506 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1508 BouncingPassThroughEffect() {}
1509 string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1510 bool needs_linear_light() const override { return false; }
1511 bool needs_texture_bounce() const override { return true; }
1512 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1515 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1516 // Note that we do the comparison in sRGB space, which is what we
1517 // typically would want; however, we do the sRGB conversion ourself
1518 // to avoid compounding errors from shader conversions into the
1520 const int size = 4096; // 12-bit.
1521 float linear_data[size], data[size], out_data[size];
1523 for (int i = 0; i < size; ++i) {
1524 linear_data[i] = i / double(size - 1);
1525 data[i] = srgb_to_linear(linear_data[i]);
1528 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1529 tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1530 tester.get_chain()->add_effect(new IdentityEffect());
1531 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1532 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1534 for (int i = 0; i < size; ++i) {
1535 out_data[i] = linear_to_srgb(out_data[i]);
1538 // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1539 // framebuffer. (Slightly more on NVIDIA cards.)
1540 expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1543 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1544 // Note that we do the comparison in sRGB space, which is what we
1545 // typically would want; however, we do the sRGB conversion ourself
1546 // to avoid compounding errors from shader conversions into the
1548 const int size = 4096; // 12-bit.
1549 float linear_data[size], data[size], out_data[size];
1551 for (int i = 0; i < size; ++i) {
1552 linear_data[i] = i / double(size - 1);
1553 data[i] = srgb_to_linear(linear_data[i]);
1556 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1557 tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1558 if (GetParam() == "compute") {
1559 if (!movit_compute_shaders_supported) {
1560 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1563 tester.get_chain()->add_effect(new IdentityComputeEffect());
1565 tester.get_chain()->add_effect(new IdentityEffect());
1567 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1568 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1570 for (int i = 0; i < size; ++i) {
1571 out_data[i] = linear_to_srgb(out_data[i]);
1574 // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1575 // framebuffer (ideal would be 0.5). That is an order of magnitude better
1576 // than in the linear test above. The RMS error is much better, too.
1577 expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1580 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1581 const int size = 256; // 8-bit.
1582 float data[size], out_data[size];
1584 for (int i = 0; i < size; ++i) {
1585 data[i] = i / double(size - 1);
1588 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1589 tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1590 tester.get_chain()->add_effect(new PassThroughEffect());
1591 tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1592 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1594 // The data should be passed through nearly exactly, since there is no effect
1595 // on the path that requires linear light. (Actually, it _is_ exact modulo
1596 // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1597 expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1600 // An effect that stores which program number was last run under.
1601 class RecordingIdentityEffect : public Effect {
1603 RecordingIdentityEffect() {}
1604 string effect_type_id() const override { return "RecordingIdentityEffect"; }
1605 string output_fragment_shader() override { return read_file("identity.frag"); }
1607 GLuint last_glsl_program_num;
1608 void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1610 last_glsl_program_num = glsl_program_num;
1614 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1620 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1621 RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1622 tester.get_chain()->add_effect(effect);
1623 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1625 expect_equal(data, out_data, 3, 2);
1627 ASSERT_NE(0u, effect->last_glsl_program_num);
1629 // Now pretend some other effect is using this program number;
1630 // ResourcePool will then need to clone it.
1631 ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1632 GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1633 EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1635 // Re-run should still give the correct data, but it should have run
1636 // with a different program.
1637 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1638 expect_equal(data, out_data, 3, 2);
1639 EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1641 // Release the program, and check one final time.
1642 resource_pool->unuse_glsl_program(master_program_num);
1643 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1644 expect_equal(data, out_data, 3, 2);
1647 TEST(ComputeShaderTest, Identity) {
1653 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1654 if (!movit_compute_shaders_supported) {
1655 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1658 tester.get_chain()->add_effect(new IdentityComputeEffect());
1659 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1661 expect_equal(data, out_data, 3, 2);
1664 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1665 // the very last effect in the chain, which means we can't output it directly
1667 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1668 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1671 TEST(ComputeShaderTest, LastEffectInChain) {
1677 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1678 if (!movit_compute_shaders_supported) {
1679 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1682 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1683 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1685 expect_equal(data, out_data, 3, 2);
1688 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1693 uint8_t out_data[6];
1694 EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1695 if (!movit_compute_shaders_supported) {
1696 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1699 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1700 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1701 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1703 expect_equal(data, out_data, 3, 2);
1706 // A compute shader to mirror the inputs, in 2x2 blocks.
1707 class MirrorComputeEffect : public Effect {
1709 MirrorComputeEffect() {}
1710 string effect_type_id() const override { return "MirrorComputeEffect"; }
1711 bool is_compute_shader() const override { return true; }
1712 string output_fragment_shader() override { return read_file("mirror.comp"); }
1713 void get_compute_dimensions(unsigned output_width, unsigned output_height,
1714 unsigned *x, unsigned *y, unsigned *z) const override {
1715 *x = output_width / 2;
1716 *y = output_height / 2;
1721 TEST(ComputeShaderTest, ComputeThenOneToOne) {
1723 0.0f, 0.25f, 0.3f, 0.8f,
1724 0.75f, 1.0f, 1.0f, 0.2f,
1726 float expected_data[] = {
1727 0.8f, 0.3f, 0.25f, 0.0f,
1728 0.2f, 1.0f, 1.0f, 0.75f,
1731 EffectChainTester tester(data, 4, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1732 if (!movit_compute_shaders_supported) {
1733 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1736 tester.get_chain()->add_effect(new MirrorComputeEffect());
1737 tester.get_chain()->add_effect(new OneToOneEffect());
1738 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1740 expect_equal(expected_data, out_data, 4, 2);
1743 // A compute shader that also resizes its input, taking the upper-left pixel
1744 // of every 2x2 group. (The shader is hard-coded to 4x2 input for simplicity.)
1745 class Downscale2xComputeEffect : public Effect {
1747 Downscale2xComputeEffect() {}
1748 string effect_type_id() const override { return "Downscale2xComputeEffect"; }
1749 bool is_compute_shader() const override { return true; }
1750 string output_fragment_shader() override { return read_file("downscale2x.comp"); }
1751 bool changes_output_size() const override { return true; }
1752 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override
1754 this->width = width;
1755 this->height = height;
1757 void get_output_size(unsigned *width, unsigned *height,
1758 unsigned *virtual_width, unsigned *virtual_height) const override {
1759 *width = *virtual_width = this->width / 2;
1760 *height = *virtual_height = this->height / 2;
1764 unsigned width, height;
1767 // Even if the compute shader is not the last effect, it's the one that should decide
1768 // the output size of the phase.
1769 TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
1771 0.0f, 0.25f, 0.3f, 0.8f,
1772 0.75f, 1.0f, 1.0f, 0.2f,
1774 float expected_data[] = {
1778 EffectChainTester tester(nullptr, 2, 1);
1779 if (!movit_compute_shaders_supported) {
1780 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1783 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 4, 2);
1785 RewritingEffect<Downscale2xComputeEffect> *downscale_effect = new RewritingEffect<Downscale2xComputeEffect>();
1786 tester.get_chain()->add_effect(downscale_effect);
1787 tester.get_chain()->add_effect(new OneToOneEffect());
1788 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1789 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1791 expect_equal(expected_data, out_data, 2, 1);
1793 Phase *phase = downscale_effect->replaced_node->containing_phase;
1794 EXPECT_EQ(2u, phase->output_width);
1795 EXPECT_EQ(1u, phase->output_height);
1798 class StrongOneToOneAddEffect : public AddEffect {
1800 StrongOneToOneAddEffect() {}
1801 string effect_type_id() const override { return "StrongOneToOneAddEffect"; }
1802 bool strong_one_to_one_sampling() const override { return true; }
1805 TEST(ComputeShaderTest, NoTwoComputeInSamePhase) {
1807 0.0f, 0.25f, 0.3f, 0.8f,
1808 0.75f, 1.0f, 1.0f, 0.2f,
1810 float expected_data[] = {
1816 format.color_space = COLORSPACE_sRGB;
1817 format.gamma_curve = GAMMA_LINEAR;
1819 EffectChainTester tester(nullptr, 2, 1);
1820 if (!movit_compute_shaders_supported) {
1821 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1825 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1826 input1->set_pixel_data(data);
1827 tester.get_chain()->add_input(input1);
1828 Effect *downscale1 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1830 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1831 input2->set_pixel_data(data);
1832 tester.get_chain()->add_input(input2);
1833 Effect *downscale2 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1835 tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), downscale1, downscale2);
1836 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1837 expect_equal(expected_data, out_data, 2, 1);
1840 // Like the previous test, but the adder effect is not directly connected
1841 // to the compute shaders (so the status has to be propagated through those effects).
1842 TEST(ComputeShaderTest, NoTwoComputeInSamePhaseIndirect) {
1844 0.0f, 0.25f, 0.3f, 0.8f,
1845 0.75f, 1.0f, 1.0f, 0.2f,
1847 float expected_data[] = {
1853 format.color_space = COLORSPACE_sRGB;
1854 format.gamma_curve = GAMMA_LINEAR;
1856 EffectChainTester tester(nullptr, 2, 1);
1857 if (!movit_compute_shaders_supported) {
1858 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1862 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1863 input1->set_pixel_data(data);
1864 tester.get_chain()->add_input(input1);
1865 tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1866 Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1868 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1869 input2->set_pixel_data(data);
1870 tester.get_chain()->add_input(input2);
1871 tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1872 Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1874 tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), identity1, identity2);
1875 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1876 expect_equal(expected_data, out_data, 2, 1);
1879 // Like the previous test, but the adder is not strong one-to-one
1880 // (so there are two different compute shader inputs, but none of them
1881 // are in the same phase).
1882 TEST(ComputeShaderTest, BounceTextureFromTwoComputeShaders) {
1884 0.0f, 0.25f, 0.3f, 0.8f,
1885 0.75f, 1.0f, 1.0f, 0.2f,
1887 float expected_data[] = {
1893 format.color_space = COLORSPACE_sRGB;
1894 format.gamma_curve = GAMMA_LINEAR;
1896 EffectChainTester tester(nullptr, 2, 1);
1897 if (!movit_compute_shaders_supported) {
1898 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1902 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1903 input1->set_pixel_data(data);
1904 tester.get_chain()->add_input(input1);
1905 tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1906 Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
1908 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1909 input2->set_pixel_data(data);
1910 tester.get_chain()->add_input(input2);
1911 tester.get_chain()->add_effect(new Downscale2xComputeEffect());
1912 Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
1914 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1915 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1916 expect_equal(expected_data, out_data, 2, 1);
1919 // Requires mipmaps, but is otherwise like IdentityEffect.
1920 class MipmapNeedingIdentityEffect : public IdentityEffect {
1922 MipmapNeedingIdentityEffect() {}
1923 MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
1924 string effect_type_id() const override { return "MipmapNeedingIdentityEffect"; }
1925 bool strong_one_to_one_sampling() const override { return true; }
1928 TEST(ComputeShaderTest, StrongOneToOneButStillNotChained) {
1930 0.0f, 0.25f, 0.3f, 0.8f,
1931 0.75f, 1.0f, 1.0f, 0.2f,
1936 format.color_space = COLORSPACE_sRGB;
1937 format.gamma_curve = GAMMA_LINEAR;
1939 EffectChainTester tester(nullptr, 4, 2);
1940 if (!movit_compute_shaders_supported) {
1941 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1945 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1946 input1->set_pixel_data(data);
1947 tester.get_chain()->add_input(input1);
1948 Effect *compute_effect = tester.get_chain()->add_effect(new IdentityComputeEffect());
1950 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
1951 input2->set_pixel_data(data);
1952 tester.get_chain()->add_input(input2);
1954 // Not chained with the compute shader because MipmapNeedingIdentityEffect comes in
1955 // the same phase, and compute shaders cannot supply mipmaps.
1956 tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), compute_effect, input2);
1957 tester.get_chain()->add_effect(new MipmapNeedingIdentityEffect());
1959 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1960 expect_equal(data, out_data, 4, 2);
1963 TEST(EffectChainTest, BounceResetsMipmapNeeds) {
1971 format.color_space = COLORSPACE_sRGB;
1972 format.gamma_curve = GAMMA_LINEAR;
1974 NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1975 input->set_pixel_data(data);
1977 RewritingEffect<IdentityEffect> *identity = new RewritingEffect<IdentityEffect>();
1979 RewritingEffect<ResizeEffect> *downscale = new RewritingEffect<ResizeEffect>(); // Needs mipmaps.
1980 ASSERT_TRUE(downscale->effect->set_int("width", 1));
1981 ASSERT_TRUE(downscale->effect->set_int("height", 1));
1983 EffectChainTester tester(nullptr, 1, 1);
1984 tester.get_chain()->add_input(input);
1985 tester.get_chain()->add_effect(identity);
1986 tester.get_chain()->add_effect(downscale);
1987 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1989 Node *input_node = identity->replaced_node->incoming_links[0];
1991 // The ResizeEffect needs mipmaps. Normally, that would mean that it should
1992 // propagate this tatus down through the IdentityEffect. However, since we
1993 // bounce (due to the resize), the dependency breaks there, and we don't
1994 // need to bounce again between the input and the IdentityEffect.
1995 EXPECT_EQ(input_node->containing_phase,
1996 identity->replaced_node->containing_phase);
1997 EXPECT_NE(identity->replaced_node->containing_phase,
1998 downscale->replaced_node->containing_phase);
2001 } // namespace movit