Loosen up some restrictions on strong one-to-one-effects.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 28 Jan 2018 16:41:07 +0000 (17:41 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 28 Jan 2018 16:41:07 +0000 (17:41 +0100)
In particular, this allows strong one-to-one-effects with multiple
inputs; this was forbidden by the comment, but not enforced anywhere,
and MixEffect was inadvertedly put as strong one-to-one. This actually
triggered at least three distinct bugs (thus three new tests).

effect.h
effect_chain.cpp
effect_chain_test.cpp
footer.comp

index 79e4020..1dab990 100644 (file)
--- a/effect.h
+++ b/effect.h
@@ -225,9 +225,9 @@ public:
        virtual bool one_to_one_sampling() const { return strong_one_to_one_sampling(); }
 
        // Similar in use to one_to_one_sampling(), but even stricter:
-       // The effect must not use texture coordinate in any way beyond
-       // giving it unmodified to its (single) input. This allows it to
-       // also be used after a compute shader, in the same phase.
+       // The effect must modify texture coordinate in any way when
+       // calling its input(s). This allows it to also be used after
+       // a compute shader, in the same phase.
        //
        // An effect that it strong one-to-one must also be one-to-one.
        virtual bool strong_one_to_one_sampling() const { return false; }
index 5cbb7f9..6d6886c 100644 (file)
@@ -412,24 +412,28 @@ void EffectChain::compile_glsl_program(Phase *phase)
        for (unsigned i = 0; i < phase->effects.size(); ++i) {
                Node *node = phase->effects[i];
                const string effect_id = phase->effect_ids[make_pair(node, IN_SAME_PHASE)];
-               if (node->incoming_links.size() == 1) {
-                       Node *input = node->incoming_links[0];
-                       NodeLinkType link_type = node->incoming_link_type[0];
-                       if (i != 0 && input->effect->is_compute_shader()) {
-                               // First effect after the compute shader reads the value
-                               // that cs_output() wrote to a global variable.
-                               frag_shader += string("#define INPUT(tc) CS_OUTPUT_VAL\n");
+               for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
+                       if (node->incoming_links.size() == 1) {
+                               frag_shader += "#define INPUT";
                        } else {
-                               frag_shader += string("#define INPUT ") + phase->effect_ids[make_pair(input, link_type)] + "\n";
-                       }
-               } else {
-                       for (unsigned j = 0; j < node->incoming_links.size(); ++j) {
-                               assert(!node->incoming_links[j]->effect->is_compute_shader());
                                char buf[256];
-                               string effect_id = phase->effect_ids[make_pair(node->incoming_links[j], node->incoming_link_type[j])];
-                               sprintf(buf, "#define INPUT%d %s\n", j + 1, effect_id.c_str());
+                               sprintf(buf, "#define INPUT%d", j + 1);
                                frag_shader += buf;
                        }
+
+                       Node *input = node->incoming_links[j];
+                       NodeLinkType link_type = node->incoming_link_type[j];
+                       if (i != 0 &&
+                           input->effect->is_compute_shader() &&
+                           node->incoming_link_type[j] == IN_SAME_PHASE) {
+                               // First effect after the compute shader reads the value
+                               // that cs_output() wrote to a global variable,
+                               // ignoring the tc (since all such effects have to be
+                               // strong one-to-one).
+                               frag_shader += "(tc) CS_OUTPUT_VAL\n";
+                       } else {
+                               frag_shader += string(" ") + phase->effect_ids[make_pair(input, link_type)] + "\n";
+                       }
                }
        
                frag_shader += "\n";
@@ -679,6 +683,8 @@ Phase *EffectChain::construct_phase(Node *output, map<Node *, Phase *> *complete
 
                phase->effects.push_back(node);
                if (node->effect->is_compute_shader()) {
+                       assert(phase->compute_shader_node == nullptr ||
+                              phase->compute_shader_node == node);
                        phase->is_compute_shader = true;
                        phase->compute_shader_node = node;
                }
@@ -753,13 +759,17 @@ Phase *EffectChain::construct_phase(Node *output, map<Node *, Phase *> *complete
                        }
 
                        if (deps[i]->effect->is_compute_shader()) {
-                               // Only one compute shader per phase; we should have been stopped
-                               // already due to the fact that compute shaders are not one-to-one.
-                               assert(!phase->is_compute_shader);
-
-                               // If all nodes so far are strong one-to-one, we can put them after
-                               // the compute shader (ie., process them on the output).
-                               start_new_phase = !node->strong_one_to_one_sampling;
+                               if (phase->is_compute_shader) {
+                                       // Only one compute shader per phase.
+                                       start_new_phase = true;
+                               } else if (!node->strong_one_to_one_sampling) {
+                                       // If all nodes so far are strong one-to-one, we can put them after
+                                       // the compute shader (ie., process them on the output).
+                                       start_new_phase = !node->strong_one_to_one_sampling;
+                               } else {
+                                       phase->is_compute_shader = true;
+                                       phase->compute_shader_node = deps[i];
+                               }
                        } else if (deps[i]->effect->sets_virtual_output_size()) {
                                assert(deps[i]->effect->changes_output_size());
                                // If the next effect sets a virtual size to rely on OpenGL's
@@ -1783,20 +1793,38 @@ void EffectChain::add_dither_if_needed()
        dither_effect = dither->effect;
 }
 
+namespace {
+
+// Whether this effect will cause the phase it is in to become a compute shader phase.
+bool induces_compute_shader(Node *node)
+{
+       if (node->effect->is_compute_shader()) {
+               return true;
+       }
+       if (!node->effect->strong_one_to_one_sampling()) {
+               // This effect can't be chained after a compute shader.
+               return false;
+       }
+       // If at least one of the effects we depend on is a compute shader,
+       // one of them will be put in the same phase as us (the other ones,
+       // if any, will be bounced).
+       for (Node *dep : node->incoming_links) {
+               if (induces_compute_shader(dep)) {
+                       return true;
+               }
+       }
+       return false;
+}
+
+}  // namespace
+
 // Compute shaders can't output to the framebuffer, so if the last
 // phase ends in a compute shader, add a dummy phase at the end that
 // only blits directly from the temporary texture.
 void EffectChain::add_dummy_effect_if_needed()
 {
        Node *output = find_output_node();
-
-       // See if the last effect that's not strong one-to-one is a compute shader.
-       Node *last_effect = output;
-       while (last_effect->effect->num_inputs() == 1 &&
-              last_effect->effect->strong_one_to_one_sampling()) {
-               last_effect = last_effect->incoming_links[0];
-       }
-       if (last_effect->effect->is_compute_shader()) {
+       if (induces_compute_shader(output)) {
                Node *dummy = add_node(new ComputeShaderOutputDisplayEffect());
                connect_nodes(output, dummy);
                has_dummy_effect = true;
index f51f988..aa574fc 100644 (file)
@@ -1776,4 +1776,113 @@ TEST(ComputeShaderTest, ResizingComputeThenOneToOne) {
        EXPECT_EQ(1u, phase->output_height);
 }
 
+class StrongOneToOneAddEffect : public AddEffect {
+public:
+       StrongOneToOneAddEffect() {}
+       string effect_type_id() const override { return "StrongOneToOneAddEffect"; }
+       bool strong_one_to_one_sampling() const override { return true; }
+};
+
+TEST(ComputeShaderTest, NoTwoComputeInSamePhase) {
+       float data[] = {
+               0.0f, 0.25f, 0.3f, 0.8f,
+               0.75f, 1.0f, 1.0f, 0.2f,
+       };
+       float expected_data[] = {
+               0.0f, 0.3f,
+       };
+       float out_data[2];
+
+       ImageFormat format;
+       format.color_space = COLORSPACE_sRGB;
+       format.gamma_curve = GAMMA_LINEAR;
+
+       EffectChainTester tester(nullptr, 2, 1);
+
+       FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input1->set_pixel_data(data);
+       tester.get_chain()->add_input(input1);
+       Effect *downscale1 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+
+       FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input2->set_pixel_data(data);
+       tester.get_chain()->add_input(input2);
+       Effect *downscale2 = tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+
+       tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), downscale1, downscale2);
+       tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+       expect_equal(expected_data, out_data, 2, 1);
+}
+
+// Like the previous test, but the adder effect is not directly connected
+// to the compute shaders (so the status has to be propagated through those effects).
+TEST(ComputeShaderTest, NoTwoComputeInSamePhaseIndirect) {
+       float data[] = {
+               0.0f, 0.25f, 0.3f, 0.8f,
+               0.75f, 1.0f, 1.0f, 0.2f,
+       };
+       float expected_data[] = {
+               0.0f, 0.3f,
+       };
+       float out_data[2];
+
+       ImageFormat format;
+       format.color_space = COLORSPACE_sRGB;
+       format.gamma_curve = GAMMA_LINEAR;
+
+       EffectChainTester tester(nullptr, 2, 1);
+
+       FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input1->set_pixel_data(data);
+       tester.get_chain()->add_input(input1);
+       tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+       Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
+
+       FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input2->set_pixel_data(data);
+       tester.get_chain()->add_input(input2);
+       tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+       Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
+
+       tester.get_chain()->add_effect(new StrongOneToOneAddEffect(), identity1, identity2);
+       tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+       expect_equal(expected_data, out_data, 2, 1);
+}
+
+// Like the previous test, but the adder is not strong one-to-one
+// (so there are two different compute shader inputs, but none of them
+// are in the same phase).
+TEST(ComputeShaderTest, BounceTextureFromTwoComputeShaders) {
+       float data[] = {
+               0.0f, 0.25f, 0.3f, 0.8f,
+               0.75f, 1.0f, 1.0f, 0.2f,
+       };
+       float expected_data[] = {
+               0.0f, 0.3f,
+       };
+       float out_data[2];
+
+       ImageFormat format;
+       format.color_space = COLORSPACE_sRGB;
+       format.gamma_curve = GAMMA_LINEAR;
+
+       EffectChainTester tester(nullptr, 2, 1);
+
+       FlatInput *input1 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input1->set_pixel_data(data);
+       tester.get_chain()->add_input(input1);
+       tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+       Effect *identity1 = tester.get_chain()->add_effect(new OneToOneEffect());
+
+       FlatInput *input2 = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, 4, 2);
+       input2->set_pixel_data(data);
+       tester.get_chain()->add_input(input2);
+       tester.get_chain()->add_effect(new Downscale2xComputeEffect());
+       Effect *identity2 = tester.get_chain()->add_effect(new OneToOneEffect());
+
+       tester.get_chain()->add_effect(new AddEffect(), identity1, identity2);
+       tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+       expect_equal(expected_data, out_data, 2, 1);
+}
+
 }  // namespace movit
index 17175d9..c59a93d 100644 (file)
@@ -26,9 +26,12 @@ void cs_output(uvec2 coord, vec4 val)
 
 void cs_output(ivec2 coord, vec4 val)
 {
-       // Run the value through any preprocessing steps we might have.
+       // Run the value through any postprocessing steps we might have.
+       // Note that we need to give in the actual coordinates, since the
+       // effect could have multiple (non-compute) inputs, and would also
+       // be allowed to make effects based on the texture coordinate alone.
        CS_OUTPUT_VAL = val;
-       val = CS_POSTPROC(vec2(0.0, 0.0));
+       val = CS_POSTPROC(NORMALIZE_TEXTURE_COORDS(coord));
 
 #if SQUARE_ROOT_TRANSFORMATION
        // Make sure we don't give negative values to sqrt.