diff --git a/README.md b/README.md index b87f6ff..e701037 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ cmake --preset vs2022-x64-debug ` -DDECKLINK_SDK_ROOT="D:/SDKs/Blackmagic DeckLink SDK 16.0" ``` +At runtime, Slang compilation follows the same root order: `SLANG_ROOT`, `THIRD_PARTY_ROOT`, repo `video-io-3rdParty/`, then repo or packaged `3rdParty/`. The packaged layout uses `3rdParty/slang/bin/slangc.exe`; development bundles can use `slang-2026.8-windows-x86_64/bin/slangc.exe`. + ## Build Configure and build the native app: diff --git a/docs/FORKING_RENDER_CADENCE_BASE.md b/docs/FORKING_RENDER_CADENCE_BASE.md index 560f868..c48400a 100644 --- a/docs/FORKING_RENDER_CADENCE_BASE.md +++ b/docs/FORKING_RENDER_CADENCE_BASE.md @@ -66,7 +66,7 @@ Do not move DeckLink, NDI, file I/O, shader compilation, or control handling int Before cutting a long-lived fork, fix or decide these items: - Keep `runtimeShaderId` empty in checked-in config unless this repo intentionally wants a default startup shader again. -- Align remaining runtime third-party discovery with CMake. Font atlas generation now checks `MSDF_ATLAS_GEN_ROOT`, `THIRD_PARTY_ROOT`, `3rdParty`, and `video-io-3rdParty`; shader compiler lookup still needs the same treatment for Slang. +- Keep runtime third-party discovery aligned with CMake. Font atlas generation and Slang compilation now both check explicit tool roots, `THIRD_PARTY_ROOT`, the private `video-io-3rdParty` bundle, and legacy or packaged `3rdParty` layouts. - Make `config/runtime-host.json` portable. Current checked-in defaults include a local NDI source name and DeckLink output. - Decide whether the fork keeps the Slang shader package contract. If not, retire or clearly isolate `shaders/SHADER_CONTRACT.md`, shader package UI, and shader manifest tests. - Mark older docs that reference `apps/LoopThroughWithOpenGLCompositing` as historical, or update them to point at the current `src/` implementation. @@ -75,13 +75,13 @@ Before cutting a long-lived fork, fix or decide these items: ## Verification Snapshot -Last checked locally on 2026-05-30 after the font-builder lookup fix: +Last checked locally on 2026-05-30 after the Slang lookup alignment: -- Worktree was clean before documentation changes. - UI production build passed with `npm.cmd run build`. -- Native debug build passed with `cmake --build --preset build-debug --parallel`. -- Native tests passed 24 of 24 with `ctest --test-dir build\vs2022-x64-debug -C Debug --output-on-failure`. +- Native debug build passed for `RenderCadenceCompositor`, `ShaderSlangValidationTests`, and `ShaderCompilerLookupTests`. +- Native tests passed 26 of 26 with `ctest --test-dir build\vs2022-x64-debug -C Debug --output-on-failure`. - `FontAtlasBuilderTests` now passes with `msdf-atlas-gen.exe` supplied by the private `video-io-3rdParty/msdf-atlas-gen` bundle. +- `ShaderCompilerLookupTests` covers `SLANG_ROOT`, `THIRD_PARTY_ROOT`, repo `video-io-3rdParty`, legacy `3rdParty`, and packaged `3rdParty/slang` Slang layouts. The generated Visual Studio `RUN_TESTS` target did not build missing test executables by itself during the first local run; building the debug preset first produced the test binaries. diff --git a/src/README.md b/src/README.md index 7c381d9..c46f0db 100644 --- a/src/README.md +++ b/src/README.md @@ -368,6 +368,8 @@ On startup the app first tries to restore `runtime/runtime_state.json`. Valid sa The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback. +The Slang compiler lookup matches the CMake third-party layout: `SLANG_ROOT` first, then `THIRD_PARTY_ROOT`, then repo `video-io-3rdParty/`, then repo or packaged `3rdParty/`. Packaged builds use `3rdParty/slang/bin/slangc.exe`; development bundles can use `slang-2026.8-windows-x86_64/bin/slangc.exe`. + `POST /api/reload` rescans `shaders/`, re-reads manifests, refreshes supported shader metadata, reconciles active layer parameters against changed definitions, and queues recompilation for every catalog-valid layer in the active stack. It does not compile every package in the shader library; packages are compiled when they are part of the active stack. Current runtime shader support is deliberately limited to stateless full-frame packages: diff --git a/src/shader/ShaderCompiler.cpp b/src/shader/ShaderCompiler.cpp index f87834b..c5d0e70 100644 --- a/src/shader/ShaderCompiler.cpp +++ b/src/shader/ShaderCompiler.cpp @@ -3,8 +3,9 @@ #include "NativeHandles.h" -#include #include +#include +#include #include #include #include @@ -150,6 +151,90 @@ std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned h source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n"; return source.str(); } + +bool UseExecutableIfPresent(const std::filesystem::path& candidate, std::filesystem::path& compilerPath) +{ + if (!std::filesystem::exists(candidate) || std::filesystem::is_directory(candidate)) + return false; + + compilerPath = candidate; + return true; +} + +bool UseSlangRootIfPresent(const std::filesystem::path& root, std::filesystem::path& compilerPath) +{ + if (root.empty()) + return false; + + if (UseExecutableIfPresent(root, compilerPath)) + return true; + + if (UseExecutableIfPresent(root / "slangc.exe", compilerPath)) + return true; + + return UseExecutableIfPresent(root / "bin" / "slangc.exe", compilerPath); +} + +std::filesystem::path EnvironmentPath(const char* variableName) +{ +#if defined(_MSC_VER) + char* value = nullptr; + std::size_t size = 0; + if (_dupenv_s(&value, &size, variableName) != 0 || value == nullptr) + return std::filesystem::path(); + std::string text(value); + std::free(value); + if (text.empty()) + return std::filesystem::path(); + return std::filesystem::path(text); +#else + const char* value = std::getenv(variableName); + if (value == nullptr || *value == '\0') + return std::filesystem::path(); + return std::filesystem::path(value); +#endif +} + +bool UseEnvironmentSlangRoot(const char* variableName, std::filesystem::path& compilerPath) +{ + return UseSlangRootIfPresent(EnvironmentPath(variableName), compilerPath); +} + +bool UseThirdPartyRootIfPresent(const std::filesystem::path& thirdPartyRoot, std::filesystem::path& compilerPath) +{ + if (thirdPartyRoot.empty()) + return false; + + const std::vector preferredRoots = { + thirdPartyRoot / "slang-2026.8-windows-x86_64", + thirdPartyRoot / "slang", + thirdPartyRoot + }; + for (const std::filesystem::path& root : preferredRoots) + { + if (UseSlangRootIfPresent(root, compilerPath)) + return true; + } + + std::error_code directoryError; + if (!std::filesystem::exists(thirdPartyRoot, directoryError) || !std::filesystem::is_directory(thirdPartyRoot, directoryError)) + return false; + + for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot, directoryError)) + { + if (directoryError) + break; + + std::error_code entryError; + if (!entry.is_directory(entryError)) + continue; + + if (UseSlangRootIfPresent(entry.path(), compilerPath)) + return true; + } + + return false; +} } ShaderCompiler::ShaderCompiler( @@ -218,38 +303,25 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const { - char slangRootBuffer[MAX_PATH] = {}; - const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast(sizeof(slangRootBuffer))); - if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer)) - { - std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe"; - if (std::filesystem::exists(candidate)) - { - compilerPath = candidate; - return true; - } - } + return FindSlangCompilerPath(mRepoRoot, compilerPath, error); +} - std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty"; - if (!std::filesystem::exists(thirdPartyRoot)) - { - error = "3rdParty directory was not found under the repository root."; - return false; - } +bool ShaderCompiler::FindSlangCompilerPath(const std::filesystem::path& repoRoot, std::filesystem::path& compilerPath, std::string& error) +{ + if (UseEnvironmentSlangRoot("SLANG_ROOT", compilerPath)) + return true; - for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot)) - { - if (!entry.is_directory()) - continue; - std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe"; - if (std::filesystem::exists(candidate)) - { - compilerPath = candidate; - return true; - } - } + const std::filesystem::path thirdPartyRoot = EnvironmentPath("THIRD_PARTY_ROOT"); + if (UseThirdPartyRootIfPresent(thirdPartyRoot, compilerPath)) + return true; - error = "Could not find slangc.exe under 3rdParty."; + if (UseThirdPartyRootIfPresent(repoRoot / "video-io-3rdParty", compilerPath)) + return true; + + if (UseThirdPartyRootIfPresent(repoRoot / "3rdParty", compilerPath)) + return true; + + error = "Could not find slangc.exe. Set SLANG_ROOT, set THIRD_PARTY_ROOT to a bundle containing slang-2026.8-windows-x86_64, or place Slang under video-io-3rdParty or 3rdParty."; return false; } diff --git a/src/shader/ShaderCompiler.h b/src/shader/ShaderCompiler.h index 9fc8c03..4b38880 100644 --- a/src/shader/ShaderCompiler.h +++ b/src/shader/ShaderCompiler.h @@ -15,6 +15,8 @@ public: const std::filesystem::path& patchedGlslPath, unsigned maxTemporalHistoryFrames); + static bool FindSlangCompilerPath(const std::filesystem::path& repoRoot, std::filesystem::path& compilerPath, std::string& error); + bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const; private: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cb9d28e..dedd24d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -137,6 +137,11 @@ add_video_shader_test(ShaderSlangValidationTests "${TEST_DIR}/ShaderSlangValidationTests.cpp" ) +add_video_shader_test(ShaderCompilerLookupTests + "${SRC_DIR}/shader/ShaderCompiler.cpp" + "${TEST_DIR}/ShaderCompilerLookupTests.cpp" +) + add_video_shader_test(Std140BufferTests "${TEST_DIR}/Std140BufferTests.cpp" ) @@ -169,5 +174,5 @@ set_tests_properties(RenderCadenceCompositorLoggerTests PROPERTIES ) set_tests_properties(ShaderSlangValidationTests PROPERTIES - ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}" + ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT};THIRD_PARTY_ROOT=${THIRD_PARTY_ROOT}" ) diff --git a/tests/ShaderCompilerLookupTests.cpp b/tests/ShaderCompilerLookupTests.cpp new file mode 100644 index 0000000..9692275 --- /dev/null +++ b/tests/ShaderCompilerLookupTests.cpp @@ -0,0 +1,203 @@ +#include "ShaderCompiler.h" + +#include +#include +#include +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const std::string& message) +{ + if (condition) + return; + + ++gFailures; + std::cerr << "FAIL: " << message << "\n"; +} + +std::filesystem::path MakeTestRoot(const std::string& name) +{ + const std::filesystem::path root = + std::filesystem::temp_directory_path() / + ("shader-compiler-lookup-" + name + "-" + std::to_string(std::filesystem::file_time_type::clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + return root; +} + +void WriteFakeSlangCompiler(const std::filesystem::path& path) +{ + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary); + output << "not a real compiler"; +} + +std::string ReadEnvironmentVariable(const char* name) +{ +#if defined(_MSC_VER) + char* value = nullptr; + std::size_t size = 0; + if (_dupenv_s(&value, &size, name) != 0 || value == nullptr) + return std::string(); + std::string text(value); + std::free(value); + return text; +#else + const char* value = std::getenv(name); + return value ? std::string(value) : std::string(); +#endif +} + +void WriteEnvironmentVariable(const char* name, const std::string& value) +{ +#if defined(_MSC_VER) + _putenv_s(name, value.c_str()); +#else + if (value.empty()) + unsetenv(name); + else + setenv(name, value.c_str(), 1); +#endif +} + +class ScopedEnvironmentVariable +{ +public: + ScopedEnvironmentVariable(const char* name, const std::string& value) : + mName(name), + mOriginal(ReadEnvironmentVariable(name)) + { + WriteEnvironmentVariable(name, value); + } + + ~ScopedEnvironmentVariable() + { + WriteEnvironmentVariable(mName.c_str(), mOriginal); + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& operator=(const ScopedEnvironmentVariable&) = delete; + +private: + std::string mName; + std::string mOriginal; +}; + +bool SamePath(const std::filesystem::path& left, const std::filesystem::path& right) +{ + std::error_code leftError; + std::error_code rightError; + return std::filesystem::weakly_canonical(left, leftError) == std::filesystem::weakly_canonical(right, rightError); +} + +void TestSlangRootWinsOverThirdPartyRoot() +{ + const std::filesystem::path root = MakeTestRoot("slang-root"); + const std::filesystem::path expected = root / "env-slang" / "bin" / "slangc.exe"; + const std::filesystem::path thirdPartyCompiler = root / "third-party" / "slang-2026.8-windows-x86_64" / "bin" / "slangc.exe"; + WriteFakeSlangCompiler(expected); + WriteFakeSlangCompiler(thirdPartyCompiler); + + ScopedEnvironmentVariable slangRoot("SLANG_ROOT", (root / "env-slang").string()); + ScopedEnvironmentVariable thirdPartyRoot("THIRD_PARTY_ROOT", (root / "third-party").string()); + + std::filesystem::path actual; + std::string error; + Expect(ShaderCompiler::FindSlangCompilerPath(root, actual, error), "SLANG_ROOT compiler is found"); + Expect(SamePath(actual, expected), "SLANG_ROOT has priority over THIRD_PARTY_ROOT"); + + std::filesystem::remove_all(root); +} + +void TestThirdPartyRootUsesCMakeBundleLayout() +{ + const std::filesystem::path root = MakeTestRoot("third-party-root"); + const std::filesystem::path thirdPartyRootPath = root / "video-io-3rdParty"; + const std::filesystem::path expected = thirdPartyRootPath / "slang-2026.8-windows-x86_64" / "bin" / "slangc.exe"; + WriteFakeSlangCompiler(expected); + + ScopedEnvironmentVariable slangRoot("SLANG_ROOT", ""); + ScopedEnvironmentVariable thirdPartyRoot("THIRD_PARTY_ROOT", thirdPartyRootPath.string()); + + std::filesystem::path actual; + std::string error; + Expect(ShaderCompiler::FindSlangCompilerPath(root, actual, error), "THIRD_PARTY_ROOT compiler is found"); + Expect(SamePath(actual, expected), "THIRD_PARTY_ROOT matches CMake's Slang bundle layout"); + + std::filesystem::remove_all(root); +} + +void TestRepoVideoIoBundleWinsOverLegacyRepoBundle() +{ + const std::filesystem::path root = MakeTestRoot("repo-video-io"); + const std::filesystem::path expected = root / "video-io-3rdParty" / "slang-2026.8-windows-x86_64" / "bin" / "slangc.exe"; + const std::filesystem::path legacy = root / "3rdParty" / "slang-2026.8-windows-x86_64" / "bin" / "slangc.exe"; + WriteFakeSlangCompiler(expected); + WriteFakeSlangCompiler(legacy); + + ScopedEnvironmentVariable slangRoot("SLANG_ROOT", ""); + ScopedEnvironmentVariable thirdPartyRoot("THIRD_PARTY_ROOT", ""); + + std::filesystem::path actual; + std::string error; + Expect(ShaderCompiler::FindSlangCompilerPath(root, actual, error), "repo video-io-3rdParty compiler is found"); + Expect(SamePath(actual, expected), "repo video-io-3rdParty has priority over repo 3rdParty"); + + std::filesystem::remove_all(root); +} + +void TestPackagedSlangFolderIsAccepted() +{ + const std::filesystem::path root = MakeTestRoot("packaged"); + const std::filesystem::path expected = root / "3rdParty" / "slang" / "bin" / "slangc.exe"; + WriteFakeSlangCompiler(expected); + + ScopedEnvironmentVariable slangRoot("SLANG_ROOT", ""); + ScopedEnvironmentVariable thirdPartyRoot("THIRD_PARTY_ROOT", ""); + + std::filesystem::path actual; + std::string error; + Expect(ShaderCompiler::FindSlangCompilerPath(root, actual, error), "packaged 3rdParty/slang compiler is found"); + Expect(SamePath(actual, expected), "packaged Slang install layout is accepted"); + + std::filesystem::remove_all(root); +} + +void TestLegacyThirdPartyScanStillAcceptsVersionedFolders() +{ + const std::filesystem::path root = MakeTestRoot("legacy-scan"); + const std::filesystem::path expected = root / "3rdParty" / "slang-custom-build" / "bin" / "slangc.exe"; + WriteFakeSlangCompiler(expected); + + ScopedEnvironmentVariable slangRoot("SLANG_ROOT", ""); + ScopedEnvironmentVariable thirdPartyRoot("THIRD_PARTY_ROOT", ""); + + std::filesystem::path actual; + std::string error; + Expect(ShaderCompiler::FindSlangCompilerPath(root, actual, error), "legacy 3rdParty scan finds versioned Slang folders"); + Expect(SamePath(actual, expected), "legacy scan returns the discovered compiler path"); + + std::filesystem::remove_all(root); +} +} + +int main() +{ + TestSlangRootWinsOverThirdPartyRoot(); + TestThirdPartyRootUsesCMakeBundleLayout(); + TestRepoVideoIoBundleWinsOverLegacyRepoBundle(); + TestPackagedSlangFolderIsAccepted(); + TestLegacyThirdPartyScanStillAcceptsVersionedFolders(); + + if (gFailures != 0) + { + std::cerr << gFailures << " ShaderCompiler lookup test failure(s).\n"; + return 1; + } + + std::cout << "ShaderCompiler lookup tests passed.\n"; + return 0; +}