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 template<class... Args>
158 RewritingEffect(Args &&... args) : effect(new T(std::forward<Args>(args)...)), replaced_node(nullptr) {}
159 string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
160 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
161 void rewrite_graph(EffectChain *graph, Node *self) override {
162 replaced_node = graph->add_node(effect);
163 graph->replace_receiver(self, replaced_node);
164 graph->replace_sender(self, replaced_node);
165 self->disabled = true;
172 TEST(EffectChainTest, RewritingWorksAndGammaConversionsAreInserted) {
177 float expected_data[6] = {
178 1.0f, 0.9771f, 0.9673f,
182 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
183 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
184 tester.get_chain()->add_effect(effect);
185 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
187 Node *node = effect->replaced_node;
188 ASSERT_EQ(1u, node->incoming_links.size());
189 ASSERT_EQ(1u, node->outgoing_links.size());
190 EXPECT_EQ("GammaExpansionEffect", node->incoming_links[0]->effect->effect_type_id());
191 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
193 expect_equal(expected_data, out_data, 3, 2);
196 TEST(EffectChainTest, RewritingWorksAndTexturesAreAskedForsRGB) {
197 unsigned char data[] = {
203 float expected_data[] = {
204 1.0000f, 1.0000f, 1.0000f, 1.0000f,
205 0.9771f, 0.9771f, 0.9771f, 1.0000f,
206 0.8983f, 0.8983f, 0.8983f, 1.0000f,
207 0.0000f, 0.0000f, 0.0000f, 1.0000f
209 float out_data[4 * 4];
210 EffectChainTester tester(nullptr, 1, 4);
211 tester.add_input(data, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_sRGB);
212 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
213 tester.get_chain()->add_effect(effect);
214 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB);
216 Node *node = effect->replaced_node;
217 ASSERT_EQ(1u, node->incoming_links.size());
218 ASSERT_EQ(1u, node->outgoing_links.size());
219 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
220 EXPECT_EQ("GammaCompressionEffect", node->outgoing_links[0]->effect->effect_type_id());
222 expect_equal(expected_data, out_data, 4, 4);
225 TEST(EffectChainTest, RewritingWorksAndColorspaceConversionsAreInserted) {
230 float expected_data[6] = {
235 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
236 RewritingEffect<InvertEffect> *effect = new RewritingEffect<InvertEffect>();
237 tester.get_chain()->add_effect(effect);
238 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
240 Node *node = effect->replaced_node;
241 ASSERT_EQ(1u, node->incoming_links.size());
242 ASSERT_EQ(1u, node->outgoing_links.size());
243 EXPECT_EQ("ColorspaceConversionEffect", node->incoming_links[0]->effect->effect_type_id());
244 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
246 expect_equal(expected_data, out_data, 3, 2);
249 // A fake input that can change its output colorspace and gamma between instantiation
251 class UnknownColorspaceInput : public FlatInput {
253 UnknownColorspaceInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
254 : FlatInput(format, pixel_format, type, width, height),
255 overridden_color_space(format.color_space),
256 overridden_gamma_curve(format.gamma_curve) {}
257 string effect_type_id() const override { return "UnknownColorspaceInput"; }
259 void set_color_space(Colorspace colorspace) {
260 overridden_color_space = colorspace;
262 void set_gamma_curve(GammaCurve gamma_curve) {
263 overridden_gamma_curve = gamma_curve;
265 Colorspace get_color_space() const override { return overridden_color_space; }
266 GammaCurve get_gamma_curve() const override { return overridden_gamma_curve; }
269 Colorspace overridden_color_space;
270 GammaCurve overridden_gamma_curve;
273 TEST(EffectChainTest, HandlesInputChangingColorspace) {
282 float out_data[size];
284 EffectChainTester tester(nullptr, 4, 1, FORMAT_GRAYSCALE);
286 // First say that we have sRGB, linear input.
288 format.color_space = COLORSPACE_sRGB;
289 format.gamma_curve = GAMMA_LINEAR;
291 UnknownColorspaceInput *input = new UnknownColorspaceInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 1);
292 input->set_pixel_data(data);
293 tester.get_chain()->add_input(input);
295 // Now we change to Rec. 601 input.
296 input->set_color_space(COLORSPACE_REC_601_625);
297 input->set_gamma_curve(GAMMA_REC_601);
299 // Now ask for Rec. 601 output. Thus, our chain should now be a no-op.
300 tester.run(out_data, GL_RED, COLORSPACE_REC_601_625, GAMMA_REC_601);
301 expect_equal(data, out_data, 4, 1);
304 TEST(EffectChainTest, NoGammaConversionsWhenLinearLightNotNeeded) {
309 float expected_data[6] = {
314 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
315 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
316 tester.get_chain()->add_effect(effect);
317 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
319 Node *node = effect->replaced_node;
320 ASSERT_EQ(1u, node->incoming_links.size());
321 EXPECT_EQ(0u, node->outgoing_links.size());
322 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
324 expect_equal(expected_data, out_data, 3, 2);
327 TEST(EffectChainTest, NoColorspaceConversionsWhensRGBPrimariesNotNeeded) {
332 float expected_data[6] = {
337 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_REC_601_525, GAMMA_LINEAR);
338 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
339 tester.get_chain()->add_effect(effect);
340 tester.run(out_data, GL_RED, COLORSPACE_REC_601_525, GAMMA_LINEAR);
342 Node *node = effect->replaced_node;
343 ASSERT_EQ(1u, node->incoming_links.size());
344 EXPECT_EQ(0u, node->outgoing_links.size());
345 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
347 expect_equal(expected_data, out_data, 3, 2);
350 // The identity effect needs linear light, and thus will get conversions on both sides.
351 // Verify that sRGB data is properly converted to and from linear light for the entire ramp.
352 TEST(EffectChainTest, IdentityThroughsRGBConversions) {
354 for (unsigned i = 0; i < 256; ++i) {
358 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
359 tester.get_chain()->add_effect(new IdentityEffect());
360 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
362 expect_equal(data, out_data, 256, 1);
365 // Same, but uses the forward sRGB table from the GPU.
366 TEST(EffectChainTest, IdentityThroughGPUsRGBConversions) {
367 unsigned char data[256];
368 float expected_data[256];
369 for (unsigned i = 0; i < 256; ++i) {
371 expected_data[i] = i / 255.0;
374 EffectChainTester tester(nullptr, 256, 1);
375 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
376 tester.get_chain()->add_effect(new IdentityEffect());
377 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_sRGB);
379 expect_equal(expected_data, out_data, 256, 1);
382 // Same, for the Rec. 601/709 gamma curve.
383 TEST(EffectChainTest, IdentityThroughRec709) {
385 for (unsigned i = 0; i < 256; ++i) {
389 EffectChainTester tester(data, 256, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_709);
390 tester.get_chain()->add_effect(new IdentityEffect());
391 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_709);
393 expect_equal(data, out_data, 256, 1);
396 // The identity effect needs premultiplied alpha, and thus will get conversions on both sides.
397 TEST(EffectChainTest, IdentityThroughAlphaConversions) {
399 float data[4 * size] = {
400 0.8f, 0.0f, 0.0f, 0.5f,
401 0.0f, 0.2f, 0.2f, 0.3f,
402 0.1f, 0.0f, 1.0f, 1.0f,
404 float out_data[4 * size];
405 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
406 tester.get_chain()->add_effect(new IdentityEffect());
407 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
409 expect_equal(data, out_data, 4, size);
412 TEST(EffectChainTest, NoAlphaConversionsWhenPremultipliedAlphaNotNeeded) {
414 float data[4 * size] = {
415 0.8f, 0.0f, 0.0f, 0.5f,
416 0.0f, 0.2f, 0.2f, 0.3f,
417 0.1f, 0.0f, 1.0f, 1.0f,
419 float expected_data[4 * size] = {
420 0.1f, 0.0f, 1.0f, 1.0f,
421 0.0f, 0.2f, 0.2f, 0.3f,
422 0.8f, 0.0f, 0.0f, 0.5f,
424 float out_data[4 * size];
425 EffectChainTester tester(data, size, 1, FORMAT_RGBA_POSTMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
426 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
427 tester.get_chain()->add_effect(effect);
428 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
430 Node *node = effect->replaced_node;
431 ASSERT_EQ(1u, node->incoming_links.size());
432 EXPECT_EQ(0u, node->outgoing_links.size());
433 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
435 expect_equal(expected_data, out_data, 4, size);
438 // An input that outputs only blue, which has blank alpha.
439 class BlueInput : public Input {
441 BlueInput() { register_int("needs_mipmaps", &needs_mipmaps); }
442 string effect_type_id() const override { return "IdentityEffect"; }
443 string output_fragment_shader() override { return read_file("blue.frag"); }
444 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
445 bool can_output_linear_gamma() const override { return true; }
446 unsigned get_width() const override { return 1; }
447 unsigned get_height() const override { return 1; }
448 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
449 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
455 // Like RewritingEffect<InvertEffect>, but splicing in a BlueInput instead,
456 // which outputs blank alpha.
457 class RewritingToBlueInput : public Input {
459 RewritingToBlueInput() : blue_node(nullptr) { register_int("needs_mipmaps", &needs_mipmaps); }
460 string effect_type_id() const override { return "RewritingToBlueInput"; }
461 string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
462 void rewrite_graph(EffectChain *graph, Node *self) override {
463 Node *blue_node = graph->add_node(new BlueInput());
464 graph->replace_receiver(self, blue_node);
465 graph->replace_sender(self, blue_node);
467 self->disabled = true;
468 this->blue_node = blue_node;
471 // Dummy values that we need to implement because we inherit from Input.
472 // Same as BlueInput.
473 AlphaHandling alpha_handling() const override { return OUTPUT_BLANK_ALPHA; }
474 bool can_output_linear_gamma() const override { return true; }
475 unsigned get_width() const override { return 1; }
476 unsigned get_height() const override { return 1; }
477 Colorspace get_color_space() const override { return COLORSPACE_sRGB; }
478 GammaCurve get_gamma_curve() const override { return GAMMA_LINEAR; }
486 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlpha) {
488 float data[4 * size] = {
489 0.0f, 0.0f, 1.0f, 1.0f,
490 0.0f, 0.0f, 1.0f, 1.0f,
491 0.0f, 0.0f, 1.0f, 1.0f,
493 float out_data[4 * size];
494 EffectChainTester tester(nullptr, size, 1);
495 RewritingToBlueInput *input = new RewritingToBlueInput();
496 tester.get_chain()->add_input(input);
497 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED);
499 Node *node = input->blue_node;
500 EXPECT_EQ(0u, node->incoming_links.size());
501 EXPECT_EQ(0u, node->outgoing_links.size());
503 expect_equal(data, out_data, 4, size);
506 // An effect that does nothing, and specifies that it preserves blank alpha.
507 class BlankAlphaPreservingEffect : public Effect {
509 BlankAlphaPreservingEffect() {}
510 string effect_type_id() const override { return "BlankAlphaPreservingEffect"; }
511 string output_fragment_shader() override { return read_file("identity.frag"); }
512 AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
515 TEST(EffectChainTest, NoAlphaConversionsWithBlankAlphaPreservingEffect) {
517 float data[4 * size] = {
518 0.0f, 0.0f, 1.0f, 1.0f,
519 0.0f, 0.0f, 1.0f, 1.0f,
520 0.0f, 0.0f, 1.0f, 1.0f,
522 float out_data[4 * size];
523 EffectChainTester tester(nullptr, size, 1);
524 tester.get_chain()->add_input(new BlueInput());
525 tester.get_chain()->add_effect(new BlankAlphaPreservingEffect());
526 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
527 tester.get_chain()->add_effect(effect);
528 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
530 Node *node = effect->replaced_node;
531 EXPECT_EQ(1u, node->incoming_links.size());
532 EXPECT_EQ(0u, node->outgoing_links.size());
534 expect_equal(data, out_data, 4, size);
537 // This is the counter-test to NoAlphaConversionsWithBlankAlphaPreservingEffect;
538 // just to be sure that with a normal INPUT_AND_OUTPUT_PREMULTIPLIED_ALPHA effect,
539 // an alpha conversion _should_ be inserted at the very end. (There is some overlap
540 // with other tests.)
541 TEST(EffectChainTest, AlphaConversionsWithNonBlankAlphaPreservingEffect) {
543 float data[4 * size] = {
544 0.0f, 0.0f, 1.0f, 1.0f,
545 0.0f, 0.0f, 1.0f, 1.0f,
546 0.0f, 0.0f, 1.0f, 1.0f,
548 float out_data[4 * size];
549 EffectChainTester tester(nullptr, size, 1);
550 tester.get_chain()->add_input(new BlueInput());
551 tester.get_chain()->add_effect(new IdentityEffect()); // Not BlankAlphaPreservingEffect.
552 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
553 tester.get_chain()->add_effect(effect);
554 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
556 Node *node = effect->replaced_node;
557 EXPECT_EQ(1u, node->incoming_links.size());
558 EXPECT_EQ(1u, node->outgoing_links.size());
559 EXPECT_EQ("AlphaDivisionEffect", node->outgoing_links[0]->effect->effect_type_id());
561 expect_equal(data, out_data, 4, size);
564 // Effectively scales down its input linearly by 4x (and repeating it),
565 // which is not attainable without mipmaps.
566 class MipmapNeedingEffect : public Effect {
568 MipmapNeedingEffect() {}
569 MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
571 // To be allowed to mess with the sampler state.
572 bool needs_texture_bounce() const override { return true; }
574 string effect_type_id() const override { return "MipmapNeedingEffect"; }
575 string output_fragment_shader() override { return read_file("mipmap_needing_effect.frag"); }
576 void inform_added(EffectChain *chain) override { this->chain = chain; }
578 void set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) override
580 Node *self = chain->find_node_for_effect(this);
581 glActiveTexture(chain->get_input_sampler(self, 0));
583 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
585 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
593 TEST(EffectChainTest, MipmapGenerationWorks) {
594 float data[] = { // In 4x4 blocks.
595 1.0f, 0.0f, 0.0f, 0.0f,
596 0.0f, 0.0f, 0.0f, 0.0f,
597 0.0f, 0.0f, 0.0f, 0.0f,
598 0.0f, 0.0f, 0.0f, 1.0f,
600 0.0f, 0.0f, 0.0f, 0.0f,
601 0.0f, 0.5f, 0.0f, 0.0f,
602 0.0f, 0.0f, 1.0f, 0.0f,
603 0.0f, 0.0f, 0.0f, 0.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,
608 1.0f, 1.0f, 1.0f, 1.0f,
610 0.0f, 0.0f, 0.0f, 0.0f,
611 0.0f, 1.0f, 1.0f, 0.0f,
612 0.0f, 1.0f, 1.0f, 0.0f,
613 0.0f, 0.0f, 0.0f, 0.0f,
615 float expected_data[] = { // Repeated four times each way.
616 0.125f, 0.125f, 0.125f, 0.125f,
617 0.09375f, 0.09375f, 0.09375f, 0.09375f,
618 1.0f, 1.0f, 1.0f, 1.0f,
619 0.25f, 0.25f, 0.25f, 0.25f,
621 0.125f, 0.125f, 0.125f, 0.125f,
622 0.09375f, 0.09375f, 0.09375f, 0.09375f,
623 1.0f, 1.0f, 1.0f, 1.0f,
624 0.25f, 0.25f, 0.25f, 0.25f,
626 0.125f, 0.125f, 0.125f, 0.125f,
627 0.09375f, 0.09375f, 0.09375f, 0.09375f,
628 1.0f, 1.0f, 1.0f, 1.0f,
629 0.25f, 0.25f, 0.25f, 0.25f,
631 0.125f, 0.125f, 0.125f, 0.125f,
632 0.09375f, 0.09375f, 0.09375f, 0.09375f,
633 1.0f, 1.0f, 1.0f, 1.0f,
634 0.25f, 0.25f, 0.25f, 0.25f,
636 float out_data[4 * 16];
637 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
638 tester.get_chain()->add_effect(new MipmapNeedingEffect());
639 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
641 expect_equal(expected_data, out_data, 4, 16);
644 class NonMipmapCapableInput : public FlatInput {
646 NonMipmapCapableInput(ImageFormat format, MovitPixelFormat pixel_format, GLenum type, unsigned width, unsigned height)
647 : FlatInput(format, pixel_format, type, width, height) {}
649 bool can_supply_mipmaps() const override { return false; }
650 bool set_int(const std::string& key, int value) override {
651 if (key == "needs_mipmaps") {
654 return FlatInput::set_int(key, value);
658 // The same test as MipmapGenerationWorks, but with an input that refuses
659 // to supply mipmaps.
660 TEST(EffectChainTest, MipmapsWithNonMipmapCapableInput) {
661 float data[] = { // In 4x4 blocks.
662 1.0f, 0.0f, 0.0f, 0.0f,
663 0.0f, 0.0f, 0.0f, 0.0f,
664 0.0f, 0.0f, 0.0f, 0.0f,
665 0.0f, 0.0f, 0.0f, 1.0f,
667 0.0f, 0.0f, 0.0f, 0.0f,
668 0.0f, 0.5f, 0.0f, 0.0f,
669 0.0f, 0.0f, 1.0f, 0.0f,
670 0.0f, 0.0f, 0.0f, 0.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,
675 1.0f, 1.0f, 1.0f, 1.0f,
677 0.0f, 0.0f, 0.0f, 0.0f,
678 0.0f, 1.0f, 1.0f, 0.0f,
679 0.0f, 1.0f, 1.0f, 0.0f,
680 0.0f, 0.0f, 0.0f, 0.0f,
682 float expected_data[] = { // Repeated four times each way.
683 0.125f, 0.125f, 0.125f, 0.125f,
684 0.09375f, 0.09375f, 0.09375f, 0.09375f,
685 1.0f, 1.0f, 1.0f, 1.0f,
686 0.25f, 0.25f, 0.25f, 0.25f,
688 0.125f, 0.125f, 0.125f, 0.125f,
689 0.09375f, 0.09375f, 0.09375f, 0.09375f,
690 1.0f, 1.0f, 1.0f, 1.0f,
691 0.25f, 0.25f, 0.25f, 0.25f,
693 0.125f, 0.125f, 0.125f, 0.125f,
694 0.09375f, 0.09375f, 0.09375f, 0.09375f,
695 1.0f, 1.0f, 1.0f, 1.0f,
696 0.25f, 0.25f, 0.25f, 0.25f,
698 0.125f, 0.125f, 0.125f, 0.125f,
699 0.09375f, 0.09375f, 0.09375f, 0.09375f,
700 1.0f, 1.0f, 1.0f, 1.0f,
701 0.25f, 0.25f, 0.25f, 0.25f,
703 float out_data[4 * 16];
704 EffectChainTester tester(nullptr, 4, 16, FORMAT_GRAYSCALE);
707 format.color_space = COLORSPACE_sRGB;
708 format.gamma_curve = GAMMA_LINEAR;
710 NonMipmapCapableInput *input = new NonMipmapCapableInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 16);
711 input->set_pixel_data(data);
712 tester.get_chain()->add_input(input);
713 tester.get_chain()->add_effect(new MipmapNeedingEffect());
714 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
716 expect_equal(expected_data, out_data, 4, 16);
719 TEST(EffectChainTest, ResizeDownByFourThenUpByFour) {
720 float data[] = { // In 4x4 blocks.
721 1.0f, 0.0f, 0.0f, 0.0f,
722 0.0f, 0.0f, 0.0f, 0.0f,
723 0.0f, 0.0f, 0.0f, 0.0f,
724 0.0f, 0.0f, 0.0f, 1.0f,
726 0.0f, 0.0f, 0.0f, 0.0f,
727 0.0f, 0.5f, 0.0f, 0.0f,
728 0.0f, 0.0f, 1.0f, 0.0f,
729 0.0f, 0.0f, 0.0f, 0.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,
734 1.0f, 1.0f, 1.0f, 1.0f,
736 0.0f, 0.0f, 0.0f, 0.0f,
737 0.0f, 1.0f, 1.0f, 0.0f,
738 0.0f, 1.0f, 1.0f, 0.0f,
739 0.0f, 0.0f, 0.0f, 0.0f,
741 float expected_data[] = { // Repeated four times horizontaly, interpolated vertically.
742 0.1250f, 0.1250f, 0.1250f, 0.1250f,
743 0.1250f, 0.1250f, 0.1250f, 0.1250f,
744 0.1211f, 0.1211f, 0.1211f, 0.1211f,
745 0.1133f, 0.1133f, 0.1133f, 0.1133f,
746 0.1055f, 0.1055f, 0.1055f, 0.1055f,
747 0.0977f, 0.0977f, 0.0977f, 0.0977f,
748 0.2070f, 0.2070f, 0.2070f, 0.2070f,
749 0.4336f, 0.4336f, 0.4336f, 0.4336f,
750 0.6602f, 0.6602f, 0.6602f, 0.6602f,
751 0.8867f, 0.8867f, 0.8867f, 0.8867f,
752 0.9062f, 0.9062f, 0.9062f, 0.9062f,
753 0.7188f, 0.7188f, 0.7188f, 0.7188f,
754 0.5312f, 0.5312f, 0.5312f, 0.5312f,
755 0.3438f, 0.3438f, 0.3438f, 0.3438f,
756 0.2500f, 0.2500f, 0.2500f, 0.2500f,
757 0.2500f, 0.2500f, 0.2500f, 0.2500f,
759 float out_data[4 * 16];
761 ResizeEffect *downscale = new ResizeEffect();
762 ASSERT_TRUE(downscale->set_int("width", 1));
763 ASSERT_TRUE(downscale->set_int("height", 4));
765 ResizeEffect *upscale = new ResizeEffect();
766 ASSERT_TRUE(upscale->set_int("width", 4));
767 ASSERT_TRUE(upscale->set_int("height", 16));
769 EffectChainTester tester(data, 4, 16, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
770 tester.get_chain()->add_effect(downscale);
771 tester.get_chain()->add_effect(upscale);
772 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
774 expect_equal(expected_data, out_data, 4, 16);
777 // An effect to verify that you can turn off mipmaps; it downscales by two,
778 // which gives blur with mipmaps and aliasing (picks out every other pixel)
780 class Downscale2xEffect : public Effect {
782 explicit Downscale2xEffect(MipmapRequirements mipmap_requirements)
783 : mipmap_requirements(mipmap_requirements)
785 register_vec2("offset", offset);
787 MipmapRequirements needs_mipmaps() const override { return mipmap_requirements; }
789 string effect_type_id() const override { return "Downscale2xEffect"; }
790 string output_fragment_shader() override { return read_file("downscale2x.frag"); }
793 const MipmapRequirements mipmap_requirements;
795 float offset[2] { 0.0f, 0.0f };
798 TEST(EffectChainTest, MipmapChainGetsSplit) {
800 0.0f, 0.0f, 0.0f, 0.0f,
801 1.0f, 0.0f, 1.0f, 0.0f,
802 0.0f, 0.0f, 0.0f, 0.0f,
803 1.0f, 0.0f, 1.0f, 0.0f,
806 // The intermediate result after the first step looks like this,
807 // assuming there are no mipmaps (the zeros are due to border behavior):
814 // so another 2x downscale towards the bottom left will give
819 // with yet more zeros coming in on the top and the right from the border.
820 float expected_data[] = {
821 0.0f, 0.0f, 0.0f, 0.0f,
822 0.0f, 0.0f, 0.0f, 0.0f,
823 0.0f, 0.0f, 0.0f, 0.0f,
824 1.0f, 0.0f, 0.0f, 0.0f,
826 float out_data[4 * 4];
828 float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
829 RewritingEffect<Downscale2xEffect> *pick_out_top_left = new RewritingEffect<Downscale2xEffect>(Effect::CANNOT_ACCEPT_MIPMAPS);
830 ASSERT_TRUE(pick_out_top_left->effect->set_vec2("offset", offset));
832 RewritingEffect<Downscale2xEffect> *downscale2x = new RewritingEffect<Downscale2xEffect>(Effect::NEEDS_MIPMAPS);
834 EffectChainTester tester(data, 4, 4, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
835 tester.get_chain()->add_effect(pick_out_top_left);
836 tester.get_chain()->add_effect(downscale2x);
837 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
839 EXPECT_NE(pick_out_top_left->replaced_node->containing_phase,
840 downscale2x->replaced_node->containing_phase);
842 expect_equal(expected_data, out_data, 4, 4);
845 // An effect that adds its two inputs together. Used below.
846 class AddEffect : public Effect {
849 string effect_type_id() const override { return "AddEffect"; }
850 string output_fragment_shader() override { return read_file("add.frag"); }
851 unsigned num_inputs() const override { return 2; }
852 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
855 // Constructs the graph
859 // MultiplyEffect MultiplyEffect |
863 // and verifies that it gives the correct output.
864 TEST(EffectChainTest, DiamondGraph) {
869 float expected_data[] = {
873 float out_data[2 * 2];
875 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
876 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
878 MultiplyEffect *mul_half = new MultiplyEffect();
879 ASSERT_TRUE(mul_half->set_vec4("factor", half));
881 MultiplyEffect *mul_two = new MultiplyEffect();
882 ASSERT_TRUE(mul_two->set_vec4("factor", two));
884 EffectChainTester tester(nullptr, 2, 2);
887 format.color_space = COLORSPACE_sRGB;
888 format.gamma_curve = GAMMA_LINEAR;
890 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
891 input->set_pixel_data(data);
893 tester.get_chain()->add_input(input);
894 tester.get_chain()->add_effect(mul_half, input);
895 tester.get_chain()->add_effect(mul_two, input);
896 tester.get_chain()->add_effect(new AddEffect(), mul_half, mul_two);
897 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
899 expect_equal(expected_data, out_data, 2, 2);
902 // Constructs the graph
906 // MultiplyEffect MultiplyEffect |
908 // \ BouncingIdentityEffect |
912 // and verifies that it gives the correct output.
913 TEST(EffectChainTest, DiamondGraphWithOneInputUsedInTwoPhases) {
918 float expected_data[] = {
922 float out_data[2 * 2];
924 const float half[] = { 0.5f, 0.5f, 0.5f, 0.5f };
925 const float two[] = { 2.0f, 2.0f, 2.0f, 0.5f };
927 MultiplyEffect *mul_half = new MultiplyEffect();
928 ASSERT_TRUE(mul_half->set_vec4("factor", half));
930 MultiplyEffect *mul_two = new MultiplyEffect();
931 ASSERT_TRUE(mul_two->set_vec4("factor", two));
933 BouncingIdentityEffect *bounce = new BouncingIdentityEffect();
935 EffectChainTester tester(nullptr, 2, 2);
938 format.color_space = COLORSPACE_sRGB;
939 format.gamma_curve = GAMMA_LINEAR;
941 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
942 input->set_pixel_data(data);
944 tester.get_chain()->add_input(input);
945 tester.get_chain()->add_effect(mul_half, input);
946 tester.get_chain()->add_effect(mul_two, input);
947 tester.get_chain()->add_effect(bounce, mul_two);
948 tester.get_chain()->add_effect(new AddEffect(), mul_half, bounce);
949 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
951 expect_equal(expected_data, out_data, 2, 2);
954 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneGammaConversion) {
959 float expected_data[] = {
960 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
963 float out_data[2 * 2];
965 EffectChainTester tester(nullptr, 2, 2);
966 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_sRGB);
968 // MirrorEffect does not get linear light, so the conversions will be
969 // inserted after it, not before.
970 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
971 tester.get_chain()->add_effect(effect);
973 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
974 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
975 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
976 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
978 expect_equal(expected_data, out_data, 2, 2);
980 Node *node = effect->replaced_node;
981 ASSERT_EQ(1u, node->incoming_links.size());
982 ASSERT_EQ(1u, node->outgoing_links.size());
983 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
984 EXPECT_EQ("GammaExpansionEffect", node->outgoing_links[0]->effect->effect_type_id());
987 TEST(EffectChainTest, EffectUsedTwiceOnlyGetsOneColorspaceConversion) {
992 float expected_data[] = {
993 0.0f, 0.5f, // 0.5 and not 1.0, since AddEffect doesn't clamp alpha properly.
996 float out_data[2 * 2];
998 EffectChainTester tester(nullptr, 2, 2);
999 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_REC_601_625, GAMMA_LINEAR);
1001 // MirrorEffect does not get linear light, so the conversions will be
1002 // inserted after it, not before.
1003 RewritingEffect<MirrorEffect> *effect = new RewritingEffect<MirrorEffect>();
1004 tester.get_chain()->add_effect(effect);
1006 Effect *identity1 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1007 Effect *identity2 = tester.get_chain()->add_effect(new IdentityEffect(), effect);
1008 tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
1009 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1011 expect_equal(expected_data, out_data, 2, 2);
1013 Node *node = effect->replaced_node;
1014 ASSERT_EQ(1u, node->incoming_links.size());
1015 ASSERT_EQ(1u, node->outgoing_links.size());
1016 EXPECT_EQ("FlatInput", node->incoming_links[0]->effect->effect_type_id());
1017 EXPECT_EQ("ColorspaceConversionEffect", node->outgoing_links[0]->effect->effect_type_id());
1020 // An effect that does nothing, but requests texture bounce and stores
1022 class SizeStoringEffect : public BouncingIdentityEffect {
1024 SizeStoringEffect() : input_width(-1), input_height(-1) {}
1025 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
1026 assert(input_num == 0);
1027 input_width = width;
1028 input_height = height;
1030 string effect_type_id() const override { return "SizeStoringEffect"; }
1032 int input_width, input_height;
1035 TEST(EffectChainTest, SameInputsGiveSameOutputs) {
1036 float data[2 * 2] = {
1040 float out_data[4 * 3];
1042 EffectChainTester tester(nullptr, 4, 3); // Note non-square aspect.
1045 format.color_space = COLORSPACE_sRGB;
1046 format.gamma_curve = GAMMA_LINEAR;
1048 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1049 input1->set_pixel_data(data);
1051 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 2, 2);
1052 input2->set_pixel_data(data);
1054 SizeStoringEffect *input_store = new SizeStoringEffect();
1056 tester.get_chain()->add_input(input1);
1057 tester.get_chain()->add_input(input2);
1058 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1059 tester.get_chain()->add_effect(input_store);
1060 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1062 EXPECT_EQ(2, input_store->input_width);
1063 EXPECT_EQ(2, input_store->input_height);
1066 TEST(EffectChainTest, AspectRatioConversion) {
1067 float data1[4 * 3] = {
1068 0.0f, 0.0f, 0.0f, 0.0f,
1069 0.0f, 0.0f, 0.0f, 0.0f,
1070 0.0f, 0.0f, 0.0f, 0.0f,
1072 float data2[7 * 7] = {
1073 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1074 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1075 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1076 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
1077 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1078 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1079 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1082 // The right conversion here is that the 7x7 image decides the size,
1083 // since it is the biggest, so everything is scaled up to 9x7
1084 // (keep the height, round the width 9.333 to 9).
1085 float out_data[9 * 7];
1087 EffectChainTester tester(nullptr, 4, 3);
1090 format.color_space = COLORSPACE_sRGB;
1091 format.gamma_curve = GAMMA_LINEAR;
1093 FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 3);
1094 input1->set_pixel_data(data1);
1096 FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 7, 7);
1097 input2->set_pixel_data(data2);
1099 SizeStoringEffect *input_store = new SizeStoringEffect();
1101 tester.get_chain()->add_input(input1);
1102 tester.get_chain()->add_input(input2);
1103 tester.get_chain()->add_effect(new AddEffect(), input1, input2);
1104 tester.get_chain()->add_effect(input_store);
1105 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1107 EXPECT_EQ(9, input_store->input_width);
1108 EXPECT_EQ(7, input_store->input_height);
1111 // Tests that putting a BlueInput (constant color) into its own pass,
1112 // which creates a phase that doesn't need texture coordinates,
1113 // doesn't mess up a second phase that actually does.
1114 TEST(EffectChainTest, FirstPhaseWithNoTextureCoordinates) {
1120 float expected_data[] = {
1121 1.0f, 1.0f, 2.0f, 2.0f,
1122 0.0f, 0.0f, 1.0f, 2.0f,
1124 float out_data[size * 4];
1125 // First say that we have sRGB, linear input.
1127 format.color_space = COLORSPACE_sRGB;
1128 format.gamma_curve = GAMMA_LINEAR;
1129 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 1, size);
1131 input->set_pixel_data(data);
1132 EffectChainTester tester(nullptr, 1, size);
1133 tester.get_chain()->add_input(new BlueInput());
1134 Effect *phase1_end = tester.get_chain()->add_effect(new BouncingIdentityEffect());
1135 tester.get_chain()->add_input(input);
1136 tester.get_chain()->add_effect(new AddEffect(), phase1_end, input);
1138 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1140 expect_equal(expected_data, out_data, 4, size);
1143 // An effect that does nothing except changing its output sizes.
1144 class VirtualResizeEffect : public Effect {
1146 VirtualResizeEffect(int width, int height, int virtual_width, int virtual_height)
1149 virtual_width(virtual_width),
1150 virtual_height(virtual_height) {}
1151 string effect_type_id() const override { return "VirtualResizeEffect"; }
1152 string output_fragment_shader() override { return read_file("identity.frag"); }
1154 bool changes_output_size() const override { return true; }
1156 void get_output_size(unsigned *width, unsigned *height,
1157 unsigned *virtual_width, unsigned *virtual_height) const override {
1158 *width = this->width;
1159 *height = this->height;
1160 *virtual_width = this->virtual_width;
1161 *virtual_height = this->virtual_height;
1165 int width, height, virtual_width, virtual_height;
1168 TEST(EffectChainTest, VirtualSizeIsSentOnToInputs) {
1169 const int size = 2, bigger_size = 3;
1170 float data[size * size] = {
1174 float out_data[size * size];
1176 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1178 SizeStoringEffect *size_store = new SizeStoringEffect();
1180 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, bigger_size, bigger_size));
1181 tester.get_chain()->add_effect(size_store);
1182 tester.get_chain()->add_effect(new VirtualResizeEffect(size, size, size, size));
1183 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1185 EXPECT_EQ(bigger_size, size_store->input_width);
1186 EXPECT_EQ(bigger_size, size_store->input_height);
1188 // If the resize is implemented as non-virtual, we'll fail here,
1189 // since bilinear scaling from 2x2 → 3x3 → 2x2 is not very exact.
1190 expect_equal(data, out_data, size, size);
1193 // An effect that is like VirtualResizeEffect, but always has virtual and real
1194 // sizes the same (and promises this).
1195 class NonVirtualResizeEffect : public VirtualResizeEffect {
1197 NonVirtualResizeEffect(int width, int height)
1198 : VirtualResizeEffect(width, height, width, height) {}
1199 string effect_type_id() const override { return "NonVirtualResizeEffect"; }
1200 bool sets_virtual_output_size() const override { return false; }
1203 // An effect that promises one-to-one sampling (unlike IdentityEffect).
1204 class OneToOneEffect : public Effect {
1207 string effect_type_id() const override { return "OneToOneEffect"; }
1208 string output_fragment_shader() override { return read_file("identity.frag"); }
1209 bool strong_one_to_one_sampling() const override { return true; }
1212 TEST_P(WithAndWithoutComputeShaderTest, NoBounceWithOneToOneSampling) {
1214 float data[size * size] = {
1218 float out_data[size * size];
1220 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1222 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1223 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1225 if (GetParam() == "compute") {
1226 tester.get_chain()->add_effect(new IdentityComputeEffect());
1228 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1230 tester.get_chain()->add_effect(effect1);
1231 tester.get_chain()->add_effect(effect2);
1232 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1234 expect_equal(data, out_data, size, size);
1236 // The first OneToOneEffect should be in the same phase as its input.
1237 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1238 EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
1239 effect1->replaced_node->containing_phase);
1241 // The second OneToOneEffect, too.
1242 EXPECT_EQ(effect1->replaced_node->containing_phase,
1243 effect2->replaced_node->containing_phase);
1246 TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
1248 float data[size * size] = {
1252 float out_data[size * size];
1254 EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1256 RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
1257 RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
1258 RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
1259 RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
1261 tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
1262 tester.get_chain()->add_effect(effect1);
1263 tester.get_chain()->add_effect(effect2);
1264 tester.get_chain()->add_effect(effect3);
1265 tester.get_chain()->add_effect(effect4);
1266 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1268 expect_equal(data, out_data, size, size);
1270 // The NonVirtualResizeEffect should be in a different phase from
1271 // the IdentityEffect (since the latter is not one-to-one),
1272 // ie., the chain should be broken somewhere between them, but exactly
1273 // where doesn't matter.
1274 ASSERT_EQ(1u, effect1->replaced_node->incoming_links.size());
1275 EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
1276 effect3->replaced_node->containing_phase);
1278 // The last OneToOneEffect should also be in the same phase as the
1279 // IdentityEffect (the phase was already broken).
1280 EXPECT_EQ(effect3->replaced_node->containing_phase,
1281 effect4->replaced_node->containing_phase);
1284 // Does not use EffectChainTest, so that it can construct an EffectChain without
1285 // a shared ResourcePool (which is also properly destroyed afterwards).
1286 // Also turns on debugging to test that code path.
1287 TEST(EffectChainTest, IdentityWithOwnPool) {
1288 const int width = 3, height = 2;
1293 const float expected_data[] = {
1297 float out_data[6], temp[6 * 4];
1299 EffectChain chain(width, height);
1300 MovitDebugLevel old_movit_debug_level = movit_debug_level;
1301 movit_debug_level = MOVIT_DEBUG_ON;
1304 format.color_space = COLORSPACE_sRGB;
1305 format.gamma_curve = GAMMA_LINEAR;
1307 FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
1308 input->set_pixel_data(data);
1309 chain.add_input(input);
1310 chain.add_output(format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
1313 glGenTextures(1, &texnum);
1315 glBindTexture(GL_TEXTURE_2D, texnum);
1317 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
1320 glGenFramebuffers(1, &fbo);
1322 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1324 glFramebufferTexture2D(
1326 GL_COLOR_ATTACHMENT0,
1331 glBindFramebuffer(GL_FRAMEBUFFER, 0);
1336 chain.render_to_fbo(fbo, width, height);
1338 glBindFramebuffer(GL_FRAMEBUFFER, fbo);
1340 glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, temp);
1342 for (unsigned i = 0; i < 6; ++i) {
1343 out_data[i] = temp[i * 4];
1346 expect_equal(expected_data, out_data, width, height);
1348 // Reset the debug status again.
1349 movit_debug_level = old_movit_debug_level;
1352 // A dummy effect whose only purpose is to test sprintf decimal behavior.
1353 class PrintfingBlueEffect : public Effect {
1355 PrintfingBlueEffect() {}
1356 string effect_type_id() const override { return "PrintfingBlueEffect"; }
1357 string output_fragment_shader() override {
1359 ss.imbue(locale("C"));
1361 ss << "vec4 FUNCNAME(vec2 tc) { return vec4("
1362 << 0.0f << ", " << 0.0f << ", "
1363 << 0.5f << ", " << 1.0f << "); }\n";
1368 TEST(EffectChainTest, StringStreamLocalesWork) {
1369 // An example of a locale with comma instead of period as decimal separator.
1370 // Obviously, if you run on a machine without this locale available,
1371 // the test will always succeed. Note that the OpenGL driver might call
1372 // setlocale() behind-the-scenes, and that might corrupt the returned
1373 // pointer, so we need to take our own copy of it here.
1374 char *saved_locale = setlocale(LC_ALL, "nb_NO.UTF_8");
1375 if (saved_locale == nullptr) {
1376 // The locale wasn't available.
1379 saved_locale = strdup(saved_locale);
1381 0.0f, 0.0f, 0.0f, 0.0f,
1383 float expected_data[] = {
1384 0.0f, 0.0f, 0.5f, 1.0f,
1387 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1388 tester.get_chain()->add_effect(new PrintfingBlueEffect());
1389 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1391 expect_equal(expected_data, out_data, 4, 1);
1393 setlocale(LC_ALL, saved_locale);
1397 TEST(EffectChainTest, sRGBIntermediate) {
1399 0.0f, 0.5f, 0.0f, 1.0f,
1402 EffectChainTester tester(data, 1, 1, FORMAT_RGBA_PREMULTIPLIED_ALPHA, COLORSPACE_sRGB, GAMMA_LINEAR);
1403 tester.get_chain()->set_intermediate_format(GL_SRGB8);
1404 tester.get_chain()->add_effect(new IdentityEffect());
1405 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1406 tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_LINEAR);
1408 EXPECT_GE(fabs(out_data[1] - data[1]), 1e-3)
1409 << "Expected sRGB not to be able to represent 0.5 exactly (got " << out_data[1] << ")";
1410 EXPECT_LT(fabs(out_data[1] - data[1]), 0.1f)
1411 << "Expected sRGB to be able to represent 0.5 approximately (got " << out_data[1] << ")";
1413 // This state should have been preserved.
1414 EXPECT_FALSE(glIsEnabled(GL_FRAMEBUFFER_SRGB));
1417 // An effect that is like IdentityEffect, but also does not require linear light.
1418 class PassThroughEffect : public IdentityEffect {
1420 PassThroughEffect() {}
1421 string effect_type_id() const override { return "PassThroughEffect"; }
1422 bool needs_linear_light() const override { return false; }
1423 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1426 // Same, just also bouncing.
1427 class BouncingPassThroughEffect : public BouncingIdentityEffect {
1429 BouncingPassThroughEffect() {}
1430 string effect_type_id() const override { return "BouncingPassThroughEffect"; }
1431 bool needs_linear_light() const override { return false; }
1432 bool needs_texture_bounce() const override { return true; }
1433 AlphaHandling alpha_handling() const override { return DONT_CARE_ALPHA_TYPE; }
1436 TEST(EffectChainTest, Linear10bitIntermediateAccuracy) {
1437 // Note that we do the comparison in sRGB space, which is what we
1438 // typically would want; however, we do the sRGB conversion ourself
1439 // to avoid compounding errors from shader conversions into the
1441 const int size = 4096; // 12-bit.
1442 float linear_data[size], data[size], out_data[size];
1444 for (int i = 0; i < size; ++i) {
1445 linear_data[i] = i / double(size - 1);
1446 data[i] = srgb_to_linear(linear_data[i]);
1449 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1450 tester.get_chain()->set_intermediate_format(GL_RGB10_A2);
1451 tester.get_chain()->add_effect(new IdentityEffect());
1452 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1453 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1455 for (int i = 0; i < size; ++i) {
1456 out_data[i] = linear_to_srgb(out_data[i]);
1459 // This maximum error is pretty bad; about 6.5 levels of a 10-bit sRGB
1460 // framebuffer. (Slightly more on NVIDIA cards.)
1461 expect_equal(linear_data, out_data, size, 1, 7.5e-3, 2e-5);
1464 TEST_P(WithAndWithoutComputeShaderTest, SquareRoot10bitIntermediateAccuracy) {
1465 // Note that we do the comparison in sRGB space, which is what we
1466 // typically would want; however, we do the sRGB conversion ourself
1467 // to avoid compounding errors from shader conversions into the
1469 const int size = 4096; // 12-bit.
1470 float linear_data[size], data[size], out_data[size];
1472 for (int i = 0; i < size; ++i) {
1473 linear_data[i] = i / double(size - 1);
1474 data[i] = srgb_to_linear(linear_data[i]);
1477 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA32F);
1478 tester.get_chain()->set_intermediate_format(GL_RGB10_A2, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1479 if (GetParam() == "compute") {
1480 tester.get_chain()->add_effect(new IdentityComputeEffect());
1482 tester.get_chain()->add_effect(new IdentityEffect());
1484 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1485 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1487 for (int i = 0; i < size; ++i) {
1488 out_data[i] = linear_to_srgb(out_data[i]);
1491 // This maximum error is much better; about 0.7 levels of a 10-bit sRGB
1492 // framebuffer (ideal would be 0.5). That is an order of magnitude better
1493 // than in the linear test above. The RMS error is much better, too.
1494 expect_equal(linear_data, out_data, size, 1, 7.5e-4, 5e-6);
1497 TEST(EffectChainTest, SquareRootIntermediateIsTurnedOffForNonLinearData) {
1498 const int size = 256; // 8-bit.
1499 float data[size], out_data[size];
1501 for (int i = 0; i < size; ++i) {
1502 data[i] = i / double(size - 1);
1505 EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_REC_601, GL_RGBA32F);
1506 tester.get_chain()->set_intermediate_format(GL_RGB8, SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION);
1507 tester.get_chain()->add_effect(new PassThroughEffect());
1508 tester.get_chain()->add_effect(new BouncingPassThroughEffect());
1509 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_REC_601);
1511 // The data should be passed through nearly exactly, since there is no effect
1512 // on the path that requires linear light. (Actually, it _is_ exact modulo
1513 // fp32 errors, but the error bounds is strictly _less than_, not zero.)
1514 expect_equal(data, out_data, size, 1, 1e-6, 1e-6);
1517 // An effect that stores which program number was last run under.
1518 class RecordingIdentityEffect : public Effect {
1520 RecordingIdentityEffect() {}
1521 string effect_type_id() const override { return "RecordingIdentityEffect"; }
1522 string output_fragment_shader() override { return read_file("identity.frag"); }
1524 GLuint last_glsl_program_num;
1525 void set_gl_state(GLuint glsl_program_num, const std::string& prefix, unsigned *sampler_num) override
1527 last_glsl_program_num = glsl_program_num;
1531 TEST(EffectChainTest, ProgramsAreClonedForMultipleThreads) {
1537 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1538 RecordingIdentityEffect *effect = new RecordingIdentityEffect();
1539 tester.get_chain()->add_effect(effect);
1540 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1542 expect_equal(data, out_data, 3, 2);
1544 ASSERT_NE(0u, effect->last_glsl_program_num);
1546 // Now pretend some other effect is using this program number;
1547 // ResourcePool will then need to clone it.
1548 ResourcePool *resource_pool = tester.get_chain()->get_resource_pool();
1549 GLuint master_program_num = resource_pool->use_glsl_program(effect->last_glsl_program_num);
1550 EXPECT_EQ(effect->last_glsl_program_num, master_program_num);
1552 // Re-run should still give the correct data, but it should have run
1553 // with a different program.
1554 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1555 expect_equal(data, out_data, 3, 2);
1556 EXPECT_NE(effect->last_glsl_program_num, master_program_num);
1558 // Release the program, and check one final time.
1559 resource_pool->unuse_glsl_program(master_program_num);
1560 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1561 expect_equal(data, out_data, 3, 2);
1564 TEST(ComputeShaderTest, Identity) {
1570 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1571 if (!movit_compute_shaders_supported) {
1572 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1575 tester.get_chain()->add_effect(new IdentityComputeEffect());
1576 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1578 expect_equal(data, out_data, 3, 2);
1581 // Like IdentityComputeEffect, but due to the alpha handling, this will be
1582 // the very last effect in the chain, which means we can't output it directly
1584 class IdentityAlphaComputeEffect : public IdentityComputeEffect {
1585 AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
1588 TEST(ComputeShaderTest, LastEffectInChain) {
1594 EffectChainTester tester(data, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1595 if (!movit_compute_shaders_supported) {
1596 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1599 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1600 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1602 expect_equal(data, out_data, 3, 2);
1605 TEST(ComputeShaderTest, Render8BitTo8Bit) {
1610 uint8_t out_data[6];
1611 EffectChainTester tester(nullptr, 3, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8);
1612 if (!movit_compute_shaders_supported) {
1613 fprintf(stderr, "Skipping test; no support for compile shaders.\n");
1616 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 3, 2);
1617 tester.get_chain()->add_effect(new IdentityAlphaComputeEffect());
1618 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1620 expect_equal(data, out_data, 3, 2);
1623 // A compute shader to mirror the inputs, in 2x2 blocks.
1624 class MirrorComputeEffect : public Effect {
1626 MirrorComputeEffect() {}
1627 string effect_type_id() const override { return "MirrorComputeEffect"; }
1628 bool is_compute_shader() const override { return true; }
1629 string output_fragment_shader() override { return read_file("mirror.comp"); }
1630 void get_compute_dimensions(unsigned output_width, unsigned output_height,
1631 unsigned *x, unsigned *y, unsigned *z) const override {
1632 *x = output_width / 2;
1633 *y = output_height / 2;
1638 TEST(ComputeShaderTest, ComputeThenOneToOne) {
1640 0.0f, 0.25f, 0.3f, 0.8f,
1641 0.75f, 1.0f, 1.0f, 0.2f,
1643 float expected_data[] = {
1644 0.8f, 0.3f, 0.25f, 0.0f,
1645 0.2f, 1.0f, 1.0f, 0.75f,
1648 EffectChainTester tester(data, 4, 2, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
1649 tester.get_chain()->add_effect(new MirrorComputeEffect());
1650 tester.get_chain()->add_effect(new OneToOneEffect());
1651 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1653 expect_equal(expected_data, out_data, 4, 2);
1656 // A compute shader that also resizes its input, taking the upper-left pixel
1657 // of every 2x2 group. (The shader is hard-coded to 4x2 input for simplicity.)
1658 class Downscale2xComputeEffect : public Effect {
1660 Downscale2xComputeEffect() {}
1661 string effect_type_id() const override { return "Downscale2xComputeEffect"; }
1662 bool is_compute_shader() const override { return true; }
1663 string output_fragment_shader() override { return read_file("downscale2x.comp"); }
1664 bool changes_output_size() const override { return true; }
1665 void inform_input_size(unsigned input_num, unsigned width, unsigned height) override
1667 this->width = width;
1668 this->height = height;
1670 void get_output_size(unsigned *width, unsigned *height,
1671 unsigned *virtual_width, unsigned *virtual_height) const override {
1672 *width = *virtual_width = this->width / 2;
1673 *height = *virtual_height = this->height / 2;
1677 unsigned width, height;
1680 // Even if the compute shader is not the last effect, it's the one that should decide
1681 // the output size of the phase.
1682 TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
1684 0.0f, 0.25f, 0.3f, 0.8f,
1685 0.75f, 1.0f, 1.0f, 0.2f,
1687 float expected_data[] = {
1691 EffectChainTester tester(nullptr, 2, 1);
1692 tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 4, 2);
1694 RewritingEffect<Downscale2xComputeEffect> *downscale_effect = new RewritingEffect<Downscale2xComputeEffect>();
1695 tester.get_chain()->add_effect(downscale_effect);
1696 tester.get_chain()->add_effect(new OneToOneEffect());
1697 tester.get_chain()->add_effect(new BouncingIdentityEffect());
1698 tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
1700 expect_equal(expected_data, out_data, 2, 1);
1702 Phase *phase = downscale_effect->replaced_node->containing_phase;
1703 EXPECT_EQ(2u, phase->output_width);
1704 EXPECT_EQ(1u, phase->output_height);
1707 } // namespace movit