diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp index 1cd2ff2..5f52656 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp @@ -3,7 +3,6 @@ #include "ControlServer.h" #include "OscServer.h" #include "RuntimeControlBridge.h" -#include "RuntimeHost.h" #include "RuntimeStore.h" #include @@ -22,11 +21,11 @@ ControlServices::~ControlServices() Stop(); } -bool ControlServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error) +bool ControlServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error) { Stop(); - if (!StartControlServicesBoundary(composite, runtimeHost, *this, *mControlServer, *mOscServer, error)) + if (!StartControlServicesBoundary(composite, runtimeStore, *this, *mControlServer, *mOscServer, error)) { Stop(); return false; diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h index 348cb71..d86e6b6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h @@ -14,7 +14,6 @@ class ControlServer; class OpenGLComposite; class OscServer; -class RuntimeHost; class RuntimeStore; struct RuntimePollEvents @@ -45,7 +44,7 @@ public: ControlServices(); ~ControlServices(); - bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error); + bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error); void BeginPolling(RuntimeStore& runtimeStore); void Stop(); void BroadcastState(); diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp index 3b2a0a1..6530d16 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.cpp @@ -4,11 +4,11 @@ #include "ControlServer.h" #include "OpenGLComposite.h" #include "OscServer.h" -#include "RuntimeHost.h" +#include "RuntimeStore.h" bool StartControlServicesBoundary( OpenGLComposite& composite, - RuntimeHost& runtimeHost, + RuntimeStore& runtimeStore, ControlServices& controlServices, ControlServer& controlServer, OscServer& oscServer, @@ -38,15 +38,16 @@ bool StartControlServicesBoundary( return true; }; - if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error)) + if (!controlServer.Start(runtimeStore.GetRuntimeUiRoot(), runtimeStore.GetRuntimeDocsRoot(), runtimeStore.GetConfiguredControlServerPort(), callbacks, error)) return false; - runtimeHost.SetServerPort(controlServer.GetPort()); + runtimeStore.SetBoundControlServerPort(controlServer.GetPort()); OscServer::Callbacks oscCallbacks; oscCallbacks.updateParameter = [&controlServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) { return controlServices.QueueOscUpdate(layerKey, parameterKey, valueJson, actionError); }; - if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error)) + if (runtimeStore.GetConfiguredOscPort() > 0 && + !oscServer.Start(runtimeStore.GetConfiguredOscBindAddress(), runtimeStore.GetConfiguredOscPort(), oscCallbacks, error)) return false; return true; diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.h b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.h index 0001065..8fb1b0f 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeControlBridge.h @@ -6,11 +6,11 @@ class ControlServer; class ControlServices; class OpenGLComposite; class OscServer; -class RuntimeHost; +class RuntimeStore; bool StartControlServicesBoundary( OpenGLComposite& composite, - RuntimeHost& runtimeHost, + RuntimeStore& runtimeStore, ControlServices& controlServices, ControlServer& controlServer, OscServer& oscServer, diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp index ecd41d1..58cc019 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp @@ -12,9 +12,9 @@ RuntimeServices::~RuntimeServices() Stop(); } -bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error) +bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error) { - return mControlServices && mControlServices->Start(composite, runtimeHost, error); + return mControlServices && mControlServices->Start(composite, runtimeStore, error); } void RuntimeServices::BeginPolling(RuntimeStore& runtimeStore) diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h index cefa79f..e0694b5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h @@ -5,7 +5,6 @@ #include #include class OpenGLComposite; -class RuntimeHost; class RuntimeStore; class RuntimeServices @@ -17,7 +16,7 @@ public: RuntimeServices(); ~RuntimeServices(); - bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error); + bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error); void BeginPolling(RuntimeStore& runtimeStore); void Stop(); void BroadcastState(); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index fb8457e..43b569c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -203,7 +203,7 @@ bool OpenGLComposite::InitOpenGLState() return false; } - if (!mRuntimeServices->Start(*this, *mRuntimeHost, runtimeError)) + if (!mRuntimeServices->Start(*this, *mRuntimeStore, runtimeError)) { MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK); return false; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp index 7b9a64a..64df428 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp @@ -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 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 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 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(serverPortValue->asNumber(mConfig.serverPort)); - if (const JsonValue* oscPortValue = configJson.find("oscPort")) - mConfig.oscPort = static_cast(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(mConfig.maxTemporalHistoryFrames)); - mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); - } - if (const JsonValue* previewFpsValue = configJson.find("previewFps")) - { - const double configuredValue = previewFpsValue->asNumber(static_cast(mConfig.previewFps)); - mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast(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 packagesById; - std::vector packageOrder; - std::vector 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 RuntimeHost::GetStackPresetNamesLocked() const -{ - std::vector 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) diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h index 7aa5dc8..ef9a9c5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h @@ -20,37 +20,8 @@ class RuntimeHost { public: RuntimeHost(); - void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); - bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); - void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, - bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); - void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, - bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); - void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); - bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); - void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, - double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount); - bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, - double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount); HealthTelemetry& GetHealthTelemetry() { return mHealthTelemetry; } const HealthTelemetry& GetHealthTelemetry() const { return mHealthTelemetry; } - - const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; } - const std::filesystem::path& GetUiRoot() const { return mUiRoot; } - const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; } - const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; } - 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; } - const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; } - const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; } - const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; } - const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; } - void SetServerPort(unsigned short port); bool AutoReloadEnabled() const { return mAutoReloadEnabled; } private: @@ -84,21 +55,12 @@ private: std::vector layers; }; - bool LoadConfig(std::string& error); - bool LoadPersistentState(std::string& error); - bool SavePersistentState(std::string& error) const; - bool ScanShaderPackages(std::string& error); bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const; ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const; void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const; - std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; - bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const; - bool ResolvePaths(std::string& error); JsonValue SerializeLayerStackLocked() const; bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector& layers, std::string& error); void NormalizePersistentLayerIdsLocked(); - std::vector GetStackPresetNamesLocked() const; - std::string MakeSafePresetFileStem(const std::string& presetName) const; JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const; std::string TemporalHistorySourceToString(TemporalHistorySource source) const; LayerPersistentState* FindLayerById(const std::string& layerId); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp index 81b68b5..5e47fd6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp @@ -69,7 +69,7 @@ bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::str unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const { - return mRuntimeHost.GetMaxTemporalHistoryFrames(); + return mRuntimeHost.mConfig.maxTemporalHistoryFrames; } RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp index 541f0b4..352f01a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp @@ -1,5 +1,13 @@ #include "RuntimeStore.h" +#include "ShaderPackageRegistry.h" +#include "RuntimeParameterUtils.h" + +#include +#include +#include +#include + namespace { std::string TrimCopy(const std::string& text) @@ -31,6 +39,59 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key) return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key); } +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(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + return text; +} + +bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) +{ + return std::filesystem::exists(candidate / "config" / "runtime-host.json") && + std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") && + std::filesystem::exists(candidate / "shaders"); +} + +bool LooksLikeRepoRoot(const std::filesystem::path& candidate) +{ + return std::filesystem::exists(candidate / "CMakeLists.txt") && + std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing"); +} + +std::filesystem::path FindRepoRootCandidate() +{ + std::vector rootsToTry; + + char currentDirectory[MAX_PATH] = {}; + if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0) + rootsToTry.push_back(std::filesystem::path(currentDirectory)); + + char modulePath[MAX_PATH] = {}; + DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); + if (moduleLength > 0 && moduleLength < MAX_PATH) + rootsToTry.push_back(std::filesystem::path(modulePath).parent_path()); + + for (const std::filesystem::path& startPath : rootsToTry) + { + std::filesystem::path candidate = startPath; + for (int depth = 0; depth < 10 && !candidate.empty(); ++depth) + { + if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate)) + return candidate; + + candidate = candidate.parent_path(); + } + } + + return std::filesystem::path(); +} + bool TextureAssetsEqual(const std::vector& left, const std::vector& right) { if (left.size() != right.size()) @@ -79,14 +140,14 @@ bool RuntimeStore::InitializeStore(std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); - if (!mRuntimeHost.ResolvePaths(error)) + if (!ResolvePaths(error)) return false; - if (!mRuntimeHost.LoadConfig(error)) + if (!LoadConfig(error)) return false; mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary; - if (!mRuntimeHost.LoadPersistentState(error)) + if (!LoadPersistentState(error)) return false; - if (!mRuntimeHost.ScanShaderPackages(error)) + if (!ScanShaderPackages(error)) return false; mRuntimeHost.NormalizePersistentLayerIdsLocked(); @@ -145,7 +206,7 @@ bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequ } const auto now = std::chrono::steady_clock::now(); - if (mRuntimeHost.mLastScanTime != std::chrono::steady_clock::time_point::min() && + if (mRuntimeHost.mLastScanTime != (std::chrono::steady_clock::time_point::min)() && std::chrono::duration_cast(now - mRuntimeHost.mLastScanTime).count() < 250) { reloadRequested = mRuntimeHost.mReloadRequested; @@ -165,7 +226,7 @@ bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequ previousLayerShaderTimes[layer.id] = std::make_pair(previous->second.shaderWriteTime, previous->second.manifestWriteTime); } - if (!mRuntimeHost.ScanShaderPackages(scanError)) + if (!ScanShaderPackages(scanError)) { error = scanError; return false; @@ -249,7 +310,7 @@ bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& e mRuntimeHost.mPersistentState.layers.push_back(layer); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error) @@ -266,7 +327,7 @@ bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& er mRuntimeHost.mPersistentState.layers.erase(it); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error) @@ -288,7 +349,7 @@ bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, st std::swap(mRuntimeHost.mPersistentState.layers[index], mRuntimeHost.mPersistentState.layers[newIndex]); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) @@ -317,7 +378,7 @@ bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_ mRuntimeHost.mPersistentState.layers.insert(mRuntimeHost.mPersistentState.layers.begin() + static_cast(targetIndex), movedLayer); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error) @@ -333,7 +394,7 @@ bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool by layer->bypass = bypassed; mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkParameterStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error) @@ -358,7 +419,7 @@ bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, con mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error) @@ -404,7 +465,7 @@ bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std layer->parameterValues[parameterId] = normalized; mRuntimeHost.MarkParameterStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error) @@ -438,7 +499,7 @@ bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerK matchedLayer->parameterValues[parameterIt->id] = normalized; mRuntimeHost.MarkParameterStateDirtyLocked(); - return !persistState || mRuntimeHost.SavePersistentState(error); + return !persistState || SavePersistentState(error); } bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error) @@ -462,13 +523,13 @@ bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, s layer->parameterValues.clear(); mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second); mRuntimeHost.MarkParameterStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const { std::lock_guard lock(mRuntimeHost.mMutex); - const std::string safeStem = mRuntimeHost.MakeSafePresetFileStem(presetName); + const std::string safeStem = MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; @@ -480,13 +541,13 @@ bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::s root.set("name", JsonValue(TrimCopy(presetName))); root.set("layers", mRuntimeHost.SerializeLayerStackLocked()); - return mRuntimeHost.WriteTextFile(mRuntimeHost.mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error); + return WriteTextFile(mRuntimeHost.mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error); } bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); - const std::string safeStem = mRuntimeHost.MakeSafePresetFileStem(presetName); + const std::string safeStem = MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; @@ -494,7 +555,7 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s } const std::filesystem::path presetPath = mRuntimeHost.mPresetRoot / (safeStem + ".json"); - std::string presetText = mRuntimeHost.ReadTextFile(presetPath, error); + std::string presetText = ReadTextFile(presetPath, error); if (presetText.empty()) return false; @@ -522,82 +583,88 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s mRuntimeHost.mPersistentState.layers = nextLayers; mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); - return mRuntimeHost.SavePersistentState(error); + return SavePersistentState(error); } const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const { - return mRuntimeHost.GetRepoRoot(); + return mRuntimeHost.mRepoRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const { - return mRuntimeHost.GetUiRoot(); + return mRuntimeHost.mUiRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const { - return mRuntimeHost.GetDocsRoot(); + return mRuntimeHost.mDocsRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const { - return mRuntimeHost.GetRuntimeRoot(); + return mRuntimeHost.mRuntimeRoot; } unsigned short RuntimeStore::GetConfiguredControlServerPort() const { - return mRuntimeHost.GetServerPort(); + return mRuntimeHost.mServerPort; } unsigned short RuntimeStore::GetConfiguredOscPort() const { - return mRuntimeHost.GetOscPort(); + return mRuntimeHost.mConfig.oscPort; } const std::string& RuntimeStore::GetConfiguredOscBindAddress() const { - return mRuntimeHost.GetOscBindAddress(); + return mRuntimeHost.mConfig.oscBindAddress; } double RuntimeStore::GetConfiguredOscSmoothing() const { - return mRuntimeHost.GetOscSmoothing(); + return mRuntimeHost.mConfig.oscSmoothing; } unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const { - return mRuntimeHost.GetMaxTemporalHistoryFrames(); + return mRuntimeHost.mConfig.maxTemporalHistoryFrames; } unsigned RuntimeStore::GetConfiguredPreviewFps() const { - return mRuntimeHost.GetPreviewFps(); + return mRuntimeHost.mConfig.previewFps; } bool RuntimeStore::IsExternalKeyingConfigured() const { - return mRuntimeHost.ExternalKeyingEnabled(); + return mRuntimeHost.mConfig.enableExternalKeying; } const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const { - return mRuntimeHost.GetInputVideoFormat(); + return mRuntimeHost.mConfig.inputVideoFormat; } const std::string& RuntimeStore::GetConfiguredInputFrameRate() const { - return mRuntimeHost.GetInputFrameRate(); + return mRuntimeHost.mConfig.inputFrameRate; } const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const { - return mRuntimeHost.GetOutputVideoFormat(); + return mRuntimeHost.mConfig.outputVideoFormat; } const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const { - return mRuntimeHost.GetOutputFrameRate(); + return mRuntimeHost.mConfig.outputFrameRate; +} + +void RuntimeStore::SetBoundControlServerPort(unsigned short port) +{ + std::lock_guard lock(mRuntimeHost.mMutex); + mRuntimeHost.mServerPort = port; } void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message) @@ -719,7 +786,7 @@ JsonValue RuntimeStore::BuildRuntimeStateValue() const root.set("shaders", shaderLibrary); JsonValue stackPresets = JsonValue::MakeArray(); - for (const std::string& presetName : mRuntimeHost.GetStackPresetNamesLocked()) + for (const std::string& presetName : GetStackPresetNamesLocked()) stackPresets.pushBack(JsonValue(presetName)); root.set("stackPresets", stackPresets); @@ -732,6 +799,360 @@ JsonValue RuntimeStore::SerializeLayerStack() const return mRuntimeHost.SerializeLayerStackLocked(); } +bool RuntimeStore::LoadConfig(std::string& error) +{ + if (!std::filesystem::exists(mRuntimeHost.mConfigPath)) + return true; + + std::string configText = ReadTextFile(mRuntimeHost.mConfigPath, error); + if (configText.empty()) + return false; + + JsonValue configJson; + if (!ParseJson(configText, configJson, error)) + return false; + + if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary")) + mRuntimeHost.mConfig.shaderLibrary = shaderLibraryValue->asString(); + if (const JsonValue* serverPortValue = configJson.find("serverPort")) + mRuntimeHost.mConfig.serverPort = static_cast(serverPortValue->asNumber(mRuntimeHost.mConfig.serverPort)); + if (const JsonValue* oscPortValue = configJson.find("oscPort")) + mRuntimeHost.mConfig.oscPort = static_cast(oscPortValue->asNumber(mRuntimeHost.mConfig.oscPort)); + if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress")) + mRuntimeHost.mConfig.oscBindAddress = oscBindAddressValue->asString(); + if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing")) + mRuntimeHost.mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mRuntimeHost.mConfig.oscSmoothing)); + if (const JsonValue* autoReloadValue = configJson.find("autoReload")) + mRuntimeHost.mConfig.autoReload = autoReloadValue->asBoolean(mRuntimeHost.mConfig.autoReload); + if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames")) + { + const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast(mRuntimeHost.mConfig.maxTemporalHistoryFrames)); + mRuntimeHost.mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); + } + if (const JsonValue* previewFpsValue = configJson.find("previewFps")) + { + const double configuredValue = previewFpsValue->asNumber(static_cast(mRuntimeHost.mConfig.previewFps)); + mRuntimeHost.mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); + } + if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) + mRuntimeHost.mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mRuntimeHost.mConfig.enableExternalKeying); + if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) + { + if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) + { + mRuntimeHost.mConfig.inputVideoFormat = videoFormatValue->asString(); + mRuntimeHost.mConfig.outputVideoFormat = videoFormatValue->asString(); + } + } + if (const JsonValue* frameRateValue = configJson.find("frameRate")) + { + if (frameRateValue->isString() && !frameRateValue->asString().empty()) + { + mRuntimeHost.mConfig.inputFrameRate = frameRateValue->asString(); + mRuntimeHost.mConfig.outputFrameRate = frameRateValue->asString(); + } + else if (frameRateValue->isNumber()) + { + std::ostringstream stream; + stream << frameRateValue->asNumber(); + mRuntimeHost.mConfig.inputFrameRate = stream.str(); + mRuntimeHost.mConfig.outputFrameRate = stream.str(); + } + } + if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat")) + { + if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty()) + mRuntimeHost.mConfig.inputVideoFormat = inputVideoFormatValue->asString(); + } + if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate")) + { + if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty()) + mRuntimeHost.mConfig.inputFrameRate = inputFrameRateValue->asString(); + else if (inputFrameRateValue->isNumber()) + { + std::ostringstream stream; + stream << inputFrameRateValue->asNumber(); + mRuntimeHost.mConfig.inputFrameRate = stream.str(); + } + } + if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat")) + { + if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty()) + mRuntimeHost.mConfig.outputVideoFormat = outputVideoFormatValue->asString(); + } + if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate")) + { + if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty()) + mRuntimeHost.mConfig.outputFrameRate = outputFrameRateValue->asString(); + else if (outputFrameRateValue->isNumber()) + { + std::ostringstream stream; + stream << outputFrameRateValue->asNumber(); + mRuntimeHost.mConfig.outputFrameRate = stream.str(); + } + } + + mRuntimeHost.mAutoReloadEnabled = mRuntimeHost.mConfig.autoReload; + return true; +} + +bool RuntimeStore::LoadPersistentState(std::string& error) +{ + if (!std::filesystem::exists(mRuntimeHost.mRuntimeStatePath)) + return true; + + std::string stateText = ReadTextFile(mRuntimeHost.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; + RuntimeHost::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()) + mRuntimeHost.mPersistentState.layers.push_back(layer); + } + } + else + { + std::string activeShaderId; + if (const JsonValue* activeShaderValue = root.find("activeShaderId")) + activeShaderId = activeShaderValue->asString(); + + if (!activeShaderId.empty()) + { + RuntimeHost::LayerPersistentState layer; + layer.id = mRuntimeHost.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; + } + } + } + + mRuntimeHost.mPersistentState.layers.push_back(layer); + } + } + + return true; +} + +bool RuntimeStore::SavePersistentState(std::string& error) const +{ + JsonValue root = JsonValue::MakeObject(); + + JsonValue layers = JsonValue::MakeArray(); + for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.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 = mRuntimeHost.mPackagesById.find(layer.shaderId); + for (const auto& parameterItem : layer.parameterValues) + { + const ShaderParameterDefinition* definition = nullptr; + if (packageIt != mRuntimeHost.mPackagesById.end()) + { + for (const ShaderParameterDefinition& candidate : packageIt->second.parameters) + { + if (candidate.id == parameterItem.first) + { + definition = &candidate; + break; + } + } + } + + if (definition) + parameterValues.set(parameterItem.first, mRuntimeHost.SerializeParameterValue(*definition, parameterItem.second)); + } + + layerValue.set("parameterValues", parameterValues); + layers.pushBack(layerValue); + } + root.set("layers", layers); + + return WriteTextFile(mRuntimeHost.mRuntimeStatePath, SerializeJson(root, true), error); +} + +bool RuntimeStore::ScanShaderPackages(std::string& error) +{ + std::map packagesById; + std::vector packageOrder; + std::vector packageStatuses; + ShaderPackageRegistry registry(mRuntimeHost.mConfig.maxTemporalHistoryFrames); + if (!registry.Scan(mRuntimeHost.mShaderRoot, packagesById, packageOrder, packageStatuses, error)) + return false; + + mRuntimeHost.mPackagesById.swap(packagesById); + mRuntimeHost.mPackageOrder.swap(packageOrder); + mRuntimeHost.mPackageStatuses.swap(packageStatuses); + + for (auto it = mRuntimeHost.mPersistentState.layers.begin(); it != mRuntimeHost.mPersistentState.layers.end();) + { + if (mRuntimeHost.mPackagesById.find(it->shaderId) == mRuntimeHost.mPackagesById.end()) + it = mRuntimeHost.mPersistentState.layers.erase(it); + else + ++it; + } + + mRuntimeHost.MarkRenderStateDirtyLocked(); + return true; +} + +std::string RuntimeStore::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 RuntimeStore::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 RuntimeStore::ResolvePaths(std::string& error) +{ + mRuntimeHost.mRepoRoot = FindRepoRootCandidate(); + if (mRuntimeHost.mRepoRoot.empty()) + { + error = "Could not locate the repository root from the current runtime path."; + return false; + } + + const std::filesystem::path builtUiRoot = mRuntimeHost.mRepoRoot / "ui" / "dist"; + mRuntimeHost.mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRuntimeHost.mRepoRoot / "ui"); + mRuntimeHost.mDocsRoot = mRuntimeHost.mRepoRoot / "docs"; + mRuntimeHost.mConfigPath = mRuntimeHost.mRepoRoot / "config" / "runtime-host.json"; + mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary; + mRuntimeHost.mRuntimeRoot = mRuntimeHost.mRepoRoot / "runtime"; + mRuntimeHost.mPresetRoot = mRuntimeHost.mRuntimeRoot / "stack_presets"; + mRuntimeHost.mRuntimeStatePath = mRuntimeHost.mRuntimeRoot / "runtime_state.json"; + mRuntimeHost.mWrapperPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang"; + mRuntimeHost.mGeneratedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.raw.frag"; + mRuntimeHost.mPatchedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.frag"; + + std::error_code fsError; + std::filesystem::create_directories(mRuntimeHost.mRuntimeRoot / "shader_cache", fsError); + std::filesystem::create_directories(mRuntimeHost.mPresetRoot, fsError); + return true; +} + +std::vector RuntimeStore::GetStackPresetNamesLocked() const +{ + std::vector presetNames; + std::error_code fsError; + if (!std::filesystem::exists(mRuntimeHost.mPresetRoot, fsError)) + return presetNames; + + for (const auto& entry : std::filesystem::directory_iterator(mRuntimeHost.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 RuntimeStore::MakeSafePresetFileStem(const std::string& presetName) const +{ + return ::MakeSafePresetFileStem(presetName); +} + bool RuntimeStore::TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey, RuntimeHost::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage, std::vector::const_iterator& parameterIt, std::string& error) const diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h index 6756a82..1bee6ae 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h @@ -42,11 +42,21 @@ public: const std::string& GetConfiguredInputFrameRate() const; const std::string& GetConfiguredOutputVideoFormat() const; const std::string& GetConfiguredOutputFrameRate() const; + void SetBoundControlServerPort(unsigned short port); void SetCompileStatus(bool succeeded, const std::string& message); void ClearReloadRequest(); private: + bool LoadConfig(std::string& error); + bool LoadPersistentState(std::string& error); + bool SavePersistentState(std::string& error) const; + bool ScanShaderPackages(std::string& error); + std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; + bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const; + bool ResolvePaths(std::string& error); + std::vector GetStackPresetNamesLocked() const; + std::string MakeSafePresetFileStem(const std::string& presetName) const; bool TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey, RuntimeHost::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage, std::vector::const_iterator& parameterIt, std::string& error) const;