From: Steinar H. Gunderson Date: Thu, 17 Sep 2015 23:48:10 +0000 (+0200) Subject: Add a mode for YCbCrInput where Cb and Cr are in the same texture. X-Git-Tag: 1.2.0~4 X-Git-Url: https://git.sesse.net/?p=movit;a=commitdiff_plain;h=ae634b4c9fd1a8275b36458862ada71a44063108 Add a mode for YCbCrInput where Cb and Cr are in the same texture. --- diff --git a/version.h b/version.h index be23939..5a08560 100644 --- a/version.h +++ b/version.h @@ -5,6 +5,6 @@ // changes, even within git versions. There is no specific version // documentation outside the regular changelogs, though. -#define MOVIT_VERSION 5 +#define MOVIT_VERSION 6 #endif // !defined(_MOVIT_VERSION_H) diff --git a/ycbcr_422interleaved_input.h b/ycbcr_422interleaved_input.h index b7000a0..a8c7773 100644 --- a/ycbcr_422interleaved_input.h +++ b/ycbcr_422interleaved_input.h @@ -28,6 +28,11 @@ // RGBA texture (from which we sample chroma). We throw away half of the color // channels each time, so bandwidth is wasted, but it makes for a very // uncomplicated shader. +// +// Note that if you can shuffle your data around very cheaply on the CPU +// (say, while you're decoding it out of some other buffer anyway), +// regular YCbCrInput with YCBCR_INPUT_SPLIT_Y_AND_CBCR will probably be +// more efficient, as it doesn't need this bandwidth waste. #include #include diff --git a/ycbcr_input.cpp b/ycbcr_input.cpp index ed1f569..7f58f3f 100644 --- a/ycbcr_input.cpp +++ b/ycbcr_input.cpp @@ -18,9 +18,11 @@ namespace movit { YCbCrInput::YCbCrInput(const ImageFormat &image_format, const YCbCrFormat &ycbcr_format, - unsigned width, unsigned height) + unsigned width, unsigned height, + YCbCrInputSplitting ycbcr_input_splitting) : image_format(image_format), ycbcr_format(ycbcr_format), + ycbcr_input_splitting(ycbcr_input_splitting), width(width), height(height), resource_pool(NULL) @@ -41,13 +43,21 @@ YCbCrInput::YCbCrInput(const ImageFormat &image_format, pixel_data[0] = pixel_data[1] = pixel_data[2] = NULL; register_uniform_sampler2d("tex_y", &uniform_tex_y); - register_uniform_sampler2d("tex_cb", &uniform_tex_cb); - register_uniform_sampler2d("tex_cr", &uniform_tex_cr); + + if (ycbcr_input_splitting == YCBCR_INPUT_SPLIT_Y_AND_CBCR) { + num_channels = 2; + register_uniform_sampler2d("tex_cbcr", &uniform_tex_cb); + } else { + assert(ycbcr_input_splitting == YCBCR_INPUT_PLANAR); + num_channels = 3; + register_uniform_sampler2d("tex_cb", &uniform_tex_cb); + register_uniform_sampler2d("tex_cr", &uniform_tex_cr); + } } YCbCrInput::~YCbCrInput() { - for (unsigned channel = 0; channel < 3; ++channel) { + for (unsigned channel = 0; channel < num_channels; ++channel) { if (texture_num[channel] != 0) { resource_pool->release_2d_texture(texture_num[channel]); } @@ -56,13 +66,22 @@ YCbCrInput::~YCbCrInput() void YCbCrInput::set_gl_state(GLuint glsl_program_num, const string& prefix, unsigned *sampler_num) { - for (unsigned channel = 0; channel < 3; ++channel) { + for (unsigned channel = 0; channel < num_channels; ++channel) { glActiveTexture(GL_TEXTURE0 + *sampler_num + channel); check_error(); if (texture_num[channel] == 0) { + GLenum format, internal_format; + if (channel == 1 && ycbcr_input_splitting == YCBCR_INPUT_SPLIT_Y_AND_CBCR) { + format = GL_RG; + internal_format = GL_RG8; + } else { + format = GL_RED; + internal_format = GL_R8; + } + // (Re-)upload the texture. - texture_num[channel] = resource_pool->create_2d_texture(GL_R8, widths[channel], heights[channel]); + texture_num[channel] = resource_pool->create_2d_texture(internal_format, widths[channel], heights[channel]); glBindTexture(GL_TEXTURE_2D, texture_num[channel]); check_error(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -73,7 +92,7 @@ void YCbCrInput::set_gl_state(GLuint glsl_program_num, const string& prefix, uns check_error(); glPixelStorei(GL_UNPACK_ROW_LENGTH, pitch[channel]); check_error(); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, widths[channel], heights[channel], GL_RED, GL_UNSIGNED_BYTE, pixel_data[channel]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, widths[channel], heights[channel], format, GL_UNSIGNED_BYTE, pixel_data[channel]); check_error(); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); check_error(); @@ -93,9 +112,11 @@ void YCbCrInput::set_gl_state(GLuint glsl_program_num, const string& prefix, uns // Bind samplers. uniform_tex_y = *sampler_num + 0; uniform_tex_cb = *sampler_num + 1; - uniform_tex_cr = *sampler_num + 2; + if (ycbcr_input_splitting == YCBCR_INPUT_PLANAR) { + uniform_tex_cr = *sampler_num + 2; + } - *sampler_num += 3; + *sampler_num += num_channels; } string YCbCrInput::output_fragment_shader() @@ -121,6 +142,15 @@ string YCbCrInput::output_fragment_shader() ycbcr_format.cr_y_position, ycbcr_format.chroma_subsampling_y, heights[2]); frag_shader += output_glsl_vec2("PREFIX(cr_offset)", cr_offset_x, cr_offset_y); + if (ycbcr_input_splitting == YCBCR_INPUT_SPLIT_Y_AND_CBCR) { + char buf[256]; + snprintf(buf, sizeof(buf), "#define CB_CR_SAME_TEXTURE 1\n#define CB_CR_OFFSETS_EQUAL %d\n", + (fabs(ycbcr_format.cb_x_position - ycbcr_format.cr_x_position) < 1e-6)); + frag_shader += buf; + } else { + frag_shader += "#define CB_CR_SAME_TEXTURE 0\n"; + } + frag_shader += read_file("ycbcr_input.frag"); return frag_shader; } diff --git a/ycbcr_input.frag b/ycbcr_input.frag index b84499d..c57c6d1 100644 --- a/ycbcr_input.frag +++ b/ycbcr_input.frag @@ -1,7 +1,8 @@ // Implicit uniforms: // uniform sampler2D PREFIX(tex_y); -// uniform sampler2D PREFIX(tex_cb); -// uniform sampler2D PREFIX(tex_cr); +// uniform sampler2D PREFIX(tex_cbcr); // If CB_CR_SAME_TEXTURE. +// uniform sampler2D PREFIX(tex_cb); // If not CB_CR_SAME_TEXTURE. +// uniform sampler2D PREFIX(tex_cr); // If not CB_CR_SAME_TEXTURE. vec4 FUNCNAME(vec2 tc) { // OpenGL's origin is bottom-left, but most graphics software assumes @@ -11,8 +12,17 @@ vec4 FUNCNAME(vec2 tc) { vec3 ycbcr; ycbcr.x = tex2D(PREFIX(tex_y), tc).x; +#if CB_CR_SAME_TEXTURE +#if CB_CR_OFFSETS_EQUAL + ycbcr.yz = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cb_offset)).xy; +#else + ycbcr.y = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cb_offset)).x; + ycbcr.z = tex2D(PREFIX(tex_cbcr), tc + PREFIX(cr_offset)).x; +#endif +#else ycbcr.y = tex2D(PREFIX(tex_cb), tc + PREFIX(cb_offset)).x; ycbcr.z = tex2D(PREFIX(tex_cr), tc + PREFIX(cr_offset)).x; +#endif ycbcr -= PREFIX(offset); diff --git a/ycbcr_input.h b/ycbcr_input.h index 7db5375..282b55a 100644 --- a/ycbcr_input.h +++ b/ycbcr_input.h @@ -19,11 +19,28 @@ namespace movit { class ResourcePool; +// Whether the data is fully planar (Y', Cb and Cr in one texture each) +// or not. Note that this input does currently not support fully interleaved +// data (Y', Cb and Cr next to each other), as 4:4:4 interleaved Y'CbCr seems +// to be rare; however, YCbCr422InterleavedInput supports the important special +// case of 4:2:2 interleaved. +enum YCbCrInputSplitting { + // The standard, default case; Y', Cb and Cr in one texture each. + YCBCR_INPUT_PLANAR, + + // Y' in one texture, and then Cb and Cr interleaved in one texture. + // In particular, this is a superset of the relatively popular NV12 mode. + // If you specify this mode, the “Cr” pointer texture will be unused + // (the ”Cb” texture contains both). + YCBCR_INPUT_SPLIT_Y_AND_CBCR, +}; + class YCbCrInput : public Input { public: YCbCrInput(const ImageFormat &image_format, const YCbCrFormat &ycbcr_format, - unsigned width, unsigned height); + unsigned width, unsigned height, + YCbCrInputSplitting ycbcr_input_splitting = YCBCR_INPUT_PLANAR); ~YCbCrInput(); virtual std::string effect_type_id() const { return "YCbCrInput"; } @@ -54,7 +71,7 @@ public: // the pointer (and PBO, if set) has to be valid at the time of the render call. void set_pixel_data(unsigned channel, const unsigned char *pixel_data, GLuint pbo = 0) { - assert(channel >= 0 && channel < 3); + assert(channel >= 0 && channel < num_channels); this->pixel_data[channel] = pixel_data; this->pbos[channel] = pbo; invalidate_pixel_data(); @@ -63,7 +80,7 @@ public: void invalidate_pixel_data(); void set_pitch(unsigned channel, unsigned pitch) { - assert(channel >= 0 && channel < 3); + assert(channel >= 0 && channel < num_channels); this->pitch[channel] = pitch; invalidate_pixel_data(); } @@ -78,6 +95,8 @@ public: private: ImageFormat image_format; YCbCrFormat ycbcr_format; + GLuint num_channels; + YCbCrInputSplitting ycbcr_input_splitting; GLuint pbos[3], texture_num[3]; GLint uniform_tex_y, uniform_tex_cb, uniform_tex_cr; diff --git a/ycbcr_input_test.cpp b/ycbcr_input_test.cpp index 873a6c5..8b69e87 100644 --- a/ycbcr_input_test.cpp +++ b/ycbcr_input_test.cpp @@ -484,4 +484,58 @@ TEST(YCbCrInputTest, PBO) { glDeleteBuffers(1, &pbo); } +TEST(YCbCrInputTest, CombinedCbAndCr) { + const int width = 1; + const int height = 5; + + // Pure-color test inputs, calculated with the formulas in Rec. 601 + // section 2.5.4. + unsigned char y[width * height] = { + 16, 235, 81, 145, 41, + }; + unsigned char cb_cr[width * height * 2] = { + 128, 128, + 128, 128, + 90, 240, + 54, 34, + 240, 110, + }; + float expected_data[4 * width * height] = { + 0.0, 0.0, 0.0, 1.0, + 1.0, 1.0, 1.0, 1.0, + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + }; + float out_data[4 * width * height]; + + EffectChainTester tester(NULL, width, height); + + ImageFormat format; + format.color_space = COLORSPACE_sRGB; + format.gamma_curve = GAMMA_sRGB; + + YCbCrFormat ycbcr_format; + ycbcr_format.luma_coefficients = YCBCR_REC_601; + ycbcr_format.full_range = false; + ycbcr_format.num_levels = 256; + ycbcr_format.chroma_subsampling_x = 1; + ycbcr_format.chroma_subsampling_y = 1; + ycbcr_format.cb_x_position = 0.5f; + ycbcr_format.cb_y_position = 0.5f; + ycbcr_format.cr_x_position = 0.5f; + ycbcr_format.cr_y_position = 0.5f; + + YCbCrInput *input = new YCbCrInput(format, ycbcr_format, width, height, YCBCR_INPUT_SPLIT_Y_AND_CBCR); + input->set_pixel_data(0, y); + input->set_pixel_data(1, cb_cr); + tester.get_chain()->add_input(input); + + tester.run(out_data, GL_RGBA, COLORSPACE_sRGB, GAMMA_sRGB); + + // Y'CbCr isn't 100% accurate (the input values are rounded), + // so we need some leeway. + expect_equal(expected_data, out_data, 4 * width, height, 0.025, 0.002); +} + } // namespace movit