more
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m37s
CI / Windows Release Package (push) Successful in 2m40s

This commit is contained in:
Aiden
2026-05-11 00:55:19 +10:00
parent 36b398ea95
commit 53e78890a8
12 changed files with 483 additions and 532 deletions

View File

@@ -675,39 +675,6 @@ RuntimeHost::RuntimeHost()
{
}
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
const HealthTelemetry::SignalStatusSnapshot previousStatus = mHealthTelemetry.GetSignalStatusSnapshot();
mHealthTelemetry.ReportSignalStatus(hasSignal, width, height, modeName);
if (previousStatus.hasSignal != hasSignal ||
previousStatus.width != width ||
previousStatus.height != height ||
previousStatus.modeName != modeName)
{
std::lock_guard<std::mutex> lock(mMutex);
MarkRenderStateDirtyLocked();
}
}
bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
const HealthTelemetry::SignalStatusSnapshot previousStatus = mHealthTelemetry.GetSignalStatusSnapshot();
if (!mHealthTelemetry.TryReportSignalStatus(hasSignal, width, height, modeName))
return false;
if (previousStatus.hasSignal != hasSignal ||
previousStatus.width != width ||
previousStatus.height != height ||
previousStatus.modeName != modeName)
{
std::lock_guard<std::mutex> lock(mMutex);
MarkRenderStateDirtyLocked();
}
return true;
}
void RuntimeHost::MarkRenderStateDirtyLocked()
{
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
@@ -719,315 +686,6 @@ void RuntimeHost::MarkParameterStateDirtyLocked()
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
{
SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable,
externalKeyingRequested, externalKeyingActive, statusMessage);
}
void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
{
mHealthTelemetry.ReportVideoIOStatus(
backendName,
modelName,
supportsInternalKeying,
supportsExternalKeying,
keyerInterfaceAvailable,
externalKeyingRequested,
externalKeyingActive,
statusMessage);
}
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
mHealthTelemetry.RecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
return mHealthTelemetry.TryRecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
void RuntimeHost::SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
mHealthTelemetry.RecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
return mHealthTelemetry.TryRecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
void RuntimeHost::SetServerPort(unsigned short port)
{
std::lock_guard<std::mutex> lock(mMutex);
mServerPort = port;
}
bool RuntimeHost::LoadConfig(std::string& error)
{
if (!std::filesystem::exists(mConfigPath))
return true;
std::string configText = ReadTextFile(mConfigPath, error);
if (configText.empty())
return false;
JsonValue configJson;
if (!ParseJson(configText, configJson, error))
return false;
if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary"))
mConfig.shaderLibrary = shaderLibraryValue->asString();
if (const JsonValue* serverPortValue = configJson.find("serverPort"))
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
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"))
{
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
}
if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
{
const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
}
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
{
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
{
mConfig.inputVideoFormat = videoFormatValue->asString();
mConfig.outputVideoFormat = videoFormatValue->asString();
}
}
if (const JsonValue* frameRateValue = configJson.find("frameRate"))
{
if (frameRateValue->isString() && !frameRateValue->asString().empty())
{
mConfig.inputFrameRate = frameRateValue->asString();
mConfig.outputFrameRate = frameRateValue->asString();
}
else if (frameRateValue->isNumber())
{
std::ostringstream stream;
stream << frameRateValue->asNumber();
mConfig.inputFrameRate = stream.str();
mConfig.outputFrameRate = stream.str();
}
}
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
{
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
mConfig.inputVideoFormat = inputVideoFormatValue->asString();
}
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
{
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
mConfig.inputFrameRate = inputFrameRateValue->asString();
else if (inputFrameRateValue->isNumber())
{
std::ostringstream stream;
stream << inputFrameRateValue->asNumber();
mConfig.inputFrameRate = stream.str();
}
}
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
{
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
mConfig.outputVideoFormat = outputVideoFormatValue->asString();
}
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
{
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
mConfig.outputFrameRate = outputFrameRateValue->asString();
else if (outputFrameRateValue->isNumber())
{
std::ostringstream stream;
stream << outputFrameRateValue->asNumber();
mConfig.outputFrameRate = stream.str();
}
}
mAutoReloadEnabled = mConfig.autoReload;
return true;
}
bool RuntimeHost::LoadPersistentState(std::string& error)
{
if (!std::filesystem::exists(mRuntimeStatePath))
return true;
std::string stateText = ReadTextFile(mRuntimeStatePath, error);
if (stateText.empty())
return false;
JsonValue root;
if (!ParseJson(stateText, root, error))
return false;
if (const JsonValue* layersValue = root.find("layers"))
{
for (const JsonValue& layerValue : layersValue->asArray())
{
if (!layerValue.isObject())
continue;
LayerPersistentState layer;
if (const JsonValue* idValue = layerValue.find("id"))
layer.id = idValue->asString();
if (const JsonValue* shaderIdValue = layerValue.find("shaderId"))
layer.shaderId = shaderIdValue->asString();
if (const JsonValue* bypassValue = layerValue.find("bypass"))
layer.bypass = bypassValue->asBoolean(false);
else if (const JsonValue* enabledValue = layerValue.find("enabled"))
layer.bypass = !enabledValue->asBoolean(true);
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
{
for (const auto& parameterItem : parameterValues->asObject())
{
ShaderParameterValue value;
const JsonValue& jsonValue = parameterItem.second;
if (jsonValue.isBoolean())
value.booleanValue = jsonValue.asBoolean();
else if (jsonValue.isString())
value.enumValue = jsonValue.asString();
else if (jsonValue.isNumber())
value.numberValues.push_back(jsonValue.asNumber());
else if (jsonValue.isArray())
value.numberValues = JsonArrayToNumbers(jsonValue);
layer.parameterValues[parameterItem.first] = value;
}
}
if (!layer.shaderId.empty())
mPersistentState.layers.push_back(layer);
}
}
else
{
// Migrate from the older single-shader state shape.
std::string activeShaderId;
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
activeShaderId = activeShaderValue->asString();
if (!activeShaderId.empty())
{
LayerPersistentState layer;
layer.id = GenerateLayerId();
layer.shaderId = activeShaderId;
layer.bypass = false;
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
{
const JsonValue* shaderValues = valuesByShader->find(activeShaderId);
if (shaderValues)
{
for (const auto& parameterItem : shaderValues->asObject())
{
ShaderParameterValue value;
const JsonValue& jsonValue = parameterItem.second;
if (jsonValue.isBoolean())
value.booleanValue = jsonValue.asBoolean();
else if (jsonValue.isString())
value.enumValue = jsonValue.asString();
else if (jsonValue.isNumber())
value.numberValues.push_back(jsonValue.asNumber());
else if (jsonValue.isArray())
value.numberValues = JsonArrayToNumbers(jsonValue);
layer.parameterValues[parameterItem.first] = value;
}
}
}
mPersistentState.layers.push_back(layer);
}
}
return true;
}
bool RuntimeHost::SavePersistentState(std::string& error) const
{
JsonValue root = JsonValue::MakeObject();
JsonValue layers = JsonValue::MakeArray();
for (const LayerPersistentState& layer : mPersistentState.layers)
{
JsonValue layerValue = JsonValue::MakeObject();
layerValue.set("id", JsonValue(layer.id));
layerValue.set("shaderId", JsonValue(layer.shaderId));
layerValue.set("bypass", JsonValue(layer.bypass));
JsonValue parameterValues = JsonValue::MakeObject();
auto packageIt = mPackagesById.find(layer.shaderId);
for (const auto& parameterItem : layer.parameterValues)
{
const ShaderParameterDefinition* definition = nullptr;
if (packageIt != mPackagesById.end())
{
for (const ShaderParameterDefinition& candidate : packageIt->second.parameters)
{
if (candidate.id == parameterItem.first)
{
definition = &candidate;
break;
}
}
}
if (definition)
parameterValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
}
layerValue.set("parameterValues", parameterValues);
layers.pushBack(layerValue);
}
root.set("layers", layers);
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
}
bool RuntimeHost::ScanShaderPackages(std::string& error)
{
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
std::vector<ShaderPackageStatus> packageStatuses;
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
return false;
mPackagesById.swap(packagesById);
mPackageOrder.swap(packageOrder);
mPackageStatuses.swap(packageStatuses);
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
{
if (mPackagesById.find(it->shaderId) == mPackagesById.end())
it = mPersistentState.layers.erase(it);
else
++it;
}
MarkRenderStateDirtyLocked();
return true;
}
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
{
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
@@ -1107,79 +765,6 @@ void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, co
}
}
std::string RuntimeHost::ReadTextFile(const std::filesystem::path& path, std::string& error) const
{
std::ifstream input(path, std::ios::binary);
if (!input)
{
error = "Could not open file: " + path.string();
return std::string();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
bool RuntimeHost::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const
{
std::error_code fsError;
std::filesystem::create_directories(path.parent_path(), fsError);
const std::filesystem::path temporaryPath = path.string() + ".tmp";
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
if (!output)
{
error = "Could not write file: " + temporaryPath.string();
return false;
}
output << contents;
output.close();
if (!output.good())
{
error = "Could not finish writing file: " + temporaryPath.string();
return false;
}
if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
{
const DWORD lastError = GetLastError();
std::filesystem::remove(temporaryPath, fsError);
error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")";
return false;
}
return true;
}
bool RuntimeHost::ResolvePaths(std::string& error)
{
mRepoRoot = FindRepoRootCandidate();
if (mRepoRoot.empty())
{
error = "Could not locate the repository root from the current runtime path.";
return false;
}
const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
mDocsRoot = mRepoRoot / "docs";
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
mRuntimeRoot = mRepoRoot / "runtime";
mPresetRoot = mRuntimeRoot / "stack_presets";
mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag";
std::error_code fsError;
std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
std::filesystem::create_directories(mPresetRoot, fsError);
return true;
}
JsonValue RuntimeHost::SerializeLayerStackLocked() const
{
JsonValue layers = JsonValue::MakeArray();
@@ -1366,31 +951,6 @@ void RuntimeHost::NormalizePersistentLayerIdsLocked()
mNextLayerId = maxLayerNumber;
}
std::vector<std::string> RuntimeHost::GetStackPresetNamesLocked() const
{
std::vector<std::string> presetNames;
std::error_code fsError;
if (!std::filesystem::exists(mPresetRoot, fsError))
return presetNames;
for (const auto& entry : std::filesystem::directory_iterator(mPresetRoot, fsError))
{
if (!entry.is_regular_file())
continue;
if (ToLowerCopy(entry.path().extension().string()) != ".json")
continue;
presetNames.push_back(entry.path().stem().string());
}
std::sort(presetNames.begin(), presetNames.end());
return presetNames;
}
std::string RuntimeHost::MakeSafePresetFileStem(const std::string& presetName) const
{
return ::MakeSafePresetFileStem(presetName);
}
JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const
{
switch (definition.type)