Files
video-shader-toys/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp
2026-05-02 16:40:21 +10:00

1285 lines
40 KiB
C++

#include "stdafx.h"
#include "RuntimeHost.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <regex>
#include <set>
#include <sstream>
namespace
{
std::string Trim(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
--end;
return text.substr(start, end - start);
}
std::string ReplaceAll(std::string text, const std::string& from, const std::string& to)
{
std::size_t offset = 0;
while ((offset = text.find(from, offset)) != std::string::npos)
{
text.replace(offset, from.length(), to);
offset += to.length();
}
return text;
}
bool IsFiniteNumber(double value)
{
return std::isfinite(value) != 0;
}
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
std::filesystem::path FindRepoRootCandidate()
{
std::vector<std::filesystem::path> 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 (std::filesystem::exists(candidate / "CMakeLists.txt") &&
std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing"))
{
return candidate;
}
candidate = candidate.parent_path();
}
}
return std::filesystem::path();
}
std::string ShaderParameterTypeToString(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "float";
case ShaderParameterType::Vec2: return "vec2";
case ShaderParameterType::Color: return "color";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "enum";
}
return "unknown";
}
std::string SlangTypeForParameter(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "uniform float";
case ShaderParameterType::Vec2: return "uniform float2";
case ShaderParameterType::Color: return "uniform float4";
case ShaderParameterType::Boolean: return "uniform bool";
case ShaderParameterType::Enum: return "uniform int";
}
return "uniform float";
}
std::string GlslTypeForUniformDeclaration(const std::string& declaration)
{
return Trim(declaration);
}
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
{
if (typeName == "float")
{
type = ShaderParameterType::Float;
return true;
}
if (typeName == "vec2")
{
type = ShaderParameterType::Vec2;
return true;
}
if (typeName == "color")
{
type = ShaderParameterType::Color;
return true;
}
if (typeName == "bool")
{
type = ShaderParameterType::Boolean;
return true;
}
if (typeName == "enum")
{
type = ShaderParameterType::Enum;
return true;
}
return false;
}
}
RuntimeHost::RuntimeHost()
: mReloadRequested(false),
mCompileSucceeded(false),
mHasSignal(false),
mSignalWidth(0),
mSignalHeight(0),
mServerPort(8080),
mAutoReloadEnabled(true),
mMixAmount(1.0),
mBypass(false),
mStartTime(std::chrono::steady_clock::now()),
mLastScanTime(std::chrono::steady_clock::time_point::min()),
mFrameCounter(0)
{
}
bool RuntimeHost::Initialize(std::string& error)
{
try
{
std::lock_guard<std::mutex> lock(mMutex);
if (!ResolvePaths(error))
return false;
if (!LoadConfig(error))
return false;
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
if (!LoadPersistentState(error))
return false;
if (!ScanShaderPackages(error))
return false;
if (mActiveShaderId.empty() && !mPackageOrder.empty())
mActiveShaderId = mPackageOrder.front();
mServerPort = mConfig.serverPort;
mAutoReloadEnabled = mConfig.autoReload;
mReloadRequested = true;
mCompileMessage = "Waiting for shader compile.";
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeHost::Initialize exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeHost::Initialize threw a non-standard exception.";
return false;
}
}
bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error)
{
try
{
std::lock_guard<std::mutex> lock(mMutex);
registryChanged = false;
reloadRequested = false;
if (!mAutoReloadEnabled)
{
reloadRequested = mReloadRequested;
return true;
}
const auto now = std::chrono::steady_clock::now();
if (mLastScanTime != std::chrono::steady_clock::time_point::min() &&
std::chrono::duration_cast<std::chrono::milliseconds>(now - mLastScanTime).count() < 250)
{
reloadRequested = mReloadRequested;
return true;
}
mLastScanTime = now;
std::string scanError;
std::map<std::string, ShaderPackage> previousPackages = mPackagesById;
std::vector<std::string> previousOrder = mPackageOrder;
const std::string previousActive = mActiveShaderId;
if (!ScanShaderPackages(scanError))
{
error = scanError;
return false;
}
registryChanged = previousOrder != mPackageOrder;
if (!registryChanged && previousPackages.size() == mPackagesById.size())
{
for (const auto& item : mPackagesById)
{
auto previous = previousPackages.find(item.first);
if (previous == previousPackages.end())
{
registryChanged = true;
break;
}
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
previous->second.manifestWriteTime != item.second.manifestWriteTime)
{
registryChanged = true;
break;
}
}
}
auto previousActiveIt = previousPackages.find(previousActive);
auto activeIt = mPackagesById.find(mActiveShaderId);
if (previousActiveIt != previousPackages.end() && activeIt != mPackagesById.end())
{
if (previousActiveIt->second.shaderWriteTime != activeIt->second.shaderWriteTime ||
previousActiveIt->second.manifestWriteTime != activeIt->second.manifestWriteTime)
{
mReloadRequested = true;
}
}
if (previousActive != mActiveShaderId)
mReloadRequested = true;
reloadRequested = mReloadRequested;
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeHost::PollFileChanges exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeHost::PollFileChanges threw a non-standard exception.";
return false;
}
}
bool RuntimeHost::ManualReloadRequested()
{
std::lock_guard<std::mutex> lock(mMutex);
return mReloadRequested;
}
void RuntimeHost::ClearReloadRequest()
{
std::lock_guard<std::mutex> lock(mMutex);
mReloadRequested = false;
}
bool RuntimeHost::SelectShader(const std::string& shaderId, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mPackagesById.find(shaderId) == mPackagesById.end())
{
error = "Unknown shader id: " + shaderId;
return false;
}
mActiveShaderId = shaderId;
mPersistentState.activeShaderId = shaderId;
mReloadRequested = true;
return SavePersistentState(error);
}
bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Unknown shader id: " + shaderId;
return false;
}
const ShaderPackage& shaderPackage = shaderIt->second;
auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(),
[&parameterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
if (parameterIt == shaderPackage.parameters.end())
{
error = "Unknown parameter id: " + parameterId;
return false;
}
ShaderParameterValue normalized;
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
return false;
mPersistentState.parameterValuesByShader[shaderId][parameterId] = normalized;
if (shaderId == mActiveShaderId)
mReloadRequested = true;
return SavePersistentState(error);
}
bool RuntimeHost::SetBypass(bool bypassEnabled, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
mBypass = bypassEnabled;
mPersistentState.bypass = bypassEnabled;
return SavePersistentState(error);
}
bool RuntimeHost::SetMixAmount(double mixAmount, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!IsFiniteNumber(mixAmount))
{
error = "Mix amount must be a finite number.";
return false;
}
mMixAmount = std::clamp(mixAmount, 0.0, 1.0);
mPersistentState.mixAmount = mMixAmount;
return SavePersistentState(error);
}
void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message)
{
std::lock_guard<std::mutex> lock(mMutex);
mCompileSucceeded = succeeded;
mCompileMessage = message;
}
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
std::lock_guard<std::mutex> lock(mMutex);
mHasSignal = hasSignal;
mSignalWidth = width;
mSignalHeight = height;
mSignalModeName = modeName;
}
void RuntimeHost::AdvanceFrame()
{
std::lock_guard<std::mutex> lock(mMutex);
++mFrameCounter;
}
bool RuntimeHost::BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error)
{
try
{
ShaderPackage shaderPackage;
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = mPackagesById.find(mActiveShaderId);
if (it == mPackagesById.end())
{
error = "No active shader is selected.";
return false;
}
shaderPackage = it->second;
}
const std::string wrapperSource = BuildWrapperSlangSource(shaderPackage);
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false;
if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error))
return false;
fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error);
if (fragmentShaderSource.empty())
return false;
if (!PatchGeneratedGlsl(fragmentShaderSource, error))
return false;
if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error))
return false;
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeHost::BuildActiveFragmentShaderSource exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeHost::BuildActiveFragmentShaderSource threw a non-standard exception.";
return false;
}
}
RuntimeRenderState RuntimeHost::GetRenderState(unsigned outputWidth, unsigned outputHeight) const
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeRenderState state;
state.activeShaderId = mActiveShaderId;
state.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
state.frameCount = static_cast<double>(mFrameCounter);
state.mixAmount = mMixAmount;
state.bypass = mBypass ? 1.0 : 0.0;
state.inputWidth = mSignalWidth;
state.inputHeight = mSignalHeight;
state.outputWidth = outputWidth;
state.outputHeight = outputHeight;
auto shaderIt = mPackagesById.find(mActiveShaderId);
if (shaderIt != mPackagesById.end())
{
state.parameterDefinitions = shaderIt->second.parameters;
auto persistedIt = mPersistentState.parameterValuesByShader.find(mActiveShaderId);
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
{
ShaderParameterValue value = DefaultValueForDefinition(definition);
if (persistedIt != mPersistentState.parameterValuesByShader.end())
{
auto valueIt = persistedIt->second.find(definition.id);
if (valueIt != persistedIt->second.end())
value = valueIt->second;
}
state.parameterValues[definition.id] = value;
}
}
return state;
}
std::string RuntimeHost::BuildStateJson() const
{
return SerializeJson(BuildStateValue(), true);
}
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* autoReloadValue = configJson.find("autoReload"))
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
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* activeShaderValue = root.find("activeShaderId"))
mPersistentState.activeShaderId = activeShaderValue->asString();
if (const JsonValue* mixAmountValue = root.find("mixAmount"))
mPersistentState.mixAmount = mixAmountValue->asNumber(1.0);
if (const JsonValue* bypassValue = root.find("bypass"))
mPersistentState.bypass = bypassValue->asBoolean(false);
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
{
for (const auto& shaderItem : valuesByShader->asObject())
{
std::map<std::string, ShaderParameterValue>& shaderValues = mPersistentState.parameterValuesByShader[shaderItem.first];
for (const auto& parameterItem : shaderItem.second.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);
}
shaderValues[parameterItem.first] = value;
}
}
}
mActiveShaderId = mPersistentState.activeShaderId;
mMixAmount = std::clamp(mPersistentState.mixAmount, 0.0, 1.0);
mBypass = mPersistentState.bypass;
return true;
}
bool RuntimeHost::SavePersistentState(std::string& error) const
{
JsonValue root = JsonValue::MakeObject();
root.set("activeShaderId", JsonValue(mActiveShaderId));
root.set("mixAmount", JsonValue(mMixAmount));
root.set("bypass", JsonValue(mBypass));
JsonValue valuesByShader = JsonValue::MakeObject();
for (const auto& shaderItem : mPersistentState.parameterValuesByShader)
{
JsonValue shaderValues = JsonValue::MakeObject();
auto packageIt = mPackagesById.find(shaderItem.first);
for (const auto& parameterItem : shaderItem.second)
{
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)
shaderValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
}
valuesByShader.set(shaderItem.first, shaderValues);
}
root.set("parameterValuesByShader", valuesByShader);
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
}
bool RuntimeHost::ScanShaderPackages(std::string& error)
{
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
if (!std::filesystem::exists(mShaderRoot))
{
error = "Shader library directory does not exist: " + mShaderRoot.string();
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(mShaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseShaderManifest(manifestPath, shaderPackage, error))
return false;
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
error = "Duplicate shader id found: " + shaderPackage.id;
return false;
}
EnsureParameterDefaultsLocked(shaderPackage);
packageOrder.push_back(shaderPackage.id);
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
mPackagesById.swap(packagesById);
mPackageOrder.swap(packageOrder);
if (!mActiveShaderId.empty() && mPackagesById.find(mActiveShaderId) == mPackagesById.end())
{
mActiveShaderId.clear();
mPersistentState.activeShaderId.clear();
}
if (mActiveShaderId.empty() && !mPackageOrder.empty())
{
mActiveShaderId = mPackageOrder.front();
mPersistentState.activeShaderId = mActiveShaderId;
}
return true;
}
bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
const JsonValue* idValue = manifestJson.find("id");
const JsonValue* nameValue = manifestJson.find("name");
if (!idValue || !nameValue)
{
error = "Shader manifest is missing required 'id' or 'name' field: " + manifestPath.string();
return false;
}
shaderPackage.id = idValue->asString();
shaderPackage.displayName = nameValue->asString();
shaderPackage.description = manifestJson.find("description") ? manifestJson.find("description")->asString() : "";
shaderPackage.category = manifestJson.find("category") ? manifestJson.find("category")->asString() : "";
shaderPackage.entryPoint = manifestJson.find("entryPoint") ? manifestJson.find("entryPoint")->asString() : "shadeVideo";
shaderPackage.directoryPath = manifestPath.parent_path();
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
shaderPackage.manifestPath = manifestPath;
if (!std::filesystem::exists(shaderPackage.shaderPath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
const JsonValue* parametersValue = manifestJson.find("parameters");
if (parametersValue && parametersValue->isArray())
{
for (const JsonValue& parameterJson : parametersValue->asArray())
{
const JsonValue* parameterIdValue = parameterJson.find("id");
const JsonValue* parameterLabelValue = parameterJson.find("label");
const JsonValue* parameterTypeValue = parameterJson.find("type");
if (!parameterIdValue || !parameterLabelValue || !parameterTypeValue)
{
error = "Shader parameter is missing required fields in: " + manifestPath.string();
return false;
}
ShaderParameterDefinition definition;
definition.id = parameterIdValue->asString();
definition.label = parameterLabelValue->asString();
if (!ParseShaderParameterType(parameterTypeValue->asString(), definition.type))
{
error = "Unsupported parameter type '" + parameterTypeValue->asString() + "' in: " + manifestPath.string();
return false;
}
if (const JsonValue* defaultValue = parameterJson.find("default"))
{
if (definition.type == ShaderParameterType::Boolean)
{
definition.defaultBoolean = defaultValue->asBoolean(false);
}
else if (definition.type == ShaderParameterType::Enum)
{
definition.defaultEnumValue = defaultValue->asString();
}
else if (defaultValue->isNumber())
{
definition.defaultNumbers.push_back(defaultValue->asNumber());
}
else if (defaultValue->isArray())
{
definition.defaultNumbers = JsonArrayToNumbers(*defaultValue);
}
}
if (const JsonValue* minValue = parameterJson.find("min"))
{
if (minValue->isNumber())
definition.minNumbers.push_back(minValue->asNumber());
else if (minValue->isArray())
definition.minNumbers = JsonArrayToNumbers(*minValue);
}
if (const JsonValue* maxValue = parameterJson.find("max"))
{
if (maxValue->isNumber())
definition.maxNumbers.push_back(maxValue->asNumber());
else if (maxValue->isArray())
definition.maxNumbers = JsonArrayToNumbers(*maxValue);
}
if (const JsonValue* stepValue = parameterJson.find("step"))
{
if (stepValue->isNumber())
definition.stepNumbers.push_back(stepValue->asNumber());
else if (stepValue->isArray())
definition.stepNumbers = JsonArrayToNumbers(*stepValue);
}
if (definition.type == ShaderParameterType::Enum)
{
const JsonValue* optionsValue = parameterJson.find("options");
if (!optionsValue || !optionsValue->isArray())
{
error = "Enum parameter is missing 'options' in: " + manifestPath.string();
return false;
}
for (const JsonValue& optionJson : optionsValue->asArray())
{
const JsonValue* value = optionJson.find("value");
const JsonValue* label = optionJson.find("label");
if (!value || !label)
{
error = "Enum parameter option is missing 'value' or 'label' in: " + manifestPath.string();
return false;
}
ShaderParameterOption option;
option.value = value->asString();
option.label = label->asString();
definition.enumOptions.push_back(option);
}
bool defaultFound = definition.defaultEnumValue.empty();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == definition.defaultEnumValue)
{
defaultFound = true;
break;
}
}
if (!defaultFound)
{
error = "Enum parameter default is not present in its option list for: " + definition.id;
return false;
}
}
shaderPackage.parameters.push_back(definition);
}
}
return true;
}
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
{
normalizedValue = DefaultValueForDefinition(definition);
switch (definition.type)
{
case ShaderParameterType::Float:
{
if (!value.isNumber())
{
error = "Expected numeric value for float parameter '" + definition.id + "'.";
return false;
}
double number = value.asNumber();
if (!IsFiniteNumber(number))
{
error = "Float parameter '" + definition.id + "' must be finite.";
return false;
}
if (!definition.minNumbers.empty())
number = std::max(number, definition.minNumbers.front());
if (!definition.maxNumbers.empty())
number = std::min(number, definition.maxNumbers.front());
normalizedValue.numberValues = { number };
return true;
}
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
std::vector<double> numbers = JsonArrayToNumbers(value);
const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4;
if (numbers.size() != expectedSize)
{
error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'.";
return false;
}
for (std::size_t index = 0; index < numbers.size(); ++index)
{
if (!IsFiniteNumber(numbers[index]))
{
error = "Parameter '" + definition.id + "' contains a non-finite value.";
return false;
}
if (index < definition.minNumbers.size())
numbers[index] = std::max(numbers[index], definition.minNumbers[index]);
if (index < definition.maxNumbers.size())
numbers[index] = std::min(numbers[index], definition.maxNumbers[index]);
}
normalizedValue.numberValues = numbers;
return true;
}
case ShaderParameterType::Boolean:
if (!value.isBoolean())
{
error = "Expected boolean value for parameter '" + definition.id + "'.";
return false;
}
normalizedValue.booleanValue = value.asBoolean();
return true;
case ShaderParameterType::Enum:
{
if (!value.isString())
{
error = "Expected string value for enum parameter '" + definition.id + "'.";
return false;
}
const std::string selectedValue = value.asString();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == selectedValue)
{
normalizedValue.enumValue = selectedValue;
return true;
}
}
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
return false;
}
}
return false;
}
ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const
{
ShaderParameterValue value;
switch (definition.type)
{
case ShaderParameterType::Float:
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
break;
case ShaderParameterType::Vec2:
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
break;
case ShaderParameterType::Color:
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
break;
case ShaderParameterType::Boolean:
value.booleanValue = definition.defaultBoolean;
break;
case ShaderParameterType::Enum:
value.enumValue = definition.defaultEnumValue;
break;
}
return value;
}
void RuntimeHost::EnsureParameterDefaultsLocked(ShaderPackage& shaderPackage)
{
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
{
auto& shaderValues = mPersistentState.parameterValuesByShader[shaderPackage.id];
if (shaderValues.find(definition.id) == shaderValues.end())
shaderValues[definition.id] = DefaultValueForDefinition(definition);
}
}
std::string RuntimeHost::BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const
{
std::ostringstream source;
source << "struct FragmentInput\n";
source << "{\n";
source << "\tfloat4 position : SV_Position;\n";
source << "\tfloat2 texCoord : TEXCOORD0;\n";
source << "};\n\n";
source << "struct ShaderContext\n";
source << "{\n";
source << "\tfloat2 uv;\n";
source << "\tfloat4 sourceColor;\n";
source << "\tfloat2 inputResolution;\n";
source << "\tfloat2 outputResolution;\n";
source << "\tfloat time;\n";
source << "\tfloat frameCount;\n";
source << "\tfloat mixAmount;\n";
source << "\tfloat bypass;\n";
source << "};\n\n";
source << "cbuffer GlobalParams\n";
source << "{\n";
source << "\tfloat gTime;\n";
source << "\tfloat2 gInputResolution;\n";
source << "\tfloat2 gOutputResolution;\n";
source << "\tfloat gFrameCount;\n";
source << "\tfloat gMixAmount;\n";
source << "\tfloat gBypass;\n";
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
source << "\t" << SlangTypeForParameter(definition.type).substr(strlen("uniform ")) << " " << definition.id << ";\n";
source << "};\n\n";
source << "Sampler2D<float4> gVideoInput;\n\n";
source << "float4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n";
source << "{\n";
source << "\tY = (Y * 256.0 - 16.0) / 219.0;\n";
source << "\tCb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n";
source << "\tCr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n";
source << "\treturn float4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n";
source << "}\n\n";
source << "float4 bilinear(float4 W, float4 X, float4 Y, float4 Z, float2 weight)\n";
source << "{\n";
source << "\tfloat4 m0 = lerp(W, Z, weight.x);\n";
source << "\tfloat4 m1 = lerp(X, Y, weight.x);\n";
source << "\treturn lerp(m0, m1, weight.y);\n";
source << "}\n\n";
source << "void textureGatherYUV(Sampler2D<float4> textureSampler, float2 tc, out float4 W, out float4 X, out float4 Y, out float4 Z)\n";
source << "{\n";
source << "\tuint width = 0;\n";
source << "\tuint height = 0;\n";
source << "\ttextureSampler.GetDimensions(width, height);\n";
source << "\tint2 tx = int2(tc * float2(width, height));\n";
source << "\tint2 tmin = int2(0, 0);\n";
source << "\tint2 tmax = int2(int(width), int(height)) - int2(1, 1);\n";
source << "\tW = textureSampler.Load(int3(tx, 0));\n";
source << "\tX = textureSampler.Load(int3(clamp(tx + int2(0, 1), tmin, tmax), 0));\n";
source << "\tY = textureSampler.Load(int3(clamp(tx + int2(1, 1), tmin, tmax), 0));\n";
source << "\tZ = textureSampler.Load(int3(clamp(tx + int2(1, 0), tmin, tmax), 0));\n";
source << "}\n\n";
source << "float4 sampleVideo(float2 tc)\n";
source << "{\n";
source << "\tfloat4 macro, macroU, macroR, macroUR;\n";
source << "\ttextureGatherYUV(gVideoInput, tc, macro, macroU, macroUR, macroR);\n";
source << "\tuint width = 0;\n";
source << "\tuint height = 0;\n";
source << "\tgVideoInput.GetDimensions(width, height);\n";
source << "\tfloat2 off = frac(tc * float2(width, height));\n";
source << "\tfloat4 pixel, pixelR, pixelU, pixelUR;\n";
source << "\tif (off.x > 0.5)\n";
source << "\t{\n";
source << "\t\tpixel = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n";
source << "\t\tpixelR = rec709YCbCr2rgba(macroR.g, macroR.b, macroR.r, 1.0);\n";
source << "\t\tpixelU = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n";
source << "\t\tpixelUR = rec709YCbCr2rgba(macroUR.g, macroUR.b, macroUR.r, 1.0);\n";
source << "\t}\n";
source << "\telse\n";
source << "\t{\n";
source << "\t\tpixel = rec709YCbCr2rgba(macro.g, macro.b, macro.r, 1.0);\n";
source << "\t\tpixelR = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n";
source << "\t\tpixelU = rec709YCbCr2rgba(macroU.g, macroU.b, macroU.r, 1.0);\n";
source << "\t\tpixelUR = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n";
source << "\t}\n";
source << "\treturn bilinear(pixel, pixelU, pixelUR, pixelR, off);\n";
source << "}\n\n";
source << "#include \"" << shaderPackage.shaderPath.generic_string() << "\"\n\n";
source << "[shader(\"fragment\")]\n";
source << "float4 fragmentMain(FragmentInput input) : SV_Target\n";
source << "{\n";
source << "\tShaderContext context;\n";
source << "\tfloat2 correctedUv = float2(input.texCoord.x, 1.0 - input.texCoord.y);\n";
source << "\tcontext.uv = correctedUv;\n";
source << "\tcontext.sourceColor = sampleVideo(correctedUv);\n";
source << "\tcontext.inputResolution = gInputResolution;\n";
source << "\tcontext.outputResolution = gOutputResolution;\n";
source << "\tcontext.time = gTime;\n";
source << "\tcontext.frameCount = gFrameCount;\n";
source << "\tcontext.mixAmount = gMixAmount;\n";
source << "\tcontext.bypass = gBypass;\n";
source << "\tfloat4 effectedColor = " << shaderPackage.entryPoint << "(context);\n";
source << "\tfloat mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);\n";
source << "\treturn lerp(context.sourceColor, effectedColor, mixValue);\n";
source << "}\n";
return source.str();
}
bool RuntimeHost::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyRoot))
{
error = "3rdParty directory was not found under the repository root.";
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
error = "Could not find slangc.exe under 3rdParty.";
return false;
}
bool RuntimeHost::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const
{
std::filesystem::path compilerPath;
if (!FindSlangCompiler(compilerPath, error))
return false;
std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string()
+ "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\"";
STARTUPINFOA startupInfo = {};
PROCESS_INFORMATION processInfo = {};
startupInfo.cb = sizeof(startupInfo);
std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
mutableCommandLine.push_back('\0');
if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo))
{
error = "Failed to launch slangc.exe.";
return false;
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processInfo.hProcess, &exitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
if (exitCode != 0)
{
error = "slangc.exe returned a non-zero exit code while compiling the active shader package.";
return false;
}
return true;
}
bool RuntimeHost::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const
{
if (shaderText.find("#version 450") == std::string::npos)
{
error = "Generated GLSL did not include the expected version header.";
return false;
}
shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core");
shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;");
shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord");
std::smatch match;
std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)");
if (std::regex_search(shaderText, match, outRegex))
{
const std::string outputName = match[1].str();
shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;");
shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor =");
}
return true;
}
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);
std::ofstream output(path, std::ios::binary);
if (!output)
{
error = "Could not write file: " + path.string();
return false;
}
output << contents;
return output.good();
}
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;
}
mUiRoot = mRepoRoot / "ui";
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
mRuntimeRoot = mRepoRoot / "runtime";
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);
return true;
}
JsonValue RuntimeHost::BuildStateValue() const
{
std::lock_guard<std::mutex> lock(mMutex);
JsonValue root = JsonValue::MakeObject();
JsonValue app = JsonValue::MakeObject();
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
app.set("autoReload", JsonValue(mAutoReloadEnabled));
root.set("app", app);
JsonValue runtime = JsonValue::MakeObject();
runtime.set("activeShaderId", JsonValue(mActiveShaderId));
runtime.set("compileSucceeded", JsonValue(mCompileSucceeded));
runtime.set("compileMessage", JsonValue(mCompileMessage));
runtime.set("mixAmount", JsonValue(mMixAmount));
runtime.set("bypass", JsonValue(mBypass));
root.set("runtime", runtime);
JsonValue video = JsonValue::MakeObject();
video.set("hasSignal", JsonValue(mHasSignal));
video.set("width", JsonValue(static_cast<double>(mSignalWidth)));
video.set("height", JsonValue(static_cast<double>(mSignalHeight)));
video.set("modeName", JsonValue(mSignalModeName));
root.set("video", video);
JsonValue shaders = JsonValue::MakeArray();
for (const std::string& shaderId : mPackageOrder)
{
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
continue;
const ShaderPackage& shaderPackage = shaderIt->second;
JsonValue shader = JsonValue::MakeObject();
shader.set("id", JsonValue(shaderPackage.id));
shader.set("name", JsonValue(shaderPackage.displayName));
shader.set("description", JsonValue(shaderPackage.description));
shader.set("category", JsonValue(shaderPackage.category));
JsonValue parameters = JsonValue::MakeArray();
auto persistedIt = mPersistentState.parameterValuesByShader.find(shaderPackage.id);
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
{
JsonValue parameter = JsonValue::MakeObject();
parameter.set("id", JsonValue(definition.id));
parameter.set("label", JsonValue(definition.label));
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
if (!definition.minNumbers.empty())
{
JsonValue minValue = JsonValue::MakeArray();
for (double number : definition.minNumbers)
minValue.pushBack(JsonValue(number));
parameter.set("min", minValue);
}
if (!definition.maxNumbers.empty())
{
JsonValue maxValue = JsonValue::MakeArray();
for (double number : definition.maxNumbers)
maxValue.pushBack(JsonValue(number));
parameter.set("max", maxValue);
}
if (!definition.stepNumbers.empty())
{
JsonValue stepValue = JsonValue::MakeArray();
for (double number : definition.stepNumbers)
stepValue.pushBack(JsonValue(number));
parameter.set("step", stepValue);
}
if (definition.type == ShaderParameterType::Enum)
{
JsonValue options = JsonValue::MakeArray();
for (const ShaderParameterOption& option : definition.enumOptions)
{
JsonValue optionValue = JsonValue::MakeObject();
optionValue.set("value", JsonValue(option.value));
optionValue.set("label", JsonValue(option.label));
options.pushBack(optionValue);
}
parameter.set("options", options);
}
ShaderParameterValue value = DefaultValueForDefinition(definition);
if (persistedIt != mPersistentState.parameterValuesByShader.end())
{
auto valueIt = persistedIt->second.find(definition.id);
if (valueIt != persistedIt->second.end())
value = valueIt->second;
}
parameter.set("value", SerializeParameterValue(definition, value));
parameters.pushBack(parameter);
}
shader.set("parameters", parameters);
shaders.pushBack(shader);
}
root.set("shaders", shaders);
return root;
}
JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const
{
switch (definition.type)
{
case ShaderParameterType::Boolean:
return JsonValue(value.booleanValue);
case ShaderParameterType::Enum:
return JsonValue(value.enumValue);
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();
}