#include "resampling_queue.h"
#include "timebase.h"
#include "timecode_renderer.h"
+#include "v210_converter.h"
#include "video_encoder.h"
class IDeckLink;
}
}
-void ensure_texture_resolution(PBOFrameAllocator::Userdata *userdata, unsigned field, unsigned width, unsigned height)
+void ensure_texture_resolution(PBOFrameAllocator::Userdata *userdata, unsigned field, unsigned width, unsigned height, unsigned v210_width)
{
- if (userdata->tex_y[field] == 0 ||
- userdata->tex_cbcr[field] == 0 ||
+ bool first;
+ if (global_flags.ten_bit_input) {
+ first = userdata->tex_v210[field] == 0 || userdata->tex_444[field] == 0;
+ } else {
+ first = userdata->tex_y[field] == 0 || userdata->tex_cbcr[field] == 0;
+ }
+
+ if (first ||
width != userdata->last_width[field] ||
height != userdata->last_height[field]) {
- size_t cbcr_width = width / 2;
-
// We changed resolution since last use of this texture, so we need to create
// a new object. Note that this each card has its own PBOFrameAllocator,
// we don't need to worry about these flip-flopping between resolutions.
- glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
- check_error();
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
- check_error();
- glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
- check_error();
- glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
- check_error();
+ if (global_flags.ten_bit_input) {
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_444[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, width, height, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullptr);
+ check_error();
+ } else {
+ size_t cbcr_width = width / 2;
+
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+ check_error();
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+ check_error();
+ }
userdata->last_width[field] = width;
userdata->last_height[field] = height;
}
+ if (global_flags.ten_bit_input &&
+ (first || v210_width != userdata->last_v210_width[field])) {
+ // Same as above; we need to recreate the texture.
+ glBindTexture(GL_TEXTURE_2D, userdata->tex_v210[field]);
+ check_error();
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, v210_width, height, 0, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, nullptr);
+ check_error();
+ userdata->last_v210_width[field] = v210_width;
+ }
}
-void upload_texture(GLuint tex, GLuint width, GLuint height, GLuint stride, bool interlaced_stride, GLenum format, GLintptr offset)
+void upload_texture(GLuint tex, GLuint width, GLuint height, GLuint stride, bool interlaced_stride, GLenum format, GLenum type, GLintptr offset)
{
if (interlaced_stride) {
stride *= 2;
check_error();
}
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, format, GL_UNSIGNED_BYTE, BUFFER_OFFSET(offset));
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, format, type, BUFFER_OFFSET(offset));
check_error();
glBindTexture(GL_TEXTURE_2D, 0);
check_error();
been_at_safe_point_since_last_starvation = false;
return;
}
- if (queue_length >= 1) {
- if (queue_length >= safe_queue_length) {
- been_at_safe_point_since_last_starvation = true;
- }
- if (++frames_with_at_least_one >= 1000 && safe_queue_length > 1) {
- --safe_queue_length;
- fprintf(stderr, "Card %u: Spare frames for more than 1000 frames, reducing safe limit to %u frame(s)\n",
- card_index, safe_queue_length);
- frames_with_at_least_one = 0;
- }
- } else {
+ if (queue_length >= safe_queue_length) {
+ been_at_safe_point_since_last_starvation = true;
+ }
+ if (++frames_with_at_least_one >= 1000 && safe_queue_length > 1) {
+ --safe_queue_length;
+ fprintf(stderr, "Card %u: Spare frames for more than 1000 frames, reducing safe limit to %u frame(s)\n",
+ card_index, safe_queue_length);
frames_with_at_least_one = 0;
}
}
inout_format.color_space = COLORSPACE_sRGB;
inout_format.gamma_curve = GAMMA_sRGB;
- // Display chain; shows the live output produced by the main chain (its RGBA version).
+ // Matches the 4:2:0 format created by the main chain.
+ YCbCrFormat ycbcr_format;
+ ycbcr_format.chroma_subsampling_x = 2;
+ ycbcr_format.chroma_subsampling_y = 2;
+ if (global_flags.ycbcr_rec709_coefficients) {
+ ycbcr_format.luma_coefficients = YCBCR_REC_709;
+ } else {
+ ycbcr_format.luma_coefficients = YCBCR_REC_601;
+ }
+ ycbcr_format.full_range = false;
+ ycbcr_format.num_levels = 256;
+ ycbcr_format.cb_x_position = 0.0f;
+ ycbcr_format.cr_x_position = 0.0f;
+ ycbcr_format.cb_y_position = 0.5f;
+ ycbcr_format.cr_y_position = 0.5f;
+
+ // Display chain; shows the live output produced by the main chain (or rather, a copy of it).
display_chain.reset(new EffectChain(global_flags.width, global_flags.height, resource_pool.get()));
check_error();
- display_input = new FlatInput(inout_format, FORMAT_RGB, GL_UNSIGNED_BYTE, global_flags.width, global_flags.height); // FIXME: GL_UNSIGNED_BYTE is really wrong.
+ display_input = new YCbCrInput(inout_format, ycbcr_format, global_flags.width, global_flags.height, YCBCR_INPUT_SPLIT_Y_AND_CBCR);
display_chain->add_input(display_input);
display_chain->add_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED);
display_chain->set_dither_bits(0); // Don't bother.
chroma_subsampler.reset(new ChromaSubsampler(resource_pool.get()));
+ if (global_flags.ten_bit_input) {
+ if (!v210Converter::has_hardware_support()) {
+ fprintf(stderr, "ERROR: --ten-bit-input requires support for OpenGL compute shaders\n");
+ fprintf(stderr, " (OpenGL 4.3, or GL_ARB_compute_shader + GL_ARB_shader_image_load_store).\n");
+ exit(1);
+ }
+ v210_converter.reset(new v210Converter());
+
+ // These are all the widths listed in the Blackmagic SDK documentation
+ // (section 2.7.3, “Display Modes”).
+ v210_converter->precompile_shader(720);
+ v210_converter->precompile_shader(1280);
+ v210_converter->precompile_shader(1920);
+ v210_converter->precompile_shader(2048);
+ v210_converter->precompile_shader(3840);
+ v210_converter->precompile_shader(4096);
+ }
+
timecode_renderer.reset(new TimecodeRenderer(resource_pool.get(), global_flags.width, global_flags.height));
display_timecode_in_stream = global_flags.display_timecode_in_stream;
display_timecode_on_stdout = global_flags.display_timecode_on_stdout;
}
while (!card->new_frames.empty()) card->new_frames.pop_front();
card->last_timecode = -1;
+ card->capture->set_pixel_format(global_flags.ten_bit_input ? PixelFormat_10BitYCbCr : PixelFormat_8BitYCbCr);
card->capture->configure_card();
// NOTE: start_bm_capture() happens in thread_func().
card->last_timecode = timecode;
- size_t expected_length = video_format.width * (video_format.height + video_format.extra_lines_top + video_format.extra_lines_bottom) * 2;
+ size_t expected_length = video_format.stride * (video_format.height + video_format.extra_lines_top + video_format.extra_lines_bottom);
if (video_frame.len - video_offset == 0 ||
video_frame.len - video_offset != expected_length) {
if (video_frame.len != 0) {
RefCountedFrame frame(video_frame);
// Upload the textures.
- size_t cbcr_width = video_format.width / 2;
- size_t cbcr_offset = video_offset / 2;
- size_t y_offset = video_frame.size / 2 + video_offset / 2;
+ const size_t cbcr_width = video_format.width / 2;
+ const size_t cbcr_offset = video_offset / 2;
+ const size_t y_offset = video_frame.size / 2 + video_offset / 2;
for (unsigned field = 0; field < num_fields; ++field) {
// Put the actual texture upload in a lambda that is executed in the main thread.
// Note that this means we must hold on to the actual frame data in <userdata>
// until the upload command is run, but we hold on to <frame> much longer than that
// (in fact, all the way until we no longer use the texture in rendering).
- auto upload_func = [field, video_format, y_offset, cbcr_offset, cbcr_width, interlaced_stride, userdata]() {
+ auto upload_func = [this, field, video_format, y_offset, video_offset, cbcr_offset, cbcr_width, interlaced_stride, userdata]() {
unsigned field_start_line;
if (field == 1) {
field_start_line = video_format.second_field_start;
} else {
field_start_line = video_format.extra_lines_top;
}
- size_t field_y_start = y_offset + video_format.width * field_start_line;
- size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
- ensure_texture_resolution(userdata, field, video_format.width, video_format.height);
+ // For 8-bit input, v210_width will be nonsensical but not used.
+ size_t v210_width = video_format.stride / sizeof(uint32_t);
+ ensure_texture_resolution(userdata, field, video_format.width, video_format.height, v210_width);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, userdata->pbo);
check_error();
- upload_texture(userdata->tex_y[field], video_format.width, video_format.height, video_format.width, interlaced_stride, GL_RED, field_y_start);
- upload_texture(userdata->tex_cbcr[field], cbcr_width, video_format.height, cbcr_width * sizeof(uint16_t), interlaced_stride, GL_RG, field_cbcr_start);
+ if (global_flags.ten_bit_input) {
+ size_t field_start = video_offset + video_format.stride * field_start_line;
+ upload_texture(userdata->tex_v210[field], v210_width, video_format.height, video_format.stride, interlaced_stride, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, field_start);
+ v210_converter->convert(userdata->tex_v210[field], userdata->tex_444[field], video_format.width, video_format.height);
+ } else {
+ size_t field_y_start = y_offset + video_format.width * field_start_line;
+ size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
+
+ // Make up our own strides, since we are interleaving.
+ upload_texture(userdata->tex_y[field], video_format.width, video_format.height, video_format.width, interlaced_stride, GL_RED, GL_UNSIGNED_BYTE, field_y_start);
+ upload_texture(userdata->tex_cbcr[field], cbcr_width, video_format.height, cbcr_width * sizeof(uint16_t), interlaced_stride, GL_RG, GL_UNSIGNED_BYTE, field_cbcr_start);
+ }
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
check_error();
theme_main_chain.setup_chain();
//theme_main_chain.chain->enable_phase_timing(true);
+ // If HDMI/SDI output is active and the user has requested auto mode,
+ // its mode overrides the existing Y'CbCr setting for the chain.
+ YCbCrLumaCoefficients ycbcr_output_coefficients;
+ if (global_flags.ycbcr_auto_coefficients && output_card_index != -1) {
+ ycbcr_output_coefficients = cards[output_card_index].output->preferred_ycbcr_coefficients();
+ } else {
+ ycbcr_output_coefficients = global_flags.ycbcr_rec709_coefficients ? YCBCR_REC_709 : YCBCR_REC_601;
+ }
+
+ // TODO: Reduce the duplication against theme.cpp.
+ YCbCrFormat output_ycbcr_format;
+ output_ycbcr_format.chroma_subsampling_x = 1;
+ output_ycbcr_format.chroma_subsampling_y = 1;
+ output_ycbcr_format.luma_coefficients = ycbcr_output_coefficients;
+ output_ycbcr_format.full_range = false;
+ output_ycbcr_format.num_levels = 256;
+ chain->change_ycbcr_output_format(output_ycbcr_format);
+
+ const int64_t av_delay = lrint(global_flags.audio_queue_length_ms * 0.001 * TIMEBASE); // Corresponds to the delay in ResamplingQueue.
GLuint y_tex, cbcr_tex;
- bool got_frame = video_encoder->begin_frame(&y_tex, &cbcr_tex);
+ bool got_frame = video_encoder->begin_frame(pts_int + av_delay, duration, ycbcr_output_coefficients, theme_main_chain.input_frames, &y_tex, &cbcr_tex);
assert(got_frame);
- // Render main chain.
+ // Render main chain. We take an extra copy of the created outputs,
+ // so that we can display it back to the screen later (it's less memory
+ // bandwidth than writing and reading back an RGBA texture, even at 16-bit).
+ // Ideally, we'd like to avoid taking copies and just use the main textures
+ // for display as well, but if they're used for zero-copy Quick Sync encoding
+ // (the default case), they're just views into VA-API memory and must be
+ // unmapped during encoding, so we can't use them for display, unfortunately.
GLuint cbcr_full_tex = resource_pool->create_2d_texture(GL_RG8, global_flags.width, global_flags.height);
- GLuint rgba_tex = resource_pool->create_2d_texture(GL_RGB565, global_flags.width, global_flags.height); // Saves texture bandwidth, although dithering gets messed up.
- GLuint fbo = resource_pool->create_fbo(y_tex, cbcr_full_tex, rgba_tex);
+ GLuint y_copy_tex = resource_pool->create_2d_texture(GL_R8, global_flags.width, global_flags.height);
+ GLuint fbo = resource_pool->create_fbo(y_tex, cbcr_full_tex, y_copy_tex);
check_error();
chain->render_to_fbo(fbo, global_flags.width, global_flags.height);
resource_pool->release_fbo(fbo);
- chroma_subsampler->subsample_chroma(cbcr_full_tex, global_flags.width, global_flags.height, cbcr_tex);
+ GLuint cbcr_copy_tex = resource_pool->create_2d_texture(GL_RG8, global_flags.width / 2, global_flags.height / 2);
+ chroma_subsampler->subsample_chroma(cbcr_full_tex, global_flags.width, global_flags.height, cbcr_tex, cbcr_copy_tex);
if (output_card_index != -1) {
- cards[output_card_index].output->send_frame(y_tex, cbcr_full_tex, theme_main_chain.input_frames, pts_int, duration);
+ cards[output_card_index].output->send_frame(y_tex, cbcr_full_tex, ycbcr_output_coefficients, theme_main_chain.input_frames, pts_int, duration);
}
resource_pool->release_2d_texture(cbcr_full_tex);
- // Set the right state for rgba_tex.
+ // Set the right state for the Y' and CbCr copies.
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- glBindTexture(GL_TEXTURE_2D, rgba_tex);
+ glBindTexture(GL_TEXTURE_2D, y_copy_tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- const int64_t av_delay = lrint(global_flags.audio_queue_length_ms * 0.001 * TIMEBASE); // Corresponds to the delay in ResamplingQueue.
- RefCountedGLsync fence = video_encoder->end_frame(pts_int + av_delay, duration, theme_main_chain.input_frames);
+ glBindTexture(GL_TEXTURE_2D, cbcr_copy_tex);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ RefCountedGLsync fence = video_encoder->end_frame();
- // The live frame just shows the RGBA texture we just rendered.
- // It owns rgba_tex now.
+ // The live frame pieces the Y'CbCr texture copies back into RGB and displays them.
+ // It owns y_copy_tex and cbcr_copy_tex now.
DisplayFrame live_frame;
live_frame.chain = display_chain.get();
- live_frame.setup_chain = [this, rgba_tex]{
- display_input->set_texture_num(rgba_tex);
+ live_frame.setup_chain = [this, y_copy_tex, cbcr_copy_tex]{
+ display_input->set_texture_num(0, y_copy_tex);
+ display_input->set_texture_num(1, cbcr_copy_tex);
};
live_frame.ready_fence = fence;
live_frame.input_frames = {};
- live_frame.temp_textures = { rgba_tex };
+ live_frame.temp_textures = { y_copy_tex, cbcr_copy_tex };
output_channel[OUTPUT_LIVE].output_frame(live_frame);
// Set up preview and any additional channels.