std::string effect_type_id() const override { return "BlurEffect"; }
- // We want this for the same reason as ResizeEffect; we could end up scaling
- // down quite a lot.
- bool needs_texture_bounce() const override { return true; }
- bool needs_mipmaps() const override { return true; }
- bool needs_srgb_primaries() const override { return false; }
-
void inform_input_size(unsigned input_num, unsigned width, unsigned height) override;
std::string output_fragment_shader() override {
std::string output_fragment_shader() override;
+ // We want this for the same reason as ResizeEffect; we could end up scaling
+ // down quite a lot.
bool needs_texture_bounce() const override { return true; }
- bool needs_mipmaps() const override { return true; }
+ MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
bool needs_srgb_primaries() const override { return false; }
AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
--- /dev/null
+// Used only for testing.
+
+// Implicit uniforms:
+// uniform vec2 PREFIX(offset);
+
+vec4 FUNCNAME(vec2 tc)
+{
+ return INPUT(tc * 2.0 + PREFIX(offset));
+// vec2 z = tc * 2.0 + PREFIX(offset);
+// return vec4(z.y, 0.0f, 0.0f, 1.0f);
+}
// either will be fine.
virtual bool needs_texture_bounce() const { return false; }
- // Whether this effect expects mipmaps or not. If you set this to
- // true, you will be sampling with bilinear filtering; if not,
- // you could be sampling with simple linear filtering and no mipmaps
- // (although there is no guarantee; if a different effect in the chain
- // needs mipmaps, you will also get them).
- virtual bool needs_mipmaps() const { return false; }
+ // Whether this effect expects mipmaps or not.
+ enum MipmapRequirements {
+ // If chosen, you will be sampling with bilinear filtering,
+ // ie. the closest mipmap will be chosen, and then there will be
+ // bilinear interpolation inside it (GL_LINEAR_MIPMAP_NEAREST).
+ NEEDS_MIPMAPS,
+
+ // Whether the effect doesn't really care whether input textures
+ // are with or without mipmaps. You could get the same effect
+ // as NEEDS_MIPMAPS or CANNOT_ACCEPT_MIPMAPS; normally, you won't
+ // get them, but if a different effect in the same phase needs mipmaps,
+ // you will also get them.
+ DOES_NOT_NEED_MIPMAPS,
+
+ // The opposite of NEEDS_MIPMAPS; you will always be sampling from
+ // the most detailed mip level (GL_LINEAR). Effects with NEEDS_MIPMAPS
+ // and CANNOT_ACCEPT_MIPMAPS can not coexist within the same phase;
+ // such phases will be split.
+ //
+ // This is the only choice that makes sense for a compute shader,
+ // given that it doesn't have screen-space derivatives and thus
+ // always will sample the most detailed mip level.
+ CANNOT_ACCEPT_MIPMAPS,
+ };
+ virtual MipmapRequirements needs_mipmaps() const {
+ if (is_compute_shader()) {
+ return CANNOT_ACCEPT_MIPMAPS;
+ } else {
+ return DOES_NOT_NEED_MIPMAPS;
+ }
+ }
// Whether there is a direct correspondence between input and output
// texels. Specifically, the effect must not:
node->output_color_space = COLORSPACE_INVALID;
node->output_gamma_curve = GAMMA_INVALID;
node->output_alpha_type = ALPHA_INVALID;
- node->needs_mipmaps = false;
+ node->needs_mipmaps = Effect::DOES_NOT_NEED_MIPMAPS;
node->one_to_one_sampling = false;
node->strong_one_to_one_sampling = false;
assert(node->effect->one_to_one_sampling() >= node->effect->strong_one_to_one_sampling());
- if (node->effect->needs_mipmaps()) {
- node->needs_mipmaps = true;
+ if (node->effect->needs_mipmaps() != Effect::DOES_NOT_NEED_MIPMAPS) {
+ // Can't have incompatible requirements imposed on us from a dependent effect;
+ // if so, it should have started a new phase instead.
+ assert(node->needs_mipmaps == Effect::DOES_NOT_NEED_MIPMAPS ||
+ node->needs_mipmaps == node->effect->needs_mipmaps());
+ node->needs_mipmaps = node->effect->needs_mipmaps();
}
// This should currently only happen for effects that are inputs
// Note that we cannot do this propagation as a normal pass,
// because it needs information about where the phases end
// (we should not propagate the flag across phases).
- if (node->needs_mipmaps) {
- if (deps[i]->effect->num_inputs() == 0) {
+ if (node->needs_mipmaps != Effect::DOES_NOT_NEED_MIPMAPS) {
+ if (deps[i]->effect->num_inputs() == 0 && node->needs_mipmaps == Effect::NEEDS_MIPMAPS) {
Input *input = static_cast<Input *>(deps[i]->effect);
start_new_phase |= !input->can_supply_mipmaps();
- } else {
- deps[i]->needs_mipmaps = true;
+ } else if (deps[i]->effect->needs_mipmaps() == Effect::DOES_NOT_NEED_MIPMAPS) {
+ deps[i]->needs_mipmaps = node->needs_mipmaps;
+ } else if (deps[i]->effect->needs_mipmaps() != node->needs_mipmaps) {
+ start_new_phase = true;
}
}
phase->input_needs_mipmaps = false;
for (unsigned i = 0; i < phase->effects.size(); ++i) {
Node *node = phase->effects[i];
- phase->input_needs_mipmaps |= node->effect->needs_mipmaps();
+ if (node->effect->needs_mipmaps() == Effect::NEEDS_MIPMAPS) {
+ phase->input_needs_mipmaps = true;
+ }
}
for (unsigned i = 0; i < phase->effects.size(); ++i) {
Node *node = phase->effects[i];
template<class T>
class RewritingEffect : public Effect {
public:
- RewritingEffect() : effect(new T()), replaced_node(nullptr) {}
+ template<class... Args>
+ RewritingEffect(Args &&... args) : effect(new T(std::forward<Args>(args)...)), replaced_node(nullptr) {}
string effect_type_id() const override { return "RewritingEffect[" + effect->effect_type_id() + "]"; }
string output_fragment_shader() override { EXPECT_TRUE(false); return read_file("identity.frag"); }
void rewrite_graph(EffectChain *graph, Node *self) override {
class MipmapNeedingEffect : public Effect {
public:
MipmapNeedingEffect() {}
- bool needs_mipmaps() const override { return true; }
+ MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
// To be allowed to mess with the sampler state.
bool needs_texture_bounce() const override { return true; }
expect_equal(expected_data, out_data, 4, 16);
}
+// An effect to verify that you can turn off mipmaps; it downscales by two,
+// which gives blur with mipmaps and aliasing (picks out every other pixel)
+// without.
+class Downscale2xEffect : public Effect {
+public:
+ explicit Downscale2xEffect(MipmapRequirements mipmap_requirements)
+ : mipmap_requirements(mipmap_requirements)
+ {
+ register_vec2("offset", offset);
+ }
+ MipmapRequirements needs_mipmaps() const override { return mipmap_requirements; }
+
+ string effect_type_id() const override { return "Downscale2xEffect"; }
+ string output_fragment_shader() override { return read_file("downscale2x.frag"); }
+
+private:
+ const MipmapRequirements mipmap_requirements;
+ EffectChain *chain;
+ float offset[2] { 0.0f, 0.0f };
+};
+
+TEST(EffectChainTest, MipmapChainGetsSplit) {
+ float data[] = {
+ 0.0f, 0.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 1.0f, 0.0f,
+ };
+
+ // The intermediate result after the first step looks like this,
+ // assuming there are no mipmaps (the zeros are due to border behavior):
+ //
+ // 0 0 0 0
+ // 0 0 0 0
+ // 1 1 0 0
+ // 1 1 0 0
+ //
+ // so another 2x downscale towards the bottom left will give
+ //
+ // 0 0
+ // 1 0
+ //
+ // with yet more zeros coming in on the top and the right from the border.
+ float expected_data[] = {
+ 0.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 0.0f,
+ 1.0f, 0.0f, 0.0f, 0.0f,
+ };
+ float out_data[4 * 4];
+
+ float offset[] = { -0.5f / 4.0f, -0.5f / 4.0f };
+ RewritingEffect<Downscale2xEffect> *pick_out_top_left = new RewritingEffect<Downscale2xEffect>(Effect::CANNOT_ACCEPT_MIPMAPS);
+ ASSERT_TRUE(pick_out_top_left->effect->set_vec2("offset", offset));
+
+ RewritingEffect<Downscale2xEffect> *downscale2x = new RewritingEffect<Downscale2xEffect>(Effect::NEEDS_MIPMAPS);
+
+ EffectChainTester tester(data, 4, 4, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR);
+ tester.get_chain()->add_effect(pick_out_top_left);
+ tester.get_chain()->add_effect(downscale2x);
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ EXPECT_NE(pick_out_top_left->replaced_node->containing_phase,
+ downscale2x->replaced_node->containing_phase);
+
+ expect_equal(expected_data, out_data, 4, 4);
+}
+
// An effect that adds its two inputs together. Used below.
class AddEffect : public Effect {
public:
} else {
glBindTexture(GL_TEXTURE_2D, texture_num);
check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, needs_mipmaps ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
+ check_error();
}
// Bind it to a sampler.
#include "test_util.h"
#include "util.h"
+using namespace std;
+
namespace movit {
TEST(FlatInput, SimpleGrayscale) {
expect_equal(expected_data, out_data, 4, size);
}
+// Just an IdentityEffect, but marks as needing mipmaps, so that we can use it
+// for downscaling to verify mipmaps were used.
+class MipmapNeedingEffect : public Effect {
+public:
+ MipmapNeedingEffect() {}
+ MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
+
+ string effect_type_id() const override { return "MipmapNeedingEffect"; }
+ string output_fragment_shader() override { return read_file("identity.frag"); }
+
+private:
+ EffectChain *chain;
+};
+
+TEST(FlatInput, ExternalTextureMipmapState) {
+ const int width = 4;
+ const int height = 4;
+
+ float data[width * height] = {
+ 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ };
+ float expected_data[] = {
+ 0.0625,
+ };
+ float out_data[1];
+
+ EffectChainTester tester(nullptr, 1, 1, FORMAT_RGB, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ ImageFormat format;
+ format.color_space = COLORSPACE_sRGB;
+ format.gamma_curve = GAMMA_LINEAR;
+
+ ResourcePool pool;
+ GLuint tex = pool.create_2d_texture(GL_R8, width, height);
+ check_error();
+ glBindTexture(GL_TEXTURE_2D, tex);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
+ check_error();
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ check_error();
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_FLOAT, data);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ check_error();
+ glGenerateMipmap(GL_TEXTURE_2D);
+ check_error();
+
+ // Turn off mipmaps, so that we verify that Movit turns it back on.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ check_error();
+
+ FlatInput *input = new FlatInput(format, FORMAT_GRAYSCALE, GL_FLOAT, width, height);
+ input->set_texture_num(tex);
+ tester.get_chain()->add_input(input);
+ tester.get_chain()->add_effect(new MipmapNeedingEffect);
+
+ tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR);
+
+ pool.release_2d_texture(tex);
+
+ expect_equal(expected_data, out_data, 1, 1);
+}
+
TEST(FlatInput, NoData) {
const int width = 2;
const int height = 4;
} else {
uniform_whole_pixel_offset = lrintf(offset) / float(input_width);
}
-
- // We specifically do not want mipmaps on the input texture;
- // they break minification.
- Node *self = chain->find_node_for_effect(this);
- if (chain->has_input_sampler(self, 0)) {
- glActiveTexture(chain->get_input_sampler(self, 0));
- check_error();
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- check_error();
- }
}
Support2DTexture::Support2DTexture()
std::string effect_type_id() const override { return "ResampleEffect"; }
- // We want this for the same reason as ResizeEffect; we could end up scaling
- // down quite a lot.
- bool needs_texture_bounce() const override { return true; }
- bool needs_srgb_primaries() const override { return false; }
-
void inform_input_size(unsigned input_num, unsigned width, unsigned height) override;
std::string output_fragment_shader() override {
bool needs_srgb_primaries() const override { return false; }
AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
+ // We specifically do not want mipmaps on the input texture;
+ // they break minification.
+ MipmapRequirements needs_mipmaps() const override { return CANNOT_ACCEPT_MIPMAPS; }
+
void inform_added(EffectChain *chain) override { this->chain = chain; }
void inform_input_size(unsigned input_num, unsigned width, unsigned height) override {
if (parent != nullptr) {
// We want processing done pre-filtering and mipmapped,
// in case we need to scale down a lot.
bool needs_texture_bounce() const override { return true; }
- bool needs_mipmaps() const override { return true; }
+ MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
AlphaHandling alpha_handling() const override { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; }
bool changes_output_size() const override { return true; }
// changes, even within git versions. There is no specific version
// documentation outside the regular changelogs, though.
-#define MOVIT_VERSION 34
+#define MOVIT_VERSION 35
#endif // !defined(_MOVIT_VERSION_H)
} else {
glBindTexture(GL_TEXTURE_2D, texture_num[channel]);
check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, needs_mipmaps ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR);
+ check_error();
}
}
class MipmapNeedingEffect : public Effect {
public:
MipmapNeedingEffect() {}
- bool needs_mipmaps() const override { return true; }
+ MipmapRequirements needs_mipmaps() const override { return NEEDS_MIPMAPS; }
// To be allowed to mess with the sampler state.
bool needs_texture_bounce() const override { return true; }