OSC fixes
This commit is contained in:
@@ -143,6 +143,7 @@ Current native test coverage includes:
|
||||
"serverPort": 8080,
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
@@ -210,7 +211,7 @@ The native host also listens for OSC parameter control on the configured `oscBin
|
||||
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
||||
```
|
||||
|
||||
For example, `/VideoShaderToys/VHS/intensity` updates the `intensity` parameter on the first matching `VHS` layer. The listener accepts float, integer, string, and boolean OSC values, and validates them through the same shader parameter path as the REST API. The default bind address is `127.0.0.1`; set `oscBindAddress` to `0.0.0.0` to accept OSC on all IPv4 interfaces. See `docs/OSC_CONTROL.md` for details.
|
||||
For example, `/VideoShaderToys/VHS/intensity` updates the `intensity` parameter on the first matching `VHS` layer. The listener accepts float, integer, string, and boolean OSC values, and validates them through the same shader parameter path as the REST API. OSC updates are coalesced and applied once per render tick, UI state broadcasts are throttled, and OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. `oscSmoothing` adds a small per-frame easing amount for numeric OSC controls such as floats, `vec2`, and `color`, while booleans, enums, text, and triggers stay immediate. The default bind address is `127.0.0.1`; set `oscBindAddress` to `0.0.0.0` to accept OSC on all IPv4 interfaces. See `docs/OSC_CONTROL.md` for details.
|
||||
|
||||
## Shader Packages
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
[¶meterKey](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)));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"shaderLibrary": "shaders",
|
||||
"serverPort": 8080,
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscBindAddress": "0.0.0.0",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
|
||||
@@ -9,12 +9,14 @@ Set the UDP port in `config/runtime-host.json`:
|
||||
```json
|
||||
{
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18
|
||||
}
|
||||
```
|
||||
|
||||
Set `oscPort` to `0` to disable the OSC listener.
|
||||
Set `oscBindAddress` to `127.0.0.1` to keep OSC local to the host, or `0.0.0.0` to listen on all IPv4 interfaces.
|
||||
Set `oscSmoothing` to a value from `0.0` to `1.0` to add a subtle per-frame easing amount for numeric OSC controls. `0.0` disables smoothing, and larger values respond more quickly.
|
||||
|
||||
## Address Pattern
|
||||
|
||||
@@ -63,6 +65,8 @@ The listener accepts these OSC argument types:
|
||||
|
||||
Single-argument messages become scalar JSON values. Multi-argument messages become JSON arrays, which lets OSC drive `vec2` and `color` parameters.
|
||||
|
||||
OSC updates are coalesced by target route and applied once per render tick, so rapid controller motion does not force one runtime mutation, disk write, and UI push per incoming UDP packet. Numeric OSC controls can also be slightly smoothed with `oscSmoothing`.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
@@ -74,6 +78,8 @@ Examples:
|
||||
|
||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
||||
|
||||
OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. Stack edits made through the UI and preset operations still persist as before. Smoothing only applies to numeric controls such as floats, `vec2`, and `color`; booleans, enums, text, and triggers stay immediate.
|
||||
|
||||
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||
|
||||
```text
|
||||
|
||||
Reference in New Issue
Block a user