refactor
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 6s
CI / React UI Build (push) Successful in 10s

This commit is contained in:
2026-05-03 11:39:21 +10:00
parent 12dda9fc8c
commit 059032c234
11 changed files with 1163 additions and 209 deletions

1
.gitignore vendored
View File

@@ -44,5 +44,6 @@ build.ninja
/runtime/* /runtime/*
!/runtime/templates/ !/runtime/templates/
!/runtime/templates/** !/runtime/templates/**
!/runtime/README.md
/ui/node_modules/ /ui/node_modules/
/ui/dist/ /ui/dist/

View File

@@ -7,7 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing") set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect") set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect" CACHE PATH "Path to the NVIDIA_GPUDirect sample directory from the Blackmagic DeckLink SDK")
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp") if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}") message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
@@ -35,8 +35,12 @@ set(APP_SOURCES
"${APP_DIR}/RuntimeHost.h" "${APP_DIR}/RuntimeHost.h"
"${APP_DIR}/RuntimeJson.cpp" "${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/RuntimeJson.h" "${APP_DIR}/RuntimeJson.h"
"${APP_DIR}/RuntimeParameterUtils.cpp"
"${APP_DIR}/RuntimeParameterUtils.h"
"${APP_DIR}/ShaderCompiler.cpp" "${APP_DIR}/ShaderCompiler.cpp"
"${APP_DIR}/ShaderCompiler.h" "${APP_DIR}/ShaderCompiler.h"
"${APP_DIR}/ShaderPackageRegistry.cpp"
"${APP_DIR}/ShaderPackageRegistry.h"
"${APP_DIR}/ShaderTypes.h" "${APP_DIR}/ShaderTypes.h"
"${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.cpp"
"${APP_DIR}/stdafx.h" "${APP_DIR}/stdafx.h"
@@ -90,6 +94,38 @@ endif()
enable_testing() enable_testing()
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests) add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
add_executable(RuntimeParameterUtilsTests
"${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/RuntimeParameterUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
)
target_include_directories(RuntimeParameterUtilsTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
endif()
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
add_executable(ShaderPackageRegistryTests
"${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
)
target_include_directories(ShaderPackageRegistryTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
endif()
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${GPUDIRECT_DIR}/bin/x64/dvp.dll" "${GPUDIRECT_DIR}/bin/x64/dvp.dll"

View File

@@ -106,6 +106,60 @@ void CopyErrorMessage(const std::string& message, int errorMessageSize, char* er
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE); strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
} }
class ScopedGlShader
{
public:
explicit ScopedGlShader(GLuint shader = 0) : mShader(shader) {}
~ScopedGlShader() { reset(); }
ScopedGlShader(const ScopedGlShader&) = delete;
ScopedGlShader& operator=(const ScopedGlShader&) = delete;
GLuint get() const { return mShader; }
GLuint release()
{
GLuint shader = mShader;
mShader = 0;
return shader;
}
void reset(GLuint shader = 0)
{
if (mShader != 0)
glDeleteShader(mShader);
mShader = shader;
}
private:
GLuint mShader;
};
class ScopedGlProgram
{
public:
explicit ScopedGlProgram(GLuint program = 0) : mProgram(program) {}
~ScopedGlProgram() { reset(); }
ScopedGlProgram(const ScopedGlProgram&) = delete;
ScopedGlProgram& operator=(const ScopedGlProgram&) = delete;
GLuint get() const { return mProgram; }
GLuint release()
{
GLuint program = mProgram;
mProgram = 0;
return program;
}
void reset(GLuint program = 0)
{
if (mProgram != 0)
glDeleteProgram(mProgram);
mProgram = program;
}
private:
GLuint mProgram;
};
std::size_t AlignStd140(std::size_t offset, std::size_t alignment) std::size_t AlignStd140(std::size_t offset, std::size_t alignment)
{ {
const std::size_t mask = alignment - 1; const std::size_t mask = alignment - 1;
@@ -1076,40 +1130,34 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
const char* fragmentSource = fragmentShaderSource.c_str(); const char* fragmentSource = fragmentShaderSource.c_str();
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER); ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL); glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader); glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult); glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
return false; return false;
} }
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER); ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL); glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader); glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult); glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false; return false;
} }
GLuint newProgram = glCreateProgram(); ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram, newVertexShader); glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram, newFragmentShader); glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram); glLinkProgram(newProgram.get());
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult); glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE) if (linkResult == GL_FALSE)
{ {
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage); glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteProgram(newProgram);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false; return false;
} }
@@ -1126,39 +1174,36 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
glDeleteTextures(1, &loadedTexture.texture); glDeleteTextures(1, &loadedTexture.texture);
} }
CopyErrorMessage(loadError, errorMessageSize, errorMessage); CopyErrorMessage(loadError, errorMessageSize, errorMessage);
glDeleteProgram(newProgram);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false; return false;
} }
textureBindings.push_back(textureBinding); textureBindings.push_back(textureBinding);
} }
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram, "GlobalParams"); const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX) if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram, globalParamsIndex, kGlobalParamsBindingPoint); glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0; const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
const GLuint shaderTextureBase = kSourceHistoryTextureUnitBase + historyCap + historyCap; const GLuint shaderTextureBase = kSourceHistoryTextureUnitBase + historyCap + historyCap;
glUseProgram(newProgram); glUseProgram(newProgram.get());
const GLint videoInputLocation = glGetUniformLocation(newProgram, "gVideoInput"); const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput");
if (videoInputLocation >= 0) if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit)); glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index) for (unsigned index = 0; index < historyCap; ++index)
{ {
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index); const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(newProgram, sourceSamplerName.c_str()); const GLint sourceSamplerLocation = glGetUniformLocation(newProgram.get(), sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0) if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index)); glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index); const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(newProgram, temporalSamplerName.c_str()); const GLint temporalSamplerLocation = glGetUniformLocation(newProgram.get(), temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0) if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index)); glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
} }
for (std::size_t index = 0; index < textureBindings.size(); ++index) for (std::size_t index = 0; index < textureBindings.size(); ++index)
{ {
const GLint textureSamplerLocation = glGetUniformLocation(newProgram, textureBindings[index].samplerName.c_str()); const GLint textureSamplerLocation = glGetUniformLocation(newProgram.get(), textureBindings[index].samplerName.c_str());
if (textureSamplerLocation >= 0) if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index))); glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
} }
@@ -1166,9 +1211,9 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
layerProgram.layerId = state.layerId; layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId; layerProgram.shaderId = state.shaderId;
layerProgram.program = newProgram; layerProgram.program = newProgram.release();
layerProgram.vertexShader = newVertexShader; layerProgram.vertexShader = newVertexShader.release();
layerProgram.fragmentShader = newFragmentShader; layerProgram.fragmentShader = newFragmentShader.release();
layerProgram.textureBindings.swap(textureBindings); layerProgram.textureBindings.swap(textureBindings);
return true; return true;
} }
@@ -1222,47 +1267,41 @@ bool OpenGLComposite::compileDecodeShader(int errorMessageSize, char* errorMessa
const char* vertexSource = kVertexShaderSource; const char* vertexSource = kVertexShaderSource;
const char* fragmentSource = kDecodeFragmentShaderSource; const char* fragmentSource = kDecodeFragmentShaderSource;
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER); ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL); glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader); glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult); glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
return false; return false;
} }
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER); ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL); glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader); glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult); glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false; return false;
} }
GLuint newProgram = glCreateProgram(); ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram, newVertexShader); glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram, newFragmentShader); glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram); glLinkProgram(newProgram.get());
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult); glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE) if (linkResult == GL_FALSE)
{ {
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage); glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
glDeleteProgram(newProgram);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false; return false;
} }
destroyDecodeShaderProgram(); destroyDecodeShaderProgram();
mDecodeProgram = newProgram; mDecodeProgram = newProgram.release();
mDecodeVertexShader = newVertexShader; mDecodeVertexShader = newVertexShader.release();
mDecodeFragmentShader = newFragmentShader; mDecodeFragmentShader = newFragmentShader.release();
return true; return true;
} }

View File

@@ -1,6 +1,8 @@
#include "stdafx.h" #include "stdafx.h"
#include "RuntimeHost.h" #include "RuntimeHost.h"
#include "RuntimeParameterUtils.h"
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#include "ShaderPackageRegistry.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -1250,37 +1252,10 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
{ {
std::map<std::string, ShaderPackage> packagesById; std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder; std::vector<std::string> packageOrder;
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
if (!std::filesystem::exists(mShaderRoot)) if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
{
error = "Shader library directory does not exist: " + mShaderRoot.string();
return false; return false;
}
for (const auto& entry : std::filesystem::directory_iterator(mShaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseShaderManifest(manifestPath, shaderPackage, error))
return false;
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
error = "Duplicate shader id found: " + shaderPackage.id;
return false;
}
packageOrder.push_back(shaderPackage.id);
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
mPackagesById.swap(packagesById); mPackagesById.swap(packagesById);
mPackageOrder.swap(packageOrder); mPackageOrder.swap(packageOrder);
@@ -1329,109 +1304,12 @@ bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath,
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
{ {
normalizedValue = DefaultValueForDefinition(definition); return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
switch (definition.type)
{
case ShaderParameterType::Float:
{
if (!value.isNumber())
{
error = "Expected numeric value for float parameter '" + definition.id + "'.";
return false;
}
double number = value.asNumber();
if (!IsFiniteNumber(number))
{
error = "Float parameter '" + definition.id + "' must be finite.";
return false;
}
if (!definition.minNumbers.empty())
number = std::max(number, definition.minNumbers.front());
if (!definition.maxNumbers.empty())
number = std::min(number, definition.maxNumbers.front());
normalizedValue.numberValues = { number };
return true;
}
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
std::vector<double> numbers = JsonArrayToNumbers(value);
const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4;
if (numbers.size() != expectedSize)
{
error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'.";
return false;
}
for (std::size_t index = 0; index < numbers.size(); ++index)
{
if (!IsFiniteNumber(numbers[index]))
{
error = "Parameter '" + definition.id + "' contains a non-finite value.";
return false;
}
if (index < definition.minNumbers.size())
numbers[index] = std::max(numbers[index], definition.minNumbers[index]);
if (index < definition.maxNumbers.size())
numbers[index] = std::min(numbers[index], definition.maxNumbers[index]);
}
normalizedValue.numberValues = numbers;
return true;
}
case ShaderParameterType::Boolean:
if (!value.isBoolean())
{
error = "Expected boolean value for parameter '" + definition.id + "'.";
return false;
}
normalizedValue.booleanValue = value.asBoolean();
return true;
case ShaderParameterType::Enum:
{
if (!value.isString())
{
error = "Expected string value for enum parameter '" + definition.id + "'.";
return false;
}
const std::string selectedValue = value.asString();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == selectedValue)
{
normalizedValue.enumValue = selectedValue;
return true;
}
}
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
return false;
}
}
return false;
} }
ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const
{ {
ShaderParameterValue value; return ::DefaultValueForDefinition(definition);
switch (definition.type)
{
case ShaderParameterType::Float:
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
break;
case ShaderParameterType::Vec2:
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
break;
case ShaderParameterType::Color:
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
break;
case ShaderParameterType::Boolean:
value.booleanValue = definition.defaultBoolean;
break;
case ShaderParameterType::Enum:
value.enumValue = definition.defaultEnumValue;
break;
}
return value;
} }
void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const
@@ -1737,26 +1615,7 @@ std::vector<std::string> RuntimeHost::GetStackPresetNamesLocked() const
std::string RuntimeHost::MakeSafePresetFileStem(const std::string& presetName) const std::string RuntimeHost::MakeSafePresetFileStem(const std::string& presetName) const
{ {
std::string trimmed = Trim(presetName); return ::MakeSafePresetFileStem(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

View File

@@ -0,0 +1,170 @@
#include "stdafx.h"
#include "RuntimeParameterUtils.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <vector>
namespace
{
std::string TrimText(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
--end;
return text.substr(start, end - start);
}
bool IsFiniteNumber(double value)
{
return std::isfinite(value) != 0;
}
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
}
std::string MakeSafePresetFileStem(const std::string& presetName)
{
std::string trimmed = TrimText(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;
}
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition)
{
ShaderParameterValue value;
switch (definition.type)
{
case ShaderParameterType::Float:
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
break;
case ShaderParameterType::Vec2:
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
break;
case ShaderParameterType::Color:
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
break;
case ShaderParameterType::Boolean:
value.booleanValue = definition.defaultBoolean;
break;
case ShaderParameterType::Enum:
value.enumValue = definition.defaultEnumValue;
break;
}
return value;
}
bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error)
{
normalizedValue = DefaultValueForDefinition(definition);
switch (definition.type)
{
case ShaderParameterType::Float:
{
if (!value.isNumber())
{
error = "Expected numeric value for float parameter '" + definition.id + "'.";
return false;
}
double number = value.asNumber();
if (!IsFiniteNumber(number))
{
error = "Float parameter '" + definition.id + "' must be finite.";
return false;
}
if (!definition.minNumbers.empty())
number = std::max(number, definition.minNumbers.front());
if (!definition.maxNumbers.empty())
number = std::min(number, definition.maxNumbers.front());
normalizedValue.numberValues = { number };
return true;
}
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
std::vector<double> numbers = JsonArrayToNumbers(value);
const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4;
if (numbers.size() != expectedSize)
{
error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'.";
return false;
}
for (std::size_t index = 0; index < numbers.size(); ++index)
{
if (!IsFiniteNumber(numbers[index]))
{
error = "Parameter '" + definition.id + "' contains a non-finite value.";
return false;
}
if (index < definition.minNumbers.size())
numbers[index] = std::max(numbers[index], definition.minNumbers[index]);
if (index < definition.maxNumbers.size())
numbers[index] = std::min(numbers[index], definition.maxNumbers[index]);
}
normalizedValue.numberValues = numbers;
return true;
}
case ShaderParameterType::Boolean:
if (!value.isBoolean())
{
error = "Expected boolean value for parameter '" + definition.id + "'.";
return false;
}
normalizedValue.booleanValue = value.asBoolean();
return true;
case ShaderParameterType::Enum:
{
if (!value.isString())
{
error = "Expected string value for enum parameter '" + definition.id + "'.";
return false;
}
const std::string selectedValue = value.asString();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == selectedValue)
{
normalizedValue.enumValue = selectedValue;
return true;
}
}
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
return false;
}
}
return false;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <string>
std::string MakeSafePresetFileStem(const std::string& presetName);
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition);
bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error);

View File

@@ -0,0 +1,549 @@
#include "stdafx.h"
#include "ShaderPackageRegistry.h"
#include "RuntimeJson.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <fstream>
#include <sstream>
namespace
{
std::string Trim(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
--end;
return text.substr(start, end - start);
}
bool IsFiniteNumber(double value)
{
return std::isfinite(value) != 0;
}
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
{
if (typeName == "float")
{
type = ShaderParameterType::Float;
return true;
}
if (typeName == "vec2")
{
type = ShaderParameterType::Vec2;
return true;
}
if (typeName == "color")
{
type = ShaderParameterType::Color;
return true;
}
if (typeName == "bool")
{
type = ShaderParameterType::Boolean;
return true;
}
if (typeName == "enum")
{
type = ShaderParameterType::Enum;
return true;
}
return false;
}
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
{
if (sourceName == "source")
{
source = TemporalHistorySource::Source;
return true;
}
if (sourceName == "preLayerInput")
{
source = TemporalHistorySource::PreLayerInput;
return true;
}
if (sourceName == "none")
{
source = TemporalHistorySource::None;
return true;
}
return false;
}
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{
std::ifstream input(path, std::ios::binary);
if (!input)
{
error = "Could not open file: " + path.string();
return std::string();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
{
return manifestPath.string();
}
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue || !fieldValue->isString())
{
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(object, fieldName, value, manifestPath, error))
return false;
if (Trim(value).empty())
{
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue)
{
value = fallback;
return true;
}
if (!fieldValue->isString())
{
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isArray())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isObject())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (value.isNumber())
{
numbers.push_back(value.asNumber());
return true;
}
if (value.isArray())
{
numbers = JsonArrayToNumbers(value);
if (numbers.size() != value.asArray().size())
{
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
for (char ch : identifier)
{
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
if (!(std::isalnum(unsignedCh) || ch == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
}
return true;
}
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
return false;
shaderPackage.directoryPath = manifestPath.parent_path();
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
shaderPackage.manifestPath = manifestPath;
return true;
}
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* texturesValue = nullptr;
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
return false;
if (!texturesValue)
return true;
for (const JsonValue& textureJson : texturesValue->asArray())
{
if (!textureJson.isObject())
{
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string textureId;
std::string texturePath;
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
{
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
return false;
ShaderTextureAsset textureAsset;
textureAsset.id = textureId;
textureAsset.path = shaderPackage.directoryPath / texturePath;
if (!std::filesystem::exists(textureAsset.path))
{
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
return false;
}
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
shaderPackage.textureAssets.push_back(textureAsset);
}
return true;
}
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* temporalValue = nullptr;
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
return false;
if (!temporalValue)
return true;
const JsonValue* enabledValue = temporalValue->find("enabled");
if (!enabledValue || !enabledValue->asBoolean(false))
return true;
std::string historySourceName;
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
{
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
return false;
}
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
if (!historyLengthValue || !historyLengthValue->isNumber())
{
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
return false;
}
TemporalHistorySource historySource = TemporalHistorySource::None;
if (!ParseTemporalHistorySource(historySourceName, historySource))
{
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
const double requestedHistoryLength = historyLengthValue->asNumber();
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
{
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
return false;
}
shaderPackage.temporal.enabled = true;
shaderPackage.temporal.historySource = historySource;
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
return true;
}
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
{
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
return true;
}
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* defaultValue = parameterJson.find("default");
if (!defaultValue)
return true;
if (definition.type == ShaderParameterType::Boolean)
{
if (!defaultValue->isBoolean())
{
error = "Boolean parameter default must be a boolean for: " + definition.id;
return false;
}
definition.defaultBoolean = defaultValue->asBoolean(false);
return true;
}
if (definition.type == ShaderParameterType::Enum)
{
if (!defaultValue->isString())
{
error = "Enum parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultEnumValue = defaultValue->asString();
return true;
}
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
}
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* optionsValue = nullptr;
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
{
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& optionJson : optionsValue->asArray())
{
if (!optionJson.isObject())
{
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
ShaderParameterOption option;
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
{
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
return false;
}
definition.enumOptions.push_back(option);
}
bool defaultFound = definition.defaultEnumValue.empty();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == definition.defaultEnumValue)
{
defaultFound = true;
break;
}
}
if (!defaultFound)
{
error = "Enum parameter default is not present in its option list for: " + definition.id;
return false;
}
return true;
}
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
if (!parameterJson.isObject())
{
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string typeName;
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
{
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ParseShaderParameterType(typeName, definition.type))
{
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
{
return false;
}
if (definition.type == ShaderParameterType::Enum)
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
return true;
}
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* parametersValue = nullptr;
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
return false;
if (!parametersValue)
return true;
for (const JsonValue& parameterJson : parametersValue->asArray())
{
ShaderParameterDefinition definition;
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
return false;
shaderPackage.parameters.push_back(definition);
}
return true;
}
}
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
{
packagesById.clear();
packageOrder.clear();
if (!std::filesystem::exists(shaderRoot))
{
error = "Shader library directory does not exist: " + shaderRoot.string();
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseManifest(manifestPath, shaderPackage, error))
return false;
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
error = "Duplicate shader id found: " + shaderPackage.id;
return false;
}
packageOrder.push_back(shaderPackage.id);
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
return true;
}
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
if (!manifestJson.isObject())
{
error = "Shader manifest root must be an object: " + manifestPath.string();
return false;
}
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false;
if (!std::filesystem::exists(shaderPackage.shaderPath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <map>
#include <string>
#include <vector>
class ShaderPackageRegistry
{
public:
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
bool Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const;
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
private:
unsigned mMaxTemporalHistoryFrames;
};

21
runtime/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Runtime Files
This directory is used by the native host for local runtime output.
Tracked files:
- `templates/`: source templates used to generate shader runtime code.
Generated files:
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer.
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`.
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
- `runtime_state.json`: persisted layer stack and parameter values.
- `stack_presets/*.json`: user-saved layer stack presets.
Git policy:
- Runtime cache/state/preset output is ignored by default.
- Template files are source files and should stay tracked.
- If a project wants shared stack presets, move selected preset JSON files into a tracked fixtures or presets directory intentionally rather than committing the whole runtime output tree.

View File

@@ -0,0 +1,120 @@
#include "RuntimeParameterUtils.h"
#include <cmath>
#include <iostream>
#include <string>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
ShaderParameterDefinition MakeFloatDefinition()
{
ShaderParameterDefinition definition;
definition.id = "gain";
definition.type = ShaderParameterType::Float;
definition.defaultNumbers = { 0.5 };
definition.minNumbers = { 0.0 };
definition.maxNumbers = { 1.0 };
return definition;
}
void TestSafePresetFileStems()
{
Expect(MakeSafePresetFileStem(" Show Look 01 ") == "show-look-01", "preset names are trimmed and dashed");
Expect(MakeSafePresetFileStem("A__B---C") == "a-b-c", "duplicate separators collapse");
Expect(MakeSafePresetFileStem("../Unsafe Name!") == "unsafe-name", "unsafe punctuation is dropped");
Expect(MakeSafePresetFileStem("!!!") == "", "names without alphanumeric characters become empty");
}
void TestFloatNormalization()
{
ShaderParameterDefinition definition = MakeFloatDefinition();
ShaderParameterValue value;
std::string error;
Expect(NormalizeAndValidateParameterValue(definition, JsonValue(2.0), value, error), "float parameter accepts numeric values");
Expect(value.numberValues.size() == 1 && value.numberValues.front() == 1.0, "float parameter clamps to max");
error.clear();
Expect(NormalizeAndValidateParameterValue(definition, JsonValue(-10.0), value, error), "float parameter accepts low numeric values");
Expect(value.numberValues.size() == 1 && value.numberValues.front() == 0.0, "float parameter clamps to min");
error.clear();
Expect(!NormalizeAndValidateParameterValue(definition, JsonValue("bad"), value, error), "float parameter rejects non-numeric values");
Expect(!error.empty(), "float rejection includes an error");
}
void TestVectorNormalization()
{
ShaderParameterDefinition definition;
definition.id = "offset";
definition.type = ShaderParameterType::Vec2;
definition.defaultNumbers = { 0.0, 0.0 };
definition.minNumbers = { -1.0, -2.0 };
definition.maxNumbers = { 1.0, 2.0 };
JsonValue input = JsonValue::MakeArray();
input.pushBack(JsonValue(5.0));
input.pushBack(JsonValue(-5.0));
ShaderParameterValue value;
std::string error;
Expect(NormalizeAndValidateParameterValue(definition, input, value, error), "vec2 parameter accepts arrays");
Expect(value.numberValues.size() == 2 && value.numberValues[0] == 1.0 && value.numberValues[1] == -2.0, "vec2 parameter clamps each component");
JsonValue shortInput = JsonValue::MakeArray();
shortInput.pushBack(JsonValue(0.0));
error.clear();
Expect(!NormalizeAndValidateParameterValue(definition, shortInput, value, error), "vec2 parameter rejects wrong component count");
}
void TestEnumAndDefaults()
{
ShaderParameterDefinition definition;
definition.id = "mode";
definition.type = ShaderParameterType::Enum;
definition.defaultEnumValue = "soft";
definition.enumOptions = {
{ "soft", "Soft" },
{ "hard", "Hard" }
};
ShaderParameterValue defaultValue = DefaultValueForDefinition(definition);
Expect(defaultValue.enumValue == "soft", "enum default is copied from definition");
ShaderParameterValue value;
std::string error;
Expect(NormalizeAndValidateParameterValue(definition, JsonValue("hard"), value, error), "enum accepts listed options");
Expect(value.enumValue == "hard", "enum stores selected option");
error.clear();
Expect(!NormalizeAndValidateParameterValue(definition, JsonValue("other"), value, error), "enum rejects unknown options");
}
}
int main()
{
TestSafePresetFileStems();
TestFloatNormalization();
TestVectorNormalization();
TestEnumAndDefaults();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeParameterUtils test failure(s).\n";
return 1;
}
std::cout << "RuntimeParameterUtils tests passed.\n";
return 0;
}

View File

@@ -0,0 +1,129 @@
#include "ShaderPackageRegistry.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
std::filesystem::path MakeTestRoot()
{
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
std::filesystem::path root = std::filesystem::temp_directory_path() / ("video-shader-registry-tests-" + std::to_string(stamp));
std::filesystem::create_directories(root);
return root;
}
void WriteFile(const std::filesystem::path& path, const std::string& contents)
{
std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
output << contents;
}
void WriteShaderPackage(const std::filesystem::path& root, const std::string& directoryName, const std::string& manifest)
{
const std::filesystem::path packageRoot = root / directoryName;
WriteFile(packageRoot / "shader.json", manifest);
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
}
void TestValidManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks");
WriteShaderPackage(root, "look", R"({
"id": "look-01",
"name": "Look 01",
"description": "Test package",
"category": "Tests",
"entryPoint": "shadeVideo",
"textures": [{ "id": "maskTex", "path": "mask.png" }],
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
"parameters": [
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 },
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
{ "value": "soft", "label": "Soft" },
{ "value": "hard", "label": "Hard" }
] }
]
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(registry.ParseManifest(root / "look" / "shader.json", package, error), "valid manifest parses");
Expect(package.id == "look-01", "manifest id is preserved");
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
Expect(package.parameters.size() == 2, "parameters parse");
std::filesystem::remove_all(root);
}
void TestInvalidManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "bad", R"({
"id": "bad",
"name": "Bad",
"entryPoint": "not-valid!",
"parameters": []
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(!registry.ParseManifest(root / "bad" / "shader.json", package, error), "invalid shader identifier is rejected");
Expect(error.find("entryPoint") != std::string::npos, "invalid manifest error names the bad field");
std::filesystem::remove_all(root);
}
void TestDuplicateScan()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "a", R"({ "id": "dupe", "name": "A", "parameters": [] })");
WriteShaderPackage(root, "b", R"({ "id": "dupe", "name": "B", "parameters": [] })");
ShaderPackageRegistry registry(4);
std::map<std::string, ShaderPackage> packages;
std::vector<std::string> order;
std::string error;
Expect(!registry.Scan(root, packages, order, error), "duplicate package ids are rejected");
Expect(error.find("Duplicate shader id") != std::string::npos, "duplicate scan error is clear");
std::filesystem::remove_all(root);
}
}
int main()
{
TestValidManifest();
TestInvalidManifest();
TestDuplicateScan();
if (gFailures != 0)
{
std::cerr << gFailures << " ShaderPackageRegistry test failure(s).\n";
return 1;
}
std::cout << "ShaderPackageRegistry tests passed.\n";
return 0;
}