1642 lines
50 KiB
C++
1642 lines
50 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::string ToLowerCopy(std::string text)
|
|
{
|
|
std::transform(text.begin(), text.end(), text.begin(),
|
|
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
|
return text;
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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),
|
|
mFrameBudgetMilliseconds(0.0),
|
|
mRenderMilliseconds(0.0),
|
|
mSmoothedRenderMilliseconds(0.0),
|
|
mServerPort(8080),
|
|
mAutoReloadEnabled(true),
|
|
mStartTime(std::chrono::steady_clock::now()),
|
|
mLastScanTime(std::chrono::steady_clock::time_point::min()),
|
|
mFrameCounter(0),
|
|
mNextLayerId(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;
|
|
|
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
|
if (shaderIt != mPackagesById.end())
|
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
}
|
|
|
|
if (mPersistentState.layers.empty() && !mPackageOrder.empty())
|
|
{
|
|
LayerPersistentState layer;
|
|
layer.id = GenerateLayerId();
|
|
layer.shaderId = mPackageOrder.front();
|
|
layer.bypass = false;
|
|
EnsureLayerDefaultsLocked(layer, mPackagesById[layer.shaderId]);
|
|
mPersistentState.layers.push_back(layer);
|
|
}
|
|
|
|
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;
|
|
std::map<std::string, std::pair<std::filesystem::file_time_type, std::filesystem::file_time_type>> previousLayerShaderTimes;
|
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto previous = previousPackages.find(layer.shaderId);
|
|
if (previous != previousPackages.end())
|
|
previousLayerShaderTimes[layer.id] = std::make_pair(previous->second.shaderWriteTime, previous->second.manifestWriteTime);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto active = mPackagesById.find(layer.shaderId);
|
|
auto previous = previousLayerShaderTimes.find(layer.id);
|
|
if (active == mPackagesById.end())
|
|
continue;
|
|
EnsureLayerDefaultsLocked(layer, active->second);
|
|
if (previous != previousLayerShaderTimes.end())
|
|
{
|
|
if (previous->second.first != active->second.shaderWriteTime ||
|
|
previous->second.second != active->second.manifestWriteTime)
|
|
{
|
|
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::AddLayer(const std::string& shaderId, 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;
|
|
}
|
|
|
|
LayerPersistentState layer;
|
|
layer.id = GenerateLayerId();
|
|
layer.shaderId = shaderId;
|
|
layer.bypass = false;
|
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
mPersistentState.layers.push_back(layer);
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
mPersistentState.layers.erase(it);
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it);
|
|
const std::ptrdiff_t newIndex = index + direction;
|
|
if (newIndex < 0 || newIndex >= static_cast<std::ptrdiff_t>(mPersistentState.layers.size()))
|
|
return true;
|
|
|
|
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
if (mPersistentState.layers.empty())
|
|
return true;
|
|
|
|
if (targetIndex >= mPersistentState.layers.size())
|
|
targetIndex = mPersistentState.layers.size() - 1;
|
|
|
|
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mPersistentState.layers.begin(), it));
|
|
if (sourceIndex == targetIndex)
|
|
return true;
|
|
|
|
LayerPersistentState movedLayer = *it;
|
|
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
|
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
LayerPersistentState* layer = FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
layer->bypass = bypassed;
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
LayerPersistentState* layer = FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto shaderIt = mPackagesById.find(shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + shaderId;
|
|
return false;
|
|
}
|
|
|
|
layer->shaderId = shaderId;
|
|
layer->parameterValues.clear();
|
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
|
mReloadRequested = true;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
|
|
LayerPersistentState* layer = FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto shaderIt = mPackagesById.find(layer->shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + layer->shaderId;
|
|
return false;
|
|
}
|
|
|
|
const ShaderPackage& shaderPackage = shaderIt->second;
|
|
auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(),
|
|
[¶meterId](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;
|
|
|
|
layer->parameterValues[parameterId] = normalized;
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
|
|
LayerPersistentState* layer = FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto shaderIt = mPackagesById.find(layer->shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + layer->shaderId;
|
|
return false;
|
|
}
|
|
|
|
layer->parameterValues.clear();
|
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeHost::SaveStackPreset(const std::string& presetName, std::string& error) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
const std::string safeStem = MakeSafePresetFileStem(presetName);
|
|
if (safeStem.empty())
|
|
{
|
|
error = "Preset name must include at least one letter or number.";
|
|
return false;
|
|
}
|
|
|
|
JsonValue root = JsonValue::MakeObject();
|
|
root.set("version", JsonValue(1.0));
|
|
root.set("name", JsonValue(Trim(presetName)));
|
|
root.set("layers", SerializeLayerStackLocked());
|
|
|
|
return WriteTextFile(mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error);
|
|
}
|
|
|
|
bool RuntimeHost::LoadStackPreset(const std::string& presetName, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
const std::string safeStem = MakeSafePresetFileStem(presetName);
|
|
if (safeStem.empty())
|
|
{
|
|
error = "Preset name must include at least one letter or number.";
|
|
return false;
|
|
}
|
|
|
|
const std::filesystem::path presetPath = mPresetRoot / (safeStem + ".json");
|
|
std::string presetText = ReadTextFile(presetPath, error);
|
|
if (presetText.empty())
|
|
return false;
|
|
|
|
JsonValue root;
|
|
if (!ParseJson(presetText, root, error))
|
|
return false;
|
|
|
|
const JsonValue* layersValue = root.find("layers");
|
|
if (!layersValue || !layersValue->isArray())
|
|
{
|
|
error = "Preset file is missing a valid 'layers' array.";
|
|
return false;
|
|
}
|
|
|
|
std::vector<LayerPersistentState> nextLayers;
|
|
if (!DeserializeLayerStackLocked(*layersValue, nextLayers, error))
|
|
return false;
|
|
|
|
if (nextLayers.empty())
|
|
{
|
|
error = "Preset does not contain any valid layers.";
|
|
return false;
|
|
}
|
|
|
|
mPersistentState.layers = nextLayers;
|
|
mReloadRequested = true;
|
|
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::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
|
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
|
if (mSmoothedRenderMilliseconds <= 0.0)
|
|
mSmoothedRenderMilliseconds = mRenderMilliseconds;
|
|
else
|
|
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
|
}
|
|
|
|
void RuntimeHost::AdvanceFrame()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
++mFrameCounter;
|
|
}
|
|
|
|
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
|
{
|
|
try
|
|
{
|
|
ShaderPackage shaderPackage;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
const LayerPersistentState* layer = FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto it = mPackagesById.find(layer->shaderId);
|
|
if (it == mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + layer->shaderId;
|
|
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::BuildLayerFragmentShaderSource exception: ") + exception.what();
|
|
return false;
|
|
}
|
|
catch (...)
|
|
{
|
|
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mMutex);
|
|
std::vector<RuntimeRenderState> states;
|
|
|
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
continue;
|
|
|
|
RuntimeRenderState state;
|
|
state.layerId = layer.id;
|
|
state.shaderId = layer.shaderId;
|
|
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 = 1.0;
|
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
|
state.inputWidth = mSignalWidth;
|
|
state.inputHeight = mSignalHeight;
|
|
state.outputWidth = outputWidth;
|
|
state.outputHeight = outputHeight;
|
|
state.parameterDefinitions = shaderIt->second.parameters;
|
|
|
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
|
{
|
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
|
auto valueIt = layer.parameterValues.find(definition.id);
|
|
if (valueIt != layer.parameterValues.end())
|
|
value = valueIt->second;
|
|
state.parameterValues[definition.id] = value;
|
|
}
|
|
|
|
states.push_back(state);
|
|
}
|
|
|
|
return states;
|
|
}
|
|
|
|
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* layersValue = root.find("layers"))
|
|
{
|
|
for (const JsonValue& layerValue : layersValue->asArray())
|
|
{
|
|
if (!layerValue.isObject())
|
|
continue;
|
|
LayerPersistentState layer;
|
|
if (const JsonValue* idValue = layerValue.find("id"))
|
|
layer.id = idValue->asString();
|
|
if (const JsonValue* shaderIdValue = layerValue.find("shaderId"))
|
|
layer.shaderId = shaderIdValue->asString();
|
|
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
|
layer.bypass = bypassValue->asBoolean(false);
|
|
else if (const JsonValue* enabledValue = layerValue.find("enabled"))
|
|
layer.bypass = !enabledValue->asBoolean(true);
|
|
|
|
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
|
|
{
|
|
for (const auto& parameterItem : parameterValues->asObject())
|
|
{
|
|
ShaderParameterValue value;
|
|
const JsonValue& jsonValue = parameterItem.second;
|
|
if (jsonValue.isBoolean())
|
|
value.booleanValue = jsonValue.asBoolean();
|
|
else if (jsonValue.isString())
|
|
value.enumValue = jsonValue.asString();
|
|
else if (jsonValue.isNumber())
|
|
value.numberValues.push_back(jsonValue.asNumber());
|
|
else if (jsonValue.isArray())
|
|
value.numberValues = JsonArrayToNumbers(jsonValue);
|
|
layer.parameterValues[parameterItem.first] = value;
|
|
}
|
|
}
|
|
|
|
if (!layer.shaderId.empty())
|
|
mPersistentState.layers.push_back(layer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Migrate from the older single-shader state shape.
|
|
std::string activeShaderId;
|
|
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
|
|
activeShaderId = activeShaderValue->asString();
|
|
|
|
if (!activeShaderId.empty())
|
|
{
|
|
LayerPersistentState layer;
|
|
layer.id = GenerateLayerId();
|
|
layer.shaderId = activeShaderId;
|
|
layer.bypass = false;
|
|
|
|
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
|
|
{
|
|
const JsonValue* shaderValues = valuesByShader->find(activeShaderId);
|
|
if (shaderValues)
|
|
{
|
|
for (const auto& parameterItem : shaderValues->asObject())
|
|
{
|
|
ShaderParameterValue value;
|
|
const JsonValue& jsonValue = parameterItem.second;
|
|
if (jsonValue.isBoolean())
|
|
value.booleanValue = jsonValue.asBoolean();
|
|
else if (jsonValue.isString())
|
|
value.enumValue = jsonValue.asString();
|
|
else if (jsonValue.isNumber())
|
|
value.numberValues.push_back(jsonValue.asNumber());
|
|
else if (jsonValue.isArray())
|
|
value.numberValues = JsonArrayToNumbers(jsonValue);
|
|
layer.parameterValues[parameterItem.first] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
mPersistentState.layers.push_back(layer);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool RuntimeHost::SavePersistentState(std::string& error) const
|
|
{
|
|
JsonValue root = JsonValue::MakeObject();
|
|
|
|
JsonValue layers = JsonValue::MakeArray();
|
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
JsonValue layerValue = JsonValue::MakeObject();
|
|
layerValue.set("id", JsonValue(layer.id));
|
|
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
|
layerValue.set("bypass", JsonValue(layer.bypass));
|
|
|
|
JsonValue parameterValues = JsonValue::MakeObject();
|
|
auto packageIt = mPackagesById.find(layer.shaderId);
|
|
for (const auto& parameterItem : layer.parameterValues)
|
|
{
|
|
const ShaderParameterDefinition* definition = nullptr;
|
|
if (packageIt != mPackagesById.end())
|
|
{
|
|
for (const ShaderParameterDefinition& candidate : packageIt->second.parameters)
|
|
{
|
|
if (candidate.id == parameterItem.first)
|
|
{
|
|
definition = &candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (definition)
|
|
parameterValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
|
|
}
|
|
|
|
layerValue.set("parameterValues", parameterValues);
|
|
layers.pushBack(layerValue);
|
|
}
|
|
root.set("layers", layers);
|
|
|
|
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
|
|
}
|
|
|
|
bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|
{
|
|
std::map<std::string, ShaderPackage> packagesById;
|
|
std::vector<std::string> packageOrder;
|
|
|
|
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;
|
|
}
|
|
|
|
packageOrder.push_back(shaderPackage.id);
|
|
packagesById[shaderPackage.id] = shaderPackage;
|
|
}
|
|
|
|
std::sort(packageOrder.begin(), packageOrder.end());
|
|
mPackagesById.swap(packagesById);
|
|
mPackageOrder.swap(packageOrder);
|
|
|
|
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
|
{
|
|
if (mPackagesById.find(it->shaderId) == mPackagesById.end())
|
|
it = mPersistentState.layers.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
|
|
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::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const
|
|
{
|
|
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
|
{
|
|
if (layerState.parameterValues.find(definition.id) == layerState.parameterValues.end())
|
|
layerState.parameterValues[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 sampleVideo(float2 tc)\n";
|
|
source << "{\n";
|
|
source << "\treturn gVideoInput.Sample(tc);\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 << "\tcontext.uv = input.texCoord;\n";
|
|
source << "\tcontext.sourceColor = sampleVideo(context.uv);\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;
|
|
}
|
|
|
|
const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
|
|
mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
|
|
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::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("layerCount", JsonValue(static_cast<double>(mPersistentState.layers.size())));
|
|
runtime.set("compileSucceeded", JsonValue(mCompileSucceeded));
|
|
runtime.set("compileMessage", JsonValue(mCompileMessage));
|
|
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 performance = JsonValue::MakeObject();
|
|
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
|
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
|
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
|
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
|
root.set("performance", performance);
|
|
|
|
JsonValue shaderLibrary = JsonValue::MakeArray();
|
|
for (const std::string& shaderId : mPackageOrder)
|
|
{
|
|
auto shaderIt = mPackagesById.find(shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
continue;
|
|
|
|
JsonValue shader = JsonValue::MakeObject();
|
|
shader.set("id", JsonValue(shaderIt->second.id));
|
|
shader.set("name", JsonValue(shaderIt->second.displayName));
|
|
shader.set("description", JsonValue(shaderIt->second.description));
|
|
shader.set("category", JsonValue(shaderIt->second.category));
|
|
shaderLibrary.pushBack(shader);
|
|
}
|
|
root.set("shaders", shaderLibrary);
|
|
|
|
JsonValue stackPresets = JsonValue::MakeArray();
|
|
for (const std::string& presetName : GetStackPresetNamesLocked())
|
|
stackPresets.pushBack(JsonValue(presetName));
|
|
root.set("stackPresets", stackPresets);
|
|
|
|
root.set("layers", SerializeLayerStackLocked());
|
|
|
|
return root;
|
|
}
|
|
|
|
JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|
{
|
|
JsonValue layers = JsonValue::MakeArray();
|
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
continue;
|
|
|
|
JsonValue layerValue = JsonValue::MakeObject();
|
|
layerValue.set("id", JsonValue(layer.id));
|
|
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
|
layerValue.set("shaderName", JsonValue(shaderIt->second.displayName));
|
|
layerValue.set("bypass", JsonValue(layer.bypass));
|
|
|
|
JsonValue parameters = JsonValue::MakeArray();
|
|
for (const ShaderParameterDefinition& definition : shaderIt->second.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);
|
|
auto valueIt = layer.parameterValues.find(definition.id);
|
|
if (valueIt != layer.parameterValues.end())
|
|
value = valueIt->second;
|
|
parameter.set("value", SerializeParameterValue(definition, value));
|
|
parameters.pushBack(parameter);
|
|
}
|
|
|
|
layerValue.set("parameters", parameters);
|
|
layers.pushBack(layerValue);
|
|
}
|
|
return layers;
|
|
}
|
|
|
|
bool RuntimeHost::DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error)
|
|
{
|
|
for (const JsonValue& layerValue : layersValue.asArray())
|
|
{
|
|
if (!layerValue.isObject())
|
|
continue;
|
|
|
|
const JsonValue* shaderIdValue = layerValue.find("shaderId");
|
|
if (!shaderIdValue)
|
|
continue;
|
|
|
|
const std::string shaderId = shaderIdValue->asString();
|
|
auto shaderIt = mPackagesById.find(shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
{
|
|
error = "Preset references unknown shader id: " + shaderId;
|
|
return false;
|
|
}
|
|
|
|
LayerPersistentState layer;
|
|
layer.id = GenerateLayerId();
|
|
layer.shaderId = shaderId;
|
|
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
|
layer.bypass = bypassValue->asBoolean(false);
|
|
|
|
if (const JsonValue* parametersValue = layerValue.find("parameters"))
|
|
{
|
|
for (const JsonValue& parameterValue : parametersValue->asArray())
|
|
{
|
|
if (!parameterValue.isObject())
|
|
continue;
|
|
|
|
const JsonValue* parameterIdValue = parameterValue.find("id");
|
|
const JsonValue* valueValue = parameterValue.find("value");
|
|
if (!parameterIdValue || !valueValue)
|
|
continue;
|
|
|
|
const std::string parameterId = parameterIdValue->asString();
|
|
auto definitionIt = std::find_if(shaderIt->second.parameters.begin(), shaderIt->second.parameters.end(),
|
|
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
|
if (definitionIt == shaderIt->second.parameters.end())
|
|
continue;
|
|
|
|
ShaderParameterValue normalizedValue;
|
|
if (!NormalizeAndValidateValue(*definitionIt, *valueValue, normalizedValue, error))
|
|
return false;
|
|
|
|
layer.parameterValues[parameterId] = normalizedValue;
|
|
}
|
|
}
|
|
|
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
layers.push_back(layer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> RuntimeHost::GetStackPresetNamesLocked() const
|
|
{
|
|
std::vector<std::string> presetNames;
|
|
std::error_code fsError;
|
|
if (!std::filesystem::exists(mPresetRoot, fsError))
|
|
return presetNames;
|
|
|
|
for (const auto& entry : std::filesystem::directory_iterator(mPresetRoot, fsError))
|
|
{
|
|
if (!entry.is_regular_file())
|
|
continue;
|
|
if (ToLowerCopy(entry.path().extension().string()) != ".json")
|
|
continue;
|
|
presetNames.push_back(entry.path().stem().string());
|
|
}
|
|
|
|
std::sort(presetNames.begin(), presetNames.end());
|
|
return presetNames;
|
|
}
|
|
|
|
std::string RuntimeHost::MakeSafePresetFileStem(const std::string& presetName) const
|
|
{
|
|
std::string trimmed = Trim(presetName);
|
|
std::string safe;
|
|
safe.reserve(trimmed.size());
|
|
|
|
for (unsigned char ch : trimmed)
|
|
{
|
|
if (std::isalnum(ch))
|
|
safe.push_back(static_cast<char>(std::tolower(ch)));
|
|
else if (ch == ' ' || ch == '-' || ch == '_')
|
|
{
|
|
if (safe.empty() || safe.back() == '-')
|
|
continue;
|
|
safe.push_back('-');
|
|
}
|
|
}
|
|
|
|
while (!safe.empty() && safe.back() == '-')
|
|
safe.pop_back();
|
|
|
|
return safe;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId)
|
|
{
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
return it == mPersistentState.layers.end() ? nullptr : &*it;
|
|
}
|
|
|
|
const RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId) const
|
|
{
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
return it == mPersistentState.layers.end() ? nullptr : &*it;
|
|
}
|
|
|
|
std::string RuntimeHost::GenerateLayerId()
|
|
{
|
|
while (true)
|
|
{
|
|
++mNextLayerId;
|
|
const std::string candidate = "layer-" + std::to_string(mNextLayerId);
|
|
if (!FindLayerById(candidate))
|
|
return candidate;
|
|
}
|
|
}
|