16 Commits

Author SHA1 Message Date
27bf2ae45c doc updates
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 18:49:27 +10:00
1ea44ba3ae fix typo
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m19s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 18:43:48 +10:00
0af9a72937 removed redundant code
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:40:56 +10:00
d650cac857 control layout updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:28:28 +10:00
a0cc86f189 description updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:11:26 +10:00
f322abf79a updates
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:07:45 +10:00
eede6938cb Update multipass shader test
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 17:41:53 +10:00
ad24a20fdb Multi pass test
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-08 17:40:09 +10:00
5ae43513a7 annotations
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 17:35:48 +10:00
cc23e73d51 Removed uneeded code 2026-05-08 17:33:57 +10:00
f85abef237 Multi pass
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:28:48 +10:00
596d370f43 Add manifest support for pass declarations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:19:30 +10:00
87cb55b80b Layer program split
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Successful in 2m26s
2026-05-08 17:10:29 +10:00
f458eb0130 Texture binding 2026-05-08 17:04:28 +10:00
7d8f9a39d1 render target pool
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m15s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:59:43 +10:00
5b6e30ad13 Render class 2026-05-08 16:55:16 +10:00
86 changed files with 3082 additions and 1274 deletions

View File

@@ -64,8 +64,11 @@ set(APP_SOURCES
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h" "${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp" "${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h" "${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp" "${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
"${APP_DIR}/gl/renderer/OpenGLRenderer.h" "${APP_DIR}/gl/renderer/OpenGLRenderer.h"
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
"${APP_DIR}/gl/renderer/RenderTargetPool.h"
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp" "${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h" "${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp" "${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"

View File

@@ -2,7 +2,7 @@
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. 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. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
## Repository Layout ## Repository Layout
@@ -62,6 +62,14 @@ npm run build
The native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development. The native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development.
The control UI provides:
- A searchable shader library for adding layers.
- Compact parameter rows with inline descriptions and OSC copy controls.
- Stack save/recall presets.
- Manual shader reload.
- Screenshot capture from the final output render target.
## Package ## Package
Build the UI, build the native Release target, then install into a portable runtime folder: Build the UI, build the native Release target, then install into a portable runtime folder:
@@ -121,8 +129,9 @@ 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. - Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
- OSC packet parsing. - OSC packet parsing.
- Slang validation for every available shader package.
## Runtime Configuration ## Runtime Configuration
@@ -182,7 +191,11 @@ http://127.0.0.1:<serverPort>/docs
Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket. Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
The control UI also has a Screenshot button. It queues a capture of the final output render target and writes a PNG under: The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, queues shader compilation, refreshes shader availability/errors, and keeps the previous working shader stack running if a changed shader fails to compile.
Each parameter row also includes a small **OSC** button. Clicking it copies that parameter's OSC route to the clipboard.
The control UI also has a **Screenshot** button. It queues a capture of the final output render target and writes a PNG under:
```text ```text
runtime/screenshots/ runtime/screenshots/
@@ -206,10 +219,11 @@ Each shader package lives under:
shaders/<id>/ shaders/<id>/
shader.json shader.json
shader.slang shader.slang
optional-extra-pass.slang
optional-font-or-texture-assets optional-font-or-texture-assets
``` ```
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
## Generated Files ## Generated Files
@@ -249,15 +263,13 @@ 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.
- Logs. - Logs.
- Add more video I/O backends now that the DeckLink path is behind `videoio/`. - 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 for an embedded native control surface.
- MSDF typography rasterisation - MSDF typography rasterisation
- More shader-library organisation and filtering as the built-in library grows. - More shader-library organisation and filtering as the built-in library grows.
- linear compositing? - Optional linear-light compositing mode.
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage - 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
- Mipmapping for shader-declared textures - Mipmapping for shader-declared textures
- Multipass for shaders at request

View File

@@ -55,7 +55,7 @@ float4 shadeVideo(ShaderContext context)
} }
``` ```
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. You can also use **Reload shaders** in the control UI to manually rescan the shader library.
## Guidance For Shaders ## Guidance For Shaders
@@ -80,7 +80,7 @@ Important rules:
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation. - If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
- If the source license is unclear or incompatible, do not add the shader package. - If the source license is unclear or incompatible, do not add the shader package.
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. CI also runs shader validation, so every available package in `shaders/` should compile successfully. Intentionally broken examples should stay visibly marked as broken rather than pretending to be production shaders.
## Manifest Fields ## Manifest Fields
@@ -97,10 +97,13 @@ Optional fields:
- `description`: display/help text for the shader library. - `description`: display/help text for the shader library.
- `category`: UI grouping label. - `category`: UI grouping label.
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`. - `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
- `textures`: texture assets to load and expose as samplers. - `textures`: texture assets to load and expose as samplers.
- `fonts`: packaged font assets for live text parameters. - `fonts`: packaged font assets for live text parameters.
- `temporal`: history-buffer requirements. - `temporal`: history-buffer requirements.
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
Shader-visible identifiers must be valid Slang-style identifiers: Shader-visible identifiers must be valid Slang-style identifiers:
- `entryPoint` - `entryPoint`
@@ -110,6 +113,87 @@ Shader-visible identifiers must be valid Slang-style identifiers:
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID. Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
## Render Passes
Most shaders should omit `passes`. The runtime then creates one implicit pass:
```json
{
"id": "main",
"source": "shader.slang",
"entryPoint": "shadeVideo",
"output": "layerOutput"
}
```
Advanced shaders may declare explicit passes. All passes may live in one `.slang` file by using different `entryPoint` values, or they may be split across multiple source files:
```json
{
"passes": [
{
"id": "blurX",
"source": "blur-x.slang",
"entryPoint": "blurHorizontal",
"inputs": ["layerInput"],
"output": "blurredX"
},
{
"id": "final",
"source": "final.slang",
"entryPoint": "finish",
"inputs": ["blurredX"],
"output": "layerOutput"
}
]
}
```
Pass fields:
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
- `source`: required Slang source path relative to the package directory.
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
- `inputs`: optional list of named inputs. The first input is used as the pass input texture.
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
Pass input names:
- `layerInput`: the input to this layer, before any of its passes run.
- `previousPass`: the previous pass output in this layer. If there is no previous pass, this falls back to `layerInput`.
- Any earlier pass `id` or `output` name from the same layer.
If `inputs` is omitted, the first pass samples `layerInput` and later passes sample `previousPass`.
Single-file multipass example:
```json
{
"passes": [
{
"id": "mask",
"source": "shader.slang",
"entryPoint": "makeMask",
"output": "maskBuffer"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finish",
"inputs": ["maskBuffer"],
"output": "layerOutput"
}
]
}
```
Pass output names:
- `layerOutput`: the final visible output of this layer.
- Any other name creates an intermediate 16-bit float render target that later passes may sample.
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
## Slang Entry Point ## Slang Entry Point
Your shader file must implement the manifest `entryPoint`. Your shader file must implement the manifest `entryPoint`.
@@ -177,7 +261,7 @@ 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 current DeckLink backend prefers 10-bit 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. If external keying is enabled, output prefers 10-bit YUVA (`Ay10`) when supported so shader alpha can drive the key signal, then falls back to 8-bit BGRA.
- 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. - 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.
@@ -547,6 +631,8 @@ When a shader compiles, the runtime writes generated files under `runtime/shader
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader. These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
For multipass shaders, these files reflect the most recently compiled pass. If a package has several passes, the reported compile error and pass name are usually more useful than assuming the cache contains the first pass.
## Common Pitfalls ## Common Pitfalls
- Do not use hyphens in parameter IDs, texture IDs, or entry point names. - Do not use hyphens in parameter IDs, texture IDs, or entry point names.

View File

@@ -181,6 +181,7 @@
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" /> <ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" /> <ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" /> <ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
<ClCompile Include="gl\renderer\RenderTargetPool.cpp" />
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" /> <ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" /> <ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" /> <ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
@@ -206,7 +207,9 @@
<ClInclude Include="gl\OpenGLComposite.h" /> <ClInclude Include="gl\OpenGLComposite.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" /> <ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" /> <ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h" />
<ClInclude Include="gl\renderer\OpenGLRenderer.h" /> <ClInclude Include="gl\renderer\OpenGLRenderer.h" />
<ClInclude Include="gl\renderer\RenderTargetPool.h" />
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h" /> <ClInclude Include="gl\shader\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h" /> <ClInclude Include="gl\pipeline\PngScreenshotWriter.h" />
<ClInclude Include="gl\shader\ShaderBuildQueue.h" /> <ClInclude Include="gl\shader\ShaderBuildQueue.h" />

View File

@@ -36,6 +36,9 @@
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp"> <ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\renderer\RenderTargetPool.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp"> <ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -92,9 +95,15 @@
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h"> <ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\renderer\OpenGLRenderer.h"> <ClInclude Include="gl\renderer\OpenGLRenderer.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\renderer\RenderTargetPool.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h"> <ClInclude Include="gl\shader\OpenGLShaderPrograms.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>

View File

@@ -105,7 +105,8 @@ bool OpenGLComposite::InitVideoIO()
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR); MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false; return false;
} }
if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason)) const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
goto error; goto error;
if (! CheckOpenGLExtensions()) if (! CheckOpenGLExtensions())

View File

@@ -2,6 +2,8 @@
#include "GlRenderConstants.h" #include "GlRenderConstants.h"
#include <map>
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) : OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
mRenderer(renderer) mRenderer(renderer)
{ {
@@ -43,26 +45,16 @@ void OpenGLRenderPass::Render(
} }
else else
{ {
GLuint sourceTexture = mRenderer.DecodedTexture(); const std::vector<RenderPassDescriptor> passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
GLuint sourceFrameBuffer = mRenderer.DecodeFramebuffer(); for (const RenderPassDescriptor& pass : passes)
for (std::size_t index = 0; index < layerStates.size() && index < layerPrograms.size(); ++index)
{ {
const std::size_t remaining = layerStates.size() - index; RenderLayerPass(
const bool writeToMain = (remaining % 2) == 1; pass,
RenderShaderProgram(
sourceTexture,
writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(),
layerPrograms[index],
layerStates[index],
inputFrameWidth, inputFrameWidth,
inputFrameHeight, inputFrameHeight,
historyCap, historyCap,
updateTextBinding, updateTextBinding,
updateGlobalParams); updateGlobalParams);
if (layerStates[index].temporalHistorySource == TemporalHistorySource::PreLayerInput)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(layerStates[index].layerId, sourceFrameBuffer, inputFrameWidth, inputFrameHeight);
sourceTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
sourceFrameBuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
} }
} }
@@ -97,10 +89,158 @@ void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned input
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
} }
std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
const std::vector<RuntimeRenderState>& layerStates,
std::vector<LayerProgram>& layerPrograms) const
{
// Flatten the layer stack into concrete GL passes. A layer may now contain
// several shader passes, but the outer stack still sees one visible output
// per layer.
std::vector<RenderPassDescriptor> passes;
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
std::size_t descriptorCount = 0;
for (std::size_t index = 0; index < passCount; ++index)
descriptorCount += layerPrograms[index].passes.size();
passes.reserve(descriptorCount);
GLuint sourceTexture = mRenderer.DecodedTexture();
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
for (std::size_t index = 0; index < passCount; ++index)
{
const RuntimeRenderState& state = layerStates[index];
LayerProgram& layerProgram = layerPrograms[index];
if (layerProgram.passes.empty())
continue;
// Preserve the original two-target layer ping-pong. Intermediate passes
// inside this layer are routed through pooled temporary targets instead.
const std::size_t remaining = layerStates.size() - index;
const bool writeToMain = (remaining % 2) == 1;
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
const GLuint layerInputTexture = sourceTexture;
const GLuint layerInputFramebuffer = sourceFramebuffer;
GLuint previousPassTexture = layerInputTexture;
GLuint previousPassFramebuffer = layerInputFramebuffer;
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
std::size_t temporaryTargetIndex = 0;
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
{
PassProgram& passProgram = layerProgram.passes[passIndex];
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
GLuint passSourceTexture = previousPassTexture;
GLuint passSourceFramebuffer = previousPassFramebuffer;
if (!passProgram.inputNames.empty())
{
// v1 multipass uses the first declared input as gVideoInput.
// Later inputs are parsed for forward compatibility.
const std::string& inputName = passProgram.inputNames.front();
if (inputName == "layerInput")
{
passSourceTexture = layerInputTexture;
passSourceFramebuffer = layerInputFramebuffer;
}
else if (inputName == "previousPass")
{
passSourceTexture = previousPassTexture;
passSourceFramebuffer = previousPassFramebuffer;
}
else
{
auto namedOutputIt = namedOutputs.find(inputName);
if (namedOutputIt != namedOutputs.end())
{
passSourceTexture = namedOutputIt->second.first;
passSourceFramebuffer = namedOutputIt->second.second;
}
}
}
GLuint passDestinationTexture = layerOutputTexture;
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
RenderPassOutputTarget outputTarget = layerOutputTarget;
if (!writesLayerOutput)
{
// Temporary targets are reserved when the shader stack is
// committed, avoiding texture allocation during playback.
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
{
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
++temporaryTargetIndex;
passDestinationTexture = temporaryTarget.texture;
passDestinationFramebuffer = temporaryTarget.framebuffer;
outputTarget = RenderPassOutputTarget::Temporary;
}
}
RenderPassDescriptor pass;
pass.kind = RenderPassKind::LayerEffect;
pass.outputTarget = outputTarget;
pass.passIndex = passes.size();
pass.passId = passProgram.passId;
pass.layerId = state.layerId;
pass.shaderId = state.shaderId;
pass.sourceTexture = passSourceTexture;
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
pass.destinationTexture = passDestinationTexture;
pass.destinationFramebuffer = passDestinationFramebuffer;
pass.layerProgram = &layerProgram;
pass.passProgram = &passProgram;
pass.layerState = &state;
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
passes.push_back(pass);
// A later pass can reference either the explicit output name or the
// pass id, which keeps small manifests pleasant to write.
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
previousPassTexture = passDestinationTexture;
previousPassFramebuffer = passDestinationFramebuffer;
}
sourceTexture = layerOutputTexture;
sourceFramebuffer = layerOutputFramebuffer;
}
return passes;
}
void OpenGLRenderPass::RenderLayerPass(
const RenderPassDescriptor& pass,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
if (pass.passProgram == nullptr || pass.layerState == nullptr)
return;
RenderShaderProgram(
pass.sourceTexture,
pass.destinationFramebuffer,
*pass.passProgram,
*pass.layerState,
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
if (pass.capturePreLayerHistory)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderShaderProgram( void OpenGLRenderPass::RenderShaderProgram(
GLuint sourceTexture, GLuint sourceTexture,
GLuint destinationFrameBuffer, GLuint destinationFrameBuffer,
LayerProgram& layerProgram, PassProgram& passProgram,
const RuntimeRenderState& state, const RuntimeRenderState& state,
unsigned inputFrameWidth, unsigned inputFrameWidth,
unsigned inputFrameHeight, unsigned inputFrameHeight,
@@ -108,7 +248,7 @@ void OpenGLRenderPass::RenderShaderProgram(
const TextBindingUpdater& updateTextBinding, const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams) const GlobalParamsUpdater& updateGlobalParams)
{ {
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings) for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
{ {
std::string textError; std::string textError;
if (!updateTextBinding(state, textBinding, textError)) if (!updateTextBinding(state, textBinding, textError))
@@ -118,52 +258,18 @@ void OpenGLRenderPass::RenderShaderProgram(
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight); glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit); const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
glBindTexture(GL_TEXTURE_2D, sourceTexture); const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
mRenderer.TemporalHistory().BindSamplers(state, sourceTexture, historyCap); const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
BindLayerTextureAssets(layerProgram); mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
glBindVertexArray(mRenderer.FullscreenVertexArray()); glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(layerProgram.program); glUseProgram(passProgram.program);
// The UBO is shared by every pass in a layer; texture routing is what
// changes from pass to pass.
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId)); updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
glDrawArrays(GL_TRIANGLES, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0); glUseProgram(0);
glBindVertexArray(0); glBindVertexArray(0);
UnbindLayerTextureAssets(layerProgram, historyCap); mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
}
void OpenGLRenderPass::BindLayerTextureAssets(const LayerProgram& layerProgram)
{
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < layerProgram.textureBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textureBindings[index].texture);
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(layerProgram.textureBindings.size());
for (std::size_t index = 0; index < layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + textTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textBindings[index].texture);
}
glActiveTexture(GL_TEXTURE0);
}
void OpenGLRenderPass::UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap)
{
for (unsigned index = 0; index < historyCap; ++index)
{
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + historyCap + index);
glBindTexture(GL_TEXTURE_2D, 0);
}
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < layerProgram.textureBindings.size() + layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, 0);
}
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
} }

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "OpenGLRenderer.h" #include "OpenGLRenderer.h"
#include "RenderPassDescriptor.h"
#include "ShaderTextureBindings.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include "VideoIOFormat.h" #include "VideoIOFormat.h"
@@ -12,6 +14,7 @@ class OpenGLRenderPass
{ {
public: public:
using LayerProgram = OpenGLRenderer::LayerProgram; using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>; using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>; using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
@@ -30,18 +33,27 @@ public:
private: private:
void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat); void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat);
std::vector<RenderPassDescriptor> BuildLayerPassDescriptors(
const std::vector<RuntimeRenderState>& layerStates,
std::vector<LayerProgram>& layerPrograms) const;
void RenderLayerPass(
const RenderPassDescriptor& pass,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams);
void RenderShaderProgram( void RenderShaderProgram(
GLuint sourceTexture, GLuint sourceTexture,
GLuint destinationFrameBuffer, GLuint destinationFrameBuffer,
LayerProgram& layerProgram, PassProgram& passProgram,
const RuntimeRenderState& state, const RuntimeRenderState& state,
unsigned inputFrameWidth, unsigned inputFrameWidth,
unsigned inputFrameHeight, unsigned inputFrameHeight,
unsigned historyCap, unsigned historyCap,
const TextBindingUpdater& updateTextBinding, const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams); const GlobalParamsUpdater& updateGlobalParams);
void BindLayerTextureAssets(const LayerProgram& layerProgram);
void UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap);
OpenGLRenderer& mRenderer; OpenGLRenderer& mRenderer;
ShaderTextureBindings mTextureBindings;
}; };

View File

@@ -34,8 +34,8 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer()); glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady) if (mOutputReady)
mOutputReady(); mOutputReady();
if (state.outputPixelFormat == VideoIOPixelFormat::V210) if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
PackOutputForV210(state); PackOutputFor10Bit(state);
glFlush(); glFlush();
const auto renderEndTime = std::chrono::steady_clock::now(); const auto renderEndTime = std::chrono::steady_clock::now();
@@ -50,7 +50,7 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
return true; return true;
} }
void OpenGLRenderPipeline::PackOutputForV210(const VideoIOState& state) void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
{ {
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height); glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
@@ -64,10 +64,13 @@ void OpenGLRenderPipeline::PackOutputForV210(const VideoIOState& state)
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution"); const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words"); const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
const GLint packFormatLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputPackFormat");
if (outputResolutionLocation >= 0) if (outputResolutionLocation >= 0)
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height)); glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
if (activeWordsLocation >= 0) if (activeWordsLocation >= 0)
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width))); glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
if (packFormatLocation >= 0)
glUniform1i(packFormatLocation, state.outputPixelFormat == VideoIOPixelFormat::Yuva10 ? 2 : 1);
glDrawArrays(GL_TRIANGLES, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0); glUseProgram(0);
@@ -79,7 +82,7 @@ void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOut
{ {
glPixelStorei(GL_PACK_ALIGNMENT, 4); glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0); glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (state.outputPixelFormat == VideoIOPixelFormat::V210) if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
{ {
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes); glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);

View File

@@ -30,7 +30,7 @@ public:
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame); bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
private: private:
void PackOutputForV210(const VideoIOState& state); void PackOutputFor10Bit(const VideoIOState& state);
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame); void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
OpenGLRenderer& mRenderer; OpenGLRenderer& mRenderer;

View File

@@ -0,0 +1,39 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <gl/gl.h>
#include <cstddef>
#include <string>
enum class RenderPassKind
{
LayerEffect
};
enum class RenderPassOutputTarget
{
Temporary,
LayerTemp,
Composite
};
struct RenderPassDescriptor
{
RenderPassKind kind = RenderPassKind::LayerEffect;
RenderPassOutputTarget outputTarget = RenderPassOutputTarget::Composite;
std::size_t passIndex = 0;
std::string passId;
std::string layerId;
std::string shaderId;
GLuint sourceTexture = 0;
GLuint sourceFramebuffer = 0;
GLuint destinationTexture = 0;
GLuint destinationFramebuffer = 0;
OpenGLRenderer::LayerProgram* layerProgram = nullptr;
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
const RuntimeRenderState* layerState = nullptr;
bool capturePreLayerHistory = false;
};

View File

@@ -212,6 +212,29 @@ void TemporalHistoryBuffers::BindSamplers(const RuntimeRenderState& state, GLuin
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
} }
std::vector<GLuint> TemporalHistoryBuffers::ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const
{
std::vector<GLuint> textures;
textures.reserve(historyCap);
for (unsigned index = 0; index < historyCap; ++index)
textures.push_back(ResolveTexture(sourceHistoryRing, fallbackTexture, index));
return textures;
}
std::vector<GLuint> TemporalHistoryBuffers::ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const
{
const Ring* temporalRing = nullptr;
auto it = preLayerHistoryByLayerId.find(state.layerId);
if (it != preLayerHistoryByLayerId.end())
temporalRing = &it->second;
std::vector<GLuint> textures;
textures.reserve(historyCap);
for (unsigned index = 0; index < historyCap; ++index)
textures.push_back(temporalRing ? ResolveTexture(*temporalRing, fallbackTexture, index) : fallbackTexture);
return textures;
}
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
{ {
if (ring.filledCount == 0 || ring.slots.empty()) if (ring.filledCount == 0 || ring.slots.empty())

View File

@@ -40,6 +40,8 @@ public:
void PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight); void PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
void PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight); void PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
void BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap); void BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap);
std::vector<GLuint> ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const;
std::vector<GLuint> ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const;
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const; GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
unsigned SourceAvailableCount() const; unsigned SourceAvailableCount() const;
unsigned AvailableCountForLayer(const std::string& layerId) const; unsigned AvailableCountForLayer(const std::string& layerId) const;

View File

@@ -12,15 +12,6 @@ namespace
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
} }
void ConfigureDisplayFrameTexture(unsigned width, unsigned height)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
}
} }
bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error) bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error)
@@ -35,80 +26,32 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight); ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glGenTextures(1, &mDecodedTexture);
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0);
glGenTextures(1, &mLayerTempTexture);
glBindTexture(GL_TEXTURE_2D, mLayerTempTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &mDecodeFrameBuf);
glGenFramebuffers(1, &mLayerTempFrameBuf);
glGenFramebuffers(1, &mIdFrameBuf);
glGenFramebuffers(1, &mOutputFrameBuf);
glGenFramebuffers(1, &mOutputPackFrameBuf);
glGenRenderbuffers(1, &mIdColorBuf); glGenRenderbuffers(1, &mIdColorBuf);
glGenRenderbuffers(1, &mIdDepthBuf); glGenRenderbuffers(1, &mIdDepthBuf);
glGenVertexArrays(1, &mFullscreenVAO); glGenVertexArrays(1, &mFullscreenVAO);
glGenBuffers(1, &mGlobalParamsUBO); glGenBuffers(1, &mGlobalParamsUBO);
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf); if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize decode framebuffer.";
return false; return false;
} if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
return false;
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf); if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize layer framebuffer.";
return false; return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
glGenTextures(1, &mFBOTexture);
glBindTexture(GL_TEXTURE_2D, mFBOTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf); glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{ {
error = "Cannot initialize framebuffer."; error = "Cannot initialize framebuffer.";
return false; return false;
} }
glGenTextures(1, &mOutputTexture); if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
glBindTexture(GL_TEXTURE_2D, mOutputTexture);
ConfigureDisplayFrameTexture(outputFrameWidth, outputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize output framebuffer.";
return false; return false;
} if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
glGenTextures(1, &mOutputPackTexture);
glBindTexture(GL_TEXTURE_2D, mOutputPackTexture);
ConfigureByteFrameTexture(outputPackTextureWidth, outputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, mOutputPackFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputPackTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize output pack framebuffer.";
return false; return false;
}
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0); glBindRenderbuffer(GL_RENDERBUFFER, 0);
@@ -137,6 +80,11 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
mOutputPackFragmentShader = fragmentShader; mOutputPackFragmentShader = fragmentShader;
} }
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
{
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
}
void OpenGLRenderer::ResizeView(int width, int height) void OpenGLRenderer::ResizeView(int width, int height)
{ {
mViewWidth = width; mViewWidth = width;
@@ -169,7 +117,7 @@ void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigne
} }
} }
glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf); glBindFramebuffer(GL_READ_FRAMEBUFFER, OutputFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST); glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, mViewWidth, mViewHeight); glViewport(0, 0, mViewWidth, mViewHeight);
@@ -186,50 +134,21 @@ void OpenGLRenderer::DestroyResources()
glDeleteVertexArrays(1, &mFullscreenVAO); glDeleteVertexArrays(1, &mFullscreenVAO);
if (mGlobalParamsUBO != 0) if (mGlobalParamsUBO != 0)
glDeleteBuffers(1, &mGlobalParamsUBO); glDeleteBuffers(1, &mGlobalParamsUBO);
if (mDecodeFrameBuf != 0)
glDeleteFramebuffers(1, &mDecodeFrameBuf);
if (mLayerTempFrameBuf != 0)
glDeleteFramebuffers(1, &mLayerTempFrameBuf);
if (mIdFrameBuf != 0)
glDeleteFramebuffers(1, &mIdFrameBuf);
if (mOutputFrameBuf != 0)
glDeleteFramebuffers(1, &mOutputFrameBuf);
if (mOutputPackFrameBuf != 0)
glDeleteFramebuffers(1, &mOutputPackFrameBuf);
if (mIdColorBuf != 0) if (mIdColorBuf != 0)
glDeleteRenderbuffers(1, &mIdColorBuf); glDeleteRenderbuffers(1, &mIdColorBuf);
if (mIdDepthBuf != 0) if (mIdDepthBuf != 0)
glDeleteRenderbuffers(1, &mIdDepthBuf); glDeleteRenderbuffers(1, &mIdDepthBuf);
if (mCaptureTexture != 0) if (mCaptureTexture != 0)
glDeleteTextures(1, &mCaptureTexture); glDeleteTextures(1, &mCaptureTexture);
if (mDecodedTexture != 0)
glDeleteTextures(1, &mDecodedTexture);
if (mLayerTempTexture != 0)
glDeleteTextures(1, &mLayerTempTexture);
if (mFBOTexture != 0)
glDeleteTextures(1, &mFBOTexture);
if (mOutputTexture != 0)
glDeleteTextures(1, &mOutputTexture);
if (mOutputPackTexture != 0)
glDeleteTextures(1, &mOutputPackTexture);
if (mTextureUploadBuffer != 0) if (mTextureUploadBuffer != 0)
glDeleteBuffers(1, &mTextureUploadBuffer); glDeleteBuffers(1, &mTextureUploadBuffer);
mRenderTargets.Destroy();
mFullscreenVAO = 0; mFullscreenVAO = 0;
mGlobalParamsUBO = 0; mGlobalParamsUBO = 0;
mDecodeFrameBuf = 0;
mLayerTempFrameBuf = 0;
mIdFrameBuf = 0;
mOutputFrameBuf = 0;
mOutputPackFrameBuf = 0;
mIdColorBuf = 0; mIdColorBuf = 0;
mIdDepthBuf = 0; mIdDepthBuf = 0;
mCaptureTexture = 0; mCaptureTexture = 0;
mDecodedTexture = 0;
mLayerTempTexture = 0;
mFBOTexture = 0;
mOutputTexture = 0;
mOutputPackTexture = 0;
mTextureUploadBuffer = 0; mTextureUploadBuffer = 0;
mGlobalParamsUBOSize = 0; mGlobalParamsUBOSize = 0;
@@ -241,7 +160,9 @@ void OpenGLRenderer::DestroyResources()
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram) void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
{ {
for (LayerProgram::TextureBinding& binding : layerProgram.textureBindings) for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
{
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
{ {
if (binding.texture != 0) if (binding.texture != 0)
{ {
@@ -249,9 +170,9 @@ void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
binding.texture = 0; binding.texture = 0;
} }
} }
layerProgram.textureBindings.clear(); passProgram.textureBindings.clear();
for (LayerProgram::TextBinding& binding : layerProgram.textBindings) for (LayerProgram::TextBinding& binding : passProgram.textBindings)
{ {
if (binding.texture != 0) if (binding.texture != 0)
{ {
@@ -259,26 +180,28 @@ void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
binding.texture = 0; binding.texture = 0;
} }
} }
layerProgram.textBindings.clear(); passProgram.textBindings.clear();
if (layerProgram.program != 0) if (passProgram.program != 0)
{ {
glDeleteProgram(layerProgram.program); glDeleteProgram(passProgram.program);
layerProgram.program = 0; passProgram.program = 0;
} }
if (layerProgram.fragmentShader != 0) if (passProgram.fragmentShader != 0)
{ {
glDeleteShader(layerProgram.fragmentShader); glDeleteShader(passProgram.fragmentShader);
layerProgram.fragmentShader = 0; passProgram.fragmentShader = 0;
} }
if (layerProgram.vertexShader != 0) if (passProgram.vertexShader != 0)
{ {
glDeleteShader(layerProgram.vertexShader); glDeleteShader(passProgram.vertexShader);
layerProgram.vertexShader = 0; passProgram.vertexShader = 0;
} }
} }
layerProgram.passes.clear();
}
void OpenGLRenderer::DestroyLayerPrograms() void OpenGLRenderer::DestroyLayerPrograms()
{ {

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "GLExtensions.h" #include "GLExtensions.h"
#include "RenderTargetPool.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include "TemporalHistoryBuffers.h" #include "TemporalHistoryBuffers.h"
@@ -36,6 +37,12 @@ public:
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
GLuint shaderTextureBase = 0; GLuint shaderTextureBase = 0;
GLuint program = 0; GLuint program = 0;
GLuint vertexShader = 0; GLuint vertexShader = 0;
@@ -44,18 +51,21 @@ public:
std::vector<TextBinding> textBindings; std::vector<TextBinding> textBindings;
}; };
std::vector<PassProgram> passes;
};
GLuint CaptureTexture() const { return mCaptureTexture; } GLuint CaptureTexture() const { return mCaptureTexture; }
GLuint DecodedTexture() const { return mDecodedTexture; } GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
GLuint LayerTempTexture() const { return mLayerTempTexture; } GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
GLuint CompositeTexture() const { return mFBOTexture; } GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
GLuint OutputTexture() const { return mOutputTexture; } GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
GLuint OutputPackTexture() const { return mOutputPackTexture; } GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; } GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; } GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; } GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
GLuint CompositeFramebuffer() const { return mIdFrameBuf; } GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
GLuint OutputFramebuffer() const { return mOutputFrameBuf; } GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
GLuint OutputPackFramebuffer() const { return mOutputPackFrameBuf; } GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
GLuint FullscreenVertexArray() const { return mFullscreenVAO; } GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; } GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
GLuint DecodeProgram() const { return mDecodeProgram; } GLuint DecodeProgram() const { return mDecodeProgram; }
@@ -65,6 +75,9 @@ public:
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); } void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; } std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; } const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; } TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; } const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader); void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
@@ -80,17 +93,7 @@ public:
private: private:
GLuint mCaptureTexture = 0; GLuint mCaptureTexture = 0;
GLuint mDecodedTexture = 0;
GLuint mLayerTempTexture = 0;
GLuint mFBOTexture = 0;
GLuint mOutputTexture = 0;
GLuint mOutputPackTexture = 0;
GLuint mTextureUploadBuffer = 0; GLuint mTextureUploadBuffer = 0;
GLuint mDecodeFrameBuf = 0;
GLuint mLayerTempFrameBuf = 0;
GLuint mIdFrameBuf = 0;
GLuint mOutputFrameBuf = 0;
GLuint mOutputPackFrameBuf = 0;
GLuint mIdColorBuf = 0; GLuint mIdColorBuf = 0;
GLuint mIdDepthBuf = 0; GLuint mIdDepthBuf = 0;
GLuint mFullscreenVAO = 0; GLuint mFullscreenVAO = 0;
@@ -105,5 +108,6 @@ private:
int mViewWidth = 0; int mViewWidth = 0;
int mViewHeight = 0; int mViewHeight = 0;
std::vector<LayerProgram> mLayerPrograms; std::vector<LayerProgram> mLayerPrograms;
RenderTargetPool mRenderTargets;
TemporalHistoryBuffers mTemporalHistory; TemporalHistoryBuffers mTemporalHistory;
}; };

View File

@@ -0,0 +1,136 @@
#include "RenderTargetPool.h"
#include <cstddef>
namespace
{
void ConfigureRenderTargetTexture(
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, pixelFormat, pixelType, NULL);
}
}
bool RenderTargetPool::Create(
RenderTargetId id,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
const char* errorPrefix,
std::string& error)
{
RenderTarget& target = mTargets[TargetIndex(id)];
if (target.texture != 0 || target.framebuffer != 0)
{
error = std::string(errorPrefix) + " render target was already initialized.";
return false;
}
glGenTextures(1, &target.texture);
glBindTexture(GL_TEXTURE_2D, target.texture);
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &target.framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = std::string("Cannot initialize ") + errorPrefix + " framebuffer.";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
target.width = width;
target.height = height;
target.internalFormat = internalFormat;
target.pixelFormat = pixelFormat;
target.pixelType = pixelType;
return true;
}
bool RenderTargetPool::ReserveTemporaryTargets(
std::size_t count,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
std::string& error)
{
if (mTemporaryTargets.size() == count)
return true;
DestroyTemporaryTargets();
mTemporaryTargets.resize(count);
for (std::size_t index = 0; index < mTemporaryTargets.size(); ++index)
{
RenderTarget& target = mTemporaryTargets[index];
glGenTextures(1, &target.texture);
glBindTexture(GL_TEXTURE_2D, target.texture);
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &target.framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize temporary render target.";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
target.width = width;
target.height = height;
target.internalFormat = internalFormat;
target.pixelFormat = pixelFormat;
target.pixelType = pixelType;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return true;
}
void RenderTargetPool::DestroyTemporaryTargets()
{
for (RenderTarget& target : mTemporaryTargets)
{
if (target.framebuffer != 0)
glDeleteFramebuffers(1, &target.framebuffer);
if (target.texture != 0)
glDeleteTextures(1, &target.texture);
}
mTemporaryTargets.clear();
}
void RenderTargetPool::Destroy()
{
for (RenderTarget& target : mTargets)
{
if (target.framebuffer != 0)
glDeleteFramebuffers(1, &target.framebuffer);
if (target.texture != 0)
glDeleteTextures(1, &target.texture);
target = RenderTarget();
}
DestroyTemporaryTargets();
}
const RenderTarget& RenderTargetPool::Target(RenderTargetId id) const
{
return mTargets[TargetIndex(id)];
}

View File

@@ -0,0 +1,64 @@
#pragma once
#include "GLExtensions.h"
#include <array>
#include <string>
#include <vector>
enum class RenderTargetId
{
Decoded,
LayerTemp,
Composite,
Output,
OutputPack,
Count
};
struct RenderTarget
{
GLuint texture = 0;
GLuint framebuffer = 0;
unsigned width = 0;
unsigned height = 0;
GLenum internalFormat = GL_RGBA8;
GLenum pixelFormat = GL_RGBA;
GLenum pixelType = GL_UNSIGNED_BYTE;
};
class RenderTargetPool
{
public:
bool Create(
RenderTargetId id,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
const char* errorPrefix,
std::string& error);
bool ReserveTemporaryTargets(
std::size_t count,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
std::string& error);
void DestroyTemporaryTargets();
void Destroy();
GLuint Texture(RenderTargetId id) const { return Target(id).texture; }
GLuint Framebuffer(RenderTargetId id) const { return Target(id).framebuffer; }
const RenderTarget& Target(RenderTargetId id) const;
const RenderTarget& TemporaryTarget(std::size_t index) const { return mTemporaryTargets[index]; }
std::size_t TemporaryTargetCount() const { return mTemporaryTargets.size(); }
private:
static std::size_t TargetIndex(RenderTargetId id) { return static_cast<std::size_t>(id); }
std::array<RenderTarget, static_cast<std::size_t>(RenderTargetId::Count)> mTargets;
std::vector<RenderTarget> mTemporaryTargets;
};

View File

@@ -90,12 +90,17 @@ const char* kOutputPackFragmentShaderSource =
"layout(binding = 0) uniform sampler2D uOutputRgb;\n" "layout(binding = 0) uniform sampler2D uOutputRgb;\n"
"uniform vec2 uOutputVideoResolution;\n" "uniform vec2 uOutputVideoResolution;\n"
"uniform float uActiveV210Words;\n" "uniform float uActiveV210Words;\n"
"uniform int uOutputPackFormat;\n"
"in vec2 vTexCoord;\n" "in vec2 vTexCoord;\n"
"layout(location = 0) out vec4 fragColor;\n" "layout(location = 0) out vec4 fragColor;\n"
"vec3 rgbAt(int x, int y)\n" "vec4 rgbaAt(int x, int y)\n"
"{\n" "{\n"
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n" " ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0).rgb, vec3(0.0), vec3(1.0));\n" " return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0), vec4(0.0), vec4(1.0));\n"
"}\n"
"vec3 rgbAt(int x, int y)\n"
"{\n"
" return rgbaAt(x, y).rgb;\n"
"}\n" "}\n"
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n" "vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
"{\n" "{\n"
@@ -112,9 +117,35 @@ const char* kOutputPackFragmentShaderSource =
"{\n" "{\n"
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n" " return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n"
"}\n" "}\n"
"vec4 bigEndianWordToBytes(uint word)\n"
"{\n"
" return vec4(float((word >> 24) & 255u), float((word >> 16) & 255u), float((word >> 8) & 255u), float(word & 255u)) / 255.0;\n"
"}\n"
"vec4 packAy10Word(ivec2 outCoord)\n"
"{\n"
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
" if (outCoord.x >= size.x)\n"
" return vec4(0.0);\n"
" int pixelBase = (outCoord.x / 2) * 2;\n"
" int y = outCoord.y;\n"
" vec4 rgba0 = rgbaAt(pixelBase + 0, y);\n"
" vec4 rgba1 = rgbaAt(pixelBase + 1, y);\n"
" vec3 c0 = rgbToLegalYcbcr10(rgba0.rgb);\n"
" vec3 c1 = rgbToLegalYcbcr10(rgba1.rgb);\n"
" float chroma = (outCoord.x & 1) == 0 ? round((c0.y + c1.y) * 0.5) : round((c0.z + c1.z) * 0.5);\n"
" float alpha = round(clamp(((outCoord.x & 1) == 0 ? rgba0.a : rgba1.a), 0.0, 1.0) * 1023.0);\n"
" float luma = (outCoord.x & 1) == 0 ? c0.x : c1.x;\n"
" uint word = ((uint(luma) & 1023u) << 22) | ((uint(chroma) & 1023u) << 12) | ((uint(alpha) & 1023u) << 2);\n"
" return bigEndianWordToBytes(word);\n"
"}\n"
"void main()\n" "void main()\n"
"{\n" "{\n"
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n" " ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
" if (uOutputPackFormat == 2)\n"
" {\n"
" fragColor = packAy10Word(outCoord);\n"
" return;\n"
" }\n"
" if (float(outCoord.x) >= uActiveV210Words)\n" " if (float(outCoord.x) >= uActiveV210Words)\n"
" {\n" " {\n"
" fragColor = vec4(0.0);\n" " fragColor = vec4(0.0);\n"

View File

@@ -13,6 +13,20 @@ 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);
} }
std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::LayerProgram>& layerPrograms)
{
// Only one layer renders at a time, so the pool needs to cover the widest
// layer, not the sum of every intermediate pass in the stack.
std::size_t requiredTargets = 0;
for (const OpenGLRenderer::LayerProgram& layerProgram : layerPrograms)
{
const std::size_t internalPasses = layerProgram.passes.size() > 0 ? layerProgram.passes.size() - 1 : 0;
if (internalPasses > requiredTargets)
requiredTargets = internalPasses;
}
return requiredTargets;
}
} }
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) : OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
@@ -39,6 +53,8 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
return false; return false;
} }
// Initial startup still compiles synchronously; auto-reload uses the build
// queue so Slang/file work stays off the playback path.
std::vector<LayerProgram> newPrograms; std::vector<LayerProgram> newPrograms;
newPrograms.reserve(layerStates.size()); newPrograms.reserve(layerStates.size());
@@ -54,6 +70,15 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
newPrograms.push_back(layerProgram); newPrograms.push_back(layerProgram);
} }
std::string targetError;
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
return false;
}
DestroyLayerPrograms(); DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms); mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = layerStates; mCommittedLayerStates = layerStates;
@@ -85,13 +110,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
return false; return false;
} }
// The prepared build already contains GLSL text for each pass. This commit
// step performs the short GL work on the render thread.
std::vector<LayerProgram> newPrograms; std::vector<LayerProgram> newPrograms;
newPrograms.reserve(preparedBuild.layers.size()); newPrograms.reserve(preparedBuild.layers.size());
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers) for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
{ {
LayerProgram layerProgram; LayerProgram layerProgram;
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.fragmentShaderSource, layerProgram, errorMessageSize, errorMessage)) if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.passes, layerProgram, errorMessageSize, errorMessage))
{ {
for (LayerProgram& program : newPrograms) for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program); DestroySingleLayerProgram(program);
@@ -100,6 +127,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
newPrograms.push_back(layerProgram); newPrograms.push_back(layerProgram);
} }
std::string targetError;
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
return false;
}
DestroyLayerPrograms(); DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms); mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = preparedBuild.layerStates; mCommittedLayerStates = preparedBuild.layerStates;

View File

@@ -120,7 +120,7 @@ PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned output
{ {
PreparedLayerShader layer; PreparedLayerShader layer;
layer.state = state; layer.state = state;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, layer.fragmentShaderSource, build.message)) if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
{ {
build.succeeded = false; build.succeeded = false;
return build; return build;

View File

@@ -14,7 +14,7 @@ class RuntimeHost;
struct PreparedLayerShader struct PreparedLayerShader
{ {
RuntimeRenderState state; RuntimeRenderState state;
std::string fragmentShaderSource; std::vector<ShaderPassBuildSource> passes;
}; };
struct PreparedShaderBuild struct PreparedShaderBuild

View File

@@ -5,6 +5,7 @@
#include "GlShaderSources.h" #include "GlShaderSources.h"
#include <cstring> #include <cstring>
#include <utility>
#include <vector> #include <vector>
namespace namespace
@@ -27,27 +28,33 @@ ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHo
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage) bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{ {
std::string fragmentShaderSource; std::vector<ShaderPassBuildSource> passSources;
std::string loadError; std::string loadError;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError)) if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
{ {
CopyErrorMessage(loadError, errorMessageSize, errorMessage); CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false; return false;
} }
return CompilePreparedLayerProgram(state, fragmentShaderSource, layerProgram, errorMessageSize, errorMessage); return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
} }
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage) bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{ {
GLsizei errorBufferSize = 0; GLsizei errorBufferSize = 0;
std::string loadError;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId;
layerProgram.passes.clear();
for (const auto& passSource : passSources)
{
GLint compileResult = GL_FALSE; GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE; GLint linkResult = GL_FALSE;
std::string loadError; const char* fragmentSource = passSource.fragmentShaderSource.c_str();
std::vector<LayerProgram::TextureBinding> textureBindings;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER)); ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL); glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
@@ -56,6 +63,7 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false; return false;
} }
@@ -66,6 +74,7 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
if (compileResult == GL_FALSE) if (compileResult == GL_FALSE)
{ {
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage); glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false; return false;
} }
@@ -77,9 +86,11 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
if (linkResult == GL_FALSE) if (linkResult == GL_FALSE)
{ {
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage); glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false; return false;
} }
std::vector<LayerProgram::TextureBinding> textureBindings;
for (const ShaderTextureAsset& textureAsset : state.textureAssets) for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{ {
LayerProgram::TextureBinding textureBinding; LayerProgram::TextureBinding textureBinding;
@@ -93,6 +104,7 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
glDeleteTextures(1, &loadedTexture.texture); glDeleteTextures(1, &loadedTexture.texture);
} }
CopyErrorMessage(loadError, errorMessageSize, errorMessage); CopyErrorMessage(loadError, errorMessageSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false; return false;
} }
textureBindings.push_back(textureBinding); textureBindings.push_back(textureBinding);
@@ -101,51 +113,28 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
std::vector<LayerProgram::TextBinding> textBindings; std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings); mTextureBindings.CreateTextBindings(state, textBindings);
PassProgram passProgram;
passProgram.passId = passSource.passId;
passProgram.inputNames = passSource.inputNames;
passProgram.outputName = passSource.outputName;
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
passProgram.textureBindings.swap(textureBindings);
passProgram.textBindings.swap(textBindings);
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams"); const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX) if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint); glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames(); const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
const GLuint shaderTextureBase = state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
glUseProgram(newProgram.get()); glUseProgram(newProgram.get());
const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput"); mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index)
{
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(newProgram.get(), sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(newProgram.get(), temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
}
for (std::size_t index = 0; index < textureBindings.size(); ++index)
{
const GLint textureSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textureBindings[index].samplerName);
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(textureBindings.size());
for (std::size_t index = 0; index < textBindings.size(); ++index)
{
const GLint textSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textBindings[index].samplerName);
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
glUseProgram(0); glUseProgram(0);
layerProgram.layerId = state.layerId; passProgram.program = newProgram.release();
layerProgram.shaderId = state.shaderId; passProgram.vertexShader = newVertexShader.release();
layerProgram.shaderTextureBase = shaderTextureBase; passProgram.fragmentShader = newFragmentShader.release();
layerProgram.program = newProgram.release(); layerProgram.passes.push_back(std::move(passProgram));
layerProgram.vertexShader = newVertexShader.release(); }
layerProgram.fragmentShader = newFragmentShader.release();
layerProgram.textureBindings.swap(textureBindings);
layerProgram.textBindings.swap(textBindings);
return true; return true;
} }

View File

@@ -5,16 +5,18 @@
#include "ShaderTextureBindings.h" #include "ShaderTextureBindings.h"
#include <string> #include <string>
#include <vector>
class ShaderProgramCompiler class ShaderProgramCompiler
{ {
public: public:
using LayerProgram = OpenGLRenderer::LayerProgram; using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings); ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage); bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage); bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompileDecodeShader(int errorMessageSize, char* errorMessage); bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage); bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);

View File

@@ -102,3 +102,122 @@ GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const st
return location; return location;
return glGetUniformLocation(program, (samplerName + "_0").c_str()); return glGetUniformLocation(program, (samplerName + "_0").c_str());
} }
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
{
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
}
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
{
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index)
{
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
}
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
}
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const
{
RuntimeTextureBindingPlan plan;
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"sourceHistory",
"gSourceHistory" + std::to_string(index),
sourceHistoryTextures[index],
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
});
}
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"temporalHistory",
"gTemporalHistory" + std::to_string(index),
temporalHistoryTextures[index],
temporalBase + static_cast<GLuint>(index)
});
}
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
plan.bindings.push_back({
"shaderTexture",
textureBinding.samplerName,
textureBinding.texture,
shaderTextureBase + static_cast<GLuint>(index)
});
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
plan.bindings.push_back({
"textTexture",
textBinding.samplerName,
textBinding.texture,
textTextureBase + static_cast<GLuint>(index)
});
}
return plan;
}
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, binding.texture);
}
glActiveTexture(GL_TEXTURE0);
}
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
}
glActiveTexture(GL_TEXTURE0);
}

View File

@@ -10,9 +10,32 @@ class ShaderTextureBindings
{ {
public: public:
using LayerProgram = OpenGLRenderer::LayerProgram; using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
struct RuntimeTextureBinding
{
std::string semanticName;
std::string samplerName;
GLuint texture = 0;
GLuint textureUnit = 0;
};
struct RuntimeTextureBindingPlan
{
std::vector<RuntimeTextureBinding> bindings;
};
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error); bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings); void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error); bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const; GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const;
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
void UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
}; };

View File

@@ -82,17 +82,6 @@ bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
return true; return true;
} }
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 LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
{ {
return std::filesystem::exists(candidate / "config" / "runtime-host.json") && return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
@@ -629,6 +618,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error)) if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false; return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) || if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
@@ -1300,7 +1292,7 @@ bool RuntimeHost::TryAdvanceFrame()
return true; return true;
} }
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error) bool RuntimeHost::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error)
{ {
try try
{ {
@@ -1324,16 +1316,30 @@ bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std
} }
ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames); ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames);
return compiler.BuildLayerFragmentShaderSource(shaderPackage, fragmentShaderSource, error); // Compile every declared pass while the caller remains backend-neutral.
// The GL layer decides how the resulting pass sources are routed.
passSources.clear();
passSources.reserve(shaderPackage.passes.size());
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
ShaderPassBuildSource passSource;
passSource.passId = pass.id;
passSource.inputNames = pass.inputNames;
passSource.outputName = pass.outputName;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error))
return false;
passSources.push_back(std::move(passSource));
}
return true;
} }
catch (const std::exception& exception) catch (const std::exception& exception)
{ {
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what(); error = std::string("RuntimeHost::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
return false; return false;
} }
catch (...) catch (...)
{ {
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception."; error = "RuntimeHost::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
return false; return false;
} }
} }
@@ -1671,39 +1677,6 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
return true; return true;
} }
bool RuntimeHost::ParseShaderManifest(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) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}
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
{ {
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error); return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
@@ -1987,6 +1960,8 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
JsonValue parameter = JsonValue::MakeObject(); JsonValue parameter = JsonValue::MakeObject();
parameter.set("id", JsonValue(definition.id)); parameter.set("id", JsonValue(definition.id));
parameter.set("label", JsonValue(definition.label)); parameter.set("label", JsonValue(definition.label));
if (!definition.description.empty())
parameter.set("description", JsonValue(definition.description));
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type))); parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition))); parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));

View File

@@ -9,6 +9,7 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
class RuntimeHost class RuntimeHost
@@ -50,7 +51,7 @@ public:
void AdvanceFrame(); void AdvanceFrame();
bool TryAdvanceFrame(); bool TryAdvanceFrame();
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error); bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const; std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const; bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const; void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
@@ -115,7 +116,6 @@ private:
bool LoadPersistentState(std::string& error); bool LoadPersistentState(std::string& error);
bool SavePersistentState(std::string& error) const; bool SavePersistentState(std::string& error) const;
bool ScanShaderPackages(std::string& error); bool ScanShaderPackages(std::string& error);
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const; bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const; ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const; void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;

View File

@@ -661,3 +661,14 @@ std::string SerializeJson(const JsonValue& value, bool pretty)
SerializeJsonImpl(value, output, pretty, 0); SerializeJsonImpl(value, output, pretty, 0);
return output.str(); return output.str();
} }
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;
}

View File

@@ -62,3 +62,4 @@ private:
bool ParseJson(const std::string& text, JsonValue& value, std::string& error); bool ParseJson(const std::string& text, JsonValue& value, std::string& error);
std::string SerializeJson(const JsonValue& value, bool pretty = false); std::string SerializeJson(const JsonValue& value, bool pretty = false);
std::vector<double> JsonArrayToNumbers(const JsonValue& value);

View File

@@ -26,17 +26,6 @@ bool IsFiniteNumber(double value)
return std::isfinite(value) != 0; 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 NormalizeTextValue(const std::string& text, unsigned maxLength) std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
{ {
std::string normalized; std::string normalized;

View File

@@ -143,10 +143,10 @@ ShaderCompiler::ShaderCompiler(
{ {
} }
bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
{ {
std::string wrapperSource; std::string wrapperSource;
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error)) if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
return false; return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error)) if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false; return false;
@@ -167,7 +167,7 @@ bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderP
return true; return true;
} }
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
{ {
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in"; const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
wrapperSource = ReadTextFile(templatePath, error); wrapperSource = ReadTextFile(templatePath, error);
@@ -183,8 +183,8 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters)); wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount)); wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount)); wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string()); wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)"); wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
return true; return true;
} }

View File

@@ -15,10 +15,10 @@ public:
const std::filesystem::path& patchedGlslPath, const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames); unsigned maxTemporalHistoryFrames);
bool BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const; bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const;
private: private:
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const; bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const;
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const; bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const; bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const; bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const;

View File

@@ -29,17 +29,6 @@ bool IsFiniteNumber(double value)
return std::isfinite(value) != 0; 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) bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
{ {
if (typeName == "float") if (typeName == "float")
@@ -250,6 +239,107 @@ bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPac
return true; return true;
} }
bool ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* passesValue = nullptr;
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
return false;
if (!passesValue)
{
// Existing shader packages are treated as a single implicit pass, so
// multipass support does not require manifest churn.
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = shaderPackage.entryPoint;
pass.sourcePath = shaderPackage.shaderPath;
pass.outputName = "layerOutput";
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
return true;
}
if (passesValue->asArray().empty())
{
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& passJson : passesValue->asArray())
{
if (!passJson.isObject())
{
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string passId;
std::string sourcePath;
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
{
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
return false;
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
{
if (existingPass.id == passId)
{
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
}
ShaderPassDefinition pass;
pass.id = passId;
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
return false;
const JsonValue* inputsValue = nullptr;
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
return false;
if (inputsValue)
{
for (const JsonValue& inputValue : inputsValue->asArray())
{
if (!inputValue.isString())
{
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
return false;
}
pass.inputNames.push_back(inputValue.asString());
}
}
// Keep source validation in the registry. Bad pass declarations then
// appear as unavailable shaders instead of failing at render time.
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
}
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
return true;
}
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error) bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{ {
const JsonValue* texturesValue = nullptr; const JsonValue* texturesValue = nullptr;
@@ -503,6 +593,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error)) if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false; return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) || if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
@@ -666,13 +759,15 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error)) if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false; return false;
if (!std::filesystem::exists(shaderPackage.shaderPath)) if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
return false; return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath); shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
}
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath); shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) && return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&

View File

@@ -26,6 +26,7 @@ struct ShaderParameterDefinition
{ {
std::string id; std::string id;
std::string label; std::string label;
std::string description;
ShaderParameterType type = ShaderParameterType::Float; ShaderParameterType type = ShaderParameterType::Float;
std::vector<double> defaultNumbers; std::vector<double> defaultNumbers;
std::vector<double> minNumbers; std::vector<double> minNumbers;
@@ -76,6 +77,24 @@ struct ShaderFontAsset
std::filesystem::file_time_type writeTime; std::filesystem::file_time_type writeTime;
}; };
struct ShaderPassDefinition
{
std::string id;
std::string entryPoint;
std::filesystem::path sourcePath;
std::filesystem::file_time_type sourceWriteTime;
std::vector<std::string> inputNames;
std::string outputName;
};
struct ShaderPassBuildSource
{
std::string passId;
std::string fragmentShaderSource;
std::vector<std::string> inputNames;
std::string outputName;
};
struct ShaderPackage struct ShaderPackage
{ {
std::string id; std::string id;
@@ -86,6 +105,7 @@ struct ShaderPackage
std::filesystem::path directoryPath; std::filesystem::path directoryPath;
std::filesystem::path shaderPath; std::filesystem::path shaderPath;
std::filesystem::path manifestPath; std::filesystem::path manifestPath;
std::vector<ShaderPassDefinition> passes;
std::vector<ShaderParameterDefinition> parameters; std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets; std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets; std::vector<ShaderFontAsset> fontAssets;

View File

@@ -54,6 +54,8 @@ const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
{ {
case VideoIOPixelFormat::V210: case VideoIOPixelFormat::V210:
return "10-bit YUV v210"; return "10-bit YUV v210";
case VideoIOPixelFormat::Yuva10:
return "10-bit YUVA Ay10";
case VideoIOPixelFormat::Bgra8: case VideoIOPixelFormat::Bgra8:
return "8-bit BGRA"; return "8-bit BGRA";
case VideoIOPixelFormat::Uyvy8: case VideoIOPixelFormat::Uyvy8:
@@ -64,7 +66,7 @@ const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format) bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
{ {
return format == VideoIOPixelFormat::V210; return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
} }
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported) VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
@@ -80,6 +82,8 @@ unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
return 2u; return 2u;
case VideoIOPixelFormat::Bgra8: case VideoIOPixelFormat::Bgra8:
return 4u; return 4u;
case VideoIOPixelFormat::Yuva10:
return 4u;
case VideoIOPixelFormat::V210: case VideoIOPixelFormat::V210:
default: default:
return 0u; return 0u;
@@ -90,6 +94,8 @@ unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
{ {
if (format == VideoIOPixelFormat::V210) if (format == VideoIOPixelFormat::V210)
return MinimumV210RowBytes(frameWidth); return MinimumV210RowBytes(frameWidth);
if (format == VideoIOPixelFormat::Yuva10)
return MinimumYuva10RowBytes(frameWidth);
return frameWidth * VideoIOBytesPerPixel(format); return frameWidth * VideoIOBytesPerPixel(format);
} }
@@ -103,6 +109,11 @@ unsigned MinimumV210RowBytes(unsigned frameWidth)
return ((frameWidth + 5u) / 6u) * 16u; return ((frameWidth + 5u) / 6u) * 16u;
} }
unsigned MinimumYuva10RowBytes(unsigned frameWidth)
{
return ((frameWidth + 63u) / 64u) * 256u;
}
unsigned ActiveV210WordsForWidth(unsigned frameWidth) unsigned ActiveV210WordsForWidth(unsigned frameWidth)
{ {
return ((frameWidth + 5u) / 6u) * 4u; return ((frameWidth + 5u) / 6u) * 4u;

View File

@@ -7,6 +7,7 @@ enum class VideoIOPixelFormat
{ {
Uyvy8, Uyvy8,
V210, V210,
Yuva10,
Bgra8 Bgra8
}; };
@@ -31,6 +32,7 @@ unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth); unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes); unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
unsigned MinimumV210RowBytes(unsigned frameWidth); unsigned MinimumV210RowBytes(unsigned frameWidth);
unsigned MinimumYuva10RowBytes(unsigned frameWidth);
unsigned ActiveV210WordsForWidth(unsigned frameWidth); unsigned ActiveV210WordsForWidth(unsigned frameWidth);
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue); V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue);
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block); std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block);

View File

@@ -95,7 +95,7 @@ public:
virtual ~VideoIODevice() = default; virtual ~VideoIODevice() = default;
virtual void ReleaseResources() = 0; virtual void ReleaseResources() = 0;
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0; virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) = 0; virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0; virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0; virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
virtual bool Start() = 0; virtual bool Start() = 0;

View File

@@ -223,7 +223,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return true; return true;
} }
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{ {
if (!output) if (!output)
{ {
@@ -239,8 +239,15 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. "; mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV); const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8; const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
if (!outputTenBitSupported) mState.outputPixelFormat = outputAlphaRequired
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
if (outputAlphaRequired && outputTenBitYuvaSupported)
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
else if (outputAlphaRequired)
mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
else if (!outputTenBitSupported)
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. "; mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
int deckLinkOutputRowBytes = 0; int deckLinkOutputRowBytes = 0;

View File

@@ -22,7 +22,7 @@ public:
void ReleaseResources() override; void ReleaseResources() override;
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override; bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) override; bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override; bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override; bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
bool Start() override; bool Start() override;

View File

@@ -6,6 +6,8 @@ BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
{ {
case VideoIOPixelFormat::V210: case VideoIOPixelFormat::V210:
return bmdFormat10BitYUV; return bmdFormat10BitYUV;
case VideoIOPixelFormat::Yuva10:
return bmdFormat10BitYUVA;
case VideoIOPixelFormat::Bgra8: case VideoIOPixelFormat::Bgra8:
return bmdFormat8BitBGRA; return bmdFormat8BitBGRA;
case VideoIOPixelFormat::Uyvy8: case VideoIOPixelFormat::Uyvy8:
@@ -18,6 +20,8 @@ VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
{ {
if (format == bmdFormat10BitYUV) if (format == bmdFormat10BitYUV)
return VideoIOPixelFormat::V210; return VideoIOPixelFormat::V210;
if (format == bmdFormat10BitYUVA)
return VideoIOPixelFormat::Yuva10;
if (format == bmdFormat8BitBGRA) if (format == bmdFormat8BitBGRA)
return VideoIOPixelFormat::Bgra8; return VideoIOPixelFormat::Bgra8;
return VideoIOPixelFormat::Uyvy8; return VideoIOPixelFormat::Uyvy8;

View File

@@ -47,6 +47,8 @@ Matching is exact first. If that fails, names are compared in a simplified form
If multiple layers use the same shader package ID or display name, the first matching layer in the stack is controlled. Use the internal layer ID shown in the UI when you need to target one duplicate layer precisely. If multiple layers use the same shader package ID or display name, the first matching layer in the stack is controlled. Use the internal layer ID shown in the UI when you need to target one duplicate layer precisely.
In the control UI, each parameter row has a small **OSC** button. Clicking it copies that parameter's exact OSC address to the clipboard, which is the safest way to target controls with long names or duplicate shader layers.
## Values ## Values
The listener accepts these OSC argument types: The listener accepts these OSC argument types:
@@ -65,7 +67,7 @@ Examples:
/VideoShaderToys/fisheye-reproject/panDegrees 45.0 /VideoShaderToys/fisheye-reproject/panDegrees 45.0
/VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid" /VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid"
/VideoShaderToys/video-transform/pan 0.25 -0.5 /VideoShaderToys/video-transform/pan 0.25 -0.5
/VideoShaderToys/composition-guides/lineColor 1.0 0.8 0.1 1.0 /VideoShaderToys/safe-area-guides/lineColor 1.0 0.8 0.1 1.0
``` ```
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output. Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.

View File

@@ -201,6 +201,7 @@ paths:
post: post:
tags: [Runtime] tags: [Runtime]
summary: Reload shaders summary: Reload shaders
description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
operationId: reloadShaders operationId: reloadShaders
requestBody: requestBody:
required: false required: false
@@ -465,6 +466,18 @@ components:
type: number type: number
budgetUsedPercent: budgetUsedPercent:
type: number type: number
completionIntervalMs:
type: number
smoothedCompletionIntervalMs:
type: number
maxCompletionIntervalMs:
type: number
lateFrameCount:
type: number
droppedFrameCount:
type: number
flushedFrameCount:
type: number
ShaderSummary: ShaderSummary:
type: object type: object
properties: properties:
@@ -476,6 +489,12 @@ components:
type: string type: string
category: category:
type: string type: string
available:
type: boolean
description: False when the shader package exists but failed manifest or compile validation.
error:
type: string
description: Error text for unavailable shader packages.
temporal: temporal:
$ref: "#/components/schemas/TemporalState" $ref: "#/components/schemas/TemporalState"
TemporalState: TemporalState:
@@ -514,9 +533,21 @@ components:
type: string type: string
label: label:
type: string type: string
description:
type: string
description: Short helper text shown under the parameter label in the control UI.
type: type:
type: string type: string
enum: [float, vec2, color, bool, enum, text, trigger] enum: [float, vec2, color, bool, enum, text, trigger]
defaultValue:
description: Default parameter value from the shader manifest.
oneOf:
- type: number
- type: boolean
- type: string
- type: array
items:
type: number
min: min:
type: array type: array
items: items:
@@ -533,6 +564,12 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ParameterOption" $ref: "#/components/schemas/ParameterOption"
maxLength:
type: number
description: Maximum length for text parameters.
font:
type: string
description: Font asset id used by text parameters, when declared.
value: value:
description: Current parameter value. description: Current parameter value.
oneOf: oneOf:

View File

@@ -14,9 +14,9 @@ Packaged documentation:
Generated files: Generated files:
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer. - `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the most recently compiled shader pass.
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`. - `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc` for the most recently compiled pass.
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path. - `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path for the most recently compiled pass.
- `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. - `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.

View File

@@ -11,11 +11,24 @@
"type": "enum", "type": "enum",
"default": "x1_33", "default": "x1_33",
"options": [ "options": [
{ "value": "x1_3", "label": "1.3x" }, {
{ "value": "x1_33", "label": "1.33x" }, "value": "x1_3",
{ "value": "x1_5", "label": "1.5x" }, "label": "1.3x"
{ "value": "x2_0", "label": "2x" } },
] {
"value": "x1_33",
"label": "1.33x"
},
{
"value": "x1_5",
"label": "1.5x"
},
{
"value": "x2_0",
"label": "2x"
}
],
"description": "Horizontal stretch factor matching the anamorphic lens or adapter."
}, },
{ {
"id": "framing", "id": "framing",
@@ -23,24 +36,50 @@
"type": "enum", "type": "enum",
"default": "fit", "default": "fit",
"options": [ "options": [
{ "value": "fit", "label": "Fit" }, {
{ "value": "fill", "label": "Fill" } "value": "fit",
] "label": "Fit"
},
{
"value": "fill",
"label": "Fill"
}
],
"description": "Fit preserves the whole image; Fill crops to remove borders."
}, },
{ {
"id": "pan", "id": "pan",
"label": "Pan", "label": "Pan",
"type": "vec2", "type": "vec2",
"default": [0.0, 0.0], "default": [
"min": [-1.0, -1.0], 0,
"max": [1.0, 1.0], 0
"step": [0.001, 0.001] ],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Reframes the desqueezed image after fit/fill scaling."
}, },
{ {
"id": "outsideColor", "id": "outsideColor",
"label": "Outside Color", "label": "Outside Color",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 1.0] "default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
} }
] ]
} }

View File

@@ -9,37 +9,41 @@
"id": "spinRotation", "id": "spinRotation",
"label": "Spin Rotation", "label": "Spin Rotation",
"type": "float", "type": "float",
"default": -2.0, "default": -2,
"min": -8.0, "min": -8,
"max": 8.0, "max": 8,
"step": 0.05 "step": 0.05,
"description": "Base rotation applied to the swirl field."
}, },
{ {
"id": "spinSpeed", "id": "spinSpeed",
"label": "Spin Speed", "label": "Spin Speed",
"type": "float", "type": "float",
"default": 7.0, "default": 7,
"min": 0.0, "min": 0,
"max": 20.0, "max": 20,
"step": 0.1 "step": 0.1,
"description": "How quickly the swirl pattern rotates."
}, },
{ {
"id": "spinAmount", "id": "spinAmount",
"label": "Spin Amount", "label": "Spin Amount",
"type": "float", "type": "float",
"default": 0.25, "default": 0.25,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Amount of radial twisting in the swirl."
}, },
{ {
"id": "spinEase", "id": "spinEase",
"label": "Spin Ease", "label": "Spin Ease",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Changes how strongly the twist falls off from the center."
}, },
{ {
"id": "contrast", "id": "contrast",
@@ -47,59 +51,94 @@
"type": "float", "type": "float",
"default": 3.5, "default": 3.5,
"min": 0.5, "min": 0.5,
"max": 8.0, "max": 8,
"step": 0.05 "step": 0.05,
"description": "Adjusts separation between dark and bright areas."
}, },
{ {
"id": "lighting", "id": "lighting",
"label": "Lighting", "label": "Lighting",
"type": "float", "type": "float",
"default": 0.4, "default": 0.4,
"min": 0.0, "min": 0,
"max": 1.5, "max": 1.5,
"step": 0.01 "step": 0.01,
"description": "Strength of the highlight/shadow modulation."
}, },
{ {
"id": "offset", "id": "offset",
"label": "Offset", "label": "Offset",
"type": "vec2", "type": "vec2",
"default": [0.0, 0.0], "default": [
"min": [-1.0, -1.0], 0,
"max": [1.0, 1.0], 0
"step": [0.001, 0.001] ],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the generated field in normalized coordinates."
}, },
{ {
"id": "colour1", "id": "colour1",
"label": "Colour 1", "label": "Colour 1",
"type": "color", "type": "color",
"default": [0.871, 0.267, 0.231, 1.0] "default": [
0.871,
0.267,
0.231,
1
],
"description": "Primary warm swirl color."
}, },
{ {
"id": "colour2", "id": "colour2",
"label": "Colour 2", "label": "Colour 2",
"type": "color", "type": "color",
"default": [0.0, 0.42, 0.706, 1.0] "default": [
0,
0.42,
0.706,
1
],
"description": "Secondary cool swirl color."
}, },
{ {
"id": "colour3", "id": "colour3",
"label": "Colour 3", "label": "Colour 3",
"type": "color", "type": "color",
"default": [0.086, 0.137, 0.145, 1.0] "default": [
0.086,
0.137,
0.145,
1
],
"description": "Dark base color in the swirl."
}, },
{ {
"id": "isRotate", "id": "isRotate",
"label": "Rotate Field", "label": "Rotate Field",
"type": "bool", "type": "bool",
"default": false "default": false,
"description": "Rotates the whole generated field over time."
}, },
{ {
"id": "sourceMix", "id": "sourceMix",
"label": "Source Mix", "label": "Source Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the generated effect with the incoming video."
} }
] ]
} }

View File

@@ -9,7 +9,8 @@
"id": "badToggle", "id": "badToggle",
"label": "Bad Toggle", "label": "Bad Toggle",
"type": "boolean", "type": "boolean",
"default": true "default": true,
"description": "Intentionally unsupported parameter type used to test shader error handling."
} }
] ]
} }

View File

@@ -9,55 +9,67 @@
"id": "showThirds", "id": "showThirds",
"label": "Rule of Thirds", "label": "Rule of Thirds",
"type": "bool", "type": "bool",
"default": true "default": true,
"description": "Shows vertical and horizontal thirds lines."
}, },
{ {
"id": "showCrosshair", "id": "showCrosshair",
"label": "Center Crosshair", "label": "Center Crosshair",
"type": "bool", "type": "bool",
"default": true "default": true,
"description": "Shows a center crosshair for lens/framing alignment."
}, },
{ {
"id": "lineColor", "id": "lineColor",
"label": "Line Color", "label": "Line Color",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0] "default": [
1,
1,
1,
1
],
"description": "Color used for guide lines and marks."
}, },
{ {
"id": "lineOpacity", "id": "lineOpacity",
"label": "Line Opacity", "label": "Line Opacity",
"type": "float", "type": "float",
"default": 0.65, "default": 0.65,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Overall visibility of the guide lines."
}, },
{ {
"id": "lineThicknessPixels", "id": "lineThicknessPixels",
"label": "Line Thickness", "label": "Line Thickness",
"type": "float", "type": "float",
"default": 2.0, "default": 2,
"min": 0.5, "min": 0.5,
"max": 12.0, "max": 12,
"step": 0.1 "step": 0.1,
"description": "Guide line width in output pixels."
}, },
{ {
"id": "crosshairSizePixels", "id": "crosshairSizePixels",
"label": "Crosshair Size", "label": "Crosshair Size",
"type": "float", "type": "float",
"default": 54.0, "default": 54,
"min": 8.0, "min": 8,
"max": 240.0, "max": 240,
"step": 1.0 "step": 1,
"description": "Length of each crosshair arm in output pixels."
}, },
{ {
"id": "crosshairGapPixels", "id": "crosshairGapPixels",
"label": "Crosshair Gap", "label": "Crosshair Gap",
"type": "float", "type": "float",
"default": 10.0, "default": 10,
"min": 0.0, "min": 0,
"max": 80.0, "max": 80,
"step": 1.0 "step": 1,
"description": "Empty gap around the exact frame center."
} }
] ]
} }

View File

@@ -15,36 +15,52 @@
"label": "Amount", "label": "Amount",
"type": "float", "type": "float",
"default": 0.45, "default": 0.45,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Strength of the blocky temporal smear."
}, },
{ {
"id": "blockCount", "id": "blockCount",
"label": "Block Count", "label": "Block Count",
"type": "vec2", "type": "vec2",
"default": [32.0, 18.0], "default": [
"min": [2.0, 2.0], 32,
"max": [160.0, 120.0], 18
"step": [1.0, 1.0] ],
"min": [
2,
2
],
"max": [
160,
120
],
"step": [
1,
1
],
"description": "Number of glitch blocks across X and Y."
}, },
{ {
"id": "tearAmount", "id": "tearAmount",
"label": "Tear", "label": "Tear",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Horizontal scanline tearing intensity."
}, },
{ {
"id": "chromaShift", "id": "chromaShift",
"label": "Chroma Shift", "label": "Chroma Shift",
"type": "float", "type": "float",
"default": 1.8, "default": 1.8,
"min": 0.0, "min": 0,
"max": 12.0, "max": 12,
"step": 0.1 "step": 0.1,
"description": "Separate color-channel offset in pixels."
} }
] ]
} }

View File

@@ -18,7 +18,8 @@
"default": 0.28, "default": 0.28,
"min": 0.12, "min": 0.12,
"max": 0.5, "max": 0.5,
"step": 0.01 "step": 0.01,
"description": "Logo size relative to the frame."
}, },
{ {
"id": "bounceSpeed", "id": "bounceSpeed",
@@ -27,34 +28,38 @@
"default": 0.22, "default": 0.22,
"min": 0.02, "min": 0.02,
"max": 0.8, "max": 0.8,
"step": 0.01 "step": 0.01,
"description": "How fast the logo moves between edge hits."
}, },
{ {
"id": "edgePadding", "id": "edgePadding",
"label": "Edge Padding", "label": "Edge Padding",
"type": "float", "type": "float",
"default": 0.018, "default": 0.018,
"min": 0.0, "min": 0,
"max": 0.08, "max": 0.08,
"step": 0.001 "step": 0.001,
"description": "Inset distance from the frame edges."
}, },
{ {
"id": "glowAmount", "id": "glowAmount",
"label": "Glow", "label": "Glow",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 0.75, "max": 0.75,
"step": 0.01 "step": 0.01,
"description": "Adds a soft colored glow around the logo."
}, },
{ {
"id": "baseAlpha", "id": "baseAlpha",
"label": "Alpha", "label": "Alpha",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.05, "min": 0.05,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Overall opacity of the overlay."
} }
] ]
} }

View File

@@ -9,10 +9,11 @@
"id": "speed", "id": "speed",
"label": "Speed", "label": "Speed",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
}, },
{ {
"id": "depth", "id": "depth",
@@ -20,65 +21,95 @@
"type": "float", "type": "float",
"default": 2.5, "default": 2.5,
"min": 0.2, "min": 0.2,
"max": 8.0, "max": 8,
"step": 0.01 "step": 0.01,
"description": "Raymarch depth through the ether volume."
}, },
{ {
"id": "density", "id": "density",
"label": "Density", "label": "Density",
"type": "float", "type": "float",
"default": 0.7, "default": 0.7,
"min": 0.0, "min": 0,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Density of the volumetric strands."
}, },
{ {
"id": "brightness", "id": "brightness",
"label": "Brightness", "label": "Brightness",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Adjusts the generated effect brightness."
}, },
{ {
"id": "contrast", "id": "contrast",
"label": "Contrast", "label": "Contrast",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Adjusts separation between dark and bright areas."
}, },
{ {
"id": "offset", "id": "offset",
"label": "Offset", "label": "Offset",
"type": "vec2", "type": "vec2",
"default": [0.9, 0.5], "default": [
"min": [0.0, 0.0], 0.9,
"max": [2.0, 2.0], 0.5
"step": [0.001, 0.001] ],
"min": [
0,
0
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Moves the generated field in normalized coordinates."
}, },
{ {
"id": "baseColor", "id": "baseColor",
"label": "Base Color", "label": "Base Color",
"type": "color", "type": "color",
"default": [0.1, 0.3, 0.4, 1.0] "default": [
0.1,
0.3,
0.4,
1
],
"description": "Low-energy color used in the generated field."
}, },
{ {
"id": "energyColor", "id": "energyColor",
"label": "Energy Color", "label": "Energy Color",
"type": "color", "type": "color",
"default": [1.0, 0.5, 0.6, 1.0] "default": [
1,
0.5,
0.6,
1
],
"description": "High-energy color used in the generated field."
}, },
{ {
"id": "sourceMix", "id": "sourceMix",
"label": "Source Mix", "label": "Source Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the generated effect with the incoming video."
} }
] ]
} }

View File

@@ -9,34 +9,38 @@
"id": "blendAmount", "id": "blendAmount",
"label": "Blend", "label": "Blend",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Mix amount for the processed result."
}, },
{ {
"id": "showLuma", "id": "showLuma",
"label": "Show Luma", "label": "Show Luma",
"type": "bool", "type": "bool",
"default": false "default": false,
"description": "Shows grayscale luminance before applying false-color mapping."
}, },
{ {
"id": "lift", "id": "lift",
"label": "Lift", "label": "Lift",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -0.25, "min": -0.25,
"max": 0.25, "max": 0.25,
"step": 0.001 "step": 0.001,
"description": "Offsets luminance before false-color mapping."
}, },
{ {
"id": "gain", "id": "gain",
"label": "Gain", "label": "Gain",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Scales luminance before false-color mapping."
} }
] ]
} }

View File

@@ -9,55 +9,85 @@
"id": "lensFovDegrees", "id": "lensFovDegrees",
"label": "Lens FOV", "label": "Lens FOV",
"type": "float", "type": "float",
"default": 190.0, "default": 190,
"min": 1.0, "min": 1,
"max": 220.0, "max": 220,
"step": 0.1 "step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
}, },
{ {
"id": "center", "id": "center",
"label": "Optical Center", "label": "Optical Center",
"type": "vec2", "type": "vec2",
"default": [0.5, 0.5], "default": [
"min": [0.0, 0.0], 0.5,
"max": [1.0, 1.0], 0.5
"step": [0.001, 0.001] ],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
}, },
{ {
"id": "radius", "id": "radius",
"label": "Fisheye Radius", "label": "Fisheye Radius",
"type": "vec2", "type": "vec2",
"default": [0.5, 0.8889], "default": [
"min": [0.001, 0.001], 0.5,
"max": [2.0, 2.0], 0.8889
"step": [0.001, 0.001] ],
"min": [
0.001,
0.001
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
}, },
{ {
"id": "yawDegrees", "id": "yawDegrees",
"label": "Yaw", "label": "Yaw",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Rotates the virtual view horizontally."
}, },
{ {
"id": "pitchDegrees", "id": "pitchDegrees",
"label": "Pitch", "label": "Pitch",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -120.0, "min": -120,
"max": 120.0, "max": 120,
"step": 0.1 "step": 0.1,
"description": "Rotates the virtual view vertically."
}, },
{ {
"id": "rollDegrees", "id": "rollDegrees",
"label": "Roll", "label": "Roll",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Live roll rotation around the viewing axis."
}, },
{ {
"id": "fisheyeModel", "id": "fisheyeModel",
@@ -65,35 +95,56 @@
"type": "enum", "type": "enum",
"default": "equidistant", "default": "equidistant",
"options": [ "options": [
{ "value": "equidistant", "label": "Equidistant" }, {
{ "value": "equisolid", "label": "Equisolid" }, "value": "equidistant",
{ "value": "stereographic", "label": "Stereographic" }, "label": "Equidistant"
{ "value": "orthographic", "label": "Orthographic" } },
] {
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
}, },
{ {
"id": "edgeFill", "id": "edgeFill",
"label": "Edge Fill", "label": "Edge Fill",
"type": "float", "type": "float",
"default": 0.06, "default": 0.06,
"min": 0.0, "min": 0,
"max": 0.3, "max": 0.3,
"step": 0.001 "step": 0.001,
"description": "Extends edge samples outward to cover small missing areas."
}, },
{ {
"id": "edgeBlur", "id": "edgeBlur",
"label": "Edge Blur", "label": "Edge Blur",
"type": "float", "type": "float",
"default": 0.018, "default": 0.018,
"min": 0.0, "min": 0,
"max": 0.12, "max": 0.12,
"step": 0.001 "step": 0.001,
"description": "Softens the dilated edge fill."
}, },
{ {
"id": "outsideColor", "id": "outsideColor",
"label": "Outside Color", "label": "Outside Color",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 1.0] "default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
} }
] ]
} }

View File

@@ -9,91 +9,125 @@
"id": "lensFovDegrees", "id": "lensFovDegrees",
"label": "Lens FOV", "label": "Lens FOV",
"type": "float", "type": "float",
"default": 190.0, "default": 190,
"min": 1.0, "min": 1,
"max": 220.0, "max": 220,
"step": 0.1 "step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
}, },
{ {
"id": "center", "id": "center",
"label": "Optical Center", "label": "Optical Center",
"type": "vec2", "type": "vec2",
"default": [0.5, 0.5], "default": [
"min": [0.0, 0.0], 0.5,
"max": [1.0, 1.0], 0.5
"step": [0.001, 0.001] ],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
}, },
{ {
"id": "radius", "id": "radius",
"label": "Fisheye Radius", "label": "Fisheye Radius",
"type": "vec2", "type": "vec2",
"default": [0.5, 0.885], "default": [
"min": [0.001, 0.001], 0.5,
"max": [2.0, 2.0], 0.885
"step": [0.001, 0.001] ],
"min": [
0.001,
0.001
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
}, },
{ {
"id": "virtualFovDegrees", "id": "virtualFovDegrees",
"label": "Virtual FOV", "label": "Virtual FOV",
"type": "float", "type": "float",
"default": 75.0, "default": 75,
"min": 1.0, "min": 1,
"max": 175.0, "max": 175,
"step": 0.1 "step": 0.1,
"description": "Field of view of the generated virtual camera."
}, },
{ {
"id": "basePanDegrees", "id": "basePanDegrees",
"label": "Base Pan", "label": "Base Pan",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Permanent horizontal alignment offset before live pan."
}, },
{ {
"id": "baseTiltDegrees", "id": "baseTiltDegrees",
"label": "Base Tilt", "label": "Base Tilt",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -120.0, "min": -120,
"max": 120.0, "max": 120,
"step": 0.1 "step": 0.1,
"description": "Permanent vertical alignment offset before live tilt."
}, },
{ {
"id": "baseRollDegrees", "id": "baseRollDegrees",
"label": "Base Roll", "label": "Base Roll",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Permanent roll alignment offset before live roll."
}, },
{ {
"id": "panDegrees", "id": "panDegrees",
"label": "Pan", "label": "Pan",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Live horizontal view rotation."
}, },
{ {
"id": "tiltDegrees", "id": "tiltDegrees",
"label": "Tilt", "label": "Tilt",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -120.0, "min": -120,
"max": 120.0, "max": 120,
"step": 0.1 "step": 0.1,
"description": "Live vertical view rotation."
}, },
{ {
"id": "rollDegrees", "id": "rollDegrees",
"label": "Roll", "label": "Roll",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Live roll rotation around the viewing axis."
}, },
{ {
"id": "fisheyeModel", "id": "fisheyeModel",
@@ -101,11 +135,24 @@
"type": "enum", "type": "enum",
"default": "equidistant", "default": "equidistant",
"options": [ "options": [
{ "value": "equidistant", "label": "Equidistant" }, {
{ "value": "equisolid", "label": "Equisolid" }, "value": "equidistant",
{ "value": "stereographic", "label": "Stereographic" }, "label": "Equidistant"
{ "value": "orthographic", "label": "Orthographic" } },
] {
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
}, },
{ {
"id": "outputProjection", "id": "outputProjection",
@@ -113,15 +160,28 @@
"type": "enum", "type": "enum",
"default": "rectilinear", "default": "rectilinear",
"options": [ "options": [
{ "value": "rectilinear", "label": "Rectilinear" }, {
{ "value": "cylindrical", "label": "Cylindrical" } "value": "rectilinear",
] "label": "Rectilinear"
},
{
"value": "cylindrical",
"label": "Cylindrical"
}
],
"description": "Chooses rectilinear perspective or cylindrical reprojection."
}, },
{ {
"id": "outsideColor", "id": "outsideColor",
"label": "Outside Color", "label": "Outside Color",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 1.0] "default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
} }
] ]
} }

View File

@@ -1,36 +1,59 @@
{ {
"id": "gaussian-blur", "id": "gaussian-blur",
"name": "Gaussian Blur", "name": "Gaussian Blur",
"description": "Applies a simple Gaussian-style blur to the decoded video input.", "description": "Applies a separable two-pass Gaussian-style blur to the decoded video input.",
"category": "Transform", "category": "Transform",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"passes": [
{
"id": "horizontal",
"source": "shader.slang",
"entryPoint": "blurHorizontal",
"inputs": [
"layerInput"
],
"output": "blurHorizontal"
},
{
"id": "vertical",
"source": "shader.slang",
"entryPoint": "blurVertical",
"inputs": [
"blurHorizontal"
],
"output": "layerOutput"
}
],
"parameters": [ "parameters": [
{ {
"id": "radius", "id": "radius",
"label": "Radius", "label": "Radius",
"type": "float", "type": "float",
"default": 2.0, "default": 2,
"min": 0.0, "min": 0,
"max": 8.0, "max": 8,
"step": 0.1 "step": 0.1,
"description": "Blur radius in pixels for each separable pass."
}, },
{ {
"id": "strength", "id": "strength",
"label": "Strength", "label": "Strength",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends between the original and blurred result."
}, },
{ {
"id": "samples", "id": "samples",
"label": "Samples", "label": "Samples",
"type": "float", "type": "float",
"default": 2.0, "default": 2,
"min": 0.0, "min": 0,
"max": 25.0, "max": 25,
"step": 1.0 "step": 1,
"description": "Number of taps per direction; higher values cost more GPU time."
} }
] ]
} }

View File

@@ -1,26 +1,23 @@
float4 shadeVideo(ShaderContext context) float4 gaussianBlurDirection(ShaderContext context, float2 direction)
{ {
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0)); float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
float blurRadius = max(radius, 0.0); float blurRadius = max(radius, 0.0) * saturate(strength);
float2 sampleStep = texel * blurRadius; float2 sampleStep = texel * blurRadius * direction;
int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5); int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5);
float4 center = sampleVideo(context.uv); float4 center = sampleVideo(context.uv);
float4 blur = float4(0.0, 0.0, 0.0, 0.0); float4 blur = float4(0.0, 0.0, 0.0, 0.0);
float totalWeight = 0.0; float totalWeight = 0.0;
for (int y = -sampleRadius; y <= sampleRadius; ++y)
{
for (int x = -sampleRadius; x <= sampleRadius; ++x) for (int x = -sampleRadius; x <= sampleRadius; ++x)
{ {
float distanceSquared = float(x * x + y * y); float distanceSquared = float(x * x);
float sigma = max(float(sampleRadius) * 0.5, 0.5); float sigma = max(float(sampleRadius) * 0.5, 0.5);
float weight = exp(-distanceSquared / (2.0 * sigma * sigma)); float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
float2 offset = float2(float(x), float(y)) * sampleStep; float2 offset = float(x) * sampleStep;
blur += sampleVideo(context.uv + offset) * weight; blur += sampleVideo(context.uv + offset) * weight;
totalWeight += weight; totalWeight += weight;
} }
}
if (sampleRadius == 0) if (sampleRadius == 0)
{ {
@@ -29,7 +26,20 @@ float4 shadeVideo(ShaderContext context)
} }
blur /= max(totalWeight, 0.0001); blur /= max(totalWeight, 0.0001);
return blur;
float mixValue = saturate(strength); }
return lerp(center, blur, mixValue);
float4 blurHorizontal(ShaderContext context)
{
return gaussianBlurDirection(context, float2(1.0, 0.0));
}
float4 blurVertical(ShaderContext context)
{
return gaussianBlurDirection(context, float2(0.0, 1.0));
}
float4 shadeVideo(ShaderContext context)
{
return blurVertical(context);
} }

View File

@@ -4,15 +4,65 @@
"description": "Production-style green/blue screen keyer with matte refinement, despill, edge treatment, and debug views.", "description": "Production-style green/blue screen keyer with matte refinement, despill, edge treatment, and debug views.",
"category": "Keying", "category": "Keying",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"passes": [
{
"id": "rawMatte",
"source": "shader.slang",
"entryPoint": "buildRawMatte",
"inputs": [
"layerInput"
],
"output": "rawMatte"
},
{
"id": "refinedMatte",
"source": "shader.slang",
"entryPoint": "refineMatte",
"inputs": [
"rawMatte"
],
"output": "refinedMatte"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "applyKey",
"inputs": [
"refinedMatte"
],
"output": "layerOutput"
}
],
"parameters": [ "parameters": [
{ {
"id": "screenColor", "id": "screenColor",
"label": "Screen Color", "label": "Screen Color",
"type": "color", "type": "color",
"default": [0.15, 0.85, 0.2, 1.0], "default": [
"min": [0.0, 0.0, 0.0, 0.0], 0.15,
"max": [1.0, 1.0, 1.0, 1.0], 0.85,
"step": [0.01, 0.01, 0.01, 0.01] 0.2,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Target screen color to remove; use green or blue depending on the backdrop."
}, },
{ {
"id": "threshold", "id": "threshold",
@@ -21,7 +71,8 @@
"default": 0.24, "default": 0.24,
"min": 0.01, "min": 0.01,
"max": 0.8, "max": 0.8,
"step": 0.005 "step": 0.005,
"description": "Higher values keep more foreground; lower values remove more screen."
}, },
{ {
"id": "softness", "id": "softness",
@@ -30,142 +81,238 @@
"default": 0.16, "default": 0.16,
"min": 0.001, "min": 0.001,
"max": 0.5, "max": 0.5,
"step": 0.005 "step": 0.005,
"description": "Feathers the transition between foreground and keyed screen."
}, },
{ {
"id": "screenBalance", "id": "screenBalance",
"label": "Screen Balance", "label": "Screen Balance",
"type": "float", "type": "float",
"default": 0.5, "default": 0.5,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.005 "step": 0.005,
"description": "Balances chroma-distance keying against color-direction keying."
}, },
{ {
"id": "screenPreBlur", "id": "screenPreBlur",
"label": "Screen PreBlur", "label": "Screen PreBlur",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 8.0, "max": 8,
"step": 0.1 "step": 0.1,
"description": "Blurs source color before matte generation to reduce noisy edges."
}, },
{ {
"id": "erodeDilate", "id": "erodeDilate",
"label": "Erode/Dilate", "label": "Erode/Dilate",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -0.3, "min": -0.3,
"max": 0.3, "max": 0.3,
"step": 0.005 "step": 0.005,
"description": "Negative erodes the matte; positive expands it."
}, },
{ {
"id": "matteBlur", "id": "matteBlur",
"label": "Matte Blur", "label": "Matte Blur",
"type": "float", "type": "float",
"default": 1.25, "default": 1.25,
"min": 0.0, "min": 0,
"max": 6.0, "max": 6,
"step": 0.1 "step": 0.1,
"description": "Softens the generated matte after keying."
}, },
{ {
"id": "matteGamma", "id": "matteGamma",
"label": "Matte Gamma", "label": "Matte Gamma",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Shapes midtone opacity in the matte."
}, },
{ {
"id": "matteContrast", "id": "matteContrast",
"label": "Matte Contrast", "label": "Matte Contrast",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Increases or reduces matte separation around 50 percent alpha."
}, },
{ {
"id": "blackCleanup", "id": "blackCleanup",
"label": "Black Cleanup", "label": "Black Cleanup",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.005 "step": 0.005,
"description": "Pushes semi-transparent dark matte areas toward transparent."
}, },
{ {
"id": "whiteCleanup", "id": "whiteCleanup",
"label": "White Cleanup", "label": "White Cleanup",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.005 "step": 0.005,
"description": "Pushes semi-transparent light matte areas toward opaque."
}, },
{ {
"id": "despill", "id": "despill",
"label": "Despill", "label": "Despill",
"type": "float", "type": "float",
"default": 0.45, "default": 0.45,
"min": 0.0, "min": 0,
"max": 1.5, "max": 1.5,
"step": 0.01 "step": 0.01,
"description": "Removes screen-colored contamination from foreground edges."
}, },
{ {
"id": "despillBias", "id": "despillBias",
"label": "Despill Bias", "label": "Despill Bias",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -0.5, "min": -0.5,
"max": 0.5, "max": 0.5,
"step": 0.005 "step": 0.005,
"description": "Offsets spill detection when foreground colors are close to the screen color."
}, },
{ {
"id": "spillTint", "id": "spillTint",
"label": "Spill Tint", "label": "Spill Tint",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0], "default": [
"min": [0.0, 0.0, 0.0, 0.0], 1,
"max": [1.0, 1.0, 1.0, 1.0], 1,
"step": [0.01, 0.01, 0.01, 0.01] 1,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint used when neutralizing spill."
}, },
{ {
"id": "edgeRecover", "id": "edgeRecover",
"label": "Edge Recover", "label": "Edge Recover",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.005 "step": 0.005,
"description": "Adds color recovery along semi-transparent matte edges."
}, },
{ {
"id": "edgeColor", "id": "edgeColor",
"label": "Edge Color", "label": "Edge Color",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0], "default": [
"min": [0.0, 0.0, 0.0, 0.0], 1,
"max": [1.0, 1.0, 1.0, 1.0], 1,
"step": [0.01, 0.01, 0.01, 0.01] 1,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint applied to recovered edge detail."
}, },
{ {
"id": "clipBlack", "id": "clipBlack",
"label": "Clip Black", "label": "Clip Black",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 0.5, "max": 0.5,
"step": 0.005 "step": 0.005,
"description": "Matte values below this become transparent."
}, },
{ {
"id": "clipWhite", "id": "clipWhite",
"label": "Clip White", "label": "Clip White",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.5, "min": 0.5,
"max": 1.0, "max": 1,
"step": 0.005 "step": 0.005,
"description": "Matte values above this become opaque."
},
{
"id": "cropLeft",
"label": "Crop Left",
"description": "Trims the final matte from the left edge as a fraction of frame width.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropRight",
"label": "Crop Right",
"description": "Trims the final matte from the right edge as a fraction of frame width.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropTop",
"label": "Crop Top",
"description": "Trims the final matte from the top edge as a fraction of frame height.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropBottom",
"label": "Crop Bottom",
"description": "Trims the final matte from the bottom edge as a fraction of frame height.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
}, },
{ {
"id": "viewMode", "id": "viewMode",
@@ -173,12 +320,28 @@
"type": "enum", "type": "enum",
"default": "composite", "default": "composite",
"options": [ "options": [
{ "value": "composite", "label": "Composite" }, {
{ "value": "matte", "label": "Matte" }, "value": "composite",
{ "value": "spill", "label": "Spill" }, "label": "Composite"
{ "value": "despill", "label": "Despill" }, },
{ "value": "status", "label": "Status" } {
] "value": "matte",
"label": "Matte"
},
{
"value": "spill",
"label": "Spill"
},
{
"value": "despill",
"label": "Despill"
},
{
"value": "status",
"label": "Status"
}
],
"description": "Debug output mode for inspecting matte, spill, and despill stages."
} }
] ]
} }

View File

@@ -50,12 +50,17 @@ float rawAlphaAt(float2 uv, ShaderContext context)
return saturate(alpha); return saturate(alpha);
} }
float refinedAlphaAt(float2 uv, ShaderContext context) float matteAlphaAt(float2 uv)
{
return saturate(sampleVideo(saturate(uv)).a);
}
float refinedAlphaFromMatte(float2 uv, ShaderContext context)
{ {
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0)); float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
float blur = max(matteBlur, 0.0); float blur = max(matteBlur, 0.0);
float aaRadius = max(blur, 0.65); float aaRadius = max(blur, 0.65);
float centerAlpha = rawAlphaAt(uv, context); float centerAlpha = matteAlphaAt(uv);
float alpha = centerAlpha * 0.30; float alpha = centerAlpha * 0.30;
if (aaRadius > 0.0001) if (aaRadius > 0.0001)
@@ -64,51 +69,51 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
float2 halfRadius = radius * 0.5; float2 halfRadius = radius * 0.5;
float alphaMin = centerAlpha; float alphaMin = centerAlpha;
float alphaMax = centerAlpha; float alphaMax = centerAlpha;
float sampleAlpha = rawAlphaAt(uv + float2(halfRadius.x, 0.0), context); float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
alpha += sampleAlpha * 0.065; alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - float2(halfRadius.x, 0.0), context); sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
alpha += sampleAlpha * 0.065; alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + float2(0.0, halfRadius.y), context); sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
alpha += sampleAlpha * 0.065; alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - float2(0.0, halfRadius.y), context); sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
alpha += sampleAlpha * 0.065; alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + float2(radius.x, 0.0), context); sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
alpha += sampleAlpha * 0.06; alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - float2(radius.x, 0.0), context); sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
alpha += sampleAlpha * 0.06; alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + float2(0.0, radius.y), context); sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
alpha += sampleAlpha * 0.06; alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - float2(0.0, radius.y), context); sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
alpha += sampleAlpha * 0.06; alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + radius, context); sampleAlpha = matteAlphaAt(uv + radius);
alpha += sampleAlpha * 0.05; alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - radius, context); sampleAlpha = matteAlphaAt(uv - radius);
alpha += sampleAlpha * 0.05; alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + float2(radius.x, -radius.y), context); sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
alpha += sampleAlpha * 0.05; alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + float2(-radius.x, radius.y), context); sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
alpha += sampleAlpha * 0.05; alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha); alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha); alphaMax = max(alphaMax, sampleAlpha);
@@ -118,7 +123,7 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
} }
else else
{ {
alpha = rawAlphaAt(uv, context); alpha = centerAlpha;
} }
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001)); alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
@@ -147,17 +152,43 @@ float3 despillColor(float3 color, float alpha)
return saturate(neutralized); return saturate(neutralized);
} }
float4 shadeVideo(ShaderContext context) float cropMaskAt(float2 uv, ShaderContext context)
{
float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x);
float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x);
float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y);
float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y);
return saturate(left * right * top * bottom);
}
float4 buildRawMatte(ShaderContext context)
{ {
float4 src = context.sourceColor; float4 src = context.sourceColor;
float3 color = saturate(src.rgb); float3 color = saturate(src.rgb);
float alpha = refinedAlphaAt(context.uv, context); float alpha = rawAlphaAt(context.uv, context);
return float4(color, alpha);
}
float4 refineMatte(ShaderContext context)
{
float4 raw = sampleVideo(context.uv);
float alpha = refinedAlphaFromMatte(context.uv, context);
return float4(saturate(raw.rgb), alpha);
}
float4 applyKey(ShaderContext context)
{
float4 keyed = sampleVideo(context.uv);
float3 color = saturate(keyed.rgb);
float alpha = saturate(keyed.a);
float spill = spillAmountForColor(color); float spill = spillAmountForColor(color);
float3 despilled = despillColor(color, alpha); float3 despilled = despillColor(color, alpha);
float cropMask = cropMaskAt(context.uv, context);
alpha *= cropMask;
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0)); float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover)); despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
alpha = saturate(lerp(alpha, rawAlphaAt(context.uv, context), edgeAmount * saturate(edgeRecover) * 0.35));
if (viewMode == 1) if (viewMode == 1)
return float4(alpha, alpha, alpha, 1.0); return float4(alpha, alpha, alpha, 1.0);
@@ -167,10 +198,15 @@ float4 shadeVideo(ShaderContext context)
return float4(despilled, 1.0); return float4(despilled, 1.0);
if (viewMode == 4) if (viewMode == 4)
{ {
float rawAlpha = rawAlphaAt(context.uv, context); float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
return float4(rawAlpha, alpha, spill, 1.0); return float4(rawAlpha, alpha, spill, 1.0);
} }
float3 premultiplied = saturate(despilled) * alpha; float3 premultiplied = saturate(despilled) * alpha;
return float4(premultiplied, alpha); return float4(premultiplied, alpha);
} }
float4 shadeVideo(ShaderContext context)
{
return applyKey(context);
}

View File

@@ -9,46 +9,51 @@
"id": "speed", "id": "speed",
"label": "Speed", "label": "Speed",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
}, },
{ {
"id": "scale", "id": "scale",
"label": "Scale", "label": "Scale",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Overall size of the effect in the frame."
}, },
{ {
"id": "raySteps", "id": "raySteps",
"label": "Ray Steps", "label": "Ray Steps",
"type": "float", "type": "float",
"default": 77.0, "default": 77,
"min": 8.0, "min": 8,
"max": 77.0, "max": 77,
"step": 1.0 "step": 1,
"description": "Raymarch iteration count; higher values increase detail and GPU cost."
}, },
{ {
"id": "intensity", "id": "intensity",
"label": "Intensity", "label": "Intensity",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.1, "min": 0.1,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Overall brightness of the accumulated raymarched light."
}, },
{ {
"id": "sourceMix", "id": "sourceMix",
"label": "Source Mix", "label": "Source Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the generated effect with the incoming video."
} }
] ]
} }

View File

@@ -9,34 +9,59 @@
"id": "lift", "id": "lift",
"label": "Lift", "label": "Lift",
"type": "color", "type": "color",
"default": [0.5, 0.5, 0.5, 1.0] "default": [
0.5,
0.5,
0.5,
1
],
"description": "Adds color mostly to shadows."
}, },
{ {
"id": "gamma", "id": "gamma",
"label": "Gamma", "label": "Gamma",
"type": "color", "type": "color",
"default": [0.5, 0.5, 0.5, 1.0] "default": [
0.5,
0.5,
0.5,
1
],
"description": "Balances midtone color response."
}, },
{ {
"id": "gain", "id": "gain",
"label": "Gain", "label": "Gain",
"type": "color", "type": "color",
"default": [0.5, 0.5, 0.5, 1.0] "default": [
0.5,
0.5,
0.5,
1
],
"description": "Scales highlights and overall channel intensity."
}, },
{ {
"id": "offset", "id": "offset",
"label": "Offset", "label": "Offset",
"type": "color", "type": "color",
"default": [0.5, 0.5, 0.5, 1.0] "default": [
0.5,
0.5,
0.5,
1
],
"description": "Adds a uniform color offset after lift/gamma/gain."
}, },
{ {
"id": "strength", "id": "strength",
"label": "Strength", "label": "Strength",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the grade with the original image."
} }
] ]
} }

View File

@@ -15,43 +15,48 @@
"id": "lutStrength", "id": "lutStrength",
"label": "LUT Strength", "label": "LUT Strength",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends between the original image and the LUT result."
}, },
{ {
"id": "preExposure", "id": "preExposure",
"label": "Pre Exposure", "label": "Pre Exposure",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -4.0, "min": -4,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Exposure offset applied before the LUT lookup."
}, },
{ {
"id": "postContrast", "id": "postContrast",
"label": "Post Contrast", "label": "Post Contrast",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Contrast adjustment applied after the LUT lookup."
}, },
{ {
"id": "ditherAmount", "id": "ditherAmount",
"label": "Output Dither", "label": "Output Dither",
"type": "float", "type": "float",
"default": 0.5, "default": 0.5,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Adds subtle output dither to reduce visible banding."
}, },
{ {
"id": "clampInput", "id": "clampInput",
"label": "Clamp Input", "label": "Clamp Input",
"type": "bool", "type": "bool",
"default": true "default": true,
"description": "Clamps colors to 0-1 before the LUT lookup."
} }
] ]
} }

View File

@@ -0,0 +1,49 @@
{
"id": "multipass-test",
"name": "Multipass Test",
"description": "Diagnostic two-pass shader that generates a mask in pass one, then samples that named intermediate in pass two.",
"category": "Utility",
"entryPoint": "shadeVideo",
"passes": [
{
"id": "mask",
"source": "shader.slang",
"entryPoint": "buildMask",
"inputs": [
"layerInput"
],
"output": "generatedMask"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "applyMask",
"inputs": [
"generatedMask"
],
"output": "layerOutput"
}
],
"parameters": [
{
"id": "intensity",
"label": "Intensity",
"type": "float",
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Opacity of the second-pass diagnostic overlay."
},
{
"id": "scale",
"label": "Scale",
"type": "float",
"default": 10,
"min": 2,
"max": 32,
"step": 1,
"description": "Size of the generated test pattern."
}
]
}

View File

@@ -0,0 +1,37 @@
float ringMask(float2 uv)
{
float2 centered = uv * 2.0 - 1.0;
float radius = length(centered);
float ring = 1.0 - smoothstep(0.015, 0.035, abs(radius - 0.55));
float cross = 1.0 - smoothstep(0.006, 0.018, min(abs(centered.x), abs(centered.y)));
return saturate(max(ring, cross));
}
float gridMask(float2 uv)
{
float2 cell = abs(frac(uv * max(scale, 1.0)) - 0.5);
float line = 1.0 - smoothstep(0.455, 0.495, max(cell.x, cell.y));
return saturate(line * 0.55);
}
float4 buildMask(ShaderContext context)
{
float mask = saturate(max(ringMask(context.uv), gridMask(context.uv)));
return float4(context.sourceColor.rgb, mask);
}
float4 applyMask(ShaderContext context)
{
float4 generated = sampleVideo(context.uv);
float mask = generated.a;
float checker = step(0.5, frac((context.uv.x + context.uv.y) * max(scale, 1.0)));
float3 testColor = lerp(float3(0.0, 0.75, 1.0), float3(1.0, 0.1, 0.85), checker);
float3 base = generated.rgb;
float3 color = lerp(base, testColor, mask * saturate(intensity));
return float4(color, 1.0);
}
float4 shadeVideo(ShaderContext context)
{
return applyMask(context);
}

View File

@@ -9,25 +9,45 @@
"id": "pixelCount", "id": "pixelCount",
"label": "Pixel Count", "label": "Pixel Count",
"type": "vec2", "type": "vec2",
"default": [96.0, 54.0], "default": [
"min": [2.0, 2.0], 96,
"max": [1920.0, 1080.0], 54
"step": [1.0, 1.0] ],
"min": [
2,
2
],
"max": [
1920,
1080
],
"step": [
1,
1
],
"description": "Number of pixel blocks across X and Y."
}, },
{ {
"id": "gridAmount", "id": "gridAmount",
"label": "Grid", "label": "Grid",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Visibility of the block grid lines."
}, },
{ {
"id": "gridColor", "id": "gridColor",
"label": "Grid Color", "label": "Grid Color",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 1.0] "default": [
0,
0,
0,
1
],
"description": "Color used for the pixel grid."
} }
] ]
} }

View File

@@ -5,32 +5,58 @@
"category": "Scopes & Guides", "category": "Scopes & Guides",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"parameters": [ "parameters": [
{ "id": "showActionSafe", "label": "Action Safe", "type": "bool", "default": true }, {
{ "id": "showTitleSafe", "label": "Title Safe", "type": "bool", "default": true }, "id": "showActionSafe",
{ "id": "showCenter", "label": "Center Marks", "type": "bool", "default": true }, "label": "Action Safe",
"type": "bool",
"default": true,
"description": "Shows the broadcast action-safe rectangle."
},
{
"id": "showTitleSafe",
"label": "Title Safe",
"type": "bool",
"default": true,
"description": "Shows the broadcast title-safe rectangle."
},
{
"id": "showCenter",
"label": "Center Marks",
"type": "bool",
"default": true,
"description": "Shows center marks for alignment."
},
{ {
"id": "lineColor", "id": "lineColor",
"label": "Line Color", "label": "Line Color",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0] "default": [
1,
1,
1,
1
],
"description": "Color used for guide lines and marks."
}, },
{ {
"id": "lineOpacity", "id": "lineOpacity",
"label": "Line Opacity", "label": "Line Opacity",
"type": "float", "type": "float",
"default": 0.65, "default": 0.65,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Overall visibility of the guide lines."
}, },
{ {
"id": "lineThicknessPixels", "id": "lineThicknessPixels",
"label": "Line Thickness", "label": "Line Thickness",
"type": "float", "type": "float",
"default": 2.0, "default": 2,
"min": 0.5, "min": 0.5,
"max": 12.0, "max": 12,
"step": 0.1 "step": 0.1,
"description": "Guide line width in output pixels."
}, },
{ {
"id": "aspectMode", "id": "aspectMode",
@@ -38,20 +64,34 @@
"type": "enum", "type": "enum",
"default": "none", "default": "none",
"options": [ "options": [
{ "value": "none", "label": "None" }, {
{ "value": "239", "label": "2.39:1" }, "value": "none",
{ "value": "185", "label": "1.85:1" }, "label": "None"
{ "value": "square", "label": "1:1" } },
] {
"value": "239",
"label": "2.39:1"
},
{
"value": "185",
"label": "1.85:1"
},
{
"value": "square",
"label": "1:1"
}
],
"description": "Adds an optional framing matte for common delivery ratios."
}, },
{ {
"id": "matteOpacity", "id": "matteOpacity",
"label": "Matte Opacity", "label": "Matte Opacity",
"type": "float", "type": "float",
"default": 0.35, "default": 0.35,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Opacity of the aspect-ratio matte outside the active image."
} }
] ]
} }

View File

@@ -9,10 +9,11 @@
"id": "speed", "id": "speed",
"label": "Speed", "label": "Speed",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
}, },
{ {
"id": "scale", "id": "scale",
@@ -21,16 +22,18 @@
"default": 0.7, "default": 0.7,
"min": 0.25, "min": 0.25,
"max": 1.5, "max": 1.5,
"step": 0.01 "step": 0.01,
"description": "Overall size of the effect in the frame."
}, },
{ {
"id": "strength", "id": "strength",
"label": "Gravity", "label": "Gravity",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.1, "min": 0.1,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Strength of the lensing/gravity distortion."
}, },
{ {
"id": "ringRadius", "id": "ringRadius",
@@ -39,7 +42,8 @@
"default": 0.7, "default": 0.7,
"min": 0.2, "min": 0.2,
"max": 1.4, "max": 1.4,
"step": 0.01 "step": 0.01,
"description": "Radius of the bright accretion ring."
}, },
{ {
"id": "tightness", "id": "tightness",
@@ -47,44 +51,61 @@
"type": "float", "type": "float",
"default": 1.35, "default": 1.35,
"min": 0.5, "min": 0.5,
"max": 3.0, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Concentration of the ring and spiral detail."
}, },
{ {
"id": "brightness", "id": "brightness",
"label": "Brightness", "label": "Brightness",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.1, "min": 0.1,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Adjusts the generated effect brightness."
}, },
{ {
"id": "colorShift", "id": "colorShift",
"label": "Color Shift", "label": "Color Shift",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": -2.0, "min": -2,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Cycles the generated color palette."
}, },
{ {
"id": "center", "id": "center",
"label": "Center", "label": "Center",
"type": "vec2", "type": "vec2",
"default": [0.0, 0.0], "default": [
"min": [-1.0, -1.0], 0,
"max": [1.0, 1.0], 0
"step": [0.001, 0.001] ],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the black hole center in normalized coordinates."
}, },
{ {
"id": "sourceMix", "id": "sourceMix",
"label": "Source Mix", "label": "Source Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the generated effect with the incoming video."
} }
] ]
} }

View File

@@ -9,7 +9,13 @@
"id": "fillColor", "id": "fillColor",
"label": "Fill", "label": "Fill",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0] "default": [
1,
1,
1,
1
],
"description": "Frame fill color; alpha is preserved for key-capable outputs."
} }
] ]
} }

View File

@@ -15,33 +15,42 @@
"label": "Echo Amount", "label": "Echo Amount",
"type": "float", "type": "float",
"default": 0.55, "default": 0.55,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Overall visibility of the temporal echoes."
}, },
{ {
"id": "decay", "id": "decay",
"label": "Decay", "label": "Decay",
"type": "float", "type": "float",
"default": 0.72, "default": 0.72,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "How quickly older temporal echoes fade away."
}, },
{ {
"id": "frameStride", "id": "frameStride",
"label": "Frame Stride", "label": "Frame Stride",
"type": "float", "type": "float",
"default": 2.0, "default": 2,
"min": 1.0, "min": 1,
"max": 6.0, "max": 6,
"step": 1.0 "step": 1,
"description": "Number of frames skipped between each echo sample."
}, },
{ {
"id": "echoTint", "id": "echoTint",
"label": "Echo Tint", "label": "Echo Tint",
"type": "color", "type": "color",
"default": [0.65, 0.85, 1.0, 1.0] "default": [
0.65,
0.85,
1,
1
],
"description": "Tint applied to older echo frames."
} }
] ]
} }

View File

@@ -15,18 +15,20 @@
"label": "Current Mix", "label": "Current Mix",
"type": "float", "type": "float",
"default": 0.72, "default": 0.72,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Contribution of the current frame in the temporal blend."
}, },
{ {
"id": "trailMix", "id": "trailMix",
"label": "Trail Mix", "label": "Trail Mix",
"type": "float", "type": "float",
"default": 0.28, "default": 0.28,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Contribution of older frames in the temporal blend."
} }
] ]
} }

View File

@@ -14,19 +14,21 @@
"id": "holdFrames", "id": "holdFrames",
"label": "Hold Frames", "label": "Hold Frames",
"type": "float", "type": "float",
"default": 3.0, "default": 3,
"min": 0.0, "min": 0,
"max": 7.0, "max": 7,
"step": 0.1 "step": 0.1,
"description": "How many previous frames to hold for the low-FPS effect."
}, },
{ {
"id": "blendAmount", "id": "blendAmount",
"label": "Blend", "label": "Blend",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Mix amount for the processed result."
} }
] ]
} }

View File

@@ -17,16 +17,30 @@
"type": "text", "type": "text",
"default": "VIDEO SHADER", "default": "VIDEO SHADER",
"font": "roboto", "font": "roboto",
"maxLength": 64 "maxLength": 64,
"description": "Text string rendered into the SDF text texture."
}, },
{ {
"id": "position", "id": "position",
"label": "Position", "label": "Position",
"type": "vec2", "type": "vec2",
"default": [0.08, 0.12], "default": [
"min": [0.0, 0.0], 0.08,
"max": [1.0, 1.0], 0.12
"step": [0.001, 0.001] ],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Normalized placement of the text block in the frame."
}, },
{ {
"id": "scale", "id": "scale",
@@ -35,37 +49,52 @@
"default": 0.42, "default": 0.42,
"min": 0.1, "min": 0.1,
"max": 3, "max": 3,
"step": 0.01 "step": 0.01,
"description": "Text size multiplier."
}, },
{ {
"id": "fillColor", "id": "fillColor",
"label": "Fill", "label": "Fill",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0] "default": [
1,
1,
1,
1
],
"description": "Main text fill color and alpha."
}, },
{ {
"id": "outlineColor", "id": "outlineColor",
"label": "Outline", "label": "Outline",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 0.8] "default": [
0,
0,
0,
0.8
],
"description": "Text outline color and alpha."
}, },
{ {
"id": "outlineWidth", "id": "outlineWidth",
"label": "Outline Width", "label": "Outline Width",
"type": "float", "type": "float",
"default": 0.12, "default": 0.12,
"min": 0.0, "min": 0,
"max": 0.5, "max": 0.5,
"step": 0.01 "step": 0.01,
"description": "Width of the SDF outline around the text."
}, },
{ {
"id": "softness", "id": "softness",
"label": "Softness", "label": "Softness",
"type": "float", "type": "float",
"default": 0.04, "default": 0.04,
"min": 0.0, "min": 0,
"max": 0.3, "max": 0.3,
"step": 0.01 "step": 0.01,
"description": "Smoothness of the SDF text edge."
} }
] ]
} }

View File

@@ -8,25 +8,40 @@
{ {
"id": "drop", "id": "drop",
"label": "Drop", "label": "Drop",
"type": "trigger" "type": "trigger",
"description": "Momentary trigger that starts a new ripple from the selected center."
}, },
{ {
"id": "center", "id": "center",
"label": "Center", "label": "Center",
"type": "vec2", "type": "vec2",
"default": [0.5, 0.5], "default": [
"min": [0.0, 0.0], 0.5,
"max": [1.0, 1.0], 0.5
"step": [0.01, 0.01] ],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.01,
0.01
],
"description": "Origin of the triggered ripple in normalized coordinates."
}, },
{ {
"id": "strength", "id": "strength",
"label": "Strength", "label": "Strength",
"type": "float", "type": "float",
"default": 0.12, "default": 0.12,
"min": 0.0, "min": 0,
"max": 0.3, "max": 0.3,
"step": 0.001 "step": 0.001,
"description": "Amount of UV distortion caused by the ripple."
}, },
{ {
"id": "speed", "id": "speed",
@@ -34,8 +49,9 @@
"type": "float", "type": "float",
"default": 0.3, "default": 0.3,
"min": 0.3, "min": 0.3,
"max": 5.0, "max": 5,
"step": 0.01 "step": 0.01,
"description": "How long the ripple takes to expand and fade."
}, },
{ {
"id": "width", "id": "width",
@@ -44,7 +60,8 @@
"default": 0.09, "default": 0.09,
"min": 0.01, "min": 0.01,
"max": 0.25, "max": 0.25,
"step": 0.001 "step": 0.001,
"description": "Thickness of the travelling ripple ring."
}, },
{ {
"id": "damping", "id": "damping",
@@ -52,8 +69,9 @@
"type": "float", "type": "float",
"default": 0.25, "default": 0.25,
"min": 0.05, "min": 0.05,
"max": 3.0, "max": 3,
"step": 0.05 "step": 0.05,
"description": "How quickly the ripple fades as it expands."
} }
] ]
} }

View File

@@ -9,7 +9,8 @@
"id": "showLocalTime", "id": "showLocalTime",
"label": "Show Local Time", "label": "Show Local Time",
"type": "bool", "type": "bool",
"default": true "default": true,
"description": "Uses the PC UTC offset for local time; disable for pure UTC."
}, },
{ {
"id": "clockScale", "id": "clockScale",
@@ -18,19 +19,32 @@
"default": 0.7, "default": 0.7,
"min": 0.25, "min": 0.25,
"max": 0.95, "max": 0.95,
"step": 0.01 "step": 0.01,
"description": "Size of the clock face relative to the frame."
}, },
{ {
"id": "faceColor", "id": "faceColor",
"label": "Face Color", "label": "Face Color",
"type": "color", "type": "color",
"default": [0.03, 0.04, 0.05, 0.82] "default": [
0.03,
0.04,
0.05,
0.82
],
"description": "Clock face fill color and alpha."
}, },
{ {
"id": "accentColor", "id": "accentColor",
"label": "Accent Color", "label": "Accent Color",
"type": "color", "type": "color",
"default": [0.1, 0.62, 0.86, 1.0] "default": [
0.1,
0.62,
0.86,
1
],
"description": "Accent color for the seconds hand and glow."
} }
] ]
} }

View File

@@ -4,123 +4,156 @@
"description": "VHS with wiggle, smear, and YIQ-style color separation inspired by nostalgic analog references.", "description": "VHS with wiggle, smear, and YIQ-style color separation inspired by nostalgic analog references.",
"category": "Glitch", "category": "Glitch",
"entryPoint": "shadeVideo", "entryPoint": "shadeVideo",
"passes": [
{
"id": "tapeSmear",
"source": "shader.slang",
"entryPoint": "buildTapeSmear",
"inputs": [
"layerInput"
],
"output": "tapeSmear"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finishVhs",
"inputs": [
"tapeSmear"
],
"output": "layerOutput"
}
],
"parameters": [ "parameters": [
{ {
"id": "wiggle", "id": "wiggle",
"label": "Wiggle", "label": "Wiggle",
"type": "float", "type": "float",
"default": 0.03, "default": 0.03,
"min": 0.0, "min": 0,
"max": 1.5, "max": 1.5,
"step": 0.01 "step": 0.01,
"description": "Horizontal tape wobble amount."
}, },
{ {
"id": "wiggleSpeed", "id": "wiggleSpeed",
"label": "Wiggle Speed", "label": "Wiggle Speed",
"type": "float", "type": "float",
"default": 25.0, "default": 25,
"min": 0.0, "min": 0,
"max": 100.0, "max": 100,
"step": 1.0 "step": 1,
"description": "Speed of the tape wobble modulation."
}, },
{ {
"id": "smear", "id": "smear",
"label": "Smear", "label": "Smear",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Horizontal color/luma smear strength."
}, },
{ {
"id": "blurSamples", "id": "blurSamples",
"label": "Blur Samples", "label": "Blur Samples",
"type": "float", "type": "float",
"default": 15.0, "default": 15,
"min": 3.0, "min": 3,
"max": 15.0, "max": 15,
"step": 1.0 "step": 1,
"description": "Number of smear samples; higher values are smoother but heavier."
}, },
{ {
"id": "vignetteAmount", "id": "vignetteAmount",
"label": "Vignette", "label": "Vignette",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 0.6, "max": 0.6,
"step": 0.01 "step": 0.01,
"description": "Darkens and softens the frame edges."
}, },
{ {
"id": "aberrationAmount", "id": "aberrationAmount",
"label": "Aberration", "label": "Aberration",
"type": "float", "type": "float",
"default": 0.75, "default": 0.75,
"min": 0.0, "min": 0,
"max": 3.0, "max": 3,
"step": 0.05 "step": 0.05,
"description": "Color-channel separation amount."
}, },
{ {
"id": "halationAmount", "id": "halationAmount",
"label": "Halation", "label": "Halation",
"type": "float", "type": "float",
"default": 0.12, "default": 0.12,
"min": 0.0, "min": 0,
"max": 0.5, "max": 0.5,
"step": 0.01 "step": 0.01,
"description": "Red/orange glow around bright areas."
}, },
{ {
"id": "bloomAmount", "id": "bloomAmount",
"label": "Bloom", "label": "Bloom",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 0.6, "max": 0.6,
"step": 0.01 "step": 0.01,
"description": "Soft glow strength from bright video regions."
}, },
{ {
"id": "fadeAmount", "id": "fadeAmount",
"label": "Fade", "label": "Fade",
"type": "float", "type": "float",
"default": 0.22, "default": 0.22,
"min": 0.0, "min": 0,
"max": 0.75, "max": 0.75,
"step": 0.01 "step": 0.01,
"description": "Washed-out tape fade amount."
}, },
{ {
"id": "noiseAmount", "id": "noiseAmount",
"label": "Noise", "label": "Noise",
"type": "float", "type": "float",
"default": 0.055, "default": 0.055,
"min": 0.0, "min": 0,
"max": 0.2, "max": 0.2,
"step": 0.005 "step": 0.005,
"description": "Fine grain/noise intensity."
}, },
{ {
"id": "staticAmount", "id": "staticAmount",
"label": "Analog Static", "label": "Analog Static",
"type": "float", "type": "float",
"default": 0.045, "default": 0.045,
"min": 0.0, "min": 0,
"max": 0.25, "max": 0.25,
"step": 0.005 "step": 0.005,
"description": "Random bright static intensity."
}, },
{ {
"id": "staticLines", "id": "staticLines",
"label": "Static Lines", "label": "Static Lines",
"type": "float", "type": "float",
"default": 0.65, "default": 0.65,
"min": 0.0, "min": 0,
"max": 1.5, "max": 1.5,
"step": 0.01 "step": 0.01,
"description": "Horizontal static line visibility."
}, },
{ {
"id": "noiseSize", "id": "noiseSize",
"label": "Noise Size", "label": "Noise Size",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.25, "min": 0.25,
"max": 6.0, "max": 6,
"step": 0.05 "step": 0.05,
"description": "Scale of the generated noise pattern."
} }
] ]
} }

View File

@@ -158,10 +158,15 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
return sum; return sum;
} }
float4 shadeVideo(ShaderContext context) float distortedTapeTime(ShaderContext context)
{
return context.time + context.startupRandom * 113.0;
}
float4 buildTapeSmear(ShaderContext context)
{ {
float2 uv = context.uv; float2 uv = context.uv;
float time = context.time + context.startupRandom * 113.0; float time = distortedTapeTime(context);
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0; float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5); int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
@@ -189,6 +194,13 @@ float4 shadeVideo(ShaderContext context)
float q = rgb2yiq(qBlur).b; float q = rgb2yiq(qBlur).b;
float3 color = yiq2rgb(float3(y, i, q)) - pow(s + e * 2.0, 3.0); float3 color = yiq2rgb(float3(y, i, q)) - pow(s + e * 2.0, 3.0);
return float4(saturate(color), 1.0);
}
float4 finishVhs(ShaderContext context)
{
float time = distortedTapeTime(context);
float3 color = sampleVideo(context.uv).rgb;
float2 centered = context.uv * 2.0 - 1.0; float2 centered = context.uv * 2.0 - 1.0;
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0); centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
@@ -238,3 +250,8 @@ float4 shadeVideo(ShaderContext context)
return float4(saturate(color), 1.0); return float4(saturate(color), 1.0);
} }
float4 shadeVideo(ShaderContext context)
{
return finishVhs(context);
}

View File

@@ -9,10 +9,11 @@
"id": "spinSpeed", "id": "spinSpeed",
"label": "Spin Speed", "label": "Spin Speed",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.0, "min": 0,
"max": 4.0, "max": 4,
"step": 0.01 "step": 0.01,
"description": "Rotation speed of the cube."
}, },
{ {
"id": "cubeScale", "id": "cubeScale",
@@ -21,25 +22,28 @@
"default": 0.85, "default": 0.85,
"min": 0.3, "min": 0.3,
"max": 1.4, "max": 1.4,
"step": 0.01 "step": 0.01,
"description": "Size of the cube in the frame."
}, },
{ {
"id": "faceZoom", "id": "faceZoom",
"label": "Face Zoom", "label": "Face Zoom",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.5, "min": 0.5,
"max": 2.0, "max": 2,
"step": 0.01 "step": 0.01,
"description": "Zoom applied to the video on each cube face."
}, },
{ {
"id": "backgroundMix", "id": "backgroundMix",
"label": "Background Mix", "label": "Background Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Mixes the original video behind the generated cube."
} }
] ]
} }

View File

@@ -9,28 +9,43 @@
"id": "zoom", "id": "zoom",
"label": "Zoom", "label": "Zoom",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.1, "min": 0.1,
"max": 8.0, "max": 8,
"step": 0.01 "step": 0.01,
"description": "Scales the source image before edge handling is applied."
}, },
{ {
"id": "pan", "id": "pan",
"label": "Pan", "label": "Pan",
"type": "vec2", "type": "vec2",
"default": [0.0, 0.0], "default": [
"min": [-1.0, -1.0], 0,
"max": [1.0, 1.0], 0
"step": [0.001, 0.001] ],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the source image in normalized frame units."
}, },
{ {
"id": "rotationDegrees", "id": "rotationDegrees",
"label": "Rotation", "label": "Rotation",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": -180.0, "min": -180,
"max": 180.0, "max": 180,
"step": 0.1 "step": 0.1,
"description": "Rotates the source image around the frame center."
}, },
{ {
"id": "edgeMode", "id": "edgeMode",
@@ -38,17 +53,36 @@
"type": "enum", "type": "enum",
"default": "black", "default": "black",
"options": [ "options": [
{ "value": "black", "label": "Black" }, {
{ "value": "clamp", "label": "Clamp" }, "value": "black",
{ "value": "wrap", "label": "Wrap" }, "label": "Black"
{ "value": "mirror", "label": "Mirror" } },
] {
"value": "clamp",
"label": "Clamp"
},
{
"value": "wrap",
"label": "Wrap"
},
{
"value": "mirror",
"label": "Mirror"
}
],
"description": "Chooses how samples outside the source frame are filled."
}, },
{ {
"id": "outsideColor", "id": "outsideColor",
"label": "Outside Color", "label": "Outside Color",
"type": "color", "type": "color",
"default": [0.0, 0.0, 0.0, 1.0] "default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
} }
] ]
} }

View File

@@ -33,44 +33,61 @@
"type": "float", "type": "float",
"default": 0.4, "default": 0.4,
"min": 0.1, "min": 0.1,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Size of the waveform panel."
}, },
{ {
"id": "overlayPosition", "id": "overlayPosition",
"label": "Overlay Position", "label": "Overlay Position",
"type": "vec2", "type": "vec2",
"default": [0.24, 0.76], "default": [
"min": [0.0, 0.0], 0.24,
"max": [1.0, 1.0], 0.76
"step": [0.01, 0.01] ],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.01,
0.01
],
"description": "Normalized position of the waveform panel."
}, },
{ {
"id": "overlayPadding", "id": "overlayPadding",
"label": "Overlay Padding", "label": "Overlay Padding",
"type": "float", "type": "float",
"default": 0.08, "default": 0.08,
"min": 0.0, "min": 0,
"max": 0.25, "max": 0.25,
"step": 0.01 "step": 0.01,
"description": "Padding inside the waveform panel."
}, },
{ {
"id": "waveformOpacity", "id": "waveformOpacity",
"label": "Waveform Opacity", "label": "Waveform Opacity",
"type": "float", "type": "float",
"default": 0.75, "default": 0.75,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Opacity of the waveform trace."
}, },
{ {
"id": "backgroundOpacity", "id": "backgroundOpacity",
"label": "Background", "label": "Background",
"type": "float", "type": "float",
"default": 0.75, "default": 0.75,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Opacity of the waveform background panel."
}, },
{ {
"id": "lineThickness", "id": "lineThickness",
@@ -78,50 +95,61 @@
"type": "float", "type": "float",
"default": 1.5, "default": 1.5,
"min": 0.5, "min": 0.5,
"max": 10.0, "max": 10,
"step": 0.1 "step": 0.1,
"description": "Thickness of the waveform trace in pixels."
}, },
{ {
"id": "gridOpacity", "id": "gridOpacity",
"label": "Grid Opacity", "label": "Grid Opacity",
"type": "float", "type": "float",
"default": 1, "default": 1,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Opacity of the waveform grid and labels."
}, },
{ {
"id": "waveformSamples", "id": "waveformSamples",
"label": "Waveform Samples", "label": "Waveform Samples",
"type": "float", "type": "float",
"default": 64.0, "default": 64,
"min": 8.0, "min": 8,
"max": 96.0, "max": 96,
"step": 1.0 "step": 1,
"description": "Number of vertical samples used to build the waveform."
}, },
{ {
"id": "waveformGain", "id": "waveformGain",
"label": "Waveform Gain", "label": "Waveform Gain",
"type": "float", "type": "float",
"default": 12.0, "default": 12,
"min": 1.0, "min": 1,
"max": 32.0, "max": 32,
"step": 0.5 "step": 0.5,
"description": "Brightness/intensity of waveform hits."
}, },
{ {
"id": "waveformNoiseReduction", "id": "waveformNoiseReduction",
"label": "Noise Reduction", "label": "Noise Reduction",
"type": "float", "type": "float",
"default": 0.08, "default": 0.08,
"min": 0.0, "min": 0,
"max": 0.6, "max": 0.6,
"step": 0.01 "step": 0.01,
"description": "Suppresses faint waveform speckle."
}, },
{ {
"id": "waveformColor", "id": "waveformColor",
"label": "Waveform Color", "label": "Waveform Color",
"type": "color", "type": "color",
"default": [1.0, 1.0, 1.0, 1.0] "default": [
1,
1,
1,
1
],
"description": "Color used for the waveform trace."
} }
] ]
} }

View File

@@ -9,10 +9,11 @@
"id": "patchCount", "id": "patchCount",
"label": "Patch Count", "label": "Patch Count",
"type": "float", "type": "float",
"default": 15.0, "default": 15,
"min": 2.0, "min": 2,
"max": 21.0, "max": 21,
"step": 1.0 "step": 1,
"description": "Number of exposure patches in the chart."
}, },
{ {
"id": "baseLevel", "id": "baseLevel",
@@ -21,25 +22,28 @@
"default": 0.00006103515625, "default": 0.00006103515625,
"min": 0.000001, "min": 0.000001,
"max": 0.01, "max": 0.01,
"step": 0.000001 "step": 0.000001,
"description": "Brightness of the darkest patch before tone mapping."
}, },
{ {
"id": "peakLevel", "id": "peakLevel",
"label": "Peak Level", "label": "Peak Level",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 0.01, "min": 0.01,
"max": 1.0, "max": 1,
"step": 0.001 "step": 0.001,
"description": "Brightness limit for the brightest patch."
}, },
{ {
"id": "gammaEncode", "id": "gammaEncode",
"label": "Display Gamma", "label": "Display Gamma",
"type": "float", "type": "float",
"default": 1.0, "default": 1,
"min": 1.0, "min": 1,
"max": 2.6, "max": 2.6,
"step": 0.01 "step": 0.01,
"description": "Gamma value used when Tone Curve is Display Gamma."
}, },
{ {
"id": "toneCurve", "id": "toneCurve",
@@ -59,7 +63,8 @@
"value": "rec709", "value": "rec709",
"label": "Rec.709" "label": "Rec.709"
} }
] ],
"description": "Output encoding used for the chart patches."
}, },
{ {
"id": "chartScale", "id": "chartScale",
@@ -67,56 +72,63 @@
"type": "float", "type": "float",
"default": 0.86, "default": 0.86,
"min": 0.25, "min": 0.25,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Overall size of the chart in frame."
}, },
{ {
"id": "gapSize", "id": "gapSize",
"label": "Gap Size", "label": "Gap Size",
"type": "float", "type": "float",
"default": 0.18, "default": 0.18,
"min": 0.0, "min": 0,
"max": 0.45, "max": 0.45,
"step": 0.01 "step": 0.01,
"description": "Spacing between exposure patches."
}, },
{ {
"id": "vertical", "id": "vertical",
"label": "Vertical", "label": "Vertical",
"type": "bool", "type": "bool",
"default": false "default": false,
"description": "Stacks patches vertically instead of horizontally."
}, },
{ {
"id": "reverseOrder", "id": "reverseOrder",
"label": "Reverse Order", "label": "Reverse Order",
"type": "bool", "type": "bool",
"default": false "default": false,
"description": "Reverses the dark-to-bright patch order."
}, },
{ {
"id": "backgroundLevel", "id": "backgroundLevel",
"label": "Background", "label": "Background",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 0.2, "max": 0.2,
"step": 0.001 "step": 0.001,
"description": "Background brightness behind the chart."
}, },
{ {
"id": "borderLevel", "id": "borderLevel",
"label": "Border", "label": "Border",
"type": "float", "type": "float",
"default": 0.08, "default": 0.08,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.001 "step": 0.001,
"description": "Brightness of patch borders."
}, },
{ {
"id": "sourceMix", "id": "sourceMix",
"label": "Source Mix", "label": "Source Mix",
"type": "float", "type": "float",
"default": 0.0, "default": 0,
"min": 0.0, "min": 0,
"max": 1.0, "max": 1,
"step": 0.01 "step": 0.01,
"description": "Blends the generated effect with the incoming video."
} }
] ]
} }

View File

@@ -59,7 +59,7 @@ void TestValidManifest()
"fonts": [{ "id": "inter", "path": "Inter.ttf" }], "fonts": [{ "id": "inter", "path": "Inter.ttf" }],
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 }, "temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
"parameters": [ "parameters": [
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }, { "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 }, { "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [ { "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
{ "value": "soft", "label": "Soft" }, { "value": "soft", "label": "Soft" },
@@ -78,8 +78,37 @@ void TestValidManifest()
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse"); Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped"); Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
Expect(package.parameters.size() == 4, "parameters parse"); Expect(package.parameters.size() == 4, "parameters parse");
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses"); Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses"); Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass");
std::filesystem::remove_all(root);
}
void TestExplicitPassManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "multi", R"({
"id": "multi-pass",
"name": "Multi Pass",
"passes": [
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
],
"parameters": []
})");
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
WriteFile(root / "multi" / "final.slang", "float4 finish(float2 uv) { return float4(uv, 1.0, 1.0); }\n");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(registry.ParseManifest(root / "multi" / "shader.json", package, error), "explicit pass manifest parses");
Expect(package.passes.size() == 2, "explicit passes parse");
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
std::filesystem::remove_all(root); std::filesystem::remove_all(root);
} }
@@ -231,6 +260,7 @@ void TestInvalidPackageDoesNotFailScan()
int main() int main()
{ {
TestValidManifest(); TestValidManifest();
TestExplicitPassManifest();
TestMissingFontAsset(); TestMissingFontAsset();
TestInvalidManifest(); TestInvalidManifest();
TestInvalidTemporalSettings(); TestInvalidTemporalSettings();

View File

@@ -81,15 +81,19 @@ int main()
if (packageIt == packagesById.end()) if (packageIt == packagesById.end())
continue; continue;
const ShaderPackage& shaderPackage = packageIt->second;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
std::string fragmentShaderSource; std::string fragmentShaderSource;
std::string compileError; std::string compileError;
if (!compiler.BuildLayerFragmentShaderSource(packageIt->second, fragmentShaderSource, compileError)) if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, compileError))
{ {
Fail("Shader package '" + packageId + "' failed Slang validation: " + compileError); Fail("Shader package '" + packageId + "' pass '" + pass.id + "' failed Slang validation: " + compileError);
continue; continue;
} }
if (fragmentShaderSource.find("#version 430 core") == std::string::npos) if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
Fail("Shader package '" + packageId + "' generated GLSL without the expected patched GLSL version header."); Fail("Shader package '" + packageId + "' pass '" + pass.id + "' generated GLSL without the expected patched GLSL version header.");
}
} }
std::error_code removeError; std::error_code removeError;

View File

@@ -31,7 +31,7 @@ public:
return true; return true;
} }
bool SelectPreferredFormats(const VideoFormatSelection&, std::string&) override bool SelectPreferredFormats(const VideoFormatSelection&, bool, std::string&) override
{ {
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8; mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
mState.outputPixelFormat = VideoIOPixelFormat::Bgra8; mState.outputPixelFormat = VideoIOPixelFormat::Bgra8;
@@ -120,7 +120,7 @@ int main()
bool outputSeen = false; bool outputSeen = false;
Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds"); Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds");
Expect(device.SelectPreferredFormats(selection, error), "fake format selection succeeds"); Expect(device.SelectPreferredFormats(selection, false, error), "fake format selection succeeds");
Expect(device.ConfigureInput([&](const VideoIOFrame& frame) { Expect(device.ConfigureInput([&](const VideoIOFrame& frame) {
inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8; inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
}, selection.input, error), "fake input config succeeds"); }, selection.input, error), "fake input config succeeds");

View File

@@ -22,6 +22,7 @@ void TestPreferredFormatSelection()
Expect(ChoosePreferredVideoIOFormat(true) == VideoIOPixelFormat::V210, "10-bit is preferred when supported"); Expect(ChoosePreferredVideoIOFormat(true) == VideoIOPixelFormat::V210, "10-bit is preferred when supported");
Expect(ChoosePreferredVideoIOFormat(false) == VideoIOPixelFormat::Uyvy8, "8-bit is used as fallback"); Expect(ChoosePreferredVideoIOFormat(false) == VideoIOPixelFormat::Uyvy8, "8-bit is used as fallback");
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::V210) == bmdFormat10BitYUV, "v210 maps to DeckLink 10-bit YUV"); Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::V210) == bmdFormat10BitYUV, "v210 maps to DeckLink 10-bit YUV");
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Yuva10) == bmdFormat10BitYUVA, "Ay10 maps to DeckLink 10-bit YUVA");
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Uyvy8) == bmdFormat8BitYUV, "UYVY maps to DeckLink 8-bit YUV"); Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Uyvy8) == bmdFormat8BitYUV, "UYVY maps to DeckLink 8-bit YUV");
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Bgra8) == bmdFormat8BitBGRA, "BGRA maps to DeckLink 8-bit BGRA"); Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Bgra8) == bmdFormat8BitBGRA, "BGRA maps to DeckLink 8-bit BGRA");
} }
@@ -31,11 +32,15 @@ void TestRowByteHelpers()
Expect(MinimumV210RowBytes(1920) == 5120, "1920-wide v210 active row bytes"); Expect(MinimumV210RowBytes(1920) == 5120, "1920-wide v210 active row bytes");
Expect(MinimumV210RowBytes(1280) == 3424, "1280-wide v210 active row bytes rounds up to six-pixel group"); Expect(MinimumV210RowBytes(1280) == 3424, "1280-wide v210 active row bytes rounds up to six-pixel group");
Expect(MinimumV210RowBytes(3840) == 10240, "3840-wide v210 active row bytes"); Expect(MinimumV210RowBytes(3840) == 10240, "3840-wide v210 active row bytes");
Expect(MinimumYuva10RowBytes(1920) == 7680, "1920-wide Ay10 row bytes");
Expect(MinimumYuva10RowBytes(1919) == 7680, "Ay10 row bytes round up to 64-pixel boundary");
Expect(MinimumYuva10RowBytes(3840) == 15360, "3840-wide Ay10 row bytes");
Expect(PackedTextureWidthFromRowBytes(5120) == 1280, "packed texture width is row bytes divided into RGBA byte texels"); Expect(PackedTextureWidthFromRowBytes(5120) == 1280, "packed texture width is row bytes divided into RGBA byte texels");
Expect(ActiveV210WordsForWidth(1920) == 1280, "active v210 words match 1920 width"); Expect(ActiveV210WordsForWidth(1920) == 1280, "active v210 words match 1920 width");
Expect(VideoIORowBytes(VideoIOPixelFormat::Uyvy8, 1920) == 3840, "UYVY row bytes"); Expect(VideoIORowBytes(VideoIOPixelFormat::Uyvy8, 1920) == 3840, "UYVY row bytes");
Expect(VideoIORowBytes(VideoIOPixelFormat::Bgra8, 1920) == 7680, "BGRA row bytes"); Expect(VideoIORowBytes(VideoIOPixelFormat::Bgra8, 1920) == 7680, "BGRA row bytes");
Expect(VideoIORowBytes(VideoIOPixelFormat::V210, 1920) == 5120, "v210 row bytes"); Expect(VideoIORowBytes(VideoIOPixelFormat::V210, 1920) == 5120, "v210 row bytes");
Expect(VideoIORowBytes(VideoIOPixelFormat::Yuva10, 1920) == 7680, "Ay10 row bytes");
} }
void TestV210PackUnpack() void TestV210PackUnpack()

View File

@@ -1,9 +1,9 @@
import Wheel from "@uiw/react-color-wheel"; import Wheel from "@uiw/react-color-wheel";
import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert"; import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert";
import { Copy, RotateCcw, Zap } from "lucide-react"; import { RotateCcw, Zap } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue"; import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
import { ParameterValueDisplay } from "./ParameterValueDisplay";
function valuesMatch(left, right) { function valuesMatch(left, right) {
return JSON.stringify(left) === JSON.stringify(right); return JSON.stringify(left) === JSON.stringify(right);
@@ -21,7 +21,10 @@ function ParameterHeader({ layer, parameter, onReset, resetDisabled }) {
return ( return (
<div className="parameter__header"> <div className="parameter__header">
<div className="parameter__title">
<label>{parameter.label}</label> <label>{parameter.label}</label>
{parameter.description ? <p title={parameter.description}>{parameter.description}</p> : null}
</div>
<button <button
type="button" type="button"
className="parameter__osc" className="parameter__osc"
@@ -29,8 +32,7 @@ function ParameterHeader({ layer, parameter, onReset, resetDisabled }) {
aria-label={`Copy OSC route ${oscRoute}`} aria-label={`Copy OSC route ${oscRoute}`}
onClick={copyRoute} onClick={copyRoute}
> >
<span>{oscRoute}</span> <span aria-hidden="true">OSC</span>
<Copy size={13} strokeWidth={1.75} aria-hidden="true" />
</button> </button>
<button <button
type="button" type="button"
@@ -88,12 +90,12 @@ function hsvaToColorValue(hsva, alpha) {
} }
export function ParameterField({ layer, parameter, onParameterChange }) { export function ParameterField({ layer, parameter, onParameterChange }) {
const [colorPickerOpen, setColorPickerOpen] = useState(false);
const colorControlRef = useRef(null);
const { const {
appliedValue,
beginInteraction, beginInteraction,
draftValue, draftValue,
endInteraction, endInteraction,
isPending,
scheduleSendValue, scheduleSendValue,
sendValue, sendValue,
} = useThrottledParameterValue(parameter, onParameterChange); } = useThrottledParameterValue(parameter, onParameterChange);
@@ -105,6 +107,32 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
sendValue(defaultValue); sendValue(defaultValue);
} }
}; };
useEffect(() => {
if (!colorPickerOpen) {
return undefined;
}
function handlePointerDown(event) {
if (!colorControlRef.current || !colorControlRef.current.contains(event.target)) {
setColorPickerOpen(false);
}
}
function handleKeyDown(event) {
if (event.key === "Escape") {
setColorPickerOpen(false);
}
}
document.addEventListener("pointerdown", handlePointerDown, true);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown, true);
document.removeEventListener("keydown", handleKeyDown);
};
}, [colorPickerOpen]);
const header = ( const header = (
<ParameterHeader <ParameterHeader
layer={layer} layer={layer}
@@ -118,6 +146,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__control">
<div className="parameter__pair"> <div className="parameter__pair">
<input <input
type="range" type="range"
@@ -147,7 +176,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
onBlur={endInteraction} onBlur={endInteraction}
/> />
</div> </div>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }
@@ -161,11 +190,13 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__pair"> <div className="parameter__control">
<div className="parameter__pair parameter__pair--vec2">
{Array.from({ length: 2 }, (_, index) => ( {Array.from({ length: 2 }, (_, index) => (
<input <input
key={index} key={index}
type="number" type="number"
aria-label={`${parameter.label} ${index === 0 ? "X" : "Y"}`}
min={parameter.min?.[index] ?? ""} min={parameter.min?.[index] ?? ""}
max={parameter.max?.[index] ?? ""} max={parameter.max?.[index] ?? ""}
step={parameter.step?.[index] ?? 0.01} step={parameter.step?.[index] ?? 0.01}
@@ -180,7 +211,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
/> />
))} ))}
</div> </div>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }
@@ -193,12 +224,46 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
const hsva = colorValueToHsva(values); const hsva = colorValueToHsva(values);
const wheelHsva = { ...hsva, v: 100 }; const wheelHsva = { ...hsva, v: 100 };
const sendHsva = (nextHsva) => scheduleSendValue(hsvaToColorValue(nextHsva, values[3])); const sendHsva = (nextHsva) => scheduleSendValue(hsvaToColorValue(nextHsva, values[3]));
const updateColorComponent = (index, nextValue) => {
const next = [...values];
next[index] = Number(nextValue);
sendValue(next);
};
return ( return (
<section className="parameter"> <section className="parameter parameter--color">
{header} {header}
<div className="parameter__wheel-row"> <div className="parameter__control" ref={colorControlRef}>
<div className="parameter__color-stack"> <div className="parameter__color-compact">
<button
type="button"
className="parameter__swatch-button"
title={`Open ${parameter.label} color picker`}
aria-label={`Open ${parameter.label} color picker`}
onClick={() => setColorPickerOpen((open) => !open)}
>
<span className="parameter__swatch" style={{ background: colorValueToHex(values) }} aria-hidden="true" />
</button>
<div className="parameter__rgba-grid">
{["R", "G", "B", "A"].map((label, index) => (
<label key={label} className="parameter__rgba-field">
<span>{label}</span>
<input
type="number"
min={parameter.min?.[index] ?? 0}
max={parameter.max?.[index] ?? 1}
step={parameter.step?.[index] ?? 0.01}
value={values[index]}
onFocus={beginInteraction}
onChange={(event) => updateColorComponent(index, event.target.value)}
onBlur={endInteraction}
/>
</label>
))}
</div>
</div>
{colorPickerOpen ? (
<div className="parameter__color-popover">
<div <div
className="parameter__wheel" className="parameter__wheel"
onPointerDown={beginInteraction} onPointerDown={beginInteraction}
@@ -214,6 +279,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
/> />
</div> </div>
<label className="parameter__value-slider"> <label className="parameter__value-slider">
<span>Value</span>
<input <input
type="range" type="range"
min={0} min={0}
@@ -234,28 +300,8 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
/> />
</label> </label>
</div> </div>
<div className="parameter__color-bottom"> ) : null}
<label className="parameter__alpha">
<span>Alpha</span>
<input
type="number"
min={parameter.min?.[3] ?? 0}
max={parameter.max?.[3] ?? 1}
step={parameter.step?.[3] ?? 0.01}
value={values[3]}
onFocus={beginInteraction}
onChange={(event) => {
const next = [...values];
next[3] = Number(event.target.value);
sendValue(next);
}}
onBlur={endInteraction}
/>
</label>
<div className="parameter__swatch" style={{ background: colorValueToHex(values) }} aria-hidden="true" />
</div> </div>
</div>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
</section> </section>
); );
} }
@@ -264,6 +310,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__control">
<label className="toggle toggle--field"> <label className="toggle toggle--field">
<input <input
type="checkbox" type="checkbox"
@@ -274,7 +321,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
/> />
<span>{draftValue ? "Enabled" : "Disabled"}</span> <span>{draftValue ? "Enabled" : "Disabled"}</span>
</label> </label>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }
@@ -283,6 +330,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__control">
<select <select
value={draftValue} value={draftValue}
onFocus={beginInteraction} onFocus={beginInteraction}
@@ -295,7 +343,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
</option> </option>
))} ))}
</select> </select>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }
@@ -304,6 +352,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__control">
<input <input
type="text" type="text"
maxLength={parameter.maxLength ?? 64} maxLength={parameter.maxLength ?? 64}
@@ -313,7 +362,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
onChange={(event) => sendValue(event.target.value)} onChange={(event) => sendValue(event.target.value)}
onBlur={endInteraction} onBlur={endInteraction}
/> />
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }
@@ -323,6 +372,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
return ( return (
<section className="parameter"> <section className="parameter">
{header} {header}
<div className="parameter__control">
<button <button
type="button" type="button"
className="button-with-icon parameter__trigger" className="button-with-icon parameter__trigger"
@@ -331,7 +381,7 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
<Zap size={16} strokeWidth={1.9} aria-hidden="true" /> <Zap size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Trigger</span> <span>Trigger</span>
</button> </button>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> </div>
</section> </section>
); );
} }

View File

@@ -1,28 +0,0 @@
function formatNumber(value, digits = 3) {
return Number(value ?? 0).toFixed(digits);
}
export function formatParameterValue(parameterType, value) {
if (parameterType === "float") {
return formatNumber(value);
}
if (parameterType === "vec2" || parameterType === "color") {
return (value ?? []).map((item) => formatNumber(item)).join(", ");
}
if (parameterType === "bool") {
return value ? "Enabled" : "Disabled";
}
if (parameterType === "trigger") {
return `Triggered ${Number(value ?? 0)} time${Number(value ?? 0) === 1 ? "" : "s"}`;
}
return `${value ?? ""}`;
}
export function ParameterValueDisplay({ parameterType, value, pending }) {
const valueText = formatParameterValue(parameterType, value);
return (
<div className={`parameter__value${pending ? " parameter__value--pending" : ""}`}>
{pending ? `Applied: ${valueText}` : valueText}
</div>
);
}

View File

@@ -43,7 +43,7 @@ export function StackPresetToolbar({
onClick={() => postJson("/api/reload", {})} onClick={() => postJson("/api/reload", {})}
> >
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" /> <RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Reload shader</span> <span>Reload shaders</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -700,8 +700,6 @@ pre {
.shader-picker__selected, .shader-picker__selected,
.shader-picker__meta, .shader-picker__meta,
.shader-picker__empty, .shader-picker__empty,
.parameter__value,
.parameter__alpha,
.parameter__osc, .parameter__osc,
.parameter__reset { .parameter__reset {
color: var(--app-muted); color: var(--app-muted);
@@ -876,31 +874,59 @@ pre {
} }
.parameter-grid { .parameter-grid {
grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 32rem), 1fr));
gap: 0.625rem; gap: 0.5rem;
align-items: start;
} }
.parameter { .parameter {
padding: 0.75rem; position: relative;
grid-template-columns: minmax(16rem, 0.9fr) minmax(18rem, 1.35fr);
gap: 0.625rem;
align-items: center;
padding: 0.55rem 0.65rem;
border: 1px solid var(--app-border); border: 1px solid var(--app-border);
background: #141a23; background: #141a23;
} }
.parameter__header { .parameter__header {
display: grid; display: grid;
grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.5rem; gap: 0.35rem;
align-items: center; align-items: center;
min-width: 0;
}
.parameter__title {
min-width: 0;
}
.parameter__title label {
display: block;
overflow: hidden;
font-size: 0.9rem;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.parameter__title p {
margin: 0.12rem 0 0;
overflow: hidden;
color: var(--app-muted);
font-size: 0.76rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
} }
.parameter__osc, .parameter__osc,
.parameter__reset { .parameter__reset {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: center;
gap: 0.375rem; width: 24px;
width: auto; min-width: 24px;
min-width: 0;
min-height: 24px; min-height: 24px;
padding: 0; padding: 0;
border: 0; border: 0;
@@ -908,18 +934,16 @@ pre {
font-weight: 500; font-weight: 500;
} }
.parameter__reset { .parameter__osc {
justify-content: center; width: 34px;
width: 24px; min-width: 34px;
min-width: 24px; font-size: 0.66rem;
color: var(--app-muted); font-weight: 800;
letter-spacing: 0.02em;
} }
.parameter__osc span { .parameter__reset {
min-width: 0; color: var(--app-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.parameter__osc svg, .parameter__osc svg,
@@ -927,33 +951,89 @@ pre {
flex: 0 0 auto; flex: 0 0 auto;
} }
.parameter__value--pending { .parameter__control {
color: var(--app-warning); position: relative;
min-width: 0;
} }
.parameter__pair { .parameter__pair {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(5.125rem, 1fr)); grid-template-columns: minmax(8rem, 1fr) minmax(5.25rem, 7rem);
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
} }
.parameter__pair--vec2 {
grid-template-columns: repeat(2, minmax(5.25rem, 1fr));
}
.parameter__pair input[type="range"] { .parameter__pair input[type="range"] {
min-width: 7.5rem; min-width: 7.5rem;
} }
.parameter__wheel-row { .parameter__color-compact {
display: grid; display: grid;
grid-template-columns: minmax(0, 196px); grid-template-columns: auto minmax(0, 1fr);
gap: 0.625rem; gap: 0.5rem;
align-items: start; align-items: end;
justify-content: center;
} }
.parameter__color-stack { .parameter__swatch-button {
width: 42px;
min-width: 42px;
height: 42px;
min-height: 42px;
padding: 0.25rem;
border-color: var(--app-border);
background: var(--app-surface-2);
}
.parameter__swatch-button:hover:not(:disabled) {
background: var(--app-surface-2);
border-color: rgba(26, 156, 219, 0.55);
}
.parameter__swatch {
display: block;
width: 100%;
min-height: 100%;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: var(--app-radius-sm);
}
.parameter__rgba-grid {
display: grid;
grid-template-columns: repeat(4, minmax(3.5rem, 1fr));
gap: 0.35rem;
}
.parameter__rgba-field {
display: grid;
gap: 0.15rem;
color: var(--app-muted);
font-size: 0.68rem;
font-weight: 700;
}
.parameter__rgba-field input {
min-height: 32px;
padding: 0.35rem 0.45rem;
font-size: 0.82rem;
}
.parameter__color-popover {
position: absolute;
z-index: 20;
top: calc(100% + 0.45rem);
left: 0;
display: grid; display: grid;
gap: 0.625rem; gap: 0.625rem;
width: 196px; width: 224px;
padding: 0.75rem;
border: 1px solid var(--app-border);
border-radius: var(--app-radius);
background: #10151d;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
} }
.parameter__wheel { .parameter__wheel {
@@ -973,24 +1053,13 @@ pre {
display: block; display: block;
} }
.parameter__color-bottom {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(4.5rem, 0.85fr);
gap: 0.625rem;
align-items: end;
width: 196px;
}
.parameter__swatch {
width: 100%;
min-height: 38px;
border: 1px solid var(--app-border);
border-radius: var(--app-radius-sm);
}
.parameter__value-slider { .parameter__value-slider {
display: block; display: grid;
gap: 0.25rem;
width: 196px; width: 196px;
color: var(--app-muted);
font-size: 0.72rem;
font-weight: 700;
} }
.parameter__value-slider input[type="range"] { .parameter__value-slider input[type="range"] {
@@ -1041,18 +1110,8 @@ pre {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.65), 0 1px 4px rgba(0, 0, 0, 0.45); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.65), 0 1px 4px rgba(0, 0, 0, 0.45);
} }
.parameter__value-slider strong { .parameter__trigger {
text-align: right; min-width: 7rem;
min-height: 1rem;
color: var(--app-text);
font-size: 0.74rem;
line-height: 1;
}
.parameter__alpha {
display: grid;
gap: 0.25rem;
font-weight: 600;
} }
.toggle { .toggle {
@@ -1141,8 +1200,8 @@ pre {
.summary-grid, .summary-grid,
.kv-rows, .kv-rows,
.parameter-grid, .parameter-grid,
.parameter__header, .parameter,
.parameter__wheel-row { .parameter__header {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1154,12 +1213,20 @@ pre {
width: 24px; width: 24px;
} }
.parameter__swatch { .parameter__pair,
grid-column: auto; .parameter__color-compact {
grid-template-columns: 1fr;
} }
.parameter__color-stack, .parameter__rgba-grid {
.parameter__color-bottom, grid-template-columns: repeat(2, minmax(0, 1fr));
}
.parameter__swatch-button {
width: 100%;
}
.parameter__color-popover,
.parameter__value-slider, .parameter__value-slider,
.parameter__value-slider input[type="range"] { .parameter__value-slider input[type="range"] {
width: 100%; width: 100%;