From fc146840d1ef347ac3eb36cd26430bfec0acb0e3 Mon Sep 17 00:00:00 2001 From: Niklas Andersson <3985238+niklaspandersson@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:28:33 +0200 Subject: [PATCH] feat: decklink support for chosing yuv output i 8-bit mode --- src/common/memory.h | 5 + src/modules/decklink/CMakeLists.txt | 2 +- src/modules/decklink/consumer/config.cpp | 19 ++++ src/modules/decklink/consumer/config.h | 7 ++ .../decklink/consumer/decklink_consumer.cpp | 11 +- .../decklink/consumer/format_strategy.h | 3 +- ..._v210_strategy.cpp => v210_strategies.cpp} | 107 ++++++++++++------ src/shell/casparcg.config | 1 + 8 files changed, 115 insertions(+), 40 deletions(-) rename src/modules/decklink/consumer/{hdr_v210_strategy.cpp => v210_strategies.cpp} (80%) diff --git a/src/common/memory.h b/src/common/memory.h index 0f5788c7fc..2eae4519d7 100644 --- a/src/common/memory.h +++ b/src/common/memory.h @@ -658,6 +658,11 @@ shared_ptr make_shared(P0&& p0) { return shared_ptr(std::make_shared(std::forward(p0))); } +template +shared_ptr make_shared(P0&& p0, P1&& p1) +{ + return shared_ptr(std::make_shared(std::forward(p0), std::forward(p1))); +} template shared_ptr make_shared(P0&& p0) diff --git a/src/modules/decklink/CMakeLists.txt b/src/modules/decklink/CMakeLists.txt index 972e6f0f0e..cb76d13566 100644 --- a/src/modules/decklink/CMakeLists.txt +++ b/src/modules/decklink/CMakeLists.txt @@ -4,7 +4,7 @@ project (decklink) set(SOURCES consumer/decklink_consumer.cpp consumer/decklink_consumer.h - consumer/hdr_v210_strategy.cpp + consumer/v210_strategies.cpp consumer/sdr_bgra_strategy.cpp consumer/format_strategy.h consumer/config.cpp diff --git a/src/modules/decklink/consumer/config.cpp b/src/modules/decklink/consumer/config.cpp index bc54f0f2f9..be1053ec63 100644 --- a/src/modules/decklink/consumer/config.cpp +++ b/src/modules/decklink/consumer/config.cpp @@ -97,6 +97,25 @@ configuration parse_xml_config(const boost::property_tree::wptree& ptree, } config.wait_for_reference_duration = ptree.get(L"wait-for-reference-duration", config.wait_for_reference_duration); + { + auto is_8bit = channel_info.depth == common::bit_depth::bit8; + auto default_pixel_format = is_8bit ? L"rgba" : L"yuv"; + auto pixel_format = ptree.get(L"pixel-format", default_pixel_format); + if (pixel_format == L"yuv") { + config.pixel_format = configuration::pixel_format_t::yuv; + } else if (pixel_format == L"rgba") { + config.pixel_format = configuration::pixel_format_t::rgba; + } else { + CASPAR_THROW_EXCEPTION(user_error() << msg_info(L"Invalid pixel format, must be rgba or yuv")); + } + + if (channel_info.depth != common::bit_depth::bit8 && + config.pixel_format == configuration::pixel_format_t::rgba) { + CASPAR_THROW_EXCEPTION(user_error() + << msg_info(L"The decklink consumer only supports rgba output on 8-bit channels")); + } + } + config.primary = parse_output_config(ptree, format_repository); if (config.primary.device_index == -1) config.primary.device_index = 1; diff --git a/src/modules/decklink/consumer/config.h b/src/modules/decklink/consumer/config.h index f7e1301c2c..52176ed45f 100644 --- a/src/modules/decklink/consumer/config.h +++ b/src/modules/decklink/consumer/config.h @@ -87,6 +87,12 @@ struct configuration disabled, }; + enum class pixel_format_t + { + rgba, + yuv, + }; + bool embedded_audio = false; keyer_t keyer = keyer_t::default_keyer; duplex_t duplex = duplex_t::default_duplex; @@ -95,6 +101,7 @@ struct configuration int wait_for_reference_duration = 10; // seconds int base_buffer_depth = 3; bool hdr = false; + pixel_format_t pixel_format = pixel_format_t::rgba; port_configuration primary; std::vector secondaries; diff --git a/src/modules/decklink/consumer/decklink_consumer.cpp b/src/modules/decklink/consumer/decklink_consumer.cpp index 25e6da32ec..cc95244666 100644 --- a/src/modules/decklink/consumer/decklink_consumer.cpp +++ b/src/modules/decklink/consumer/decklink_consumer.cpp @@ -197,7 +197,12 @@ core::video_format_desc get_decklink_format(const port_configuration& confi spl::shared_ptr create_format_strategy(const configuration& config) { - return config.hdr ? create_hdr_v210_strategy(config.color_space) : create_sdr_bgra_strategy(); + if (config.hdr) { + return create_hdr_v210_strategy(config.color_space); + } else { + return config.pixel_format == configuration::pixel_format_t::yuv ? create_sdr_v210_strategy(config.color_space) + : create_sdr_bgra_strategy(); + } } enum EOTF @@ -1006,9 +1011,7 @@ struct decklink_consumer final : public IDeckLinkVideoOutputCallback audio_scheduled_ += nb_samples; // TODO - what if there are too many/few samples in this frame? } - void schedule_next_video(std::shared_ptr image_data, - int nb_samples, - BMDTimeValue display_time) + void schedule_next_video(std::shared_ptr image_data, int nb_samples, BMDTimeValue display_time) { auto fmt = format_strategy_->get_pixel_format(); auto row_bytes = format_strategy_->get_row_bytes(decklink_format_desc_.width); diff --git a/src/modules/decklink/consumer/format_strategy.h b/src/modules/decklink/consumer/format_strategy.h index 1b86a2b1cf..3b3b2e9ec1 100644 --- a/src/modules/decklink/consumer/format_strategy.h +++ b/src/modules/decklink/consumer/format_strategy.h @@ -41,7 +41,7 @@ class format_strategy public: format_strategy& operator=(const format_strategy&) = delete; - virtual ~format_strategy() = default; + virtual ~format_strategy() = default; format_strategy(const format_strategy&) = delete; @@ -57,6 +57,7 @@ class format_strategy }; spl::shared_ptr create_sdr_bgra_strategy(); +spl::shared_ptr create_sdr_v210_strategy(core::color_space colorspace); spl::shared_ptr create_hdr_v210_strategy(core::color_space colorspace); }} // namespace caspar::decklink diff --git a/src/modules/decklink/consumer/hdr_v210_strategy.cpp b/src/modules/decklink/consumer/v210_strategies.cpp similarity index 80% rename from src/modules/decklink/consumer/hdr_v210_strategy.cpp rename to src/modules/decklink/consumer/v210_strategies.cpp index a51a5e2dcd..11715eb761 100644 --- a/src/modules/decklink/consumer/hdr_v210_strategy.cpp +++ b/src/modules/decklink/consumer/v210_strategies.cpp @@ -26,6 +26,8 @@ #include #endif +#include + #include "format_strategy.h" #include @@ -153,15 +155,17 @@ inline void pack_v210_avx2(__m256i luma[6], __m256i chroma[6], __m128i** v210_de } } +template struct ARGBPixel { - uint16_t R; - uint16_t G; - uint16_t B; - uint16_t A; + T R; + T G; + T B; + T A; }; -void pack_v210(const ARGBPixel* src, const std::vector& color_matrix, uint32_t* dest, int num_pixels) +template +void pack_v210(const ARGBPixel* src, const std::vector& color_matrix, uint32_t* dest, int num_pixels) { auto write_v210 = [dest, index = 0, shift = 0](uint32_t val) mutable { dest[index] |= ((val & 0x3FF) << shift); @@ -202,9 +206,9 @@ void pack_v210(const ARGBPixel* src, const std::vector& color_matrix, u } } -class hdr_v210_strategy +class v210_strategy : public format_strategy - , std::enable_shared_from_this + , std::enable_shared_from_this { std::vector bt709{0.212639005871510, 0.715168678767756, @@ -227,13 +231,15 @@ class hdr_v210_strategy std::vector color_matrix; __m128i black_batch; + uint8_t bpc; public: - explicit hdr_v210_strategy(core::color_space color_space) + explicit v210_strategy(core::color_space color_space, uint8_t bpc) : color_matrix(create_int_matrix(color_space == core::color_space::bt2020 ? bt2020 : bt709)) + , bpc(bpc) { // setup black batch (6 pixels of black, encoded as v210) - ARGBPixel black[6]; + ARGBPixel<> black[6]; memset(black, 0, sizeof(black)); memset(&black_batch, 0, sizeof(__m128i)); pack_v210(black, color_matrix, reinterpret_cast(&black_batch), 6); @@ -248,33 +254,47 @@ class hdr_v210_strategy auto size = get_row_bytes(format_desc.width) * format_desc.height; return create_aligned_buffer(size, 128); } - std::shared_ptr convert_frame_for_port(const core::video_format_desc& channel_format_desc, const core::video_format_desc& decklink_format_desc, const port_configuration& config, const core::const_frame& frame1, const core::const_frame& frame2, BMDFieldDominance field_dominance) override + { + return bpc == 1 ? do_convert_frame_for_port( + channel_format_desc, decklink_format_desc, config, frame1, frame2, field_dominance) + : do_convert_frame_for_port( + channel_format_desc, decklink_format_desc, config, frame1, frame2, field_dominance); + } + + private: + template + std::shared_ptr do_convert_frame_for_port(const core::video_format_desc& channel_format_desc, + const core::video_format_desc& decklink_format_desc, + const port_configuration& config, + const core::const_frame& frame1, + const core::const_frame& frame2, + BMDFieldDominance field_dominance) { std::shared_ptr image_data = allocate_frame_data(decklink_format_desc); if (field_dominance != bmdProgressiveFrame) { - convert_frame(channel_format_desc, - decklink_format_desc, - config, - image_data, - field_dominance == bmdUpperFieldFirst, - frame1); - - convert_frame(channel_format_desc, - decklink_format_desc, - config, - image_data, - field_dominance != bmdUpperFieldFirst, - frame2); + convert_frame(channel_format_desc, + decklink_format_desc, + config, + image_data, + field_dominance == bmdUpperFieldFirst, + frame1); + + convert_frame(channel_format_desc, + decklink_format_desc, + config, + image_data, + field_dominance != bmdUpperFieldFirst, + frame2); } else { - convert_frame(channel_format_desc, decklink_format_desc, config, image_data, true, frame1); + convert_frame(channel_format_desc, decklink_format_desc, config, image_data, true, frame1); } if (config.key_only) { @@ -284,7 +304,7 @@ class hdr_v210_strategy return image_data; } - private: + template void convert_frame(const core::video_format_desc& channel_format_desc, const core::video_format_desc& decklink_format_desc, const port_configuration& config, @@ -337,7 +357,7 @@ class hdr_v210_strategy continue; } - auto src = reinterpret_cast(frame.image_data(0).data()) + + auto src = reinterpret_cast*>(frame.image_data(0).data()) + (src_y * channel_format_desc.width + config.src_x); // Pack pixels in batches of 48 for (int batch_index_x = 0; batch_index_x < fullspeed_x_batches; batch_index_x++) { @@ -348,12 +368,26 @@ class hdr_v210_strategy __m256i zero = _mm256_setzero_si256(); for (int packet_index = 0; packet_index < 6; packet_index++) { - __m256i p0123 = _mm256_loadu_si256(pixeldata + packet_index * 2); - __m256i p4567 = _mm256_loadu_si256(pixeldata + packet_index * 2 + 1); - - // shift down to 10 bit precision - p0123 = _mm256_srli_epi16(p0123, 6); - p4567 = _mm256_srli_epi16(p4567, 6); + __m256i p0123, p4567; + if constexpr (std::is_same()) { + p0123 = _mm256_loadu_si256(pixeldata + packet_index * 2); + p4567 = _mm256_loadu_si256(pixeldata + packet_index * 2 + 1); + + // shift down to 10 bit precision + p0123 = _mm256_srli_epi16(p0123, 6); + p4567 = _mm256_srli_epi16(p4567, 6); + } else if constexpr (std::is_same()) { + auto p01234567 = _mm256_loadu_si256(pixeldata + packet_index); + auto p01452367 = _mm256_permute4x64_epi64(p01234567, 0b11011000); + + p0123 = _mm256_unpacklo_epi8(p01452367, zero); + p4567 = _mm256_unpackhi_epi8(p01452367, zero); + + p0123 = _mm256_slli_epi16(p0123, 2); + p4567 = _mm256_slli_epi16(p4567, 2); + } else { + static_assert(!std::is_same(), "Unsupported template type for v210 conversion"); + } // unpack 16 bit values to 32 bit registers, padding with zeros __m256i pixel_pairs[4]; @@ -388,7 +422,7 @@ class hdr_v210_strategy // // pad the rest with black pixels to fill the v210 packet int last_x_pixels = rest_x_pixels - rest_x_6pixels; if (last_x_pixels > 0) { - ARGBPixel pixels[6]; + ARGBPixel pixels[6]; memset(pixels, 0, sizeof(pixels)); memcpy(pixels, src, last_x_pixels * 8); pack_v210(pixels, color_matrix, reinterpret_cast(v210_dest), 6); @@ -410,9 +444,14 @@ class hdr_v210_strategy } }; +spl::shared_ptr create_sdr_v210_strategy(core::color_space color_space) +{ + return spl::make_shared(color_space, 1); +} + spl::shared_ptr create_hdr_v210_strategy(core::color_space color_space) { - return spl::make_shared(color_space); + return spl::make_shared(color_space, 2); } }} // namespace caspar::decklink diff --git a/src/shell/casparcg.config b/src/shell/casparcg.config index 902ae5fba7..efc2a2d2de 100644 --- a/src/shell/casparcg.config +++ b/src/shell/casparcg.config @@ -90,6 +90,7 @@ external [external|external_separate_device|internal|default] false [true|false] 3 [1..] + [yuv|rgba](default is rgba for 8bit channels, yuv for high bit-depth channels) (Run the decklink at a different video-mode. Note: the framerate must match that of the channel) 0 (x offset into the channel)