legacy code cleanup
This commit is contained in:
@@ -38,7 +38,6 @@ set(VIDEO_SHADER_INCLUDE_DIRS
|
|||||||
"${SRC_DIR}/video"
|
"${SRC_DIR}/video"
|
||||||
"${SRC_DIR}/video/core"
|
"${SRC_DIR}/video/core"
|
||||||
"${SRC_DIR}/video/decklink"
|
"${SRC_DIR}/video/decklink"
|
||||||
"${SRC_DIR}/video/legacy"
|
|
||||||
"${SRC_DIR}/video/playout"
|
"${SRC_DIR}/video/playout"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,21 +113,6 @@ set(VIDEO_FORMAT_SOURCES
|
|||||||
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
|
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
set(LEGACY_VIDEO_BACKEND_SOURCES
|
|
||||||
"${SRC_DIR}/video/legacy/VideoBackend.cpp"
|
|
||||||
"${SRC_DIR}/video/legacy/VideoBackend.h"
|
|
||||||
"${SRC_DIR}/video/legacy/VideoBackendLifecycle.cpp"
|
|
||||||
"${SRC_DIR}/video/legacy/VideoBackendLifecycle.h"
|
|
||||||
"${SRC_DIR}/video/playout/OutputProductionController.cpp"
|
|
||||||
"${SRC_DIR}/video/playout/OutputProductionController.h"
|
|
||||||
"${SRC_DIR}/video/playout/RenderCadenceController.cpp"
|
|
||||||
"${SRC_DIR}/video/playout/RenderCadenceController.h"
|
|
||||||
"${SRC_DIR}/video/playout/RenderOutputQueue.cpp"
|
|
||||||
"${SRC_DIR}/video/playout/RenderOutputQueue.h"
|
|
||||||
"${SRC_DIR}/video/playout/SystemOutputFramePool.cpp"
|
|
||||||
"${SRC_DIR}/video/playout/SystemOutputFramePool.h"
|
|
||||||
)
|
|
||||||
|
|
||||||
set(SLANG_RUNTIME_FILES
|
set(SLANG_RUNTIME_FILES
|
||||||
"${SLANG_ROOT}/bin/slangc.exe"
|
"${SLANG_ROOT}/bin/slangc.exe"
|
||||||
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
||||||
@@ -164,10 +148,6 @@ else()
|
|||||||
"${SRC_DIR}/*.cpp"
|
"${SRC_DIR}/*.cpp"
|
||||||
"${SRC_DIR}/*.h"
|
"${SRC_DIR}/*.h"
|
||||||
)
|
)
|
||||||
list(REMOVE_ITEM RENDER_CADENCE_APP_SOURCES
|
|
||||||
${LEGACY_VIDEO_BACKEND_SOURCES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||||
video_shader_target_defaults(RenderCadenceCompositor)
|
video_shader_target_defaults(RenderCadenceCompositor)
|
||||||
target_link_libraries(RenderCadenceCompositor PRIVATE
|
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to
|
|||||||
- Add more video I/O backends now that the DeckLink path is isolated under `src/video/decklink/`.
|
- Add more video I/O backends now that the DeckLink path is isolated under `src/video/decklink/`.
|
||||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||||
- Add WebView2 for an embedded native control surface.
|
- Add WebView2 for an embedded native control surface.
|
||||||
- MSDF typography rasterisation
|
|
||||||
- More shader-library organisation and filtering as the built-in library grows.
|
- More shader-library organisation and filtering as the built-in library grows.
|
||||||
- Optional linear-light compositing mode.
|
- Optional linear-light compositing mode.
|
||||||
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include "VideoMode.h"
|
#include "VideoMode.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
enum class VideoIOBackend
|
enum class VideoIOBackend
|
||||||
@@ -21,13 +20,6 @@ enum class VideoIOCompletionResult
|
|||||||
Unknown
|
Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOConfig
|
|
||||||
{
|
|
||||||
VideoFormatSelection videoModes;
|
|
||||||
bool externalKeyingEnabled = false;
|
|
||||||
bool preferTenBit = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOState
|
struct VideoIOState
|
||||||
{
|
{
|
||||||
FrameSize inputFrameSize;
|
FrameSize inputFrameSize;
|
||||||
@@ -109,56 +101,3 @@ struct VideoPlayoutRecoveryDecision
|
|||||||
uint64_t lateStreak = 0;
|
uint64_t lateStreak = 0;
|
||||||
uint64_t dropStreak = 0;
|
uint64_t dropStreak = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class VideoIODevice
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
|
|
||||||
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
|
||||||
|
|
||||||
virtual ~VideoIODevice() = default;
|
|
||||||
virtual void ReleaseResources() = 0;
|
|
||||||
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
|
||||||
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
|
||||||
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
|
||||||
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
|
||||||
virtual bool PrepareOutputSchedule() = 0;
|
|
||||||
virtual bool StartInputStreams() = 0;
|
|
||||||
virtual bool StartScheduledPlayback() = 0;
|
|
||||||
virtual bool Start() = 0;
|
|
||||||
virtual bool Stop() = 0;
|
|
||||||
virtual const VideoIOState& State() const = 0;
|
|
||||||
virtual VideoIOState& MutableState() = 0;
|
|
||||||
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth) = 0;
|
|
||||||
|
|
||||||
bool HasInputDevice() const { return State().hasInputDevice; }
|
|
||||||
bool HasInputSource() const { return State().hasInputSource; }
|
|
||||||
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
|
|
||||||
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
|
|
||||||
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
|
|
||||||
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
|
|
||||||
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
|
|
||||||
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
|
|
||||||
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
|
|
||||||
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
|
|
||||||
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
|
|
||||||
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
|
|
||||||
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
|
|
||||||
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
|
|
||||||
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
|
|
||||||
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
|
|
||||||
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
|
||||||
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
|
||||||
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
|
||||||
const std::string& OutputModelName() const { return State().outputModelName; }
|
|
||||||
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
|
|
||||||
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
|
|
||||||
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
|
|
||||||
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
|
||||||
const std::string& StatusMessage() const { return State().statusMessage; }
|
|
||||||
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
|
||||||
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,52 +2,6 @@
|
|||||||
|
|
||||||
#include "DeckLinkSession.h"
|
#include "DeckLinkSession.h"
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// DeckLink Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
|
|
||||||
m_pOwner(pOwner),
|
|
||||||
mRefCount(1)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::QueryInterface(REFIID, LPVOID* ppv)
|
|
||||||
{
|
|
||||||
*ppv = NULL;
|
|
||||||
return E_NOINTERFACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG CaptureDelegate::AddRef()
|
|
||||||
{
|
|
||||||
return InterlockedIncrement(&mRefCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG CaptureDelegate::Release()
|
|
||||||
{
|
|
||||||
int newCount = InterlockedDecrement(&mRefCount);
|
|
||||||
if (newCount == 0)
|
|
||||||
delete this;
|
|
||||||
return newCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket*)
|
|
||||||
{
|
|
||||||
if (!inputFrame)
|
|
||||||
{
|
|
||||||
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
|
|
||||||
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
|
||||||
{
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
// DeckLink Playout Delegate Class
|
// DeckLink Playout Delegate Class
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
@@ -84,7 +38,7 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF
|
|||||||
case bmdOutputFrameDropped:
|
case bmdOutputFrameDropped:
|
||||||
case bmdOutputFrameCompleted:
|
case bmdOutputFrameCompleted:
|
||||||
case bmdOutputFrameFlushed:
|
case bmdOutputFrameFlushed:
|
||||||
// Late/drop counts are recorded by VideoBackend; keep this callback lean.
|
// Late/drop counts are recorded by the output edge; keep this callback lean.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
||||||
|
|||||||
@@ -8,26 +8,6 @@
|
|||||||
|
|
||||||
class DeckLinkSession;
|
class DeckLinkSession;
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class CaptureDelegate : public IDeckLinkInputCallback
|
|
||||||
{
|
|
||||||
DeckLinkSession* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
CaptureDelegate(DeckLinkSession* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
// Render Delegate Class
|
// Render Delegate Class
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
|
|||||||
@@ -100,24 +100,6 @@ std::string BstrToUtf8(BSTR value)
|
|||||||
return std::string(utf8Name.data());
|
return std::string(utf8Name.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
|
||||||
{
|
|
||||||
if (input == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
BOOL supported = FALSE;
|
|
||||||
BMDDisplayMode actualMode = bmdModeUnknown;
|
|
||||||
const HRESULT result = input->DoesSupportVideoMode(
|
|
||||||
bmdVideoConnectionUnspecified,
|
|
||||||
displayMode,
|
|
||||||
pixelFormat,
|
|
||||||
bmdNoVideoInputConversion,
|
|
||||||
bmdSupportedVideoModeDefault,
|
|
||||||
&actualMode,
|
|
||||||
&supported);
|
|
||||||
return result == S_OK && supported != FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
if (output == nullptr)
|
if (output == nullptr)
|
||||||
@@ -144,11 +126,6 @@ DeckLinkSession::~DeckLinkSession()
|
|||||||
|
|
||||||
void DeckLinkSession::ReleaseResources()
|
void DeckLinkSession::ReleaseResources()
|
||||||
{
|
{
|
||||||
if (input != nullptr)
|
|
||||||
input->SetCallback(nullptr);
|
|
||||||
captureDelegate.Release();
|
|
||||||
input.Release();
|
|
||||||
|
|
||||||
if (output != nullptr)
|
if (output != nullptr)
|
||||||
output->SetScheduledFrameCompletionCallback(nullptr);
|
output->SetScheduledFrameCompletionCallback(nullptr);
|
||||||
|
|
||||||
@@ -167,24 +144,17 @@ void DeckLinkSession::ReleaseResources()
|
|||||||
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
||||||
CComPtr<IDeckLinkDisplayMode> inputMode;
|
|
||||||
CComPtr<IDeckLinkDisplayMode> outputMode;
|
CComPtr<IDeckLinkDisplayMode> outputMode;
|
||||||
BMDDisplayMode inputDisplayMode = bmdModeUnknown;
|
|
||||||
BMDDisplayMode outputDisplayMode = 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))
|
if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
|
||||||
{
|
{
|
||||||
error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName;
|
error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mState.inputDisplayModeName = videoModes.input.displayName;
|
|
||||||
mState.outputDisplayModeName = videoModes.output.displayName;
|
mState.outputDisplayModeName = videoModes.output.displayName;
|
||||||
|
mState.inputDisplayModeName = "No input - output session";
|
||||||
|
|
||||||
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
||||||
if (FAILED(result))
|
if (FAILED(result))
|
||||||
@@ -226,11 +196,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inputUsed = false;
|
if (!output)
|
||||||
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
|
|
||||||
inputUsed = true;
|
|
||||||
|
|
||||||
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
|
|
||||||
{
|
{
|
||||||
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
|
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
|
||||||
output.Release();
|
output.Release();
|
||||||
@@ -244,7 +210,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
|
|
||||||
deckLink.Release();
|
deckLink.Release();
|
||||||
|
|
||||||
if (output && input)
|
if (output)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,22 +221,6 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
|
|
||||||
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
|
|
||||||
{
|
|
||||||
error = "Cannot get input Display Mode Iterator.";
|
|
||||||
ReleaseResources();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, inputDisplayMode, &inputMode))
|
|
||||||
{
|
|
||||||
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
|
|
||||||
ReleaseResources();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inputDisplayModeIterator.Release();
|
|
||||||
|
|
||||||
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
|
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
|
||||||
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
|
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
|
||||||
{
|
{
|
||||||
@@ -287,11 +237,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
}
|
}
|
||||||
|
|
||||||
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
||||||
mState.inputFrameSize = inputMode
|
mState.inputFrameSize = mState.outputFrameSize;
|
||||||
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
|
|
||||||
: mState.outputFrameSize;
|
|
||||||
if (!input)
|
|
||||||
mState.inputDisplayModeName = "No input - black frame";
|
|
||||||
BMDTimeValue frameDuration = 0;
|
BMDTimeValue frameDuration = 0;
|
||||||
BMDTimeScale frameTimescale = 0;
|
BMDTimeScale frameTimescale = 0;
|
||||||
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
||||||
@@ -302,7 +248,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
|
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
|
||||||
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
||||||
mState.outputPackTextureWidth = mState.outputFrameSize.width;
|
mState.outputPackTextureWidth = mState.outputFrameSize.width;
|
||||||
mState.hasInputDevice = input != nullptr;
|
mState.hasInputDevice = false;
|
||||||
mState.hasInputSource = false;
|
mState.hasInputSource = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -317,19 +263,14 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
|
|||||||
}
|
}
|
||||||
|
|
||||||
mState.formatStatusMessage.clear();
|
mState.formatStatusMessage.clear();
|
||||||
BMDDisplayMode inputDisplayMode = bmdModeUnknown;
|
|
||||||
BMDDisplayMode outputDisplayMode = bmdModeUnknown;
|
BMDDisplayMode outputDisplayMode = bmdModeUnknown;
|
||||||
if (!DeckLinkDisplayModeForVideoFormat(videoModes.input, inputDisplayMode) ||
|
if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
|
||||||
!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode))
|
|
||||||
{
|
{
|
||||||
error = "DeckLink format selection failed while mapping configured video modes.";
|
error = "DeckLink format selection failed while mapping the configured output mode.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, inputDisplayMode, bmdFormat10BitYUV);
|
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
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, outputDisplayMode, bmdFormat10BitYUV);
|
const bool outputTenBitSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUV);
|
||||||
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA);
|
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA);
|
||||||
@@ -371,75 +312,13 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
|
|||||||
: mState.inputFrameSize.width / 2u;
|
: mState.inputFrameSize.width / 2u;
|
||||||
|
|
||||||
std::ostringstream status;
|
std::ostringstream status;
|
||||||
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
|
status << "DeckLink formats: input none, output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
|
||||||
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
|
|
||||||
if (!mState.formatStatusMessage.empty())
|
if (!mState.formatStatusMessage.empty())
|
||||||
status << " " << mState.formatStatusMessage;
|
status << " " << mState.formatStatusMessage;
|
||||||
mState.formatStatusMessage = status.str();
|
mState.formatStatusMessage = status.str();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
|
|
||||||
{
|
|
||||||
mInputFrameCallback = std::move(callback);
|
|
||||||
|
|
||||||
if (!input)
|
|
||||||
{
|
|
||||||
mState.hasInputSource = false;
|
|
||||||
mState.inputDisplayModeName = "No input - black frame";
|
|
||||||
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(inputDisplayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
|
||||||
{
|
|
||||||
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
|
|
||||||
{
|
|
||||||
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
|
|
||||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
|
||||||
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
|
||||||
if (input->EnableVideoInput(inputDisplayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
|
||||||
{
|
|
||||||
std::ostringstream status;
|
|
||||||
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
|
|
||||||
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
|
|
||||||
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
|
|
||||||
mState.formatStatusMessage = status.str();
|
|
||||||
goto input_enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
|
|
||||||
input.Release();
|
|
||||||
mState.hasInputDevice = false;
|
|
||||||
mState.hasInputSource = false;
|
|
||||||
mState.inputDisplayModeName = "No input - black frame";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
input_enabled:
|
|
||||||
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
|
|
||||||
if (captureDelegate == nullptr)
|
|
||||||
{
|
|
||||||
error = "DeckLink input setup failed while creating the capture callback.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (input->SetCallback(captureDelegate) != S_OK)
|
|
||||||
{
|
|
||||||
error = "DeckLink input setup failed while installing the capture callback.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||||
{
|
{
|
||||||
mOutputFrameCallback = std::move(callback);
|
mOutputFrameCallback = std::move(callback);
|
||||||
@@ -772,19 +651,6 @@ bool DeckLinkSession::PrepareOutputSchedule()
|
|||||||
return output != nullptr;
|
return output != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::StartInputStreams()
|
|
||||||
{
|
|
||||||
if (!input)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (input->StartStreams() != S_OK)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DeckLinkSession::StartScheduledPlayback()
|
bool DeckLinkSession::StartScheduledPlayback()
|
||||||
{
|
{
|
||||||
if (!output)
|
if (!output)
|
||||||
@@ -802,42 +668,6 @@ bool DeckLinkSession::StartScheduledPlayback()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::Start()
|
|
||||||
{
|
|
||||||
if (!output)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (outputVideoFrameQueue.empty())
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
||||||
mPlayoutPolicy = policy;
|
|
||||||
if (!PrepareOutputSchedule())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < policy.targetPrerollFrames; i++)
|
|
||||||
{
|
|
||||||
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
|
||||||
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Could not acquire a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ScheduleBlackFrame(outputVideoFrame))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return StartInputStreams() && StartScheduledPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DeckLinkSession::Stop()
|
bool DeckLinkSession::Stop()
|
||||||
{
|
{
|
||||||
if (keyer != nullptr)
|
if (keyer != nullptr)
|
||||||
@@ -846,12 +676,6 @@ bool DeckLinkSession::Stop()
|
|||||||
mState.externalKeyingActive = false;
|
mState.externalKeyingActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input)
|
|
||||||
{
|
|
||||||
input->StopStreams();
|
|
||||||
input->DisableVideoInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output)
|
if (output)
|
||||||
{
|
{
|
||||||
output->StopScheduledPlayback(0, NULL, 0);
|
output->StopScheduledPlayback(0, NULL, 0);
|
||||||
@@ -861,42 +685,6 @@ bool DeckLinkSession::Stop()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
|
||||||
{
|
|
||||||
mState.hasInputSource = !hasNoInputSource;
|
|
||||||
if (hasNoInputSource || mInputFrameCallback == nullptr)
|
|
||||||
{
|
|
||||||
VideoIOFrame frame;
|
|
||||||
frame.width = mState.inputFrameSize.width;
|
|
||||||
frame.height = mState.inputFrameSize.height;
|
|
||||||
frame.pixelFormat = mState.inputPixelFormat;
|
|
||||||
frame.hasNoInputSource = hasNoInputSource;
|
|
||||||
if (mInputFrameCallback)
|
|
||||||
mInputFrameCallback(frame);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
|
||||||
void* videoPixels = nullptr;
|
|
||||||
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
|
|
||||||
return;
|
|
||||||
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
|
||||||
return;
|
|
||||||
|
|
||||||
inputFrameBuffer->GetBytes(&videoPixels);
|
|
||||||
|
|
||||||
VideoIOFrame frame;
|
|
||||||
frame.bytes = videoPixels;
|
|
||||||
frame.rowBytes = inputFrame->GetRowBytes();
|
|
||||||
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
|
|
||||||
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
|
|
||||||
frame.pixelFormat = mState.inputPixelFormat;
|
|
||||||
frame.hasNoInputSource = hasNoInputSource;
|
|
||||||
mInputFrameCallback(frame);
|
|
||||||
|
|
||||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||||
{
|
{
|
||||||
RefreshBufferedVideoFrameCount();
|
RefreshBufferedVideoFrameCount();
|
||||||
|
|||||||
@@ -11,28 +11,28 @@
|
|||||||
|
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
class OpenGLComposite;
|
class OpenGLComposite;
|
||||||
|
|
||||||
class DeckLinkSession : public VideoIODevice
|
class DeckLinkSession
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DeckLinkSession() = default;
|
DeckLinkSession() = default;
|
||||||
~DeckLinkSession();
|
~DeckLinkSession();
|
||||||
|
|
||||||
void ReleaseResources() override;
|
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
||||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
|
|
||||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
|
void ReleaseResources();
|
||||||
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
||||||
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
|
||||||
bool PrepareOutputSchedule() override;
|
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
|
||||||
bool StartInputStreams() override;
|
bool PrepareOutputSchedule();
|
||||||
bool StartScheduledPlayback() override;
|
bool StartScheduledPlayback();
|
||||||
bool Start() override;
|
bool Stop();
|
||||||
bool Stop() override;
|
|
||||||
|
|
||||||
bool HasInputDevice() const { return mState.hasInputDevice; }
|
bool HasInputDevice() const { return mState.hasInputDevice; }
|
||||||
bool HasInputSource() const { return mState.hasInputSource; }
|
bool HasInputSource() const { return mState.hasInputSource; }
|
||||||
@@ -61,14 +61,13 @@ public:
|
|||||||
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
|
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
|
||||||
const std::string& StatusMessage() const { return mState.statusMessage; }
|
const std::string& StatusMessage() const { return mState.statusMessage; }
|
||||||
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
||||||
const VideoIOState& State() const override { return mState; }
|
const VideoIOState& State() const { return mState; }
|
||||||
VideoIOState& MutableState() override { return mState; }
|
VideoIOState& MutableState() { return mState; }
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth) override;
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth);
|
||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
|
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
||||||
void EndOutputFrame(VideoIOOutputFrame& frame) override;
|
void EndOutputFrame(VideoIOOutputFrame& frame);
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
|
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
|
||||||
void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
|
||||||
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -83,9 +82,7 @@ private:
|
|||||||
void RefreshBufferedVideoFrameCount();
|
void RefreshBufferedVideoFrameCount();
|
||||||
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
CComPtr<CaptureDelegate> captureDelegate;
|
|
||||||
CComPtr<PlayoutDelegate> playoutDelegate;
|
CComPtr<PlayoutDelegate> playoutDelegate;
|
||||||
CComPtr<IDeckLinkInput> input;
|
|
||||||
CComPtr<IDeckLinkOutput> output;
|
CComPtr<IDeckLinkOutput> output;
|
||||||
CComPtr<IDeckLinkKeyer> keyer;
|
CComPtr<IDeckLinkKeyer> keyer;
|
||||||
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||||
@@ -97,6 +94,5 @@ private:
|
|||||||
bool mScheduleRealignmentPending = false;
|
bool mScheduleRealignmentPending = false;
|
||||||
bool mScheduleRealignmentArmed = true;
|
bool mScheduleRealignmentArmed = true;
|
||||||
bool mProactiveScheduleRealignmentArmed = true;
|
bool mProactiveScheduleRealignmentArmed = true;
|
||||||
InputFrameCallback mInputFrameCallback;
|
|
||||||
OutputFrameCallback mOutputFrameCallback;
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,161 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OutputProductionController.h"
|
|
||||||
#include "RenderCadenceController.h"
|
|
||||||
#include "RenderOutputQueue.h"
|
|
||||||
#include "SystemOutputFramePool.h"
|
|
||||||
#include "VideoBackendLifecycle.h"
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
#include "VideoPlayoutPolicy.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <deque>
|
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
class HealthTelemetry;
|
|
||||||
class OpenGLVideoIOBridge;
|
|
||||||
class RenderEngine;
|
|
||||||
class RuntimeEventDispatcher;
|
|
||||||
class VideoIODevice;
|
|
||||||
|
|
||||||
class VideoBackend
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher);
|
|
||||||
~VideoBackend();
|
|
||||||
|
|
||||||
void ReleaseResources();
|
|
||||||
VideoBackendLifecycleState LifecycleState() const;
|
|
||||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
|
||||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
|
|
||||||
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
|
|
||||||
bool ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
|
|
||||||
bool Start();
|
|
||||||
bool Stop();
|
|
||||||
|
|
||||||
const VideoIOState& State() const;
|
|
||||||
VideoIOState& MutableState();
|
|
||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
|
||||||
void EndOutputFrame(VideoIOOutputFrame& frame);
|
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
|
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth);
|
|
||||||
void RecordBackendPlayoutHealth(VideoIOCompletionResult result, const VideoPlayoutRecoveryDecision& recoveryDecision);
|
|
||||||
|
|
||||||
bool HasInputDevice() const;
|
|
||||||
bool HasInputSource() const;
|
|
||||||
unsigned InputFrameWidth() const;
|
|
||||||
unsigned InputFrameHeight() const;
|
|
||||||
unsigned OutputFrameWidth() const;
|
|
||||||
unsigned OutputFrameHeight() const;
|
|
||||||
unsigned CaptureTextureWidth() const;
|
|
||||||
unsigned OutputPackTextureWidth() const;
|
|
||||||
VideoIOPixelFormat InputPixelFormat() const;
|
|
||||||
const std::string& InputDisplayModeName() const;
|
|
||||||
const std::string& OutputModelName() const;
|
|
||||||
bool SupportsInternalKeying() const;
|
|
||||||
bool SupportsExternalKeying() const;
|
|
||||||
bool KeyerInterfaceAvailable() const;
|
|
||||||
bool ExternalKeyingActive() const;
|
|
||||||
const std::string& StatusMessage() const;
|
|
||||||
bool ShouldPrioritizeOutputOverPreview() const;
|
|
||||||
void SetStatusMessage(const std::string& message);
|
|
||||||
void PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage = std::string());
|
|
||||||
void ReportNoInputDeviceSignalStatus();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void HandleInputFrame(const VideoIOFrame& frame);
|
|
||||||
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
|
|
||||||
void StartOutputCompletionWorker();
|
|
||||||
void StopOutputCompletionWorker();
|
|
||||||
void OutputCompletionWorkerMain();
|
|
||||||
void StartOutputProducerWorker();
|
|
||||||
void StopOutputProducerWorker();
|
|
||||||
void OutputProducerWorkerMain();
|
|
||||||
void NotifyOutputProducer();
|
|
||||||
bool WarmupOutputPreroll();
|
|
||||||
std::chrono::milliseconds OutputProducerWakeInterval() const;
|
|
||||||
void ProcessOutputFrameCompletion(const VideoIOCompletion& completion);
|
|
||||||
std::size_t ProduceReadyOutputFrames(const VideoIOCompletion& completion, std::size_t maxFrames);
|
|
||||||
OutputProductionPressure BuildOutputProductionPressure(const RenderOutputQueueMetrics& metrics) const;
|
|
||||||
bool RenderReadyOutputFrame(const VideoIOState& state, const VideoIOCompletion& completion);
|
|
||||||
std::size_t ScheduleReadyOutputFramesToTarget();
|
|
||||||
bool ScheduleReadyOutputFrame();
|
|
||||||
bool ScheduleBlackUnderrunFrame();
|
|
||||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
|
||||||
void RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics);
|
|
||||||
void RecordDeckLinkBufferTelemetry();
|
|
||||||
void RecordSystemMemoryPlayoutStats();
|
|
||||||
void RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds);
|
|
||||||
bool ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message);
|
|
||||||
bool ApplyLifecycleFailure(const std::string& message);
|
|
||||||
void PublishBackendStateChanged(const std::string& state, const std::string& message);
|
|
||||||
void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state);
|
|
||||||
void PublishInputFrameArrived(const VideoIOFrame& frame);
|
|
||||||
void PublishOutputFrameScheduled(const VideoIOOutputFrame& frame);
|
|
||||||
void PublishOutputFrameCompleted(const VideoIOCompletion& completion);
|
|
||||||
void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit);
|
|
||||||
static std::string CompletionResultName(VideoIOCompletionResult result);
|
|
||||||
static std::string PixelFormatName(VideoIOPixelFormat pixelFormat);
|
|
||||||
static bool IsEnvironmentFlagEnabled(const char* name);
|
|
||||||
|
|
||||||
HealthTelemetry& mHealthTelemetry;
|
|
||||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
|
||||||
VideoBackendLifecycle mLifecycle;
|
|
||||||
VideoPlayoutPolicy mPlayoutPolicy;
|
|
||||||
OutputProductionController mOutputProductionController;
|
|
||||||
RenderCadenceController mRenderCadenceController;
|
|
||||||
RenderOutputQueue mReadyOutputQueue;
|
|
||||||
SystemOutputFramePool mSystemOutputFramePool;
|
|
||||||
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
|
||||||
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
|
|
||||||
std::mutex mOutputCompletionMutex;
|
|
||||||
std::condition_variable mOutputCompletionCondition;
|
|
||||||
std::deque<VideoIOCompletion> mPendingOutputCompletions;
|
|
||||||
std::thread mOutputCompletionWorker;
|
|
||||||
std::mutex mOutputProducerMutex;
|
|
||||||
std::condition_variable mOutputProducerCondition;
|
|
||||||
std::thread mOutputProducerWorker;
|
|
||||||
VideoIOCompletion mLastOutputProductionCompletion;
|
|
||||||
std::chrono::steady_clock::time_point mLastOutputProductionTime;
|
|
||||||
std::mutex mOutputProductionMutex;
|
|
||||||
std::mutex mOutputSchedulingMutex;
|
|
||||||
mutable std::mutex mOutputMetricsMutex;
|
|
||||||
bool mOutputCompletionWorkerRunning = false;
|
|
||||||
bool mOutputCompletionWorkerStopping = false;
|
|
||||||
bool mOutputProducerWorkerRunning = false;
|
|
||||||
bool mOutputProducerWorkerStopping = false;
|
|
||||||
bool mInputCaptureDisabled = false;
|
|
||||||
uint64_t mNextReadyOutputFrameIndex = 0;
|
|
||||||
uint64_t mInputFrameIndex = 0;
|
|
||||||
uint64_t mOutputFrameScheduleIndex = 0;
|
|
||||||
uint64_t mOutputFrameCompletionIndex = 0;
|
|
||||||
bool mHasLastInputSignal = false;
|
|
||||||
bool mLastInputSignal = false;
|
|
||||||
unsigned mLastInputSignalWidth = 0;
|
|
||||||
unsigned mLastInputSignalHeight = 0;
|
|
||||||
std::string mLastInputSignalModeName;
|
|
||||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
|
||||||
double mCompletionIntervalMilliseconds = 0.0;
|
|
||||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
|
||||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
|
||||||
bool mHasReadyQueueDepthBaseline = false;
|
|
||||||
std::size_t mMinReadyQueueDepth = 0;
|
|
||||||
std::size_t mMaxReadyQueueDepth = 0;
|
|
||||||
uint64_t mReadyQueueZeroDepthCount = 0;
|
|
||||||
double mOutputRenderMilliseconds = 0.0;
|
|
||||||
double mSmoothedOutputRenderMilliseconds = 0.0;
|
|
||||||
double mMaxOutputRenderMilliseconds = 0.0;
|
|
||||||
double mOutputFrameAcquireMilliseconds = 0.0;
|
|
||||||
double mOutputFrameRenderRequestMilliseconds = 0.0;
|
|
||||||
double mOutputFrameEndAccessMilliseconds = 0.0;
|
|
||||||
uint64_t mLastLateStreak = 0;
|
|
||||||
uint64_t mLastDropStreak = 0;
|
|
||||||
uint64_t mLateFrameCount = 0;
|
|
||||||
uint64_t mDroppedFrameCount = 0;
|
|
||||||
uint64_t mFlushedFrameCount = 0;
|
|
||||||
};
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#include "VideoBackendLifecycle.h"
|
|
||||||
|
|
||||||
VideoBackendLifecycleState VideoBackendLifecycle::State() const
|
|
||||||
{
|
|
||||||
return mState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& VideoBackendLifecycle::FailureReason() const
|
|
||||||
{
|
|
||||||
return mFailureReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoBackendLifecycleTransition VideoBackendLifecycle::TransitionTo(VideoBackendLifecycleState next, const std::string& reason)
|
|
||||||
{
|
|
||||||
VideoBackendLifecycleTransition transition;
|
|
||||||
transition.previous = mState;
|
|
||||||
transition.current = next;
|
|
||||||
transition.reason = reason;
|
|
||||||
transition.accepted = CanTransition(mState, next);
|
|
||||||
if (!transition.accepted)
|
|
||||||
{
|
|
||||||
transition.current = mState;
|
|
||||||
transition.errorMessage = std::string("Invalid video backend lifecycle transition from ") +
|
|
||||||
StateName(mState) + " to " + StateName(next) + ".";
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
mState = next;
|
|
||||||
transition.current = mState;
|
|
||||||
if (mState != VideoBackendLifecycleState::Failed)
|
|
||||||
mFailureReason.clear();
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoBackendLifecycleTransition VideoBackendLifecycle::Fail(const std::string& reason)
|
|
||||||
{
|
|
||||||
VideoBackendLifecycleTransition transition = TransitionTo(VideoBackendLifecycleState::Failed, reason);
|
|
||||||
if (transition.accepted)
|
|
||||||
mFailureReason = reason;
|
|
||||||
return transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackendLifecycle::CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next)
|
|
||||||
{
|
|
||||||
if (current == next)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
switch (current)
|
|
||||||
{
|
|
||||||
case VideoBackendLifecycleState::Uninitialized:
|
|
||||||
return next == VideoBackendLifecycleState::Discovering ||
|
|
||||||
next == VideoBackendLifecycleState::Stopped ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Discovering:
|
|
||||||
return next == VideoBackendLifecycleState::Discovered ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Discovered:
|
|
||||||
return next == VideoBackendLifecycleState::Configuring ||
|
|
||||||
next == VideoBackendLifecycleState::Stopped ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Configuring:
|
|
||||||
return next == VideoBackendLifecycleState::Configured ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Configured:
|
|
||||||
return next == VideoBackendLifecycleState::Prerolling ||
|
|
||||||
next == VideoBackendLifecycleState::Stopped ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Prerolling:
|
|
||||||
return next == VideoBackendLifecycleState::Running ||
|
|
||||||
next == VideoBackendLifecycleState::Stopping ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Running:
|
|
||||||
return next == VideoBackendLifecycleState::Degraded ||
|
|
||||||
next == VideoBackendLifecycleState::Stopping ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Degraded:
|
|
||||||
return next == VideoBackendLifecycleState::Running ||
|
|
||||||
next == VideoBackendLifecycleState::Stopping ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Stopping:
|
|
||||||
return next == VideoBackendLifecycleState::Stopped ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Stopped:
|
|
||||||
return next == VideoBackendLifecycleState::Discovering ||
|
|
||||||
next == VideoBackendLifecycleState::Failed;
|
|
||||||
case VideoBackendLifecycleState::Failed:
|
|
||||||
return next == VideoBackendLifecycleState::Stopped ||
|
|
||||||
next == VideoBackendLifecycleState::Discovering;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* VideoBackendLifecycle::StateName(VideoBackendLifecycleState state)
|
|
||||||
{
|
|
||||||
switch (state)
|
|
||||||
{
|
|
||||||
case VideoBackendLifecycleState::Uninitialized:
|
|
||||||
return "uninitialized";
|
|
||||||
case VideoBackendLifecycleState::Discovering:
|
|
||||||
return "discovering";
|
|
||||||
case VideoBackendLifecycleState::Discovered:
|
|
||||||
return "discovered";
|
|
||||||
case VideoBackendLifecycleState::Configuring:
|
|
||||||
return "configuring";
|
|
||||||
case VideoBackendLifecycleState::Configured:
|
|
||||||
return "configured";
|
|
||||||
case VideoBackendLifecycleState::Prerolling:
|
|
||||||
return "prerolling";
|
|
||||||
case VideoBackendLifecycleState::Running:
|
|
||||||
return "running";
|
|
||||||
case VideoBackendLifecycleState::Degraded:
|
|
||||||
return "degraded";
|
|
||||||
case VideoBackendLifecycleState::Stopping:
|
|
||||||
return "stopping";
|
|
||||||
case VideoBackendLifecycleState::Stopped:
|
|
||||||
return "stopped";
|
|
||||||
case VideoBackendLifecycleState::Failed:
|
|
||||||
return "failed";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class VideoBackendLifecycleState
|
|
||||||
{
|
|
||||||
Uninitialized,
|
|
||||||
Discovering,
|
|
||||||
Discovered,
|
|
||||||
Configuring,
|
|
||||||
Configured,
|
|
||||||
Prerolling,
|
|
||||||
Running,
|
|
||||||
Degraded,
|
|
||||||
Stopping,
|
|
||||||
Stopped,
|
|
||||||
Failed
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoBackendLifecycleTransition
|
|
||||||
{
|
|
||||||
VideoBackendLifecycleState previous = VideoBackendLifecycleState::Uninitialized;
|
|
||||||
VideoBackendLifecycleState current = VideoBackendLifecycleState::Uninitialized;
|
|
||||||
bool accepted = false;
|
|
||||||
std::string reason;
|
|
||||||
std::string errorMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
class VideoBackendLifecycle
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
VideoBackendLifecycleState State() const;
|
|
||||||
const std::string& FailureReason() const;
|
|
||||||
VideoBackendLifecycleTransition TransitionTo(VideoBackendLifecycleState next, const std::string& reason);
|
|
||||||
VideoBackendLifecycleTransition Fail(const std::string& reason);
|
|
||||||
|
|
||||||
static bool CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next);
|
|
||||||
static const char* StateName(VideoBackendLifecycleState state);
|
|
||||||
|
|
||||||
private:
|
|
||||||
VideoBackendLifecycleState mState = VideoBackendLifecycleState::Uninitialized;
|
|
||||||
std::string mFailureReason;
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#include "OutputProductionController.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::size_t ClampReadyLimit(unsigned value, std::size_t capacity)
|
|
||||||
{
|
|
||||||
const std::size_t requested = static_cast<std::size_t>(value);
|
|
||||||
if (capacity == 0)
|
|
||||||
return requested;
|
|
||||||
return (std::min)(requested, capacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputProductionController::OutputProductionController(const VideoPlayoutPolicy& policy) :
|
|
||||||
mPolicy(NormalizeVideoPlayoutPolicy(policy))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void OutputProductionController::Configure(const VideoPlayoutPolicy& policy)
|
|
||||||
{
|
|
||||||
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputProductionDecision OutputProductionController::Decide(const OutputProductionPressure& pressure) const
|
|
||||||
{
|
|
||||||
OutputProductionDecision decision;
|
|
||||||
|
|
||||||
const std::size_t configuredMaxReadyFrames = static_cast<std::size_t>(mPolicy.maxReadyFrames);
|
|
||||||
const std::size_t effectiveMaxReadyFrames = pressure.readyQueueCapacity > 0
|
|
||||||
? (std::min)(configuredMaxReadyFrames, pressure.readyQueueCapacity)
|
|
||||||
: configuredMaxReadyFrames;
|
|
||||||
const std::size_t effectiveTargetReadyFrames = (std::min)(
|
|
||||||
ClampReadyLimit(mPolicy.targetReadyFrames, pressure.readyQueueCapacity),
|
|
||||||
effectiveMaxReadyFrames);
|
|
||||||
|
|
||||||
decision.targetReadyFrames = effectiveTargetReadyFrames;
|
|
||||||
decision.maxReadyFrames = effectiveMaxReadyFrames;
|
|
||||||
|
|
||||||
if (effectiveMaxReadyFrames == 0)
|
|
||||||
{
|
|
||||||
decision.action = OutputProductionAction::Throttle;
|
|
||||||
decision.reason = "no-ready-frame-capacity";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pressure.readyQueueDepth >= effectiveMaxReadyFrames)
|
|
||||||
{
|
|
||||||
decision.action = OutputProductionAction::Throttle;
|
|
||||||
decision.reason = "ready-queue-full";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pressure.readyQueueDepth < effectiveTargetReadyFrames)
|
|
||||||
{
|
|
||||||
decision.action = OutputProductionAction::Produce;
|
|
||||||
decision.requestedFrames = effectiveTargetReadyFrames - pressure.readyQueueDepth;
|
|
||||||
decision.reason = "ready-queue-below-target";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((pressure.lateStreak > 0 || pressure.dropStreak > 0 || pressure.readyQueueUnderrunCount > 0) &&
|
|
||||||
pressure.readyQueueDepth < effectiveMaxReadyFrames)
|
|
||||||
{
|
|
||||||
decision.action = OutputProductionAction::Produce;
|
|
||||||
decision.requestedFrames = 1;
|
|
||||||
decision.reason = "playout-pressure";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
decision.action = OutputProductionAction::Wait;
|
|
||||||
decision.reason = "ready-queue-at-target";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* OutputProductionActionName(OutputProductionAction action)
|
|
||||||
{
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case OutputProductionAction::Produce:
|
|
||||||
return "Produce";
|
|
||||||
case OutputProductionAction::Throttle:
|
|
||||||
return "Throttle";
|
|
||||||
case OutputProductionAction::Wait:
|
|
||||||
default:
|
|
||||||
return "Wait";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoPlayoutPolicy.h"
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class OutputProductionAction
|
|
||||||
{
|
|
||||||
Produce,
|
|
||||||
Wait,
|
|
||||||
Throttle
|
|
||||||
};
|
|
||||||
|
|
||||||
struct OutputProductionPressure
|
|
||||||
{
|
|
||||||
std::size_t readyQueueDepth = 0;
|
|
||||||
std::size_t readyQueueCapacity = 0;
|
|
||||||
uint64_t readyQueueUnderrunCount = 0;
|
|
||||||
uint64_t lateStreak = 0;
|
|
||||||
uint64_t dropStreak = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct OutputProductionDecision
|
|
||||||
{
|
|
||||||
OutputProductionAction action = OutputProductionAction::Wait;
|
|
||||||
std::size_t requestedFrames = 0;
|
|
||||||
std::size_t targetReadyFrames = 0;
|
|
||||||
std::size_t maxReadyFrames = 0;
|
|
||||||
std::string reason;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OutputProductionController
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit OutputProductionController(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
|
|
||||||
|
|
||||||
void Configure(const VideoPlayoutPolicy& policy);
|
|
||||||
OutputProductionDecision Decide(const OutputProductionPressure& pressure) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
VideoPlayoutPolicy mPolicy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* OutputProductionActionName(OutputProductionAction action);
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#include "RenderCadenceController.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
void RenderCadenceController::Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy)
|
|
||||||
{
|
|
||||||
mTargetFrameDuration = IsPositive(targetFrameDuration) ? targetFrameDuration : std::chrono::milliseconds(1);
|
|
||||||
mPolicy = policy;
|
|
||||||
if (mPolicy.skipThresholdFrames < 1.0)
|
|
||||||
mPolicy.skipThresholdFrames = 1.0;
|
|
||||||
Reset(firstRenderTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderCadenceController::Reset(TimePoint firstRenderTime)
|
|
||||||
{
|
|
||||||
mNextRenderTime = firstRenderTime;
|
|
||||||
mNextFrameIndex = 0;
|
|
||||||
mMetrics = RenderCadenceMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderCadenceDecision RenderCadenceController::Tick(TimePoint now)
|
|
||||||
{
|
|
||||||
RenderCadenceDecision decision;
|
|
||||||
decision.frameIndex = mNextFrameIndex;
|
|
||||||
decision.renderTargetTime = mNextRenderTime;
|
|
||||||
decision.nextRenderTime = mNextRenderTime;
|
|
||||||
|
|
||||||
if (now < mNextRenderTime)
|
|
||||||
{
|
|
||||||
decision.action = RenderCadenceAction::Wait;
|
|
||||||
decision.waitDuration = mNextRenderTime - now;
|
|
||||||
decision.reason = "waiting-for-next-render-tick";
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Duration lateness = now - mNextRenderTime;
|
|
||||||
const uint64_t skippedTicks = SkippedTicksForLateness(lateness);
|
|
||||||
if (skippedTicks > 0)
|
|
||||||
{
|
|
||||||
decision.skippedTicks = skippedTicks;
|
|
||||||
decision.frameIndex = mNextFrameIndex + skippedTicks;
|
|
||||||
decision.renderTargetTime = mNextRenderTime + (mTargetFrameDuration * skippedTicks);
|
|
||||||
decision.reason = "late-skip-render-ticks";
|
|
||||||
mMetrics.skippedTickCount += skippedTicks;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
decision.reason = IsPositive(lateness) ? "late-render-now" : "on-time-render";
|
|
||||||
}
|
|
||||||
|
|
||||||
decision.action = RenderCadenceAction::Render;
|
|
||||||
decision.lateness = now > decision.renderTargetTime
|
|
||||||
? now - decision.renderTargetTime
|
|
||||||
: Duration::zero();
|
|
||||||
mNextFrameIndex = decision.frameIndex + 1;
|
|
||||||
mNextRenderTime = decision.renderTargetTime + mTargetFrameDuration;
|
|
||||||
decision.nextRenderTime = mNextRenderTime;
|
|
||||||
|
|
||||||
++mMetrics.renderedFrameCount;
|
|
||||||
mMetrics.nextFrameIndex = mNextFrameIndex;
|
|
||||||
mMetrics.lastLateness = decision.lateness;
|
|
||||||
if (IsPositive(decision.lateness))
|
|
||||||
{
|
|
||||||
++mMetrics.lateFrameCount;
|
|
||||||
mMetrics.maxLateness = (std::max)(mMetrics.maxLateness, decision.lateness);
|
|
||||||
}
|
|
||||||
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t RenderCadenceController::SkippedTicksForLateness(Duration lateness) const
|
|
||||||
{
|
|
||||||
if (!mPolicy.skipLateTicks || !IsPositive(lateness) || !IsPositive(mTargetFrameDuration))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const double lateFrames = static_cast<double>(lateness.count()) / static_cast<double>(mTargetFrameDuration.count());
|
|
||||||
if (lateFrames < mPolicy.skipThresholdFrames)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const uint64_t elapsedTicks = static_cast<uint64_t>(std::floor(lateFrames));
|
|
||||||
if (elapsedTicks == 0)
|
|
||||||
return 0;
|
|
||||||
return (std::min)(elapsedTicks, mPolicy.maxSkippedTicksPerDecision);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderCadenceController::IsPositive(Duration duration)
|
|
||||||
{
|
|
||||||
return duration > Duration::zero();
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* RenderCadenceActionName(RenderCadenceAction action)
|
|
||||||
{
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case RenderCadenceAction::Render:
|
|
||||||
return "Render";
|
|
||||||
case RenderCadenceAction::Wait:
|
|
||||||
default:
|
|
||||||
return "Wait";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
enum class RenderCadenceAction
|
|
||||||
{
|
|
||||||
Wait,
|
|
||||||
Render
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderCadencePolicy
|
|
||||||
{
|
|
||||||
bool skipLateTicks = true;
|
|
||||||
uint64_t maxSkippedTicksPerDecision = 4;
|
|
||||||
double skipThresholdFrames = 2.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderCadenceDecision
|
|
||||||
{
|
|
||||||
RenderCadenceAction action = RenderCadenceAction::Wait;
|
|
||||||
uint64_t frameIndex = 0;
|
|
||||||
uint64_t skippedTicks = 0;
|
|
||||||
std::chrono::steady_clock::time_point renderTargetTime;
|
|
||||||
std::chrono::steady_clock::time_point nextRenderTime;
|
|
||||||
std::chrono::steady_clock::duration waitDuration = std::chrono::steady_clock::duration::zero();
|
|
||||||
std::chrono::steady_clock::duration lateness = std::chrono::steady_clock::duration::zero();
|
|
||||||
const char* reason = "waiting-for-next-render-tick";
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderCadenceMetrics
|
|
||||||
{
|
|
||||||
uint64_t nextFrameIndex = 0;
|
|
||||||
uint64_t renderedFrameCount = 0;
|
|
||||||
uint64_t skippedTickCount = 0;
|
|
||||||
uint64_t lateFrameCount = 0;
|
|
||||||
std::chrono::steady_clock::duration lastLateness = std::chrono::steady_clock::duration::zero();
|
|
||||||
std::chrono::steady_clock::duration maxLateness = std::chrono::steady_clock::duration::zero();
|
|
||||||
};
|
|
||||||
|
|
||||||
class RenderCadenceController
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using Clock = std::chrono::steady_clock;
|
|
||||||
using TimePoint = Clock::time_point;
|
|
||||||
using Duration = Clock::duration;
|
|
||||||
|
|
||||||
void Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy = RenderCadencePolicy());
|
|
||||||
void Reset(TimePoint firstRenderTime);
|
|
||||||
RenderCadenceDecision Tick(TimePoint now);
|
|
||||||
|
|
||||||
Duration TargetFrameDuration() const { return mTargetFrameDuration; }
|
|
||||||
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
|
||||||
uint64_t NextFrameIndex() const { return mNextFrameIndex; }
|
|
||||||
const RenderCadenceMetrics& Metrics() const { return mMetrics; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint64_t SkippedTicksForLateness(Duration lateness) const;
|
|
||||||
static bool IsPositive(Duration duration);
|
|
||||||
|
|
||||||
Duration mTargetFrameDuration = std::chrono::milliseconds(16);
|
|
||||||
TimePoint mNextRenderTime;
|
|
||||||
uint64_t mNextFrameIndex = 0;
|
|
||||||
RenderCadencePolicy mPolicy;
|
|
||||||
RenderCadenceMetrics mMetrics;
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* RenderCadenceActionName(RenderCadenceAction action);
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
#include "RenderOutputQueue.h"
|
|
||||||
|
|
||||||
RenderOutputQueue::RenderOutputQueue(const VideoPlayoutPolicy& policy) :
|
|
||||||
mPolicy(NormalizeVideoPlayoutPolicy(policy))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderOutputQueue::Configure(const VideoPlayoutPolicy& policy)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
|
||||||
while (mReadyFrames.size() > CapacityLocked())
|
|
||||||
{
|
|
||||||
ReleaseFrame(mReadyFrames.front());
|
|
||||||
mReadyFrames.pop_front();
|
|
||||||
++mDroppedCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderOutputQueue::Push(RenderOutputFrame frame)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (mReadyFrames.size() >= CapacityLocked())
|
|
||||||
{
|
|
||||||
ReleaseFrame(mReadyFrames.front());
|
|
||||||
mReadyFrames.pop_front();
|
|
||||||
++mDroppedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
mReadyFrames.push_back(frame);
|
|
||||||
++mPushedCount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderOutputQueue::TryPop(RenderOutputFrame& frame)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (mReadyFrames.empty())
|
|
||||||
{
|
|
||||||
++mUnderrunCount;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame = mReadyFrames.front();
|
|
||||||
mReadyFrames.pop_front();
|
|
||||||
++mPoppedCount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderOutputQueue::DropOldestFrame()
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (mReadyFrames.empty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ReleaseFrame(mReadyFrames.front());
|
|
||||||
mReadyFrames.pop_front();
|
|
||||||
++mDroppedCount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderOutputQueue::Clear()
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
for (RenderOutputFrame& frame : mReadyFrames)
|
|
||||||
ReleaseFrame(frame);
|
|
||||||
mReadyFrames.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics RenderOutputQueue::GetMetrics() const
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
RenderOutputQueueMetrics metrics;
|
|
||||||
metrics.depth = mReadyFrames.size();
|
|
||||||
metrics.capacity = CapacityLocked();
|
|
||||||
metrics.pushedCount = mPushedCount;
|
|
||||||
metrics.poppedCount = mPoppedCount;
|
|
||||||
metrics.droppedCount = mDroppedCount;
|
|
||||||
metrics.underrunCount = mUnderrunCount;
|
|
||||||
return metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t RenderOutputQueue::CapacityLocked() const
|
|
||||||
{
|
|
||||||
return static_cast<std::size_t>(mPolicy.maxReadyFrames);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderOutputQueue::ReleaseFrame(RenderOutputFrame& frame)
|
|
||||||
{
|
|
||||||
if (frame.releaseFrame)
|
|
||||||
frame.releaseFrame(frame.frame);
|
|
||||||
frame.releaseFrame = {};
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
#include "VideoPlayoutPolicy.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <deque>
|
|
||||||
#include <functional>
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
struct RenderOutputFrame
|
|
||||||
{
|
|
||||||
VideoIOOutputFrame frame;
|
|
||||||
uint64_t frameIndex = 0;
|
|
||||||
bool stale = false;
|
|
||||||
std::function<void(VideoIOOutputFrame& frame)> releaseFrame;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderOutputQueueMetrics
|
|
||||||
{
|
|
||||||
std::size_t depth = 0;
|
|
||||||
std::size_t capacity = 0;
|
|
||||||
uint64_t pushedCount = 0;
|
|
||||||
uint64_t poppedCount = 0;
|
|
||||||
uint64_t droppedCount = 0;
|
|
||||||
uint64_t underrunCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RenderOutputQueue
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit RenderOutputQueue(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
|
|
||||||
|
|
||||||
void Configure(const VideoPlayoutPolicy& policy);
|
|
||||||
bool Push(RenderOutputFrame frame);
|
|
||||||
bool TryPop(RenderOutputFrame& frame);
|
|
||||||
bool DropOldestFrame();
|
|
||||||
void Clear();
|
|
||||||
RenderOutputQueueMetrics GetMetrics() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::size_t CapacityLocked() const;
|
|
||||||
static void ReleaseFrame(RenderOutputFrame& frame);
|
|
||||||
|
|
||||||
mutable std::mutex mMutex;
|
|
||||||
VideoPlayoutPolicy mPolicy;
|
|
||||||
std::deque<RenderOutputFrame> mReadyFrames;
|
|
||||||
uint64_t mPushedCount = 0;
|
|
||||||
uint64_t mPoppedCount = 0;
|
|
||||||
uint64_t mDroppedCount = 0;
|
|
||||||
uint64_t mUnderrunCount = 0;
|
|
||||||
};
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
#include "SystemOutputFramePool.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
SystemOutputFramePoolConfig NormalizeConfig(SystemOutputFramePoolConfig config)
|
|
||||||
{
|
|
||||||
if (config.rowBytes == 0)
|
|
||||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemOutputFramePool::SystemOutputFramePool(const SystemOutputFramePoolConfig& config)
|
|
||||||
{
|
|
||||||
Configure(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SystemOutputFramePool::Configure(const SystemOutputFramePoolConfig& config)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
mConfig = NormalizeConfig(config);
|
|
||||||
mReadySlots.clear();
|
|
||||||
mSlots.clear();
|
|
||||||
mSlots.resize(mConfig.capacity);
|
|
||||||
|
|
||||||
const std::size_t byteCount = FrameByteCount();
|
|
||||||
for (StoredSlot& slot : mSlots)
|
|
||||||
{
|
|
||||||
slot.bytes.resize(byteCount);
|
|
||||||
slot.state = OutputFrameSlotState::Free;
|
|
||||||
++slot.generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
mAcquireMissCount = 0;
|
|
||||||
mReadyUnderrunCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemOutputFramePoolConfig SystemOutputFramePool::Config() const
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
return mConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::AcquireFreeSlot(OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
|
||||||
{
|
|
||||||
if (mSlots[index].state != OutputFrameSlotState::Free)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
mSlots[index].state = OutputFrameSlotState::Rendering;
|
|
||||||
++mSlots[index].generation;
|
|
||||||
FillOutputSlotLocked(index, slot);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
slot = OutputFrameSlot();
|
|
||||||
++mAcquireMissCount;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::AcquireRenderingSlot(OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
return AcquireFreeSlot(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::PublishReadySlot(const OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (!TransitionSlotLocked(slot, OutputFrameSlotState::Rendering, OutputFrameSlotState::Completed))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mReadySlots.push_back(slot.index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::PublishCompletedSlot(const OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
return PublishReadySlot(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
while (!mReadySlots.empty())
|
|
||||||
{
|
|
||||||
const std::size_t index = mReadySlots.front();
|
|
||||||
mReadySlots.pop_front();
|
|
||||||
if (index >= mSlots.size() || mSlots[index].state != OutputFrameSlotState::Completed)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
FillOutputSlotLocked(index, slot);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
slot = OutputFrameSlot();
|
|
||||||
++mReadyUnderrunCount;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ConsumeCompletedSlot(OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
return ConsumeReadySlot(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::MarkScheduled(const OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (!IsValidSlotLocked(slot))
|
|
||||||
return false;
|
|
||||||
if (mSlots[slot.index].state != OutputFrameSlotState::Completed)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
RemoveReadyIndexLocked(slot.index);
|
|
||||||
mSlots[slot.index].state = OutputFrameSlotState::Scheduled;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::MarkScheduledByBuffer(void* bytes)
|
|
||||||
{
|
|
||||||
if (bytes == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
|
||||||
{
|
|
||||||
if (mSlots[index].bytes.empty() || mSlots[index].bytes.data() != bytes)
|
|
||||||
continue;
|
|
||||||
if (mSlots[index].state != OutputFrameSlotState::Completed)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
RemoveReadyIndexLocked(index);
|
|
||||||
mSlots[index].state = OutputFrameSlotState::Scheduled;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ReleaseSlot(const OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state == OutputFrameSlotState::Free)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return ReleaseSlotByIndexLocked(slot.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ReleaseScheduledSlot(const OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
return TransitionSlotLocked(slot, OutputFrameSlotState::Scheduled, OutputFrameSlotState::Free);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ReleaseSlotByBuffer(void* bytes)
|
|
||||||
{
|
|
||||||
if (bytes == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
|
||||||
{
|
|
||||||
if (!mSlots[index].bytes.empty() && mSlots[index].bytes.data() == bytes)
|
|
||||||
return ReleaseSlotByIndexLocked(index);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SystemOutputFramePool::Clear()
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
mReadySlots.clear();
|
|
||||||
for (StoredSlot& slot : mSlots)
|
|
||||||
{
|
|
||||||
slot.state = OutputFrameSlotState::Free;
|
|
||||||
++slot.generation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics SystemOutputFramePool::GetMetrics() const
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
SystemOutputFramePoolMetrics metrics;
|
|
||||||
metrics.capacity = mSlots.size();
|
|
||||||
metrics.readyCount = mReadySlots.size();
|
|
||||||
metrics.acquireMissCount = mAcquireMissCount;
|
|
||||||
metrics.readyUnderrunCount = mReadyUnderrunCount;
|
|
||||||
|
|
||||||
for (const StoredSlot& slot : mSlots)
|
|
||||||
{
|
|
||||||
switch (slot.state)
|
|
||||||
{
|
|
||||||
case OutputFrameSlotState::Free:
|
|
||||||
++metrics.freeCount;
|
|
||||||
break;
|
|
||||||
case OutputFrameSlotState::Rendering:
|
|
||||||
++metrics.renderingCount;
|
|
||||||
++metrics.acquiredCount;
|
|
||||||
break;
|
|
||||||
case OutputFrameSlotState::Completed:
|
|
||||||
++metrics.completedCount;
|
|
||||||
break;
|
|
||||||
case OutputFrameSlotState::Scheduled:
|
|
||||||
++metrics.scheduledCount;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::IsValidSlotLocked(const OutputFrameSlot& slot) const
|
|
||||||
{
|
|
||||||
return slot.index < mSlots.size() && mSlots[slot.index].generation == slot.generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState)
|
|
||||||
{
|
|
||||||
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state != expectedState)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mSlots[slot.index].state = nextState;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SystemOutputFramePool::FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot)
|
|
||||||
{
|
|
||||||
StoredSlot& storedSlot = mSlots[index];
|
|
||||||
slot.index = index;
|
|
||||||
slot.generation = storedSlot.generation;
|
|
||||||
slot.frame.bytes = storedSlot.bytes.empty() ? nullptr : storedSlot.bytes.data();
|
|
||||||
slot.frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
|
||||||
slot.frame.width = mConfig.width;
|
|
||||||
slot.frame.height = mConfig.height;
|
|
||||||
slot.frame.pixelFormat = mConfig.pixelFormat;
|
|
||||||
slot.frame.nativeFrame = nullptr;
|
|
||||||
slot.frame.nativeBuffer = slot.frame.bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SystemOutputFramePool::RemoveReadyIndexLocked(std::size_t index)
|
|
||||||
{
|
|
||||||
mReadySlots.erase(std::remove(mReadySlots.begin(), mReadySlots.end(), index), mReadySlots.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemOutputFramePool::ReleaseSlotByIndexLocked(std::size_t index)
|
|
||||||
{
|
|
||||||
if (index >= mSlots.size() || mSlots[index].state == OutputFrameSlotState::Free)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
RemoveReadyIndexLocked(index);
|
|
||||||
mSlots[index].state = OutputFrameSlotState::Free;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t SystemOutputFramePool::FrameByteCount() const
|
|
||||||
{
|
|
||||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <deque>
|
|
||||||
#include <mutex>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
enum class OutputFrameSlotState
|
|
||||||
{
|
|
||||||
Free,
|
|
||||||
Rendering,
|
|
||||||
Completed,
|
|
||||||
Scheduled
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SystemOutputFramePoolConfig
|
|
||||||
{
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
unsigned rowBytes = 0;
|
|
||||||
std::size_t capacity = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct OutputFrameSlot
|
|
||||||
{
|
|
||||||
VideoIOOutputFrame frame;
|
|
||||||
std::size_t index = 0;
|
|
||||||
uint64_t generation = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SystemOutputFramePoolMetrics
|
|
||||||
{
|
|
||||||
std::size_t capacity = 0;
|
|
||||||
std::size_t freeCount = 0;
|
|
||||||
std::size_t renderingCount = 0;
|
|
||||||
std::size_t completedCount = 0;
|
|
||||||
std::size_t scheduledCount = 0;
|
|
||||||
std::size_t acquiredCount = 0;
|
|
||||||
std::size_t readyCount = 0;
|
|
||||||
std::size_t consumedCount = 0;
|
|
||||||
uint64_t acquireMissCount = 0;
|
|
||||||
uint64_t readyUnderrunCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SystemOutputFramePool
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
SystemOutputFramePool() = default;
|
|
||||||
explicit SystemOutputFramePool(const SystemOutputFramePoolConfig& config);
|
|
||||||
|
|
||||||
void Configure(const SystemOutputFramePoolConfig& config);
|
|
||||||
SystemOutputFramePoolConfig Config() const;
|
|
||||||
|
|
||||||
bool AcquireFreeSlot(OutputFrameSlot& slot);
|
|
||||||
bool AcquireRenderingSlot(OutputFrameSlot& slot);
|
|
||||||
bool PublishReadySlot(const OutputFrameSlot& slot);
|
|
||||||
bool PublishCompletedSlot(const OutputFrameSlot& slot);
|
|
||||||
bool ConsumeReadySlot(OutputFrameSlot& slot);
|
|
||||||
bool ConsumeCompletedSlot(OutputFrameSlot& slot);
|
|
||||||
bool MarkScheduled(const OutputFrameSlot& slot);
|
|
||||||
bool MarkScheduledByBuffer(void* bytes);
|
|
||||||
bool ReleaseSlot(const OutputFrameSlot& slot);
|
|
||||||
bool ReleaseScheduledSlot(const OutputFrameSlot& slot);
|
|
||||||
bool ReleaseSlotByBuffer(void* bytes);
|
|
||||||
void Clear();
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics GetMetrics() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct StoredSlot
|
|
||||||
{
|
|
||||||
std::vector<unsigned char> bytes;
|
|
||||||
OutputFrameSlotState state = OutputFrameSlotState::Free;
|
|
||||||
uint64_t generation = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool IsValidSlotLocked(const OutputFrameSlot& slot) const;
|
|
||||||
bool TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState);
|
|
||||||
void FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot);
|
|
||||||
void RemoveReadyIndexLocked(std::size_t index);
|
|
||||||
bool ReleaseSlotByIndexLocked(std::size_t index);
|
|
||||||
std::size_t FrameByteCount() const;
|
|
||||||
|
|
||||||
mutable std::mutex mMutex;
|
|
||||||
SystemOutputFramePoolConfig mConfig;
|
|
||||||
std::vector<StoredSlot> mSlots;
|
|
||||||
std::deque<std::size_t> mReadySlots;
|
|
||||||
uint64_t mAcquireMissCount = 0;
|
|
||||||
uint64_t mReadyUnderrunCount = 0;
|
|
||||||
};
|
|
||||||
@@ -146,37 +146,6 @@ add_video_shader_test(VideoOutputThreadTests
|
|||||||
"${TEST_DIR}/VideoOutputThreadTests.cpp"
|
"${TEST_DIR}/VideoOutputThreadTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_video_shader_test(OutputProductionControllerTests
|
|
||||||
"${SRC_DIR}/video/playout/OutputProductionController.cpp"
|
|
||||||
"${TEST_DIR}/OutputProductionControllerTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_video_shader_test(RenderOutputQueueTests
|
|
||||||
"${SRC_DIR}/video/playout/RenderOutputQueue.cpp"
|
|
||||||
"${TEST_DIR}/RenderOutputQueueTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_video_shader_test(RenderCadenceControllerTests
|
|
||||||
"${SRC_DIR}/video/playout/RenderCadenceController.cpp"
|
|
||||||
"${TEST_DIR}/RenderCadenceControllerTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_video_shader_test(SystemOutputFramePoolTests
|
|
||||||
"${SRC_DIR}/video/playout/SystemOutputFramePool.cpp"
|
|
||||||
${VIDEO_FORMAT_SOURCES}
|
|
||||||
"${TEST_DIR}/SystemOutputFramePoolTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_video_shader_test(VideoBackendLifecycleTests
|
|
||||||
"${SRC_DIR}/video/legacy/VideoBackendLifecycle.cpp"
|
|
||||||
"${TEST_DIR}/VideoBackendLifecycleTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_video_shader_test(VideoIODeviceFakeTests
|
|
||||||
${VIDEO_FORMAT_SOURCES}
|
|
||||||
"${TEST_DIR}/VideoIODeviceFakeTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
set_tests_properties(RenderCadenceCompositorLoggerTests PROPERTIES
|
set_tests_properties(RenderCadenceCompositorLoggerTests PROPERTIES
|
||||||
ENVIRONMENT "VIDEO_SHADER_TEST_LOG_TO_CONSOLE=0"
|
ENVIRONMENT "VIDEO_SHADER_TEST_LOG_TO_CONSOLE=0"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
#include "OutputProductionController.h"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestLowQueueRequestsProductionToTarget()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 3;
|
|
||||||
policy.maxReadyFrames = 5;
|
|
||||||
OutputProductionController controller(policy);
|
|
||||||
|
|
||||||
OutputProductionPressure pressure;
|
|
||||||
pressure.readyQueueDepth = 1;
|
|
||||||
pressure.readyQueueCapacity = 5;
|
|
||||||
|
|
||||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Produce, "low ready queue requests production");
|
|
||||||
Expect(decision.requestedFrames == 2, "low ready queue requests enough frames to reach target");
|
|
||||||
Expect(decision.targetReadyFrames == 3, "decision reports effective target");
|
|
||||||
Expect(decision.maxReadyFrames == 5, "decision reports effective max");
|
|
||||||
Expect(decision.reason == "ready-queue-below-target", "low queue decision names reason");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestFullQueueThrottles()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 2;
|
|
||||||
policy.maxReadyFrames = 4;
|
|
||||||
OutputProductionController controller(policy);
|
|
||||||
|
|
||||||
OutputProductionPressure pressure;
|
|
||||||
pressure.readyQueueDepth = 4;
|
|
||||||
pressure.readyQueueCapacity = 4;
|
|
||||||
|
|
||||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Throttle, "full ready queue throttles production");
|
|
||||||
Expect(decision.requestedFrames == 0, "full ready queue requests no frames");
|
|
||||||
Expect(decision.reason == "ready-queue-full", "full queue decision names reason");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestAtTargetWaitsWithoutPressure()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 2;
|
|
||||||
policy.maxReadyFrames = 4;
|
|
||||||
OutputProductionController controller(policy);
|
|
||||||
|
|
||||||
OutputProductionPressure pressure;
|
|
||||||
pressure.readyQueueDepth = 2;
|
|
||||||
pressure.readyQueueCapacity = 4;
|
|
||||||
|
|
||||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Wait, "ready queue at target waits without pressure");
|
|
||||||
Expect(decision.requestedFrames == 0, "wait decision requests no frames");
|
|
||||||
Expect(decision.reason == "ready-queue-at-target", "wait decision names reason");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestLateDropPressureRequestsHeadroom()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 2;
|
|
||||||
policy.maxReadyFrames = 4;
|
|
||||||
OutputProductionController controller(policy);
|
|
||||||
|
|
||||||
OutputProductionPressure pressure;
|
|
||||||
pressure.readyQueueDepth = 2;
|
|
||||||
pressure.readyQueueCapacity = 4;
|
|
||||||
pressure.lateStreak = 1;
|
|
||||||
|
|
||||||
OutputProductionDecision decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Produce, "late pressure requests extra headroom");
|
|
||||||
Expect(decision.requestedFrames == 1, "late pressure requests one frame");
|
|
||||||
Expect(decision.reason == "playout-pressure", "late pressure decision names reason");
|
|
||||||
|
|
||||||
pressure.lateStreak = 0;
|
|
||||||
pressure.dropStreak = 2;
|
|
||||||
decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Produce, "drop pressure requests extra headroom");
|
|
||||||
|
|
||||||
pressure.dropStreak = 0;
|
|
||||||
pressure.readyQueueUnderrunCount = 1;
|
|
||||||
decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Produce, "underrun pressure requests extra headroom");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestPolicyNormalizesAndClampsToCapacity()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 0;
|
|
||||||
policy.maxReadyFrames = 8;
|
|
||||||
OutputProductionController controller(policy);
|
|
||||||
|
|
||||||
OutputProductionPressure pressure;
|
|
||||||
pressure.readyQueueDepth = 1;
|
|
||||||
pressure.readyQueueCapacity = 3;
|
|
||||||
|
|
||||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
|
||||||
Expect(decision.action == OutputProductionAction::Wait, "normalized target at current depth waits");
|
|
||||||
Expect(decision.targetReadyFrames == 1, "target normalizes to at least one frame");
|
|
||||||
Expect(decision.maxReadyFrames == 3, "max ready frames clamps to queue capacity");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestActionNames()
|
|
||||||
{
|
|
||||||
Expect(OutputProductionActionName(OutputProductionAction::Produce) == std::string("Produce"), "produce action has name");
|
|
||||||
Expect(OutputProductionActionName(OutputProductionAction::Wait) == std::string("Wait"), "wait action has name");
|
|
||||||
Expect(OutputProductionActionName(OutputProductionAction::Throttle) == std::string("Throttle"), "throttle action has name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
TestLowQueueRequestsProductionToTarget();
|
|
||||||
TestFullQueueThrottles();
|
|
||||||
TestAtTargetWaitsWithoutPressure();
|
|
||||||
TestLateDropPressureRequestsHeadroom();
|
|
||||||
TestPolicyNormalizesAndClampsToCapacity();
|
|
||||||
TestActionNames();
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " OutputProductionController test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "OutputProductionController tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#include "RenderCadenceController.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
using Clock = RenderCadenceController::Clock;
|
|
||||||
using Duration = RenderCadenceController::Duration;
|
|
||||||
using TimePoint = RenderCadenceController::TimePoint;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration Ms(int64_t value)
|
|
||||||
{
|
|
||||||
return std::chrono::duration_cast<Duration>(std::chrono::milliseconds(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestExactCadenceAdvancesFrameIndexAndNextTick()
|
|
||||||
{
|
|
||||||
RenderCadenceController controller;
|
|
||||||
const TimePoint start = Clock::time_point(Ms(1000));
|
|
||||||
controller.Configure(Ms(20), start);
|
|
||||||
|
|
||||||
RenderCadenceDecision first = controller.Tick(start);
|
|
||||||
Expect(first.action == RenderCadenceAction::Render, "first exact tick renders");
|
|
||||||
Expect(first.frameIndex == 0, "first exact tick renders frame zero");
|
|
||||||
Expect(first.renderTargetTime == start, "first exact target is configured start");
|
|
||||||
Expect(first.nextRenderTime == start + Ms(20), "first exact tick advances next render time");
|
|
||||||
Expect(first.skippedTicks == 0, "first exact tick skips no ticks");
|
|
||||||
Expect(first.lateness == Duration::zero(), "first exact tick records no lateness");
|
|
||||||
|
|
||||||
RenderCadenceDecision second = controller.Tick(start + Ms(20));
|
|
||||||
Expect(second.action == RenderCadenceAction::Render, "second exact tick renders");
|
|
||||||
Expect(second.frameIndex == 1, "second exact tick renders frame one");
|
|
||||||
Expect(controller.NextFrameIndex() == 2, "controller tracks next frame index after exact ticks");
|
|
||||||
Expect(controller.Metrics().renderedFrameCount == 2, "metrics count exact rendered frames");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestEarlyTickWaitsWithoutAdvancing()
|
|
||||||
{
|
|
||||||
RenderCadenceController controller;
|
|
||||||
const TimePoint start = Clock::time_point(Ms(0));
|
|
||||||
controller.Configure(Ms(20), start);
|
|
||||||
(void)controller.Tick(start);
|
|
||||||
|
|
||||||
RenderCadenceDecision decision = controller.Tick(start + Ms(10));
|
|
||||||
Expect(decision.action == RenderCadenceAction::Wait, "early tick waits");
|
|
||||||
Expect(decision.waitDuration == Ms(10), "early tick reports wait duration");
|
|
||||||
Expect(decision.frameIndex == 1, "early tick reports next pending frame");
|
|
||||||
Expect(controller.NextFrameIndex() == 1, "early tick does not advance frame index");
|
|
||||||
Expect(controller.NextRenderTime() == start + Ms(20), "early tick does not advance next render time");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestSlightLatenessRendersAndRecordsMetrics()
|
|
||||||
{
|
|
||||||
RenderCadencePolicy policy;
|
|
||||||
policy.skipThresholdFrames = 3.0;
|
|
||||||
|
|
||||||
RenderCadenceController controller;
|
|
||||||
const TimePoint start = Clock::time_point(Ms(0));
|
|
||||||
controller.Configure(Ms(20), start, policy);
|
|
||||||
|
|
||||||
RenderCadenceDecision decision = controller.Tick(start + Ms(5));
|
|
||||||
Expect(decision.action == RenderCadenceAction::Render, "slightly late tick renders");
|
|
||||||
Expect(decision.frameIndex == 0, "slightly late tick keeps pending frame");
|
|
||||||
Expect(decision.skippedTicks == 0, "slightly late tick skips no ticks");
|
|
||||||
Expect(decision.lateness == Ms(5), "slightly late tick reports lateness");
|
|
||||||
Expect(controller.Metrics().lateFrameCount == 1, "metrics count late rendered frame");
|
|
||||||
Expect(controller.Metrics().lastLateness == Ms(5), "metrics keep last lateness");
|
|
||||||
Expect(controller.Metrics().maxLateness == Ms(5), "metrics keep max lateness");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestLargeLatenessSkipsTicksAccordingToPolicy()
|
|
||||||
{
|
|
||||||
RenderCadencePolicy policy;
|
|
||||||
policy.skipLateTicks = true;
|
|
||||||
policy.skipThresholdFrames = 2.0;
|
|
||||||
policy.maxSkippedTicksPerDecision = 8;
|
|
||||||
|
|
||||||
RenderCadenceController controller;
|
|
||||||
const TimePoint start = Clock::time_point(Ms(0));
|
|
||||||
controller.Configure(Ms(20), start, policy);
|
|
||||||
|
|
||||||
RenderCadenceDecision decision = controller.Tick(start + Ms(70));
|
|
||||||
Expect(decision.action == RenderCadenceAction::Render, "large late tick renders newest allowed frame");
|
|
||||||
Expect(decision.skippedTicks == 3, "large late tick skips elapsed render ticks");
|
|
||||||
Expect(decision.frameIndex == 3, "large late tick renders skipped-to frame");
|
|
||||||
Expect(decision.renderTargetTime == start + Ms(60), "large late tick targets newest elapsed tick");
|
|
||||||
Expect(decision.lateness == Ms(10), "large late tick measures residual lateness");
|
|
||||||
Expect(controller.NextFrameIndex() == 4, "large late tick advances past rendered frame");
|
|
||||||
Expect(controller.NextRenderTime() == start + Ms(80), "large late tick advances to following cadence");
|
|
||||||
Expect(controller.Metrics().skippedTickCount == 3, "metrics count skipped ticks");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestSkipPolicyCanDisableOrCapSkippedTicks()
|
|
||||||
{
|
|
||||||
const TimePoint start = Clock::time_point(Ms(0));
|
|
||||||
|
|
||||||
RenderCadencePolicy disabledPolicy;
|
|
||||||
disabledPolicy.skipLateTicks = false;
|
|
||||||
RenderCadenceController disabledController;
|
|
||||||
disabledController.Configure(Ms(20), start, disabledPolicy);
|
|
||||||
RenderCadenceDecision disabled = disabledController.Tick(start + Ms(90));
|
|
||||||
Expect(disabled.skippedTicks == 0, "disabled skip policy renders pending frame");
|
|
||||||
Expect(disabled.frameIndex == 0, "disabled skip policy preserves pending frame index");
|
|
||||||
|
|
||||||
RenderCadencePolicy cappedPolicy;
|
|
||||||
cappedPolicy.skipThresholdFrames = 1.0;
|
|
||||||
cappedPolicy.maxSkippedTicksPerDecision = 2;
|
|
||||||
RenderCadenceController cappedController;
|
|
||||||
cappedController.Configure(Ms(20), start, cappedPolicy);
|
|
||||||
RenderCadenceDecision capped = cappedController.Tick(start + Ms(90));
|
|
||||||
Expect(capped.skippedTicks == 2, "skip policy caps skipped ticks");
|
|
||||||
Expect(capped.frameIndex == 2, "capped skip renders capped frame index");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestResetRestartsCadenceAndMetrics()
|
|
||||||
{
|
|
||||||
RenderCadenceController controller;
|
|
||||||
const TimePoint start = Clock::time_point(Ms(0));
|
|
||||||
controller.Configure(Ms(20), start);
|
|
||||||
(void)controller.Tick(start + Ms(50));
|
|
||||||
|
|
||||||
const TimePoint restarted = start + Ms(200);
|
|
||||||
controller.Reset(restarted);
|
|
||||||
|
|
||||||
Expect(controller.NextFrameIndex() == 0, "reset restarts frame index");
|
|
||||||
Expect(controller.NextRenderTime() == restarted, "reset restarts next render time");
|
|
||||||
Expect(controller.Metrics().renderedFrameCount == 0, "reset clears rendered metrics");
|
|
||||||
|
|
||||||
RenderCadenceDecision decision = controller.Tick(restarted);
|
|
||||||
Expect(decision.action == RenderCadenceAction::Render, "reset cadence renders at new start");
|
|
||||||
Expect(decision.frameIndex == 0, "reset cadence renders frame zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestActionNames()
|
|
||||||
{
|
|
||||||
Expect(RenderCadenceActionName(RenderCadenceAction::Render) == std::string("Render"), "render action has name");
|
|
||||||
Expect(RenderCadenceActionName(RenderCadenceAction::Wait) == std::string("Wait"), "wait action has name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
TestExactCadenceAdvancesFrameIndexAndNextTick();
|
|
||||||
TestEarlyTickWaitsWithoutAdvancing();
|
|
||||||
TestSlightLatenessRendersAndRecordsMetrics();
|
|
||||||
TestLargeLatenessSkipsTicksAccordingToPolicy();
|
|
||||||
TestSkipPolicyCanDisableOrCapSkippedTicks();
|
|
||||||
TestResetRestartsCadenceAndMetrics();
|
|
||||||
TestActionNames();
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " RenderCadenceController test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "RenderCadenceController tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
#include "RenderOutputQueue.h"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
int gReleasedFrames = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderOutputFrame MakeFrame(uint64_t index)
|
|
||||||
{
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
frame.frameIndex = index;
|
|
||||||
frame.frame.nativeFrame = reinterpret_cast<void*>(static_cast<uintptr_t>(index + 1));
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CountReleasedFrame(VideoIOOutputFrame& frame)
|
|
||||||
{
|
|
||||||
if (frame.nativeFrame != nullptr)
|
|
||||||
{
|
|
||||||
++gReleasedFrames;
|
|
||||||
frame.nativeFrame = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderOutputFrame MakeOwnedFrame(uint64_t index)
|
|
||||||
{
|
|
||||||
RenderOutputFrame frame = MakeFrame(index);
|
|
||||||
frame.releaseFrame = CountReleasedFrame;
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestQueuePreservesOrdering()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.maxReadyFrames = 3;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
|
|
||||||
Expect(queue.Push(MakeFrame(1)), "first ready frame pushes");
|
|
||||||
Expect(queue.Push(MakeFrame(2)), "second ready frame pushes");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "first ready frame pops");
|
|
||||||
Expect(frame.frameIndex == 1, "queue pops first frame first");
|
|
||||||
Expect(queue.TryPop(frame), "second ready frame pops");
|
|
||||||
Expect(frame.frameIndex == 2, "queue pops second frame second");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestBoundedQueueDropsOldestFrame()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.maxReadyFrames = 2;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
|
|
||||||
queue.Push(MakeFrame(1));
|
|
||||||
queue.Push(MakeFrame(2));
|
|
||||||
queue.Push(MakeFrame(3));
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
|
||||||
Expect(metrics.depth == 2, "bounded queue depth stays at capacity");
|
|
||||||
Expect(metrics.droppedCount == 1, "bounded queue counts dropped oldest frame");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "bounded queue pops after drop");
|
|
||||||
Expect(frame.frameIndex == 2, "oldest frame was dropped when queue overflowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestOverflowReleasesDroppedFrame()
|
|
||||||
{
|
|
||||||
gReleasedFrames = 0;
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.targetReadyFrames = 1;
|
|
||||||
policy.maxReadyFrames = 1;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
|
|
||||||
queue.Push(MakeOwnedFrame(1));
|
|
||||||
queue.Push(MakeOwnedFrame(2));
|
|
||||||
|
|
||||||
Expect(gReleasedFrames == 1, "overflow releases dropped ready frame");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "newest owned frame remains queued");
|
|
||||||
Expect(frame.frameIndex == 2, "overflow keeps newest owned frame");
|
|
||||||
Expect(gReleasedFrames == 1, "pop transfers ownership without releasing");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestDropOldestFrameReleasesFrame()
|
|
||||||
{
|
|
||||||
gReleasedFrames = 0;
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.maxReadyFrames = 2;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
|
|
||||||
queue.Push(MakeOwnedFrame(1));
|
|
||||||
queue.Push(MakeOwnedFrame(2));
|
|
||||||
|
|
||||||
Expect(queue.DropOldestFrame(), "oldest ready frame can be explicitly dropped");
|
|
||||||
Expect(gReleasedFrames == 1, "explicit drop releases oldest frame");
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
|
||||||
Expect(metrics.depth == 1, "explicit drop reduces queue depth");
|
|
||||||
Expect(metrics.droppedCount == 1, "explicit drop increments dropped count");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "newest frame remains after explicit drop");
|
|
||||||
Expect(frame.frameIndex == 2, "explicit drop keeps newest frame");
|
|
||||||
Expect(!queue.DropOldestFrame(), "empty queue cannot drop a frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestUnderrunIsCounted()
|
|
||||||
{
|
|
||||||
RenderOutputQueue queue;
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(!queue.TryPop(frame), "empty queue reports underrun");
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
|
||||||
Expect(metrics.underrunCount == 1, "empty pop increments underrun count");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestConfigureShrinksDepthToNewCapacity()
|
|
||||||
{
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.maxReadyFrames = 4;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
queue.Push(MakeFrame(1));
|
|
||||||
queue.Push(MakeFrame(2));
|
|
||||||
queue.Push(MakeFrame(3));
|
|
||||||
|
|
||||||
VideoPlayoutPolicy smallerPolicy;
|
|
||||||
smallerPolicy.targetReadyFrames = 1;
|
|
||||||
smallerPolicy.maxReadyFrames = 1;
|
|
||||||
queue.Configure(smallerPolicy);
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
|
||||||
Expect(metrics.depth == 1, "configure trims queue to new capacity");
|
|
||||||
Expect(metrics.droppedCount == 2, "configure counts trimmed frames as drops");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "trimmed queue still has newest frame");
|
|
||||||
Expect(frame.frameIndex == 3, "configure keeps newest ready frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestConfigureReleasesTrimmedFrames()
|
|
||||||
{
|
|
||||||
gReleasedFrames = 0;
|
|
||||||
VideoPlayoutPolicy policy;
|
|
||||||
policy.maxReadyFrames = 3;
|
|
||||||
RenderOutputQueue queue(policy);
|
|
||||||
queue.Push(MakeOwnedFrame(1));
|
|
||||||
queue.Push(MakeOwnedFrame(2));
|
|
||||||
queue.Push(MakeOwnedFrame(3));
|
|
||||||
|
|
||||||
VideoPlayoutPolicy smallerPolicy;
|
|
||||||
smallerPolicy.targetReadyFrames = 1;
|
|
||||||
smallerPolicy.maxReadyFrames = 1;
|
|
||||||
queue.Configure(smallerPolicy);
|
|
||||||
|
|
||||||
Expect(gReleasedFrames == 2, "configure releases trimmed ready frames");
|
|
||||||
|
|
||||||
RenderOutputFrame frame;
|
|
||||||
Expect(queue.TryPop(frame), "trimmed owned queue still has newest frame");
|
|
||||||
Expect(frame.frameIndex == 3, "configure keeps newest owned frame after release");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestClearReleasesQueuedFrames()
|
|
||||||
{
|
|
||||||
gReleasedFrames = 0;
|
|
||||||
RenderOutputQueue queue;
|
|
||||||
queue.Push(MakeOwnedFrame(1));
|
|
||||||
queue.Push(MakeOwnedFrame(2));
|
|
||||||
|
|
||||||
queue.Clear();
|
|
||||||
|
|
||||||
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
|
||||||
Expect(metrics.depth == 0, "clear empties ready queue");
|
|
||||||
Expect(gReleasedFrames == 2, "clear releases queued ready frames");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
TestQueuePreservesOrdering();
|
|
||||||
TestBoundedQueueDropsOldestFrame();
|
|
||||||
TestOverflowReleasesDroppedFrame();
|
|
||||||
TestDropOldestFrameReleasesFrame();
|
|
||||||
TestUnderrunIsCounted();
|
|
||||||
TestConfigureShrinksDepthToNewCapacity();
|
|
||||||
TestConfigureReleasesTrimmedFrames();
|
|
||||||
TestClearReleasesQueuedFrames();
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " render output queue test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "RenderOutputQueue tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
#include "SystemOutputFramePool.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemOutputFramePoolConfig MakeConfig(std::size_t capacity = 2)
|
|
||||||
{
|
|
||||||
SystemOutputFramePoolConfig config;
|
|
||||||
config.width = 4;
|
|
||||||
config.height = 3;
|
|
||||||
config.pixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
config.capacity = capacity;
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestAcquireHonorsCapacityAndFrameShape()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(2));
|
|
||||||
|
|
||||||
OutputFrameSlot first;
|
|
||||||
OutputFrameSlot second;
|
|
||||||
OutputFrameSlot third;
|
|
||||||
Expect(pool.AcquireFreeSlot(first), "first slot can be acquired");
|
|
||||||
Expect(pool.AcquireFreeSlot(second), "second slot can be acquired");
|
|
||||||
Expect(!pool.AcquireFreeSlot(third), "fixed capacity rejects third acquire");
|
|
||||||
|
|
||||||
Expect(first.frame.bytes != nullptr, "acquired slot has system memory");
|
|
||||||
Expect(first.frame.nativeBuffer == first.frame.bytes, "native buffer points at system memory");
|
|
||||||
Expect(first.frame.nativeFrame == nullptr, "system frame has no native frame");
|
|
||||||
Expect(first.frame.width == 4, "frame width is configured");
|
|
||||||
Expect(first.frame.height == 3, "frame height is configured");
|
|
||||||
Expect(first.frame.rowBytes == 16, "BGRA8 row bytes are inferred");
|
|
||||||
Expect(first.frame.pixelFormat == VideoIOPixelFormat::Bgra8, "BGRA8 is the default output format");
|
|
||||||
Expect(first.frame.bytes != second.frame.bytes, "each slot owns distinct memory");
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 0, "all slots are in use");
|
|
||||||
Expect(metrics.renderingCount == 2, "rendering slots are counted");
|
|
||||||
Expect(metrics.acquiredCount == 2, "acquired slots are counted");
|
|
||||||
Expect(metrics.acquireMissCount == 1, "capacity miss is counted");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestPhase77StateContract()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 1, "new pool starts with one free slot");
|
|
||||||
Expect(metrics.renderingCount == 0, "new pool starts with no rendering slots");
|
|
||||||
Expect(metrics.completedCount == 0, "new pool starts with no completed slots");
|
|
||||||
Expect(metrics.scheduledCount == 0, "new pool starts with no scheduled slots");
|
|
||||||
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
Expect(pool.AcquireRenderingSlot(slot), "free slot moves to rendering");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 0, "rendering slot leaves free pool");
|
|
||||||
Expect(metrics.renderingCount == 1, "rendering slot is counted");
|
|
||||||
|
|
||||||
Expect(pool.PublishCompletedSlot(slot), "rendering slot moves to completed");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.renderingCount == 0, "completed slot leaves rendering");
|
|
||||||
Expect(metrics.completedCount == 1, "completed slot is counted");
|
|
||||||
Expect(metrics.readyCount == 1, "completed slot is available to scheduler");
|
|
||||||
|
|
||||||
OutputFrameSlot completed;
|
|
||||||
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued for scheduling");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.completedCount == 1, "dequeued completed slot remains completed until scheduled");
|
|
||||||
Expect(metrics.readyCount == 0, "dequeued completed slot leaves ready queue");
|
|
||||||
|
|
||||||
Expect(pool.MarkScheduled(completed), "completed slot moves to scheduled");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.completedCount == 0, "scheduled slot leaves completed state");
|
|
||||||
Expect(metrics.scheduledCount == 1, "scheduled slot is counted");
|
|
||||||
|
|
||||||
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot returns to free");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 1, "released scheduled slot returns to free");
|
|
||||||
Expect(metrics.scheduledCount == 0, "released scheduled slot leaves scheduled state");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestReadySlotsAreConsumedFifo()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(2));
|
|
||||||
|
|
||||||
OutputFrameSlot first;
|
|
||||||
OutputFrameSlot second;
|
|
||||||
Expect(pool.AcquireFreeSlot(first), "first FIFO slot can be acquired");
|
|
||||||
Expect(pool.AcquireFreeSlot(second), "second FIFO slot can be acquired");
|
|
||||||
Expect(pool.PublishReadySlot(first), "first FIFO slot can be published");
|
|
||||||
Expect(pool.PublishReadySlot(second), "second FIFO slot can be published");
|
|
||||||
|
|
||||||
OutputFrameSlot consumed;
|
|
||||||
Expect(pool.ConsumeReadySlot(consumed), "first ready slot can be consumed");
|
|
||||||
Expect(consumed.index == first.index, "first published slot is consumed first");
|
|
||||||
Expect(pool.MarkScheduled(consumed), "consumed slot can be marked scheduled");
|
|
||||||
Expect(pool.ReleaseScheduledSlot(consumed), "scheduled slot can be released");
|
|
||||||
|
|
||||||
Expect(pool.ConsumeReadySlot(consumed), "second ready slot can be consumed");
|
|
||||||
Expect(consumed.index == second.index, "second published slot is consumed second");
|
|
||||||
Expect(pool.ReleaseSlot(consumed), "consumed slot can be released without scheduling");
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 2, "released slots return to free pool");
|
|
||||||
Expect(metrics.readyCount == 0, "ready queue is empty after consumption");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestCompletedSlotCannotBeAcquiredUntilReleased()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
|
||||||
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
OutputFrameSlot extra;
|
|
||||||
Expect(pool.AcquireRenderingSlot(slot), "single slot can be acquired for rendering");
|
|
||||||
Expect(pool.PublishCompletedSlot(slot), "single slot can be published completed");
|
|
||||||
Expect(!pool.AcquireRenderingSlot(extra), "completed slot is not available for rendering");
|
|
||||||
|
|
||||||
OutputFrameSlot completed;
|
|
||||||
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued");
|
|
||||||
Expect(!pool.AcquireRenderingSlot(extra), "dequeued completed slot is still not free");
|
|
||||||
Expect(pool.MarkScheduled(completed), "dequeued completed slot can be scheduled");
|
|
||||||
Expect(!pool.AcquireRenderingSlot(extra), "scheduled slot is still not free");
|
|
||||||
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot can be released");
|
|
||||||
Expect(pool.AcquireRenderingSlot(extra), "released slot can be acquired again");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestReadySlotCanBeScheduledByBuffer()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
|
||||||
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
Expect(pool.AcquireFreeSlot(slot), "buffer schedule slot can be acquired");
|
|
||||||
void* bytes = slot.frame.bytes;
|
|
||||||
Expect(pool.PublishReadySlot(slot), "buffer schedule slot can be published");
|
|
||||||
Expect(pool.MarkScheduledByBuffer(bytes), "ready slot can be marked scheduled by buffer");
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.readyCount == 0, "scheduled-by-buffer removes slot from ready queue");
|
|
||||||
Expect(metrics.scheduledCount == 1, "scheduled-by-buffer counts scheduled slot");
|
|
||||||
|
|
||||||
Expect(pool.ReleaseSlotByBuffer(bytes), "scheduled slot can be released by buffer");
|
|
||||||
metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.freeCount == 1, "released-by-buffer slot returns to free pool");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestInvalidTransitionsAreRejected()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
|
||||||
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
Expect(pool.AcquireFreeSlot(slot), "transition slot can be acquired");
|
|
||||||
Expect(!pool.MarkScheduled(slot), "acquired slot cannot be marked scheduled");
|
|
||||||
Expect(pool.PublishReadySlot(slot), "acquired slot can be published");
|
|
||||||
Expect(!pool.PublishReadySlot(slot), "ready slot cannot be published twice");
|
|
||||||
Expect(pool.ReleaseSlot(slot), "ready slot can be released to free");
|
|
||||||
Expect(!pool.ReleaseSlot(slot), "free slot cannot be released again");
|
|
||||||
|
|
||||||
OutputFrameSlot next;
|
|
||||||
Expect(pool.AcquireFreeSlot(next), "slot can be reacquired after release");
|
|
||||||
Expect(next.index == slot.index, "same storage slot can be reused");
|
|
||||||
Expect(next.generation != slot.generation, "stale handles are invalidated on reacquire");
|
|
||||||
Expect(!pool.PublishReadySlot(slot), "stale handle cannot publish reacquired slot");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestPixelFormatAwareSizing()
|
|
||||||
{
|
|
||||||
SystemOutputFramePoolConfig config;
|
|
||||||
config.width = 7;
|
|
||||||
config.height = 2;
|
|
||||||
config.pixelFormat = VideoIOPixelFormat::V210;
|
|
||||||
config.capacity = 1;
|
|
||||||
|
|
||||||
SystemOutputFramePool pool(config);
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
Expect(pool.AcquireFreeSlot(slot), "v210 slot can be acquired");
|
|
||||||
Expect(slot.frame.pixelFormat == VideoIOPixelFormat::V210, "slot keeps configured pixel format");
|
|
||||||
Expect(slot.frame.rowBytes == static_cast<long>(MinimumV210RowBytes(config.width)), "v210 row bytes are inferred");
|
|
||||||
|
|
||||||
SystemOutputFramePoolConfig explicitConfig = config;
|
|
||||||
explicitConfig.pixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
explicitConfig.rowBytes = 64;
|
|
||||||
pool.Configure(explicitConfig);
|
|
||||||
Expect(pool.AcquireFreeSlot(slot), "explicit row-byte slot can be acquired");
|
|
||||||
Expect(slot.frame.pixelFormat == VideoIOPixelFormat::Uyvy8, "slot keeps reconfigured pixel format");
|
|
||||||
Expect(slot.frame.rowBytes == 64, "explicit row bytes are preserved");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestEmptyReadyQueueUnderrunIsCounted()
|
|
||||||
{
|
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
|
||||||
OutputFrameSlot slot;
|
|
||||||
Expect(!pool.ConsumeReadySlot(slot), "empty ready queue cannot be consumed");
|
|
||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
|
||||||
Expect(metrics.readyUnderrunCount == 1, "ready underrun is counted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
TestAcquireHonorsCapacityAndFrameShape();
|
|
||||||
TestPhase77StateContract();
|
|
||||||
TestReadySlotsAreConsumedFifo();
|
|
||||||
TestCompletedSlotCannotBeAcquiredUntilReleased();
|
|
||||||
TestReadySlotCanBeScheduledByBuffer();
|
|
||||||
TestInvalidTransitionsAreRejected();
|
|
||||||
TestPixelFormatAwareSizing();
|
|
||||||
TestEmptyReadyQueueUnderrunIsCounted();
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " system output frame pool test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "SystemOutputFramePool tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#include "VideoBackendLifecycle.h"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestAllowedLifecycleTransitions()
|
|
||||||
{
|
|
||||||
VideoBackendLifecycle lifecycle;
|
|
||||||
Expect(lifecycle.State() == VideoBackendLifecycleState::Uninitialized, "lifecycle starts uninitialized");
|
|
||||||
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Discovering, "discover").accepted,
|
|
||||||
"uninitialized can transition to discovering");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Discovered, "discovered").accepted,
|
|
||||||
"discovering can transition to discovered");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Configuring, "configuring").accepted,
|
|
||||||
"discovered can transition to configuring");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Configured, "configured").accepted,
|
|
||||||
"configuring can transition to configured");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Prerolling, "preroll").accepted,
|
|
||||||
"configured can transition to prerolling");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Running, "running").accepted,
|
|
||||||
"prerolling can transition to running");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Degraded, "degraded").accepted,
|
|
||||||
"running can transition to degraded");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Running, "recovered").accepted,
|
|
||||||
"degraded can transition back to running");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Stopping, "stopping").accepted,
|
|
||||||
"running can transition to stopping");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Stopped, "stopped").accepted,
|
|
||||||
"stopping can transition to stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestInvalidLifecycleTransitionIsRejected()
|
|
||||||
{
|
|
||||||
VideoBackendLifecycle lifecycle;
|
|
||||||
const VideoBackendLifecycleTransition transition =
|
|
||||||
lifecycle.TransitionTo(VideoBackendLifecycleState::Running, "skip setup");
|
|
||||||
Expect(!transition.accepted, "uninitialized cannot transition directly to running");
|
|
||||||
Expect(lifecycle.State() == VideoBackendLifecycleState::Uninitialized, "invalid transition leaves state unchanged");
|
|
||||||
Expect(transition.errorMessage.find("Invalid video backend lifecycle transition") != std::string::npos,
|
|
||||||
"invalid transition reports an error");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestFailureStateRecordsReasonAndCanRecover()
|
|
||||||
{
|
|
||||||
VideoBackendLifecycle lifecycle;
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Discovering, "discover").accepted,
|
|
||||||
"lifecycle can start discovery");
|
|
||||||
Expect(lifecycle.Fail("no device").accepted, "discovering can transition to failed");
|
|
||||||
Expect(lifecycle.State() == VideoBackendLifecycleState::Failed, "failure transition sets failed state");
|
|
||||||
Expect(lifecycle.FailureReason() == "no device", "failure reason is retained");
|
|
||||||
Expect(lifecycle.TransitionTo(VideoBackendLifecycleState::Discovering, "retry").accepted,
|
|
||||||
"failed lifecycle can retry discovery");
|
|
||||||
Expect(lifecycle.FailureReason().empty(), "successful non-failed transition clears failure reason");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestStateNamesAreStable()
|
|
||||||
{
|
|
||||||
Expect(std::string(VideoBackendLifecycle::StateName(VideoBackendLifecycleState::Uninitialized)) == "uninitialized",
|
|
||||||
"uninitialized state name is stable");
|
|
||||||
Expect(std::string(VideoBackendLifecycle::StateName(VideoBackendLifecycleState::Running)) == "running",
|
|
||||||
"running state name is stable");
|
|
||||||
Expect(std::string(VideoBackendLifecycle::StateName(VideoBackendLifecycleState::Failed)) == "failed",
|
|
||||||
"failed state name is stable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
TestAllowedLifecycleTransitions();
|
|
||||||
TestInvalidLifecycleTransitionIsRejected();
|
|
||||||
TestFailureStateRecordsReasonAndCanRecover();
|
|
||||||
TestStateNamesAreStable();
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " video backend lifecycle test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "VideoBackendLifecycle tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeVideoIODevice : public VideoIODevice
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void ReleaseResources() override {}
|
|
||||||
|
|
||||||
bool DiscoverDevicesAndModes(const VideoFormatSelection&, std::string&) override
|
|
||||||
{
|
|
||||||
mState.inputFrameSize = { 1920, 1080 };
|
|
||||||
mState.outputFrameSize = { 1920, 1080 };
|
|
||||||
mState.inputDisplayModeName = "fake 1080p";
|
|
||||||
mState.outputModelName = "Fake Video IO";
|
|
||||||
mState.hasInputDevice = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SelectPreferredFormats(const VideoFormatSelection&, bool, std::string&) override
|
|
||||||
{
|
|
||||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
mState.outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
mState.inputFrameRowBytes = VideoIORowBytes(mState.inputPixelFormat, mState.inputFrameSize.width);
|
|
||||||
mState.outputFrameRowBytes = VideoIORowBytes(mState.outputPixelFormat, mState.outputFrameSize.width);
|
|
||||||
mState.captureTextureWidth = PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes);
|
|
||||||
mState.outputPackTextureWidth = mState.outputFrameSize.width;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ConfigureInput(InputFrameCallback callback, const VideoFormat&, std::string&) override
|
|
||||||
{
|
|
||||||
mInputCallback = callback;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat&, bool, std::string&) override
|
|
||||||
{
|
|
||||||
mOutputCallback = callback;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PrepareOutputSchedule() override
|
|
||||||
{
|
|
||||||
mPreparedOutputSchedule = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StartInputStreams() override
|
|
||||||
{
|
|
||||||
mInputStreamsStarted = true;
|
|
||||||
mState.hasInputSource = true;
|
|
||||||
VideoIOFrame input;
|
|
||||||
input.bytes = mInputBytes.data();
|
|
||||||
input.rowBytes = static_cast<long>(mState.inputFrameRowBytes);
|
|
||||||
input.width = mState.inputFrameSize.width;
|
|
||||||
input.height = mState.inputFrameSize.height;
|
|
||||||
input.pixelFormat = mState.inputPixelFormat;
|
|
||||||
if (mInputCallback)
|
|
||||||
mInputCallback(input);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StartScheduledPlayback() override
|
|
||||||
{
|
|
||||||
mScheduledPlaybackStarted = true;
|
|
||||||
if (mOutputCallback)
|
|
||||||
mOutputCallback(VideoIOCompletion{ VideoIOCompletionResult::Completed });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Start() override
|
|
||||||
{
|
|
||||||
return PrepareOutputSchedule() && StartInputStreams() && StartScheduledPlayback();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Stop() override { return true; }
|
|
||||||
const VideoIOState& State() const override { return mState; }
|
|
||||||
VideoIOState& MutableState() override { return mState; }
|
|
||||||
|
|
||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame) override
|
|
||||||
{
|
|
||||||
frame.bytes = mOutputBytes.data();
|
|
||||||
frame.rowBytes = static_cast<long>(mState.outputFrameRowBytes);
|
|
||||||
frame.width = mState.outputFrameSize.width;
|
|
||||||
frame.height = mState.outputFrameSize.height;
|
|
||||||
frame.pixelFormat = mState.outputPixelFormat;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EndOutputFrame(VideoIOOutputFrame&) override {}
|
|
||||||
|
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame&) override
|
|
||||||
{
|
|
||||||
++mScheduledFrames;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth) override
|
|
||||||
{
|
|
||||||
mLastCompletion = result;
|
|
||||||
mLastReadyQueueDepth = readyQueueDepth;
|
|
||||||
VideoPlayoutRecoveryDecision decision;
|
|
||||||
decision.result = result;
|
|
||||||
decision.readyQueueDepth = readyQueueDepth;
|
|
||||||
return decision;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned ScheduledFrames() const { return mScheduledFrames; }
|
|
||||||
bool PreparedOutputSchedule() const { return mPreparedOutputSchedule; }
|
|
||||||
bool InputStreamsStarted() const { return mInputStreamsStarted; }
|
|
||||||
bool ScheduledPlaybackStarted() const { return mScheduledPlaybackStarted; }
|
|
||||||
VideoIOCompletionResult LastCompletion() const { return mLastCompletion; }
|
|
||||||
uint64_t LastReadyQueueDepth() const { return mLastReadyQueueDepth; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
VideoIOState mState;
|
|
||||||
InputFrameCallback mInputCallback;
|
|
||||||
OutputFrameCallback mOutputCallback;
|
|
||||||
std::array<unsigned char, 3840> mInputBytes = {};
|
|
||||||
std::array<unsigned char, 7680> mOutputBytes = {};
|
|
||||||
unsigned mScheduledFrames = 0;
|
|
||||||
bool mPreparedOutputSchedule = false;
|
|
||||||
bool mInputStreamsStarted = false;
|
|
||||||
bool mScheduledPlaybackStarted = false;
|
|
||||||
VideoIOCompletionResult mLastCompletion = VideoIOCompletionResult::Unknown;
|
|
||||||
uint64_t mLastReadyQueueDepth = 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
FakeVideoIODevice device;
|
|
||||||
VideoFormatSelection selection;
|
|
||||||
std::string error;
|
|
||||||
bool inputSeen = false;
|
|
||||||
bool outputSeen = false;
|
|
||||||
|
|
||||||
Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds");
|
|
||||||
Expect(device.SelectPreferredFormats(selection, false, error), "fake format selection succeeds");
|
|
||||||
Expect(device.ConfigureInput([&](const VideoIOFrame& frame) {
|
|
||||||
inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
|
||||||
}, selection.input, error), "fake input config succeeds");
|
|
||||||
Expect(device.ConfigureOutput([&](const VideoIOCompletion& completion) {
|
|
||||||
outputSeen = completion.result == VideoIOCompletionResult::Completed;
|
|
||||||
}, selection.output, false, error), "fake output config succeeds");
|
|
||||||
Expect(device.Start(), "fake device starts");
|
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
|
||||||
Expect(device.BeginOutputFrame(outputFrame), "fake output frame can be acquired");
|
|
||||||
device.EndOutputFrame(outputFrame);
|
|
||||||
device.AccountForCompletionResult(VideoIOCompletionResult::Completed, 2);
|
|
||||||
Expect(device.ScheduleOutputFrame(outputFrame), "fake output frame can be scheduled");
|
|
||||||
|
|
||||||
Expect(inputSeen, "fake input callback emits generic frame");
|
|
||||||
Expect(outputSeen, "fake output callback emits generic completion");
|
|
||||||
Expect(device.PreparedOutputSchedule(), "fake output schedule was prepared");
|
|
||||||
Expect(device.InputStreamsStarted(), "fake input streams started");
|
|
||||||
Expect(device.ScheduledPlaybackStarted(), "fake scheduled playback started");
|
|
||||||
Expect(device.ScheduledFrames() == 1, "fake backend schedules one frame");
|
|
||||||
Expect(device.LastCompletion() == VideoIOCompletionResult::Completed, "fake backend records generic completion");
|
|
||||||
Expect(device.LastReadyQueueDepth() == 2, "fake backend records ready queue depth");
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " VideoIODevice fake test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "VideoIODevice fake tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user