INput
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-12 18:39:08 +10:00
parent 6e32941675
commit 0a8b335048
14 changed files with 822 additions and 24 deletions

View File

@@ -318,6 +318,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h" "${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.h"
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp"
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h"
@@ -327,6 +329,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/logging/Logger.h" "${RENDER_CADENCE_APP_DIR}/logging/Logger.h"
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp" "${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp"
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h" "${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h"
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.cpp"
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.h"
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.cpp" "${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.cpp"
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.h" "${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.h"
"${RENDER_CADENCE_APP_DIR}/render/readback/PboReadbackRing.cpp" "${RENDER_CADENCE_APP_DIR}/render/readback/PboReadbackRing.cpp"
@@ -360,6 +364,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp"
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"
"${RENDER_CADENCE_APP_DIR}/video/SyntheticInputProducer.cpp"
"${RENDER_CADENCE_APP_DIR}/video/SyntheticInputProducer.h"
) )
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES}) add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
@@ -790,6 +796,23 @@ endif()
add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests) add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests)
add_executable(RenderCadenceCompositorInputFrameMailboxTests
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp"
)
target_include_directories(RenderCadenceCompositorInputFrameMailboxTests PRIVATE
"${APP_DIR}/videoio"
"${RENDER_CADENCE_APP_DIR}/frames"
)
if(MSVC)
target_compile_options(RenderCadenceCompositorInputFrameMailboxTests PRIVATE /W3)
endif()
add_test(NAME RenderCadenceCompositorInputFrameMailboxTests COMMAND RenderCadenceCompositorInputFrameMailboxTests)
add_executable(RenderCadenceCompositorClockTests add_executable(RenderCadenceCompositorClockTests
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp" "${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp"

View File

@@ -11,10 +11,17 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
```text ```text
RenderThread RenderThread
owns a hidden OpenGL context owns a hidden OpenGL context
polls latest input frames without waiting
uploads input frames into a render-owned GL texture
renders simple BGRA8 motion at selected cadence renders simple BGRA8 motion at selected cadence
queues async PBO readback queues async PBO readback
publishes completed frames into SystemFrameExchange publishes completed frames into SystemFrameExchange
InputFrameMailbox
owns latest disposable CPU input slots
drops older unsampled input frames when newer frames arrive
protects the one frame currently being uploaded by render
SystemFrameExchange SystemFrameExchange
owns Free / Rendering / Completed / Scheduled slots owns Free / Rendering / Completed / Scheduled slots
drops old completed unscheduled frames when render needs space drops old completed unscheduled frames when render needs space
@@ -37,6 +44,9 @@ Included now:
- hidden render-thread-owned OpenGL context - hidden render-thread-owned OpenGL context
- simple smooth-motion renderer - simple smooth-motion renderer
- BGRA8-only output - BGRA8-only output
- synthetic BGRA8 input producer
- non-blocking latest-frame input mailbox
- render-thread-owned input texture upload
- async PBO readback - async PBO readback
- latest-N system-memory frame exchange - latest-N system-memory frame exchange
- rendered-frame warmup - rendered-frame warmup
@@ -55,18 +65,24 @@ Included now:
- JSON serialization for cadence telemetry snapshots - JSON serialization for cadence telemetry snapshots
- background logging with `log`, `warning`, and `error` levels - background logging with `log`, `warning`, and `error` levels
- local HTTP control server matching the OpenAPI route surface - local HTTP control server matching the OpenAPI route surface
- HTTP layer controls for add, remove, reorder, bypass, shader change, parameter update, and parameter reset
- trigger parameters as latest-pulse controls with shader-visible count/time
- startup config provider for `config/runtime-host.json` - startup config provider for `config/runtime-host.json`
- quiet telemetry health monitor - quiet telemetry health monitor
- non-GL frame-exchange tests - non-GL frame-exchange tests
- non-GL input-mailbox tests
Intentionally not included yet: Intentionally not included yet:
- DeckLink input - real DeckLink input capture
- input format conversion
- temporal/history/feedback shader storage - temporal/history/feedback shader storage
- texture/LUT asset upload - texture/LUT asset upload
- text-parameter rasterization - text-parameter rasterization
- runtime state - runtime state
- OSC/API control - OSC control
- persistent control/state writes
- trigger event history for stacked repeated pulses
- preview - preview
- screenshots - screenshots
- persistence - persistence
@@ -99,16 +115,22 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [x] Startup config loading from `config/runtime-host.json` - [x] Startup config loading from `config/runtime-host.json`
- [x] Cadence telemetry JSON - [x] Cadence telemetry JSON
- [x] Health logging for schedule/drop/starvation events - [x] Health logging for schedule/drop/starvation events
- [x] Runtime parameter updates from HTTP controls
- [x] Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
- [x] Trigger parameter pulse count/time for latest trigger events
- [x] Synthetic BGRA8 frame input producer
- [x] Latest-frame CPU input mailbox
- [x] Render-owned input texture upload
- [x] Runtime shaders receive input through `gVideoInput`
- [ ] DeckLink input capture - [ ] DeckLink input capture
- [ ] Input frame upload into the render scene - [ ] Live DeckLink input bound to `gVideoInput`
- [ ] Live video input bound to `gVideoInput` - [ ] Input format conversion/scaling
- [ ] Temporal history buffers - [ ] Temporal history buffers
- [ ] Feedback buffers - [ ] Feedback buffers
- [ ] Texture asset loading and upload - [ ] Texture asset loading and upload
- [ ] LUT asset loading and upload - [ ] LUT asset loading and upload
- [ ] Text parameter rasterization - [ ] Text parameter rasterization
- [ ] Runtime parameter updates from controls - [ ] Trigger history/event buffers for overlapping repeated trigger effects
- [ ] Layer reorder/bypass/set-shader/update-parameter/reset-parameter controls
- [ ] Full runtime state store/read model - [ ] Full runtime state store/read model
- [ ] Persistent layer stack/config writes - [ ] Persistent layer stack/config writes
- [ ] OSC ingress - [ ] OSC ingress
@@ -256,13 +278,17 @@ Current runtime shader support is deliberately limited to stateless full-frame p
- no feedback storage - no feedback storage
- no texture/LUT assets yet - no texture/LUT assets yet
- no text parameters yet - no text parameters yet
- manifest defaults are used for parameters - manifest defaults initialize parameters
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
- repeated trigger history is not stored yet, so effects such as `trigger-ripple` restart from the latest trigger rather than accumulating overlapping ripples
- the first layer receives a small fallback source texture until DeckLink input is added - the first layer receives a small fallback source texture until DeckLink input is added
- stacked layers receive the previous ready layer output through both `gVideoInput` and `gLayerInput` - the first layer receives the latest synthetic input texture through both `gVideoInput` and `gLayerInput` when input frames are available
- stacked layers receive the original input through `gVideoInput` and the previous ready layer output through `gLayerInput`
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now. The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds. Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target. When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
@@ -314,6 +340,9 @@ This app keeps the same core behavior but splits it into modules that can grow:
- `frames/`: system-memory handoff - `frames/`: system-memory handoff
- `platform/`: COM/Win32/hidden GL context support - `platform/`: COM/Win32/hidden GL context support
- `render/`: cadence thread, clock, and simple renderer - `render/`: cadence thread, clock, and simple renderer
- `frames/InputFrameMailbox`: non-blocking latest-frame CPU input handoff
- `video/SyntheticInputProducer`: temporary BGRA8 test-pattern producer for proving the frame-input path
- `render/InputFrameTexture`: render-thread-owned upload of the latest CPU input frame into GL
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication - `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers - `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker - `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
@@ -336,4 +365,4 @@ Only after this app matches the probe's smooth output:
3. port runtime snapshots/live state 3. port runtime snapshots/live state
4. add control services 4. add control services
5. add preview/screenshot from system-memory frames 5. add preview/screenshot from system-memory frames
6. add DeckLink input as a CPU latest-frame mailbox 6. replace synthetic input with DeckLink input capture into the existing CPU latest-frame mailbox

View File

@@ -1,9 +1,11 @@
#include "app/AppConfig.h" #include "app/AppConfig.h"
#include "app/AppConfigProvider.h" #include "app/AppConfigProvider.h"
#include "app/RenderCadenceApp.h" #include "app/RenderCadenceApp.h"
#include "frames/InputFrameMailbox.h"
#include "frames/SystemFrameExchange.h" #include "frames/SystemFrameExchange.h"
#include "logging/Logger.h" #include "logging/Logger.h"
#include "render/RenderThread.h" #include "render/RenderThread.h"
#include "video/SyntheticInputProducer.h"
#include "VideoIOFormat.h" #include "VideoIOFormat.h"
#include <windows.h> #include <windows.h>
@@ -80,13 +82,33 @@ int main(int argc, char** argv)
SystemFrameExchange frameExchange(frameExchangeConfig); SystemFrameExchange frameExchange(frameExchangeConfig);
InputFrameMailboxConfig inputMailboxConfig;
RenderCadenceCompositor::VideoFormatDimensions(
appConfig.inputVideoFormat,
inputMailboxConfig.width,
inputMailboxConfig.height);
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
inputMailboxConfig.capacity = 4;
InputFrameMailbox inputMailbox(inputMailboxConfig);
RenderCadenceCompositor::SyntheticInputProducerConfig inputProducerConfig;
inputProducerConfig.width = inputMailboxConfig.width;
inputProducerConfig.height = inputMailboxConfig.height;
inputProducerConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.inputFrameRate);
RenderCadenceCompositor::SyntheticInputProducer syntheticInput(inputMailbox, inputProducerConfig);
if (syntheticInput.Start())
RenderCadenceCompositor::Log("app", "Synthetic BGRA8 input producer started.");
else
RenderCadenceCompositor::LogWarning("app", "Synthetic input producer did not start; shaders will use fallback input.");
RenderThread::Config renderConfig; RenderThread::Config renderConfig;
renderConfig.width = frameExchangeConfig.width; renderConfig.width = frameExchangeConfig.width;
renderConfig.height = frameExchangeConfig.height; renderConfig.height = frameExchangeConfig.height;
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate); renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
renderConfig.pboDepth = 6; renderConfig.pboDepth = 6;
RenderThread renderThread(frameExchange, renderConfig); RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig); RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
@@ -94,6 +116,7 @@ int main(int argc, char** argv)
if (!app.Start(error)) if (!app.Start(error))
{ {
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error); RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
syntheticInput.Stop();
RenderCadenceCompositor::Logger::Instance().Stop(); RenderCadenceCompositor::Logger::Instance().Stop();
return 1; return 1;
} }
@@ -101,6 +124,7 @@ int main(int argc, char** argv)
std::string line; std::string line;
std::getline(std::cin, line); std::getline(std::cin, line);
app.Stop(); app.Stop();
syntheticInput.Stop();
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped."); RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
RenderCadenceCompositor::Logger::Instance().Stop(); RenderCadenceCompositor::Logger::Instance().Stop();
return 0; return 0;

View File

@@ -0,0 +1,233 @@
#include "InputFrameMailbox.h"
#include <algorithm>
#include <cstring>
namespace
{
InputFrameMailboxConfig NormalizeConfig(InputFrameMailboxConfig config)
{
if (config.rowBytes == 0)
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
return config;
}
}
InputFrameMailbox::InputFrameMailbox(const InputFrameMailboxConfig& config)
{
Configure(config);
}
void InputFrameMailbox::Configure(const InputFrameMailboxConfig& config)
{
std::lock_guard<std::mutex> lock(mMutex);
mConfig = NormalizeConfig(config);
mReadyIndices.clear();
mSlots.clear();
mSlots.resize(mConfig.capacity);
const std::size_t byteCount = FrameByteCount();
for (Slot& slot : mSlots)
{
slot.bytes.resize(byteCount);
slot.state = InputFrameSlotState::Free;
slot.frameIndex = 0;
++slot.generation;
}
mCounters = InputFrameMailboxMetrics();
}
InputFrameMailboxConfig InputFrameMailbox::Config() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mConfig;
}
bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex)
{
if (bytes == nullptr || rowBytes == 0)
return false;
std::lock_guard<std::mutex> lock(mMutex);
if (mSlots.empty() || mConfig.width == 0 || mConfig.height == 0)
return false;
std::size_t slotIndex = mSlots.size();
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
if (mSlots[index].state == InputFrameSlotState::Free)
{
slotIndex = index;
break;
}
}
if (slotIndex == mSlots.size())
{
if (!DropOldestReadyLocked())
{
++mCounters.submitMisses;
return false;
}
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
if (mSlots[index].state == InputFrameSlotState::Free)
{
slotIndex = index;
break;
}
}
}
if (slotIndex == mSlots.size())
{
++mCounters.submitMisses;
return false;
}
Slot& slot = mSlots[slotIndex];
const std::size_t destinationRowBytes = mConfig.rowBytes;
const std::size_t copyRowBytes = (std::min)(static_cast<std::size_t>(rowBytes), destinationRowBytes);
const unsigned char* source = static_cast<const unsigned char*>(bytes);
for (unsigned y = 0; y < mConfig.height; ++y)
{
std::memcpy(slot.bytes.data() + static_cast<std::size_t>(y) * destinationRowBytes, source + static_cast<std::size_t>(y) * rowBytes, copyRowBytes);
}
slot.state = InputFrameSlotState::Ready;
slot.frameIndex = frameIndex;
++slot.generation;
mReadyIndices.push_back(slotIndex);
++mCounters.submittedFrames;
mCounters.latestFrameIndex = frameIndex;
return true;
}
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
{
std::lock_guard<std::mutex> lock(mMutex);
while (!mReadyIndices.empty())
{
const std::size_t index = mReadyIndices.back();
mReadyIndices.pop_back();
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
continue;
while (!mReadyIndices.empty())
{
const std::size_t olderIndex = mReadyIndices.front();
mReadyIndices.pop_front();
if (olderIndex >= mSlots.size() || mSlots[olderIndex].state != InputFrameSlotState::Ready)
continue;
mSlots[olderIndex].state = InputFrameSlotState::Free;
++mSlots[olderIndex].generation;
++mCounters.droppedReadyFrames;
}
mSlots[index].state = InputFrameSlotState::Reading;
FillFrameLocked(index, frame);
++mCounters.consumedFrames;
return true;
}
frame = InputFrame();
++mCounters.consumeMisses;
return false;
}
bool InputFrameMailbox::Release(const InputFrame& frame)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!IsValidLocked(frame))
return false;
Slot& slot = mSlots[frame.index];
if (slot.state != InputFrameSlotState::Reading)
return false;
slot.state = InputFrameSlotState::Free;
slot.frameIndex = 0;
++slot.generation;
return true;
}
void InputFrameMailbox::Clear()
{
std::lock_guard<std::mutex> lock(mMutex);
mReadyIndices.clear();
for (Slot& slot : mSlots)
{
slot.state = InputFrameSlotState::Free;
slot.frameIndex = 0;
++slot.generation;
}
}
InputFrameMailboxMetrics InputFrameMailbox::Metrics() const
{
std::lock_guard<std::mutex> lock(mMutex);
InputFrameMailboxMetrics metrics = mCounters;
metrics.capacity = mSlots.size();
for (const Slot& slot : mSlots)
{
switch (slot.state)
{
case InputFrameSlotState::Free:
++metrics.freeCount;
break;
case InputFrameSlotState::Ready:
++metrics.readyCount;
break;
case InputFrameSlotState::Reading:
++metrics.readingCount;
break;
}
}
return metrics;
}
bool InputFrameMailbox::IsValidLocked(const InputFrame& frame) const
{
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
}
void InputFrameMailbox::FillFrameLocked(std::size_t index, InputFrame& frame) const
{
const 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;
}
bool InputFrameMailbox::DropOldestReadyLocked()
{
while (!mReadyIndices.empty())
{
const std::size_t index = mReadyIndices.front();
mReadyIndices.pop_front();
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
continue;
mSlots[index].state = InputFrameSlotState::Free;
mSlots[index].frameIndex = 0;
++mSlots[index].generation;
++mCounters.droppedReadyFrames;
return true;
}
return false;
}
std::size_t InputFrameMailbox::FrameByteCount() const
{
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
}

View File

@@ -0,0 +1,87 @@
#pragma once
#include "VideoIOFormat.h"
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
enum class InputFrameSlotState
{
Free,
Ready,
Reading
};
struct InputFrameMailboxConfig
{
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
unsigned rowBytes = 0;
std::size_t capacity = 0;
};
struct InputFrame
{
const 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 InputFrameMailboxMetrics
{
std::size_t capacity = 0;
std::size_t freeCount = 0;
std::size_t readyCount = 0;
std::size_t readingCount = 0;
uint64_t submittedFrames = 0;
uint64_t consumedFrames = 0;
uint64_t droppedReadyFrames = 0;
uint64_t submitMisses = 0;
uint64_t consumeMisses = 0;
uint64_t latestFrameIndex = 0;
};
class InputFrameMailbox
{
public:
InputFrameMailbox() = default;
explicit InputFrameMailbox(const InputFrameMailboxConfig& config);
void Configure(const InputFrameMailboxConfig& config);
InputFrameMailboxConfig Config() const;
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
bool TryAcquireLatest(InputFrame& frame);
bool Release(const InputFrame& frame);
void Clear();
InputFrameMailboxMetrics Metrics() const;
private:
struct Slot
{
std::vector<unsigned char> bytes;
InputFrameSlotState state = InputFrameSlotState::Free;
uint64_t frameIndex = 0;
uint64_t generation = 0;
};
bool IsValidLocked(const InputFrame& frame) const;
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
bool DropOldestReadyLocked();
std::size_t FrameByteCount() const;
mutable std::mutex mMutex;
InputFrameMailboxConfig mConfig;
std::vector<Slot> mSlots;
std::deque<std::size_t> mReadyIndices;
InputFrameMailboxMetrics mCounters;
};

View File

@@ -0,0 +1,83 @@
#include "InputFrameTexture.h"
InputFrameTexture::~InputFrameTexture()
{
ShutdownGl();
}
GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
{
if (mailbox == nullptr)
return mTexture;
InputFrame frame;
if (!mailbox->TryAcquireLatest(frame))
{
++mUploadMisses;
return mTexture;
}
if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Bgra8 && EnsureTexture(frame))
{
glBindTexture(GL_TEXTURE_2D, mTexture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
glTexSubImage2D(
GL_TEXTURE_2D,
0,
0,
0,
static_cast<GLsizei>(frame.width),
static_cast<GLsizei>(frame.height),
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
frame.bytes);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glBindTexture(GL_TEXTURE_2D, 0);
++mUploadedFrames;
}
mailbox->Release(frame);
return mTexture;
}
void InputFrameTexture::ShutdownGl()
{
if (mTexture != 0)
glDeleteTextures(1, &mTexture);
mTexture = 0;
mWidth = 0;
mHeight = 0;
}
bool InputFrameTexture::EnsureTexture(const InputFrame& frame)
{
if (frame.width == 0 || frame.height == 0)
return false;
if (mTexture != 0 && mWidth == frame.width && mHeight == frame.height)
return true;
ShutdownGl();
glGenTextures(1, &mTexture);
glBindTexture(GL_TEXTURE_2D, mTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
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>(frame.width),
static_cast<GLsizei>(frame.height),
0,
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
mWidth = frame.width;
mHeight = frame.height;
return mTexture != 0;
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include "../frames/InputFrameMailbox.h"
#include "GLExtensions.h"
#include <cstdint>
class InputFrameTexture
{
public:
InputFrameTexture() = default;
InputFrameTexture(const InputFrameTexture&) = delete;
InputFrameTexture& operator=(const InputFrameTexture&) = delete;
~InputFrameTexture();
GLuint PollAndUpload(InputFrameMailbox* mailbox);
GLuint Texture() const { return mTexture; }
uint64_t UploadedFrames() const { return mUploadedFrames; }
uint64_t UploadMisses() const { return mUploadMisses; }
void ShutdownGl();
private:
bool EnsureTexture(const InputFrame& frame);
GLuint mTexture = 0;
unsigned mWidth = 0;
unsigned mHeight = 0;
uint64_t mUploadedFrames = 0;
uint64_t mUploadMisses = 0;
};

View File

@@ -1,9 +1,11 @@
#include "RenderThread.h" #include "RenderThread.h"
#include "../frames/InputFrameMailbox.h"
#include "../frames/SystemFrameExchange.h" #include "../frames/SystemFrameExchange.h"
#include "../frames/SystemFrameTypes.h" #include "../frames/SystemFrameTypes.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../platform/HiddenGlWindow.h" #include "../platform/HiddenGlWindow.h"
#include "InputFrameTexture.h"
#include "readback/Bgra8ReadbackPipeline.h" #include "readback/Bgra8ReadbackPipeline.h"
#include "GLExtensions.h" #include "GLExtensions.h"
#include "runtime/RuntimeRenderScene.h" #include "runtime/RuntimeRenderScene.h"
@@ -20,6 +22,13 @@ RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
{ {
} }
RenderThread::RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config) :
mFrameExchange(frameExchange),
mInputMailbox(inputMailbox),
mConfig(config)
{
}
RenderThread::~RenderThread() RenderThread::~RenderThread()
{ {
Stop(); Stop();
@@ -109,6 +118,7 @@ void RenderThread::ThreadMain()
SimpleMotionRenderer renderer; SimpleMotionRenderer renderer;
RuntimeRenderScene runtimeRenderScene; RuntimeRenderScene runtimeRenderScene;
Bgra8ReadbackPipeline readback; Bgra8ReadbackPipeline readback;
InputFrameTexture inputTexture;
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error)) if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
{ {
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error); SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
@@ -145,9 +155,10 @@ void RenderThread::ThreadMain()
} }
TryCommitReadyRuntimeShader(runtimeRenderScene); TryCommitReadyRuntimeShader(runtimeRenderScene);
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene](uint64_t index) { const GLuint videoInputTexture = inputTexture.PollAndUpload(mInputMailbox);
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene, videoInputTexture](uint64_t index) {
if (runtimeRenderScene.HasLayers()) if (runtimeRenderScene.HasLayers())
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height); runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
else else
renderer.RenderFrame(index); renderer.RenderFrame(index);
})) }))
@@ -175,6 +186,7 @@ void RenderThread::ThreadMain()
} }
readback.Shutdown(); readback.Shutdown();
inputTexture.ShutdownGl();
runtimeRenderScene.ShutdownGl(); runtimeRenderScene.ShutdownGl();
renderer.ShutdownGl(); renderer.ShutdownGl();
window.ClearCurrent(); window.ClearCurrent();

View File

@@ -14,6 +14,7 @@
#include <thread> #include <thread>
class SystemFrameExchange; class SystemFrameExchange;
class InputFrameMailbox;
class RenderThread class RenderThread
{ {
@@ -39,6 +40,7 @@ public:
}; };
RenderThread(SystemFrameExchange& frameExchange, Config config); RenderThread(SystemFrameExchange& frameExchange, Config config);
RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config);
RenderThread(const RenderThread&) = delete; RenderThread(const RenderThread&) = delete;
RenderThread& operator=(const RenderThread&) = delete; RenderThread& operator=(const RenderThread&) = delete;
~RenderThread(); ~RenderThread();
@@ -63,6 +65,7 @@ private:
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers); bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
SystemFrameExchange& mFrameExchange; SystemFrameExchange& mFrameExchange;
InputFrameMailbox* mInputMailbox = nullptr;
Config mConfig; Config mConfig;
std::thread mThread; std::thread mThread;
std::atomic<bool> mStopping{ false }; std::atomic<bool> mStopping{ false };

View File

@@ -120,7 +120,7 @@ bool RuntimeRenderScene::HasLayers()
return false; return false;
} }
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height) void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
{ {
ConsumePreparedPrograms(); ConsumePreparedPrograms();
@@ -148,18 +148,18 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (readyLayers.size() == 1) if (readyLayers.size() == 1)
{ {
RenderLayer(*readyLayers.front(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true); RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return; return;
} }
if (!EnsureLayerTargets(width, height)) if (!EnsureLayerTargets(width, height))
{ {
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer)); glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers.back(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true); RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return; return;
} }
GLuint layerInputTexture = 0; GLuint layerInputTexture = videoInputTexture;
std::size_t nextTargetIndex = 0; std::size_t nextTargetIndex = 0;
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex) for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
{ {
@@ -167,11 +167,11 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (isFinalLayer) if (isFinalLayer)
{ {
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer)); glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true); RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
continue; continue;
} }
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true); RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
layerInputTexture = mLayerTextures[nextTargetIndex]; layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex; nextTargetIndex = 1 - nextTargetIndex;
} }
@@ -309,6 +309,7 @@ GLuint RuntimeRenderScene::RenderLayer(
uint64_t frameIndex, uint64_t frameIndex,
unsigned width, unsigned width,
unsigned height, unsigned height,
GLuint videoInputTexture,
GLuint layerInputTexture, GLuint layerInputTexture,
GLuint outputFramebuffer, GLuint outputFramebuffer,
bool renderToOutput) bool renderToOutput)
@@ -327,7 +328,11 @@ GLuint RuntimeRenderScene::RenderLayer(
if (!pass.inputNames.empty()) if (!pass.inputNames.empty())
{ {
const std::string& inputName = pass.inputNames.front(); const std::string& inputName = pass.inputNames.front();
if (inputName != "layerInput" && inputName != "videoInput") if (inputName == "videoInput")
{
sourceTexture = videoInputTexture;
}
else if (inputName != "layerInput")
{ {
for (std::size_t index = 0; index < 2; ++index) for (std::size_t index = 0; index < 2; ++index)
{ {
@@ -344,7 +349,7 @@ GLuint RuntimeRenderScene::RenderLayer(
if (writesLayerOutput && renderToOutput) if (writesLayerOutput && renderToOutput)
{ {
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer); glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, sourceTexture); pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
lastOutputTexture = 0; lastOutputTexture = 0;
continue; continue;
} }
@@ -355,7 +360,7 @@ GLuint RuntimeRenderScene::RenderLayer(
const std::size_t targetIndex = nextTargetIndex; const std::size_t targetIndex = nextTargetIndex;
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2; nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]); glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, sourceTexture); pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
const std::size_t namedIndex = targetIndex - 2; const std::size_t namedIndex = targetIndex - 2;
namedOutputs[namedIndex] = mLayerTextures[targetIndex]; namedOutputs[namedIndex] = mLayerTextures[targetIndex];
namedOutputNames[namedIndex] = pass.outputName; namedOutputNames[namedIndex] = pass.outputName;

View File

@@ -22,7 +22,7 @@ public:
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error); bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error); bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
bool HasLayers(); bool HasLayers();
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height); void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
void ShutdownGl(); void ShutdownGl();
private: private:
@@ -48,7 +48,7 @@ private:
void TryCommitPendingPrograms(LayerProgram& layer); void TryCommitPendingPrograms(LayerProgram& layer);
bool EnsureLayerTargets(unsigned width, unsigned height); bool EnsureLayerTargets(unsigned width, unsigned height);
void DestroyLayerTargets(); void DestroyLayerTargets();
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput); GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
LayerProgram* FindLayer(const std::string& layerId); LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const; const LayerProgram* FindLayer(const std::string& layerId) const;
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId); LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);

View File

@@ -0,0 +1,111 @@
#include "SyntheticInputProducer.h"
#include "VideoIOFormat.h"
#include <algorithm>
#include <cmath>
namespace RenderCadenceCompositor
{
SyntheticInputProducer::SyntheticInputProducer(InputFrameMailbox& mailbox, SyntheticInputProducerConfig config) :
mMailbox(mailbox),
mConfig(config)
{
}
SyntheticInputProducer::~SyntheticInputProducer()
{
Stop();
}
bool SyntheticInputProducer::Start()
{
if (mThread.joinable())
return true;
if (mConfig.width == 0 || mConfig.height == 0)
return false;
mStopping.store(false, std::memory_order_release);
mThread = std::thread([this]() { ThreadMain(); });
return true;
}
void SyntheticInputProducer::Stop()
{
mStopping.store(true, std::memory_order_release);
if (mThread.joinable())
mThread.join();
}
SyntheticInputProducerMetrics SyntheticInputProducer::Metrics() const
{
SyntheticInputProducerMetrics metrics;
metrics.generatedFrames = mGeneratedFrames.load(std::memory_order_relaxed);
metrics.submitMisses = mSubmitMisses.load(std::memory_order_relaxed);
return metrics;
}
void SyntheticInputProducer::ThreadMain()
{
const unsigned rowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, mConfig.width);
std::vector<unsigned char> buffer(static_cast<std::size_t>(rowBytes) * static_cast<std::size_t>(mConfig.height));
const auto frameDuration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double, std::milli>(mConfig.frameDurationMilliseconds));
uint64_t frameIndex = 0;
auto nextFrameTime = std::chrono::steady_clock::now();
while (!mStopping.load(std::memory_order_acquire))
{
const auto now = std::chrono::steady_clock::now();
if (now < nextFrameTime)
{
std::this_thread::sleep_for((std::min)(std::chrono::milliseconds(1), std::chrono::duration_cast<std::chrono::milliseconds>(nextFrameTime - now)));
continue;
}
GenerateFrame(frameIndex, buffer);
if (!mMailbox.SubmitFrame(buffer.data(), rowBytes, frameIndex))
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
mGeneratedFrames.fetch_add(1, std::memory_order_relaxed);
++frameIndex;
nextFrameTime += frameDuration;
if (std::chrono::steady_clock::now() - nextFrameTime > frameDuration * 4)
nextFrameTime = std::chrono::steady_clock::now() + frameDuration;
}
}
void SyntheticInputProducer::GenerateFrame(uint64_t frameIndex, std::vector<unsigned char>& buffer) const
{
const float t = static_cast<float>(frameIndex) / 60.0f;
const unsigned boxWidth = (std::max)(1u, mConfig.width / 5u);
const unsigned boxHeight = (std::max)(1u, mConfig.height / 6u);
const unsigned maxX = mConfig.width > boxWidth ? mConfig.width - boxWidth : 0u;
const unsigned maxY = mConfig.height > boxHeight ? mConfig.height - boxHeight : 0u;
const unsigned boxX = static_cast<unsigned>((0.5f + 0.5f * std::sin(t * 1.4f)) * static_cast<float>(maxX));
const unsigned boxY = static_cast<unsigned>((0.5f + 0.5f * std::sin(t * 0.9f + 1.2f)) * static_cast<float>(maxY));
const unsigned rowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, mConfig.width);
for (unsigned y = 0; y < mConfig.height; ++y)
{
for (unsigned x = 0; x < mConfig.width; ++x)
{
const std::size_t offset = static_cast<std::size_t>(y) * rowBytes + static_cast<std::size_t>(x) * 4;
const unsigned checker = ((x / 80u) + (y / 80u) + static_cast<unsigned>(frameIndex / 15u)) & 1u;
unsigned char red = checker ? 42 : 16;
unsigned char green = checker ? 70 : 28;
unsigned char blue = checker ? 110 : 55;
if (x >= boxX && x < boxX + boxWidth && y >= boxY && y < boxY + boxHeight)
{
red = 245;
green = static_cast<unsigned char>(160 + 60 * (0.5f + 0.5f * std::sin(t * 2.1f)));
blue = 35;
}
buffer[offset + 0] = blue;
buffer[offset + 1] = green;
buffer[offset + 2] = red;
buffer[offset + 3] = 255;
}
}
}
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include "../frames/InputFrameMailbox.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
#include <vector>
namespace RenderCadenceCompositor
{
struct SyntheticInputProducerConfig
{
unsigned width = 1920;
unsigned height = 1080;
double frameDurationMilliseconds = 1000.0 / 59.94;
};
struct SyntheticInputProducerMetrics
{
uint64_t generatedFrames = 0;
uint64_t submitMisses = 0;
};
class SyntheticInputProducer
{
public:
SyntheticInputProducer(InputFrameMailbox& mailbox, SyntheticInputProducerConfig config);
SyntheticInputProducer(const SyntheticInputProducer&) = delete;
SyntheticInputProducer& operator=(const SyntheticInputProducer&) = delete;
~SyntheticInputProducer();
bool Start();
void Stop();
SyntheticInputProducerMetrics Metrics() const;
private:
void ThreadMain();
void GenerateFrame(uint64_t frameIndex, std::vector<unsigned char>& buffer) const;
InputFrameMailbox& mMailbox;
SyntheticInputProducerConfig mConfig;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<uint64_t> mGeneratedFrames{ 0 };
std::atomic<uint64_t> mSubmitMisses{ 0 };
};
}

View File

@@ -0,0 +1,109 @@
#include "InputFrameMailbox.h"
#include <cstdint>
#include <cstring>
#include <iostream>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
++gFailures;
std::cerr << "FAIL: " << message << "\n";
}
InputFrameMailboxConfig MakeConfig(std::size_t capacity = 2)
{
InputFrameMailboxConfig config;
config.width = 2;
config.height = 2;
config.pixelFormat = VideoIOPixelFormat::Bgra8;
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
config.capacity = capacity;
return config;
}
std::vector<unsigned char> MakeFrame(unsigned char value)
{
return std::vector<unsigned char>(16, value);
}
void TestAcquireLatestDropsOlderReadyFrames()
{
InputFrameMailbox mailbox(MakeConfig(3));
const std::vector<unsigned char> frame1 = MakeFrame(1);
const std::vector<unsigned char> frame2 = MakeFrame(2);
const std::vector<unsigned char> frame3 = MakeFrame(3);
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first input frame submits");
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second input frame submits");
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third input frame submits");
InputFrame latest;
Expect(mailbox.TryAcquireLatest(latest), "latest input frame can be acquired");
Expect(latest.frameIndex == 3, "mailbox returns newest frame");
Expect(latest.bytes != nullptr && static_cast<const unsigned char*>(latest.bytes)[0] == 3, "latest frame bytes match newest frame");
Expect(mailbox.Release(latest), "latest input frame can be released");
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
Expect(metrics.droppedReadyFrames == 2, "older ready input frames are dropped after latest acquire");
Expect(metrics.freeCount == 3, "all slots are free after release");
}
void TestSubmitDropsOldestWhenFull()
{
InputFrameMailbox mailbox(MakeConfig(2));
const std::vector<unsigned char> frame1 = MakeFrame(1);
const std::vector<unsigned char> frame2 = MakeFrame(2);
const std::vector<unsigned char> frame3 = MakeFrame(3);
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first frame submits into full test");
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second frame submits into full test");
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
InputFrame latest;
Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop");
Expect(latest.frameIndex == 3, "newest frame survived full mailbox");
Expect(mailbox.Release(latest), "newest frame releases");
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
Expect(metrics.submitMisses == 0, "full mailbox did not block producer when ready slots were disposable");
}
void TestReadingFrameIsProtected()
{
InputFrameMailbox mailbox(MakeConfig(1));
const std::vector<unsigned char> frame1 = MakeFrame(1);
const std::vector<unsigned char> frame2 = MakeFrame(2);
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
InputFrame acquired;
Expect(mailbox.TryAcquireLatest(acquired), "protected frame acquired");
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
Expect(mailbox.Release(acquired), "protected frame releases");
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
}
}
int main()
{
TestAcquireLatestDropsOlderReadyFrames();
TestSubmitDropsOldestWhenFull();
TestReadingFrameIsProtected();
if (gFailures != 0)
{
std::cerr << gFailures << " RenderCadenceCompositorInputFrameMailbox test failure(s).\n";
return 1;
}
std::cout << "RenderCadenceCompositorInputFrameMailbox tests passed.\n";
return 0;
}