Slang seperation
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m21s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-30 21:05:30 +10:00
parent 067c606092
commit fbc2851ccb
7 changed files with 322 additions and 36 deletions

View File

@@ -101,6 +101,8 @@ cmake --preset vs2022-x64-debug `
-DDECKLINK_SDK_ROOT="D:/SDKs/Blackmagic DeckLink SDK 16.0" -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 ## Build
Configure and build the native app: Configure and build the native app:

View File

@@ -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: 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. - 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. - 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. - 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. - 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 ## 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`. - UI production build passed with `npm.cmd run build`.
- Native debug build passed with `cmake --build --preset build-debug --parallel`. - Native debug build passed for `RenderCadenceCompositor`, `ShaderSlangValidationTests`, and `ShaderCompilerLookupTests`.
- Native tests passed 24 of 24 with `ctest --test-dir build\vs2022-x64-debug -C Debug --output-on-failure`. - 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. - `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. 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.

View File

@@ -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 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. `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: Current runtime shader support is deliberately limited to stateless full-frame packages:

View File

@@ -3,8 +3,9 @@
#include "NativeHandles.h" #include "NativeHandles.h"
#include <fstream>
#include <cctype> #include <cctype>
#include <cstdlib>
#include <fstream>
#include <regex> #include <regex>
#include <sstream> #include <sstream>
#include <vector> #include <vector>
@@ -150,6 +151,90 @@ std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned h
source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n"; source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n";
return source.str(); 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<std::filesystem::path> 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( ShaderCompiler::ShaderCompiler(
@@ -218,38 +303,25 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{ {
char slangRootBuffer[MAX_PATH] = {}; return FindSlangCompilerPath(mRepoRoot, compilerPath, error);
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer))); }
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
{ bool ShaderCompiler::FindSlangCompilerPath(const std::filesystem::path& repoRoot, std::filesystem::path& compilerPath, std::string& error)
std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe"; {
if (std::filesystem::exists(candidate)) if (UseEnvironmentSlangRoot("SLANG_ROOT", compilerPath))
{
compilerPath = candidate;
return true; return true;
}
}
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty"; const std::filesystem::path thirdPartyRoot = EnvironmentPath("THIRD_PARTY_ROOT");
if (!std::filesystem::exists(thirdPartyRoot)) if (UseThirdPartyRootIfPresent(thirdPartyRoot, compilerPath))
{
error = "3rdParty directory was not found under the repository root.";
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true; 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; return false;
} }

View File

@@ -15,6 +15,8 @@ public:
const std::filesystem::path& patchedGlslPath, const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames); 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; bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const;
private: private:

View File

@@ -137,6 +137,11 @@ add_video_shader_test(ShaderSlangValidationTests
"${TEST_DIR}/ShaderSlangValidationTests.cpp" "${TEST_DIR}/ShaderSlangValidationTests.cpp"
) )
add_video_shader_test(ShaderCompilerLookupTests
"${SRC_DIR}/shader/ShaderCompiler.cpp"
"${TEST_DIR}/ShaderCompilerLookupTests.cpp"
)
add_video_shader_test(Std140BufferTests add_video_shader_test(Std140BufferTests
"${TEST_DIR}/Std140BufferTests.cpp" "${TEST_DIR}/Std140BufferTests.cpp"
) )
@@ -169,5 +174,5 @@ set_tests_properties(RenderCadenceCompositorLoggerTests PROPERTIES
) )
set_tests_properties(ShaderSlangValidationTests PROPERTIES set_tests_properties(ShaderSlangValidationTests PROPERTIES
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}" ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT};THIRD_PARTY_ROOT=${THIRD_PARTY_ROOT}"
) )

View File

@@ -0,0 +1,203 @@
#include "ShaderCompiler.h"
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
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;
}