From 1f45a72638138f2e0645d4c3847dd6fa5c439cfb Mon Sep 17 00:00:00 2001 From: Johanness Date: Tue, 19 May 2026 14:56:13 +0300 Subject: [PATCH] Add synthetic v210 feed source --- AppConfig.cpp | 12 +- AppConfig.hpp | 1 + CMakeLists.txt | 1 + FeedCreate.cpp | 13 +++ SyntheticV210Feed.cpp | 256 ++++++++++++++++++++++++++++++++++++++++++ SyntheticV210Feed.hpp | 55 +++++++++ 6 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 SyntheticV210Feed.cpp create mode 100644 SyntheticV210Feed.hpp diff --git a/AppConfig.cpp b/AppConfig.cpp index a4d1df7..ef61f26 100644 --- a/AppConfig.cpp +++ b/AppConfig.cpp @@ -686,6 +686,13 @@ std::optional parseFeedKind(std::string_view text) return FeedKind::SmpteBars; } + if (text == "v210" || + text == "synthetic-v210" || + text == "v210test") + { + return FeedKind::SyntheticV210; + } + if (text == "mxlsdk") { return FeedKind::MxlSdk; @@ -724,6 +731,9 @@ std::string_view feedKindName(FeedKind kind) case FeedKind::SmpteBars: return "smpte"; + case FeedKind::SyntheticV210: + return "v210"; + case FeedKind::MxlSdk: return "mxlsdk"; @@ -754,7 +764,7 @@ void printUsage(const char* executableName) << " --flow Set MXL flow UUID for feed slot\n" << " --version Print version\n" << " --help Print this help\n" - << "Feed kinds: nosignal, none, fake, mxl, smpte, mxlsdk\n" + << "Feed kinds: nosignal, none, fake, mxl, smpte, v210, mxlsdk\n" << "Default: 2x2 grid, all slots nosignal" << std::endl; } diff --git a/AppConfig.hpp b/AppConfig.hpp index 2ee72db..05fc600 100644 --- a/AppConfig.hpp +++ b/AppConfig.hpp @@ -19,6 +19,7 @@ enum class FeedKind NoSignal, MxlPlaceholder, SmpteBars, + SyntheticV210, MxlSdk }; diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cb2f3d..633e9e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(mxl_multiviewer MxlInstanceCache.cpp MxlSdkFeed.cpp SmpteBarsFeed.cpp + SyntheticV210Feed.cpp TileLayout.cpp VulkanContext.cpp VulkanUtils.cpp diff --git a/FeedCreate.cpp b/FeedCreate.cpp index 5adb7c7..25f5f77 100644 --- a/FeedCreate.cpp +++ b/FeedCreate.cpp @@ -4,6 +4,7 @@ #include "MxlFeed.hpp" #include "MxlSdkFeed.hpp" #include "SmpteBarsFeed.hpp" +#include "SyntheticV210Feed.hpp" #include @@ -74,6 +75,18 @@ std::unique_ptr createFeed( frameHeight ); + case FeedKind::SyntheticV210: + if (verbose) + { + std::cerr << "Feed " << (feedIndex + 1) + << ": synthetic v210" << std::endl; + } + return std::make_unique( + feedIndex, + frameWidth, + frameHeight + ); + case FeedKind::NoSignal: default: if (verbose) diff --git a/SyntheticV210Feed.cpp b/SyntheticV210Feed.cpp new file mode 100644 index 0000000..187b3d5 --- /dev/null +++ b/SyntheticV210Feed.cpp @@ -0,0 +1,256 @@ +#include "SyntheticV210Feed.hpp" +#include "PixelFormat.hpp" + +#include +#include +#include +#include + +namespace +{ +constexpr uint32_t SyntheticWidth = 1920; +constexpr uint32_t SyntheticHeight = 1080; +constexpr uint32_t SyntheticPatternCount = 4; + +uint32_t clamp10(int32_t value) +{ + return static_cast( + value < 0 ? 0 : (value > 1023 ? 1023 : value)); +} + +uint32_t packWord(uint32_t a, uint32_t b, uint32_t c) +{ + return (a & 0x3FFu) | + ((b & 0x3FFu) << 10) | + ((c & 0x3FFu) << 20); +} + +void storeWord(uint8_t* dst, uint32_t word) +{ + dst[0] = static_cast(word & 0xFFu); + dst[1] = static_cast((word >> 8) & 0xFFu); + dst[2] = static_cast((word >> 16) & 0xFFu); + dst[3] = static_cast((word >> 24) & 0xFFu); +} + +double syntheticRateForFeed(uint32_t feedIndex) +{ + static constexpr std::array Rates = + { + 25.0, 25.0, 25.0, + 50.0, 50.0, 60.0, + 25.0, 50.0, 60.0 + }; + + return Rates[feedIndex % Rates.size()]; +} +} + +SyntheticV210Feed::SyntheticV210Feed( + uint32_t feedIndex, + uint32_t frameWidth, + uint32_t frameHeight) + : mFeedIndex(feedIndex) +{ + mSourceWidth = SyntheticWidth; + mSourceHeight = SyntheticHeight; + mSourceStride = v210LineStride(mSourceWidth); + mFrameRate = syntheticRateForFeed(feedIndex); + + mFrame.width = frameWidth; + mFrame.height = frameHeight; + mFrame.pixels.resize( + static_cast(frameWidth) * frameHeight, + packRgba8({24, 24, 24})); + + generatePayloads(); +} + +const VideoFrame& SyntheticV210Feed::getFrame(uint32_t frameCounter) +{ + const uint64_t index = syntheticIndex(frameCounter); + if (index != mGrainIndex) + { + mGrainIndex = index; + ++mFrameVersion; + } + + return mFrame; +} + +FeedRuntimeStatus SyntheticV210Feed::status() const +{ + return FeedRuntimeStatus::Live; +} + +std::string SyntheticV210Feed::displayName() const +{ + return "SYNTH V210"; +} + +std::string SyntheticV210Feed::sourceInfo() const +{ + std::ostringstream out; + out << "synthetic v210 " + << mSourceWidth << "x" << mSourceHeight + << "@" << static_cast(mFrameRate); + return out.str(); +} + +uint64_t SyntheticV210Feed::frameVersion() const +{ + return mFrameVersion; +} + +bool SyntheticV210Feed::hasGrainIndex() const +{ + return true; +} + +uint64_t SyntheticV210Feed::grainIndex() const +{ + return mGrainIndex; +} + +bool SyntheticV210Feed::hasFrameRate() const +{ + return true; +} + +double SyntheticV210Feed::frameRate() const +{ + return mFrameRate; +} + +float SyntheticV210Feed::srcAspectRatio() const +{ + return static_cast(mSourceWidth) / + static_cast(mSourceHeight); +} + +bool SyntheticV210Feed::hasV210() const +{ + return !mPayloads.empty(); +} + +const uint8_t* SyntheticV210Feed::v210Data() const +{ + if (mPayloads.empty()) + { + return nullptr; + } + + return mPayloads[mGrainIndex % mPayloads.size()].data(); +} + +bool SyntheticV210Feed::supportsDirectV210Read() const +{ + return true; +} + +bool SyntheticV210Feed::readV210FrameInto( + uint32_t frameCounter, + void* destination, + size_t destinationSize) +{ + if (destination == nullptr || mPayloads.empty()) + { + return false; + } + + const uint64_t index = syntheticIndex(frameCounter); + if (index == mGrainIndex) + { + return false; + } + + const std::vector& payload = + mPayloads[index % mPayloads.size()]; + if (destinationSize < payload.size()) + { + return false; + } + + std::memcpy(destination, payload.data(), payload.size()); + mGrainIndex = index; + ++mFrameVersion; + return true; +} + +uint32_t SyntheticV210Feed::v210Width() const +{ + return mSourceWidth; +} + +uint32_t SyntheticV210Feed::v210Height() const +{ + return mSourceHeight; +} + +uint32_t SyntheticV210Feed::v210Stride() const +{ + return mSourceStride; +} + +uint64_t SyntheticV210Feed::syntheticIndex( + uint32_t frameCounter) const +{ + return static_cast( + (static_cast(frameCounter) * mFrameRate) / 60.0); +} + +void SyntheticV210Feed::generatePayloads() +{ + const size_t payloadSize = + static_cast(mSourceStride) * mSourceHeight; + mPayloads.resize(SyntheticPatternCount); + + for (uint32_t pattern = 0; + pattern < SyntheticPatternCount; + ++pattern) + { + std::vector& payload = mPayloads[pattern]; + payload.assign(payloadSize, 0); + + for (uint32_t y = 0; y < mSourceHeight; ++y) + { + encodeLine( + y, + pattern, + payload.data() + + static_cast(y) * mSourceStride); + } + } +} + +void SyntheticV210Feed::encodeLine( + uint32_t y, + uint32_t patternIndex, + uint8_t* line) const +{ + const uint32_t groups = (mSourceWidth + 5) / 6; + const uint32_t bandHeight = std::max(1u, mSourceHeight / 8u); + const uint32_t yBand = (y / bandHeight + patternIndex) % 8u; + + for (uint32_t group = 0; group < groups; ++group) + { + const uint32_t x = group * 6u; + const uint32_t xBand = + ((x * 8u) / mSourceWidth + mFeedIndex + patternIndex) % + 8u; + const uint32_t luma = + clamp10(96 + static_cast(xBand) * 96); + const uint32_t altLuma = + clamp10(96 + static_cast(yBand) * 96); + const uint32_t cb = + clamp10(512 + (static_cast(xBand) - 3) * 56); + const uint32_t cr = + clamp10(512 + (static_cast(yBand) - 3) * 56); + + uint8_t* dst = line + group * 16u; + storeWord(dst + 0, packWord(cb, luma, cr)); + storeWord(dst + 4, packWord(altLuma, cb, luma)); + storeWord(dst + 8, packWord(cr, altLuma, cb)); + storeWord(dst + 12, packWord(luma, cr, altLuma)); + } +} diff --git a/SyntheticV210Feed.hpp b/SyntheticV210Feed.hpp new file mode 100644 index 0000000..f2b7c83 --- /dev/null +++ b/SyntheticV210Feed.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "IVideoFeed.hpp" + +#include +#include +#include + +class SyntheticV210Feed : public IVideoFeed +{ +public: + SyntheticV210Feed( + uint32_t feedIndex, + uint32_t frameWidth, + uint32_t frameHeight); + + const VideoFrame& getFrame(uint32_t frameCounter) override; + FeedRuntimeStatus status() const override; + std::string displayName() const override; + std::string sourceInfo() const override; + uint64_t frameVersion() const override; + bool hasGrainIndex() const override; + uint64_t grainIndex() const override; + bool hasFrameRate() const override; + double frameRate() const override; + float srcAspectRatio() const override; + bool hasV210() const override; + const uint8_t* v210Data() const override; + bool supportsDirectV210Read() const override; + bool readV210FrameInto( + uint32_t frameCounter, + void* destination, + size_t destinationSize) override; + uint32_t v210Width() const override; + uint32_t v210Height() const override; + uint32_t v210Stride() const override; + +private: + uint64_t syntheticIndex(uint32_t frameCounter) const; + void generatePayloads(); + void encodeLine( + uint32_t y, + uint32_t patternIndex, + uint8_t* line) const; + + uint32_t mFeedIndex = 0; + uint32_t mSourceWidth = 1920; + uint32_t mSourceHeight = 1080; + uint32_t mSourceStride = 0; + double mFrameRate = 60.0; + uint64_t mFrameVersion = 0; + uint64_t mGrainIndex = 0; + std::vector> mPayloads; + VideoFrame mFrame; +};