diff --git a/CMakeLists.txt b/CMakeLists.txt index 185643e..96a9fd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,14 @@ set(SHADER_MANIFEST_SOURCES "${SRC_DIR}/shader/ShaderPackageRegistry.cpp" ) +set(VIDEO_MODE_SOURCES + "${SRC_DIR}/video/core/VideoMode.cpp" +) + +set(VIDEO_FORMAT_SOURCES + "${SRC_DIR}/video/core/VideoIOFormat.cpp" +) + set(SLANG_RUNTIME_FILES "${SLANG_ROOT}/bin/slangc.exe" "${SLANG_ROOT}/bin/slang-compiler.dll" diff --git a/README.md b/README.md index 56a8e74..f51f1c8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Native app internals are grouped by boundary: - `render/`: render thread, readback, runtime render scene, and shared-context shader program preparation. - `runtime/`: shader catalog support, layer model, Slang build bridge, font atlas build, and runtime-state persistence. - `shader/`: shader package parsing and Slang compilation helpers. -- `video/core/`: backend-neutral video IO handoff contracts, formats, and output scheduling thread. +- `video/core/`: backend-neutral video IO handoff contracts, mode descriptions, pixel formats, and output scheduling thread. - `video/decklink/`: current DeckLink input/output backend. - `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers. - `video/legacy/`: older backend pipeline pieces kept separate while the new edge model settles. @@ -164,7 +164,7 @@ Current native test coverage includes: } ``` -`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`. +`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode through a backend-neutral mode description; the current DeckLink backend maps that mode to a `BMDDisplayMode` at the DeckLink boundary. Supported modes still depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`. Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present. diff --git a/docs/CURRENT_SYSTEM_ARCHITECTURE.md b/docs/CURRENT_SYSTEM_ARCHITECTURE.md index 1b7e220..beef382 100644 --- a/docs/CURRENT_SYSTEM_ARCHITECTURE.md +++ b/docs/CURRENT_SYSTEM_ARCHITECTURE.md @@ -25,7 +25,7 @@ Primary source areas: - `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox - `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker - `src/frames`: system-memory frame exchange -- `src/video/core`: generic video IO edge contracts, formats, and output scheduling thread +- `src/video/core`: generic video IO edge contracts, mode descriptions, formats, and output scheduling thread - `src/video/decklink`: current DeckLink input/output backend - `src/video/playout`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers - `src/video/legacy`: older backend pipeline pieces kept separate from the current app path @@ -133,7 +133,7 @@ When a runtime shader build completes, the app publishes a render-layer artifact ## Video And Preview -Video input and output are optional edges. DeckLink is the current concrete backend. +Video input and output are optional edges. DeckLink is the current concrete backend. Configured video modes are represented in `src/video/core` and translated to DeckLink display modes only inside `src/video/decklink`. The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code. diff --git a/src/RenderCadenceCompositor.cpp b/src/RenderCadenceCompositor.cpp index dd2384a..ba06335 100644 --- a/src/RenderCadenceCompositor.cpp +++ b/src/RenderCadenceCompositor.cpp @@ -7,8 +7,9 @@ #include "render/thread/RenderThread.h" #include "video/decklink/DeckLinkInput.h" #include "video/decklink/DeckLinkInputThread.h" -#include "DeckLinkDisplayMode.h" -#include "VideoIOFormat.h" +#include "video/decklink/DeckLinkDisplayMode.h" +#include "video/core/VideoIOFormat.h" +#include "video/core/VideoMode.h" #include @@ -93,7 +94,7 @@ int main(int argc, char** argv) } SystemFrameExchangeConfig frameExchangeConfig; - RenderCadenceCompositor::VideoFormatDimensions( + VideoFormatDimensions( appConfig.outputVideoFormat, frameExchangeConfig.width, frameExchangeConfig.height); @@ -108,7 +109,7 @@ int main(int argc, char** argv) SystemFrameExchange frameExchange(frameExchangeConfig); InputFrameMailboxConfig inputMailboxConfig; - RenderCadenceCompositor::VideoFormatDimensions( + VideoFormatDimensions( appConfig.inputVideoFormat, inputMailboxConfig.width, inputMailboxConfig.height); @@ -194,9 +195,9 @@ int main(int argc, char** argv) RenderThread::Config renderConfig; renderConfig.width = frameExchangeConfig.width; renderConfig.height = frameExchangeConfig.height; - const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate); + const double fallbackFrameDurationMilliseconds = FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate); renderConfig.frameDurationMilliseconds = outputVideoModeResolved - ? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds) + ? outputVideoMode.frameDurationMilliseconds : fallbackFrameDurationMilliseconds; renderConfig.pboDepth = kReadbackDepth; diff --git a/src/app/AppConfigProvider.cpp b/src/app/AppConfigProvider.cpp index 39cdb52..301b4a9 100644 --- a/src/app/AppConfigProvider.cpp +++ b/src/app/AppConfigProvider.cpp @@ -2,8 +2,6 @@ #include "RuntimeJson.h" -#include -#include #include #include #include @@ -166,92 +164,6 @@ void AppConfigProvider::ApplyCommandLine(int argc, char** argv) } } -double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate) -{ - double rate = fallbackRate; - try - { - rate = std::stod(rateText); - } - catch (...) - { - rate = fallbackRate; - } - - if (rate <= 0.0) - rate = fallbackRate; - return 1000.0 / rate; -} - -double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds) -{ - struct ModeRate - { - BMDDisplayMode mode; - int64_t frameDuration; - int64_t timeScale; - }; - - static const ModeRate rates[] = - { - { bmdModeHD720p50, 1, 50 }, - { bmdModeHD720p5994, 1001, 60000 }, - { bmdModeHD720p60, 1, 60 }, - { bmdModeHD1080i50, 1, 25 }, - { bmdModeHD1080i5994, 1001, 30000 }, - { bmdModeHD1080i6000, 1, 30 }, - { bmdModeHD1080p2398, 1001, 24000 }, - { bmdModeHD1080p24, 1, 24 }, - { bmdModeHD1080p25, 1, 25 }, - { bmdModeHD1080p2997, 1001, 30000 }, - { bmdModeHD1080p30, 1, 30 }, - { bmdModeHD1080p50, 1, 50 }, - { bmdModeHD1080p5994, 1001, 60000 }, - { bmdModeHD1080p6000, 1, 60 }, - { bmdMode4K2160p2398, 1001, 24000 }, - { bmdMode4K2160p24, 1, 24 }, - { bmdMode4K2160p25, 1, 25 }, - { bmdMode4K2160p2997, 1001, 30000 }, - { bmdMode4K2160p30, 1, 30 }, - { bmdMode4K2160p50, 1, 50 }, - { bmdMode4K2160p5994, 1001, 60000 }, - { bmdMode4K2160p60, 1, 60 } - }; - - for (const ModeRate& rate : rates) - { - if (rate.mode == displayMode && rate.timeScale > 0) - return (static_cast(rate.frameDuration) * 1000.0) / static_cast(rate.timeScale); - } - - return fallbackMilliseconds; -} - -void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height) -{ - std::string normalized = formatName; - std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) { - return static_cast(std::tolower(character)); - }); - - if (normalized == "720p") - { - width = 1280; - height = 720; - return; - } - - if (normalized == "2160p" || normalized == "4k" || normalized == "uhd") - { - width = 3840; - height = 2160; - return; - } - - width = 1920; - height = 1080; -} - std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath) { return FindRepoPath(relativePath); diff --git a/src/app/AppConfigProvider.h b/src/app/AppConfigProvider.h index 4992311..27951fa 100644 --- a/src/app/AppConfigProvider.h +++ b/src/app/AppConfigProvider.h @@ -1,7 +1,7 @@ #pragma once #include "AppConfig.h" -#include "DeckLinkDisplayMode.h" +#include "VideoMode.h" #include #include @@ -27,9 +27,6 @@ private: bool mLoadedFromFile = false; }; -double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94); -double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds); -void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height); std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json"); std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath); } diff --git a/src/video/core/VideoIOTypes.h b/src/video/core/VideoIOTypes.h index 77217bc..1b31ee1 100644 --- a/src/video/core/VideoIOTypes.h +++ b/src/video/core/VideoIOTypes.h @@ -1,7 +1,7 @@ #pragma once -#include "DeckLinkDisplayMode.h" #include "VideoIOFormat.h" +#include "VideoMode.h" #include #include diff --git a/src/video/core/VideoMode.cpp b/src/video/core/VideoMode.cpp new file mode 100644 index 0000000..394ec7f --- /dev/null +++ b/src/video/core/VideoMode.cpp @@ -0,0 +1,160 @@ +#include "VideoMode.h" + +#include +#include + +std::string NormalizeModeToken(const std::string& value) +{ + std::string normalized; + for (unsigned char ch : value) + { + if (std::isalnum(ch)) + normalized.push_back(static_cast(std::tolower(ch))); + } + return normalized; +} + +double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate) +{ + double rate = fallbackRate; + try + { + rate = std::stod(rateText); + } + catch (...) + { + rate = fallbackRate; + } + + if (rate <= 0.0) + rate = fallbackRate; + return 1000.0 / rate; +} + +void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height) +{ + std::string normalized = formatName; + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + + if (normalized == "720p") + { + width = 1280; + height = 720; + return; + } + + if (normalized == "2160p" || normalized == "4k" || normalized == "uhd") + { + width = 3840; + height = 2160; + return; + } + + width = 1920; + height = 1080; +} + +bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode) +{ + const std::string formatToken = NormalizeModeToken(videoFormat); + const std::string frameToken = NormalizeModeToken(frameRate); + const std::string combinedToken = formatToken + frameToken; + + struct ModeOption + { + const char* token; + const char* formatName; + const char* frameRate; + const char* displayName; + }; + + static const ModeOption options[] = + { + { "720p50", "720p", "50", "720p50" }, + { "hd720p50", "720p", "50", "720p50" }, + { "720p5994", "720p", "59.94", "720p59.94" }, + { "hd720p5994", "720p", "59.94", "720p59.94" }, + { "720p60", "720p", "60", "720p60" }, + { "hd720p60", "720p", "60", "720p60" }, + { "1080i50", "1080i", "50", "1080i50" }, + { "hd1080i50", "1080i", "50", "1080i50" }, + { "1080i5994", "1080i", "59.94", "1080i59.94" }, + { "hd1080i5994", "1080i", "59.94", "1080i59.94" }, + { "1080i60", "1080i", "60", "1080i60" }, + { "hd1080i60", "1080i", "60", "1080i60" }, + { "1080p2398", "1080p", "23.98", "1080p23.98" }, + { "hd1080p2398", "1080p", "23.98", "1080p23.98" }, + { "1080p24", "1080p", "24", "1080p24" }, + { "hd1080p24", "1080p", "24", "1080p24" }, + { "1080p25", "1080p", "25", "1080p25" }, + { "hd1080p25", "1080p", "25", "1080p25" }, + { "1080p2997", "1080p", "29.97", "1080p29.97" }, + { "hd1080p2997", "1080p", "29.97", "1080p29.97" }, + { "1080p30", "1080p", "30", "1080p30" }, + { "hd1080p30", "1080p", "30", "1080p30" }, + { "1080p50", "1080p", "50", "1080p50" }, + { "hd1080p50", "1080p", "50", "1080p50" }, + { "1080p5994", "1080p", "59.94", "1080p59.94" }, + { "hd1080p5994", "1080p", "59.94", "1080p59.94" }, + { "1080p60", "1080p", "60", "1080p60" }, + { "hd1080p60", "1080p", "60", "1080p60" }, + { "2160p2398", "2160p", "23.98", "2160p23.98" }, + { "4k2160p2398", "2160p", "23.98", "2160p23.98" }, + { "2160p24", "2160p", "24", "2160p24" }, + { "4k2160p24", "2160p", "24", "2160p24" }, + { "2160p25", "2160p", "25", "2160p25" }, + { "4k2160p25", "2160p", "25", "2160p25" }, + { "2160p2997", "2160p", "29.97", "2160p29.97" }, + { "4k2160p2997", "2160p", "29.97", "2160p29.97" }, + { "2160p30", "2160p", "30", "2160p30" }, + { "4k2160p30", "2160p", "30", "2160p30" }, + { "2160p50", "2160p", "50", "2160p50" }, + { "4k2160p50", "2160p", "50", "2160p50" }, + { "2160p5994", "2160p", "59.94", "2160p59.94" }, + { "4k2160p5994", "2160p", "59.94", "2160p59.94" }, + { "2160p60", "2160p", "60", "2160p60" }, + { "4k2160p60", "2160p", "60", "2160p60" } + }; + + for (const ModeOption& option : options) + { + if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token)) + { + videoMode.formatName = option.formatName; + videoMode.frameRate = option.frameRate; + videoMode.displayName = option.displayName; + VideoFormatDimensions(videoMode.formatName, videoMode.frameSize.width, videoMode.frameSize.height); + videoMode.frameDurationMilliseconds = FrameDurationMillisecondsFromRateString(videoMode.frameRate); + return true; + } + } + + return false; +} + +bool ResolveConfiguredVideoFormats( + const std::string& inputVideoFormat, + const std::string& inputFrameRate, + const std::string& outputVideoFormat, + const std::string& outputFrameRate, + VideoFormatSelection& videoModes, + std::string& error) +{ + if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input)) + { + error = "Unsupported inputVideoFormat/inputFrameRate in config/runtime-host.json: " + + inputVideoFormat + " / " + inputFrameRate; + return false; + } + + if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output)) + { + error = "Unsupported outputVideoFormat/outputFrameRate in config/runtime-host.json: " + + outputVideoFormat + " / " + outputFrameRate; + return false; + } + + return true; +} diff --git a/src/video/core/VideoMode.h b/src/video/core/VideoMode.h new file mode 100644 index 0000000..9963813 --- /dev/null +++ b/src/video/core/VideoMode.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +struct FrameSize +{ + unsigned width = 0; + unsigned height = 0; + + bool IsEmpty() const { return width == 0 || height == 0; } +}; + +inline bool operator==(const FrameSize& left, const FrameSize& right) +{ + return left.width == right.width && left.height == right.height; +} + +inline bool operator!=(const FrameSize& left, const FrameSize& right) +{ + return !(left == right); +} + +struct VideoFormat +{ + std::string formatName = "1080p"; + std::string frameRate = "59.94"; + std::string displayName = "1080p59.94"; + FrameSize frameSize = { 1920, 1080 }; + double frameDurationMilliseconds = 1000.0 / 59.94; +}; + +struct VideoFormatSelection +{ + VideoFormat input; + VideoFormat output; +}; + +std::string NormalizeModeToken(const std::string& value); +double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94); +void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height); +bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode); +bool ResolveConfiguredVideoFormats( + const std::string& inputVideoFormat, + const std::string& inputFrameRate, + const std::string& outputVideoFormat, + const std::string& outputFrameRate, + VideoFormatSelection& videoModes, + std::string& error); diff --git a/src/video/decklink/DeckLinkDisplayMode.cpp b/src/video/decklink/DeckLinkDisplayMode.cpp index 85bf5ea..0fabd4f 100644 --- a/src/video/decklink/DeckLinkDisplayMode.cpp +++ b/src/video/decklink/DeckLinkDisplayMode.cpp @@ -1,16 +1,50 @@ #include "DeckLinkDisplayMode.h" -#include - -std::string NormalizeModeToken(const std::string& value) +bool DeckLinkDisplayModeForVideoFormat(const VideoFormat& videoMode, BMDDisplayMode& displayMode) { - std::string normalized; - for (unsigned char ch : value) + struct ModeOption { - if (std::isalnum(ch)) - normalized.push_back(static_cast(std::tolower(ch))); + const char* displayName; + BMDDisplayMode mode; + }; + + static const ModeOption options[] = + { + { "720p50", bmdModeHD720p50 }, + { "720p59.94", bmdModeHD720p5994 }, + { "720p60", bmdModeHD720p60 }, + { "1080i50", bmdModeHD1080i50 }, + { "1080i59.94", bmdModeHD1080i5994 }, + { "1080i60", bmdModeHD1080i6000 }, + { "1080p23.98", bmdModeHD1080p2398 }, + { "1080p24", bmdModeHD1080p24 }, + { "1080p25", bmdModeHD1080p25 }, + { "1080p29.97", bmdModeHD1080p2997 }, + { "1080p30", bmdModeHD1080p30 }, + { "1080p50", bmdModeHD1080p50 }, + { "1080p59.94", bmdModeHD1080p5994 }, + { "1080p60", bmdModeHD1080p6000 }, + { "2160p23.98", bmdMode4K2160p2398 }, + { "2160p24", bmdMode4K2160p24 }, + { "2160p25", bmdMode4K2160p25 }, + { "2160p29.97", bmdMode4K2160p2997 }, + { "2160p30", bmdMode4K2160p30 }, + { "2160p50", bmdMode4K2160p50 }, + { "2160p59.94", bmdMode4K2160p5994 }, + { "2160p60", bmdMode4K2160p60 } + }; + + const std::string displayToken = NormalizeModeToken(videoMode.displayName); + for (const ModeOption& option : options) + { + if (displayToken == NormalizeModeToken(option.displayName)) + { + displayMode = option.mode; + return true; + } } - return normalized; + + return false; } bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName) @@ -19,108 +53,55 @@ bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::str if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode)) return false; - displayMode = videoMode.displayMode; + if (!DeckLinkDisplayModeForVideoFormat(videoMode, displayMode)) + return false; + displayModeName = videoMode.displayName; return true; } -bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode) +double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds) { - const std::string formatToken = NormalizeModeToken(videoFormat); - const std::string frameToken = NormalizeModeToken(frameRate); - const std::string combinedToken = formatToken + frameToken; - - struct ModeOption + struct ModeRate { - const char* token; BMDDisplayMode mode; - const char* displayName; + int64_t frameDuration; + int64_t timeScale; }; - static const ModeOption options[] = + static const ModeRate rates[] = { - { "720p50", bmdModeHD720p50, "720p50" }, - { "hd720p50", bmdModeHD720p50, "720p50" }, - { "720p5994", bmdModeHD720p5994, "720p59.94" }, - { "hd720p5994", bmdModeHD720p5994, "720p59.94" }, - { "720p60", bmdModeHD720p60, "720p60" }, - { "hd720p60", bmdModeHD720p60, "720p60" }, - { "1080i50", bmdModeHD1080i50, "1080i50" }, - { "hd1080i50", bmdModeHD1080i50, "1080i50" }, - { "1080i5994", bmdModeHD1080i5994, "1080i59.94" }, - { "hd1080i5994", bmdModeHD1080i5994, "1080i59.94" }, - { "1080i60", bmdModeHD1080i6000, "1080i60" }, - { "hd1080i60", bmdModeHD1080i6000, "1080i60" }, - { "1080p2398", bmdModeHD1080p2398, "1080p23.98" }, - { "hd1080p2398", bmdModeHD1080p2398, "1080p23.98" }, - { "1080p24", bmdModeHD1080p24, "1080p24" }, - { "hd1080p24", bmdModeHD1080p24, "1080p24" }, - { "1080p25", bmdModeHD1080p25, "1080p25" }, - { "hd1080p25", bmdModeHD1080p25, "1080p25" }, - { "1080p2997", bmdModeHD1080p2997, "1080p29.97" }, - { "hd1080p2997", bmdModeHD1080p2997, "1080p29.97" }, - { "1080p30", bmdModeHD1080p30, "1080p30" }, - { "hd1080p30", bmdModeHD1080p30, "1080p30" }, - { "1080p50", bmdModeHD1080p50, "1080p50" }, - { "hd1080p50", bmdModeHD1080p50, "1080p50" }, - { "1080p5994", bmdModeHD1080p5994, "1080p59.94" }, - { "hd1080p5994", bmdModeHD1080p5994, "1080p59.94" }, - { "1080p60", bmdModeHD1080p6000, "1080p60" }, - { "hd1080p60", bmdModeHD1080p6000, "1080p60" }, - { "2160p2398", bmdMode4K2160p2398, "2160p23.98" }, - { "4k2160p2398", bmdMode4K2160p2398, "2160p23.98" }, - { "2160p24", bmdMode4K2160p24, "2160p24" }, - { "4k2160p24", bmdMode4K2160p24, "2160p24" }, - { "2160p25", bmdMode4K2160p25, "2160p25" }, - { "4k2160p25", bmdMode4K2160p25, "2160p25" }, - { "2160p2997", bmdMode4K2160p2997, "2160p29.97" }, - { "4k2160p2997", bmdMode4K2160p2997, "2160p29.97" }, - { "2160p30", bmdMode4K2160p30, "2160p30" }, - { "4k2160p30", bmdMode4K2160p30, "2160p30" }, - { "2160p50", bmdMode4K2160p50, "2160p50" }, - { "4k2160p50", bmdMode4K2160p50, "2160p50" }, - { "2160p5994", bmdMode4K2160p5994, "2160p59.94" }, - { "4k2160p5994", bmdMode4K2160p5994, "2160p59.94" }, - { "2160p60", bmdMode4K2160p60, "2160p60" }, - { "4k2160p60", bmdMode4K2160p60, "2160p60" } + { bmdModeHD720p50, 1, 50 }, + { bmdModeHD720p5994, 1001, 60000 }, + { bmdModeHD720p60, 1, 60 }, + { bmdModeHD1080i50, 1, 25 }, + { bmdModeHD1080i5994, 1001, 30000 }, + { bmdModeHD1080i6000, 1, 30 }, + { bmdModeHD1080p2398, 1001, 24000 }, + { bmdModeHD1080p24, 1, 24 }, + { bmdModeHD1080p25, 1, 25 }, + { bmdModeHD1080p2997, 1001, 30000 }, + { bmdModeHD1080p30, 1, 30 }, + { bmdModeHD1080p50, 1, 50 }, + { bmdModeHD1080p5994, 1001, 60000 }, + { bmdModeHD1080p6000, 1, 60 }, + { bmdMode4K2160p2398, 1001, 24000 }, + { bmdMode4K2160p24, 1, 24 }, + { bmdMode4K2160p25, 1, 25 }, + { bmdMode4K2160p2997, 1001, 30000 }, + { bmdMode4K2160p30, 1, 30 }, + { bmdMode4K2160p50, 1, 50 }, + { bmdMode4K2160p5994, 1001, 60000 }, + { bmdMode4K2160p60, 1, 60 } }; - for (const ModeOption& option : options) + for (const ModeRate& rate : rates) { - if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token)) - { - videoMode.displayMode = option.mode; - videoMode.displayName = option.displayName; - return true; - } + if (rate.mode == displayMode && rate.timeScale > 0) + return (static_cast(rate.frameDuration) * 1000.0) / static_cast(rate.timeScale); } - return false; -} - -bool ResolveConfiguredVideoFormats( - const std::string& inputVideoFormat, - const std::string& inputFrameRate, - const std::string& outputVideoFormat, - const std::string& outputFrameRate, - VideoFormatSelection& videoModes, - std::string& error) -{ - if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input)) - { - error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " + - inputVideoFormat + " / " + inputFrameRate; - return false; - } - - if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output)) - { - error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " + - outputVideoFormat + " / " + outputFrameRate; - return false; - } - - return true; + return fallbackMilliseconds; } bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode) diff --git a/src/video/decklink/DeckLinkDisplayMode.h b/src/video/decklink/DeckLinkDisplayMode.h index 3be24fd..78ffe58 100644 --- a/src/video/decklink/DeckLinkDisplayMode.h +++ b/src/video/decklink/DeckLinkDisplayMode.h @@ -1,47 +1,11 @@ #pragma once #include "DeckLinkAPI_h.h" +#include "VideoMode.h" #include -struct FrameSize -{ - unsigned width = 0; - unsigned height = 0; - - bool IsEmpty() const { return width == 0 || height == 0; } -}; - -inline bool operator==(const FrameSize& left, const FrameSize& right) -{ - return left.width == right.width && left.height == right.height; -} - -inline bool operator!=(const FrameSize& left, const FrameSize& right) -{ - return !(left == right); -} - -struct VideoFormat -{ - BMDDisplayMode displayMode = bmdModeHD1080p5994; - std::string displayName = "1080p59.94"; -}; - -struct VideoFormatSelection -{ - VideoFormat input; - VideoFormat output; -}; - -std::string NormalizeModeToken(const std::string& value); +bool DeckLinkDisplayModeForVideoFormat(const VideoFormat& videoMode, BMDDisplayMode& displayMode); bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName); -bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode); -bool ResolveConfiguredVideoFormats( - const std::string& inputVideoFormat, - const std::string& inputFrameRate, - const std::string& outputVideoFormat, - const std::string& outputFrameRate, - VideoFormatSelection& videoModes, - std::string& error); +double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds); bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode); diff --git a/src/video/decklink/DeckLinkInput.cpp b/src/video/decklink/DeckLinkInput.cpp index e3f63fd..8cd2666 100644 --- a/src/video/decklink/DeckLinkInput.cpp +++ b/src/video/decklink/DeckLinkInput.cpp @@ -86,10 +86,17 @@ bool DeckLinkInput::Initialize(const DeckLinkInputConfig& config, std::string& e mConfig = config; Log("decklink-input", "Initializing DeckLink input for " + config.videoFormat.displayName + "."); + BMDDisplayMode displayMode = bmdModeUnknown; + if (!DeckLinkDisplayModeForVideoFormat(config.videoFormat, displayMode)) + { + error = "DeckLink input setup failed while mapping " + config.videoFormat.displayName + " to a DeckLink display mode."; + return false; + } + if (!DiscoverInput(config, error)) return false; - if (mInput->EnableVideoInput(config.videoFormat.displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK) + if (mInput->EnableVideoInput(displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK) { error = "DeckLink input setup failed while enabling " + std::string(mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8") + @@ -260,6 +267,13 @@ void DeckLinkInput::HandleFormatChanged() bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error) { + BMDDisplayMode targetDisplayMode = bmdModeUnknown; + if (!DeckLinkDisplayModeForVideoFormat(config.videoFormat, targetDisplayMode)) + { + error = "DeckLink input discovery failed while mapping " + config.videoFormat.displayName + " to a DeckLink display mode."; + return false; + } + CComPtr iterator; HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast(&iterator)); if (FAILED(result)) @@ -275,8 +289,8 @@ bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string if (deckLink->QueryInterface(IID_IDeckLinkInput, reinterpret_cast(&candidateInput)) == S_OK && candidateInput != nullptr) { CComPtr displayMode; - if (FindInputDisplayMode(candidateInput, config.videoFormat.displayMode, &displayMode) && - SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitBGRA)) + if (FindInputDisplayMode(candidateInput, targetDisplayMode, &displayMode) && + SupportsInputFormat(candidateInput, targetDisplayMode, bmdFormat8BitBGRA)) { mInput = candidateInput; mCapturePixelFormat = bmdFormat8BitBGRA; @@ -284,7 +298,7 @@ bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string return true; } if (displayMode != nullptr && - SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitYUV)) + SupportsInputFormat(candidateInput, targetDisplayMode, bmdFormat8BitYUV)) { mInput = candidateInput; mCapturePixelFormat = bmdFormat8BitYUV; diff --git a/src/video/decklink/DeckLinkSession.cpp b/src/video/decklink/DeckLinkSession.cpp index 20ae9da..8ce8383 100644 --- a/src/video/decklink/DeckLinkSession.cpp +++ b/src/video/decklink/DeckLinkSession.cpp @@ -169,6 +169,19 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM CComPtr deckLinkIterator; CComPtr inputMode; CComPtr outputMode; + BMDDisplayMode inputDisplayMode = bmdModeUnknown; + BMDDisplayMode outputDisplayMode = bmdModeUnknown; + + if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode)) + { + error = "Cannot map configured input mode to DeckLink BMDDisplayMode: " + videoModes.input.displayName; + return false; + } + if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode)) + { + error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName; + return false; + } mState.inputDisplayModeName = videoModes.input.displayName; mState.outputDisplayModeName = videoModes.output.displayName; @@ -250,7 +263,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM return false; } - if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode)) + if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, inputDisplayMode, &inputMode)) { error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName; ReleaseResources(); @@ -266,7 +279,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM return false; } - if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode)) + if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, outputDisplayMode, &outputMode)) { error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName; ReleaseResources(); @@ -304,14 +317,22 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo } mState.formatStatusMessage.clear(); + BMDDisplayMode inputDisplayMode = bmdModeUnknown; + BMDDisplayMode outputDisplayMode = bmdModeUnknown; + if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode) || + !DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode)) + { + error = "DeckLink format selection failed while mapping configured video modes."; + return false; + } - const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV); + const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, inputDisplayMode, bmdFormat10BitYUV); mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8; if (input != nullptr && !inputTenBitSupported) mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. "; - const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV); - const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA); + const bool outputTenBitSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUV); + const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA); mState.outputPixelFormat = outputAlphaRequired ? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8) : (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8); @@ -369,8 +390,14 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFor return true; } + BMDDisplayMode inputDisplayMode = bmdModeUnknown; + if (!DeckLinkDisplayModeForVideoFormat(inputVideoMode, inputDisplayMode)) + { + error = "DeckLink input setup failed while mapping " + inputVideoMode.displayName + " to a DeckLink display mode."; + return false; + } const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat); - if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK) + if (input->EnableVideoInput(inputDisplayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK) { if (mState.inputPixelFormat == VideoIOPixelFormat::V210) { @@ -378,7 +405,7 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFor mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8; mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u; mState.captureTextureWidth = mState.inputFrameSize.width / 2u; - if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK) + if (input->EnableVideoInput(inputDisplayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK) { std::ostringstream status; status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat) @@ -417,7 +444,13 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoF { mOutputFrameCallback = std::move(callback); - if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK) + BMDDisplayMode outputDisplayMode = bmdModeUnknown; + if (!DeckLinkDisplayModeForVideoFormat(outputVideoMode, outputDisplayMode)) + { + error = "DeckLink output setup failed while mapping " + outputVideoMode.displayName + " to a DeckLink display mode."; + return false; + } + if (output->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK) { error = "DeckLink output setup failed while enabling video output."; return false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f930b1b..12cf7ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,13 +20,13 @@ add_video_shader_test(RenderCadenceCompositorTelemetryTests add_video_shader_test(RenderCadenceCompositorFrameExchangeTests "${SRC_DIR}/frames/SystemFrameExchange.cpp" - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/RenderCadenceCompositorFrameExchangeTests.cpp" ) add_video_shader_test(RenderCadenceCompositorInputFrameMailboxTests "${SRC_DIR}/frames/InputFrameMailbox.cpp" - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/RenderCadenceCompositorInputFrameMailboxTests.cpp" ) @@ -72,6 +72,7 @@ add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests "${SRC_DIR}/app/AppConfig.cpp" "${SRC_DIR}/app/AppConfigProvider.cpp" + ${VIDEO_MODE_SOURCES} "${SRC_DIR}/json/JsonWriter.cpp" ${RUNTIME_LAYER_SOURCES} ${RUNTIME_TEXT_SOURCES} @@ -96,6 +97,8 @@ target_link_libraries(RenderCadenceCompositorHttpControlServerTests PRIVATE Ws2_ add_video_shader_test(RenderCadenceCompositorAppConfigProviderTests "${SRC_DIR}/app/AppConfig.cpp" "${SRC_DIR}/app/AppConfigProvider.cpp" + ${VIDEO_MODE_SOURCES} + "${SRC_DIR}/video/decklink/DeckLinkDisplayMode.cpp" ${RUNTIME_JSON_SOURCES} "${TEST_DIR}/RenderCadenceCompositorAppConfigProviderTests.cpp" ) @@ -129,7 +132,7 @@ add_video_shader_test(Std140BufferTests add_video_shader_test(VideoIOFormatTests "${SRC_DIR}/video/decklink/DeckLinkVideoIOFormat.cpp" - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/VideoIOFormatTests.cpp" ) @@ -139,7 +142,7 @@ add_video_shader_test(VideoPlayoutSchedulerTests ) add_video_shader_test(VideoOutputThreadTests - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/VideoOutputThreadTests.cpp" ) @@ -160,7 +163,7 @@ add_video_shader_test(RenderCadenceControllerTests add_video_shader_test(SystemOutputFramePoolTests "${SRC_DIR}/video/playout/SystemOutputFramePool.cpp" - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/SystemOutputFramePoolTests.cpp" ) @@ -170,7 +173,7 @@ add_video_shader_test(VideoBackendLifecycleTests ) add_video_shader_test(VideoIODeviceFakeTests - "${SRC_DIR}/video/core/VideoIOFormat.cpp" + ${VIDEO_FORMAT_SOURCES} "${TEST_DIR}/VideoIODeviceFakeTests.cpp" ) diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index 30f71e6..8fb6fba 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -1,4 +1,5 @@ #include "AppConfigProvider.h" +#include "DeckLinkDisplayMode.h" #include #include