From c25ae7b25b810ed1c5baf0e8153c3f10b897f28f Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 21:05:42 +1000 Subject: [PATCH] input buffer --- CMakeLists.txt | 5 + apps/RenderCadenceCompositor/README.md | 9 +- .../RenderCadenceCompositor.cpp | 34 +++ .../app/RuntimeLayerController.cpp | 269 ------------------ .../app/RuntimeLayerControllerBuild.cpp | 122 ++++++++ .../app/RuntimeLayerControllerControls.cpp | 160 +++++++++++ .../control/http/HttpControlServer.cpp | 193 ------------- .../control/http/HttpControlServerRoutes.cpp | 203 +++++++++++++ .../render/runtime/RuntimeRenderScene.cpp | 211 -------------- .../runtime/RuntimeRenderSceneRender.cpp | 219 ++++++++++++++ 10 files changed, 750 insertions(+), 675 deletions(-) create mode 100644 apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp create mode 100644 apps/RenderCadenceCompositor/app/RuntimeLayerControllerControls.cpp create mode 100644 apps/RenderCadenceCompositor/control/http/HttpControlServerRoutes.cpp create mode 100644 apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ffb3b5..74cf985 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,12 +310,15 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h" "${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h" "${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}/control/ControlActionResult.h" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp" "${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/RuntimeStateJson.h" "${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/RuntimeRenderScene.cpp" "${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.h" "${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h" @@ -973,6 +977,7 @@ add_executable(RenderCadenceCompositorHttpControlServerTests "${APP_DIR}/runtime/support/RuntimeJson.cpp" "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.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}/json/JsonWriter.cpp" "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 3a3d936..29f33bf 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -34,7 +34,7 @@ DeckLinkOutputThread 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 @@ -48,6 +48,7 @@ Included now: - BGRA8-only output - non-blocking latest-frame input mailbox - fast contiguous mailbox copy path for matching input row strides +- bounded input warmup before render cadence starts - render-thread-owned input texture upload - async PBO readback - 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] Latest-frame CPU input mailbox - [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] Runtime shaders receive input through `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 3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes 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. +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. Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events: diff --git a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp index 194c5a8..0f4983e 100644 --- a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp +++ b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp @@ -12,9 +12,11 @@ #include +#include #include #include #include +#include namespace { @@ -41,6 +43,19 @@ private: bool mInitialized = false; 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) @@ -123,6 +138,25 @@ int main(int argc, char** argv) { deckLinkInputStarted = true; 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 { diff --git a/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp b/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp index 6a04c9b..4e78987 100644 --- a/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp +++ b/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp @@ -1,11 +1,7 @@ #include "RuntimeLayerController.h" -#include "AppConfigProvider.h" -#include "RuntimeJson.h" #include "../logging/Logger.h" -#include - namespace RenderCadenceCompositor { RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) : @@ -48,137 +44,6 @@ void RuntimeLayerController::Stop() 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 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 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 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 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 lock(mRuntimeLayerMutex); - if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error)) - return { false, error }; - } - PublishRuntimeRenderLayers(); - return { true, std::string() }; - } - case RuntimeControlCommandType::SetLayerBypass: - { - { - std::lock_guard lock(mRuntimeLayerMutex); - if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error)) - return { false, error }; - } - PublishRuntimeRenderLayers(); - return { true, std::string() }; - } - case RuntimeControlCommandType::SetLayerShader: - { - { - std::lock_guard 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 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 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 { std::lock_guard lock(mRuntimeLayerMutex); @@ -191,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr 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 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 lock(mRuntimeLayerMutex); - std::string error; - mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error); - } - - auto bridge = std::make_unique(); - RuntimeShaderBridge* bridgePtr = bridge.get(); - { - std::lock_guard 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 bridge; - { - std::lock_guard 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> readyToStop; - { - std::lock_guard 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& bridge : readyToStop) - bridge->Stop(); -} - -void RuntimeLayerController::StopAllRuntimeShaderBuilds() -{ - std::map> builds; - std::vector> retiredBuilds; - { - std::lock_guard lock(mShaderBuildMutex); - builds.swap(mShaderBuilds); - retiredBuilds.swap(mRetiredShaderBuilds); - } - for (auto& entry : builds) - entry.second->Stop(); - for (auto& bridge : retiredBuilds) - bridge->Stop(); -} - void RuntimeLayerController::PublishRuntimeRenderLayers() { if (!mPublisher) @@ -331,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l LogWarning("runtime-shader", error); } -std::string RuntimeLayerController::FirstRuntimeLayerId() const -{ - std::lock_guard 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; -} } diff --git a/apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp b/apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp new file mode 100644 index 0000000..fd63321 --- /dev/null +++ b/apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp @@ -0,0 +1,122 @@ +#include "RuntimeLayerController.h" + +#include "AppConfigProvider.h" +#include "../logging/Logger.h" + +#include + +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 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 lock(mRuntimeLayerMutex); + std::string error; + mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error); + } + + auto bridge = std::make_unique(); + RuntimeShaderBridge* bridgePtr = bridge.get(); + { + std::lock_guard 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 bridge; + { + std::lock_guard 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> readyToStop; + { + std::lock_guard 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& bridge : readyToStop) + bridge->Stop(); +} + +void RuntimeLayerController::StopAllRuntimeShaderBuilds() +{ + std::map> builds; + std::vector> retiredBuilds; + { + std::lock_guard 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 lock(mRuntimeLayerMutex); + return mRuntimeLayerModel.FirstLayerId(); +} +} diff --git a/apps/RenderCadenceCompositor/app/RuntimeLayerControllerControls.cpp b/apps/RenderCadenceCompositor/app/RuntimeLayerControllerControls.cpp new file mode 100644 index 0000000..de82d2f --- /dev/null +++ b/apps/RenderCadenceCompositor/app/RuntimeLayerControllerControls.cpp @@ -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 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 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 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 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 lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::SetLayerBypass: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::SetLayerShader: + { + { + std::lock_guard 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 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 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; +} +} diff --git a/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp b/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp index d85dd58..79b69ef 100644 --- a/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp +++ b/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp @@ -1,15 +1,11 @@ #include "HttpControlServer.h" -#include "../json/JsonWriter.h" #include "../logging/Logger.h" #include #include -#include -#include #include -#include namespace RenderCadenceCompositor { @@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error) 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) : @@ -260,179 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques 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 << "\n" - << "Video Shader Toys API Docs\n" - << "\n" - << "
\n" - << "\n" - << "\n" - << "\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(std::tolower(character)); - }); - return text; -} - bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request) { const std::size_t requestLineEnd = rawRequest.find("\r\n"); diff --git a/apps/RenderCadenceCompositor/control/http/HttpControlServerRoutes.cpp b/apps/RenderCadenceCompositor/control/http/HttpControlServerRoutes.cpp new file mode 100644 index 0000000..9a005c4 --- /dev/null +++ b/apps/RenderCadenceCompositor/control/http/HttpControlServerRoutes.cpp @@ -0,0 +1,203 @@ +#include "HttpControlServer.h" + +#include "../json/JsonWriter.h" + +#include +#include +#include +#include + +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 << "\n" + << "Video Shader Toys API Docs\n" + << "\n" + << "
\n" + << "\n" + << "\n" + << "\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(std::tolower(character)); + }); + return text; +} +} diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp index 5530475..302504f 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp @@ -102,84 +102,6 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vectorpasses) - { - 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 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(outputFramebuffer), true); - return; - } - - if (!EnsureLayerTargets(width, height)) - { - glBindFramebuffer(GL_FRAMEBUFFER, static_cast(outputFramebuffer)); - RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast(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(outputFramebuffer)); - RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast(outputFramebuffer), true); - continue; - } - - RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true); - layerInputTexture = mLayerTextures[nextTargetIndex]; - nextTargetIndex = 1 - nextTargetIndex; - } -} - void RuntimeRenderScene::ShutdownGl() { mPrepareWorker.Stop(); @@ -307,139 +229,6 @@ void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer) 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(width), - static_cast(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) { for (LayerProgram& layer : mLayers) diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp new file mode 100644 index 0000000..c3e2cfb --- /dev/null +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp @@ -0,0 +1,219 @@ +#include "RuntimeRenderScene.h" + +#include +#include + +#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 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(outputFramebuffer), true); + return; + } + + if (!EnsureLayerTargets(width, height)) + { + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(outputFramebuffer)); + RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast(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(outputFramebuffer)); + RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast(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(width), + static_cast(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; +}