Layer stacking
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -42,3 +42,5 @@ build.ninja
|
|||||||
*.dmp
|
*.dmp
|
||||||
*.tmp
|
*.tmp
|
||||||
/runtime/
|
/runtime/
|
||||||
|
/ui/node_modules/
|
||||||
|
/ui/dist/
|
||||||
|
|||||||
@@ -34,6 +34,38 @@ std::string ToLower(std::string text)
|
|||||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsSafeUiPath(const std::filesystem::path& relativePath)
|
||||||
|
{
|
||||||
|
for (const std::filesystem::path& part : relativePath)
|
||||||
|
{
|
||||||
|
if (part == "..")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !relativePath.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GuessContentType(const std::filesystem::path& assetPath)
|
||||||
|
{
|
||||||
|
const std::string extension = ToLower(assetPath.extension().string());
|
||||||
|
if (extension == ".js" || extension == ".mjs")
|
||||||
|
return "text/javascript";
|
||||||
|
if (extension == ".css")
|
||||||
|
return "text/css";
|
||||||
|
if (extension == ".json")
|
||||||
|
return "application/json";
|
||||||
|
if (extension == ".svg")
|
||||||
|
return "image/svg+xml";
|
||||||
|
if (extension == ".png")
|
||||||
|
return "image/png";
|
||||||
|
if (extension == ".jpg" || extension == ".jpeg")
|
||||||
|
return "image/jpeg";
|
||||||
|
if (extension == ".ico")
|
||||||
|
return "image/x-icon";
|
||||||
|
if (extension == ".map")
|
||||||
|
return "application/json";
|
||||||
|
return "text/html";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlServer::ControlServer()
|
ControlServer::ControlServer()
|
||||||
@@ -204,20 +236,21 @@ bool ControlServer::HandleHttpRequest(SOCKET clientSocket, const std::string& re
|
|||||||
closesocket(clientSocket);
|
closesocket(clientSocket);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (path == "/app.js" || path == "/styles.css")
|
|
||||||
{
|
|
||||||
std::string contentType;
|
|
||||||
std::string body = LoadUiAsset(path.substr(1), contentType);
|
|
||||||
SendHttpResponse(clientSocket, "200 OK", contentType, body);
|
|
||||||
closesocket(clientSocket);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (path == "/api/state")
|
if (path == "/api/state")
|
||||||
{
|
{
|
||||||
SendHttpResponse(clientSocket, "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
SendHttpResponse(clientSocket, "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||||
closesocket(clientSocket);
|
closesocket(clientSocket);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string contentType;
|
||||||
|
std::string body = LoadUiAsset(path.substr(1), contentType);
|
||||||
|
if (!body.empty())
|
||||||
|
{
|
||||||
|
SendHttpResponse(clientSocket, "200 OK", contentType, body);
|
||||||
|
closesocket(clientSocket);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (method == "POST")
|
else if (method == "POST")
|
||||||
{
|
{
|
||||||
@@ -234,36 +267,69 @@ bool ControlServer::HandleHttpRequest(SOCKET clientSocket, const std::string& re
|
|||||||
bool success = false;
|
bool success = false;
|
||||||
std::string actionError;
|
std::string actionError;
|
||||||
|
|
||||||
if (path == "/api/select-shader")
|
if (path == "/api/layers/add")
|
||||||
{
|
{
|
||||||
const JsonValue* shaderId = root.find("shaderId");
|
const JsonValue* shaderId = root.find("shaderId");
|
||||||
success = shaderId && mCallbacks.selectShader && mCallbacks.selectShader(shaderId->asString(), actionError);
|
success = shaderId && mCallbacks.addLayer && mCallbacks.addLayer(shaderId->asString(), actionError);
|
||||||
}
|
}
|
||||||
else if (path == "/api/update-parameter")
|
else if (path == "/api/layers/remove")
|
||||||
{
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
|
success = layerId && mCallbacks.removeLayer && mCallbacks.removeLayer(layerId->asString(), actionError);
|
||||||
|
}
|
||||||
|
else if (path == "/api/layers/move")
|
||||||
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
|
const JsonValue* direction = root.find("direction");
|
||||||
|
if (layerId && direction && mCallbacks.moveLayer)
|
||||||
|
success = mCallbacks.moveLayer(layerId->asString(), static_cast<int>(direction->asNumber()), actionError);
|
||||||
|
}
|
||||||
|
else if (path == "/api/layers/reorder")
|
||||||
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
|
const JsonValue* targetIndex = root.find("targetIndex");
|
||||||
|
if (layerId && targetIndex && mCallbacks.moveLayerToIndex)
|
||||||
|
success = mCallbacks.moveLayerToIndex(layerId->asString(), static_cast<std::size_t>(targetIndex->asNumber()), actionError);
|
||||||
|
}
|
||||||
|
else if (path == "/api/layers/set-bypass")
|
||||||
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
|
const JsonValue* bypass = root.find("bypass");
|
||||||
|
if (layerId && bypass && mCallbacks.setLayerBypass)
|
||||||
|
success = mCallbacks.setLayerBypass(layerId->asString(), bypass->asBoolean(), actionError);
|
||||||
|
}
|
||||||
|
else if (path == "/api/layers/set-shader")
|
||||||
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
const JsonValue* shaderId = root.find("shaderId");
|
const JsonValue* shaderId = root.find("shaderId");
|
||||||
|
if (layerId && shaderId && mCallbacks.setLayerShader)
|
||||||
|
success = mCallbacks.setLayerShader(layerId->asString(), shaderId->asString(), actionError);
|
||||||
|
}
|
||||||
|
else if (path == "/api/layers/update-parameter")
|
||||||
|
{
|
||||||
|
const JsonValue* layerId = root.find("layerId");
|
||||||
const JsonValue* parameterId = root.find("parameterId");
|
const JsonValue* parameterId = root.find("parameterId");
|
||||||
const JsonValue* value = root.find("value");
|
const JsonValue* value = root.find("value");
|
||||||
if (shaderId && parameterId && value && mCallbacks.updateParameter)
|
if (layerId && parameterId && value && mCallbacks.updateLayerParameter)
|
||||||
success = mCallbacks.updateParameter(shaderId->asString(), parameterId->asString(), SerializeJson(*value, false), actionError);
|
success = mCallbacks.updateLayerParameter(layerId->asString(), parameterId->asString(), SerializeJson(*value, false), actionError);
|
||||||
}
|
}
|
||||||
else if (path == "/api/reset-parameters")
|
else if (path == "/api/layers/reset-parameters")
|
||||||
{
|
{
|
||||||
const JsonValue* shaderId = root.find("shaderId");
|
const JsonValue* layerId = root.find("layerId");
|
||||||
if (shaderId && mCallbacks.resetParameters)
|
if (layerId && mCallbacks.resetLayerParameters)
|
||||||
success = mCallbacks.resetParameters(shaderId->asString(), actionError);
|
success = mCallbacks.resetLayerParameters(layerId->asString(), actionError);
|
||||||
}
|
}
|
||||||
else if (path == "/api/set-bypass")
|
else if (path == "/api/stack-presets/save")
|
||||||
{
|
{
|
||||||
const JsonValue* bypass = root.find("bypass");
|
const JsonValue* presetName = root.find("presetName");
|
||||||
if (bypass && mCallbacks.setBypass)
|
if (presetName && mCallbacks.saveStackPreset)
|
||||||
success = mCallbacks.setBypass(bypass->asBoolean(), actionError);
|
success = mCallbacks.saveStackPreset(presetName->asString(), actionError);
|
||||||
}
|
}
|
||||||
else if (path == "/api/set-mix")
|
else if (path == "/api/stack-presets/load")
|
||||||
{
|
{
|
||||||
const JsonValue* mixAmount = root.find("mixAmount");
|
const JsonValue* presetName = root.find("presetName");
|
||||||
if (mixAmount && mCallbacks.setMixAmount)
|
if (presetName && mCallbacks.loadStackPreset)
|
||||||
success = mCallbacks.setMixAmount(mixAmount->asNumber(), actionError);
|
success = mCallbacks.loadStackPreset(presetName->asString(), actionError);
|
||||||
}
|
}
|
||||||
else if (path == "/api/reload")
|
else if (path == "/api/reload")
|
||||||
{
|
{
|
||||||
@@ -357,17 +423,16 @@ void ControlServer::BroadcastStateLocked()
|
|||||||
|
|
||||||
std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const
|
std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const
|
||||||
{
|
{
|
||||||
const std::filesystem::path assetPath = mUiRoot / relativePath;
|
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||||
|
if (!IsSafeUiPath(sanitizedPath))
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
const std::filesystem::path assetPath = mUiRoot / sanitizedPath;
|
||||||
std::ifstream input(assetPath, std::ios::binary);
|
std::ifstream input(assetPath, std::ios::binary);
|
||||||
if (!input)
|
if (!input)
|
||||||
return "<!doctype html><title>Missing UI asset</title><p>UI asset missing.</p>";
|
return std::string();
|
||||||
|
|
||||||
if (assetPath.extension() == ".js")
|
contentType = GuessContentType(assetPath);
|
||||||
contentType = "text/javascript";
|
|
||||||
else if (assetPath.extension() == ".css")
|
|
||||||
contentType = "text/css";
|
|
||||||
else
|
|
||||||
contentType = "text/html";
|
|
||||||
|
|
||||||
std::ostringstream buffer;
|
std::ostringstream buffer;
|
||||||
buffer << input.rdbuf();
|
buffer << input.rdbuf();
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ public:
|
|||||||
struct Callbacks
|
struct Callbacks
|
||||||
{
|
{
|
||||||
std::function<std::string()> getStateJson;
|
std::function<std::string()> getStateJson;
|
||||||
std::function<bool(const std::string&, std::string&)> selectShader;
|
std::function<bool(const std::string&, std::string&)> addLayer;
|
||||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
std::function<bool(const std::string&, std::string&)> removeLayer;
|
||||||
std::function<bool(const std::string&, std::string&)> resetParameters;
|
std::function<bool(const std::string&, int, std::string&)> moveLayer;
|
||||||
std::function<bool(bool, std::string&)> setBypass;
|
std::function<bool(const std::string&, std::size_t, std::string&)> moveLayerToIndex;
|
||||||
std::function<bool(double, std::string&)> setMixAmount;
|
std::function<bool(const std::string&, bool, std::string&)> setLayerBypass;
|
||||||
|
std::function<bool(const std::string&, const std::string&, std::string&)> setLayerShader;
|
||||||
|
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateLayerParameter;
|
||||||
|
std::function<bool(const std::string&, std::string&)> resetLayerParameters;
|
||||||
|
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
||||||
|
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
||||||
std::function<bool(std::string&)> reloadShader;
|
std::function<bool(std::string&)> reloadShader;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -156,16 +156,15 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mFastTransferExtensionAvailable(false),
|
mFastTransferExtensionAvailable(false),
|
||||||
mCaptureTexture(0),
|
mCaptureTexture(0),
|
||||||
mDecodedTexture(0),
|
mDecodedTexture(0),
|
||||||
|
mLayerTempTexture(0),
|
||||||
mFBOTexture(0),
|
mFBOTexture(0),
|
||||||
mDecodeFrameBuf(0),
|
mDecodeFrameBuf(0),
|
||||||
|
mLayerTempFrameBuf(0),
|
||||||
mFullscreenVAO(0),
|
mFullscreenVAO(0),
|
||||||
mGlobalParamsUBO(0),
|
mGlobalParamsUBO(0),
|
||||||
mDecodeProgram(0),
|
mDecodeProgram(0),
|
||||||
mDecodeVertexShader(0),
|
mDecodeVertexShader(0),
|
||||||
mDecodeFragmentShader(0),
|
mDecodeFragmentShader(0),
|
||||||
mProgram(0),
|
|
||||||
mVertexShader(0),
|
|
||||||
mFragmentShader(0),
|
|
||||||
mGlobalParamsUBOSize(0)
|
mGlobalParamsUBOSize(0)
|
||||||
{
|
{
|
||||||
InitializeCriticalSection(&pMutex);
|
InitializeCriticalSection(&pMutex);
|
||||||
@@ -228,6 +227,8 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
glDeleteBuffers(1, &mGlobalParamsUBO);
|
glDeleteBuffers(1, &mGlobalParamsUBO);
|
||||||
if (mDecodeFrameBuf != 0)
|
if (mDecodeFrameBuf != 0)
|
||||||
glDeleteFramebuffers(1, &mDecodeFrameBuf);
|
glDeleteFramebuffers(1, &mDecodeFrameBuf);
|
||||||
|
if (mLayerTempFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mLayerTempFrameBuf);
|
||||||
if (mIdFrameBuf != 0)
|
if (mIdFrameBuf != 0)
|
||||||
glDeleteFramebuffers(1, &mIdFrameBuf);
|
glDeleteFramebuffers(1, &mIdFrameBuf);
|
||||||
if (mIdColorBuf != 0)
|
if (mIdColorBuf != 0)
|
||||||
@@ -238,12 +239,14 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
glDeleteTextures(1, &mCaptureTexture);
|
glDeleteTextures(1, &mCaptureTexture);
|
||||||
if (mDecodedTexture != 0)
|
if (mDecodedTexture != 0)
|
||||||
glDeleteTextures(1, &mDecodedTexture);
|
glDeleteTextures(1, &mDecodedTexture);
|
||||||
|
if (mLayerTempTexture != 0)
|
||||||
|
glDeleteTextures(1, &mLayerTempTexture);
|
||||||
if (mFBOTexture != 0)
|
if (mFBOTexture != 0)
|
||||||
glDeleteTextures(1, &mFBOTexture);
|
glDeleteTextures(1, &mFBOTexture);
|
||||||
if (mUnpinnedTextureBuffer != 0)
|
if (mUnpinnedTextureBuffer != 0)
|
||||||
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
||||||
|
|
||||||
destroyShaderProgram();
|
destroyLayerPrograms();
|
||||||
destroyDecodeShaderProgram();
|
destroyDecodeShaderProgram();
|
||||||
if (mControlServer)
|
if (mControlServer)
|
||||||
mControlServer->Stop();
|
mControlServer->Stop();
|
||||||
@@ -536,13 +539,18 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
|
|
||||||
ControlServer::Callbacks callbacks;
|
ControlServer::Callbacks callbacks;
|
||||||
callbacks.getStateJson = [this]() { return GetRuntimeStateJson(); };
|
callbacks.getStateJson = [this]() { return GetRuntimeStateJson(); };
|
||||||
callbacks.selectShader = [this](const std::string& shaderId, std::string& error) { return SelectShader(shaderId, error); };
|
callbacks.addLayer = [this](const std::string& shaderId, std::string& error) { return AddLayer(shaderId, error); };
|
||||||
callbacks.updateParameter = [this](const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error) {
|
callbacks.removeLayer = [this](const std::string& layerId, std::string& error) { return RemoveLayer(layerId, error); };
|
||||||
return UpdateParameterJson(shaderId, parameterId, valueJson, error);
|
callbacks.moveLayer = [this](const std::string& layerId, int direction, std::string& error) { return MoveLayer(layerId, direction, error); };
|
||||||
|
callbacks.moveLayerToIndex = [this](const std::string& layerId, std::size_t targetIndex, std::string& error) { return MoveLayerToIndex(layerId, targetIndex, error); };
|
||||||
|
callbacks.setLayerBypass = [this](const std::string& layerId, bool bypassed, std::string& error) { return SetLayerBypass(layerId, bypassed, error); };
|
||||||
|
callbacks.setLayerShader = [this](const std::string& layerId, const std::string& shaderId, std::string& error) { return SetLayerShader(layerId, shaderId, error); };
|
||||||
|
callbacks.updateLayerParameter = [this](const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error) {
|
||||||
|
return UpdateLayerParameterJson(layerId, parameterId, valueJson, error);
|
||||||
};
|
};
|
||||||
callbacks.resetParameters = [this](const std::string& shaderId, std::string& error) { return ResetShaderParameters(shaderId, error); };
|
callbacks.resetLayerParameters = [this](const std::string& layerId, std::string& error) { return ResetLayerParameters(layerId, error); };
|
||||||
callbacks.setBypass = [this](bool bypassEnabled, std::string& error) { return SetBypassEnabled(bypassEnabled, error); };
|
callbacks.saveStackPreset = [this](const std::string& presetName, std::string& error) { return SaveStackPreset(presetName, error); };
|
||||||
callbacks.setMixAmount = [this](double mixAmount, std::string& error) { return SetMixAmount(mixAmount, error); };
|
callbacks.loadStackPreset = [this](const std::string& presetName, std::string& error) { return LoadStackPreset(presetName, error); };
|
||||||
callbacks.reloadShader = [this](std::string& error) {
|
callbacks.reloadShader = [this](std::string& error) {
|
||||||
if (!ReloadShader())
|
if (!ReloadShader())
|
||||||
{
|
{
|
||||||
@@ -567,7 +575,7 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
if (! compileLayerPrograms(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
{
|
{
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||||
return false;
|
return false;
|
||||||
@@ -606,10 +614,20 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenTextures(1, &mLayerTempTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mLayerTempTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
|
||||||
// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
|
// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
|
||||||
// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
|
// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
|
||||||
glGenFramebuffers(1, &mDecodeFrameBuf);
|
glGenFramebuffers(1, &mDecodeFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mLayerTempFrameBuf);
|
||||||
glGenFramebuffers(1, &mIdFrameBuf);
|
glGenFramebuffers(1, &mIdFrameBuf);
|
||||||
glGenRenderbuffers(1, &mIdColorBuf);
|
glGenRenderbuffers(1, &mIdColorBuf);
|
||||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||||
@@ -625,6 +643,15 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
|
||||||
|
glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (glStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
MessageBox(NULL, _T("Cannot initialize layer framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
|
||||||
// Texture for FBO
|
// Texture for FBO
|
||||||
@@ -872,7 +899,7 @@ bool OpenGLComposite::ReloadShader()
|
|||||||
EnterCriticalSection(&pMutex);
|
EnterCriticalSection(&pMutex);
|
||||||
wglMakeCurrent(hGLDC, hGLRC);
|
wglMakeCurrent(hGLDC, hGLRC);
|
||||||
|
|
||||||
bool success = compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage);
|
bool success = compileLayerPrograms(sizeof(compilerErrorMessage), compilerErrorMessage);
|
||||||
if (mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
mRuntimeHost->ClearReloadRequest();
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
|
||||||
@@ -895,6 +922,98 @@ bool OpenGLComposite::ReloadShader()
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
GLsizei errorBufferSize = 0;
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
std::string loadError;
|
||||||
|
const char* vertexSource = kVertexShaderSource;
|
||||||
|
|
||||||
|
if (!mRuntimeHost->BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* fragmentSource = fragmentShaderSource.c_str();
|
||||||
|
|
||||||
|
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL);
|
||||||
|
glCompileShader(newVertexShader);
|
||||||
|
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
|
||||||
|
glCompileShader(newFragmentShader);
|
||||||
|
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
glDeleteShader(newFragmentShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint newProgram = glCreateProgram();
|
||||||
|
glAttachShader(newProgram, newVertexShader);
|
||||||
|
glAttachShader(newProgram, newFragmentShader);
|
||||||
|
glLinkProgram(newProgram);
|
||||||
|
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteProgram(newProgram);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
glDeleteShader(newFragmentShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layerProgram.layerId = state.layerId;
|
||||||
|
layerProgram.shaderId = state.shaderId;
|
||||||
|
layerProgram.program = newProgram;
|
||||||
|
layerProgram.vertexShader = newVertexShader;
|
||||||
|
layerProgram.fragmentShader = newFragmentShader;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::compileLayerPrograms(int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector<RuntimeRenderState>();
|
||||||
|
std::vector<LayerProgram> newPrograms;
|
||||||
|
newPrograms.reserve(layerStates.size());
|
||||||
|
|
||||||
|
for (const RuntimeRenderState& state : layerStates)
|
||||||
|
{
|
||||||
|
LayerProgram layerProgram;
|
||||||
|
if (!compileSingleLayerProgram(state, layerProgram, errorMessageSize, errorMessage))
|
||||||
|
{
|
||||||
|
for (LayerProgram& program : newPrograms)
|
||||||
|
destroySingleLayerProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newPrograms.push_back(layerProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyLayerPrograms();
|
||||||
|
mLayerPrograms.swap(newPrograms);
|
||||||
|
|
||||||
|
if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||||
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::compileDecodeShader(int errorMessageSize, char* errorMessage)
|
bool OpenGLComposite::compileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
GLsizei errorBufferSize = 0;
|
GLsizei errorBufferSize = 0;
|
||||||
@@ -947,27 +1066,34 @@ bool OpenGLComposite::compileDecodeShader(int errorMessageSize, char* errorMessa
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::destroyShaderProgram()
|
void OpenGLComposite::destroySingleLayerProgram(LayerProgram& layerProgram)
|
||||||
{
|
{
|
||||||
if (mProgram != 0)
|
if (layerProgram.program != 0)
|
||||||
{
|
{
|
||||||
glDeleteProgram(mProgram);
|
glDeleteProgram(layerProgram.program);
|
||||||
mProgram = 0;
|
layerProgram.program = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFragmentShader != 0)
|
if (layerProgram.fragmentShader != 0)
|
||||||
{
|
{
|
||||||
glDeleteShader(mFragmentShader);
|
glDeleteShader(layerProgram.fragmentShader);
|
||||||
mFragmentShader = 0;
|
layerProgram.fragmentShader = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mVertexShader != 0)
|
if (layerProgram.vertexShader != 0)
|
||||||
{
|
{
|
||||||
glDeleteShader(mVertexShader);
|
glDeleteShader(layerProgram.vertexShader);
|
||||||
mVertexShader = 0;
|
layerProgram.vertexShader = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::destroyLayerPrograms()
|
||||||
|
{
|
||||||
|
for (LayerProgram& layerProgram : mLayerPrograms)
|
||||||
|
destroySingleLayerProgram(layerProgram);
|
||||||
|
mLayerPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void OpenGLComposite::destroyDecodeShaderProgram()
|
void OpenGLComposite::destroyDecodeShaderProgram()
|
||||||
{
|
{
|
||||||
if (mDecodeProgram != 0)
|
if (mDecodeProgram != 0)
|
||||||
@@ -1006,29 +1132,45 @@ void OpenGLComposite::renderEffect()
|
|||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
renderDecodePass();
|
renderDecodePass();
|
||||||
|
|
||||||
|
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector<RuntimeRenderState>();
|
||||||
|
if (layerStates.empty() || mLayerPrograms.empty())
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, 0, 0, mFrameWidth, mFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GLuint sourceTexture = mDecodedTexture;
|
||||||
|
for (std::size_t index = 0; index < layerStates.size() && index < mLayerPrograms.size(); ++index)
|
||||||
|
{
|
||||||
|
const std::size_t remaining = layerStates.size() - index;
|
||||||
|
const bool writeToMain = (remaining % 2) == 1;
|
||||||
|
renderShaderProgram(sourceTexture, writeToMain ? mIdFrameBuf : mLayerTempFrameBuf, mLayerPrograms[index], layerStates[index]);
|
||||||
|
sourceTexture = writeToMain ? mFBOTexture : mLayerTempTexture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mFastTransferExtensionAvailable)
|
||||||
|
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
||||||
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
||||||
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
|
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||||
glBindVertexArray(mFullscreenVAO);
|
glBindVertexArray(mFullscreenVAO);
|
||||||
glUseProgram(mProgram);
|
glUseProgram(layerProgram.program);
|
||||||
|
|
||||||
if (mRuntimeHost)
|
|
||||||
{
|
|
||||||
const RuntimeRenderState state = mRuntimeHost->GetRenderState(mFrameWidth, mFrameHeight);
|
|
||||||
updateGlobalParamsBuffer(state);
|
updateGlobalParamsBuffer(state);
|
||||||
}
|
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
if (mFastTransferExtensionAvailable)
|
|
||||||
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::renderDecodePass()
|
void OpenGLComposite::renderDecodePass()
|
||||||
@@ -1056,82 +1198,6 @@ void OpenGLComposite::renderDecodePass()
|
|||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a fullscreen shader pass from the runtime Slang source into a core-profile
|
|
||||||
// GLSL program. The renderer owns the fullscreen pass and parameter UBO layout.
|
|
||||||
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
GLsizei errorBufferSize = 0;
|
|
||||||
GLint compileResult = GL_FALSE;
|
|
||||||
GLint linkResult = GL_FALSE;
|
|
||||||
std::string fragmentShaderSource;
|
|
||||||
std::string loadError;
|
|
||||||
const char* vertexSource = kVertexShaderSource;
|
|
||||||
|
|
||||||
if (!mRuntimeHost->BuildActiveFragmentShaderSource(fragmentShaderSource, loadError))
|
|
||||||
{
|
|
||||||
mRuntimeHost->SetCompileStatus(false, loadError);
|
|
||||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* fragmentSource = fragmentShaderSource.c_str();
|
|
||||||
|
|
||||||
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER);
|
|
||||||
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL);
|
|
||||||
glCompileShader(newVertexShader);
|
|
||||||
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
glDeleteShader(newVertexShader);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
|
|
||||||
glCompileShader(newFragmentShader);
|
|
||||||
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
glDeleteShader(newVertexShader);
|
|
||||||
glDeleteShader(newFragmentShader);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint newProgram = glCreateProgram();
|
|
||||||
glAttachShader(newProgram, newVertexShader);
|
|
||||||
glAttachShader(newProgram, newFragmentShader);
|
|
||||||
glLinkProgram(newProgram);
|
|
||||||
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult);
|
|
||||||
if (linkResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
glDeleteProgram(newProgram);
|
|
||||||
glDeleteShader(newVertexShader);
|
|
||||||
glDeleteShader(newFragmentShader);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyShaderProgram();
|
|
||||||
|
|
||||||
mProgram = newProgram;
|
|
||||||
mVertexShader = newVertexShader;
|
|
||||||
mFragmentShader = newFragmentShader;
|
|
||||||
const RuntimeRenderState state = mRuntimeHost->GetRenderState(mFrameWidth, mFrameHeight);
|
|
||||||
if (!updateGlobalParamsBuffer(state))
|
|
||||||
{
|
|
||||||
CopyErrorMessage("Failed to allocate the runtime parameter UBO.", errorMessageSize, errorMessage);
|
|
||||||
destroyShaderProgram();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRuntimeHost->SetCompileStatus(true, "Shader compiled successfully.");
|
|
||||||
mRuntimeHost->ClearReloadRequest();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::PollRuntimeChanges()
|
bool OpenGLComposite::PollRuntimeChanges()
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost)
|
if (!mRuntimeHost)
|
||||||
@@ -1154,7 +1220,7 @@ bool OpenGLComposite::PollRuntimeChanges()
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
char compilerErrorMessage[1024] = {};
|
char compilerErrorMessage[1024] = {};
|
||||||
if (!compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
if (!compileLayerPrograms(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
{
|
{
|
||||||
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||||
mRuntimeHost->ClearReloadRequest();
|
mRuntimeHost->ClearReloadRequest();
|
||||||
@@ -1251,9 +1317,9 @@ std::string OpenGLComposite::GetRuntimeStateJson() const
|
|||||||
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
|
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::SelectShader(const std::string& shaderId, std::string& error)
|
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost->SelectShader(shaderId, error))
|
if (!mRuntimeHost->AddLayer(shaderId, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ReloadShader();
|
ReloadShader();
|
||||||
@@ -1261,40 +1327,93 @@ bool OpenGLComposite::SelectShader(const std::string& shaderId, std::string& err
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error)
|
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->SetLayerBypass(layerId, bypassed, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->SetLayerShader(layerId, shaderId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
|
||||||
{
|
{
|
||||||
JsonValue parsedValue;
|
JsonValue parsedValue;
|
||||||
if (!ParseJson(valueJson, parsedValue, error))
|
if (!ParseJson(valueJson, parsedValue, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!mRuntimeHost->UpdateParameter(shaderId, parameterId, parsedValue, error))
|
if (!mRuntimeHost->UpdateLayerParameter(layerId, parameterId, parsedValue, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::ResetShaderParameters(const std::string& shaderId, std::string& error)
|
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost->ResetParameters(shaderId, error))
|
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::SetBypassEnabled(bool bypassEnabled, std::string& error)
|
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost->SetBypass(bypassEnabled, error))
|
if (!mRuntimeHost->SaveStackPreset(presetName, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::SetMixAmount(double mixAmount, std::string& error)
|
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost->SetMixAmount(mixAmount, error))
|
if (!mRuntimeHost->LoadStackPreset(presetName, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,16 @@ public:
|
|||||||
bool Stop();
|
bool Stop();
|
||||||
bool ReloadShader();
|
bool ReloadShader();
|
||||||
std::string GetRuntimeStateJson() const;
|
std::string GetRuntimeStateJson() const;
|
||||||
bool SelectShader(const std::string& shaderId, std::string& error);
|
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||||
bool UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||||
bool ResetShaderParameters(const std::string& shaderId, std::string& error);
|
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||||
bool SetBypassEnabled(bool bypassEnabled, std::string& error);
|
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||||
bool SetMixAmount(double mixAmount, std::string& error);
|
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||||
|
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||||
|
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||||
|
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||||
|
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||||
|
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
void resizeGL(WORD width, WORD height);
|
||||||
void paintGL();
|
void paintGL();
|
||||||
@@ -118,9 +123,11 @@ private:
|
|||||||
bool mFastTransferExtensionAvailable;
|
bool mFastTransferExtensionAvailable;
|
||||||
GLuint mCaptureTexture;
|
GLuint mCaptureTexture;
|
||||||
GLuint mDecodedTexture;
|
GLuint mDecodedTexture;
|
||||||
|
GLuint mLayerTempTexture;
|
||||||
GLuint mFBOTexture;
|
GLuint mFBOTexture;
|
||||||
GLuint mUnpinnedTextureBuffer;
|
GLuint mUnpinnedTextureBuffer;
|
||||||
GLuint mDecodeFrameBuf;
|
GLuint mDecodeFrameBuf;
|
||||||
|
GLuint mLayerTempFrameBuf;
|
||||||
GLuint mIdFrameBuf;
|
GLuint mIdFrameBuf;
|
||||||
GLuint mIdColorBuf;
|
GLuint mIdColorBuf;
|
||||||
GLuint mIdDepthBuf;
|
GLuint mIdDepthBuf;
|
||||||
@@ -129,21 +136,31 @@ private:
|
|||||||
GLuint mDecodeProgram;
|
GLuint mDecodeProgram;
|
||||||
GLuint mDecodeVertexShader;
|
GLuint mDecodeVertexShader;
|
||||||
GLuint mDecodeFragmentShader;
|
GLuint mDecodeFragmentShader;
|
||||||
GLuint mProgram;
|
|
||||||
GLuint mVertexShader;
|
|
||||||
GLuint mFragmentShader;
|
|
||||||
GLsizeiptr mGlobalParamsUBOSize;
|
GLsizeiptr mGlobalParamsUBOSize;
|
||||||
int mViewWidth;
|
int mViewWidth;
|
||||||
int mViewHeight;
|
int mViewHeight;
|
||||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||||
std::unique_ptr<ControlServer> mControlServer;
|
std::unique_ptr<ControlServer> mControlServer;
|
||||||
|
|
||||||
|
struct LayerProgram
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
GLuint program = 0;
|
||||||
|
GLuint vertexShader = 0;
|
||||||
|
GLuint fragmentShader = 0;
|
||||||
|
};
|
||||||
|
std::vector<LayerProgram> mLayerPrograms;
|
||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
bool compileFragmentShader(int errorMessageSize, char* errorMessage);
|
bool compileLayerPrograms(int errorMessageSize, char* errorMessage);
|
||||||
|
bool compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||||
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||||
void destroyShaderProgram();
|
void destroyLayerPrograms();
|
||||||
|
void destroySingleLayerProgram(LayerProgram& layerProgram);
|
||||||
void destroyDecodeShaderProgram();
|
void destroyDecodeShaderProgram();
|
||||||
void renderDecodePass();
|
void renderDecodePass();
|
||||||
|
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
bool PollRuntimeChanges();
|
bool PollRuntimeChanges();
|
||||||
void broadcastRuntimeState();
|
void broadcastRuntimeState();
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ bool IsFiniteNumber(double value)
|
|||||||
return std::isfinite(value) != 0;
|
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> JsonArrayToNumbers(const JsonValue& value)
|
||||||
{
|
{
|
||||||
std::vector<double> numbers;
|
std::vector<double> numbers;
|
||||||
@@ -107,11 +114,6 @@ std::string SlangTypeForParameter(ShaderParameterType type)
|
|||||||
return "uniform float";
|
return "uniform float";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GlslTypeForUniformDeclaration(const std::string& declaration)
|
|
||||||
{
|
|
||||||
return Trim(declaration);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
||||||
{
|
{
|
||||||
if (typeName == "float")
|
if (typeName == "float")
|
||||||
@@ -154,11 +156,10 @@ RuntimeHost::RuntimeHost()
|
|||||||
mSmoothedRenderMilliseconds(0.0),
|
mSmoothedRenderMilliseconds(0.0),
|
||||||
mServerPort(8080),
|
mServerPort(8080),
|
||||||
mAutoReloadEnabled(true),
|
mAutoReloadEnabled(true),
|
||||||
mMixAmount(1.0),
|
|
||||||
mBypass(false),
|
|
||||||
mStartTime(std::chrono::steady_clock::now()),
|
mStartTime(std::chrono::steady_clock::now()),
|
||||||
mLastScanTime(std::chrono::steady_clock::time_point::min()),
|
mLastScanTime(std::chrono::steady_clock::time_point::min()),
|
||||||
mFrameCounter(0)
|
mFrameCounter(0),
|
||||||
|
mNextLayerId(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +179,22 @@ bool RuntimeHost::Initialize(std::string& error)
|
|||||||
if (!ScanShaderPackages(error))
|
if (!ScanShaderPackages(error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (mActiveShaderId.empty() && !mPackageOrder.empty())
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||||
mActiveShaderId = mPackageOrder.front();
|
{
|
||||||
|
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;
|
mServerPort = mConfig.serverPort;
|
||||||
mAutoReloadEnabled = mConfig.autoReload;
|
mAutoReloadEnabled = mConfig.autoReload;
|
||||||
@@ -226,7 +241,13 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
std::string scanError;
|
std::string scanError;
|
||||||
std::map<std::string, ShaderPackage> previousPackages = mPackagesById;
|
std::map<std::string, ShaderPackage> previousPackages = mPackagesById;
|
||||||
std::vector<std::string> previousOrder = mPackageOrder;
|
std::vector<std::string> previousOrder = mPackageOrder;
|
||||||
const std::string previousActive = mActiveShaderId;
|
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))
|
if (!ScanShaderPackages(scanError))
|
||||||
{
|
{
|
||||||
@@ -254,19 +275,22 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto previousActiveIt = previousPackages.find(previousActive);
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||||
auto activeIt = mPackagesById.find(mActiveShaderId);
|
|
||||||
if (previousActiveIt != previousPackages.end() && activeIt != mPackagesById.end())
|
|
||||||
{
|
{
|
||||||
if (previousActiveIt->second.shaderWriteTime != activeIt->second.shaderWriteTime ||
|
auto active = mPackagesById.find(layer.shaderId);
|
||||||
previousActiveIt->second.manifestWriteTime != activeIt->second.manifestWriteTime)
|
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;
|
mReloadRequested = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (previousActive != mActiveShaderId)
|
|
||||||
mReloadRequested = true;
|
|
||||||
|
|
||||||
reloadRequested = mReloadRequested;
|
reloadRequested = mReloadRequested;
|
||||||
return true;
|
return true;
|
||||||
@@ -295,24 +319,115 @@ void RuntimeHost::ClearReloadRequest()
|
|||||||
mReloadRequested = false;
|
mReloadRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::SelectShader(const std::string& shaderId, std::string& error)
|
bool RuntimeHost::AddLayer(const std::string& shaderId, std::string& error)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (mPackagesById.find(shaderId) == mPackagesById.end())
|
auto shaderIt = mPackagesById.find(shaderId);
|
||||||
|
if (shaderIt == mPackagesById.end())
|
||||||
{
|
{
|
||||||
error = "Unknown shader id: " + shaderId;
|
error = "Unknown shader id: " + shaderId;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mActiveShaderId = shaderId;
|
LayerPersistentState layer;
|
||||||
mPersistentState.activeShaderId = shaderId;
|
layer.id = GenerateLayerId();
|
||||||
|
layer.shaderId = shaderId;
|
||||||
|
layer.bypass = false;
|
||||||
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
||||||
|
mPersistentState.layers.push_back(layer);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error)
|
bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
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);
|
auto shaderIt = mPackagesById.find(shaderId);
|
||||||
if (shaderIt == mPackagesById.end())
|
if (shaderIt == mPackagesById.end())
|
||||||
@@ -321,6 +436,31 @@ bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string
|
|||||||
return false;
|
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;
|
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||||
auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(),
|
auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(),
|
||||||
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
||||||
@@ -334,49 +474,89 @@ bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string
|
|||||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mPersistentState.parameterValuesByShader[shaderId][parameterId] = normalized;
|
layer->parameterValues[parameterId] = normalized;
|
||||||
|
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::ResetParameters(const std::string& shaderId, std::string& error)
|
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|
||||||
auto shaderIt = mPackagesById.find(shaderId);
|
LayerPersistentState* layer = FindLayerById(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto shaderIt = mPackagesById.find(layer->shaderId);
|
||||||
if (shaderIt == mPackagesById.end())
|
if (shaderIt == mPackagesById.end())
|
||||||
{
|
{
|
||||||
error = "Unknown shader id: " + shaderId;
|
error = "Unknown shader id: " + layer->shaderId;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<std::string, ShaderParameterValue>& shaderValues = mPersistentState.parameterValuesByShader[shaderId];
|
layer->parameterValues.clear();
|
||||||
shaderValues.clear();
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
||||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
|
||||||
shaderValues[definition.id] = DefaultValueForDefinition(definition);
|
|
||||||
|
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::SetBypass(bool bypassEnabled, std::string& error)
|
bool RuntimeHost::SaveStackPreset(const std::string& presetName, std::string& error) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
mBypass = bypassEnabled;
|
const std::string safeStem = MakeSafePresetFileStem(presetName);
|
||||||
mPersistentState.bypass = bypassEnabled;
|
if (safeStem.empty())
|
||||||
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.";
|
error = "Preset name must include at least one letter or number.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mMixAmount = std::clamp(mixAmount, 0.0, 1.0);
|
JsonValue root = JsonValue::MakeObject();
|
||||||
mPersistentState.mixAmount = mMixAmount;
|
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);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,17 +593,24 @@ void RuntimeHost::AdvanceFrame()
|
|||||||
++mFrameCounter;
|
++mFrameCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error)
|
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ShaderPackage shaderPackage;
|
ShaderPackage shaderPackage;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
auto it = mPackagesById.find(mActiveShaderId);
|
const LayerPersistentState* layer = FindLayerById(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = mPackagesById.find(layer->shaderId);
|
||||||
if (it == mPackagesById.end())
|
if (it == mPackagesById.end())
|
||||||
{
|
{
|
||||||
error = "No active shader is selected.";
|
error = "Unknown shader id: " + layer->shaderId;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
shaderPackage = it->second;
|
shaderPackage = it->second;
|
||||||
@@ -450,49 +637,53 @@ bool RuntimeHost::BuildActiveFragmentShaderSource(std::string& fragmentShaderSou
|
|||||||
}
|
}
|
||||||
catch (const std::exception& exception)
|
catch (const std::exception& exception)
|
||||||
{
|
{
|
||||||
error = std::string("RuntimeHost::BuildActiveFragmentShaderSource exception: ") + exception.what();
|
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
error = "RuntimeHost::BuildActiveFragmentShaderSource threw a non-standard exception.";
|
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeRenderState RuntimeHost::GetRenderState(unsigned outputWidth, unsigned outputHeight) const
|
std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
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;
|
RuntimeRenderState state;
|
||||||
state.activeShaderId = mActiveShaderId;
|
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.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
state.frameCount = static_cast<double>(mFrameCounter);
|
state.frameCount = static_cast<double>(mFrameCounter);
|
||||||
state.mixAmount = mMixAmount;
|
state.mixAmount = 1.0;
|
||||||
state.bypass = mBypass ? 1.0 : 0.0;
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||||
state.inputWidth = mSignalWidth;
|
state.inputWidth = mSignalWidth;
|
||||||
state.inputHeight = mSignalHeight;
|
state.inputHeight = mSignalHeight;
|
||||||
state.outputWidth = outputWidth;
|
state.outputWidth = outputWidth;
|
||||||
state.outputHeight = outputHeight;
|
state.outputHeight = outputHeight;
|
||||||
|
|
||||||
auto shaderIt = mPackagesById.find(mActiveShaderId);
|
|
||||||
if (shaderIt != mPackagesById.end())
|
|
||||||
{
|
|
||||||
state.parameterDefinitions = shaderIt->second.parameters;
|
state.parameterDefinitions = shaderIt->second.parameters;
|
||||||
auto persistedIt = mPersistentState.parameterValuesByShader.find(mActiveShaderId);
|
|
||||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||||
{
|
{
|
||||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||||
if (persistedIt != mPersistentState.parameterValuesByShader.end())
|
auto valueIt = layer.parameterValues.find(definition.id);
|
||||||
{
|
if (valueIt != layer.parameterValues.end())
|
||||||
auto valueIt = persistedIt->second.find(definition.id);
|
|
||||||
if (valueIt != persistedIt->second.end())
|
|
||||||
value = valueIt->second;
|
value = valueIt->second;
|
||||||
}
|
|
||||||
state.parameterValues[definition.id] = value;
|
state.parameterValues[definition.id] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
states.push_back(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string RuntimeHost::BuildStateJson() const
|
std::string RuntimeHost::BuildStateJson() const
|
||||||
@@ -543,62 +734,102 @@ bool RuntimeHost::LoadPersistentState(std::string& error)
|
|||||||
if (!ParseJson(stateText, root, error))
|
if (!ParseJson(stateText, root, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
|
if (const JsonValue* layersValue = root.find("layers"))
|
||||||
mPersistentState.activeShaderId = activeShaderValue->asString();
|
{
|
||||||
if (const JsonValue* mixAmountValue = root.find("mixAmount"))
|
for (const JsonValue& layerValue : layersValue->asArray())
|
||||||
mPersistentState.mixAmount = mixAmountValue->asNumber(1.0);
|
{
|
||||||
if (const JsonValue* bypassValue = root.find("bypass"))
|
if (!layerValue.isObject())
|
||||||
mPersistentState.bypass = bypassValue->asBoolean(false);
|
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* valuesByShader = root.find("parameterValuesByShader"))
|
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
|
||||||
{
|
{
|
||||||
for (const auto& shaderItem : valuesByShader->asObject())
|
for (const auto& parameterItem : parameterValues->asObject())
|
||||||
{
|
|
||||||
std::map<std::string, ShaderParameterValue>& shaderValues = mPersistentState.parameterValuesByShader[shaderItem.first];
|
|
||||||
for (const auto& parameterItem : shaderItem.second.asObject())
|
|
||||||
{
|
{
|
||||||
ShaderParameterValue value;
|
ShaderParameterValue value;
|
||||||
const JsonValue& jsonValue = parameterItem.second;
|
const JsonValue& jsonValue = parameterItem.second;
|
||||||
if (jsonValue.isBoolean())
|
if (jsonValue.isBoolean())
|
||||||
{
|
|
||||||
value.booleanValue = jsonValue.asBoolean();
|
value.booleanValue = jsonValue.asBoolean();
|
||||||
}
|
|
||||||
else if (jsonValue.isString())
|
else if (jsonValue.isString())
|
||||||
{
|
|
||||||
value.enumValue = jsonValue.asString();
|
value.enumValue = jsonValue.asString();
|
||||||
}
|
|
||||||
else if (jsonValue.isNumber())
|
else if (jsonValue.isNumber())
|
||||||
{
|
|
||||||
value.numberValues.push_back(jsonValue.asNumber());
|
value.numberValues.push_back(jsonValue.asNumber());
|
||||||
}
|
|
||||||
else if (jsonValue.isArray())
|
else if (jsonValue.isArray())
|
||||||
{
|
|
||||||
value.numberValues = JsonArrayToNumbers(jsonValue);
|
value.numberValues = JsonArrayToNumbers(jsonValue);
|
||||||
|
layer.parameterValues[parameterItem.first] = value;
|
||||||
}
|
}
|
||||||
shaderValues[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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mActiveShaderId = mPersistentState.activeShaderId;
|
mPersistentState.layers.push_back(layer);
|
||||||
mMixAmount = std::clamp(mPersistentState.mixAmount, 0.0, 1.0);
|
}
|
||||||
mBypass = mPersistentState.bypass;
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::SavePersistentState(std::string& error) const
|
bool RuntimeHost::SavePersistentState(std::string& error) const
|
||||||
{
|
{
|
||||||
JsonValue root = JsonValue::MakeObject();
|
JsonValue root = JsonValue::MakeObject();
|
||||||
root.set("activeShaderId", JsonValue(mActiveShaderId));
|
|
||||||
root.set("mixAmount", JsonValue(mMixAmount));
|
|
||||||
root.set("bypass", JsonValue(mBypass));
|
|
||||||
|
|
||||||
JsonValue valuesByShader = JsonValue::MakeObject();
|
JsonValue layers = JsonValue::MakeArray();
|
||||||
for (const auto& shaderItem : mPersistentState.parameterValuesByShader)
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
||||||
{
|
{
|
||||||
JsonValue shaderValues = JsonValue::MakeObject();
|
JsonValue layerValue = JsonValue::MakeObject();
|
||||||
auto packageIt = mPackagesById.find(shaderItem.first);
|
layerValue.set("id", JsonValue(layer.id));
|
||||||
for (const auto& parameterItem : shaderItem.second)
|
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;
|
const ShaderParameterDefinition* definition = nullptr;
|
||||||
if (packageIt != mPackagesById.end())
|
if (packageIt != mPackagesById.end())
|
||||||
@@ -614,11 +845,13 @@ bool RuntimeHost::SavePersistentState(std::string& error) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (definition)
|
if (definition)
|
||||||
shaderValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
|
parameterValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
|
||||||
}
|
}
|
||||||
valuesByShader.set(shaderItem.first, shaderValues);
|
|
||||||
|
layerValue.set("parameterValues", parameterValues);
|
||||||
|
layers.pushBack(layerValue);
|
||||||
}
|
}
|
||||||
root.set("parameterValuesByShader", valuesByShader);
|
root.set("layers", layers);
|
||||||
|
|
||||||
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
|
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
|
||||||
}
|
}
|
||||||
@@ -653,7 +886,6 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureParameterDefaultsLocked(shaderPackage);
|
|
||||||
packageOrder.push_back(shaderPackage.id);
|
packageOrder.push_back(shaderPackage.id);
|
||||||
packagesById[shaderPackage.id] = shaderPackage;
|
packagesById[shaderPackage.id] = shaderPackage;
|
||||||
}
|
}
|
||||||
@@ -662,16 +894,12 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
mPackagesById.swap(packagesById);
|
mPackagesById.swap(packagesById);
|
||||||
mPackageOrder.swap(packageOrder);
|
mPackageOrder.swap(packageOrder);
|
||||||
|
|
||||||
if (!mActiveShaderId.empty() && mPackagesById.find(mActiveShaderId) == mPackagesById.end())
|
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
||||||
{
|
{
|
||||||
mActiveShaderId.clear();
|
if (mPackagesById.find(it->shaderId) == mPackagesById.end())
|
||||||
mPersistentState.activeShaderId.clear();
|
it = mPersistentState.layers.erase(it);
|
||||||
}
|
else
|
||||||
|
++it;
|
||||||
if (mActiveShaderId.empty() && !mPackageOrder.empty())
|
|
||||||
{
|
|
||||||
mActiveShaderId = mPackageOrder.front();
|
|
||||||
mPersistentState.activeShaderId = mActiveShaderId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -739,22 +967,14 @@ bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath,
|
|||||||
if (const JsonValue* defaultValue = parameterJson.find("default"))
|
if (const JsonValue* defaultValue = parameterJson.find("default"))
|
||||||
{
|
{
|
||||||
if (definition.type == ShaderParameterType::Boolean)
|
if (definition.type == ShaderParameterType::Boolean)
|
||||||
{
|
|
||||||
definition.defaultBoolean = defaultValue->asBoolean(false);
|
definition.defaultBoolean = defaultValue->asBoolean(false);
|
||||||
}
|
|
||||||
else if (definition.type == ShaderParameterType::Enum)
|
else if (definition.type == ShaderParameterType::Enum)
|
||||||
{
|
|
||||||
definition.defaultEnumValue = defaultValue->asString();
|
definition.defaultEnumValue = defaultValue->asString();
|
||||||
}
|
|
||||||
else if (defaultValue->isNumber())
|
else if (defaultValue->isNumber())
|
||||||
{
|
|
||||||
definition.defaultNumbers.push_back(defaultValue->asNumber());
|
definition.defaultNumbers.push_back(defaultValue->asNumber());
|
||||||
}
|
|
||||||
else if (defaultValue->isArray())
|
else if (defaultValue->isArray())
|
||||||
{
|
|
||||||
definition.defaultNumbers = JsonArrayToNumbers(*defaultValue);
|
definition.defaultNumbers = JsonArrayToNumbers(*defaultValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (const JsonValue* minValue = parameterJson.find("min"))
|
if (const JsonValue* minValue = parameterJson.find("min"))
|
||||||
{
|
{
|
||||||
@@ -934,13 +1154,12 @@ ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParamete
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::EnsureParameterDefaultsLocked(ShaderPackage& shaderPackage)
|
void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const
|
||||||
{
|
{
|
||||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||||
{
|
{
|
||||||
auto& shaderValues = mPersistentState.parameterValuesByShader[shaderPackage.id];
|
if (layerState.parameterValues.find(definition.id) == layerState.parameterValues.end())
|
||||||
if (shaderValues.find(definition.id) == shaderValues.end())
|
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
shaderValues[definition.id] = DefaultValueForDefinition(definition);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1127,10 +1346,12 @@ bool RuntimeHost::ResolvePaths(std::string& error)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mUiRoot = mRepoRoot / "ui";
|
const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
|
||||||
|
mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
|
||||||
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
|
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
|
||||||
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
|
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
|
||||||
mRuntimeRoot = mRepoRoot / "runtime";
|
mRuntimeRoot = mRepoRoot / "runtime";
|
||||||
|
mPresetRoot = mRuntimeRoot / "stack_presets";
|
||||||
mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
|
mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
|
||||||
mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
|
mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
|
||||||
mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
|
mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
|
||||||
@@ -1138,6 +1359,7 @@ bool RuntimeHost::ResolvePaths(std::string& error)
|
|||||||
|
|
||||||
std::error_code fsError;
|
std::error_code fsError;
|
||||||
std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
|
std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
|
||||||
|
std::filesystem::create_directories(mPresetRoot, fsError);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,11 +1375,9 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
root.set("app", app);
|
root.set("app", app);
|
||||||
|
|
||||||
JsonValue runtime = JsonValue::MakeObject();
|
JsonValue runtime = JsonValue::MakeObject();
|
||||||
runtime.set("activeShaderId", JsonValue(mActiveShaderId));
|
runtime.set("layerCount", JsonValue(static_cast<double>(mPersistentState.layers.size())));
|
||||||
runtime.set("compileSucceeded", JsonValue(mCompileSucceeded));
|
runtime.set("compileSucceeded", JsonValue(mCompileSucceeded));
|
||||||
runtime.set("compileMessage", JsonValue(mCompileMessage));
|
runtime.set("compileMessage", JsonValue(mCompileMessage));
|
||||||
runtime.set("mixAmount", JsonValue(mMixAmount));
|
|
||||||
runtime.set("bypass", JsonValue(mBypass));
|
|
||||||
root.set("runtime", runtime);
|
root.set("runtime", runtime);
|
||||||
|
|
||||||
JsonValue video = JsonValue::MakeObject();
|
JsonValue video = JsonValue::MakeObject();
|
||||||
@@ -1174,23 +1394,49 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
||||||
root.set("performance", performance);
|
root.set("performance", performance);
|
||||||
|
|
||||||
JsonValue shaders = JsonValue::MakeArray();
|
JsonValue shaderLibrary = JsonValue::MakeArray();
|
||||||
for (const std::string& shaderId : mPackageOrder)
|
for (const std::string& shaderId : mPackageOrder)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(shaderId);
|
auto shaderIt = mPackagesById.find(shaderId);
|
||||||
if (shaderIt == mPackagesById.end())
|
if (shaderIt == mPackagesById.end())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
|
||||||
JsonValue shader = JsonValue::MakeObject();
|
JsonValue shader = JsonValue::MakeObject();
|
||||||
shader.set("id", JsonValue(shaderPackage.id));
|
shader.set("id", JsonValue(shaderIt->second.id));
|
||||||
shader.set("name", JsonValue(shaderPackage.displayName));
|
shader.set("name", JsonValue(shaderIt->second.displayName));
|
||||||
shader.set("description", JsonValue(shaderPackage.description));
|
shader.set("description", JsonValue(shaderIt->second.description));
|
||||||
shader.set("category", JsonValue(shaderPackage.category));
|
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();
|
JsonValue parameters = JsonValue::MakeArray();
|
||||||
auto persistedIt = mPersistentState.parameterValuesByShader.find(shaderPackage.id);
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
|
||||||
{
|
{
|
||||||
JsonValue parameter = JsonValue::MakeObject();
|
JsonValue parameter = JsonValue::MakeObject();
|
||||||
parameter.set("id", JsonValue(definition.id));
|
parameter.set("id", JsonValue(definition.id));
|
||||||
@@ -1218,7 +1464,6 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
stepValue.pushBack(JsonValue(number));
|
stepValue.pushBack(JsonValue(number));
|
||||||
parameter.set("step", stepValue);
|
parameter.set("step", stepValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
if (definition.type == ShaderParameterType::Enum)
|
||||||
{
|
{
|
||||||
JsonValue options = JsonValue::MakeArray();
|
JsonValue options = JsonValue::MakeArray();
|
||||||
@@ -1233,22 +1478,119 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||||
if (persistedIt != mPersistentState.parameterValuesByShader.end())
|
auto valueIt = layer.parameterValues.find(definition.id);
|
||||||
{
|
if (valueIt != layer.parameterValues.end())
|
||||||
auto valueIt = persistedIt->second.find(definition.id);
|
|
||||||
if (valueIt != persistedIt->second.end())
|
|
||||||
value = valueIt->second;
|
value = valueIt->second;
|
||||||
}
|
|
||||||
parameter.set("value", SerializeParameterValue(definition, value));
|
parameter.set("value", SerializeParameterValue(definition, value));
|
||||||
parameters.pushBack(parameter);
|
parameters.pushBack(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
shader.set("parameters", parameters);
|
layerValue.set("parameters", parameters);
|
||||||
shaders.pushBack(shader);
|
layers.pushBack(layerValue);
|
||||||
}
|
}
|
||||||
root.set("shaders", shaders);
|
return layers;
|
||||||
|
}
|
||||||
|
|
||||||
return root;
|
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
|
JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const
|
||||||
@@ -1272,3 +1614,28 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
|
|||||||
}
|
}
|
||||||
return JsonValue();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ struct ShaderPackage
|
|||||||
|
|
||||||
struct RuntimeRenderState
|
struct RuntimeRenderState
|
||||||
{
|
{
|
||||||
std::string activeShaderId;
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
double timeSeconds = 0.0;
|
double timeSeconds = 0.0;
|
||||||
@@ -86,19 +87,24 @@ public:
|
|||||||
bool ManualReloadRequested();
|
bool ManualReloadRequested();
|
||||||
void ClearReloadRequest();
|
void ClearReloadRequest();
|
||||||
|
|
||||||
bool SelectShader(const std::string& shaderId, std::string& error);
|
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||||
bool UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||||
bool ResetParameters(const std::string& shaderId, std::string& error);
|
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||||
bool SetBypass(bool bypassEnabled, std::string& error);
|
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||||
bool SetMixAmount(double mixAmount, std::string& error);
|
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||||
|
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||||
|
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
||||||
|
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||||
|
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
||||||
|
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||||
|
|
||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
void AdvanceFrame();
|
void AdvanceFrame();
|
||||||
|
|
||||||
bool BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error);
|
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
||||||
RuntimeRenderState GetRenderState(unsigned outputWidth, unsigned outputHeight) const;
|
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||||
std::string BuildStateJson() const;
|
std::string BuildStateJson() const;
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||||
@@ -116,12 +122,17 @@ private:
|
|||||||
bool autoReload = true;
|
bool autoReload = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LayerPersistentState
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string shaderId;
|
||||||
|
bool bypass = false;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
|
};
|
||||||
|
|
||||||
struct PersistentState
|
struct PersistentState
|
||||||
{
|
{
|
||||||
std::string activeShaderId;
|
std::vector<LayerPersistentState> layers;
|
||||||
double mixAmount = 1.0;
|
|
||||||
bool bypass = false;
|
|
||||||
std::map<std::string, std::map<std::string, ShaderParameterValue>> parameterValuesByShader;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bool LoadConfig(std::string& error);
|
bool LoadConfig(std::string& error);
|
||||||
@@ -131,7 +142,7 @@ private:
|
|||||||
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||||
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
||||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
||||||
void EnsureParameterDefaultsLocked(ShaderPackage& shaderPackage);
|
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
|
||||||
std::string BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const;
|
std::string BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const;
|
||||||
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
|
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
|
||||||
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
|
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
|
||||||
@@ -140,7 +151,14 @@ private:
|
|||||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||||
bool ResolvePaths(std::string& error);
|
bool ResolvePaths(std::string& error);
|
||||||
JsonValue BuildStateValue() const;
|
JsonValue BuildStateValue() const;
|
||||||
|
JsonValue SerializeLayerStackLocked() const;
|
||||||
|
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||||
|
std::vector<std::string> GetStackPresetNamesLocked() const;
|
||||||
|
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
||||||
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
||||||
|
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||||
|
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||||
|
std::string GenerateLayerId();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
@@ -150,6 +168,7 @@ private:
|
|||||||
std::filesystem::path mUiRoot;
|
std::filesystem::path mUiRoot;
|
||||||
std::filesystem::path mShaderRoot;
|
std::filesystem::path mShaderRoot;
|
||||||
std::filesystem::path mRuntimeRoot;
|
std::filesystem::path mRuntimeRoot;
|
||||||
|
std::filesystem::path mPresetRoot;
|
||||||
std::filesystem::path mRuntimeStatePath;
|
std::filesystem::path mRuntimeStatePath;
|
||||||
std::filesystem::path mConfigPath;
|
std::filesystem::path mConfigPath;
|
||||||
std::filesystem::path mWrapperPath;
|
std::filesystem::path mWrapperPath;
|
||||||
@@ -157,7 +176,6 @@ private:
|
|||||||
std::filesystem::path mPatchedGlslPath;
|
std::filesystem::path mPatchedGlslPath;
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
std::vector<std::string> mPackageOrder;
|
std::vector<std::string> mPackageOrder;
|
||||||
std::string mActiveShaderId;
|
|
||||||
bool mReloadRequested;
|
bool mReloadRequested;
|
||||||
bool mCompileSucceeded;
|
bool mCompileSucceeded;
|
||||||
std::string mCompileMessage;
|
std::string mCompileMessage;
|
||||||
@@ -170,9 +188,8 @@ private:
|
|||||||
double mSmoothedRenderMilliseconds;
|
double mSmoothedRenderMilliseconds;
|
||||||
unsigned short mServerPort;
|
unsigned short mServerPort;
|
||||||
bool mAutoReloadEnabled;
|
bool mAutoReloadEnabled;
|
||||||
double mMixAmount;
|
|
||||||
bool mBypass;
|
|
||||||
std::chrono::steady_clock::time_point mStartTime;
|
std::chrono::steady_clock::time_point mStartTime;
|
||||||
std::chrono::steady_clock::time_point mLastScanTime;
|
std::chrono::steady_clock::time_point mLastScanTime;
|
||||||
uint64_t mFrameCounter;
|
uint64_t mFrameCounter;
|
||||||
|
uint64_t mNextLayerId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,33 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 0.5,
|
"max": 0.5,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bloomAmount",
|
||||||
|
"label": "Bloom",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.18,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 0.6,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fadeAmount",
|
||||||
|
"label": "Fade",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.22,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 0.75,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "noiseAmount",
|
||||||
|
"label": "Noise",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.055,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 0.2,
|
||||||
|
"step": 0.005
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,38 @@ float3 yiq2rgb(float3 c)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float noiseHash(float2 p)
|
||||||
|
{
|
||||||
|
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 chromaSpeckle(float2 uv, float framecount)
|
||||||
|
{
|
||||||
|
float2 coarseUv = floor(uv);
|
||||||
|
float r = noiseHash(coarseUv + float2(framecount * 19.0, framecount * 11.0));
|
||||||
|
float g = noiseHash(coarseUv + float2(framecount * 23.0 + 17.0, framecount * 7.0 + 31.0));
|
||||||
|
float b = noiseHash(coarseUv + float2(framecount * 13.0 + 47.0, framecount * 29.0 + 9.0));
|
||||||
|
return float3(r, g, b) - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
||||||
|
{
|
||||||
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
||||||
|
float2 dx = float2(pixel.x * radius, 0.0);
|
||||||
|
float2 dy = float2(0.0, pixel.y * radius);
|
||||||
|
|
||||||
|
float3 sum = sampleVideo(frac(uv)).rgb * 0.28;
|
||||||
|
sum += sampleVideo(frac(uv + dx)).rgb * 0.14;
|
||||||
|
sum += sampleVideo(frac(uv - dx)).rgb * 0.14;
|
||||||
|
sum += sampleVideo(frac(uv + dy)).rgb * 0.14;
|
||||||
|
sum += sampleVideo(frac(uv - dy)).rgb * 0.14;
|
||||||
|
sum += sampleVideo(frac(uv + dx + dy)).rgb * 0.075;
|
||||||
|
sum += sampleVideo(frac(uv + dx - dy)).rgb * 0.075;
|
||||||
|
sum += sampleVideo(frac(uv - dx + dy)).rgb * 0.075;
|
||||||
|
sum += sampleVideo(frac(uv - dx - dy)).rgb * 0.075;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
float3 blurVhs(float2 uv, float d, int sampleCount)
|
float3 blurVhs(float2 uv, float d, int sampleCount)
|
||||||
{
|
{
|
||||||
float3 sum = float3(0.0, 0.0, 0.0);
|
float3 sum = float3(0.0, 0.0, 0.0);
|
||||||
@@ -107,6 +139,26 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
||||||
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
||||||
|
|
||||||
|
float3 bloomSource = softBloom(context.uv, context.outputResolution, 2.0 + smear * 2.5);
|
||||||
|
float bloomLuma = dot(bloomSource, float3(0.299, 0.587, 0.114));
|
||||||
|
float bloomMask = smoothstep(0.32, 1.0, bloomLuma) * bloomAmount;
|
||||||
|
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
||||||
|
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
||||||
|
|
||||||
|
float2 noiseUv = context.uv * context.outputResolution * float2(0.55, 1.1);
|
||||||
|
float3 speckle = chromaSpeckle(noiseUv, framecount);
|
||||||
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
|
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
||||||
|
float3 chromaNoise = float3(speckle.x * 1.1, speckle.y * 0.45, speckle.z * 1.2);
|
||||||
|
color += chromaNoise * noiseAmount * noiseMask;
|
||||||
|
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.18, 0.35);
|
||||||
|
color.b = lerp(color.b, color.b + speckle.z * noiseAmount * 0.22, 0.45);
|
||||||
|
|
||||||
|
float3 grayscale = float3(luma, luma, luma);
|
||||||
|
color = lerp(color, grayscale, fadeAmount * 0.18);
|
||||||
|
color = color * (1.0 - fadeAmount * 0.08) + float3(0.055, 0.055, 0.065) * fadeAmount;
|
||||||
|
color = lerp(color, softBloom(context.uv, context.outputResolution, 1.0 + smear), fadeAmount * 0.12);
|
||||||
|
|
||||||
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
|
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
|
||||||
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
|
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
|
||||||
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
|
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
|
||||||
|
|||||||
232
ui/app.js
232
ui/app.js
@@ -1,232 +0,0 @@
|
|||||||
const shaderSelect = document.getElementById("shader-select");
|
|
||||||
const mixSlider = document.getElementById("mix-slider");
|
|
||||||
const bypassToggle = document.getElementById("bypass-toggle");
|
|
||||||
const reloadButton = document.getElementById("reload-button");
|
|
||||||
const resetParametersButton = document.getElementById("reset-parameters-button");
|
|
||||||
const runtimeStatus = document.getElementById("runtime-status");
|
|
||||||
const videoStatus = document.getElementById("video-status");
|
|
||||||
const compileStatus = document.getElementById("compile-status");
|
|
||||||
const parameterForm = document.getElementById("parameter-form");
|
|
||||||
|
|
||||||
let appState = null;
|
|
||||||
let websocket = null;
|
|
||||||
|
|
||||||
function createKv(target, values) {
|
|
||||||
target.innerHTML = "";
|
|
||||||
values.forEach(([key, value]) => {
|
|
||||||
const dt = document.createElement("dt");
|
|
||||||
dt.textContent = key;
|
|
||||||
const dd = document.createElement("dd");
|
|
||||||
dd.textContent = value;
|
|
||||||
target.append(dt, dd);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function postJson(path, payload) {
|
|
||||||
return fetch(path, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderParameters(shader) {
|
|
||||||
parameterForm.innerHTML = "";
|
|
||||||
if (!shader) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader.parameters.forEach((parameter) => {
|
|
||||||
const section = document.createElement("section");
|
|
||||||
section.className = "parameter";
|
|
||||||
|
|
||||||
const label = document.createElement("label");
|
|
||||||
label.textContent = parameter.label;
|
|
||||||
section.appendChild(label);
|
|
||||||
|
|
||||||
const valueLabel = document.createElement("div");
|
|
||||||
valueLabel.className = "parameter__value";
|
|
||||||
|
|
||||||
const sendValue = (value) => {
|
|
||||||
postJson("/api/update-parameter", {
|
|
||||||
shaderId: shader.id,
|
|
||||||
parameterId: parameter.id,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parameter.type === "float") {
|
|
||||||
const range = document.createElement("input");
|
|
||||||
range.type = "range";
|
|
||||||
range.min = parameter.min?.[0] ?? 0;
|
|
||||||
range.max = parameter.max?.[0] ?? 1;
|
|
||||||
range.step = parameter.step?.[0] ?? 0.01;
|
|
||||||
range.value = parameter.value;
|
|
||||||
const number = document.createElement("input");
|
|
||||||
number.type = "number";
|
|
||||||
number.min = range.min;
|
|
||||||
number.max = range.max;
|
|
||||||
number.step = range.step;
|
|
||||||
number.value = parameter.value;
|
|
||||||
const pair = document.createElement("div");
|
|
||||||
pair.className = "parameter__pair";
|
|
||||||
pair.append(range, number);
|
|
||||||
section.append(pair, valueLabel);
|
|
||||||
const update = (value) => {
|
|
||||||
valueLabel.textContent = Number(value).toFixed(3);
|
|
||||||
range.value = value;
|
|
||||||
number.value = value;
|
|
||||||
};
|
|
||||||
update(parameter.value);
|
|
||||||
range.addEventListener("input", () => update(range.value));
|
|
||||||
range.addEventListener("change", () => sendValue(Number(range.value)));
|
|
||||||
number.addEventListener("change", () => {
|
|
||||||
update(number.value);
|
|
||||||
sendValue(Number(number.value));
|
|
||||||
});
|
|
||||||
} else if (parameter.type === "vec2" || parameter.type === "color") {
|
|
||||||
const pair = document.createElement("div");
|
|
||||||
pair.className = "parameter__pair";
|
|
||||||
const values = parameter.value.slice();
|
|
||||||
values.forEach((component, index) => {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "number";
|
|
||||||
input.step = parameter.step?.[index] ?? 0.01;
|
|
||||||
input.min = parameter.min?.[index] ?? "";
|
|
||||||
input.max = parameter.max?.[index] ?? "";
|
|
||||||
input.value = component;
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
values[index] = Number(input.value);
|
|
||||||
valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", ");
|
|
||||||
sendValue(values);
|
|
||||||
});
|
|
||||||
pair.appendChild(input);
|
|
||||||
});
|
|
||||||
valueLabel.textContent = values.map((value) => Number(value).toFixed(3)).join(", ");
|
|
||||||
section.append(pair, valueLabel);
|
|
||||||
} else if (parameter.type === "bool") {
|
|
||||||
const toggle = document.createElement("input");
|
|
||||||
toggle.type = "checkbox";
|
|
||||||
toggle.checked = parameter.value;
|
|
||||||
valueLabel.textContent = parameter.value ? "Enabled" : "Disabled";
|
|
||||||
toggle.addEventListener("change", () => {
|
|
||||||
valueLabel.textContent = toggle.checked ? "Enabled" : "Disabled";
|
|
||||||
sendValue(toggle.checked);
|
|
||||||
});
|
|
||||||
section.append(toggle, valueLabel);
|
|
||||||
} else if (parameter.type === "enum") {
|
|
||||||
const select = document.createElement("select");
|
|
||||||
parameter.options.forEach((option) => {
|
|
||||||
const item = document.createElement("option");
|
|
||||||
item.value = option.value;
|
|
||||||
item.textContent = option.label;
|
|
||||||
if (option.value === parameter.value) {
|
|
||||||
item.selected = true;
|
|
||||||
}
|
|
||||||
select.appendChild(item);
|
|
||||||
});
|
|
||||||
valueLabel.textContent = parameter.value;
|
|
||||||
select.addEventListener("change", () => {
|
|
||||||
valueLabel.textContent = select.value;
|
|
||||||
sendValue(select.value);
|
|
||||||
});
|
|
||||||
section.append(select, valueLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
parameterForm.appendChild(section);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderState(state) {
|
|
||||||
appState = state;
|
|
||||||
const shaders = state.shaders || [];
|
|
||||||
const activeShaderId = state.runtime.activeShaderId;
|
|
||||||
const activeShader = shaders.find((shader) => shader.id === activeShaderId) || shaders[0];
|
|
||||||
const performance = state.performance || {};
|
|
||||||
const frameBudgetMs = Number(performance.frameBudgetMs || 0);
|
|
||||||
const renderMs = Number(performance.renderMs || 0);
|
|
||||||
const smoothedRenderMs = Number(performance.smoothedRenderMs || 0);
|
|
||||||
const budgetUsedPercent = Number(performance.budgetUsedPercent || 0);
|
|
||||||
|
|
||||||
shaderSelect.innerHTML = "";
|
|
||||||
shaders.forEach((shader) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = shader.id;
|
|
||||||
option.textContent = shader.name;
|
|
||||||
if (shader.id === activeShaderId) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
shaderSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
mixSlider.value = state.runtime.mixAmount ?? 1;
|
|
||||||
bypassToggle.checked = Boolean(state.runtime.bypass);
|
|
||||||
compileStatus.textContent = state.runtime.compileMessage || "No compiler output.";
|
|
||||||
resetParametersButton.disabled = !activeShader || activeShader.parameters.length === 0;
|
|
||||||
|
|
||||||
createKv(runtimeStatus, [
|
|
||||||
["Active Shader", activeShader?.name || "None"],
|
|
||||||
["Auto Reload", state.app.autoReload ? "On" : "Off"],
|
|
||||||
["Control URL", `http://127.0.0.1:${state.app.serverPort}`],
|
|
||||||
["Compile Status", state.runtime.compileSucceeded ? "Ready" : "Error"],
|
|
||||||
["Render Time", `${renderMs.toFixed(2)} ms`],
|
|
||||||
["Smoothed Time", `${smoothedRenderMs.toFixed(2)} ms`],
|
|
||||||
["Frame Budget", `${frameBudgetMs.toFixed(2)} ms`],
|
|
||||||
["Budget Used", `${budgetUsedPercent.toFixed(1)}%`],
|
|
||||||
]);
|
|
||||||
|
|
||||||
createKv(videoStatus, [
|
|
||||||
["Signal", state.video.hasSignal ? "Present" : "Missing"],
|
|
||||||
["Mode", state.video.modeName || "Unknown"],
|
|
||||||
["Resolution", `${state.video.width || 0} x ${state.video.height || 0}`],
|
|
||||||
]);
|
|
||||||
|
|
||||||
renderParameters(activeShader);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInitialState() {
|
|
||||||
const response = await fetch("/api/state");
|
|
||||||
renderState(await response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectWebSocket() {
|
|
||||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
|
||||||
websocket = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
||||||
websocket.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
renderState(JSON.parse(event.data));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to parse state update", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
websocket.onclose = () => {
|
|
||||||
setTimeout(connectWebSocket, 1000);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderSelect.addEventListener("change", () => {
|
|
||||||
postJson("/api/select-shader", { shaderId: shaderSelect.value });
|
|
||||||
});
|
|
||||||
|
|
||||||
mixSlider.addEventListener("change", () => {
|
|
||||||
postJson("/api/set-mix", { mixAmount: Number(mixSlider.value) });
|
|
||||||
});
|
|
||||||
|
|
||||||
bypassToggle.addEventListener("change", () => {
|
|
||||||
postJson("/api/set-bypass", { bypass: bypassToggle.checked });
|
|
||||||
});
|
|
||||||
|
|
||||||
reloadButton.addEventListener("click", () => {
|
|
||||||
postJson("/api/reload", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
resetParametersButton.addEventListener("click", () => {
|
|
||||||
const activeShaderId = appState?.runtime?.activeShaderId;
|
|
||||||
if (!activeShaderId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
postJson("/api/reset-parameters", { shaderId: activeShaderId });
|
|
||||||
});
|
|
||||||
|
|
||||||
loadInitialState().then(connectWebSocket);
|
|
||||||
@@ -1,53 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Video Shader Host</title>
|
<title>Video Shader Host</title>
|
||||||
<link rel="stylesheet" href="/styles.css">
|
</head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<div id="root"></div>
|
||||||
<main class="layout">
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
<section class="toolbar">
|
</body>
|
||||||
<div class="toolbar__group">
|
|
||||||
<label for="shader-select">Shader</label>
|
|
||||||
<select id="shader-select"></select>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar__group toolbar__group--wide">
|
|
||||||
<label for="mix-slider">Mix</label>
|
|
||||||
<input id="mix-slider" type="range" min="0" max="1" step="0.01">
|
|
||||||
</div>
|
|
||||||
<label class="toggle">
|
|
||||||
<input id="bypass-toggle" type="checkbox">
|
|
||||||
<span>Bypass</span>
|
|
||||||
</label>
|
|
||||||
<button id="reload-button" type="button">Reload Shader</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="status-grid">
|
|
||||||
<div class="panel">
|
|
||||||
<h2>Runtime</h2>
|
|
||||||
<dl id="runtime-status" class="kv"></dl>
|
|
||||||
</div>
|
|
||||||
<div class="panel">
|
|
||||||
<h2>Video</h2>
|
|
||||||
<dl id="video-status" class="kv"></dl>
|
|
||||||
</div>
|
|
||||||
<div class="panel panel--full">
|
|
||||||
<h2>Compiler</h2>
|
|
||||||
<pre id="compile-status"></pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel__header">
|
|
||||||
<h2>Parameters</h2>
|
|
||||||
<button id="reset-parameters-button" type="button">Reset Parameters</button>
|
|
||||||
</div>
|
|
||||||
<form id="parameter-form" class="parameter-grid"></form>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script src="/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
1726
ui/package-lock.json
generated
Normal file
1726
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
ui/package.json
Normal file
20
ui/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "video-shader-control-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.511.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
639
ui/src/App.jsx
Normal file
639
ui/src/App.jsx
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { GripVertical, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
function KvList({ values }) {
|
||||||
|
return (
|
||||||
|
<dl className="kv">
|
||||||
|
{values.map(([key, value]) => (
|
||||||
|
<FragmentRow key={key} label={key} value={value} />
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FragmentRow({ label, value }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<dt>{label}</dt>
|
||||||
|
<dd>{value}</dd>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function postJson(path, payload) {
|
||||||
|
return fetch(path, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveItem(array, fromIndex, toIndex) {
|
||||||
|
if (fromIndex < 0 || fromIndex >= array.length || toIndex < 0 || toIndex >= array.length) {
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = [...array];
|
||||||
|
const [item] = copy.splice(fromIndex, 1);
|
||||||
|
copy.splice(toIndex, 0, item);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value, digits = 3) {
|
||||||
|
return Number(value ?? 0).toFixed(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatParameterValue(parameterType, value) {
|
||||||
|
if (parameterType === "float") {
|
||||||
|
return formatNumber(value);
|
||||||
|
}
|
||||||
|
if (parameterType === "vec2" || parameterType === "color") {
|
||||||
|
return (value ?? []).map((item) => formatNumber(item)).join(", ");
|
||||||
|
}
|
||||||
|
if (parameterType === "bool") {
|
||||||
|
return value ? "Enabled" : "Disabled";
|
||||||
|
}
|
||||||
|
return `${value ?? ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valuesMatch(left, right) {
|
||||||
|
return JSON.stringify(left) === JSON.stringify(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParameterField({ parameter, onParameterChange }) {
|
||||||
|
const [draftValue, setDraftValue] = useState(parameter.value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraftValue(parameter.value);
|
||||||
|
}, [parameter.value]);
|
||||||
|
|
||||||
|
const sendValue = (value) => {
|
||||||
|
setDraftValue(value);
|
||||||
|
onParameterChange(parameter.id, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = <label>{parameter.label}</label>;
|
||||||
|
const isPending = !valuesMatch(draftValue, parameter.value);
|
||||||
|
const appliedValueText = formatParameterValue(parameter.type, parameter.value);
|
||||||
|
|
||||||
|
if (parameter.type === "float") {
|
||||||
|
return (
|
||||||
|
<section className="parameter">
|
||||||
|
{label}
|
||||||
|
<div className="parameter__pair">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={parameter.min?.[0] ?? 0}
|
||||||
|
max={parameter.max?.[0] ?? 1}
|
||||||
|
step={parameter.step?.[0] ?? 0.01}
|
||||||
|
value={draftValue}
|
||||||
|
onChange={(event) => sendValue(Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={parameter.min?.[0] ?? ""}
|
||||||
|
max={parameter.max?.[0] ?? ""}
|
||||||
|
step={parameter.step?.[0] ?? 0.01}
|
||||||
|
value={draftValue}
|
||||||
|
onChange={(event) => sendValue(Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
||||||
|
{isPending ? `Applied: ${appliedValueText}` : appliedValueText}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.type === "vec2" || parameter.type === "color") {
|
||||||
|
const componentCount = parameter.type === "color" ? 4 : 2;
|
||||||
|
const values = [...(draftValue ?? [])];
|
||||||
|
while (values.length < componentCount) {
|
||||||
|
values.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="parameter">
|
||||||
|
{label}
|
||||||
|
<div className="parameter__pair">
|
||||||
|
{Array.from({ length: componentCount }, (_, index) => (
|
||||||
|
<input
|
||||||
|
key={index}
|
||||||
|
type="number"
|
||||||
|
min={parameter.min?.[index] ?? ""}
|
||||||
|
max={parameter.max?.[index] ?? ""}
|
||||||
|
step={parameter.step?.[index] ?? 0.01}
|
||||||
|
value={values[index]}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = [...values];
|
||||||
|
next[index] = Number(event.target.value);
|
||||||
|
sendValue(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
||||||
|
{isPending ? `Applied: ${appliedValueText}` : appliedValueText}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.type === "bool") {
|
||||||
|
return (
|
||||||
|
<section className="parameter">
|
||||||
|
{label}
|
||||||
|
<label className="toggle toggle--field">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(draftValue)}
|
||||||
|
onChange={(event) => sendValue(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>{draftValue ? "Enabled" : "Disabled"}</span>
|
||||||
|
</label>
|
||||||
|
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
||||||
|
{isPending ? `Applied: ${appliedValueText}` : appliedValueText}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.type === "enum") {
|
||||||
|
return (
|
||||||
|
<section className="parameter">
|
||||||
|
{label}
|
||||||
|
<select value={draftValue} onChange={(event) => sendValue(event.target.value)}>
|
||||||
|
{parameter.options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
||||||
|
{isPending ? `Applied: ${appliedValueText}` : appliedValueText}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LayerCard({
|
||||||
|
layer,
|
||||||
|
index,
|
||||||
|
shaders,
|
||||||
|
expanded,
|
||||||
|
isDragging,
|
||||||
|
isDropTarget,
|
||||||
|
onToggleExpanded,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onDragOver,
|
||||||
|
onDrop,
|
||||||
|
onRemove,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`layer-card${expanded ? " layer-card--expanded" : ""}${isDragging ? " layer-card--dragging" : ""}${isDropTarget ? " layer-card--drop-target" : ""}`}
|
||||||
|
onDragOver={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onDragOver(layer.id);
|
||||||
|
}}
|
||||||
|
onDrop={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onDrop(event, layer.id, index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="layer-card__header">
|
||||||
|
<div className="layer-card__meta">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="layer-card__drag-handle"
|
||||||
|
title="Drag to reorder"
|
||||||
|
aria-label="Drag to reorder"
|
||||||
|
draggable
|
||||||
|
onDragStart={(event) => {
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
event.dataTransfer.setData("text/plain", layer.id);
|
||||||
|
event.stopPropagation();
|
||||||
|
onDragStart(layer.id);
|
||||||
|
}}
|
||||||
|
onDragEnd={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onDragEnd();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GripVertical size={16} strokeWidth={1.75} />
|
||||||
|
</button>
|
||||||
|
<span className="layer-card__index">{index + 1}</span>
|
||||||
|
<button type="button" className="layer-card__title" onClick={() => onToggleExpanded(layer.id)}>
|
||||||
|
{layer.shaderName}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="layer-card__actions">
|
||||||
|
<label className="toggle toggle--compact">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(layer.bypass)}
|
||||||
|
onChange={(event) =>
|
||||||
|
postJson("/api/layers/set-bypass", {
|
||||||
|
layerId: layer.id,
|
||||||
|
bypass: event.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Bypass</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" onClick={() => onToggleExpanded(layer.id)}>
|
||||||
|
{expanded ? "Hide" : "Controls"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
title="Remove layer"
|
||||||
|
aria-label="Remove layer"
|
||||||
|
onClick={() => onRemove(layer.id)}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} strokeWidth={1.75} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded ? (
|
||||||
|
<div className="layer-card__body">
|
||||||
|
<div className="layer-card__field">
|
||||||
|
<label htmlFor={`shader-${layer.id}`}>Shader</label>
|
||||||
|
<select
|
||||||
|
id={`shader-${layer.id}`}
|
||||||
|
value={layer.shaderId}
|
||||||
|
onChange={(event) =>
|
||||||
|
postJson("/api/layers/set-shader", {
|
||||||
|
layerId: layer.id,
|
||||||
|
shaderId: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shaders.map((shader) => (
|
||||||
|
<option key={shader.id} value={shader.id}>
|
||||||
|
{shader.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="layer-card__subheader">
|
||||||
|
<h3>Parameters</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={layer.parameters.length === 0}
|
||||||
|
onClick={() => postJson("/api/layers/reset-parameters", { layerId: layer.id })}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{layer.parameters.length > 0 ? (
|
||||||
|
<div className="parameter-grid">
|
||||||
|
{layer.parameters.map((parameter) => (
|
||||||
|
<ParameterField
|
||||||
|
key={`${layer.id}:${parameter.id}:${JSON.stringify(parameter.value)}`}
|
||||||
|
parameter={parameter}
|
||||||
|
onParameterChange={(parameterId, value) =>
|
||||||
|
updateLayerParameterOptimistically(layer.id, parameterId, value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="muted">This shader does not expose any user parameters.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [appState, setAppState] = useState(null);
|
||||||
|
const [pendingShaderId, setPendingShaderId] = useState("");
|
||||||
|
const [presetName, setPresetName] = useState("");
|
||||||
|
const [selectedPresetName, setSelectedPresetName] = useState("");
|
||||||
|
const [expandedLayerIds, setExpandedLayerIds] = useState([]);
|
||||||
|
const [dragLayerId, setDragLayerId] = useState(null);
|
||||||
|
const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
let socket;
|
||||||
|
let retryTimer;
|
||||||
|
|
||||||
|
const connectWebSocket = () => {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
socket = new WebSocket(`${protocol}://${window.location.host}/ws`);
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const nextState = JSON.parse(event.data);
|
||||||
|
if (mounted) {
|
||||||
|
setAppState(nextState);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse state update", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.onclose = () => {
|
||||||
|
if (mounted) {
|
||||||
|
retryTimer = window.setTimeout(connectWebSocket, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("/api/state")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((state) => {
|
||||||
|
if (mounted) {
|
||||||
|
setAppState(state);
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to load initial state", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
if (retryTimer) {
|
||||||
|
window.clearTimeout(retryTimer);
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const layers = appState?.layers ?? [];
|
||||||
|
const shaders = appState?.shaders ?? [];
|
||||||
|
const performance = appState?.performance ?? {};
|
||||||
|
const runtime = appState?.runtime ?? {};
|
||||||
|
const video = appState?.video ?? {};
|
||||||
|
const app = appState?.app ?? {};
|
||||||
|
const stackPresets = appState?.stackPresets ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingShaderId && shaders.length > 0) {
|
||||||
|
setPendingShaderId(shaders[0].id);
|
||||||
|
} else if (pendingShaderId && !shaders.some((shader) => shader.id === pendingShaderId)) {
|
||||||
|
setPendingShaderId(shaders[0]?.id ?? "");
|
||||||
|
}
|
||||||
|
}, [pendingShaderId, shaders]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPresetName && stackPresets.length > 0) {
|
||||||
|
setSelectedPresetName(stackPresets[0]);
|
||||||
|
} else if (selectedPresetName && !stackPresets.includes(selectedPresetName)) {
|
||||||
|
setSelectedPresetName(stackPresets[0] ?? "");
|
||||||
|
}
|
||||||
|
}, [selectedPresetName, stackPresets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const layerIds = new Set(layers.map((layer) => layer.id));
|
||||||
|
setExpandedLayerIds((current) => current.filter((layerId) => layerIds.has(layerId)));
|
||||||
|
}, [layers]);
|
||||||
|
|
||||||
|
const expandedSet = useMemo(() => new Set(expandedLayerIds), [expandedLayerIds]);
|
||||||
|
|
||||||
|
function updateLayerParameterOptimistically(layerId, parameterId, value) {
|
||||||
|
postJson("/api/layers/update-parameter", {
|
||||||
|
layerId,
|
||||||
|
parameterId,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpanded(layerId) {
|
||||||
|
setExpandedLayerIds((current) =>
|
||||||
|
current.includes(layerId) ? current.filter((id) => id !== layerId) : [...current, layerId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLayer(layerId) {
|
||||||
|
setExpandedLayerIds((current) => current.filter((id) => id !== layerId));
|
||||||
|
postJson("/api/layers/remove", { layerId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event, targetLayerId, targetIndex) {
|
||||||
|
const sourceLayerId = event.dataTransfer.getData("text/plain") || dragLayerId;
|
||||||
|
if (!sourceLayerId || sourceLayerId === targetLayerId) {
|
||||||
|
setDragLayerId(null);
|
||||||
|
setDropTargetLayerId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState((current) => {
|
||||||
|
if (!current?.layers) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceIndex = current.layers.findIndex((layer) => layer.id === sourceLayerId);
|
||||||
|
const destinationIndex = current.layers.findIndex((layer) => layer.id === targetLayerId);
|
||||||
|
if (sourceIndex < 0 || destinationIndex < 0 || sourceIndex === destinationIndex) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
layers: moveItem(current.layers, sourceIndex, destinationIndex),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
postJson("/api/layers/reorder", {
|
||||||
|
layerId: sourceLayerId,
|
||||||
|
targetIndex,
|
||||||
|
});
|
||||||
|
setDragLayerId(null);
|
||||||
|
setDropTargetLayerId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appState) {
|
||||||
|
return (
|
||||||
|
<main className="layout">
|
||||||
|
<section className="panel">
|
||||||
|
<h2>Loading</h2>
|
||||||
|
<p className="muted">Waiting for control state from the native host.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="layout">
|
||||||
|
<section className="toolbar">
|
||||||
|
<div className="toolbar__group toolbar__group--wide">
|
||||||
|
<label htmlFor="preset-name">Save Stack</label>
|
||||||
|
<div className="toolbar__inline">
|
||||||
|
<input
|
||||||
|
id="preset-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Preset name"
|
||||||
|
value={presetName}
|
||||||
|
onChange={(event) => setPresetName(event.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!presetName.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
const trimmedName = presetName.trim();
|
||||||
|
if (!trimmedName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postJson("/api/stack-presets/save", { presetName: trimmedName });
|
||||||
|
setSelectedPresetName(trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toolbar__group toolbar__group--wide">
|
||||||
|
<label htmlFor="preset-select">Recall Stack</label>
|
||||||
|
<div className="toolbar__inline">
|
||||||
|
<select
|
||||||
|
id="preset-select"
|
||||||
|
value={selectedPresetName}
|
||||||
|
onChange={(event) => setSelectedPresetName(event.target.value)}
|
||||||
|
>
|
||||||
|
{stackPresets.length === 0 ? <option value="">No presets</option> : null}
|
||||||
|
{stackPresets.map((preset) => (
|
||||||
|
<option key={preset} value={preset}>
|
||||||
|
{preset}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!selectedPresetName}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedPresetName) {
|
||||||
|
postJson("/api/stack-presets/load", { presetName: selectedPresetName });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recall
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" onClick={() => postJson("/api/reload", {})}>
|
||||||
|
Reload Shader
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="status-grid">
|
||||||
|
<div className="panel">
|
||||||
|
<h2>Runtime</h2>
|
||||||
|
<KvList
|
||||||
|
values={[
|
||||||
|
["Layer Count", `${runtime.layerCount || 0}`],
|
||||||
|
["Auto Reload", app.autoReload ? "On" : "Off"],
|
||||||
|
["Control URL", `http://127.0.0.1:${app.serverPort}`],
|
||||||
|
["Compile Status", runtime.compileSucceeded ? "Ready" : "Error"],
|
||||||
|
["Render Time", `${formatNumber(performance.renderMs, 2)} ms`],
|
||||||
|
["Smoothed Time", `${formatNumber(performance.smoothedRenderMs, 2)} ms`],
|
||||||
|
["Frame Budget", `${formatNumber(performance.frameBudgetMs, 2)} ms`],
|
||||||
|
["Budget Used", `${formatNumber(performance.budgetUsedPercent, 1)}%`],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="panel">
|
||||||
|
<h2>Video</h2>
|
||||||
|
<KvList
|
||||||
|
values={[
|
||||||
|
["Signal", video.hasSignal ? "Present" : "Missing"],
|
||||||
|
["Mode", video.modeName || "Unknown"],
|
||||||
|
["Resolution", `${video.width || 0} x ${video.height || 0}`],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="panel panel--full">
|
||||||
|
<h2>Compiler</h2>
|
||||||
|
<pre>{runtime.compileMessage || "No compiler output."}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel">
|
||||||
|
<div className="panel__header">
|
||||||
|
<h2>Layers</h2>
|
||||||
|
<p className="muted">Drag layers to reorder them. Each layer processes the output of the one above it.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="layer-stack">
|
||||||
|
{layers.map((layer, index) => (
|
||||||
|
<LayerCard
|
||||||
|
key={layer.id}
|
||||||
|
layer={layer}
|
||||||
|
index={index}
|
||||||
|
shaders={shaders}
|
||||||
|
expanded={expandedSet.has(layer.id)}
|
||||||
|
isDragging={dragLayerId === layer.id}
|
||||||
|
isDropTarget={dropTargetLayerId === layer.id}
|
||||||
|
onToggleExpanded={toggleExpanded}
|
||||||
|
onDragStart={setDragLayerId}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDragLayerId(null);
|
||||||
|
setDropTargetLayerId(null);
|
||||||
|
}}
|
||||||
|
onDragOver={setDropTargetLayerId}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onRemove={removeLayer}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="layer-card layer-card--add">
|
||||||
|
<div className="layer-card__header">
|
||||||
|
<div className="layer-card__meta">
|
||||||
|
<span className="layer-card__index">+</span>
|
||||||
|
<div className="layer-card__title layer-card__title--static">Add Layer</div>
|
||||||
|
</div>
|
||||||
|
<div className="layer-card__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!pendingShaderId}
|
||||||
|
onClick={() => {
|
||||||
|
if (pendingShaderId) {
|
||||||
|
postJson("/api/layers/add", { shaderId: pendingShaderId });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="layer-card__body">
|
||||||
|
<div className="layer-card__field">
|
||||||
|
<label htmlFor="add-layer-select">Shader</label>
|
||||||
|
<select
|
||||||
|
id="add-layer-select"
|
||||||
|
value={pendingShaderId}
|
||||||
|
onChange={(event) => setPendingShaderId(event.target.value)}
|
||||||
|
>
|
||||||
|
{shaders.map((shader) => (
|
||||||
|
<option key={shader.id} value={shader.id}>
|
||||||
|
{shader.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
10
ui/src/main.jsx
Normal file
10
ui/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
325
ui/src/styles.css
Normal file
325
ui/src/styles.css
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
background: #111318;
|
||||||
|
color: #edf1f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #111318;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
pre {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"],
|
||||||
|
select,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #38445b;
|
||||||
|
background: #0f131a;
|
||||||
|
color: inherit;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #22314a;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #c9d5ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.status-grid,
|
||||||
|
.parameter-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #181c24;
|
||||||
|
border: 1px solid #2a3140;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel--full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__header button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 160px 1fr;
|
||||||
|
gap: 8px 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv dt {
|
||||||
|
color: #94a4c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #2a3140;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #131720;
|
||||||
|
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card:hover {
|
||||||
|
border-color: #42516b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card--expanded {
|
||||||
|
border-color: #5d81c3;
|
||||||
|
background: #151d2a;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(93, 129, 195, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card--dragging {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card--drop-target {
|
||||||
|
border-color: #8db0ee;
|
||||||
|
box-shadow: 0 0 0 2px rgba(141, 176, 238, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__header,
|
||||||
|
.layer-card__meta,
|
||||||
|
.layer-card__actions,
|
||||||
|
.layer-card__subheader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__header,
|
||||||
|
.layer-card__subheader {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__meta,
|
||||||
|
.layer-card__actions {
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
width: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__index {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #22314a;
|
||||||
|
color: #c9d5ea;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__title {
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: left;
|
||||||
|
background: #1e2a3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__title--static {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__drag-handle {
|
||||||
|
color: #94a4c2;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__actions button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__body {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card--add {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__field {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__subheader button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #2a3140;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #131720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__value {
|
||||||
|
color: #94a4c2;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__value--pending {
|
||||||
|
color: #d3b26a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__pair {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle--compact {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle--field {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a4c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.toolbar,
|
||||||
|
.status-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__header,
|
||||||
|
.layer-card__subheader {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-card__actions,
|
||||||
|
.layer-card__actions button,
|
||||||
|
.layer-card__field select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
ui/styles.css
169
ui/styles.css
@@ -1,169 +0,0 @@
|
|||||||
:root {
|
|
||||||
color-scheme: dark;
|
|
||||||
font-family: "Segoe UI", sans-serif;
|
|
||||||
background: #111318;
|
|
||||||
color: #edf1f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #111318;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px;
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar,
|
|
||||||
.status-grid,
|
|
||||||
.parameter-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
grid-template-columns: minmax(220px, 2fr) minmax(220px, 3fr) auto auto;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar__group {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar__group--wide {
|
|
||||||
min-width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: #181c24;
|
|
||||||
border: 1px solid #2a3140;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel--full {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel__header button {
|
|
||||||
width: auto;
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.kv {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 160px 1fr;
|
|
||||||
gap: 8px 12px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kv dt {
|
|
||||||
color: #94a4c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kv dd {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter-grid {
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #2a3140;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #131720;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter__value {
|
|
||||||
color: #94a4c2;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter__pair {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
button,
|
|
||||||
pre {
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"],
|
|
||||||
select,
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #38445b;
|
|
||||||
background: #0f131a;
|
|
||||||
color: inherit;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
background: #22314a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
min-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
color: #c9d5ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.toolbar {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
ui/vite.config.js
Normal file
13
ui/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user