Compare commits
2 Commits
4e2ac4a091
...
07a5c91427
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a5c91427 | |||
| 53b980913b |
@@ -59,7 +59,7 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --build --preset build-debug
|
run: cmake --build --preset build-debug
|
||||||
|
|
||||||
- name: Run Native Tests
|
- name: Run Native Tests And Shader Validation
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --build --preset build-debug --target RUN_TESTS
|
run: cmake --build --preset build-debug --target RUN_TESTS
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,29 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||||
|
|
||||||
|
add_executable(ShaderSlangValidationTests
|
||||||
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ShaderSlangValidationTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
|
||||||
|
set_tests_properties(ShaderSlangValidationTests PROPERTIES
|
||||||
|
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(OscServerTests
|
add_executable(OscServerTests
|
||||||
"${APP_DIR}/control/OscServer.cpp"
|
"${APP_DIR}/control/OscServer.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Video Shader
|
# Video Shader
|
||||||
|
|
||||||
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI.
|
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
|
||||||
|
|
||||||
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
|
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Native app internals are grouped by boundary:
|
|||||||
- Windows with Visual Studio 2022 C++ tooling.
|
- Windows with Visual Studio 2022 C++ tooling.
|
||||||
- CMake 3.24 or newer.
|
- CMake 3.24 or newer.
|
||||||
- Node.js and npm for the control UI.
|
- Node.js and npm for the control UI.
|
||||||
- Blackmagic Desktop Video drivers and a DeckLink device.
|
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
||||||
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
||||||
|
|
||||||
Default expected Slang path:
|
Default expected Slang path:
|
||||||
@@ -85,6 +85,8 @@ dist/VideoShader/
|
|||||||
shaders/
|
shaders/
|
||||||
3rdParty/slang/bin/
|
3rdParty/slang/bin/
|
||||||
ui/dist/
|
ui/dist/
|
||||||
|
docs/
|
||||||
|
SHADER_CONTRACT.md
|
||||||
runtime/templates/
|
runtime/templates/
|
||||||
third_party_notices/
|
third_party_notices/
|
||||||
```
|
```
|
||||||
@@ -119,6 +121,8 @@ Current native test coverage includes:
|
|||||||
- JSON parsing and serialization.
|
- JSON parsing and serialization.
|
||||||
- Parameter normalization and preset filename safety.
|
- Parameter normalization and preset filename safety.
|
||||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||||
|
- Video I/O format helpers, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||||
|
- OSC packet parsing.
|
||||||
|
|
||||||
## Runtime Configuration
|
## Runtime Configuration
|
||||||
|
|
||||||
@@ -139,7 +143,7 @@ Current native test coverage includes:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
|
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
|
||||||
|
|
||||||
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
||||||
|
|
||||||
@@ -216,6 +220,7 @@ Runtime-generated files are intentionally ignored:
|
|||||||
- `runtime/shader_cache/active_shader.frag`
|
- `runtime/shader_cache/active_shader.frag`
|
||||||
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
||||||
- `runtime/stack_presets/*.json`
|
- `runtime/stack_presets/*.json`
|
||||||
|
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
|
||||||
|
|
||||||
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
||||||
|
|
||||||
@@ -244,16 +249,15 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
|
|||||||
|
|
||||||
- Audio.
|
- Audio.
|
||||||
- Genlock.
|
- Genlock.
|
||||||
- Find a better UI library for react.
|
- Find a better UI library for React.
|
||||||
- Logs.
|
- Logs.
|
||||||
- Continue source cleanup/refactoring. Pass 3 done
|
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
|
||||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||||
- Add WebView2
|
- Add WebView2
|
||||||
- move to MSDF, typography rasterisation
|
- MSDF typography rasterisation
|
||||||
- better shader search UI, pass 1
|
- More shader-library organisation and filtering as the built-in library grows.
|
||||||
- More comprehensive greenscreen shader
|
|
||||||
- linear compositing?
|
- linear compositing?
|
||||||
- compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store
|
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
||||||
- allow shaders to read other shaders data store based on name? or output over OSC
|
- allow shaders to read other shaders data store based on name? or output over OSC
|
||||||
- Mipmappong for shader declared textures
|
- Mipmapping for shader-declared textures
|
||||||
- Multipass for shaders at request
|
- Multipass for shaders at request
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ Fields:
|
|||||||
- `uv`: normalized texture coordinates, usually `0..1`.
|
- `uv`: normalized texture coordinates, usually `0..1`.
|
||||||
- `sourceColor`: decoded RGBA source video at `uv`.
|
- `sourceColor`: decoded RGBA source video at `uv`.
|
||||||
- `inputResolution`: decoded input video resolution in pixels.
|
- `inputResolution`: decoded input video resolution in pixels.
|
||||||
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured video I/O output mode.
|
||||||
- `time`: elapsed runtime time in seconds.
|
- `time`: elapsed runtime time in seconds.
|
||||||
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
|
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
|
||||||
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
||||||
@@ -177,8 +177,8 @@ Fields:
|
|||||||
Color/precision notes:
|
Color/precision notes:
|
||||||
|
|
||||||
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
||||||
- The host prefers 10-bit DeckLink YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
|
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
|
||||||
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than the packed DeckLink byte formats.
|
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
|
||||||
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
||||||
|
|
||||||
## Helper Functions
|
## Helper Functions
|
||||||
|
|||||||
@@ -190,6 +190,18 @@ 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] = {};
|
||||||
|
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
|
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
|
||||||
if (!std::filesystem::exists(thirdPartyRoot))
|
if (!std::filesystem::exists(thirdPartyRoot))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ components:
|
|||||||
$ref: "#/components/schemas/VideoStatus"
|
$ref: "#/components/schemas/VideoStatus"
|
||||||
decklink:
|
decklink:
|
||||||
$ref: "#/components/schemas/DeckLinkStatus"
|
$ref: "#/components/schemas/DeckLinkStatus"
|
||||||
|
videoIO:
|
||||||
|
$ref: "#/components/schemas/VideoIOStatus"
|
||||||
performance:
|
performance:
|
||||||
$ref: "#/components/schemas/PerformanceStatus"
|
$ref: "#/components/schemas/PerformanceStatus"
|
||||||
shaders:
|
shaders:
|
||||||
@@ -415,6 +417,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
DeckLinkStatus:
|
DeckLinkStatus:
|
||||||
type: object
|
type: object
|
||||||
|
deprecated: true
|
||||||
|
description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
|
||||||
properties:
|
properties:
|
||||||
modelName:
|
modelName:
|
||||||
type: string
|
type: string
|
||||||
@@ -430,6 +434,26 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
statusMessage:
|
statusMessage:
|
||||||
type: string
|
type: string
|
||||||
|
VideoIOStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
example: decklink
|
||||||
|
modelName:
|
||||||
|
type: string
|
||||||
|
supportsInternalKeying:
|
||||||
|
type: boolean
|
||||||
|
supportsExternalKeying:
|
||||||
|
type: boolean
|
||||||
|
keyerInterfaceAvailable:
|
||||||
|
type: boolean
|
||||||
|
externalKeyingRequested:
|
||||||
|
type: boolean
|
||||||
|
externalKeyingActive:
|
||||||
|
type: boolean
|
||||||
|
statusMessage:
|
||||||
|
type: string
|
||||||
PerformanceStatus:
|
PerformanceStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Generated files:
|
|||||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
||||||
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
|
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
|
||||||
- `stack_presets/*.json`: user-saved layer stack presets.
|
- `stack_presets/*.json`: user-saved layer stack presets.
|
||||||
|
- `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.
|
||||||
|
|
||||||
Git policy:
|
Git policy:
|
||||||
|
|
||||||
|
|||||||
106
tests/ShaderSlangValidationTests.cpp
Normal file
106
tests/ShaderSlangValidationTests.cpp
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#include "ShaderCompiler.h"
|
||||||
|
#include "ShaderPackageRegistry.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Fail(const std::string& message)
|
||||||
|
{
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path FindRepoRoot()
|
||||||
|
{
|
||||||
|
std::filesystem::path current = std::filesystem::current_path();
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(current / "shaders") &&
|
||||||
|
std::filesystem::exists(current / "runtime" / "templates" / "shader_wrapper.slang.in"))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current.has_parent_path() || current.parent_path() == current)
|
||||||
|
return std::filesystem::path();
|
||||||
|
current = current.parent_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsExpectedInvalidPackage(const ShaderPackageStatus& status)
|
||||||
|
{
|
||||||
|
return status.id.find("broken-shader-example") != std::string::npos &&
|
||||||
|
status.error.find("Unsupported parameter type") != std::string::npos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
const std::filesystem::path repoRoot = FindRepoRoot();
|
||||||
|
if (repoRoot.empty())
|
||||||
|
{
|
||||||
|
Fail("Could not locate repository root from current working directory.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, ShaderPackage> packagesById;
|
||||||
|
std::vector<std::string> packageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> packageStatuses;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
ShaderPackageRegistry registry(12);
|
||||||
|
if (!registry.Scan(repoRoot / "shaders", packagesById, packageOrder, packageStatuses, error))
|
||||||
|
{
|
||||||
|
Fail("Shader package scan failed: " + error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ShaderPackageStatus& status : packageStatuses)
|
||||||
|
{
|
||||||
|
if (status.available || IsExpectedInvalidPackage(status))
|
||||||
|
continue;
|
||||||
|
Fail("Unexpected invalid shader package '" + status.id + "': " + status.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path validationRoot = repoRoot / "runtime" / "shader_validation";
|
||||||
|
const std::filesystem::path wrapperPath = validationRoot / "validation_shader_wrapper.slang";
|
||||||
|
const std::filesystem::path generatedGlslPath = validationRoot / "validation_shader.raw.frag";
|
||||||
|
const std::filesystem::path patchedGlslPath = validationRoot / "validation_shader.frag";
|
||||||
|
|
||||||
|
ShaderCompiler compiler(repoRoot, wrapperPath, generatedGlslPath, patchedGlslPath, 12);
|
||||||
|
for (const std::string& packageId : packageOrder)
|
||||||
|
{
|
||||||
|
auto packageIt = packagesById.find(packageId);
|
||||||
|
if (packageIt == packagesById.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
std::string compileError;
|
||||||
|
if (!compiler.BuildLayerFragmentShaderSource(packageIt->second, fragmentShaderSource, compileError))
|
||||||
|
{
|
||||||
|
Fail("Shader package '" + packageId + "' failed Slang validation: " + compileError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
|
||||||
|
Fail("Shader package '" + packageId + "' generated GLSL without the expected patched GLSL version header.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code removeError;
|
||||||
|
std::filesystem::remove_all(validationRoot, removeError);
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " shader Slang validation failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Validated " << packagesById.size() << " shader package(s) through Slang.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user