V2 working
This commit is contained in:
157
apps/RenderCadenceCompositor/README.md
Normal file
157
apps/RenderCadenceCompositor/README.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# RenderCadenceCompositor
|
||||
|
||||
This app is the modular version of the working DeckLink render-cadence probe.
|
||||
|
||||
Its job is to prove the production-facing foundation before the current compositor's shader/runtime/control features are ported over.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
drops old completed unscheduled frames when render needs space
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
consumes completed system-memory frames
|
||||
schedules them into DeckLink up to target depth
|
||||
never renders
|
||||
```
|
||||
|
||||
Startup warms up real rendered frames before DeckLink scheduled playback starts.
|
||||
|
||||
## Current Scope
|
||||
|
||||
Included now:
|
||||
|
||||
- output-only DeckLink
|
||||
- hidden render-thread-owned OpenGL context
|
||||
- simple smooth-motion renderer
|
||||
- BGRA8-only output
|
||||
- async PBO readback
|
||||
- latest-N system-memory frame exchange
|
||||
- rendered-frame warmup
|
||||
- compact telemetry
|
||||
- non-GL frame-exchange tests
|
||||
|
||||
Intentionally not included yet:
|
||||
|
||||
- DeckLink input
|
||||
- shader package rendering
|
||||
- runtime state
|
||||
- OSC/API control
|
||||
- preview
|
||||
- screenshots
|
||||
- persistence
|
||||
|
||||
Those features should be ported only after the cadence spine is stable.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositor -- /m:1
|
||||
```
|
||||
|
||||
The executable is:
|
||||
|
||||
```text
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Run from VS Code with:
|
||||
|
||||
```text
|
||||
Debug RenderCadenceCompositor
|
||||
```
|
||||
|
||||
Or from a terminal:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
Press Enter to stop.
|
||||
|
||||
## Expected Telemetry
|
||||
|
||||
The app prints one line per second:
|
||||
|
||||
```text
|
||||
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=0 scheduleFailures=0 completions=119 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=0.0
|
||||
```
|
||||
|
||||
Healthy first-run signs:
|
||||
|
||||
- visible DeckLink output is smooth
|
||||
- `renderFps` is close to the selected cadence
|
||||
- `scheduleFps` is close to the selected cadence after warmup
|
||||
- `scheduled` stays near 4
|
||||
- `decklinkBuffered` stays near 4 when available
|
||||
- `late` and `dropped` do not increase continuously
|
||||
- `scheduleFailures` does not increase
|
||||
|
||||
`completedPollMisses` means the DeckLink scheduling thread woke up before a completed frame was available. It is not a DeckLink playout underrun by itself. Treat it as healthy polling noise when `scheduled`, `decklinkBuffered`, `late`, `dropped`, and `scheduleFailures` remain stable.
|
||||
|
||||
## Baseline Result
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
User-visible result:
|
||||
|
||||
- output was smooth
|
||||
- DeckLink held a 4-frame buffer
|
||||
|
||||
Representative telemetry:
|
||||
|
||||
```text
|
||||
renderFps=59.9 scheduleFps=59.9 free=8 completed=0 scheduled=4 completedPollMisses=30 scheduleFailures=0 completions=720 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=1.2
|
||||
renderFps=59.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 completedPollMisses=36 scheduleFailures=0 completions=1080 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=4.7
|
||||
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=86 scheduleFailures=0 completions=1381 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=2.1
|
||||
```
|
||||
|
||||
Read:
|
||||
|
||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||
- app scheduled depth stayed at 4
|
||||
- actual DeckLink buffered depth stayed at 4
|
||||
- no late frames, dropped frames, or schedule failures were observed
|
||||
- completed poll misses were benign because playout remained fully fed
|
||||
|
||||
## Tests
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositorFrameExchangeTests -- /m:1
|
||||
ctest --test-dir build\vs2022-x64-debug -C Debug -R RenderCadenceCompositorFrameExchangeTests --output-on-failure
|
||||
```
|
||||
|
||||
## Relationship To The Probe
|
||||
|
||||
`apps/DeckLinkRenderCadenceProbe` proved the timing model in one compact file.
|
||||
|
||||
This app keeps the same core behavior but splits it into modules that can grow:
|
||||
|
||||
- `frames/`: system-memory handoff
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence, simple rendering, PBO readback
|
||||
- `video/`: DeckLink output wrapper and scheduling thread
|
||||
- `telemetry/`: cadence telemetry
|
||||
- `app/`: startup/shutdown orchestration
|
||||
|
||||
## Next Porting Steps
|
||||
|
||||
Only after this app matches the probe's smooth output:
|
||||
|
||||
1. replace `SimpleMotionRenderer` with a render-scene interface
|
||||
2. port shader package rendering
|
||||
3. port runtime snapshots/live state
|
||||
4. add control services
|
||||
5. add preview/screenshot from system-memory frames
|
||||
6. add DeckLink input as a CPU latest-frame mailbox
|
||||
83
apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp
Normal file
83
apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "app/AppConfig.h"
|
||||
#include "app/RenderCadenceApp.h"
|
||||
#include "frames/SystemFrameExchange.h"
|
||||
#include "render/RenderThread.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
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()
|
||||
{
|
||||
ComInitGuard com;
|
||||
if (!com.Initialize())
|
||||
{
|
||||
std::cerr << "COM initialization failed: 0x" << std::hex << com.Result() << std::dec << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "RenderCadenceCompositor\n"
|
||||
<< " Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry.\n"
|
||||
<< " Press Enter to stop.\n";
|
||||
|
||||
SystemFrameExchangeConfig frameExchangeConfig;
|
||||
frameExchangeConfig.width = 1920;
|
||||
frameExchangeConfig.height = 1080;
|
||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||
frameExchangeConfig.capacity = 12;
|
||||
|
||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
renderConfig.frameDurationMilliseconds = 1000.0 / 59.94;
|
||||
renderConfig.pboDepth = 6;
|
||||
|
||||
RenderThread renderThread(frameExchange, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::AppConfig appConfig = RenderCadenceCompositor::DefaultAppConfig();
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
|
||||
std::string error;
|
||||
if (!app.Start(error))
|
||||
{
|
||||
std::cerr << "RenderCadenceCompositor start failed: " << error << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
app.Stop();
|
||||
return 0;
|
||||
}
|
||||
18
apps/RenderCadenceCompositor/app/AppConfig.cpp
Normal file
18
apps/RenderCadenceCompositor/app/AppConfig.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "AppConfig.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
AppConfig DefaultAppConfig()
|
||||
{
|
||||
AppConfig config;
|
||||
config.deckLink.externalKeyingEnabled = false;
|
||||
config.deckLink.outputAlphaRequired = false;
|
||||
config.outputThread.targetBufferedFrames = 4;
|
||||
config.telemetry.interval = std::chrono::seconds(1);
|
||||
config.warmupCompletedFrames = 4;
|
||||
config.warmupTimeout = std::chrono::seconds(3);
|
||||
config.prerollTimeout = std::chrono::seconds(3);
|
||||
config.prerollPoll = std::chrono::milliseconds(2);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
24
apps/RenderCadenceCompositor/app/AppConfig.h
Normal file
24
apps/RenderCadenceCompositor/app/AppConfig.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "../telemetry/TelemetryPrinter.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct AppConfig
|
||||
{
|
||||
DeckLinkOutputConfig deckLink;
|
||||
DeckLinkOutputThreadConfig outputThread;
|
||||
TelemetryPrinterConfig telemetry;
|
||||
std::size_t warmupCompletedFrames = 4;
|
||||
std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
|
||||
};
|
||||
|
||||
AppConfig DefaultAppConfig();
|
||||
}
|
||||
148
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
148
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "../telemetry/TelemetryPrinter.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool())
|
||||
{
|
||||
return renderThread.Start(error);
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type)
|
||||
{
|
||||
return renderThread.Start();
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type)
|
||||
{
|
||||
renderThread.Start();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool())
|
||||
{
|
||||
return StartRenderThreadWithoutError(renderThread, std::is_same<decltype(renderThread.Start()), bool>());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename RenderThread, typename SystemFrameExchange>
|
||||
class RenderCadenceApp
|
||||
{
|
||||
public:
|
||||
RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) :
|
||||
mRenderThread(renderThread),
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config),
|
||||
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
|
||||
mTelemetry(mConfig.telemetry)
|
||||
{
|
||||
}
|
||||
|
||||
RenderCadenceApp(const RenderCadenceApp&) = delete;
|
||||
RenderCadenceApp& operator=(const RenderCadenceApp&) = delete;
|
||||
|
||||
~RenderCadenceApp()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
if (!mOutput.Initialize(
|
||||
mConfig.deckLink,
|
||||
[this](const VideoIOCompletion& completion) {
|
||||
mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer);
|
||||
},
|
||||
error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!detail::StartRenderThread(mRenderThread, error, 0))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
||||
{
|
||||
error = "Timed out waiting for rendered warmup frames.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mOutputThread.Start())
|
||||
{
|
||||
error = "DeckLink output thread failed to start.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WaitForPreroll())
|
||||
{
|
||||
error = "Timed out waiting for DeckLink preroll frames.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mOutput.StartScheduledPlayback(error))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
mTelemetry.Start(mFrameExchange, mOutput, mOutputThread);
|
||||
mStarted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mTelemetry.Stop();
|
||||
mOutputThread.Stop();
|
||||
mOutput.Stop();
|
||||
mRenderThread.Stop();
|
||||
mOutput.ReleaseResources();
|
||||
mStarted = false;
|
||||
}
|
||||
|
||||
bool Started() const { return mStarted; }
|
||||
const DeckLinkOutput& Output() const { return mOutput; }
|
||||
|
||||
private:
|
||||
bool WaitForPreroll() const
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
||||
while (std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(mConfig.prerollPoll);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderThread& mRenderThread;
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
AppConfig mConfig;
|
||||
DeckLinkOutput mOutput;
|
||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
||||
TelemetryPrinter mTelemetry;
|
||||
bool mStarted = false;
|
||||
};
|
||||
}
|
||||
245
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
245
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
#include "SystemFrameExchange.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
SystemFrameExchangeConfig NormalizeConfig(SystemFrameExchangeConfig config)
|
||||
{
|
||||
if (config.rowBytes == 0)
|
||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
SystemFrameExchange::SystemFrameExchange(const SystemFrameExchangeConfig& config)
|
||||
{
|
||||
Configure(config);
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Configure(const SystemFrameExchangeConfig& config)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mConfig = NormalizeConfig(config);
|
||||
mCompletedIndices.clear();
|
||||
mSlots.clear();
|
||||
mSlots.resize(mConfig.capacity);
|
||||
|
||||
const std::size_t byteCount = FrameByteCount();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.bytes.resize(byteCount);
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
|
||||
mCounters = SystemFrameExchangeMetrics();
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
SystemFrameExchangeConfig SystemFrameExchange::Config() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
++mCounters.acquiredFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!IsValidLocked(frame))
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[frame.index];
|
||||
if (slot.state != SystemFrameSlotState::Rendering)
|
||||
return false;
|
||||
|
||||
slot.state = SystemFrameSlotState::Completed;
|
||||
slot.frameIndex = frame.frameIndex;
|
||||
mCompletedIndices.push_back(frame.index);
|
||||
++mCounters.completedFrames;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::ConsumeCompletedForSchedule(SystemFrame& 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 != SystemFrameSlotState::Completed)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = SystemFrameSlotState::Scheduled;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.scheduledFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
frame = SystemFrame();
|
||||
++mCounters.completedPollMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::ReleaseScheduledByBytes(void* bytes)
|
||||
{
|
||||
if (bytes == nullptr)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (slot.bytes.empty() || slot.bytes.data() != bytes)
|
||||
continue;
|
||||
if (slot.state != SystemFrameSlotState::Scheduled)
|
||||
return false;
|
||||
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::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;
|
||||
});
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mCompletedIndices.clear();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
SystemFrameExchangeMetrics SystemFrameExchange::Metrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
SystemFrameExchangeMetrics metrics = mCounters;
|
||||
metrics.capacity = mSlots.size();
|
||||
metrics.completedDepth = mCompletedIndices.size();
|
||||
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
switch (slot.state)
|
||||
{
|
||||
case SystemFrameSlotState::Free:
|
||||
++metrics.freeCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Rendering:
|
||||
++metrics.renderingCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Completed:
|
||||
++metrics.completedCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Scheduled:
|
||||
++metrics.scheduledCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
||||
{
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (slot.state != SystemFrameSlotState::Free)
|
||||
continue;
|
||||
|
||||
slot.state = SystemFrameSlotState::Rendering;
|
||||
++slot.generation;
|
||||
FillFrameLocked(index, frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void SystemFrameExchange::FillFrameLocked(std::size_t index, SystemFrame& frame)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||
frame.width = mConfig.width;
|
||||
frame.height = mConfig.height;
|
||||
frame.pixelFormat = mConfig.pixelFormat;
|
||||
frame.index = index;
|
||||
frame.generation = slot.generation;
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
}
|
||||
|
||||
std::size_t SystemFrameExchange::CompletedCountLocked() const
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
if (slot.state == SystemFrameSlotState::Completed)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t SystemFrameExchange::FrameByteCount() const
|
||||
{
|
||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||
}
|
||||
51
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
51
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "SystemFrameTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SystemFrameExchange
|
||||
{
|
||||
public:
|
||||
SystemFrameExchange() = default;
|
||||
explicit SystemFrameExchange(const SystemFrameExchangeConfig& config);
|
||||
|
||||
void Configure(const SystemFrameExchangeConfig& config);
|
||||
SystemFrameExchangeConfig Config() const;
|
||||
|
||||
bool AcquireForRender(SystemFrame& frame);
|
||||
bool PublishCompleted(const SystemFrame& frame);
|
||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||
bool ReleaseScheduledByBytes(void* bytes);
|
||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||
void Clear();
|
||||
|
||||
SystemFrameExchangeMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
SystemFrameSlotState state = SystemFrameSlotState::Free;
|
||||
uint64_t generation = 1;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
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;
|
||||
std::size_t FrameByteCount() const;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
SystemFrameExchangeConfig mConfig;
|
||||
std::vector<Slot> mSlots;
|
||||
std::deque<std::size_t> mCompletedIndices;
|
||||
SystemFrameExchangeMetrics mCounters;
|
||||
};
|
||||
51
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
51
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
enum class SystemFrameSlotState
|
||||
{
|
||||
Free,
|
||||
Rendering,
|
||||
Completed,
|
||||
Scheduled
|
||||
};
|
||||
|
||||
struct SystemFrameExchangeConfig
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
};
|
||||
|
||||
struct SystemFrame
|
||||
{
|
||||
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 SystemFrameExchangeMetrics
|
||||
{
|
||||
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 completedDepth = 0;
|
||||
uint64_t acquiredFrames = 0;
|
||||
uint64_t completedFrames = 0;
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
};
|
||||
120
apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp
Normal file
120
apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "HiddenGlWindow.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr const char* kWindowClassName = "RenderCadenceCompositorHiddenGlWindow";
|
||||
}
|
||||
|
||||
HiddenGlWindow::~HiddenGlWindow()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
||||
{
|
||||
Destroy();
|
||||
|
||||
mInstance = GetModuleHandle(nullptr);
|
||||
|
||||
WNDCLASSA wc = {};
|
||||
wc.style = CS_OWNDC;
|
||||
wc.lpfnWndProc = HiddenGlWindow::WindowProc;
|
||||
wc.hInstance = mInstance;
|
||||
wc.lpszClassName = kWindowClassName;
|
||||
|
||||
mClassAtom = RegisterClassA(&wc);
|
||||
if (mClassAtom == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||
{
|
||||
error = "RegisterClassA failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mWindow = CreateWindowA(
|
||||
kWindowClassName,
|
||||
"Render Cadence Compositor Hidden GL",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
nullptr,
|
||||
nullptr,
|
||||
mInstance,
|
||||
nullptr);
|
||||
if (!mWindow)
|
||||
{
|
||||
error = "CreateWindowA failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mDc = GetDC(mWindow);
|
||||
if (!mDc)
|
||||
{
|
||||
error = "GetDC failed for hidden OpenGL window.";
|
||||
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 pixel format for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mGlrc = wglCreateContext(mDc);
|
||||
if (!mGlrc)
|
||||
{
|
||||
error = "wglCreateContext failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HiddenGlWindow::MakeCurrent() const
|
||||
{
|
||||
return mDc != nullptr && mGlrc != nullptr && wglMakeCurrent(mDc, mGlrc) == TRUE;
|
||||
}
|
||||
|
||||
void HiddenGlWindow::ClearCurrent() const
|
||||
{
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void HiddenGlWindow::Destroy()
|
||||
{
|
||||
ClearCurrent();
|
||||
|
||||
if (mGlrc)
|
||||
{
|
||||
wglDeleteContext(mGlrc);
|
||||
mGlrc = nullptr;
|
||||
}
|
||||
if (mWindow && mDc)
|
||||
{
|
||||
ReleaseDC(mWindow, mDc);
|
||||
mDc = nullptr;
|
||||
}
|
||||
if (mWindow)
|
||||
{
|
||||
DestroyWindow(mWindow);
|
||||
mWindow = nullptr;
|
||||
}
|
||||
|
||||
mInstance = nullptr;
|
||||
mClassAtom = 0;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK HiddenGlWindow::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||
}
|
||||
31
apps/RenderCadenceCompositor/platform/HiddenGlWindow.h
Normal file
31
apps/RenderCadenceCompositor/platform/HiddenGlWindow.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
class HiddenGlWindow
|
||||
{
|
||||
public:
|
||||
HiddenGlWindow() = default;
|
||||
HiddenGlWindow(const HiddenGlWindow&) = delete;
|
||||
HiddenGlWindow& operator=(const HiddenGlWindow&) = delete;
|
||||
~HiddenGlWindow();
|
||||
|
||||
bool Create(unsigned width, unsigned height, std::string& error);
|
||||
bool MakeCurrent() const;
|
||||
void ClearCurrent() const;
|
||||
void Destroy();
|
||||
|
||||
HDC DeviceContext() const { return mDc; }
|
||||
HGLRC Context() const { return mGlrc; }
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
HINSTANCE mInstance = nullptr;
|
||||
HWND mWindow = nullptr;
|
||||
HDC mDc = nullptr;
|
||||
HGLRC mGlrc = nullptr;
|
||||
ATOM mClassAtom = 0;
|
||||
};
|
||||
142
apps/RenderCadenceCompositor/render/Bgra8ReadbackPipeline.cpp
Normal file
142
apps/RenderCadenceCompositor/render/Bgra8ReadbackPipeline.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "Bgra8ReadbackPipeline.h"
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::Initialize(unsigned width, unsigned height, std::size_t pboDepth)
|
||||
{
|
||||
Shutdown();
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mRowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, width);
|
||||
if (mWidth == 0 || mHeight == 0 || mRowBytes == 0)
|
||||
return false;
|
||||
|
||||
if (!CreateRenderTarget())
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
|
||||
if (!mPboRing.Initialize(pboDepth, byteCount))
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::Shutdown()
|
||||
{
|
||||
mPboRing.Shutdown();
|
||||
DestroyRenderTarget();
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
mRowBytes = 0;
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame)
|
||||
{
|
||||
if (mFramebuffer == 0 || !renderFrame)
|
||||
return false;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
renderFrame(frameIndex);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
const AcquireFrameCallback& acquireFrame,
|
||||
const PublishFrameCallback& publishFrame,
|
||||
const CounterCallback& onAcquireMiss,
|
||||
const CounterCallback& onCompleted)
|
||||
{
|
||||
if (!acquireFrame || !publishFrame)
|
||||
return;
|
||||
|
||||
PboReadbackRing::CompletedReadback readback;
|
||||
while (mPboRing.TryAcquireCompleted(readback))
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (!mapped)
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (acquireFrame(frame))
|
||||
{
|
||||
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * static_cast<std::size_t>(frame.height);
|
||||
if (frame.bytes != nullptr && byteCount <= readback.byteCount)
|
||||
{
|
||||
std::memcpy(frame.bytes, mapped, byteCount);
|
||||
frame.frameIndex = readback.frameIndex;
|
||||
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
publishFrame(frame);
|
||||
if (onCompleted)
|
||||
onCompleted();
|
||||
}
|
||||
}
|
||||
else if (onAcquireMiss)
|
||||
{
|
||||
onAcquireMiss();
|
||||
}
|
||||
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
}
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::CreateRenderTarget()
|
||||
{
|
||||
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 Bgra8ReadbackPipeline::DestroyRenderTarget()
|
||||
{
|
||||
if (mFramebuffer != 0)
|
||||
glDeleteFramebuffers(1, &mFramebuffer);
|
||||
if (mTexture != 0)
|
||||
glDeleteTextures(1, &mTexture);
|
||||
mFramebuffer = 0;
|
||||
mTexture = 0;
|
||||
}
|
||||
52
apps/RenderCadenceCompositor/render/Bgra8ReadbackPipeline.h
Normal file
52
apps/RenderCadenceCompositor/render/Bgra8ReadbackPipeline.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "PboReadbackRing.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
struct SystemFrame;
|
||||
|
||||
class Bgra8ReadbackPipeline
|
||||
{
|
||||
public:
|
||||
using RenderCallback = std::function<void(uint64_t frameIndex)>;
|
||||
using AcquireFrameCallback = std::function<bool(SystemFrame& frame)>;
|
||||
using PublishFrameCallback = std::function<bool(const SystemFrame& frame)>;
|
||||
using CounterCallback = std::function<void()>;
|
||||
|
||||
Bgra8ReadbackPipeline() = default;
|
||||
Bgra8ReadbackPipeline(const Bgra8ReadbackPipeline&) = delete;
|
||||
Bgra8ReadbackPipeline& operator=(const Bgra8ReadbackPipeline&) = delete;
|
||||
~Bgra8ReadbackPipeline();
|
||||
|
||||
bool Initialize(unsigned width, unsigned height, std::size_t pboDepth);
|
||||
void Shutdown();
|
||||
|
||||
bool RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame);
|
||||
void ConsumeCompleted(
|
||||
const AcquireFrameCallback& acquireFrame,
|
||||
const PublishFrameCallback& publishFrame,
|
||||
const CounterCallback& onAcquireMiss = {},
|
||||
const CounterCallback& onCompleted = {});
|
||||
|
||||
GLuint Framebuffer() const { return mFramebuffer; }
|
||||
unsigned Width() const { return mWidth; }
|
||||
unsigned Height() const { return mHeight; }
|
||||
unsigned RowBytes() const { return mRowBytes; }
|
||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||
|
||||
private:
|
||||
bool CreateRenderTarget();
|
||||
void DestroyRenderTarget();
|
||||
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
unsigned mRowBytes = 0;
|
||||
GLuint mFramebuffer = 0;
|
||||
GLuint mTexture = 0;
|
||||
PboReadbackRing mPboRing;
|
||||
};
|
||||
138
apps/RenderCadenceCompositor/render/PboReadbackRing.cpp
Normal file
138
apps/RenderCadenceCompositor/render/PboReadbackRing.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "PboReadbackRing.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
PboReadbackRing::~PboReadbackRing()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool PboReadbackRing::Initialize(std::size_t depth, std::size_t byteCount)
|
||||
{
|
||||
Shutdown();
|
||||
if (depth == 0 || byteCount == 0)
|
||||
return false;
|
||||
|
||||
mSlots.resize(depth);
|
||||
mByteCount = byteCount;
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
glGenBuffers(1, &slot.pbo);
|
||||
if (slot.pbo == 0)
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PboReadbackRing::Shutdown()
|
||||
{
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
if (slot.fence)
|
||||
glDeleteSync(slot.fence);
|
||||
if (slot.pbo != 0)
|
||||
glDeleteBuffers(1, &slot.pbo);
|
||||
slot = {};
|
||||
}
|
||||
mSlots.clear();
|
||||
mWriteIndex = 0;
|
||||
mReadIndex = 0;
|
||||
mByteCount = 0;
|
||||
}
|
||||
|
||||
bool PboReadbackRing::QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex)
|
||||
{
|
||||
if (mSlots.empty())
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[mWriteIndex];
|
||||
if (slot.inFlight || slot.acquired)
|
||||
{
|
||||
++mQueueMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
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>(mByteCount), nullptr, GL_STREAM_READ);
|
||||
glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 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);
|
||||
|
||||
if (!slot.inFlight)
|
||||
return false;
|
||||
|
||||
mWriteIndex = (mWriteIndex + 1) % mSlots.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PboReadbackRing::TryAcquireCompleted(CompletedReadback& readback)
|
||||
{
|
||||
if (mSlots.empty())
|
||||
return false;
|
||||
|
||||
for (std::size_t checked = 0; checked < mSlots.size(); ++checked)
|
||||
{
|
||||
Slot& slot = mSlots[mReadIndex];
|
||||
if (!slot.inFlight || slot.acquired || slot.fence == nullptr)
|
||||
{
|
||||
mReadIndex = (mReadIndex + 1) % mSlots.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
|
||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||
return false;
|
||||
|
||||
slot.acquired = true;
|
||||
readback.pbo = slot.pbo;
|
||||
readback.frameIndex = slot.frameIndex;
|
||||
readback.byteCount = mByteCount;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PboReadbackRing::ReleaseCompleted(const CompletedReadback& readback)
|
||||
{
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (!slot.acquired || slot.pbo != readback.pbo)
|
||||
continue;
|
||||
ResetSlot(slot);
|
||||
mReadIndex = (index + 1) % mSlots.size();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void PboReadbackRing::DrainCompleted()
|
||||
{
|
||||
for (std::size_t pass = 0; pass < mSlots.size() * 2; ++pass)
|
||||
{
|
||||
CompletedReadback readback;
|
||||
if (!TryAcquireCompleted(readback))
|
||||
break;
|
||||
ReleaseCompleted(readback);
|
||||
}
|
||||
}
|
||||
|
||||
void PboReadbackRing::ResetSlot(Slot& slot)
|
||||
{
|
||||
if (slot.fence)
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
slot.inFlight = false;
|
||||
slot.acquired = false;
|
||||
slot.frameIndex = 0;
|
||||
}
|
||||
52
apps/RenderCadenceCompositor/render/PboReadbackRing.h
Normal file
52
apps/RenderCadenceCompositor/render/PboReadbackRing.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class PboReadbackRing
|
||||
{
|
||||
public:
|
||||
struct CompletedReadback
|
||||
{
|
||||
GLuint pbo = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
std::size_t byteCount = 0;
|
||||
};
|
||||
|
||||
PboReadbackRing() = default;
|
||||
PboReadbackRing(const PboReadbackRing&) = delete;
|
||||
PboReadbackRing& operator=(const PboReadbackRing&) = delete;
|
||||
~PboReadbackRing();
|
||||
|
||||
bool Initialize(std::size_t depth, std::size_t byteCount);
|
||||
void Shutdown();
|
||||
|
||||
bool QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex);
|
||||
bool TryAcquireCompleted(CompletedReadback& readback);
|
||||
void ReleaseCompleted(const CompletedReadback& readback);
|
||||
void DrainCompleted();
|
||||
|
||||
std::size_t Depth() const { return mSlots.size(); }
|
||||
uint64_t QueueMisses() const { return mQueueMisses; }
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
GLuint pbo = 0;
|
||||
GLsync fence = nullptr;
|
||||
bool inFlight = false;
|
||||
bool acquired = false;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
void ResetSlot(Slot& slot);
|
||||
|
||||
std::vector<Slot> mSlots;
|
||||
std::size_t mWriteIndex = 0;
|
||||
std::size_t mReadIndex = 0;
|
||||
std::size_t mByteCount = 0;
|
||||
uint64_t mQueueMisses = 0;
|
||||
};
|
||||
45
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
45
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "RenderCadenceClock.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
||||
{
|
||||
mFrameDuration = std::chrono::duration_cast<Duration>(std::chrono::duration<double, std::milli>(frameDurationMilliseconds));
|
||||
if (mFrameDuration <= Duration::zero())
|
||||
mFrameDuration = std::chrono::milliseconds(16);
|
||||
Reset();
|
||||
}
|
||||
|
||||
void RenderCadenceClock::Reset(TimePoint now)
|
||||
{
|
||||
mNextRenderTime = now;
|
||||
mOverrunCount = 0;
|
||||
mSkippedFrameCount = 0;
|
||||
}
|
||||
|
||||
RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
{
|
||||
Tick tick;
|
||||
if (now < mNextRenderTime)
|
||||
{
|
||||
tick.sleepFor = std::min(Duration(std::chrono::milliseconds(1)), mNextRenderTime - now);
|
||||
return tick;
|
||||
}
|
||||
|
||||
tick.due = true;
|
||||
const Duration lateBy = now - mNextRenderTime;
|
||||
if (lateBy > mFrameDuration)
|
||||
{
|
||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||
++mOverrunCount;
|
||||
mSkippedFrameCount += tick.skippedFrames;
|
||||
}
|
||||
return tick;
|
||||
}
|
||||
|
||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||
{
|
||||
mNextRenderTime += mFrameDuration;
|
||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||
mNextRenderTime = now + mFrameDuration;
|
||||
}
|
||||
36
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
36
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
class RenderCadenceClock
|
||||
{
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Duration = Clock::duration;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
struct Tick
|
||||
{
|
||||
bool due = false;
|
||||
uint64_t skippedFrames = 0;
|
||||
Duration sleepFor = Duration::zero();
|
||||
};
|
||||
|
||||
explicit RenderCadenceClock(double frameDurationMilliseconds = 1000.0 / 60.0);
|
||||
|
||||
void Reset(TimePoint now = Clock::now());
|
||||
Tick Poll(TimePoint now = Clock::now());
|
||||
void MarkRendered(TimePoint now = Clock::now());
|
||||
|
||||
Duration FrameDuration() const { return mFrameDuration; }
|
||||
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
||||
uint64_t OverrunCount() const { return mOverrunCount; }
|
||||
uint64_t SkippedFrameCount() const { return mSkippedFrameCount; }
|
||||
|
||||
private:
|
||||
Duration mFrameDuration;
|
||||
TimePoint mNextRenderTime = Clock::now();
|
||||
uint64_t mOverrunCount = 0;
|
||||
uint64_t mSkippedFrameCount = 0;
|
||||
};
|
||||
181
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
181
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "RenderThread.h"
|
||||
|
||||
#include "../frames/SystemFrameExchange.h"
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "../platform/HiddenGlWindow.h"
|
||||
#include "Bgra8ReadbackPipeline.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "SimpleMotionRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
RenderThread::~RenderThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RenderThread::Start(std::string& error)
|
||||
{
|
||||
if (mThread.joinable())
|
||||
return true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStarted = false;
|
||||
mStartupError.clear();
|
||||
}
|
||||
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
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;
|
||||
lock.unlock();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderThread::Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
RenderThread::Metrics RenderThread::GetMetrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
return mMetrics;
|
||||
}
|
||||
|
||||
void RenderThread::ThreadMain()
|
||||
{
|
||||
HiddenGlWindow window;
|
||||
std::string error;
|
||||
if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent())
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||
return;
|
||||
}
|
||||
if (!ResolveGLExtensions())
|
||||
{
|
||||
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleMotionRenderer renderer;
|
||||
Bgra8ReadbackPipeline readback;
|
||||
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
|
||||
{
|
||||
SignalStartupFailure("Render pipeline initialization failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
RenderCadenceClock clock(mConfig.frameDurationMilliseconds);
|
||||
uint64_t frameIndex = 0;
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
SignalStarted();
|
||||
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
{
|
||||
readback.ConsumeCompleted(
|
||||
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||
[this]() {
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
|
||||
const auto now = RenderCadenceClock::Clock::now();
|
||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||
if (!tick.due)
|
||||
{
|
||||
if (tick.sleepFor > RenderCadenceClock::Duration::zero())
|
||||
std::this_thread::sleep_for(tick.sleepFor);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!readback.RenderAndQueue(frameIndex, [&renderer](uint64_t index) { renderer.RenderFrame(index); }))
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.pboQueueMisses;
|
||||
}
|
||||
|
||||
CountRendered();
|
||||
++frameIndex;
|
||||
clock.MarkRendered(RenderCadenceClock::Clock::now());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
mMetrics.clockOverruns = clock.OverrunCount();
|
||||
mMetrics.skippedFrames = clock.SkippedFrameCount();
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < mConfig.pboDepth * 2; ++i)
|
||||
{
|
||||
readback.ConsumeCompleted(
|
||||
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||
[this]() {
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
}
|
||||
|
||||
readback.Shutdown();
|
||||
renderer.ShutdownGl();
|
||||
window.ClearCurrent();
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void RenderThread::SignalStarted()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStarted = true;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
void RenderThread::SignalStartupFailure(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStartupError = error;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
void RenderThread::CountRendered()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.renderedFrames;
|
||||
}
|
||||
|
||||
void RenderThread::CountCompleted()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.completedReadbacks;
|
||||
}
|
||||
|
||||
void RenderThread::CountAcquireMiss()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.acquireMisses;
|
||||
}
|
||||
68
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
68
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderCadenceClock.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class SystemFrameExchange;
|
||||
|
||||
class RenderThread
|
||||
{
|
||||
public:
|
||||
struct Config
|
||||
{
|
||||
unsigned width = 1920;
|
||||
unsigned height = 1080;
|
||||
double frameDurationMilliseconds = 1000.0 / 59.94;
|
||||
std::size_t pboDepth = 6;
|
||||
};
|
||||
|
||||
struct Metrics
|
||||
{
|
||||
uint64_t renderedFrames = 0;
|
||||
uint64_t completedReadbacks = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t pboQueueMisses = 0;
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t skippedFrames = 0;
|
||||
};
|
||||
|
||||
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
||||
RenderThread(const RenderThread&) = delete;
|
||||
RenderThread& operator=(const RenderThread&) = delete;
|
||||
~RenderThread();
|
||||
|
||||
bool Start(std::string& error);
|
||||
void Stop();
|
||||
|
||||
Metrics GetMetrics() const;
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
void SignalStarted();
|
||||
void SignalStartupFailure(const std::string& error);
|
||||
void CountRendered();
|
||||
void CountCompleted();
|
||||
void CountAcquireMiss();
|
||||
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
Config mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
|
||||
mutable std::mutex mStartupMutex;
|
||||
std::condition_variable mStartupCondition;
|
||||
bool mStarted = false;
|
||||
std::string mStartupError;
|
||||
|
||||
mutable std::mutex mMetricsMutex;
|
||||
Metrics mMetrics;
|
||||
};
|
||||
50
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.cpp
Normal file
50
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "SimpleMotionRenderer.h"
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||
{
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
return mWidth > 0 && mHeight > 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::RenderFrame(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));
|
||||
|
||||
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 = (std::max)(1, static_cast<int>(mWidth / 6));
|
||||
const int boxHeight = (std::max)(1, 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 - static_cast<unsigned>(boxWidth)));
|
||||
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - static_cast<unsigned>(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);
|
||||
|
||||
const int stripeWidth = (std::max)(1, static_cast<int>(mWidth / 80));
|
||||
const int stripeX = static_cast<int>((frameIndex % 120) * (mWidth - static_cast<unsigned>(stripeWidth)) / 119);
|
||||
glScissor(stripeX, 0, stripeWidth, static_cast<GLsizei>(mHeight));
|
||||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::ShutdownGl()
|
||||
{
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
}
|
||||
20
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.h
Normal file
20
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class SimpleMotionRenderer
|
||||
{
|
||||
public:
|
||||
SimpleMotionRenderer() = default;
|
||||
|
||||
bool InitializeGl(unsigned width, unsigned height);
|
||||
void RenderFrame(uint64_t frameIndex);
|
||||
void ShutdownGl();
|
||||
|
||||
unsigned Width() const { return mWidth; }
|
||||
unsigned Height() const { return mHeight; }
|
||||
|
||||
private:
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
};
|
||||
89
apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h
Normal file
89
apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct CadenceTelemetrySnapshot
|
||||
{
|
||||
double sampleSeconds = 0.0;
|
||||
double renderFps = 0.0;
|
||||
double scheduleFps = 0.0;
|
||||
std::size_t freeFrames = 0;
|
||||
std::size_t completedFrames = 0;
|
||||
std::size_t scheduledFrames = 0;
|
||||
uint64_t renderedTotal = 0;
|
||||
uint64_t scheduledTotal = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
bool deckLinkBufferedAvailable = false;
|
||||
uint64_t deckLinkBuffered = 0;
|
||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class CadenceTelemetry
|
||||
{
|
||||
public:
|
||||
template <typename SystemFrameExchange, typename OutputThread>
|
||||
CadenceTelemetrySnapshot Sample(
|
||||
const SystemFrameExchange& exchange,
|
||||
const DeckLinkOutput& output,
|
||||
const OutputThread& outputThread)
|
||||
{
|
||||
const auto now = Clock::now();
|
||||
const double seconds = mHasLastSample
|
||||
? std::chrono::duration_cast<std::chrono::duration<double>>(now - mLastSampleTime).count()
|
||||
: 0.0;
|
||||
|
||||
const auto exchangeMetrics = exchange.Metrics();
|
||||
const DeckLinkOutputMetrics outputMetrics = output.Metrics();
|
||||
const auto threadMetrics = outputThread.Metrics();
|
||||
|
||||
CadenceTelemetrySnapshot snapshot;
|
||||
snapshot.sampleSeconds = seconds;
|
||||
snapshot.renderedTotal = exchangeMetrics.completedFrames;
|
||||
snapshot.scheduledTotal = exchangeMetrics.scheduledFrames;
|
||||
snapshot.freeFrames = exchangeMetrics.freeCount;
|
||||
snapshot.completedFrames = exchangeMetrics.completedCount;
|
||||
snapshot.scheduledFrames = exchangeMetrics.scheduledCount;
|
||||
snapshot.completedPollMisses = threadMetrics.completedPollMisses;
|
||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||
? outputMetrics.scheduleFailures
|
||||
: threadMetrics.scheduleFailures;
|
||||
snapshot.completions = outputMetrics.completions;
|
||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||
snapshot.dropped = outputMetrics.dropped;
|
||||
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||
|
||||
if (mHasLastSample && seconds > 0.0)
|
||||
{
|
||||
snapshot.renderFps = static_cast<double>(snapshot.renderedTotal - mLastRenderedFrames) / seconds;
|
||||
snapshot.scheduleFps = static_cast<double>(snapshot.scheduledTotal - mLastScheduledFrames) / seconds;
|
||||
}
|
||||
|
||||
mLastSampleTime = now;
|
||||
mLastRenderedFrames = snapshot.renderedTotal;
|
||||
mLastScheduledFrames = snapshot.scheduledTotal;
|
||||
mHasLastSample = true;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
Clock::time_point mLastSampleTime = Clock::now();
|
||||
uint64_t mLastRenderedFrames = 0;
|
||||
uint64_t mLastScheduledFrames = 0;
|
||||
bool mHasLastSample = false;
|
||||
};
|
||||
}
|
||||
86
apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h
Normal file
86
apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include "CadenceTelemetry.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct TelemetryPrinterConfig
|
||||
{
|
||||
std::chrono::milliseconds interval = std::chrono::seconds(1);
|
||||
};
|
||||
|
||||
class TelemetryPrinter
|
||||
{
|
||||
public:
|
||||
explicit TelemetryPrinter(TelemetryPrinterConfig config = TelemetryPrinterConfig()) :
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
TelemetryPrinter(const TelemetryPrinter&) = delete;
|
||||
TelemetryPrinter& operator=(const TelemetryPrinter&) = delete;
|
||||
|
||||
~TelemetryPrinter()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
template <typename SystemFrameExchange, typename OutputThread>
|
||||
void Start(const SystemFrameExchange& exchange, const DeckLinkOutput& output, const OutputThread& outputThread)
|
||||
{
|
||||
if (mRunning)
|
||||
return;
|
||||
mStopping = false;
|
||||
mThread = std::thread([this, &exchange, &output, &outputThread]() {
|
||||
CadenceTelemetry telemetry;
|
||||
while (!mStopping)
|
||||
{
|
||||
std::this_thread::sleep_for(mConfig.interval);
|
||||
Print(telemetry.Sample(exchange, output, outputThread));
|
||||
}
|
||||
});
|
||||
mRunning = true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
private:
|
||||
static void Print(const CadenceTelemetrySnapshot& snapshot)
|
||||
{
|
||||
std::cout << std::fixed << std::setprecision(1)
|
||||
<< "renderFps=" << snapshot.renderFps
|
||||
<< " scheduleFps=" << snapshot.scheduleFps
|
||||
<< " free=" << snapshot.freeFrames
|
||||
<< " completed=" << snapshot.completedFrames
|
||||
<< " scheduled=" << snapshot.scheduledFrames
|
||||
<< " completedPollMisses=" << snapshot.completedPollMisses
|
||||
<< " scheduleFailures=" << snapshot.scheduleFailures
|
||||
<< " completions=" << snapshot.completions
|
||||
<< " late=" << snapshot.displayedLate
|
||||
<< " dropped=" << snapshot.dropped
|
||||
<< " decklinkBuffered=";
|
||||
if (snapshot.deckLinkBufferedAvailable)
|
||||
std::cout << snapshot.deckLinkBuffered;
|
||||
else
|
||||
std::cout << "n/a";
|
||||
std::cout << " scheduleCallMs=" << snapshot.deckLinkScheduleCallMilliseconds << "\n";
|
||||
}
|
||||
|
||||
TelemetryPrinterConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
};
|
||||
}
|
||||
105
apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp
Normal file
105
apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "DeckLinkOutput.h"
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
DeckLinkOutput::~DeckLinkOutput()
|
||||
{
|
||||
ReleaseResources();
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error)
|
||||
{
|
||||
mConfig = config;
|
||||
mCompletionCallback = completionCallback;
|
||||
|
||||
VideoFormatSelection formats;
|
||||
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||
return false;
|
||||
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||
return false;
|
||||
if (!mSession.ConfigureOutput(
|
||||
[this](const VideoIOCompletion& completion) { HandleCompletion(completion); },
|
||||
formats.output,
|
||||
config.externalKeyingEnabled,
|
||||
error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!mSession.PrepareOutputSchedule())
|
||||
{
|
||||
error = "DeckLink output schedule preparation failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::StartScheduledPlayback(std::string& error)
|
||||
{
|
||||
if (mSession.StartScheduledPlayback())
|
||||
return true;
|
||||
error = "DeckLink scheduled playback failed to start.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
|
||||
{
|
||||
return mSession.ScheduleOutputFrame(frame);
|
||||
}
|
||||
|
||||
void DeckLinkOutput::Stop()
|
||||
{
|
||||
mSession.Stop();
|
||||
}
|
||||
|
||||
void DeckLinkOutput::ReleaseResources()
|
||||
{
|
||||
mSession.ReleaseResources();
|
||||
}
|
||||
|
||||
const VideoIOState& DeckLinkOutput::State() const
|
||||
{
|
||||
return mSession.State();
|
||||
}
|
||||
|
||||
DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
||||
{
|
||||
DeckLinkOutputMetrics metrics;
|
||||
metrics.completions = mCompletions.load();
|
||||
metrics.displayedLate = mDisplayedLate.load();
|
||||
metrics.dropped = mDropped.load();
|
||||
metrics.flushed = mFlushed.load();
|
||||
|
||||
const VideoIOState& state = mSession.State();
|
||||
metrics.scheduleFailures = state.deckLinkScheduleFailureCount;
|
||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void DeckLinkOutput::HandleCompletion(const VideoIOCompletion& completion)
|
||||
{
|
||||
++mCompletions;
|
||||
switch (completion.result)
|
||||
{
|
||||
case VideoIOCompletionResult::DisplayedLate:
|
||||
++mDisplayedLate;
|
||||
break;
|
||||
case VideoIOCompletionResult::Dropped:
|
||||
++mDropped;
|
||||
break;
|
||||
case VideoIOCompletionResult::Flushed:
|
||||
++mFlushed;
|
||||
break;
|
||||
case VideoIOCompletionResult::Completed:
|
||||
case VideoIOCompletionResult::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (mCompletionCallback)
|
||||
mCompletionCallback(completion);
|
||||
}
|
||||
}
|
||||
61
apps/RenderCadenceCompositor/video/DeckLinkOutput.h
Normal file
61
apps/RenderCadenceCompositor/video/DeckLinkOutput.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkSession.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputConfig
|
||||
{
|
||||
bool externalKeyingEnabled = false;
|
||||
bool outputAlphaRequired = false;
|
||||
};
|
||||
|
||||
struct DeckLinkOutputMetrics
|
||||
{
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
uint64_t flushed = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
bool actualBufferedFramesAvailable = false;
|
||||
uint64_t actualBufferedFrames = 0;
|
||||
double scheduleCallMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class DeckLinkOutput
|
||||
{
|
||||
public:
|
||||
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
||||
|
||||
DeckLinkOutput() = default;
|
||||
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
||||
DeckLinkOutput& operator=(const DeckLinkOutput&) = delete;
|
||||
~DeckLinkOutput();
|
||||
|
||||
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
||||
bool StartScheduledPlayback(std::string& error);
|
||||
bool ScheduleFrame(const VideoIOOutputFrame& frame);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
|
||||
const VideoIOState& State() const;
|
||||
DeckLinkOutputMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
void HandleCompletion(const VideoIOCompletion& completion);
|
||||
|
||||
DeckLinkSession mSession;
|
||||
DeckLinkOutputConfig mConfig;
|
||||
CompletionCallback mCompletionCallback;
|
||||
std::atomic<uint64_t> mCompletions{ 0 };
|
||||
std::atomic<uint64_t> mDisplayedLate{ 0 };
|
||||
std::atomic<uint64_t> mDropped{ 0 };
|
||||
std::atomic<uint64_t> mFlushed{ 0 };
|
||||
};
|
||||
}
|
||||
124
apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h
Normal file
124
apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h
Normal file
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "DeckLinkOutput.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputThreadConfig
|
||||
{
|
||||
std::size_t targetBufferedFrames = 4;
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
|
||||
};
|
||||
|
||||
struct DeckLinkOutputThreadMetrics
|
||||
{
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
};
|
||||
|
||||
template <typename SystemFrameExchange>
|
||||
class DeckLinkOutputThread
|
||||
{
|
||||
public:
|
||||
DeckLinkOutputThread(DeckLinkOutput& output, SystemFrameExchange& exchange, DeckLinkOutputThreadConfig config = DeckLinkOutputThreadConfig()) :
|
||||
mOutput(output),
|
||||
mExchange(exchange),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkOutputThread(const DeckLinkOutputThread&) = delete;
|
||||
DeckLinkOutputThread& operator=(const DeckLinkOutputThread&) = delete;
|
||||
|
||||
~DeckLinkOutputThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start()
|
||||
{
|
||||
if (mRunning)
|
||||
return true;
|
||||
mStopping = false;
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
mRunning = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
DeckLinkOutputThreadMetrics Metrics() const
|
||||
{
|
||||
DeckLinkOutputThreadMetrics metrics;
|
||||
metrics.scheduledFrames = mScheduledFrames.load();
|
||||
metrics.completedPollMisses = mCompletedPollMisses.load();
|
||||
metrics.scheduleFailures = mScheduleFailures.load();
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private:
|
||||
void ThreadMain()
|
||||
{
|
||||
while (!mStopping)
|
||||
{
|
||||
const auto exchangeMetrics = mExchange.Metrics();
|
||||
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
|
||||
{
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
++mCompletedPollMisses;
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
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 (!mOutput.ScheduleFrame(outputFrame))
|
||||
{
|
||||
++mScheduleFailures;
|
||||
mExchange.ReleaseScheduledByBytes(frame.bytes);
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
++mScheduledFrames;
|
||||
}
|
||||
}
|
||||
|
||||
DeckLinkOutput& mOutput;
|
||||
SystemFrameExchange& mExchange;
|
||||
DeckLinkOutputThreadConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
std::atomic<uint64_t> mScheduledFrames{ 0 };
|
||||
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
|
||||
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user