AlphaDivisionEffect() {}
virtual std::string effect_type_id() const { return "AlphaDivisionEffect"; }
std::string output_fragment_shader();
+ virtual bool one_to_one_sampling() const { return true; }
};
} // namespace movit
AlphaMultiplicationEffect() {}
virtual std::string effect_type_id() const { return "AlphaMultiplicationEffect"; }
std::string output_fragment_shader();
+ virtual bool one_to_one_sampling() const { return true; }
};
} // namespace movit
}
}
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return true; }
+ virtual bool one_to_one_sampling() const { return false; } // Can sample outside the border.
virtual void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const {
*width = this->width;
virtual bool needs_srgb_primaries() const { return false; }
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
// Get a conversion matrix from the given color space to XYZ.
static Eigen::Matrix3d get_xyz_matrix(Colorspace space);
// no way of expressing that currently.
virtual bool needs_texture_bounce() const { return true; }
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height);
virtual void get_output_size(unsigned *width, unsigned *height,
virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
unsigned num_inputs() const { return 2; }
+ virtual bool one_to_one_sampling() const { return true; }
private:
float blurred_mix_amount;
// premultiplied error. However, we need to do dithering in the same
// space as quantization, whether that be pre- or postmultiply.
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
// needs mipmaps, you will also get them).
virtual bool needs_mipmaps() const { return false; }
+ // Whether there is a direct correspondence between input and output
+ // texels. Specifically, the effect must not:
+ //
+ // 1. Try to sample in the border (ie., outside the 0.0 to 1.0 area).
+ // 2. Try to sample between texels.
+ // 3. Sample with an x- or y-derivative different from -1 or 1.
+ // (This also means needs_mipmaps() and one_to_one_sampling()
+ // together would make no sense.)
+ //
+ // The most common case for this would be an effect that has an exact
+ // 1:1-correspondence between input and output texels, e.g. SaturationEffect.
+ // However, more creative things, like mirroring/flipping or padding,
+ // would also be allowed.
+ //
+ // The primary gain from setting this is that you can sample directly
+ // from an effect that changes output size (see changes_output_size() below),
+ // without going through a bounce texture. It won't work for effects that
+ // set sets_virtual_output_size(), though.
+ //
+ // Does not make a lot of sense together with needs_texture_bounce().
+ virtual bool one_to_one_sampling() const { return false; }
+
// Whether this effect wants to output to a different size than
- // its input(s) (see inform_input_size(), below). If you set this to
- // true, the output will be bounced to a texture (similarly to if the
- // next effect set needs_texture_bounce()).
+ // its input(s) (see inform_input_size(), below). See also
+ // sets_virtual_output_size() below.
virtual bool changes_output_size() const { return false; }
+ // Whether your get_output_size() function (see below) intends to ever set
+ // virtual_width different from width, or similar for height.
+ // It does not make sense to set this to true if changes_output_size() is false.
+ virtual bool sets_virtual_output_size() const { return changes_output_size(); }
+
// Whether this effect is effectively sampling from a a single texture.
// If so, it will override needs_texture_bounce(); however, there are also
// two demands it needs to fulfill:
node->output_gamma_curve = GAMMA_INVALID;
node->output_alpha_type = ALPHA_INVALID;
node->needs_mipmaps = false;
+ node->one_to_one_sampling = false;
nodes.push_back(node);
node_map[effect] = node;
// Construct GLSL programs, starting at the given effect and following
// the chain from there. We end a program every time we come to an effect
// marked as "needs texture bounce", one that is used by multiple other
-// effects, every time an effect wants to change the output size,
-// and of course at the end.
+// effects, every time we need to bounce due to output size change
+// (not all size changes require ending), and of course at the end.
//
// We follow a quite simple depth-first search from the output, although
// without recursing explicitly within each phase.
Phase *phase = new Phase;
phase->output_node = output;
+ // If the output effect has one-to-one sampling, we try to trace this
+ // status down through the dependency chain. This is important in case
+ // we hit an effect that changes output size (and not sets a virtual
+ // output size); if we have one-to-one sampling, we don't have to break
+ // the phase.
+ output->one_to_one_sampling = output->effect->one_to_one_sampling();
+
// Effects that we have yet to calculate, but that we know should
// be in the current phase.
stack<Node *> effects_todo_this_phase;
}
}
- if (deps[i]->effect->changes_output_size()) {
+ 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
+ // bilinear sampling, we'll really need to break the phase here.
+ start_new_phase = true;
+ } else if (deps[i]->effect->changes_output_size() && !node->one_to_one_sampling) {
+ // If the next effect changes size and we don't have one-to-one sampling,
+ // we also need to break here.
start_new_phase = true;
}
phase->inputs.push_back(construct_phase(deps[i], completed_effects));
} else {
effects_todo_this_phase.push(deps[i]);
+
+ // Propagate the one-to-one status down through the dependency.
+ deps[i]->one_to_one_sampling = node->one_to_one_sampling &&
+ deps[i]->effect->one_to_one_sampling();
}
}
}
}
}
+ // Tell each node which phase it ended up in, so that the unit test
+ // can check that the phases were split in the right place.
+ // Note that this ignores that effects may be part of multiple phases;
+ // if the unit tests need to test such cases, we'll reconsider.
+ for (unsigned i = 0; i < phase->effects.size(); ++i) {
+ phase->effects[i]->containing_phase = phase;
+ }
+
// Actually make the shader for this phase.
compile_glsl_program(phase);
if (node->effect->changes_output_size()) {
// We cannot call get_output_size() before we've done inform_input_size()
// on all inputs.
- unsigned real_width_ignored, real_height_ignored;
- node->effect->get_output_size(&real_width_ignored, &real_height_ignored,
+ unsigned real_width, real_height;
+ node->effect->get_output_size(&real_width, &real_height,
&node->output_width, &node->output_height);
+ assert(node->effect->sets_virtual_output_size() ||
+ (real_width == node->output_width &&
+ real_height == node->output_height));
} else {
node->output_width = this_output_width;
node->output_height = this_output_height;
if (output_node->effect->changes_output_size()) {
output_node->effect->get_output_size(&phase->output_width, &phase->output_height,
&phase->virtual_output_width, &phase->virtual_output_height);
+ assert(output_node->effect->sets_virtual_output_size() ||
+ (phase->output_width == phase->virtual_output_width &&
+ phase->output_height == phase->virtual_output_height));
return;
}
std::vector<Node *> outgoing_links;
std::vector<Node *> incoming_links;
+ // For unit tests only. Do not use from other code.
+ // Will contain an arbitrary choice if the node is in multiple phases.
+ Phase *containing_phase;
+
private:
// Logical size of the output of this effect, ie. the resolution
// you would get if you sampled it as a texture. If it is undefined
AlphaType output_alpha_type;
bool needs_mipmaps; // Directly or indirectly.
+ // Set if this effect, and all effects consuming output from this node
+ // (in the same phase) have one_to_one_sampling() set.
+ bool one_to_one_sampling;
+
friend class EffectChain;
};
expect_equal(data, out_data, size, size);
}
+// An effect that is like VirtualResizeEffect, but always has virtual and real
+// sizes the same (and promises this).
+class NonVirtualResizeEffect : public VirtualResizeEffect {
+public:
+ NonVirtualResizeEffect(int width, int height)
+ : VirtualResizeEffect(width, height, width, height) {}
+ virtual string effect_type_id() const { return "NonVirtualResizeEffect"; }
+ virtual bool sets_virtual_output_size() const { return false; }
+};
+
+// An effect that promises one-to-one sampling (unlike IdentityEffect).
+class OneToOneEffect : public Effect {
+public:
+ OneToOneEffect() {}
+ virtual string effect_type_id() const { return "OneToOneEffect"; }
+ string output_fragment_shader() { return read_file("identity.frag"); }
+ virtual bool one_to_one_sampling() const { return true; }
+};
+
+TEST(EffectChainTest, NoBounceWithOneToOneSampling) {
+ const int size = 2;
+ float data[size * size] = {
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ };
+ float out_data[size * size];
+
+ EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
+ RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
+
+ tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
+ tester.get_chain()->add_effect(effect1);
+ tester.get_chain()->add_effect(effect2);
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ expect_equal(data, out_data, size, size);
+
+ // The first OneToOneEffect should be in the same phase as its input.
+ ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
+ EXPECT_EQ(effect1->replaced_node->incoming_links[0]->containing_phase,
+ effect1->replaced_node->containing_phase);
+
+ // The second OneToOneEffect, too.
+ EXPECT_EQ(effect1->replaced_node->containing_phase,
+ effect2->replaced_node->containing_phase);
+}
+
+TEST(EffectChainTest, BounceWhenOneToOneIsBroken) {
+ const int size = 2;
+ float data[size * size] = {
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ };
+ float out_data[size * size];
+
+ EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ RewritingEffect<OneToOneEffect> *effect1 = new RewritingEffect<OneToOneEffect>();
+ RewritingEffect<OneToOneEffect> *effect2 = new RewritingEffect<OneToOneEffect>();
+ RewritingEffect<IdentityEffect> *effect3 = new RewritingEffect<IdentityEffect>();
+ RewritingEffect<OneToOneEffect> *effect4 = new RewritingEffect<OneToOneEffect>();
+
+ tester.get_chain()->add_effect(new NonVirtualResizeEffect(size, size));
+ tester.get_chain()->add_effect(effect1);
+ tester.get_chain()->add_effect(effect2);
+ tester.get_chain()->add_effect(effect3);
+ tester.get_chain()->add_effect(effect4);
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ expect_equal(data, out_data, size, size);
+
+ // The NonVirtualResizeEffect should be in a different phase from
+ // the IdentityEffect (since the latter is not one-to-one),
+ // ie., the chain should be broken somewhere between them, but exactly
+ // where doesn't matter.
+ ASSERT_EQ(1, effect1->replaced_node->incoming_links.size());
+ EXPECT_NE(effect1->replaced_node->incoming_links[0]->containing_phase,
+ effect3->replaced_node->containing_phase);
+
+ // The last OneToOneEffect should also be in the same phase as the
+ // IdentityEffect (the phase was already broken).
+ EXPECT_EQ(effect3->replaced_node->containing_phase,
+ effect4->replaced_node->containing_phase);
+}
+
// Does not use EffectChainTest, so that it can construct an EffectChain without
// a shared ResourcePool (which is also properly destroyed afterwards).
// Also turns on debugging to test that code path.
// in our own phase, which is exactly what we want.
virtual bool needs_texture_bounce() const { return true; }
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height)
{
virtual void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
virtual bool needs_srgb_primaries() const { return false; }
+ virtual bool one_to_one_sampling() const { return true; }
// Actually needs postmultiplied input as well as outputting it.
// EffectChain will take care of that.
virtual bool needs_linear_light() const { return false; }
virtual bool needs_srgb_primaries() const { return false; }
+ virtual bool one_to_one_sampling() const { return true; }
// Actually processes its input in a nonlinear fashion,
// but does not touch alpha, and we are a special case anyway.
std::string output_fragment_shader();
virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
+ virtual bool one_to_one_sampling() const { return true; }
private:
float cutoff;
LiftGammaGainEffect();
virtual std::string effect_type_id() const { return "LiftGammaGainEffect"; }
virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
+ virtual bool one_to_one_sampling() const { return true; }
std::string output_fragment_shader();
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
virtual bool needs_srgb_primaries() const { return false; }
virtual unsigned num_inputs() const { return 3; }
+ virtual bool one_to_one_sampling() const { return true; }
virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
private:
virtual bool needs_linear_light() const { return false; }
virtual bool needs_srgb_primaries() const { return false; }
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
};
} // namespace movit
virtual bool needs_srgb_primaries() const { return false; }
virtual unsigned num_inputs() const { return 2; }
+ virtual bool one_to_one_sampling() const { return true; }
// TODO: In the common case where a+b=1, it would be useful to be able to set
// alpha_handling() to INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK. However, right now
MultiplyEffect();
virtual std::string effect_type_id() const { return "MultiplyEffect"; }
std::string output_fragment_shader();
+ virtual bool one_to_one_sampling() const { return true; }
private:
RGBATuple factor;
virtual bool needs_srgb_primaries() const { return false; }
virtual unsigned num_inputs() const { return 2; }
+ virtual bool one_to_one_sampling() const { return true; }
// Actually, if _either_ image has blank alpha, our output will have
// blank alpha, too (this only tells the framework that having _both_
virtual AlphaHandling alpha_handling() const;
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
+ virtual bool one_to_one_sampling() const { return true; }
virtual void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const;
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height);
}
}
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
virtual void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const {
*virtual_width = *width = this->output_width;
virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
virtual void get_output_size(unsigned *width, unsigned *height, unsigned *virtual_width, unsigned *virtual_height) const;
private:
SaturationEffect();
virtual std::string effect_type_id() const { return "SaturationEffect"; }
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
std::string output_fragment_shader();
private:
std::string output_fragment_shader();
virtual bool needs_texture_bounce() const { return true; }
virtual bool changes_output_size() const { return true; }
+ virtual bool sets_virtual_output_size() const { return false; }
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height);
virtual void get_output_size(unsigned *width, unsigned *height,
unsigned *virtual_width, unsigned *virtual_height) const;
virtual bool needs_srgb_primaries() const { return false; }
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height);
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);
WhiteBalanceEffect();
virtual std::string effect_type_id() const { return "WhiteBalanceEffect"; }
virtual AlphaHandling alpha_handling() const { return DONT_CARE_ALPHA_TYPE; }
+ virtual bool one_to_one_sampling() const { return true; }
std::string output_fragment_shader();
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num);