timing refactor
This commit is contained in:
@@ -11,14 +11,14 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
polls latest input frames without waiting
|
||||
polls the oldest ready input frame without waiting
|
||||
uploads input frames into a render-owned GL texture
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
InputFrameMailbox
|
||||
owns latest disposable CPU input slots
|
||||
owns bounded FIFO CPU input slots
|
||||
keeps a bounded three-ready-frame input buffer for render
|
||||
trims frames beyond that bound to avoid runaway input latency
|
||||
protects the one frame currently being uploaded by render
|
||||
@@ -26,7 +26,7 @@ InputFrameMailbox
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
drops old completed unscheduled frames when render needs space
|
||||
preserves completed output frames once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
@@ -286,8 +286,8 @@ Input telemetry:
|
||||
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||
- `completedDrops`: completed unscheduled system-memory frames dropped so render could reuse the slot
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot
|
||||
- `completedDrops`: completed unscheduled system-memory frames dropped by latest-N acquire paths; expected to stay flat in the cadence compositor output path
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
|
||||
@@ -111,14 +111,23 @@ int main(int argc, char** argv)
|
||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
@@ -173,7 +182,10 @@ int main(int argc, char** argv)
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = 6;
|
||||
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -184,6 +185,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
||||
return 1000.0 / rate;
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||
{
|
||||
struct ModeRate
|
||||
{
|
||||
BMDDisplayMode mode;
|
||||
int64_t frameDuration;
|
||||
int64_t timeScale;
|
||||
};
|
||||
|
||||
static const ModeRate rates[] =
|
||||
{
|
||||
{ bmdModeHD720p50, 1, 50 },
|
||||
{ bmdModeHD720p5994, 1001, 60000 },
|
||||
{ bmdModeHD720p60, 1, 60 },
|
||||
{ bmdModeHD1080i50, 1, 25 },
|
||||
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||
{ bmdModeHD1080i6000, 1, 30 },
|
||||
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||
{ bmdModeHD1080p24, 1, 24 },
|
||||
{ bmdModeHD1080p25, 1, 25 },
|
||||
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||
{ bmdModeHD1080p30, 1, 30 },
|
||||
{ bmdModeHD1080p50, 1, 50 },
|
||||
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||
{ bmdModeHD1080p6000, 1, 60 },
|
||||
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||
{ bmdMode4K2160p24, 1, 24 },
|
||||
{ bmdMode4K2160p25, 1, 25 },
|
||||
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||
{ bmdMode4K2160p30, 1, 30 },
|
||||
{ bmdMode4K2160p50, 1, 50 },
|
||||
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||
{ bmdMode4K2160p60, 1, 60 }
|
||||
};
|
||||
|
||||
for (const ModeRate& rate : rates)
|
||||
{
|
||||
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||
}
|
||||
|
||||
return fallbackMilliseconds;
|
||||
}
|
||||
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||
{
|
||||
std::string normalized = formatName;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -27,6 +28,7 @@ private:
|
||||
};
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||
|
||||
@@ -120,38 +120,6 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.back();
|
||||
mReadyIndices.pop_back();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t olderIndex = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (olderIndex >= mSlots.size() || mSlots[olderIndex].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
mSlots[olderIndex].state = InputFrameSlotState::Free;
|
||||
++mSlots[olderIndex].generation;
|
||||
++mCounters.droppedReadyFrames;
|
||||
}
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Reading;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.consumedFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
frame = InputFrame();
|
||||
++mCounters.consumeMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
@@ -64,7 +64,6 @@ public:
|
||||
InputFrameMailboxConfig Config() const;
|
||||
|
||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||
bool TryAcquireLatest(InputFrame& frame);
|
||||
bool TryAcquireOldest(InputFrame& frame);
|
||||
bool Release(const InputFrame& frame);
|
||||
void Clear();
|
||||
|
||||
@@ -47,12 +47,9 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!AcquireFreeLocked(frame))
|
||||
{
|
||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
||||
{
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
++mCounters.acquiredFrames;
|
||||
@@ -234,27 +231,6 @@ bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||
{
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||
continue;
|
||||
|
||||
Slot& slot = mSlots[index];
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
++mCounters.completedDrops;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
|
||||
@@ -40,7 +40,6 @@ private:
|
||||
};
|
||||
|
||||
bool AcquireFreeLocked(SystemFrame& frame);
|
||||
bool DropOldestCompletedLocked();
|
||||
bool IsValidLocked(const SystemFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||
std::size_t CompletedCountLocked() const;
|
||||
|
||||
@@ -65,7 +65,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
|
||||
}
|
||||
|
||||
// Shader source contract:
|
||||
// - gVideoInput is the decoded latest input texture for every layer in the stack.
|
||||
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||
GLuint layerInputTexture = videoInputTexture;
|
||||
std::size_t nextTargetIndex = 0;
|
||||
|
||||
@@ -82,7 +82,6 @@ private:
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user