OSC fixes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m43s

This commit is contained in:
Aiden
2026-05-10 18:37:30 +10:00
parent f11d531e0c
commit d7ca42b51b
14 changed files with 549 additions and 16 deletions

View File

@@ -17,6 +17,7 @@
namespace
{
constexpr DWORD kStateBroadcastIntervalMs = 250;
constexpr DWORD kStateBroadcastThrottleMs = 50;
bool InitializeWinsock(std::string& error)
{
@@ -75,7 +76,7 @@ std::string GuessContentType(const std::filesystem::path& assetPath)
}
ControlServer::ControlServer()
: mPort(0), mRunning(false)
: mPort(0), mRunning(false), mBroadcastPending(false)
{
}
@@ -161,10 +162,16 @@ void ControlServer::Stop()
void ControlServer::BroadcastState()
{
mBroadcastPending = false;
std::lock_guard<std::mutex> lock(mMutex);
BroadcastStateLocked();
}
void ControlServer::RequestBroadcastState()
{
mBroadcastPending = true;
}
void ControlServer::ServerLoop()
{
DWORD lastStateBroadcastMs = GetTickCount();
@@ -173,7 +180,12 @@ void ControlServer::ServerLoop()
TryAcceptClient();
const DWORD nowMs = GetTickCount();
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
if (mBroadcastPending && nowMs - lastStateBroadcastMs >= kStateBroadcastThrottleMs)
{
BroadcastState();
lastStateBroadcastMs = nowMs;
}
else if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
{
BroadcastState();
lastStateBroadcastMs = nowMs;
@@ -469,6 +481,7 @@ bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const Http
client.socket.reset(clientSocket.release());
client.websocket = true;
mClients.push_back(std::move(client));
mBroadcastPending = false;
BroadcastStateLocked();
}
return true;

View File

@@ -41,6 +41,7 @@ public:
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
void Stop();
void BroadcastState();
void RequestBroadcastState();
unsigned short GetPort() const { return mPort; }
@@ -100,6 +101,7 @@ private:
unsigned short mPort;
std::thread mThread;
std::atomic<bool> mRunning;
std::atomic<bool> mBroadcastPending;
mutable std::mutex mMutex;
std::vector<ClientConnection> mClients;
};

View File

@@ -4,10 +4,12 @@
#include "OpenGLComposite.h"
#include "OscServer.h"
#include "RuntimeHost.h"
#include "RuntimeServices.h"
bool StartRuntimeControlServices(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
RuntimeServices& runtimeServices,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error)
@@ -41,8 +43,8 @@ bool StartRuntimeControlServices(
runtimeHost.SetServerPort(controlServer.GetPort());
OscServer::Callbacks oscCallbacks;
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
oscCallbacks.updateParameter = [&runtimeServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return runtimeServices.QueueOscUpdate(layerKey, parameterKey, valueJson, actionError);
};
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
return false;

View File

@@ -6,10 +6,12 @@ class ControlServer;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
class RuntimeServices;
bool StartRuntimeControlServices(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
RuntimeServices& runtimeServices,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error);

View File

@@ -4,7 +4,6 @@
#include "OscServer.h"
#include "RuntimeControlBridge.h"
#include "RuntimeHost.h"
#include <windows.h>
RuntimeServices::RuntimeServices() :
@@ -26,7 +25,7 @@ bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost
{
Stop();
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
if (!StartRuntimeControlServices(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
{
Stop();
return false;
@@ -57,6 +56,62 @@ void RuntimeServices::BroadcastState()
mControlServer->BroadcastState();
}
void RuntimeServices::RequestBroadcastState()
{
if (mControlServer)
mControlServer->RequestBroadcastState();
}
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
{
(void)error;
PendingOscUpdate update;
update.layerKey = layerKey;
update.parameterKey = parameterKey;
update.valueJson = valueJson;
const std::string routeKey = layerKey + "\n" + parameterKey;
{
std::lock_guard<std::mutex> lock(mPendingOscMutex);
mPendingOscUpdates[routeKey] = std::move(update);
}
return true;
}
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
{
appliedUpdates.clear();
std::map<std::string, PendingOscUpdate> pending;
{
std::lock_guard<std::mutex> lock(mPendingOscMutex);
if (mPendingOscUpdates.empty())
return true;
pending.swap(mPendingOscUpdates);
}
for (const auto& entry : pending)
{
JsonValue targetValue;
std::string parseError;
if (!ParseJson(entry.second.valueJson, targetValue, parseError))
{
OutputDebugStringA(("OSC queued value parse failed: " + parseError + "\n").c_str());
continue;
}
AppliedOscUpdate appliedUpdate;
appliedUpdate.layerKey = entry.second.layerKey;
appliedUpdate.parameterKey = entry.second.parameterKey;
appliedUpdate.targetValue = targetValue;
appliedUpdates.push_back(std::move(appliedUpdate));
}
(void)error;
return true;
}
RuntimePollEvents RuntimeServices::ConsumePollEvents()
{
RuntimePollEvents events;

View File

@@ -1,6 +1,10 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <string>
@@ -22,6 +26,13 @@ struct RuntimePollEvents
class RuntimeServices
{
public:
struct AppliedOscUpdate
{
std::string layerKey;
std::string parameterKey;
JsonValue targetValue;
};
RuntimeServices();
~RuntimeServices();
@@ -29,9 +40,19 @@ public:
void BeginPolling(RuntimeHost& runtimeHost);
void Stop();
void BroadcastState();
void RequestBroadcastState();
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
RuntimePollEvents ConsumePollEvents();
private:
struct PendingOscUpdate
{
std::string layerKey;
std::string parameterKey;
std::string valueJson;
};
void StartPolling(RuntimeHost& runtimeHost);
void StopPolling();
void PollLoop(RuntimeHost& runtimeHost);
@@ -45,4 +66,6 @@ private:
std::atomic<bool> mPollFailed;
std::mutex mPollErrorMutex;
std::string mPollError;
std::mutex mPendingOscMutex;
std::map<std::string, PendingOscUpdate> mPendingOscUpdates;
};

View File

@@ -8,19 +8,74 @@
#include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h"
#include "RuntimeParameterUtils.h"
#include "RuntimeServices.h"
#include "ShaderBuildQueue.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <vector>
namespace
{
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
std::string SimplifyOscControlKey(const std::string& text)
{
std::string simplified;
for (unsigned char ch : text)
{
if (std::isalnum(ch))
simplified.push_back(static_cast<char>(std::tolower(ch)));
}
return simplified;
}
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
{
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
}
double ClampOscAlpha(double value)
{
return (std::max)(0.0, (std::min)(1.0, value));
}
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
{
switch (definition.type)
{
case ShaderParameterType::Boolean:
return JsonValue(value.booleanValue);
case ShaderParameterType::Enum:
return JsonValue(value.enumValue);
case ShaderParameterType::Text:
return JsonValue(value.textValue);
case ShaderParameterType::Trigger:
case ShaderParameterType::Float:
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
JsonValue array = JsonValue::MakeArray();
for (double number : value.numberValues)
array.pushBack(JsonValue(number));
return array;
}
}
return JsonValue();
}
}
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mVideoIO(std::make_unique<DeckLinkSession>()),
@@ -322,12 +377,158 @@ bool OpenGLComposite::RequestScreenshot(std::string& error)
void OpenGLComposite::renderEffect()
{
ProcessRuntimePollResults();
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
if (mRuntimeHost && mRuntimeServices)
{
std::string oscError;
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
}
std::set<std::string> pendingOscRouteKeys;
const auto oscNow = std::chrono::steady_clock::now();
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
{
const std::string routeKey = update.layerKey + "\n" + update.parameterKey;
auto overlayIt = mOscOverlayStates.find(routeKey);
if (overlayIt == mOscOverlayStates.end())
{
OscOverlayState overlay;
overlay.layerKey = update.layerKey;
overlay.parameterKey = update.parameterKey;
overlay.targetValue = update.targetValue;
overlay.lastUpdatedTime = oscNow;
mOscOverlayStates[routeKey] = std::move(overlay);
}
else
{
overlayIt->second.targetValue = update.targetValue;
overlayIt->second.lastUpdatedTime = oscNow;
}
pendingOscRouteKeys.insert(routeKey);
}
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
{
if (states.empty() || mOscOverlayStates.empty() || !mRuntimeHost)
return;
const double smoothing = ClampOscAlpha(mRuntimeHost->GetOscSmoothing());
std::vector<std::string> overlayKeysToRemove;
for (auto& item : mOscOverlayStates)
{
OscOverlayState& overlay = item.second;
auto stateIt = std::find_if(states.begin(), states.end(),
[&overlay](const RuntimeRenderState& state)
{
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
MatchesOscControlKey(state.shaderName, overlay.layerKey);
});
if (stateIt == states.end())
continue;
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
[&overlay](const ShaderParameterDefinition& definition)
{
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
MatchesOscControlKey(definition.label, overlay.parameterKey);
});
if (definitionIt == stateIt->parameterDefinitions.end())
continue;
if (definitionIt->type == ShaderParameterType::Trigger)
{
if (pendingOscRouteKeys.find(item.first) == pendingOscRouteKeys.end())
continue;
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = stateIt->timeSeconds;
value.numberValues = { previousCount + 1.0, triggerTime };
overlayKeysToRemove.push_back(item.first);
continue;
}
ShaderParameterValue targetValue;
std::string normalizeError;
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
continue;
const bool smoothable =
smoothing > 0.0 &&
(definitionIt->type == ShaderParameterType::Float ||
definitionIt->type == ShaderParameterType::Vec2 ||
definitionIt->type == ShaderParameterType::Color);
if (!smoothable)
{
overlay.currentValue = targetValue;
overlay.hasCurrentValue = true;
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
if (allowCommit && oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay)
{
std::string commitError;
if (mRuntimeHost->UpdateLayerParameterByControlKey(overlay.layerKey, overlay.parameterKey, overlay.targetValue, false, commitError))
overlayKeysToRemove.push_back(item.first);
}
continue;
}
if (!overlay.hasCurrentValue)
{
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
if (currentIt != stateIt->parameterValues.end())
overlay.currentValue = currentIt->second;
overlay.hasCurrentValue = true;
}
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
overlay.currentValue.numberValues = targetValue.numberValues;
ShaderParameterValue nextValue = targetValue;
bool converged = true;
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
{
const double currentNumber = overlay.currentValue.numberValues[index];
const double targetNumber = targetValue.numberValues[index];
const double delta = targetNumber - currentNumber;
double nextNumber = currentNumber + delta * smoothing;
if (std::fabs(delta) <= 0.0005)
nextNumber = targetNumber;
else
converged = false;
nextValue.numberValues[index] = nextNumber;
}
if (converged)
nextValue.numberValues = targetValue.numberValues;
overlay.currentValue = nextValue;
overlay.hasCurrentValue = true;
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
if (allowCommit && converged && oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay)
{
std::string commitError;
JsonValue committedValue = BuildOscCommitValue(*definitionIt, overlay.currentValue);
if (mRuntimeHost->UpdateLayerParameterByControlKey(overlay.layerKey, overlay.parameterKey, committedValue, false, commitError))
overlayKeysToRemove.push_back(item.first);
}
}
if (allowCommit)
{
for (const std::string& overlayKey : overlayKeysToRemove)
mOscOverlayStates.erase(overlayKey);
}
};
const bool hasInputSource = mVideoIO->HasInputSource();
std::vector<RuntimeRenderState> layerStates;
if (mUseCommittedLayerStates)
{
layerStates = mShaderPrograms->CommittedLayerStates();
applyOscOverlays(layerStates, false);
if (mRuntimeHost)
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
@@ -336,6 +537,7 @@ void OpenGLComposite::renderEffect()
const unsigned renderWidth = mVideoIO->InputFrameWidth();
const unsigned renderHeight = mVideoIO->InputFrameHeight();
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
const uint64_t parameterStateVersion = mRuntimeHost->GetParameterStateVersion();
const bool renderStateCacheValid =
!mCachedLayerRenderStates.empty() &&
mCachedRenderStateVersion == renderStateVersion &&
@@ -344,6 +546,13 @@ void OpenGLComposite::renderEffect()
if (renderStateCacheValid)
{
applyOscOverlays(mCachedLayerRenderStates, true);
if (mCachedParameterStateVersion != parameterStateVersion &&
mRuntimeHost->TryRefreshCachedLayerStates(mCachedLayerRenderStates))
{
mCachedParameterStateVersion = parameterStateVersion;
applyOscOverlays(mCachedLayerRenderStates, true);
}
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
@@ -353,11 +562,15 @@ void OpenGLComposite::renderEffect()
{
mCachedLayerRenderStates = layerStates;
mCachedRenderStateVersion = renderStateVersion;
mCachedParameterStateVersion = parameterStateVersion;
mCachedRenderStateWidth = renderWidth;
mCachedRenderStateHeight = renderHeight;
applyOscOverlays(mCachedLayerRenderStates, true);
layerStates = mCachedLayerRenderStates;
}
else
{
applyOscOverlays(mCachedLayerRenderStates, true);
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}

View File

@@ -73,6 +73,15 @@ private:
bool CheckOpenGLExtensions();
void PublishVideoIOStatus(const std::string& statusMessage);
using LayerProgram = OpenGLRenderer::LayerProgram;
struct OscOverlayState
{
std::string layerKey;
std::string parameterKey;
JsonValue targetValue;
ShaderParameterValue currentValue;
bool hasCurrentValue = false;
std::chrono::steady_clock::time_point lastUpdatedTime;
};
HWND hGLWnd;
HDC hGLDC;
@@ -90,8 +99,10 @@ private:
std::unique_ptr<RuntimeServices> mRuntimeServices;
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
uint64_t mCachedRenderStateVersion = 0;
uint64_t mCachedParameterStateVersion = 0;
unsigned mCachedRenderStateWidth = 0;
unsigned mCachedRenderStateHeight = 0;
std::map<std::string, OscOverlayState> mOscOverlayStates;
std::atomic<bool> mUseCommittedLayerStates;
std::atomic<bool> mScreenshotRequested;
std::chrono::steady_clock::time_point mLastPreviewPresentTime;

View File

@@ -33,6 +33,11 @@ bool IsFiniteNumber(double value)
return std::isfinite(value) != 0;
}
double Clamp01(double value)
{
return std::max(0.0, std::min(1.0, value));
}
std::string ToLowerCopy(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(),
@@ -56,6 +61,20 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key)
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
}
bool JsonValueContainsOnlyNumbers(const JsonValue& value)
{
if (!value.isArray())
return false;
for (const JsonValue& item : value.asArray())
{
if (!item.isNumber() || !IsFiniteNumber(item.asNumber()))
return false;
}
return true;
}
double GenerateStartupRandom()
{
std::random_device randomDevice;
@@ -970,7 +989,7 @@ bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std:
layer->bypass = bypassed;
mReloadRequested = true;
MarkRenderStateDirtyLocked();
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
@@ -1032,7 +1051,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
value.numberValues = { previousCount + 1.0, triggerTime };
MarkRenderStateDirtyLocked();
MarkParameterStateDirtyLocked();
return true;
}
@@ -1041,11 +1060,16 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
return false;
layer->parameterValues[parameterId] = normalized;
MarkRenderStateDirtyLocked();
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
{
return UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, true, error);
}
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
@@ -1089,7 +1113,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
value.numberValues = { previousCount + 1.0, triggerTime };
MarkRenderStateDirtyLocked();
MarkParameterStateDirtyLocked();
return true;
}
@@ -1098,8 +1122,141 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
return false;
matchedLayer->parameterValues[parameterIt->id] = normalized;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
MarkParameterStateDirtyLocked();
return !persistState || SavePersistentState(error);
}
bool RuntimeHost::ApplyOscTargetByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& targetValue, double smoothingAmount, bool& keepApplying, std::string& resolvedLayerId, std::string& resolvedParameterId, ShaderParameterValue& appliedValue, std::string& error)
{
keepApplying = false;
resolvedLayerId.clear();
resolvedParameterId.clear();
appliedValue = ShaderParameterValue();
std::lock_guard<std::mutex> lock(mMutex);
LayerPersistentState* matchedLayer = nullptr;
const ShaderPackage* matchedPackage = nullptr;
for (LayerPersistentState& layer : mPersistentState.layers)
{
auto shaderIt = mPackagesById.find(layer.shaderId);
if (shaderIt == mPackagesById.end())
continue;
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) ||
MatchesControlKey(shaderIt->second.displayName, layerKey))
{
matchedLayer = &layer;
matchedPackage = &shaderIt->second;
break;
}
}
if (!matchedLayer || !matchedPackage)
{
error = "Unknown OSC layer key: " + layerKey;
return false;
}
resolvedLayerId = matchedLayer->id;
const auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
[&parameterKey](const ShaderParameterDefinition& definition)
{
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
});
if (parameterIt == matchedPackage->parameters.end())
{
error = "Unknown OSC parameter key: " + parameterKey;
return false;
}
resolvedParameterId = parameterIt->id;
if (parameterIt->type == ShaderParameterType::Trigger)
{
ShaderParameterValue& value = matchedLayer->parameterValues[parameterIt->id];
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
value.numberValues = { previousCount + 1.0, triggerTime };
MarkParameterStateDirtyLocked();
appliedValue = value;
return true;
}
ShaderParameterValue normalizedTarget;
if (!NormalizeAndValidateValue(*parameterIt, targetValue, normalizedTarget, error))
return false;
const bool smoothableType =
parameterIt->type == ShaderParameterType::Float ||
parameterIt->type == ShaderParameterType::Vec2 ||
parameterIt->type == ShaderParameterType::Color;
const bool smoothableInput = targetValue.isNumber() || JsonValueContainsOnlyNumbers(targetValue);
const double alpha = Clamp01(smoothingAmount);
if (!smoothableType || !smoothableInput || alpha <= 0.0)
{
matchedLayer->parameterValues[parameterIt->id] = normalizedTarget;
MarkParameterStateDirtyLocked();
appliedValue = normalizedTarget;
return true;
}
ShaderParameterValue currentValue = DefaultValueForDefinition(*parameterIt);
auto currentIt = matchedLayer->parameterValues.find(parameterIt->id);
if (currentIt != matchedLayer->parameterValues.end())
currentValue = currentIt->second;
ShaderParameterValue nextValue = normalizedTarget;
nextValue.numberValues = normalizedTarget.numberValues;
if (currentValue.numberValues.size() != normalizedTarget.numberValues.size())
currentValue.numberValues = normalizedTarget.numberValues;
bool changed = false;
bool converged = true;
for (std::size_t index = 0; index < normalizedTarget.numberValues.size(); ++index)
{
const double currentNumber = currentValue.numberValues[index];
const double targetNumber = normalizedTarget.numberValues[index];
const double delta = targetNumber - currentNumber;
double nextNumber = currentNumber + delta * alpha;
if (std::fabs(delta) <= 0.0005)
{
nextNumber = targetNumber;
}
else
{
converged = false;
}
if (std::fabs(nextNumber - currentNumber) > 0.0000001)
changed = true;
nextValue.numberValues[index] = nextNumber;
}
if (!converged)
{
keepApplying = true;
}
else
{
nextValue.numberValues = normalizedTarget.numberValues;
}
if (!changed && !keepApplying)
{
appliedValue = matchedLayer->parameterValues[parameterIt->id];
return true;
}
matchedLayer->parameterValues[parameterIt->id] = nextValue;
MarkParameterStateDirtyLocked();
appliedValue = nextValue;
return true;
}
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
@@ -1122,7 +1279,7 @@ bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string&
layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
MarkRenderStateDirtyLocked();
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
@@ -1226,6 +1383,12 @@ void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned
void RuntimeHost::MarkRenderStateDirtyLocked()
{
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::MarkParameterStateDirtyLocked()
{
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
@@ -1388,6 +1551,34 @@ bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputH
return true;
}
bool RuntimeHost::TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
if (!lock.owns_lock())
return false;
for (RuntimeRenderState& state : states)
{
const auto layerIt = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&state](const LayerPersistentState& layer) { return layer.id == state.layerId; });
if (layerIt == mPersistentState.layers.end())
continue;
state.bypass = layerIt->bypass ? 1.0 : 0.0;
state.parameterValues.clear();
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
ShaderParameterValue value = DefaultValueForDefinition(definition);
auto valueIt = layerIt->parameterValues.find(definition.id);
if (valueIt != layerIt->parameterValues.end())
value = valueIt->second;
state.parameterValues[definition.id] = value;
}
}
return true;
}
void RuntimeHost::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
{
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
@@ -1415,6 +1606,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
RuntimeRenderState state;
state.layerId = layer.id;
state.shaderId = layer.shaderId;
state.shaderName = shaderIt->second.displayName;
state.mixAmount = 1.0;
state.bypass = layer.bypass ? 1.0 : 0.0;
state.inputWidth = mSignalWidth;
@@ -1476,6 +1668,8 @@ bool RuntimeHost::LoadConfig(std::string& error)
mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress"))
mConfig.oscBindAddress = oscBindAddressValue->asString();
if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing"))
mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mConfig.oscSmoothing));
if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
@@ -1873,6 +2067,7 @@ JsonValue RuntimeHost::BuildStateValue() const
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress));
app.set("oscSmoothing", JsonValue(mConfig.oscSmoothing));
app.set("autoReload", JsonValue(mAutoReloadEnabled));
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));

View File

@@ -31,6 +31,8 @@ public:
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error);
bool ApplyOscTargetByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& targetValue, double smoothingAmount, bool& keepApplying, std::string& resolvedLayerId, std::string& resolvedParameterId, ShaderParameterValue& appliedValue, std::string& error);
bool ResetLayerParameters(const std::string& layerId, std::string& error);
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
bool LoadStackPreset(const std::string& presetName, std::string& error);
@@ -54,9 +56,11 @@ public:
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
bool TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
std::string BuildStateJson() const;
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
uint64_t GetParameterStateVersion() const { return mParameterStateVersion.load(std::memory_order_relaxed); }
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
@@ -65,6 +69,7 @@ public:
unsigned short GetServerPort() const { return mServerPort; }
unsigned short GetOscPort() const { return mConfig.oscPort; }
const std::string& GetOscBindAddress() const { return mConfig.oscBindAddress; }
double GetOscSmoothing() const { return mConfig.oscSmoothing; }
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
unsigned GetPreviewFps() const { return mConfig.previewFps; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
@@ -82,6 +87,7 @@ private:
unsigned short serverPort = 8080;
unsigned short oscPort = 9000;
std::string oscBindAddress = "127.0.0.1";
double oscSmoothing = 0.18;
bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4;
unsigned previewFps = 30;
@@ -141,6 +147,7 @@ private:
std::string GenerateLayerId();
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void MarkRenderStateDirtyLocked();
void MarkParameterStateDirtyLocked();
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
@@ -187,5 +194,6 @@ private:
std::chrono::steady_clock::time_point mLastScanTime;
std::atomic<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
std::atomic<uint64_t> mParameterStateVersion{ 0 };
uint64_t mNextLayerId;
};

View File

@@ -128,6 +128,7 @@ struct RuntimeRenderState
{
std::string layerId;
std::string shaderId;
std::string shaderName;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;