refactor
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
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
|
||||||
|
|||||||
170
apps/LoopThroughWithOpenGLCompositing/RuntimeParameterUtils.cpp
Normal file
170
apps/LoopThroughWithOpenGLCompositing/RuntimeParameterUtils.cpp
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
549
apps/LoopThroughWithOpenGLCompositing/ShaderPackageRegistry.cpp
Normal file
549
apps/LoopThroughWithOpenGLCompositing/ShaderPackageRegistry.cpp
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
21
runtime/README.md
Normal 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.
|
||||||
120
tests/RuntimeParameterUtilsTests.cpp
Normal file
120
tests/RuntimeParameterUtilsTests.cpp
Normal 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;
|
||||||
|
}
|
||||||
129
tests/ShaderPackageRegistryTests.cpp
Normal file
129
tests/ShaderPackageRegistryTests.cpp
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user