Test works
This commit is contained in:
920
apps/DeckLinkRenderCadenceProbe/DeckLinkRenderCadenceProbe.cpp
Normal file
920
apps/DeckLinkRenderCadenceProbe/DeckLinkRenderCadenceProbe.cpp
Normal file
@@ -0,0 +1,920 @@
|
||||
#include "DeckLinkSession.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "VideoIOFormat.h"
|
||||
#include "VideoPlayoutPolicy.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr unsigned kDefaultWidth = 1920;
|
||||
constexpr unsigned kDefaultHeight = 1080;
|
||||
constexpr std::size_t kSystemFrameSlots = 12;
|
||||
constexpr std::size_t kPboDepth = 6;
|
||||
constexpr std::size_t kWarmupFrames = 4;
|
||||
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||
|
||||
enum class ProbeSlotState
|
||||
{
|
||||
Free,
|
||||
Rendering,
|
||||
Completed,
|
||||
Scheduled
|
||||
};
|
||||
|
||||
struct ProbeFrame
|
||||
{
|
||||
void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
std::size_t index = 0;
|
||||
uint64_t generation = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
struct ProbeMetrics
|
||||
{
|
||||
uint64_t renderedFrames = 0;
|
||||
uint64_t completedFrames = 0;
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t scheduleUnderruns = 0;
|
||||
uint64_t pboQueueMisses = 0;
|
||||
std::size_t freeCount = 0;
|
||||
std::size_t renderingCount = 0;
|
||||
std::size_t completedCount = 0;
|
||||
std::size_t scheduledCount = 0;
|
||||
};
|
||||
|
||||
class LatestFrameStore
|
||||
{
|
||||
public:
|
||||
LatestFrameStore(unsigned width, unsigned height, std::size_t capacity) :
|
||||
mWidth(width),
|
||||
mHeight(height),
|
||||
mRowBytes(VideoIORowBytes(VideoIOPixelFormat::Bgra8, width))
|
||||
{
|
||||
mSlots.resize(capacity);
|
||||
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.bytes.resize(byteCount);
|
||||
slot.generation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool AcquireForRender(ProbeFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!AcquireFreeLocked(frame))
|
||||
{
|
||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
||||
{
|
||||
++mMetrics.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PublishCompleted(const ProbeFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!IsValidLocked(frame))
|
||||
return false;
|
||||
Slot& slot = mSlots[frame.index];
|
||||
if (slot.state != ProbeSlotState::Rendering)
|
||||
return false;
|
||||
slot.state = ProbeSlotState::Completed;
|
||||
slot.frameIndex = frame.frameIndex;
|
||||
mCompletedIndices.push_back(frame.index);
|
||||
++mMetrics.completedFrames;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConsumeCompleted(ProbeFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != ProbeSlotState::Completed)
|
||||
continue;
|
||||
mSlots[index].state = ProbeSlotState::Scheduled;
|
||||
FillFrameLocked(index, frame);
|
||||
++mMetrics.scheduledFrames;
|
||||
return true;
|
||||
}
|
||||
++mMetrics.scheduleUnderruns;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReleaseByBytes(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.data() != bytes)
|
||||
continue;
|
||||
mSlots[index].state = ProbeSlotState::Free;
|
||||
++mSlots[index].generation;
|
||||
RemoveCompletedIndexLocked(index);
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
return mCondition.wait_for(lock, timeout, [&]() {
|
||||
return CompletedCountLocked() >= targetDepth;
|
||||
});
|
||||
}
|
||||
|
||||
ProbeMetrics Metrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
ProbeMetrics metrics = mMetrics;
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
switch (slot.state)
|
||||
{
|
||||
case ProbeSlotState::Free:
|
||||
++metrics.freeCount;
|
||||
break;
|
||||
case ProbeSlotState::Rendering:
|
||||
++metrics.renderingCount;
|
||||
break;
|
||||
case ProbeSlotState::Completed:
|
||||
++metrics.completedCount;
|
||||
break;
|
||||
case ProbeSlotState::Scheduled:
|
||||
++metrics.scheduledCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void CountRenderedFrame()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
++mMetrics.renderedFrames;
|
||||
}
|
||||
|
||||
void CountPboQueueMiss()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
++mMetrics.pboQueueMisses;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
ProbeSlotState state = ProbeSlotState::Free;
|
||||
uint64_t generation = 1;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
bool AcquireFreeLocked(ProbeFrame& frame)
|
||||
{
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
if (mSlots[index].state != ProbeSlotState::Free)
|
||||
continue;
|
||||
mSlots[index].state = ProbeSlotState::Rendering;
|
||||
++mSlots[index].generation;
|
||||
FillFrameLocked(index, frame);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DropOldestCompletedLocked()
|
||||
{
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != ProbeSlotState::Completed)
|
||||
continue;
|
||||
mSlots[index].state = ProbeSlotState::Free;
|
||||
++mSlots[index].generation;
|
||||
++mMetrics.completedDrops;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FillFrameLocked(std::size_t index, ProbeFrame& frame) const
|
||||
{
|
||||
const Slot& slot = mSlots[index];
|
||||
frame.bytes = const_cast<unsigned char*>(slot.bytes.data());
|
||||
frame.rowBytes = static_cast<long>(mRowBytes);
|
||||
frame.width = mWidth;
|
||||
frame.height = mHeight;
|
||||
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
frame.index = index;
|
||||
frame.generation = slot.generation;
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
}
|
||||
|
||||
bool IsValidLocked(const ProbeFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
}
|
||||
|
||||
void RemoveCompletedIndexLocked(std::size_t index)
|
||||
{
|
||||
mCompletedIndices.erase(std::remove(mCompletedIndices.begin(), mCompletedIndices.end(), index), mCompletedIndices.end());
|
||||
}
|
||||
|
||||
std::size_t CompletedCountLocked() const
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
if (slot.state == ProbeSlotState::Completed)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
unsigned mRowBytes = 0;
|
||||
std::vector<Slot> mSlots;
|
||||
std::deque<std::size_t> mCompletedIndices;
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
ProbeMetrics mMetrics;
|
||||
};
|
||||
|
||||
LRESULT CALLBACK ProbeWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
class HiddenOpenGLContext
|
||||
{
|
||||
public:
|
||||
~HiddenOpenGLContext()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Create(unsigned width, unsigned height, std::string& error)
|
||||
{
|
||||
mInstance = GetModuleHandle(nullptr);
|
||||
WNDCLASSA wc = {};
|
||||
wc.style = CS_OWNDC;
|
||||
wc.lpfnWndProc = ProbeWindowProc;
|
||||
wc.hInstance = mInstance;
|
||||
wc.lpszClassName = "DeckLinkRenderCadenceProbeWindow";
|
||||
RegisterClassA(&wc);
|
||||
|
||||
mWindow = CreateWindowA(
|
||||
wc.lpszClassName,
|
||||
"DeckLink Render Cadence Probe",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
nullptr,
|
||||
nullptr,
|
||||
mInstance,
|
||||
nullptr);
|
||||
if (!mWindow)
|
||||
{
|
||||
error = "CreateWindowA failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mDc = GetDC(mWindow);
|
||||
if (!mDc)
|
||||
{
|
||||
error = "GetDC failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
PIXELFORMATDESCRIPTOR pfd = {};
|
||||
pfd.nSize = sizeof(pfd);
|
||||
pfd.nVersion = 1;
|
||||
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
||||
pfd.iPixelType = PFD_TYPE_RGBA;
|
||||
pfd.cColorBits = 32;
|
||||
pfd.cDepthBits = 0;
|
||||
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||
|
||||
const int pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
||||
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
||||
{
|
||||
error = "Could not choose/set a pixel format.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mGlrc = wglCreateContext(mDc);
|
||||
if (!mGlrc)
|
||||
{
|
||||
error = "wglCreateContext failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MakeCurrent()
|
||||
{
|
||||
return mDc && mGlrc && wglMakeCurrent(mDc, mGlrc);
|
||||
}
|
||||
|
||||
void ClearCurrent()
|
||||
{
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void Destroy()
|
||||
{
|
||||
ClearCurrent();
|
||||
if (mGlrc)
|
||||
{
|
||||
wglDeleteContext(mGlrc);
|
||||
mGlrc = nullptr;
|
||||
}
|
||||
if (mWindow && mDc)
|
||||
{
|
||||
ReleaseDC(mWindow, mDc);
|
||||
mDc = nullptr;
|
||||
}
|
||||
if (mWindow)
|
||||
{
|
||||
DestroyWindow(mWindow);
|
||||
mWindow = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
HINSTANCE mInstance = nullptr;
|
||||
HWND mWindow = nullptr;
|
||||
HDC mDc = nullptr;
|
||||
HGLRC mGlrc = nullptr;
|
||||
};
|
||||
|
||||
class RenderCadenceProbe
|
||||
{
|
||||
public:
|
||||
RenderCadenceProbe(LatestFrameStore& frameStore, unsigned width, unsigned height, double frameDurationMs) :
|
||||
mFrameStore(frameStore),
|
||||
mWidth(width),
|
||||
mHeight(height),
|
||||
mFrameDuration(std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double, std::milli>(frameDurationMs)))
|
||||
{
|
||||
if (mFrameDuration <= Clock::duration::zero())
|
||||
mFrameDuration = std::chrono::milliseconds(16);
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
mStopping = false;
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
std::unique_lock<std::mutex> lock(mStartupMutex);
|
||||
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() { return mStarted || !mStartupError.empty(); }))
|
||||
{
|
||||
error = "Timed out starting render thread.";
|
||||
return false;
|
||||
}
|
||||
if (!mStartupError.empty())
|
||||
{
|
||||
error = mStartupError;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
struct PboSlot
|
||||
{
|
||||
GLuint pbo = 0;
|
||||
GLsync fence = nullptr;
|
||||
bool inFlight = false;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void ThreadMain()
|
||||
{
|
||||
std::string error;
|
||||
HiddenOpenGLContext context;
|
||||
if (!context.Create(mWidth, mHeight, error) || !context.MakeCurrent())
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||
return;
|
||||
}
|
||||
if (!ResolveGLExtensions())
|
||||
{
|
||||
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||
return;
|
||||
}
|
||||
if (!CreateRenderTargets())
|
||||
{
|
||||
SignalStartupFailure("OpenGL render target creation failed.");
|
||||
return;
|
||||
}
|
||||
CreatePbos();
|
||||
SignalStarted();
|
||||
|
||||
auto nextRenderTime = Clock::now();
|
||||
while (!mStopping)
|
||||
{
|
||||
ConsumeCompletedPbos();
|
||||
|
||||
const auto now = Clock::now();
|
||||
if (now < nextRenderTime)
|
||||
{
|
||||
std::this_thread::sleep_for((std::min)(std::chrono::milliseconds(1), std::chrono::duration_cast<std::chrono::milliseconds>(nextRenderTime - now)));
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderPattern(mFrameIndex);
|
||||
if (!QueueReadback(mFrameIndex))
|
||||
mFrameStore.CountPboQueueMiss();
|
||||
mFrameStore.CountRenderedFrame();
|
||||
++mFrameIndex;
|
||||
nextRenderTime += mFrameDuration;
|
||||
if (Clock::now() - nextRenderTime > mFrameDuration * 4)
|
||||
nextRenderTime = Clock::now() + mFrameDuration;
|
||||
}
|
||||
|
||||
FlushPbos();
|
||||
DestroyPbos();
|
||||
DestroyRenderTargets();
|
||||
context.ClearCurrent();
|
||||
}
|
||||
|
||||
bool CreateRenderTargets()
|
||||
{
|
||||
glGenFramebuffers(1, &mFramebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
glGenTextures(1, &mTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight), 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return complete;
|
||||
}
|
||||
|
||||
void DestroyRenderTargets()
|
||||
{
|
||||
if (mFramebuffer != 0)
|
||||
glDeleteFramebuffers(1, &mFramebuffer);
|
||||
if (mTexture != 0)
|
||||
glDeleteTextures(1, &mTexture);
|
||||
mFramebuffer = 0;
|
||||
mTexture = 0;
|
||||
}
|
||||
|
||||
void CreatePbos()
|
||||
{
|
||||
mPbos.resize(kPboDepth);
|
||||
const std::size_t byteCount = static_cast<std::size_t>(VideoIORowBytes(VideoIOPixelFormat::Bgra8, mWidth)) * mHeight;
|
||||
for (PboSlot& slot : mPbos)
|
||||
{
|
||||
glGenBuffers(1, &slot.pbo);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(byteCount), nullptr, GL_STREAM_READ);
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
}
|
||||
|
||||
void DestroyPbos()
|
||||
{
|
||||
for (PboSlot& slot : mPbos)
|
||||
{
|
||||
if (slot.fence)
|
||||
glDeleteSync(slot.fence);
|
||||
if (slot.pbo != 0)
|
||||
glDeleteBuffers(1, &slot.pbo);
|
||||
slot = {};
|
||||
}
|
||||
mPbos.clear();
|
||||
}
|
||||
|
||||
void FlushPbos()
|
||||
{
|
||||
for (std::size_t i = 0; i < mPbos.size() * 2; ++i)
|
||||
ConsumeCompletedPbos();
|
||||
}
|
||||
|
||||
void RenderPattern(uint64_t frameIndex)
|
||||
{
|
||||
const float t = static_cast<float>(frameIndex) / 60.0f;
|
||||
const float red = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t));
|
||||
const float green = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t * 0.73f + 1.0f));
|
||||
const float blue = 0.15f + 0.3f * (0.5f + 0.5f * std::sin(t * 0.41f + 2.0f));
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(red, green, blue, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const int boxWidth = static_cast<int>(mWidth / 6);
|
||||
const int boxHeight = static_cast<int>(mHeight / 5);
|
||||
const float phase = 0.5f + 0.5f * std::sin(t * 1.7f);
|
||||
const int x = static_cast<int>(phase * static_cast<float>(mWidth - boxWidth));
|
||||
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - boxHeight));
|
||||
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(x, y, boxWidth, boxHeight);
|
||||
glClearColor(1.0f - red, 0.85f, 0.15f + blue, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
|
||||
bool QueueReadback(uint64_t frameIndex)
|
||||
{
|
||||
if (mPbos.empty())
|
||||
return false;
|
||||
|
||||
PboSlot& slot = mPbos[mWriteIndex];
|
||||
if (slot.inFlight)
|
||||
return false;
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(VideoIORowBytes(VideoIOPixelFormat::Bgra8, mWidth)) * mHeight;
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mFramebuffer);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(byteCount), nullptr, GL_STREAM_READ);
|
||||
glReadPixels(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
slot.inFlight = slot.fence != nullptr;
|
||||
slot.frameIndex = frameIndex;
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mWriteIndex = (mWriteIndex + 1) % mPbos.size();
|
||||
return slot.inFlight;
|
||||
}
|
||||
|
||||
void ConsumeCompletedPbos()
|
||||
{
|
||||
for (std::size_t checked = 0; checked < mPbos.size(); ++checked)
|
||||
{
|
||||
PboSlot& slot = mPbos[mReadIndex];
|
||||
if (!slot.inFlight || slot.fence == nullptr)
|
||||
{
|
||||
mReadIndex = (mReadIndex + 1) % mPbos.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
|
||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||
return;
|
||||
|
||||
ProbeFrame frame;
|
||||
if (mFrameStore.AcquireForRender(frame))
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (mapped)
|
||||
{
|
||||
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * frame.height;
|
||||
std::memcpy(frame.bytes, mapped, byteCount);
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
mFrameStore.PublishCompleted(frame);
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
}
|
||||
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
slot.inFlight = false;
|
||||
mReadIndex = (mReadIndex + 1) % mPbos.size();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalStarted()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStarted = true;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
void SignalStartupFailure(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStartupError = error;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
LatestFrameStore& mFrameStore;
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
Clock::duration mFrameDuration;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::mutex mStartupMutex;
|
||||
std::condition_variable mStartupCondition;
|
||||
bool mStarted = false;
|
||||
std::string mStartupError;
|
||||
GLuint mFramebuffer = 0;
|
||||
GLuint mTexture = 0;
|
||||
std::vector<PboSlot> mPbos;
|
||||
std::size_t mWriteIndex = 0;
|
||||
std::size_t mReadIndex = 0;
|
||||
uint64_t mFrameIndex = 0;
|
||||
};
|
||||
|
||||
class DeckLinkProbePlayout
|
||||
{
|
||||
public:
|
||||
DeckLinkProbePlayout(DeckLinkSession& session, LatestFrameStore& frameStore) :
|
||||
mSession(session),
|
||||
mFrameStore(frameStore)
|
||||
{
|
||||
}
|
||||
|
||||
bool Start()
|
||||
{
|
||||
mStopping = false;
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
void ThreadMain()
|
||||
{
|
||||
while (!mStopping)
|
||||
{
|
||||
const ProbeMetrics metrics = mFrameStore.Metrics();
|
||||
if (metrics.scheduledCount >= kDeckLinkTargetBufferedFrames)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
ProbeFrame frame;
|
||||
if (!mFrameStore.ConsumeCompleted(frame))
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
VideoIOOutputFrame outputFrame;
|
||||
outputFrame.bytes = frame.bytes;
|
||||
outputFrame.nativeBuffer = frame.bytes;
|
||||
outputFrame.rowBytes = frame.rowBytes;
|
||||
outputFrame.width = frame.width;
|
||||
outputFrame.height = frame.height;
|
||||
outputFrame.pixelFormat = frame.pixelFormat;
|
||||
|
||||
if (!mSession.ScheduleOutputFrame(outputFrame))
|
||||
{
|
||||
mFrameStore.ReleaseByBytes(frame.bytes);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
DeckLinkSession& mSession;
|
||||
LatestFrameStore& mFrameStore;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
};
|
||||
|
||||
std::string CompletionResultToString(VideoIOCompletionResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case VideoIOCompletionResult::Completed:
|
||||
return "completed";
|
||||
case VideoIOCompletionResult::DisplayedLate:
|
||||
return "late";
|
||||
case VideoIOCompletionResult::Dropped:
|
||||
return "dropped";
|
||||
case VideoIOCompletionResult::Flushed:
|
||||
return "flushed";
|
||||
case VideoIOCompletionResult::Unknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void PrintUsage()
|
||||
{
|
||||
std::cout << "DeckLinkRenderCadenceProbe\n"
|
||||
<< " Renders a simple OpenGL BGRA8 motion pattern on one GL thread,\n"
|
||||
<< " copies completed PBO readbacks into latest-N system memory slots,\n"
|
||||
<< " warms up rendered frames, then feeds DeckLink scheduled playback.\n\n"
|
||||
<< "Press Enter to stop.\n";
|
||||
}
|
||||
|
||||
class ComInitGuard
|
||||
{
|
||||
public:
|
||||
~ComInitGuard()
|
||||
{
|
||||
if (mInitialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitialize(nullptr);
|
||||
mInitialized = SUCCEEDED(result);
|
||||
mResult = result;
|
||||
return mInitialized;
|
||||
}
|
||||
|
||||
HRESULT Result() const { return mResult; }
|
||||
|
||||
private:
|
||||
bool mInitialized = false;
|
||||
HRESULT mResult = S_OK;
|
||||
};
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
PrintUsage();
|
||||
|
||||
ComInitGuard com;
|
||||
if (!com.Initialize())
|
||||
{
|
||||
std::cerr << "COM initialization failed: 0x" << std::hex << com.Result() << std::dec << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
LatestFrameStore frameStore(kDefaultWidth, kDefaultHeight, kSystemFrameSlots);
|
||||
DeckLinkSession deckLink;
|
||||
std::atomic<uint64_t> completions{ 0 };
|
||||
std::atomic<uint64_t> late{ 0 };
|
||||
std::atomic<uint64_t> dropped{ 0 };
|
||||
|
||||
VideoFormatSelection formats;
|
||||
std::string error;
|
||||
if (!deckLink.DiscoverDevicesAndModes(formats, error))
|
||||
{
|
||||
std::cerr << "DeckLink discovery failed: " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
if (!deckLink.SelectPreferredFormats(formats, false, error))
|
||||
{
|
||||
std::cerr << "DeckLink format selection failed: " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
if (!deckLink.ConfigureOutput(
|
||||
[&](const VideoIOCompletion& completion) {
|
||||
frameStore.ReleaseByBytes(completion.outputFrameBuffer);
|
||||
++completions;
|
||||
if (completion.result == VideoIOCompletionResult::DisplayedLate)
|
||||
++late;
|
||||
else if (completion.result == VideoIOCompletionResult::Dropped)
|
||||
++dropped;
|
||||
},
|
||||
formats.output,
|
||||
false,
|
||||
error))
|
||||
{
|
||||
std::cerr << "DeckLink output configuration failed: " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
if (!deckLink.PrepareOutputSchedule())
|
||||
{
|
||||
std::cerr << "DeckLink schedule preparation failed.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const VideoIOState& state = deckLink.State();
|
||||
if (state.outputFrameSize.width != kDefaultWidth || state.outputFrameSize.height != kDefaultHeight)
|
||||
{
|
||||
std::cerr << "This probe currently expects 1920x1080 output. Selected mode is "
|
||||
<< state.outputFrameSize.width << "x" << state.outputFrameSize.height << ".\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
RenderCadenceProbe renderer(frameStore, state.outputFrameSize.width, state.outputFrameSize.height, state.frameBudgetMilliseconds);
|
||||
if (!renderer.Start(error))
|
||||
{
|
||||
std::cerr << "Render thread start failed: " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Warming up " << kWarmupFrames << " rendered frames at cadence...\n";
|
||||
if (!frameStore.WaitForCompletedDepth(kWarmupFrames, std::chrono::seconds(3)))
|
||||
{
|
||||
std::cerr << "Timed out waiting for rendered warmup frames.\n";
|
||||
renderer.Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
DeckLinkProbePlayout playout(deckLink, frameStore);
|
||||
playout.Start();
|
||||
|
||||
const auto prerollDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||
while (std::chrono::steady_clock::now() < prerollDeadline)
|
||||
{
|
||||
if (frameStore.Metrics().scheduledCount >= kDeckLinkTargetBufferedFrames)
|
||||
break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
|
||||
if (!deckLink.StartScheduledPlayback())
|
||||
{
|
||||
std::cerr << "DeckLink scheduled playback failed to start.\n";
|
||||
playout.Stop();
|
||||
renderer.Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::atomic<bool> metricsStopping{ false };
|
||||
std::thread metricsThread([&]() {
|
||||
uint64_t lastRendered = 0;
|
||||
uint64_t lastScheduled = 0;
|
||||
auto lastTime = std::chrono::steady_clock::now();
|
||||
while (!metricsStopping)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const double seconds = std::chrono::duration_cast<std::chrono::duration<double>>(now - lastTime).count();
|
||||
const ProbeMetrics metrics = frameStore.Metrics();
|
||||
const double renderFps = seconds > 0.0 ? static_cast<double>(metrics.renderedFrames - lastRendered) / seconds : 0.0;
|
||||
const double scheduleFps = seconds > 0.0 ? static_cast<double>(metrics.scheduledFrames - lastScheduled) / seconds : 0.0;
|
||||
lastRendered = metrics.renderedFrames;
|
||||
lastScheduled = metrics.scheduledFrames;
|
||||
lastTime = now;
|
||||
|
||||
std::cout << std::fixed << std::setprecision(1)
|
||||
<< "renderFps=" << renderFps
|
||||
<< " scheduleFps=" << scheduleFps
|
||||
<< " free=" << metrics.freeCount
|
||||
<< " completed=" << metrics.completedCount
|
||||
<< " scheduled=" << metrics.scheduledCount
|
||||
<< " drops=" << metrics.completedDrops
|
||||
<< " pboMiss=" << metrics.pboQueueMisses
|
||||
<< " completions=" << completions.load()
|
||||
<< " late=" << late.load()
|
||||
<< " dropped=" << dropped.load()
|
||||
<< " decklinkBuffered=" << deckLink.State().actualDeckLinkBufferedFrames
|
||||
<< "\n";
|
||||
}
|
||||
});
|
||||
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
|
||||
metricsStopping = true;
|
||||
if (metricsThread.joinable())
|
||||
metricsThread.join();
|
||||
playout.Stop();
|
||||
deckLink.Stop();
|
||||
renderer.Stop();
|
||||
deckLink.ReleaseResources();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user