2 Commits

Author SHA1 Message Date
Aiden
f43b6f6519 shader control
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m3s
CI / Windows Release Package (push) Has been skipped
2026-05-12 17:52:55 +10:00
Aiden
dfd49fd0e3 Multipass shaders 2026-05-12 17:08:35 +10:00
26 changed files with 954 additions and 81 deletions

View File

@@ -290,6 +290,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${APP_DIR}/gl/shader/Std140Buffer.h"
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${APP_DIR}/runtime/support/RuntimeJson.h"
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
"${APP_DIR}/runtime/support/RuntimeParameterUtils.h"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderCompiler.h"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
@@ -310,6 +312,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.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/HttpControlServerWebSocket.cpp"
@@ -841,6 +845,7 @@ add_executable(RenderCadenceCompositorRuntimeLayerModelTests
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
)
@@ -909,6 +914,7 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
@@ -940,6 +946,8 @@ endif()
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
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/HttpControlServerWebSocket.cpp"
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
@@ -948,6 +956,8 @@ add_executable(RenderCadenceCompositorHttpControlServerTests
)
target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime/support"
"${RENDER_CADENCE_APP_DIR}/control"
"${RENDER_CADENCE_APP_DIR}/control/http"
"${RENDER_CADENCE_APP_DIR}/json"

View File

@@ -47,7 +47,9 @@ Included now:
- shared-context GL prepare worker for runtime shader program compile/link
- render-thread-only GL program swap once a prepared program is ready
- manifest-driven stateless single-pass shader packages
- HTTP shader list populated from supported stateless single-pass shader packages
- manifest-driven stateless named-pass shader packages
- atomic render-plan swap after every pass program is prepared
- HTTP shader list populated from supported stateless full-frame shader packages
- default float, vec2, color, boolean, enum, and trigger parameters
- small JSON writer for future HTTP/WebSocket payloads
- JSON serialization for cadence telemetry snapshots
@@ -60,7 +62,6 @@ Included now:
Intentionally not included yet:
- DeckLink input
- multipass shader rendering
- temporal/history/feedback shader storage
- texture/LUT asset upload
- text-parameter rasterization
@@ -86,6 +87,8 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [x] Shared-context GL shader/program preparation
- [x] Render-thread program swap at a frame boundary
- [x] Stateless single-pass shader rendering
- [x] Stateless named-pass shader rendering
- [x] Atomic multipass render-plan commit
- [x] Shader add/remove control path
- [x] Previous-layer texture handoff for stacked shaders
- [x] Supported shader list in HTTP/UI state
@@ -99,7 +102,6 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [ ] DeckLink input capture
- [ ] Input frame upload into the render scene
- [ ] Live video input bound to `gVideoInput`
- [ ] Multipass shader rendering
- [ ] Temporal history buffers
- [ ] Feedback buffers
- [ ] Texture asset loading and upload
@@ -196,10 +198,10 @@ Current endpoints:
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
- `GET /docs`: serves Swagger UI
- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only
- `POST /api/layers/add`, `/remove`, `/reorder`, `/set-bypass`, `/set-shader`, `/update-parameter`, and `/reset-parameters` use the shared runtime control-command path
- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and does not call render work or DeckLink scheduling.
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
## Optional DeckLink Output
@@ -244,9 +246,12 @@ On startup the app begins compiling the selected shader package on a background
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
Current runtime shader support is deliberately limited to stateless single-pass packages:
Current runtime shader support is deliberately limited to stateless full-frame packages:
- one pass only
- one or more named passes
- one sampled source input per pass
- named intermediate outputs routed by the pass manifest
- final visible output must be named `layerOutput`
- no temporal history
- no feedback storage
- no texture/LUT assets yet
@@ -255,11 +260,11 @@ Current runtime shader support is deliberately limited to stateless single-pass
- the first layer receives a small fallback source texture until DeckLink input is added
- stacked layers receive the previous ready layer output through both `gVideoInput` and `gLayerInput`
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as multipass, temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds.
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed programs to the shared-context prepare worker, swaps in prepared programs when available, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; the final ready layer renders to the output target.
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
Successful handoff signs:

View File

@@ -184,6 +184,13 @@ private:
callbacks.removeLayer = [this](const std::string& body) {
return mRuntimeLayers.HandleRemoveLayer(body);
};
callbacks.executePost = [this](const std::string& path, const std::string& body) {
RuntimeControlCommand command;
std::string error;
if (!ParseRuntimeControlCommand(path, body, command, error))
return ControlActionResult{ false, error };
return mRuntimeLayers.HandleControlCommand(command);
};
std::string error;
if (!mHttpServer.Start(

View File

@@ -90,6 +90,95 @@ ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string&
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
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);

View File

@@ -1,6 +1,7 @@
#pragma once
#include "../control/ControlActionResult.h"
#include "../control/RuntimeControlCommand.h"
#include "../runtime/RuntimeLayerModel.h"
#include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.h"
@@ -32,6 +33,7 @@ public:
ControlActionResult HandleAddLayer(const std::string& body);
ControlActionResult HandleRemoveLayer(const std::string& body);
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }

View File

@@ -0,0 +1,127 @@
#include "RuntimeControlCommand.h"
namespace RenderCadenceCompositor
{
namespace
{
const JsonValue* RequireObjectField(const JsonValue& root, const char* fieldName, std::string& error)
{
const JsonValue* field = root.find(fieldName);
if (!field)
error = std::string("Request field '") + fieldName + "' is required.";
return field;
}
bool RequireStringField(const JsonValue& root, const char* fieldName, std::string& value, std::string& error)
{
const JsonValue* field = RequireObjectField(root, fieldName, error);
if (!field)
return false;
if (!field->isString() || field->asString().empty())
{
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
return false;
}
value = field->asString();
return true;
}
bool RequireBoolField(const JsonValue& root, const char* fieldName, bool& value, std::string& error)
{
const JsonValue* field = RequireObjectField(root, fieldName, error);
if (!field)
return false;
if (!field->isBoolean())
{
error = std::string("Request field '") + fieldName + "' must be a boolean.";
return false;
}
value = field->asBoolean();
return true;
}
bool RequireIntegerField(const JsonValue& root, const char* fieldName, int& value, std::string& error)
{
const JsonValue* field = RequireObjectField(root, fieldName, error);
if (!field)
return false;
if (!field->isNumber())
{
error = std::string("Request field '") + fieldName + "' must be a number.";
return false;
}
value = static_cast<int>(field->asNumber());
return true;
}
}
bool ParseRuntimeControlCommand(
const std::string& path,
const std::string& body,
RuntimeControlCommand& command,
std::string& error)
{
command = RuntimeControlCommand();
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;
}
if (path == "/api/layers/add")
{
command.type = RuntimeControlCommandType::AddLayer;
return RequireStringField(root, "shaderId", command.shaderId, error);
}
if (path == "/api/layers/remove")
{
command.type = RuntimeControlCommandType::RemoveLayer;
return RequireStringField(root, "layerId", command.layerId, error);
}
if (path == "/api/layers/reorder")
{
command.type = RuntimeControlCommandType::ReorderLayer;
return RequireStringField(root, "layerId", command.layerId, error)
&& RequireIntegerField(root, "targetIndex", command.targetIndex, error);
}
if (path == "/api/layers/set-bypass")
{
command.type = RuntimeControlCommandType::SetLayerBypass;
return RequireStringField(root, "layerId", command.layerId, error)
&& RequireBoolField(root, "bypass", command.bypass, error);
}
if (path == "/api/layers/set-shader")
{
command.type = RuntimeControlCommandType::SetLayerShader;
return RequireStringField(root, "layerId", command.layerId, error)
&& RequireStringField(root, "shaderId", command.shaderId, error);
}
if (path == "/api/layers/update-parameter")
{
command.type = RuntimeControlCommandType::UpdateLayerParameter;
const JsonValue* value = nullptr;
if (!RequireStringField(root, "layerId", command.layerId, error)
|| !RequireStringField(root, "parameterId", command.parameterId, error))
{
return false;
}
value = RequireObjectField(root, "value", error);
if (!value)
return false;
command.value = *value;
return true;
}
if (path == "/api/layers/reset-parameters")
{
command.type = RuntimeControlCommandType::ResetLayerParameters;
return RequireStringField(root, "layerId", command.layerId, error);
}
command.type = RuntimeControlCommandType::Unsupported;
error = "Endpoint is not implemented in RenderCadenceCompositor yet.";
return false;
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "RuntimeJson.h"
#include <string>
namespace RenderCadenceCompositor
{
enum class RuntimeControlCommandType
{
AddLayer,
RemoveLayer,
ReorderLayer,
SetLayerBypass,
SetLayerShader,
UpdateLayerParameter,
ResetLayerParameters,
Unsupported
};
struct RuntimeControlCommand
{
RuntimeControlCommandType type = RuntimeControlCommandType::Unsupported;
std::string layerId;
std::string shaderId;
std::string parameterId;
int targetIndex = 0;
bool bypass = false;
JsonValue value;
};
bool ParseRuntimeControlCommand(
const std::string& path,
const std::string& body,
RuntimeControlCommand& command,
std::string& error);
}

View File

@@ -93,6 +93,31 @@ inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameter
writer.Null();
}
inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value)
{
switch (parameter.type)
{
case ShaderParameterType::Boolean:
writer.Bool(value.booleanValue);
return;
case ShaderParameterType::Enum:
writer.String(value.enumValue);
return;
case ShaderParameterType::Text:
writer.String(value.textValue);
return;
case ShaderParameterType::Trigger:
case ShaderParameterType::Float:
writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front());
return;
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
WriteNumberArray(writer, value.numberValues);
return;
}
writer.Null();
}
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
{
writer.BeginObject();
@@ -122,7 +147,7 @@ inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
return "unknown";
}
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter)
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
{
writer.BeginObject();
writer.KeyString("id", parameter.id);
@@ -132,7 +157,10 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
writer.Key("defaultValue");
WriteDefaultParameterValue(writer, parameter);
writer.Key("value");
WriteDefaultParameterValue(writer, parameter);
if (value)
WriteParameterValue(writer, parameter, *value);
else
WriteDefaultParameterValue(writer, parameter);
if (!parameter.minNumbers.empty())
{
@@ -197,10 +225,10 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp
WriteFeedbackJson(writer, FeedbackSettings());
writer.Key("parameters");
writer.BeginArray();
if (shaderPackage)
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
{
for (const ShaderParameterDefinition& parameter : shaderPackage->parameters)
WriteParameterDefinitionJson(writer, parameter);
const auto valueIt = layer.parameterValues.find(parameter.id);
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
}
writer.EndArray();
writer.EndObject();

View File

@@ -286,6 +286,12 @@ HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest&
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);

View File

@@ -28,6 +28,7 @@ struct HttpControlServerCallbacks
std::function<std::string()> getStateJson;
std::function<ControlActionResult(const std::string&)> addLayer;
std::function<ControlActionResult(const std::string&)> removeLayer;
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
};
class UniqueSocket

View File

@@ -28,7 +28,10 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
nextOrder.reserve(layers.size());
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
nextOrder.push_back(layer.id);
{
if (!layer.bypass)
nextOrder.push_back(layer.id);
}
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();)
{
@@ -39,14 +42,20 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
continue;
}
if (layerIt->renderer)
layerIt->renderer->ShutdownGl();
for (LayerProgram::PassProgram& pass : layerIt->passes)
{
if (pass.renderer)
pass.renderer->ShutdownGl();
}
ReleasePendingPrograms(*layerIt);
layerIt = mLayers.erase(layerIt);
}
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
{
if (layer.artifact.fragmentShaderSource.empty())
if (layer.bypass)
continue;
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
continue;
const std::string fingerprint = Fingerprint(layer.artifact);
@@ -55,16 +64,32 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
{
LayerProgram next;
next.layerId = layer.id;
next.renderer = std::make_unique<RuntimeShaderRenderer>();
mLayers.push_back(std::move(next));
program = &mLayers.back();
}
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
bool hasReadyPass = false;
for (const LayerProgram::PassProgram& pass : program->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
{
hasReadyPass = true;
break;
}
}
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass)
{
for (LayerProgram::PassProgram& pass : program->passes)
{
if (pass.renderer)
pass.renderer->UpdateArtifactState(layer.artifact);
}
continue;
}
if (program->pendingFingerprint == fingerprint)
continue;
ReleasePendingPrograms(*program);
program->shaderId = layer.shaderId;
program->pendingFingerprint = fingerprint;
layersToPrepare.push_back(layer);
@@ -84,8 +109,13 @@ bool RuntimeRenderScene::HasLayers()
for (const std::string& layerId : mLayerOrder)
{
const LayerProgram* layer = FindLayer(layerId);
if (layer && layer->renderer && layer->renderer->HasProgram())
return true;
if (!layer)
continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
return true;
}
}
return false;
}
@@ -98,9 +128,16 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
for (const std::string& layerId : mLayerOrder)
{
LayerProgram* layer = FindLayer(layerId);
if (!layer || !layer->renderer || !layer->renderer->HasProgram())
if (!layer)
continue;
readyLayers.push_back(layer);
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
{
readyLayers.push_back(layer);
break;
}
}
}
if (readyLayers.empty())
@@ -111,14 +148,14 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (readyLayers.size() == 1)
{
readyLayers.front()->renderer->RenderFrame(frameIndex, width, height);
RenderLayer(*readyLayers.front(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true);
return;
}
if (!EnsureLayerTargets(width, height))
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers.back()->renderer->RenderFrame(frameIndex, width, height);
RenderLayer(*readyLayers.back(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true);
return;
}
@@ -130,12 +167,11 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (isFinalLayer)
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
continue;
}
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[nextTargetIndex]);
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex;
}
@@ -146,8 +182,12 @@ void RuntimeRenderScene::ShutdownGl()
mPrepareWorker.Stop();
for (LayerProgram& layer : mLayers)
{
if (layer.renderer)
layer.renderer->ShutdownGl();
for (LayerProgram::PassProgram& pass : layer.passes)
{
if (pass.renderer)
pass.renderer->ShutdownGl();
}
ReleasePendingPrograms(layer);
}
mLayers.clear();
mLayerOrder.clear();
@@ -172,28 +212,165 @@ void RuntimeRenderScene::ConsumePreparedPrograms()
continue;
}
bool replacesExistingPendingPass = false;
for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms)
{
if (existing.passId != preparedProgram.passId)
continue;
existing.ReleaseGl();
existing = std::move(preparedProgram);
replacesExistingPendingPass = true;
break;
}
if (!replacesExistingPendingPass)
layer->pendingPreparedPrograms.push_back(std::move(preparedProgram));
TryCommitPendingPrograms(*layer);
}
}
void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer)
{
for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms)
program.ReleaseGl();
layer.pendingPreparedPrograms.clear();
}
void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
{
if (layer.pendingPreparedPrograms.empty())
return;
const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact;
const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size();
if (layer.pendingPreparedPrograms.size() < expectedPassCount)
return;
std::vector<LayerProgram::PassProgram> nextPasses;
nextPasses.reserve(expectedPassCount);
for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes)
{
auto preparedIt = std::find_if(
layer.pendingPreparedPrograms.begin(),
layer.pendingPreparedPrograms.end(),
[&passArtifact](const RuntimePreparedShaderProgram& prepared) {
return prepared.passId == passArtifact.passId;
});
if (preparedIt == layer.pendingPreparedPrograms.end())
return;
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
std::string error;
if (!nextRenderer->CommitPreparedProgram(preparedProgram, error))
if (!nextRenderer->CommitPreparedProgram(*preparedIt, error))
{
preparedProgram.ReleaseGl();
ReleasePendingPrograms(layer);
return;
}
LayerProgram::PassProgram nextPass;
nextPass.passId = preparedIt->passId;
nextPass.inputNames = preparedIt->inputNames;
nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName;
nextPass.renderer = std::move(nextRenderer);
nextPasses.push_back(std::move(nextPass));
}
if (artifact.passes.empty())
{
RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front();
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
std::string error;
if (!nextRenderer->CommitPreparedProgram(prepared, error))
{
ReleasePendingPrograms(layer);
return;
}
LayerProgram::PassProgram nextPass;
nextPass.passId = prepared.passId;
nextPass.inputNames = prepared.inputNames;
nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName;
nextPass.renderer = std::move(nextRenderer);
nextPasses.push_back(std::move(nextPass));
}
for (LayerProgram::PassProgram& pass : layer.passes)
{
if (pass.renderer)
pass.renderer->ShutdownGl();
}
layer.passes = std::move(nextPasses);
layer.shaderId = artifact.shaderId;
layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint;
layer.pendingFingerprint.clear();
layer.pendingPreparedPrograms.clear();
}
GLuint RuntimeRenderScene::RenderLayer(
LayerProgram& layer,
uint64_t frameIndex,
unsigned width,
unsigned height,
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 = layerInputTexture;
if (!pass.inputNames.empty())
{
const std::string& inputName = pass.inputNames.front();
if (inputName != "layerInput" && inputName != "videoInput")
{
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, sourceTexture);
lastOutputTexture = 0;
continue;
}
if (layer->renderer)
layer->renderer->ShutdownGl();
layer->renderer = std::move(nextRenderer);
layer->shaderId = preparedProgram.shaderId;
layer->sourceFingerprint = preparedProgram.sourceFingerprint;
layer->pendingFingerprint.clear();
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, sourceTexture);
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 && mLayerTextures[0] != 0 && mLayerTextures[1] != 0
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;
@@ -201,9 +378,9 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
mLayerTargetWidth = width;
mLayerTargetHeight = height;
glGenFramebuffers(2, mLayerFramebuffers);
glGenTextures(2, mLayerTextures);
for (int index = 0; index < 2; ++index)
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);
@@ -239,14 +416,15 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
void RuntimeRenderScene::DestroyLayerTargets()
{
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0)
glDeleteFramebuffers(2, mLayerFramebuffers);
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0)
glDeleteTextures(2, mLayerTextures);
mLayerFramebuffers[0] = 0;
mLayerFramebuffers[1] = 0;
mLayerTextures[0] = 0;
mLayerTextures[1] = 0;
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;
}
@@ -271,8 +449,23 @@ const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std:
return nullptr;
}
RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId)
{
for (LayerProgram::PassProgram& pass : layer.passes)
{
if (pass.passId == passId)
return &pass;
}
return nullptr;
}
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
{
const std::hash<std::string> hasher;
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
std::string source;
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
if (source.empty())
source = artifact.fragmentShaderSource;
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
}

View File

@@ -32,21 +32,33 @@ private:
std::string shaderId;
std::string sourceFingerprint;
std::string pendingFingerprint;
std::unique_ptr<RuntimeShaderRenderer> renderer;
std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
std::unique_ptr<RuntimeShaderRenderer> renderer;
};
std::vector<PassProgram> passes;
};
void ConsumePreparedPrograms();
void ReleasePendingPrograms(LayerProgram& layer);
void TryCommitPendingPrograms(LayerProgram& layer);
bool EnsureLayerTargets(unsigned width, unsigned height);
void DestroyLayerTargets();
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const;
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
RuntimeShaderPrepareWorker mPrepareWorker;
std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder;
GLuint mLayerFramebuffers[2] = {};
GLuint mLayerTextures[2] = {};
GLuint mLayerFramebuffers[4] = {};
GLuint mLayerTextures[4] = {};
unsigned mLayerTargetWidth = 0;
unsigned mLayerTargetHeight = 0;
};

View File

@@ -82,7 +82,10 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
{
const ShaderParameterValue value = DefaultValueForDefinition(definition);
const auto valueIt = artifact.parameterValues.find(definition.id);
const ShaderParameterValue value = valueIt == artifact.parameterValues.end()
? DefaultValueForDefinition(definition)
: valueIt->second;
switch (definition.type)
{
case ShaderParameterType::Float:

View File

@@ -77,20 +77,35 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceComposito
std::lock_guard<std::mutex> lock(mMutex);
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
{
if (layer.artifact.fragmentShaderSource.empty())
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
continue;
PrepareRequest request;
request.layerId = layer.id;
request.shaderId = layer.shaderId;
request.sourceFingerprint = Fingerprint(layer.artifact);
request.artifact = layer.artifact;
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
if (passes.empty())
{
RuntimeShaderPassArtifact pass;
pass.passId = "main";
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
pass.outputName = "layerOutput";
passes.push_back(std::move(pass));
}
auto sameLayer = [&request](const PrepareRequest& existing) {
return existing.layerId == request.layerId;
auto sameLayer = [&layer](const PrepareRequest& existing) {
return existing.layerId == layer.id;
};
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
mRequests.push_back(std::move(request));
for (const RuntimeShaderPassArtifact& pass : passes)
{
PrepareRequest request;
request.layerId = layer.id;
request.shaderId = layer.shaderId;
request.passId = pass.passId;
request.sourceFingerprint = Fingerprint(layer.artifact);
request.artifact = layer.artifact;
request.passArtifact = pass;
mRequests.push_back(std::move(request));
}
}
mCondition.notify_one();
}
@@ -137,10 +152,11 @@ void RuntimeShaderPrepareWorker::ThreadMain()
}
RuntimePreparedShaderProgram preparedProgram;
RuntimeShaderRenderer::BuildPreparedProgram(
RuntimeShaderRenderer::BuildPreparedPassProgram(
request.layerId,
request.sourceFingerprint,
request.artifact,
request.passArtifact,
preparedProgram);
glFlush();
@@ -154,5 +170,10 @@ void RuntimeShaderPrepareWorker::ThreadMain()
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
{
const std::hash<std::string> hasher;
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
std::string source;
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
if (source.empty())
source = artifact.fragmentShaderSource;
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
}

View File

@@ -35,8 +35,10 @@ private:
{
std::string layerId;
std::string shaderId;
std::string passId;
std::string sourceFingerprint;
RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
};
void ThreadMain();

View File

@@ -4,13 +4,18 @@
#include "../../runtime/RuntimeShaderArtifact.h"
#include <string>
#include <vector>
struct RuntimePreparedShaderProgram
{
std::string layerId;
std::string shaderId;
std::string passId;
std::string sourceFingerprint;
RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
std::vector<std::string> inputNames;
std::string outputName;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;

View File

@@ -94,26 +94,53 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram&
return true;
}
void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
{
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
mArtifact.parameterValues = artifact.parameterValues;
mArtifact.message = artifact.message;
}
bool RuntimeShaderRenderer::BuildPreparedProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram)
{
RuntimeShaderPassArtifact passArtifact;
passArtifact.passId = "main";
passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
passArtifact.outputName = "layerOutput";
if (!artifact.passes.empty())
passArtifact = artifact.passes.front();
return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
}
bool RuntimeShaderRenderer::BuildPreparedPassProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
const RuntimeShaderPassArtifact& passArtifact,
RuntimePreparedShaderProgram& preparedProgram)
{
preparedProgram = RuntimePreparedShaderProgram();
preparedProgram.layerId = layerId;
preparedProgram.shaderId = artifact.shaderId;
preparedProgram.passId = passArtifact.passId;
preparedProgram.sourceFingerprint = sourceFingerprint;
preparedProgram.artifact = artifact;
preparedProgram.passArtifact = passArtifact;
preparedProgram.inputNames = passArtifact.inputNames;
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
if (artifact.fragmentShaderSource.empty())
if (passArtifact.fragmentShaderSource.empty())
{
preparedProgram.error = "Cannot prepare an empty fragment shader.";
return false;
}
if (!BuildProgram(
artifact.fragmentShaderSource,
passArtifact.fragmentShaderSource,
preparedProgram.program,
preparedProgram.vertexShader,
preparedProgram.fragmentShader,

View File

@@ -20,6 +20,7 @@ public:
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
bool HasProgram() const { return mProgram != 0; }
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
void ShutdownGl();
@@ -28,6 +29,12 @@ public:
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram);
static bool BuildPreparedPassProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
const RuntimeShaderPassArtifact& passArtifact,
RuntimePreparedShaderProgram& preparedProgram);
private:
bool EnsureStaticGlResources(std::string& error);

View File

@@ -1,5 +1,8 @@
#include "RuntimeLayerModel.h"
#include "RuntimeParameterUtils.h"
#include <algorithm>
#include <utility>
namespace RenderCadenceCompositor
@@ -26,6 +29,7 @@ bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shad
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
layer.buildState = RuntimeLayerBuildState::Pending;
layer.message = "Runtime Slang build is waiting to start.";
InitializeDefaultParameterValues(layer, *shaderPackage);
mLayers.push_back(std::move(layer));
error.clear();
return true;
@@ -46,6 +50,7 @@ bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, co
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
layer.buildState = RuntimeLayerBuildState::Pending;
layer.message = "Runtime Slang build is waiting to start.";
InitializeDefaultParameterValues(layer, *shaderPackage);
layerId = layer.id;
mLayers.push_back(std::move(layer));
error.clear();
@@ -68,6 +73,117 @@ bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& err
return false;
}
bool RuntimeLayerModel::ReorderLayer(const std::string& layerId, int targetIndex, std::string& error)
{
auto layerIt = std::find_if(mLayers.begin(), mLayers.end(), [&layerId](const Layer& layer) {
return layer.id == layerId;
});
if (layerIt == mLayers.end())
{
error = "Unknown runtime layer id: " + layerId;
return false;
}
if (targetIndex < 0)
targetIndex = 0;
if (targetIndex >= static_cast<int>(mLayers.size()))
targetIndex = static_cast<int>(mLayers.size()) - 1;
Layer layer = std::move(*layerIt);
mLayers.erase(layerIt);
std::size_t destinationIndex = static_cast<std::size_t>(targetIndex);
if (destinationIndex > mLayers.size())
destinationIndex = mLayers.size();
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(destinationIndex), std::move(layer));
error.clear();
return true;
}
bool RuntimeLayerModel::SetLayerBypass(const std::string& layerId, bool bypass, std::string& error)
{
Layer* layer = FindLayer(layerId);
if (!layer)
{
error = "Unknown runtime layer id: " + layerId;
return false;
}
layer->bypass = bypass;
error.clear();
return true;
}
bool RuntimeLayerModel::SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
{
Layer* layer = FindLayer(layerId);
if (!layer)
{
error = "Unknown runtime layer id: " + layerId;
return false;
}
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
if (!shaderPackage)
{
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
return false;
}
layer->shaderId = shaderPackage->id;
layer->shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
layer->buildState = RuntimeLayerBuildState::Pending;
layer->message = "Runtime Slang build is waiting to start.";
layer->renderReady = false;
layer->artifact = RuntimeShaderArtifact();
InitializeDefaultParameterValues(*layer, *shaderPackage);
error.clear();
return true;
}
bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error)
{
Layer* layer = FindLayer(layerId);
if (!layer)
{
error = "Unknown runtime layer id: " + layerId;
return false;
}
const ShaderParameterDefinition* definition = FindParameterDefinition(*layer, parameterId);
if (!definition)
{
error = "Unknown parameter id '" + parameterId + "' for layer " + layerId + ".";
return false;
}
ShaderParameterValue normalizedValue;
if (!NormalizeAndValidateParameterValue(*definition, value, normalizedValue, error))
return false;
layer->parameterValues[parameterId] = normalizedValue;
if (layer->renderReady)
layer->artifact.parameterValues = layer->parameterValues;
error.clear();
return true;
}
bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& error)
{
Layer* layer = FindLayer(layerId);
if (!layer)
{
error = "Unknown runtime layer id: " + layerId;
return false;
}
layer->parameterValues.clear();
for (const ShaderParameterDefinition& definition : layer->parameterDefinitions)
layer->parameterValues[definition.id] = DefaultValueForDefinition(definition);
if (layer->renderReady)
layer->artifact.parameterValues = layer->parameterValues;
error.clear();
return true;
}
void RuntimeLayerModel::Clear()
{
mLayers.clear();
@@ -106,6 +222,7 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st
layer->message = artifact.message;
layer->renderReady = true;
layer->artifact = artifact;
layer->artifact.parameterValues = layer->parameterValues;
error.clear();
return true;
}
@@ -159,7 +276,9 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
RuntimeRenderLayerModel renderLayer;
renderLayer.id = layer.id;
renderLayer.shaderId = layer.shaderId;
renderLayer.bypass = layer.bypass;
renderLayer.artifact = layer.artifact;
renderLayer.artifact.parameterValues = layer.parameterValues;
snapshot.renderLayers.push_back(std::move(renderLayer));
}
}
@@ -204,6 +323,24 @@ RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::
return nullptr;
}
void RuntimeLayerModel::InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage)
{
layer.parameterDefinitions = shaderPackage.parameters;
layer.parameterValues.clear();
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
layer.parameterValues[definition.id] = DefaultValueForDefinition(definition);
}
const ShaderParameterDefinition* RuntimeLayerModel::FindParameterDefinition(const Layer& layer, const std::string& parameterId)
{
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
{
if (definition.id == parameterId)
return &definition;
}
return nullptr;
}
std::string RuntimeLayerModel::AllocateLayerId()
{
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
@@ -219,6 +356,8 @@ RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer)
readModel.buildState = layer.buildState;
readModel.message = layer.message;
readModel.renderReady = layer.renderReady;
readModel.parameterDefinitions = layer.parameterDefinitions;
readModel.parameterValues = layer.parameterValues;
return readModel;
}
}

View File

@@ -1,9 +1,11 @@
#pragma once
#include "RuntimeJson.h"
#include "RuntimeShaderArtifact.h"
#include "SupportedShaderCatalog.h"
#include <cstdint>
#include <map>
#include <string>
#include <vector>
@@ -25,12 +27,15 @@ struct RuntimeLayerReadModel
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
std::string message;
bool renderReady = false;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
};
struct RuntimeRenderLayerModel
{
std::string id;
std::string shaderId;
bool bypass = false;
RuntimeShaderArtifact artifact;
};
@@ -50,6 +55,11 @@ public:
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
bool RemoveLayer(const std::string& layerId, std::string& error);
bool ReorderLayer(const std::string& layerId, int targetIndex, std::string& error);
bool SetLayerBypass(const std::string& layerId, bool bypass, std::string& error);
bool SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error);
bool ResetParameters(const std::string& layerId, std::string& error);
bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error);
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
@@ -69,12 +79,16 @@ private:
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
std::string message;
bool renderReady = false;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
RuntimeShaderArtifact artifact;
};
Layer* FindLayer(const std::string& layerId);
const Layer* FindLayer(const std::string& layerId) const;
Layer* FindFirstLayerForShader(const std::string& shaderId);
static void InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage);
static const ShaderParameterDefinition* FindParameterDefinition(const Layer& layer, const std::string& parameterId);
std::string AllocateLayerId();
static RuntimeLayerReadModel ToReadModel(const Layer& layer);

View File

@@ -2,15 +2,26 @@
#include "ShaderTypes.h"
#include <map>
#include <string>
#include <vector>
struct RuntimeShaderPassArtifact
{
std::string passId;
std::string fragmentShaderSource;
std::vector<std::string> inputNames;
std::string outputName;
};
struct RuntimeShaderArtifact
{
std::string layerId;
std::string shaderId;
std::string displayName;
std::string fragmentShaderSource;
std::vector<RuntimeShaderPassArtifact> passes;
std::string message;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
};

View File

@@ -112,8 +112,6 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
return build;
}
const ShaderPassDefinition& pass = shaderPackage.passes.front();
ShaderCompiler compiler(
repoRoot,
runtimeBuildDir / (shaderId + ".wrapper.slang"),
@@ -122,11 +120,22 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
0);
const auto start = std::chrono::steady_clock::now();
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error))
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
build.succeeded = false;
build.message = error.empty() ? "Slang compile failed." : error;
return build;
std::string fragmentShaderSource;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
{
build.succeeded = false;
build.message = error.empty() ? "Slang compile failed." : error;
return build;
}
RuntimeShaderPassArtifact passArtifact;
passArtifact.passId = pass.id;
passArtifact.fragmentShaderSource = std::move(fragmentShaderSource);
passArtifact.inputNames = pass.inputNames;
passArtifact.outputName = pass.outputName;
build.artifact.passes.push_back(std::move(passArtifact));
}
const auto end = std::chrono::steady_clock::now();
@@ -135,6 +144,8 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
build.artifact.shaderId = shaderPackage.id;
build.artifact.displayName = shaderPackage.displayName;
build.artifact.parameterDefinitions = shaderPackage.parameters;
if (!build.artifact.passes.empty())
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
build.message = build.artifact.message;
return build;

View File

@@ -9,8 +9,8 @@ namespace RenderCadenceCompositor
{
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
{
if (shaderPackage.passes.size() != 1)
return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." };
if (shaderPackage.passes.empty())
return { false, "Shader package has no render passes." };
if (shaderPackage.temporal.enabled)
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
@@ -30,6 +30,35 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
}
bool writesLayerOutput = false;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
if (pass.sourcePath.empty())
{
return { false, "Shader pass '" + pass.id + "' has no source." };
}
if (pass.outputName == "layerOutput")
writesLayerOutput = true;
for (const std::string& inputName : pass.inputNames)
{
if (inputName == "videoInput" || inputName == "layerInput")
continue;
bool matchesNamedOutput = false;
for (const ShaderPassDefinition& outputPass : shaderPackage.passes)
{
if (outputPass.outputName == inputName)
{
matchesNamedOutput = true;
break;
}
}
if (!matchesNamedOutput)
return { false, "Shader pass '" + pass.id + "' references unknown input '" + inputName + "'." };
}
}
if (!writesLayerOutput)
return { false, "Shader package must write a pass output named 'layerOutput'." };
return { true, std::string() };
}

View File

@@ -147,6 +147,29 @@ void TestLayerPostEndpointsUseCallbacks()
Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
}
void TestGenericPostCallbackHandlesControlRoutes()
{
using namespace RenderCadenceCompositor;
HttpControlServer server;
HttpControlServerCallbacks callbacks;
callbacks.executePost = [](const std::string& path, const std::string& body) {
ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path");
Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body");
return ControlActionResult{ true, std::string() };
};
server.SetCallbacksForTest(callbacks);
HttpControlServer::HttpRequest request;
request.method = "POST";
request.path = "/api/layers/set-bypass";
request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}";
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
ExpectEquals(response.status, "200 OK", "generic control callback success returns 200");
Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success");
}
void TestUnknownEndpointReturns404()
{
using namespace RenderCadenceCompositor;
@@ -169,6 +192,7 @@ int main()
TestRootServesUiIndex();
TestKnownPostEndpointReturnsActionError();
TestLayerPostEndpointsUseCallbacks();
TestGenericPostCallbackHandlesControlRoutes();
TestUnknownEndpointReturns404();
if (gFailures != 0)

View File

@@ -143,6 +143,49 @@ void TestAddAndRemoveLayers()
std::filesystem::remove_all(root);
}
void TestLayerControlsUpdateDisplayAndRenderModels()
{
std::filesystem::path root;
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
RenderCadenceCompositor::RuntimeLayerModel model;
std::string error;
std::string firstLayerId;
std::string secondLayerId;
Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first control layer can be added");
Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added");
Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set");
Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered");
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index");
Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model");
JsonValue gainValue(0.75);
Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated");
snapshot = model.Snapshot();
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible");
RuntimeShaderArtifact artifact;
artifact.layerId = firstLayerId;
artifact.shaderId = "solid";
artifact.displayName = "Solid";
artifact.fragmentShaderSource = "void main(){}";
artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions;
artifact.message = "build ready";
Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state");
snapshot = model.Snapshot();
Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model");
Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state");
Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value");
Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults");
snapshot = model.Snapshot();
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value");
std::filesystem::remove_all(root);
}
}
int main()
@@ -151,6 +194,7 @@ int main()
TestRejectsUnsupportedStartupShader();
TestBuildFailureStaysDisplaySide();
TestAddAndRemoveLayers();
TestLayerControlsUpdateDisplayAndRenderModels();
if (gFailures != 0)
{

View File

@@ -23,6 +23,8 @@ ShaderPackage MakeSinglePassPackage()
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = "mainImage";
pass.sourcePath = "shader.slang";
pass.outputName = "layerOutput";
shaderPackage.passes.push_back(pass);
return shaderPackage;
}
@@ -37,19 +39,35 @@ void SupportsSinglePassStatelessPackage()
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
}
void RejectsMultipassPackage()
void SupportsStatelessNamedPassPackage()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
shaderPackage.passes.front().outputName = "generatedMask";
ShaderPassDefinition secondPass;
secondPass.id = "second";
secondPass.entryPoint = "mainImage";
secondPass.sourcePath = "shader.slang";
secondPass.inputNames.push_back("generatedMask");
secondPass.outputName = "layerOutput";
shaderPackage.passes.push_back(secondPass);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "multipass packages should be rejected");
Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit");
Expect(result.supported, "stateless named-pass packages should be supported");
Expect(result.reason.empty(), "supported named-pass packages should not report a rejection reason");
}
void RejectsUnknownPassInput()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
shaderPackage.passes.front().inputNames.push_back("missingIntermediate");
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "packages with unknown pass inputs should be rejected");
Expect(result.reason.find("unknown input") != std::string::npos, "unknown input rejection should explain the missing named output");
}
void RejectsTemporalPackage()
@@ -97,7 +115,8 @@ void RejectsTextParameters()
int main()
{
SupportsSinglePassStatelessPackage();
RejectsMultipassPackage();
SupportsStatelessNamedPassPackage();
RejectsUnknownPassInput();
RejectsTemporalPackage();
RejectsTextureAssets();
RejectsTextParameters();