Slang seperation
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
#include "NativeHandles.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
@@ -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<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(
|
||||
@@ -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<DWORD>(sizeof(slangRootBuffer)));
|
||||
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
|
||||
return FindSlangCompilerPath(mRepoRoot, compilerPath, error);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
compilerPath = candidate;
|
||||
if (UseEnvironmentSlangRoot("SLANG_ROOT", compilerPath))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
|
||||
if (!std::filesystem::exists(thirdPartyRoot))
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
203
tests/ShaderCompilerLookupTests.cpp
Normal file
203
tests/ShaderCompilerLookupTests.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user