input buffer
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled

This commit is contained in:
Aiden
2026-05-12 21:05:42 +10:00
parent a39be6fb20
commit c25ae7b25b
10 changed files with 750 additions and 675 deletions

View File

@@ -310,12 +310,15 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h" "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h" "${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp" "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h" "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h" "${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
"${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.cpp"
@@ -345,6 +348,7 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderSceneRender.cpp"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
@@ -973,6 +977,7 @@ add_executable(RenderCadenceCompositorHttpControlServerTests
"${APP_DIR}/runtime/support/RuntimeJson.cpp" "${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"

View File

@@ -34,7 +34,7 @@ DeckLinkOutputThread
never renders never renders
``` ```
Startup warms up real rendered frames before DeckLink scheduled playback starts. Startup warms up real rendered frames before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for initial input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
## Current Scope ## Current Scope
@@ -48,6 +48,7 @@ Included now:
- BGRA8-only output - BGRA8-only output
- non-blocking latest-frame input mailbox - non-blocking latest-frame input mailbox
- fast contiguous mailbox copy path for matching input row strides - fast contiguous mailbox copy path for matching input row strides
- bounded input warmup before render cadence starts
- render-thread-owned input texture upload - render-thread-owned input texture upload
- async PBO readback - async PBO readback
- latest-N system-memory frame exchange - latest-N system-memory frame exchange
@@ -123,6 +124,7 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture - [x] UYVY8 input capture with render-thread GPU decode to shader input texture
- [x] Latest-frame CPU input mailbox - [x] Latest-frame CPU input mailbox
- [x] Fast contiguous input mailbox copy when source/destination stride matches - [x] Fast contiguous input mailbox copy when source/destination stride matches
- [x] Bounded input warmup before render cadence starts
- [x] Render-owned input texture upload - [x] Render-owned input texture upload
- [x] Runtime shaders receive input through `gVideoInput` - [x] Runtime shaders receive input through `gVideoInput`
- [x] Live DeckLink input bound to `gVideoInput` - [x] Live DeckLink input bound to `gVideoInput`
@@ -252,10 +254,13 @@ Startup order is:
2. try to attach DeckLink input for the configured input mode 2. try to attach DeckLink input for the configured input mode
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes 3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
4. start `DeckLinkInputThread` 4. start `DeckLinkInputThread`
5. leave input absent if discovery, setup, format support, or stream startup fails 5. wait briefly for initial input warmup frames before starting render cadence
6. leave input absent if discovery, setup, format support, or stream startup fails
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The mailbox uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted. `DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The mailbox uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
The app samples telemetry once per second. The app samples telemetry once per second.
Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events: Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events:

View File

@@ -12,9 +12,11 @@
#include <windows.h> #include <windows.h>
#include <chrono>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <thread>
namespace namespace
{ {
@@ -41,6 +43,19 @@ private:
bool mInitialized = false; bool mInitialized = false;
HRESULT mResult = S_OK; HRESULT mResult = S_OK;
}; };
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
{
const auto start = std::chrono::steady_clock::now();
while (std::chrono::steady_clock::now() - start < timeout)
{
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
if (metrics.readyCount >= targetReadyFrames || metrics.submittedFrames >= targetReadyFrames)
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
return false;
}
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@@ -123,6 +138,25 @@ int main(int argc, char** argv)
{ {
deckLinkInputStarted = true; deckLinkInputStarted = true;
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + "."); RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
constexpr std::size_t kInputWarmupFrames = 2;
constexpr std::chrono::milliseconds kInputWarmupTimeout(500);
if (WaitForInputWarmup(inputMailbox, kInputWarmupFrames, kInputWarmupTimeout))
{
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
RenderCadenceCompositor::Log(
"app",
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
}
else
{
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
RenderCadenceCompositor::LogWarning(
"app",
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
}
} }
else else
{ {

View File

@@ -1,11 +1,7 @@
#include "RuntimeLayerController.h" #include "RuntimeLayerController.h"
#include "AppConfigProvider.h"
#include "RuntimeJson.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include <filesystem>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) : RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
@@ -48,137 +44,6 @@ void RuntimeLayerController::Stop()
StopAllRuntimeShaderBuilds(); StopAllRuntimeShaderBuilds();
} }
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string shaderId;
std::string error;
if (!ExtractStringField(body, "shaderId", shaderId, error))
return { false, error };
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
StartLayerShaderBuild(layerId, shaderId);
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string layerId;
std::string error;
if (!ExtractStringField(body, "layerId", layerId, error))
return { false, error };
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + layerId);
RetireLayerShaderBuild(layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
{
CleanupRetiredShaderBuilds();
std::string error;
switch (command.type)
{
case RuntimeControlCommandType::AddLayer:
{
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::RemoveLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + command.layerId);
RetireLayerShaderBuild(command.layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ReorderLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerBypass:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerShader:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
return { false, error };
}
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(command.layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::UpdateLayerParameter:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ResetLayerParameters:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::Unsupported:
break;
}
return { false, "Unsupported runtime control command." };
}
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
{ {
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex); std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
@@ -191,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr
return snapshot; return snapshot;
} }
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
{
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
std::string error;
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
{
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
return;
}
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
}
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
{
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
runtimeShaderId.clear();
mRuntimeLayerModel.Clear();
}
}
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
{
CleanupRetiredShaderBuilds();
RetireLayerShaderBuild(layerId);
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
}
auto bridge = std::make_unique<RuntimeShaderBridge>();
RuntimeShaderBridge* bridgePtr = bridge.get();
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
mShaderBuilds[layerId] = std::move(bridge);
}
bridgePtr->Start(
layerId,
shaderId,
[this](const RuntimeShaderArtifact& artifact) {
if (MarkRuntimeBuildReady(artifact))
PublishRuntimeRenderLayers();
},
[this, layerId](const std::string& message) {
MarkRuntimeBuildFailedForLayer(layerId, message);
LogError("runtime-shader", "Runtime Slang build failed: " + message);
});
}
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
{
std::unique_ptr<RuntimeShaderBridge> bridge;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
auto bridgeIt = mShaderBuilds.find(layerId);
if (bridgeIt == mShaderBuilds.end())
return;
bridge = std::move(bridgeIt->second);
mShaderBuilds.erase(bridgeIt);
bridge->RequestStop();
mRetiredShaderBuilds.push_back(std::move(bridge));
}
}
void RuntimeLayerController::CleanupRetiredShaderBuilds()
{
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
{
if ((*it)->CanStopWithoutWaiting())
{
readyToStop.push_back(std::move(*it));
it = mRetiredShaderBuilds.erase(it);
continue;
}
++it;
}
}
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
bridge->Stop();
}
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
{
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
builds.swap(mShaderBuilds);
retiredBuilds.swap(mRetiredShaderBuilds);
}
for (auto& entry : builds)
entry.second->Stop();
for (auto& bridge : retiredBuilds)
bridge->Stop();
}
void RuntimeLayerController::PublishRuntimeRenderLayers() void RuntimeLayerController::PublishRuntimeRenderLayers()
{ {
if (!mPublisher) if (!mPublisher)
@@ -331,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l
LogWarning("runtime-shader", error); LogWarning("runtime-shader", error);
} }
std::string RuntimeLayerController::FirstRuntimeLayerId() const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
return mRuntimeLayerModel.FirstLayerId();
}
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
{
JsonValue root;
std::string parseError;
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
{
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
return false;
}
const JsonValue* field = root.find(fieldName);
if (!field || !field->isString() || field->asString().empty())
{
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
return false;
}
value = field->asString();
error.clear();
return true;
}
} }

View File

@@ -0,0 +1,122 @@
#include "RuntimeLayerController.h"
#include "AppConfigProvider.h"
#include "../logging/Logger.h"
#include <filesystem>
namespace RenderCadenceCompositor
{
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
{
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
std::string error;
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
{
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
return;
}
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
}
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
{
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
runtimeShaderId.clear();
mRuntimeLayerModel.Clear();
}
}
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
{
CleanupRetiredShaderBuilds();
RetireLayerShaderBuild(layerId);
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
}
auto bridge = std::make_unique<RuntimeShaderBridge>();
RuntimeShaderBridge* bridgePtr = bridge.get();
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
mShaderBuilds[layerId] = std::move(bridge);
}
bridgePtr->Start(
layerId,
shaderId,
[this](const RuntimeShaderArtifact& artifact) {
if (MarkRuntimeBuildReady(artifact))
PublishRuntimeRenderLayers();
},
[this, layerId](const std::string& message) {
MarkRuntimeBuildFailedForLayer(layerId, message);
LogError("runtime-shader", "Runtime Slang build failed: " + message);
});
}
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
{
std::unique_ptr<RuntimeShaderBridge> bridge;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
auto bridgeIt = mShaderBuilds.find(layerId);
if (bridgeIt == mShaderBuilds.end())
return;
bridge = std::move(bridgeIt->second);
mShaderBuilds.erase(bridgeIt);
bridge->RequestStop();
mRetiredShaderBuilds.push_back(std::move(bridge));
}
}
void RuntimeLayerController::CleanupRetiredShaderBuilds()
{
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
{
if ((*it)->CanStopWithoutWaiting())
{
readyToStop.push_back(std::move(*it));
it = mRetiredShaderBuilds.erase(it);
continue;
}
++it;
}
}
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
bridge->Stop();
}
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
{
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
builds.swap(mShaderBuilds);
retiredBuilds.swap(mRetiredShaderBuilds);
}
for (auto& entry : builds)
entry.second->Stop();
for (auto& bridge : retiredBuilds)
bridge->Stop();
}
std::string RuntimeLayerController::FirstRuntimeLayerId() const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
return mRuntimeLayerModel.FirstLayerId();
}
}

View File

@@ -0,0 +1,160 @@
#include "RuntimeLayerController.h"
#include "RuntimeJson.h"
#include "../logging/Logger.h"
namespace RenderCadenceCompositor
{
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string shaderId;
std::string error;
if (!ExtractStringField(body, "shaderId", shaderId, error))
return { false, error };
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
StartLayerShaderBuild(layerId, shaderId);
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string layerId;
std::string error;
if (!ExtractStringField(body, "layerId", layerId, error))
return { false, error };
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + layerId);
RetireLayerShaderBuild(layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
{
CleanupRetiredShaderBuilds();
std::string error;
switch (command.type)
{
case RuntimeControlCommandType::AddLayer:
{
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::RemoveLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + command.layerId);
RetireLayerShaderBuild(command.layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ReorderLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerBypass:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerShader:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
return { false, error };
}
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(command.layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::UpdateLayerParameter:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ResetLayerParameters:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::Unsupported:
break;
}
return { false, "Unsupported runtime control command." };
}
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
{
JsonValue root;
std::string parseError;
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
{
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
return false;
}
const JsonValue* field = root.find(fieldName);
if (!field || !field->isString() || field->asString().empty())
{
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
return false;
}
value = field->asString();
error.clear();
return true;
}
}

View File

@@ -1,15 +1,11 @@
#include "HttpControlServer.h" #include "HttpControlServer.h"
#include "../json/JsonWriter.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream> #include <sstream>
#include <vector>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
@@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error)
return true; return true;
} }
bool IsKnownPostEndpoint(const std::string& path)
{
return path == "/api/layers/add"
|| path == "/api/layers/remove"
|| path == "/api/layers/move"
|| path == "/api/layers/reorder"
|| path == "/api/layers/set-bypass"
|| path == "/api/layers/set-shader"
|| path == "/api/layers/update-parameter"
|| path == "/api/layers/reset-parameters"
|| path == "/api/stack-presets/save"
|| path == "/api/stack-presets/load"
|| path == "/api/reload"
|| path == "/api/screenshot";
}
} }
UniqueSocket::UniqueSocket(SOCKET socket) : UniqueSocket::UniqueSocket(SOCKET socket) :
@@ -260,179 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques
return TextResponse("404 Not Found", "Not Found"); return TextResponse("404 Not Found", "Not Found");
} }
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
{
if (request.path == "/api/state")
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
return ServeOpenApiSpec();
if (request.path == "/docs" || request.path == "/docs/")
return ServeSwaggerDocs();
if (request.path == "/" || request.path == "/index.html")
return ServeUiAsset("index.html");
if (request.path.rfind("/assets/", 0) == 0)
return ServeUiAsset(request.path.substr(1));
if (request.path.size() > 1)
{
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
if (asset.status != "404 Not Found")
return asset;
}
return ServeUiAsset("index.html");
}
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
{
if (!IsKnownPostEndpoint(request.path))
return TextResponse("404 Not Found", "Not Found");
if (mCallbacks.executePost)
{
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
{
const ControlActionResult result = mCallbacks.addLayer(request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
{
const ControlActionResult result = mCallbacks.removeLayer(request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
return {
"400 Bad Request",
"application/json",
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
};
}
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
{
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
const std::string body = LoadTextFile(path);
return body.empty()
? TextResponse("404 Not Found", "OpenAPI spec not found")
: HttpResponse{ "200 OK", GuessContentType(path), body };
}
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
{
std::ostringstream html;
html << "<!doctype html>\n"
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
<< "<body><div id=\"swagger-ui\"></div>\n"
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
<< "</body></html>\n";
return { "200 OK", "text/html", html.str() };
}
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
{
if (mUiRoot.empty())
return TextResponse("404 Not Found", "UI root is not configured");
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
if (!IsSafeRelativePath(sanitizedPath))
return TextResponse("404 Not Found", "Not Found");
const std::filesystem::path path = mUiRoot / sanitizedPath;
const std::string body = LoadTextFile(path);
if (body.empty())
return TextResponse("404 Not Found", "Not Found");
return { "200 OK", GuessContentType(path), body };
}
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
{
std::ifstream input(path, std::ios::binary);
if (!input)
return std::string();
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
{
return { status, "application/json", body };
}
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
{
return { status, "text/plain", body };
}
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
{
return { status, "text/html", body };
}
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
{
JsonWriter writer;
writer.BeginObject();
writer.KeyBool("ok", ok);
if (!error.empty())
writer.KeyString("error", error);
writer.EndObject();
return writer.StringValue();
}
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
{
const std::string extension = ToLower(path.extension().string());
if (extension == ".yaml" || extension == ".yml")
return "application/yaml";
if (extension == ".json")
return "application/json";
if (extension == ".js" || extension == ".mjs")
return "text/javascript";
if (extension == ".css")
return "text/css";
if (extension == ".html" || extension == ".htm")
return "text/html";
if (extension == ".svg")
return "image/svg+xml";
if (extension == ".png")
return "image/png";
if (extension == ".jpg" || extension == ".jpeg")
return "image/jpeg";
if (extension == ".ico")
return "image/x-icon";
if (extension == ".map")
return "application/json";
return "text/plain";
}
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
{
if (path.empty() || path.is_absolute())
return false;
for (const std::filesystem::path& part : path)
{
if (part == "..")
return false;
}
return true;
}
std::string HttpControlServer::ToLower(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return text;
}
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request) bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
{ {
const std::size_t requestLineEnd = rawRequest.find("\r\n"); const std::size_t requestLineEnd = rawRequest.find("\r\n");

View File

@@ -0,0 +1,203 @@
#include "HttpControlServer.h"
#include "../json/JsonWriter.h"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <sstream>
namespace RenderCadenceCompositor
{
namespace
{
bool IsKnownPostEndpoint(const std::string& path)
{
return path == "/api/layers/add"
|| path == "/api/layers/remove"
|| path == "/api/layers/move"
|| path == "/api/layers/reorder"
|| path == "/api/layers/set-bypass"
|| path == "/api/layers/set-shader"
|| path == "/api/layers/update-parameter"
|| path == "/api/layers/reset-parameters"
|| path == "/api/stack-presets/save"
|| path == "/api/stack-presets/load"
|| path == "/api/reload"
|| path == "/api/screenshot";
}
}
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
{
if (request.path == "/api/state")
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
return ServeOpenApiSpec();
if (request.path == "/docs" || request.path == "/docs/")
return ServeSwaggerDocs();
if (request.path == "/" || request.path == "/index.html")
return ServeUiAsset("index.html");
if (request.path.rfind("/assets/", 0) == 0)
return ServeUiAsset(request.path.substr(1));
if (request.path.size() > 1)
{
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
if (asset.status != "404 Not Found")
return asset;
}
return ServeUiAsset("index.html");
}
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
{
if (!IsKnownPostEndpoint(request.path))
return TextResponse("404 Not Found", "Not Found");
if (mCallbacks.executePost)
{
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
{
const ControlActionResult result = mCallbacks.addLayer(request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
{
const ControlActionResult result = mCallbacks.removeLayer(request.body);
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
}
return {
"400 Bad Request",
"application/json",
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
};
}
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
{
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
const std::string body = LoadTextFile(path);
return body.empty()
? TextResponse("404 Not Found", "OpenAPI spec not found")
: HttpResponse{ "200 OK", GuessContentType(path), body };
}
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
{
std::ostringstream html;
html << "<!doctype html>\n"
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
<< "<body><div id=\"swagger-ui\"></div>\n"
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
<< "</body></html>\n";
return { "200 OK", "text/html", html.str() };
}
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
{
if (mUiRoot.empty())
return TextResponse("404 Not Found", "UI root is not configured");
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
if (!IsSafeRelativePath(sanitizedPath))
return TextResponse("404 Not Found", "Not Found");
const std::filesystem::path path = mUiRoot / sanitizedPath;
const std::string body = LoadTextFile(path);
if (body.empty())
return TextResponse("404 Not Found", "Not Found");
return { "200 OK", GuessContentType(path), body };
}
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
{
std::ifstream input(path, std::ios::binary);
if (!input)
return std::string();
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
{
return { status, "application/json", body };
}
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
{
return { status, "text/plain", body };
}
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
{
return { status, "text/html", body };
}
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
{
JsonWriter writer;
writer.BeginObject();
writer.KeyBool("ok", ok);
if (!error.empty())
writer.KeyString("error", error);
writer.EndObject();
return writer.StringValue();
}
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
{
const std::string extension = ToLower(path.extension().string());
if (extension == ".yaml" || extension == ".yml")
return "application/yaml";
if (extension == ".json")
return "application/json";
if (extension == ".js" || extension == ".mjs")
return "text/javascript";
if (extension == ".css")
return "text/css";
if (extension == ".html" || extension == ".htm")
return "text/html";
if (extension == ".svg")
return "image/svg+xml";
if (extension == ".png")
return "image/png";
if (extension == ".jpg" || extension == ".jpeg")
return "image/jpeg";
if (extension == ".ico")
return "image/x-icon";
if (extension == ".map")
return "application/json";
return "text/plain";
}
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
{
if (path.empty() || path.is_absolute())
return false;
for (const std::filesystem::path& part : path)
{
if (part == "..")
return false;
}
return true;
}
std::string HttpControlServer::ToLower(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return text;
}
}

View File

@@ -102,84 +102,6 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
return true; return true;
} }
bool RuntimeRenderScene::HasLayers()
{
ConsumePreparedPrograms();
for (const std::string& layerId : mLayerOrder)
{
const LayerProgram* layer = FindLayer(layerId);
if (!layer)
continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
return true;
}
}
return false;
}
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
{
ConsumePreparedPrograms();
std::vector<LayerProgram*> readyLayers;
for (const std::string& layerId : mLayerOrder)
{
LayerProgram* layer = FindLayer(layerId);
if (!layer)
continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
{
readyLayers.push_back(layer);
break;
}
}
}
if (readyLayers.empty())
return;
GLint outputFramebuffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
if (readyLayers.size() == 1)
{
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return;
}
if (!EnsureLayerTargets(width, height))
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return;
}
// Shader source contract:
// - gVideoInput is the decoded latest input texture for every layer in the stack.
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
GLuint layerInputTexture = videoInputTexture;
std::size_t nextTargetIndex = 0;
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
{
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
if (isFinalLayer)
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
continue;
}
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex;
}
}
void RuntimeRenderScene::ShutdownGl() void RuntimeRenderScene::ShutdownGl()
{ {
mPrepareWorker.Stop(); mPrepareWorker.Stop();
@@ -307,139 +229,6 @@ void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
layer.pendingPreparedPrograms.clear(); layer.pendingPreparedPrograms.clear();
} }
GLuint RuntimeRenderScene::RenderLayer(
LayerProgram& layer,
uint64_t frameIndex,
unsigned width,
unsigned height,
GLuint videoInputTexture,
GLuint layerInputTexture,
GLuint outputFramebuffer,
bool renderToOutput)
{
GLuint namedOutputs[2] = {};
std::string namedOutputNames[2];
std::size_t nextTargetIndex = 2;
GLuint lastOutputTexture = layerInputTexture;
for (LayerProgram::PassProgram& pass : layer.passes)
{
if (!pass.renderer || !pass.renderer->HasProgram())
continue;
GLuint sourceTexture = videoInputTexture;
if (!pass.inputNames.empty())
{
const std::string& inputName = pass.inputNames.front();
if (inputName == "videoInput")
{
sourceTexture = videoInputTexture;
}
else if (inputName != "layerInput")
{
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
// sampling and gVideoInput for the original input frame.
for (std::size_t index = 0; index < 2; ++index)
{
if (namedOutputNames[index] == inputName)
{
sourceTexture = namedOutputs[index];
break;
}
}
}
}
const bool writesLayerOutput = pass.outputName == "layerOutput";
if (writesLayerOutput && renderToOutput)
{
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
lastOutputTexture = 0;
continue;
}
if (!EnsureLayerTargets(width, height))
continue;
const std::size_t targetIndex = nextTargetIndex;
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
const std::size_t namedIndex = targetIndex - 2;
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
namedOutputNames[namedIndex] = pass.outputName;
lastOutputTexture = mLayerTextures[targetIndex];
}
return lastOutputTexture;
}
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
{
if (width == 0 || height == 0)
return false;
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
return true;
DestroyLayerTargets();
mLayerTargetWidth = width;
mLayerTargetHeight = height;
glGenFramebuffers(4, mLayerFramebuffers);
glGenTextures(4, mLayerTextures);
for (int index = 0; index < 4; ++index)
{
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
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>(width),
static_cast<GLsizei>(height),
0,
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
DestroyLayerTargets();
return false;
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void RuntimeRenderScene::DestroyLayerTargets()
{
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
glDeleteFramebuffers(4, mLayerFramebuffers);
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
glDeleteTextures(4, mLayerTextures);
for (int index = 0; index < 4; ++index)
{
mLayerFramebuffers[index] = 0;
mLayerTextures[index] = 0;
}
mLayerTargetWidth = 0;
mLayerTargetHeight = 0;
}
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
{ {
for (LayerProgram& layer : mLayers) for (LayerProgram& layer : mLayers)

View File

@@ -0,0 +1,219 @@
#include "RuntimeRenderScene.h"
#include <string>
#include <vector>
#ifndef GL_FRAMEBUFFER_BINDING
#define GL_FRAMEBUFFER_BINDING 0x8CA6
#endif
bool RuntimeRenderScene::HasLayers()
{
ConsumePreparedPrograms();
for (const std::string& layerId : mLayerOrder)
{
const LayerProgram* layer = FindLayer(layerId);
if (!layer)
continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
return true;
}
}
return false;
}
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
{
ConsumePreparedPrograms();
std::vector<LayerProgram*> readyLayers;
for (const std::string& layerId : mLayerOrder)
{
LayerProgram* layer = FindLayer(layerId);
if (!layer)
continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
{
readyLayers.push_back(layer);
break;
}
}
}
if (readyLayers.empty())
return;
GLint outputFramebuffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
if (readyLayers.size() == 1)
{
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return;
}
if (!EnsureLayerTargets(width, height))
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
return;
}
// Shader source contract:
// - gVideoInput is the decoded latest input texture for every layer in the stack.
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
GLuint layerInputTexture = videoInputTexture;
std::size_t nextTargetIndex = 0;
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
{
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
if (isFinalLayer)
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
continue;
}
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex;
}
}
GLuint RuntimeRenderScene::RenderLayer(
LayerProgram& layer,
uint64_t frameIndex,
unsigned width,
unsigned height,
GLuint videoInputTexture,
GLuint layerInputTexture,
GLuint outputFramebuffer,
bool renderToOutput)
{
GLuint namedOutputs[2] = {};
std::string namedOutputNames[2];
std::size_t nextTargetIndex = 2;
GLuint lastOutputTexture = layerInputTexture;
for (LayerProgram::PassProgram& pass : layer.passes)
{
if (!pass.renderer || !pass.renderer->HasProgram())
continue;
GLuint sourceTexture = videoInputTexture;
if (!pass.inputNames.empty())
{
const std::string& inputName = pass.inputNames.front();
if (inputName == "videoInput")
{
sourceTexture = videoInputTexture;
}
else if (inputName != "layerInput")
{
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
// sampling and gVideoInput for the original input frame.
for (std::size_t index = 0; index < 2; ++index)
{
if (namedOutputNames[index] == inputName)
{
sourceTexture = namedOutputs[index];
break;
}
}
}
}
const bool writesLayerOutput = pass.outputName == "layerOutput";
if (writesLayerOutput && renderToOutput)
{
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
lastOutputTexture = 0;
continue;
}
if (!EnsureLayerTargets(width, height))
continue;
const std::size_t targetIndex = nextTargetIndex;
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
const std::size_t namedIndex = targetIndex - 2;
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
namedOutputNames[namedIndex] = pass.outputName;
lastOutputTexture = mLayerTextures[targetIndex];
}
return lastOutputTexture;
}
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
{
if (width == 0 || height == 0)
return false;
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
return true;
DestroyLayerTargets();
mLayerTargetWidth = width;
mLayerTargetHeight = height;
glGenFramebuffers(4, mLayerFramebuffers);
glGenTextures(4, mLayerTextures);
for (int index = 0; index < 4; ++index)
{
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
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>(width),
static_cast<GLsizei>(height),
0,
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
DestroyLayerTargets();
return false;
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void RuntimeRenderScene::DestroyLayerTargets()
{
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
glDeleteFramebuffers(4, mLayerFramebuffers);
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
glDeleteTextures(4, mLayerTextures);
for (int index = 0; index < 4; ++index)
{
mLayerFramebuffers[index] = 0;
mLayerTextures[index] = 0;
}
mLayerTargetWidth = 0;
mLayerTargetHeight = 0;
}