20 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
07a5c91427 shader validation checks
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 2m27s
2026-05-08 16:46:03 +10:00
53b980913b docs update 2026-05-08 16:42:23 +10:00
4e2ac4a091 re organisation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m42s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:38:47 +10:00
3eb5bb5de3 Splitting out rendering 2026-05-08 16:33:55 +10:00
116 changed files with 3894 additions and 1824 deletions

View File

@@ -59,7 +59,7 @@ jobs:
shell: powershell
run: cmake --build --preset build-debug
- name: Run Native Tests
- name: Run Native Tests And Shader Validation
shell: powershell
run: cmake --build --preset build-debug --target RUN_TESTS

View File

@@ -31,7 +31,7 @@ if(NOT EXISTS "${SLANG_LICENSE_FILE}")
endif()
set(APP_SOURCES
"${APP_DIR}/DeckLinkAPI_i.c"
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
"${APP_DIR}/control/ControlServer.cpp"
"${APP_DIR}/control/ControlServer.h"
"${APP_DIR}/control/OscServer.cpp"
@@ -40,48 +40,54 @@ set(APP_SOURCES
"${APP_DIR}/control/RuntimeControlBridge.h"
"${APP_DIR}/control/RuntimeServices.cpp"
"${APP_DIR}/control/RuntimeServices.h"
"${APP_DIR}/decklink/DeckLinkDisplayMode.cpp"
"${APP_DIR}/decklink/DeckLinkDisplayMode.h"
"${APP_DIR}/decklink/DeckLinkFrameTransfer.cpp"
"${APP_DIR}/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/decklink/DeckLinkSession.cpp"
"${APP_DIR}/decklink/DeckLinkSession.h"
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/gl/GLExtensions.cpp"
"${APP_DIR}/gl/GLExtensions.h"
"${APP_DIR}/gl/GlobalParamsBuffer.cpp"
"${APP_DIR}/gl/GlobalParamsBuffer.h"
"${APP_DIR}/gl/GlRenderConstants.h"
"${APP_DIR}/gl/GlScopedObjects.h"
"${APP_DIR}/gl/GlShaderSources.cpp"
"${APP_DIR}/gl/GlShaderSources.h"
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
"${APP_DIR}/gl/renderer/GLExtensions.h"
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
"${APP_DIR}/gl/shader/GlobalParamsBuffer.h"
"${APP_DIR}/gl/renderer/GlRenderConstants.h"
"${APP_DIR}/gl/renderer/GlScopedObjects.h"
"${APP_DIR}/gl/shader/GlShaderSources.cpp"
"${APP_DIR}/gl/shader/GlShaderSources.h"
"${APP_DIR}/gl/OpenGLComposite.cpp"
"${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/OpenGLRenderPass.h"
"${APP_DIR}/gl/OpenGLRenderer.cpp"
"${APP_DIR}/gl/OpenGLRenderer.h"
"${APP_DIR}/gl/OpenGLVideoIOBridge.cpp"
"${APP_DIR}/gl/OpenGLVideoIOBridge.h"
"${APP_DIR}/gl/OpenGLShaderPrograms.cpp"
"${APP_DIR}/gl/OpenGLShaderPrograms.h"
"${APP_DIR}/gl/PngScreenshotWriter.cpp"
"${APP_DIR}/gl/PngScreenshotWriter.h"
"${APP_DIR}/gl/ShaderProgramCompiler.cpp"
"${APP_DIR}/gl/ShaderProgramCompiler.h"
"${APP_DIR}/gl/ShaderBuildQueue.cpp"
"${APP_DIR}/gl/ShaderBuildQueue.h"
"${APP_DIR}/gl/ShaderTextureBindings.cpp"
"${APP_DIR}/gl/ShaderTextureBindings.h"
"${APP_DIR}/gl/Std140Buffer.h"
"${APP_DIR}/gl/TextRasterizer.cpp"
"${APP_DIR}/gl/TextRasterizer.h"
"${APP_DIR}/gl/TextureAssetLoader.cpp"
"${APP_DIR}/gl/TextureAssetLoader.h"
"${APP_DIR}/gl/TemporalHistoryBuffers.cpp"
"${APP_DIR}/gl/TemporalHistoryBuffers.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
"${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.h"
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
"${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
"${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
"${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
"${APP_DIR}/gl/shader/ShaderBuildQueue.h"
"${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
"${APP_DIR}/gl/shader/ShaderTextureBindings.h"
"${APP_DIR}/gl/shader/Std140Buffer.h"
"${APP_DIR}/gl/shader/TextRasterizer.cpp"
"${APP_DIR}/gl/shader/TextRasterizer.h"
"${APP_DIR}/gl/shader/TextureAssetLoader.cpp"
"${APP_DIR}/gl/shader/TextureAssetLoader.h"
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.cpp"
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
@@ -116,12 +122,15 @@ add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}"
"${APP_DIR}/control"
"${APP_DIR}/decklink"
"${APP_DIR}/gl"
"${APP_DIR}/gl/pipeline"
"${APP_DIR}/gl/renderer"
"${APP_DIR}/gl/shader"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
@@ -202,6 +211,7 @@ add_executable(Std140BufferTests
target_include_directories(Std140BufferTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/gl"
"${APP_DIR}/gl/shader"
)
if(MSVC)
@@ -228,6 +238,29 @@ endif()
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
add_executable(ShaderSlangValidationTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
)
target_include_directories(ShaderSlangValidationTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
)
if(MSVC)
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
endif()
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
set_tests_properties(ShaderSlangValidationTests PROPERTIES
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
)
add_executable(OscServerTests
"${APP_DIR}/control/OscServer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
@@ -250,15 +283,15 @@ endif()
add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(VideoIOFormatTests
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
)
target_include_directories(VideoIOFormatTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/decklink"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)
@@ -274,8 +307,8 @@ add_executable(VideoPlayoutSchedulerTests
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/decklink"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)
@@ -291,8 +324,8 @@ add_executable(VideoIODeviceFakeTests
target_include_directories(VideoIODeviceFakeTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/decklink"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)

View File

@@ -1,8 +1,8 @@
# Video Shader
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI.
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. 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
@@ -15,12 +15,20 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
- `tests/`: focused native tests for pure runtime logic.
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
Native app internals are grouped by boundary:
- `videoio/`: backend-neutral video I/O contracts, formats, and playout timing.
- `videoio/decklink/`: DeckLink-specific device adapter, callbacks, and SDK bindings.
- `gl/renderer/`: low-level OpenGL resources and extension helpers.
- `gl/pipeline/`: frame pipeline, render passes, video I/O bridge, preview/readback, and screenshots.
- `gl/shader/`: shader compilation, texture/text assets, UBO packing, and shader program ownership.
## Requirements
- Windows with Visual Studio 2022 C++ tooling.
- CMake 3.24 or newer.
- Node.js and npm for the control UI.
- Blackmagic Desktop Video drivers and a DeckLink device.
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
Default expected Slang path:
@@ -54,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 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
Build the UI, build the native Release target, then install into a portable runtime folder:
@@ -77,6 +93,8 @@ dist/VideoShader/
shaders/
3rdParty/slang/bin/
ui/dist/
docs/
SHADER_CONTRACT.md
runtime/templates/
third_party_notices/
```
@@ -111,6 +129,9 @@ Current native test coverage includes:
- JSON parsing and serialization.
- Parameter normalization and preset filename safety.
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
- 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.
- Slang validation for every available shader package.
## Runtime Configuration
@@ -131,7 +152,7 @@ Current native test coverage includes:
}
```
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
@@ -170,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.
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
runtime/screenshots/
@@ -194,10 +219,11 @@ Each shader package lives under:
shaders/<id>/
shader.json
shader.slang
optional-extra-pass.slang
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
@@ -208,6 +234,7 @@ Runtime-generated files are intentionally ignored:
- `runtime/shader_cache/active_shader.frag`
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
- `runtime/stack_presets/*.json`
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
Only `runtime/templates/` and `runtime/README.md` are tracked.
@@ -236,16 +263,13 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
- Audio.
- Genlock.
- Find a better UI library for react.
- Logs.
- Continue source cleanup/refactoring. Pass 2 done
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
- Add WebView2
- move to MSDF, typography rasterisation
- better shader search UI, pass 1
- More comprehensive greenscreen shader
- linear compositing?
- compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store
- Add WebView2 for an embedded native control surface.
- MSDF typography rasterisation
- More shader-library organisation and filtering as the built-in library grows.
- Optional linear-light compositing mode.
- 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
- Mipmappong for shader declared textures
- Multipass for shaders at request
- Mipmapping for shader-declared textures

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
@@ -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 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
@@ -97,10 +97,13 @@ Optional fields:
- `description`: display/help text for the shader library.
- `category`: UI grouping label.
- `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.
- `fonts`: packaged font assets for live text parameters.
- `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:
- `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.
## 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
Your shader file must implement the manifest `entryPoint`.
@@ -163,7 +247,7 @@ Fields:
- `uv`: normalized texture coordinates, usually `0..1`.
- `sourceColor`: decoded RGBA source video at `uv`.
- `inputResolution`: decoded input video resolution in pixels.
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured video I/O output mode.
- `time`: elapsed runtime time in seconds.
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
@@ -177,8 +261,8 @@ Fields:
Color/precision notes:
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
- The host prefers 10-bit DeckLink YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than the packed DeckLink byte formats.
- 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.
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
## Helper Functions
@@ -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.
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
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
@@ -89,7 +89,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
@@ -111,7 +111,7 @@
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
@@ -131,7 +131,7 @@
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
@@ -156,7 +156,7 @@
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
@@ -175,47 +175,52 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="gl\GLExtensions.cpp" />
<ClCompile Include="gl\renderer\GLExtensions.cpp" />
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
<ClCompile Include="gl\OpenGLComposite.cpp" />
<ClCompile Include="gl\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\OpenGLRenderer.cpp" />
<ClCompile Include="gl\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\TemporalHistoryBuffers.cpp" />
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
<ClCompile Include="gl\renderer\RenderTargetPool.cpp" />
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp" />
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="DeckLinkAPI_i.c" />
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c" />
<ClCompile Include="control\RuntimeServices.cpp" />
<ClCompile Include="decklink\DeckLinkSession.cpp" />
<ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp" />
<ClCompile Include="runtime\RuntimeClock.cpp" />
<ClCompile Include="videoio\VideoIOFormat.cpp" />
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="gl\GLExtensions.h" />
<ClInclude Include="gl\renderer\GLExtensions.h" />
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
<ClInclude Include="gl\OpenGLComposite.h" />
<ClInclude Include="gl\OpenGLRenderPass.h" />
<ClInclude Include="gl\OpenGLRenderer.h" />
<ClInclude Include="gl\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\PngScreenshotWriter.h" />
<ClInclude Include="gl\ShaderBuildQueue.h" />
<ClInclude Include="gl\TemporalHistoryBuffers.h" />
<ClInclude Include="gl\OpenGLVideoIOBridge.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h" />
<ClInclude Include="gl\renderer\OpenGLRenderer.h" />
<ClInclude Include="gl\renderer\RenderTargetPool.h" />
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h" />
<ClInclude Include="gl\shader\ShaderBuildQueue.h" />
<ClInclude Include="gl\pipeline\TemporalHistoryBuffers.h" />
<ClInclude Include="gl\pipeline\OpenGLVideoIOBridge.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="control\RuntimeServices.h" />
<ClInclude Include="decklink\DeckLinkSession.h" />
<ClInclude Include="decklink\DeckLinkVideoIOFormat.h" />
<ClInclude Include="videoio\decklink\DeckLinkSession.h" />
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h" />
<ClInclude Include="runtime\RuntimeClock.h" />
<ClInclude Include="videoio\VideoIOFormat.h" />
<ClInclude Include="videoio\VideoIOTypes.h" />

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
@@ -18,7 +18,7 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="gl\GLExtensions.cpp">
<ClCompile Include="gl\renderer\GLExtensions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
@@ -27,40 +27,46 @@
<ClCompile Include="gl\OpenGLComposite.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\OpenGLRenderPass.cpp">
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\OpenGLRenderer.cpp">
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\OpenGLShaderPrograms.cpp">
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\PngScreenshotWriter.cpp">
<ClCompile Include="gl\renderer\RenderTargetPool.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\ShaderBuildQueue.cpp">
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\TemporalHistoryBuffers.cpp">
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp">
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="stdafx.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DeckLinkAPI_i.c">
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c">
<Filter>DeckLink API</Filter>
</ClCompile>
<ClCompile Include="control\RuntimeServices.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="decklink\DeckLinkSession.cpp">
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp">
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="runtime\RuntimeClock.cpp">
@@ -74,7 +80,7 @@
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="gl\GLExtensions.h">
<ClInclude Include="gl\renderer\GLExtensions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
@@ -83,25 +89,34 @@
<ClInclude Include="gl\OpenGLComposite.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\OpenGLRenderPass.h">
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\OpenGLRenderer.h">
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\OpenGLShaderPrograms.h">
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\PngScreenshotWriter.h">
<ClInclude Include="gl\renderer\OpenGLRenderer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\ShaderBuildQueue.h">
<ClInclude Include="gl\renderer\RenderTargetPool.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\TemporalHistoryBuffers.h">
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\OpenGLVideoIOBridge.h">
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\shader\ShaderBuildQueue.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\TemporalHistoryBuffers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\OpenGLVideoIOBridge.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
@@ -116,10 +131,10 @@
<ClInclude Include="control\RuntimeServices.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="decklink\DeckLinkSession.h">
<ClInclude Include="videoio\decklink\DeckLinkSession.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="decklink\DeckLinkVideoIOFormat.h">
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="runtime\RuntimeClock.h">

View File

@@ -4,6 +4,7 @@
#include "GLExtensions.h"
#include "GlRenderConstants.h"
#include "OpenGLRenderPass.h"
#include "OpenGLRenderPipeline.h"
#include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h"
@@ -29,16 +30,20 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
{
InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>();
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
*mVideoIO,
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
*mRenderer,
*mRuntimeHost,
pMutex,
hGLDC,
hGLRC,
[this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(); });
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
*mVideoIO,
*mRenderer,
*mRenderPipeline,
*mRuntimeHost,
pMutex,
hGLDC,
hGLRC);
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
@@ -100,7 +105,8 @@ bool OpenGLComposite::InitVideoIO()
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false;
}
if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason))
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
goto error;
if (! CheckOpenGLExtensions())

View File

@@ -27,6 +27,7 @@
class VideoIODevice;
class OpenGLVideoIOBridge;
class OpenGLRenderPass;
class OpenGLRenderPipeline;
class OpenGLShaderPrograms;
class RuntimeServices;
class ShaderBuildQueue;
@@ -81,6 +82,7 @@ private:
std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
std::unique_ptr<OpenGLRenderPass> mRenderPass;
std::unique_ptr<OpenGLRenderPipeline> mRenderPipeline;
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
std::unique_ptr<RuntimeServices> mRuntimeServices;

View File

@@ -1,169 +0,0 @@
#include "OpenGLRenderPass.h"
#include "GlRenderConstants.h"
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
mRenderer(renderer)
{
}
void OpenGLRenderPass::Render(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (hasInputSource)
{
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
}
else
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
if (layerStates.empty() || layerPrograms.empty())
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
}
else
{
GLuint sourceTexture = mRenderer.DecodedTexture();
GLuint sourceFrameBuffer = mRenderer.DecodeFramebuffer();
for (std::size_t index = 0; index < layerStates.size() && index < layerPrograms.size(); ++index)
{
const std::size_t remaining = layerStates.size() - index;
const bool writeToMain = (remaining % 2) == 1;
RenderShaderProgram(
sourceTexture,
writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(),
layerPrograms[index],
layerStates[index],
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
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();
}
}
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.DecodeProgram());
const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution");
const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution");
const GLint inputPixelFormatLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uInputPixelFormat");
if (packedResolutionLocation >= 0)
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
if (decodedResolutionLocation >= 0)
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
if (inputPixelFormatLocation >= 0)
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
void OpenGLRenderPass::RenderShaderProgram(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
LayerProgram& layerProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings)
{
std::string textError;
if (!updateTextBinding(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
mRenderer.TemporalHistory().BindSamplers(state, sourceTexture, historyCap);
BindLayerTextureAssets(layerProgram);
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(layerProgram.program);
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
UnbindLayerTextureAssets(layerProgram, historyCap);
}
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,244 +0,0 @@
#include "ShaderProgramCompiler.h"
#include "GlRenderConstants.h"
#include "GlScopedObjects.h"
#include "GlShaderSources.h"
#include <cstring>
#include <vector>
namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
return;
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
}
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mTextureBindings(textureBindings)
{
}
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::string fragmentShaderSource;
std::string loadError;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
return CompilePreparedLayerProgram(state, fragmentShaderSource, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
std::string loadError;
std::vector<LayerProgram::TextureBinding> textureBindings;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings);
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
const GLuint shaderTextureBase = state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
glUseProgram(newProgram.get());
const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "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(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);
layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId;
layerProgram.shaderTextureBase = shaderTextureBase;
layerProgram.program = newProgram.release();
layerProgram.vertexShader = newVertexShader.release();
layerProgram.fragmentShader = newFragmentShader.release();
layerProgram.textureBindings.swap(textureBindings);
layerProgram.textBindings.swap(textBindings);
return true;
}
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kDecodeFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
mRenderer.DestroyDecodeShaderProgram();
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kOutputPackFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
glUseProgram(newProgram.get());
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
if (outputSamplerLocation >= 0)
glUniform1i(outputSamplerLocation, 0);
glUseProgram(0);
mRenderer.DestroyOutputPackShaderProgram();
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}

View File

@@ -1,104 +0,0 @@
#include "ShaderTextureBindings.h"
#include "GlRenderConstants.h"
#include "TextRasterizer.h"
#include "TextureAssetLoader.h"
#include <algorithm>
#include <filesystem>
namespace
{
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
auto valueIt = state.parameterValues.find(parameterId);
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
if (!definition.fontId.empty())
{
for (const ShaderFontAsset& fontAsset : state.fontAssets)
{
if (fontAsset.id == definition.fontId)
return &fontAsset;
}
}
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
}
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
return ::LoadTextureAsset(textureAsset, textureId, error);
}
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
{
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
LayerProgram::TextBinding textBinding;
textBinding.parameterId = definition.id;
textBinding.samplerName = definition.id + "Texture";
textBinding.fontId = definition.fontId;
glGenTextures(1, &textBinding.texture);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
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);
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBindings.push_back(textBinding);
}
}
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
const std::string text = TextValueForBinding(state, textBinding.parameterId);
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
return true;
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
if (definitionIt == state.parameterDefinitions.end())
return true;
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
std::filesystem::path fontPath;
if (fontAsset)
fontPath = fontAsset->path;
std::vector<unsigned char> sdf;
if (!RasterizeTextSdf(text, fontPath, sdf, error))
return false;
GLint previousActiveTexture = 0;
GLint previousUnpackBuffer = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
glActiveTexture(GL_TEXTURE0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
textBinding.renderedText = text;
textBinding.renderedWidth = kTextTextureWidth;
textBinding.renderedHeight = kTextTextureHeight;
return true;
}
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
{
GLint location = glGetUniformLocation(program, samplerName.c_str());
if (location >= 0)
return location;
return glGetUniformLocation(program, (samplerName + "_0").c_str());
}

View File

@@ -1,18 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <string>
#include <vector>
class ShaderTextureBindings
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
};

View File

@@ -0,0 +1,275 @@
#include "OpenGLRenderPass.h"
#include "GlRenderConstants.h"
#include <map>
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
mRenderer(renderer)
{
}
void OpenGLRenderPass::Render(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (hasInputSource)
{
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
}
else
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
if (layerStates.empty() || layerPrograms.empty())
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
}
else
{
const std::vector<RenderPassDescriptor> passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
for (const RenderPassDescriptor& pass : passes)
{
RenderLayerPass(
pass,
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
}
}
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.DecodeProgram());
const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution");
const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution");
const GLint inputPixelFormatLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uInputPixelFormat");
if (packedResolutionLocation >= 0)
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
if (decodedResolutionLocation >= 0)
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
if (inputPixelFormatLocation >= 0)
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
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(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
{
std::string textError;
if (!updateTextBinding(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
glBindVertexArray(mRenderer.FullscreenVertexArray());
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));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
}

View File

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

View File

@@ -0,0 +1,95 @@
#include "OpenGLRenderPipeline.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "VideoIOFormat.h"
#include <chrono>
#include <gl/gl.h>
OpenGLRenderPipeline::OpenGLRenderPipeline(
OpenGLRenderer& renderer,
RuntimeHost& runtimeHost,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mRenderEffect(renderEffect),
mOutputReady(outputReady),
mPaint(paint)
{
}
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
{
const VideoIOState& state = context.videoState;
const auto renderStartTime = std::chrono::steady_clock::now();
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
mRenderEffect();
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady)
mOutputReady();
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
PackOutputFor10Bit(state);
glFlush();
const auto renderEndTime = std::chrono::steady_clock::now();
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
mRuntimeHost.TrySetPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
mRuntimeHost.TryAdvanceFrame();
ReadOutputFrame(state, outputFrame);
if (mPaint)
mPaint();
return true;
}
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.OutputPackProgram());
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
const GLint packFormatLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputPackFormat");
if (outputResolutionLocation >= 0)
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
if (activeWordsLocation >= 0)
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);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
{
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
}
else
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes);
}
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include "VideoIOTypes.h"
#include <functional>
class OpenGLRenderer;
class RuntimeHost;
struct RenderPipelineFrameContext
{
VideoIOState videoState;
VideoIOCompletion completion;
};
class OpenGLRenderPipeline
{
public:
using RenderEffectCallback = std::function<void()>;
using OutputReadyCallback = std::function<void()>;
using PaintCallback = std::function<void()>;
OpenGLRenderPipeline(
OpenGLRenderer& renderer,
RuntimeHost& runtimeHost,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint);
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
private:
void PackOutputFor10Bit(const VideoIOState& state);
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
RenderEffectCallback mRenderEffect;
OutputReadyCallback mOutputReady;
PaintCallback mPaint;
};

View File

@@ -9,22 +9,18 @@
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
VideoIODevice& videoIO,
OpenGLRenderer& renderer,
OpenGLRenderPipeline& renderPipeline,
RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint) :
HGLRC hglrc) :
mVideoIO(videoIO),
mRenderer(renderer),
mRenderPipeline(renderPipeline),
mRuntimeHost(runtimeHost),
mMutex(mutex),
mHdc(hdc),
mHglrc(hglrc),
mRenderEffect(renderEffect),
mOutputReady(outputReady),
mPaint(paint)
mHglrc(hglrc)
{
}
@@ -113,58 +109,7 @@ void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& complet
// make GL context current in this thread
wglMakeCurrent(mHdc, mHglrc);
// Draw the effect output to the off-screen framebuffer.
const auto renderStartTime = std::chrono::steady_clock::now();
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
mRenderEffect();
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady)
mOutputReady();
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.OutputPackProgram());
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
if (outputResolutionLocation >= 0)
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
if (activeWordsLocation >= 0)
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
glFlush();
const auto renderEndTime = std::chrono::steady_clock::now();
const double frameBudgetMilliseconds = state.frameBudgetMilliseconds;
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
mRuntimeHost.TryAdvanceFrame();
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
}
else
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes);
}
mPaint();
mRenderPipeline.RenderFrame(frameContext, outputFrame);
mVideoIO.EndOutputFrame(outputFrame);

View File

@@ -1,39 +1,25 @@
#pragma once
#include "VideoIOTypes.h"
#include "OpenGLRenderPipeline.h"
#include <windows.h>
#include <chrono>
#include <functional>
#include <cstdint>
class OpenGLRenderer;
class RuntimeHost;
struct RenderPipelineFrameContext
{
VideoIOState videoState;
VideoIOCompletion completion;
};
class OpenGLVideoIOBridge
{
public:
using RenderEffectCallback = std::function<void()>;
using OutputReadyCallback = std::function<void()>;
using PaintCallback = std::function<void()>;
OpenGLVideoIOBridge(
VideoIODevice& videoIO,
OpenGLRenderer& renderer,
OpenGLRenderPipeline& renderPipeline,
RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint);
HGLRC hglrc);
void VideoFrameArrived(const VideoIOFrame& inputFrame);
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
@@ -43,13 +29,11 @@ private:
VideoIODevice& mVideoIO;
OpenGLRenderer& mRenderer;
OpenGLRenderPipeline& mRenderPipeline;
RuntimeHost& mRuntimeHost;
CRITICAL_SECTION& mMutex;
HDC mHdc;
HGLRC mHglrc;
RenderEffectCallback mRenderEffect;
OutputReadyCallback mOutputReady;
PaintCallback mPaint;
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
double mCompletionIntervalMilliseconds = 0.0;
double mSmoothedCompletionIntervalMilliseconds = 0.0;

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);
}
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
{
if (ring.filledCount == 0 || ring.slots.empty())

View File

@@ -40,6 +40,8 @@ public:
void PushSourceFramebuffer(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);
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;
unsigned SourceAvailableCount() 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);
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)
@@ -35,80 +26,32 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
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, &mIdDepthBuf);
glGenVertexArrays(1, &mFullscreenVAO);
glGenBuffers(1, &mGlobalParamsUBO);
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize decode framebuffer.";
if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize layer framebuffer.";
if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
return false;
if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
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);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
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)
{
error = "Cannot initialize framebuffer.";
return false;
}
glGenTextures(1, &mOutputTexture);
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.";
if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
return false;
}
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.";
if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
return false;
}
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
@@ -137,6 +80,11 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
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)
{
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);
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, mViewWidth, mViewHeight);
@@ -186,50 +134,21 @@ void OpenGLRenderer::DestroyResources()
glDeleteVertexArrays(1, &mFullscreenVAO);
if (mGlobalParamsUBO != 0)
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)
glDeleteRenderbuffers(1, &mIdColorBuf);
if (mIdDepthBuf != 0)
glDeleteRenderbuffers(1, &mIdDepthBuf);
if (mCaptureTexture != 0)
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)
glDeleteBuffers(1, &mTextureUploadBuffer);
mRenderTargets.Destroy();
mFullscreenVAO = 0;
mGlobalParamsUBO = 0;
mDecodeFrameBuf = 0;
mLayerTempFrameBuf = 0;
mIdFrameBuf = 0;
mOutputFrameBuf = 0;
mOutputPackFrameBuf = 0;
mIdColorBuf = 0;
mIdDepthBuf = 0;
mCaptureTexture = 0;
mDecodedTexture = 0;
mLayerTempTexture = 0;
mFBOTexture = 0;
mOutputTexture = 0;
mOutputPackTexture = 0;
mTextureUploadBuffer = 0;
mGlobalParamsUBOSize = 0;
@@ -241,43 +160,47 @@ void OpenGLRenderer::DestroyResources()
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
{
for (LayerProgram::TextureBinding& binding : layerProgram.textureBindings)
for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
{
if (binding.texture != 0)
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
if (binding.texture != 0)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
}
}
passProgram.textureBindings.clear();
for (LayerProgram::TextBinding& binding : passProgram.textBindings)
{
if (binding.texture != 0)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
}
}
passProgram.textBindings.clear();
if (passProgram.program != 0)
{
glDeleteProgram(passProgram.program);
passProgram.program = 0;
}
if (passProgram.fragmentShader != 0)
{
glDeleteShader(passProgram.fragmentShader);
passProgram.fragmentShader = 0;
}
if (passProgram.vertexShader != 0)
{
glDeleteShader(passProgram.vertexShader);
passProgram.vertexShader = 0;
}
}
layerProgram.textureBindings.clear();
for (LayerProgram::TextBinding& binding : layerProgram.textBindings)
{
if (binding.texture != 0)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
}
}
layerProgram.textBindings.clear();
if (layerProgram.program != 0)
{
glDeleteProgram(layerProgram.program);
layerProgram.program = 0;
}
if (layerProgram.fragmentShader != 0)
{
glDeleteShader(layerProgram.fragmentShader);
layerProgram.fragmentShader = 0;
}
if (layerProgram.vertexShader != 0)
{
glDeleteShader(layerProgram.vertexShader);
layerProgram.vertexShader = 0;
}
layerProgram.passes.clear();
}
void OpenGLRenderer::DestroyLayerPrograms()

View File

@@ -1,6 +1,7 @@
#pragma once
#include "GLExtensions.h"
#include "RenderTargetPool.h"
#include "ShaderTypes.h"
#include "TemporalHistoryBuffers.h"
@@ -36,26 +37,35 @@ public:
std::string layerId;
std::string shaderId;
GLuint shaderTextureBase = 0;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
std::vector<TextureBinding> textureBindings;
std::vector<TextBinding> textBindings;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
GLuint shaderTextureBase = 0;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
std::vector<TextureBinding> textureBindings;
std::vector<TextBinding> textBindings;
};
std::vector<PassProgram> passes;
};
GLuint CaptureTexture() const { return mCaptureTexture; }
GLuint DecodedTexture() const { return mDecodedTexture; }
GLuint LayerTempTexture() const { return mLayerTempTexture; }
GLuint CompositeTexture() const { return mFBOTexture; }
GLuint OutputTexture() const { return mOutputTexture; }
GLuint OutputPackTexture() const { return mOutputPackTexture; }
GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; }
GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; }
GLuint CompositeFramebuffer() const { return mIdFrameBuf; }
GLuint OutputFramebuffer() const { return mOutputFrameBuf; }
GLuint OutputPackFramebuffer() const { return mOutputPackFrameBuf; }
GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
GLuint DecodeProgram() const { return mDecodeProgram; }
@@ -65,6 +75,9 @@ public:
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
std::vector<LayerProgram>& LayerPrograms() { 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; }
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
@@ -80,17 +93,7 @@ public:
private:
GLuint mCaptureTexture = 0;
GLuint mDecodedTexture = 0;
GLuint mLayerTempTexture = 0;
GLuint mFBOTexture = 0;
GLuint mOutputTexture = 0;
GLuint mOutputPackTexture = 0;
GLuint mTextureUploadBuffer = 0;
GLuint mDecodeFrameBuf = 0;
GLuint mLayerTempFrameBuf = 0;
GLuint mIdFrameBuf = 0;
GLuint mOutputFrameBuf = 0;
GLuint mOutputPackFrameBuf = 0;
GLuint mIdColorBuf = 0;
GLuint mIdDepthBuf = 0;
GLuint mFullscreenVAO = 0;
@@ -105,5 +108,6 @@ private:
int mViewWidth = 0;
int mViewHeight = 0;
std::vector<LayerProgram> mLayerPrograms;
RenderTargetPool mRenderTargets;
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"
"uniform vec2 uOutputVideoResolution;\n"
"uniform float uActiveV210Words;\n"
"uniform int uOutputPackFormat;\n"
"in vec2 vTexCoord;\n"
"layout(location = 0) out vec4 fragColor;\n"
"vec3 rgbAt(int x, int y)\n"
"vec4 rgbaAt(int x, int y)\n"
"{\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"
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
"{\n"
@@ -112,9 +117,35 @@ const char* kOutputPackFragmentShaderSource =
"{\n"
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\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"
"{\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"
" {\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);
}
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) :
@@ -39,6 +53,8 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
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;
newPrograms.reserve(layerStates.size());
@@ -54,6 +70,15 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
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();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = layerStates;
@@ -85,13 +110,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
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;
newPrograms.reserve(preparedBuild.layers.size());
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
{
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)
DestroySingleLayerProgram(program);
@@ -100,6 +127,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
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();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = preparedBuild.layerStates;

View File

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

View File

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

View File

@@ -0,0 +1,233 @@
#include "ShaderProgramCompiler.h"
#include "GlRenderConstants.h"
#include "GlScopedObjects.h"
#include "GlShaderSources.h"
#include <cstring>
#include <utility>
#include <vector>
namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
return;
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
}
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mTextureBindings(textureBindings)
{
}
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::vector<ShaderPassBuildSource> passSources;
std::string loadError;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
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 linkResult = GL_FALSE;
const char* fragmentSource = passSource.fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
std::vector<LayerProgram::TextureBinding> textureBindings;
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> 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");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
glUseProgram(newProgram.get());
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
glUseProgram(0);
passProgram.program = newProgram.release();
passProgram.vertexShader = newVertexShader.release();
passProgram.fragmentShader = newFragmentShader.release();
layerProgram.passes.push_back(std::move(passProgram));
}
return true;
}
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kDecodeFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
mRenderer.DestroyDecodeShaderProgram();
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kOutputPackFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
glUseProgram(newProgram.get());
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
if (outputSamplerLocation >= 0)
glUniform1i(outputSamplerLocation, 0);
glUseProgram(0);
mRenderer.DestroyOutputPackShaderProgram();
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}

View File

@@ -5,16 +5,18 @@
#include "ShaderTextureBindings.h"
#include <string>
#include <vector>
class ShaderProgramCompiler
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
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 CompileOutputPackShader(int errorMessageSize, char* errorMessage);

View File

@@ -0,0 +1,223 @@
#include "ShaderTextureBindings.h"
#include "GlRenderConstants.h"
#include "TextRasterizer.h"
#include "TextureAssetLoader.h"
#include <algorithm>
#include <filesystem>
namespace
{
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
auto valueIt = state.parameterValues.find(parameterId);
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
if (!definition.fontId.empty())
{
for (const ShaderFontAsset& fontAsset : state.fontAssets)
{
if (fontAsset.id == definition.fontId)
return &fontAsset;
}
}
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
}
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
return ::LoadTextureAsset(textureAsset, textureId, error);
}
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
{
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
LayerProgram::TextBinding textBinding;
textBinding.parameterId = definition.id;
textBinding.samplerName = definition.id + "Texture";
textBinding.fontId = definition.fontId;
glGenTextures(1, &textBinding.texture);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
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);
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBindings.push_back(textBinding);
}
}
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
const std::string text = TextValueForBinding(state, textBinding.parameterId);
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
return true;
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
if (definitionIt == state.parameterDefinitions.end())
return true;
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
std::filesystem::path fontPath;
if (fontAsset)
fontPath = fontAsset->path;
std::vector<unsigned char> sdf;
if (!RasterizeTextSdf(text, fontPath, sdf, error))
return false;
GLint previousActiveTexture = 0;
GLint previousUnpackBuffer = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
glActiveTexture(GL_TEXTURE0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
textBinding.renderedText = text;
textBinding.renderedWidth = kTextTextureWidth;
textBinding.renderedHeight = kTextTextureHeight;
return true;
}
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
{
GLint location = glGetUniformLocation(program, samplerName.c_str());
if (location >= 0)
return location;
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

@@ -0,0 +1,41 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <string>
#include <vector>
class ShaderTextureBindings
{
public:
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);
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
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;
}
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)
{
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))
return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
@@ -1300,7 +1292,7 @@ bool RuntimeHost::TryAdvanceFrame()
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
{
@@ -1324,16 +1316,30 @@ bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std
}
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)
{
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what();
error = std::string("RuntimeHost::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception.";
error = "RuntimeHost::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
return false;
}
}
@@ -1671,39 +1677,6 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
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
{
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
@@ -1987,6 +1960,8 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
JsonValue parameter = JsonValue::MakeObject();
parameter.set("id", JsonValue(definition.id));
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("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));

View File

@@ -9,6 +9,7 @@
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
class RuntimeHost
@@ -50,7 +51,7 @@ public:
void AdvanceFrame();
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;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
@@ -115,7 +116,6 @@ private:
bool LoadPersistentState(std::string& error);
bool SavePersistentState(std::string& error) const;
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;
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) 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);
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);
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;
}
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 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;
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error))
if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false;
@@ -167,7 +167,7 @@ bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderP
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";
wrapperSource = ReadTextFile(templatePath, error);
@@ -183,13 +183,25 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
return true;
}
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{
char slangRootBuffer[MAX_PATH] = {};
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer)));
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
{
std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyRoot))
{

View File

@@ -15,10 +15,10 @@ public:
const std::filesystem::path& patchedGlslPath,
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:
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 RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, 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;
}
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)
{
if (typeName == "float")
@@ -250,6 +239,107 @@ bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPac
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)
{
const JsonValue* texturesValue = nullptr;
@@ -503,6 +593,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, 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))
return false;
if (!std::filesystem::exists(shaderPackage.shaderPath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
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);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&

View File

@@ -26,6 +26,7 @@ struct ShaderParameterDefinition
{
std::string id;
std::string label;
std::string description;
ShaderParameterType type = ShaderParameterType::Float;
std::vector<double> defaultNumbers;
std::vector<double> minNumbers;
@@ -76,6 +77,24 @@ struct ShaderFontAsset
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
{
std::string id;
@@ -86,6 +105,7 @@ struct ShaderPackage
std::filesystem::path directoryPath;
std::filesystem::path shaderPath;
std::filesystem::path manifestPath;
std::vector<ShaderPassDefinition> passes;
std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ public:
virtual ~VideoIODevice() = default;
virtual void ReleaseResources() = 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 ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
virtual bool Start() = 0;

View File

@@ -223,7 +223,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return true;
}
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error)
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
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. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8;
if (!outputTenBitSupported)
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
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. ";
int deckLinkOutputRowBytes = 0;

View File

@@ -22,7 +22,7 @@ public:
void ReleaseResources() 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 ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
bool Start() override;

View File

@@ -6,6 +6,8 @@ BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
{
case VideoIOPixelFormat::V210:
return bmdFormat10BitYUV;
case VideoIOPixelFormat::Yuva10:
return bmdFormat10BitYUVA;
case VideoIOPixelFormat::Bgra8:
return bmdFormat8BitBGRA;
case VideoIOPixelFormat::Uyvy8:
@@ -18,6 +20,8 @@ VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
{
if (format == bmdFormat10BitYUV)
return VideoIOPixelFormat::V210;
if (format == bmdFormat10BitYUVA)
return VideoIOPixelFormat::Yuva10;
if (format == bmdFormat8BitBGRA)
return VideoIOPixelFormat::Bgra8;
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.
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
The listener accepts these OSC argument types:
@@ -65,7 +67,7 @@ Examples:
/VideoShaderToys/fisheye-reproject/panDegrees 45.0
/VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid"
/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.

View File

@@ -201,6 +201,7 @@ paths:
post:
tags: [Runtime]
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
requestBody:
required: false
@@ -358,6 +359,8 @@ components:
$ref: "#/components/schemas/VideoStatus"
decklink:
$ref: "#/components/schemas/DeckLinkStatus"
videoIO:
$ref: "#/components/schemas/VideoIOStatus"
performance:
$ref: "#/components/schemas/PerformanceStatus"
shaders:
@@ -415,6 +418,8 @@ components:
type: string
DeckLinkStatus:
type: object
deprecated: true
description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
properties:
modelName:
type: string
@@ -430,6 +435,26 @@ components:
type: boolean
statusMessage:
type: string
VideoIOStatus:
type: object
properties:
backend:
type: string
example: decklink
modelName:
type: string
supportsInternalKeying:
type: boolean
supportsExternalKeying:
type: boolean
keyerInterfaceAvailable:
type: boolean
externalKeyingRequested:
type: boolean
externalKeyingActive:
type: boolean
statusMessage:
type: string
PerformanceStatus:
type: object
properties:
@@ -441,6 +466,18 @@ components:
type: number
budgetUsedPercent:
type: number
completionIntervalMs:
type: number
smoothedCompletionIntervalMs:
type: number
maxCompletionIntervalMs:
type: number
lateFrameCount:
type: number
droppedFrameCount:
type: number
flushedFrameCount:
type: number
ShaderSummary:
type: object
properties:
@@ -452,6 +489,12 @@ components:
type: string
category:
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:
$ref: "#/components/schemas/TemporalState"
TemporalState:
@@ -490,9 +533,21 @@ components:
type: string
label:
type: string
description:
type: string
description: Short helper text shown under the parameter label in the control UI.
type:
type: string
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:
type: array
items:
@@ -509,6 +564,12 @@ components:
type: array
items:
$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:
description: Current parameter value.
oneOf:

View File

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

View File

@@ -11,11 +11,24 @@
"type": "enum",
"default": "x1_33",
"options": [
{ "value": "x1_3", "label": "1.3x" },
{ "value": "x1_33", "label": "1.33x" },
{ "value": "x1_5", "label": "1.5x" },
{ "value": "x2_0", "label": "2x" }
]
{
"value": "x1_3",
"label": "1.3x"
},
{
"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",
@@ -23,24 +36,50 @@
"type": "enum",
"default": "fit",
"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",
"label": "Pan",
"type": "vec2",
"default": [0.0, 0.0],
"min": [-1.0, -1.0],
"max": [1.0, 1.0],
"step": [0.001, 0.001]
"default": [
0,
0
],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Reframes the desqueezed image after fit/fill scaling."
},
{
"id": "outsideColor",
"label": "Outside 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",
"label": "Spin Rotation",
"type": "float",
"default": -2.0,
"min": -8.0,
"max": 8.0,
"step": 0.05
"default": -2,
"min": -8,
"max": 8,
"step": 0.05,
"description": "Base rotation applied to the swirl field."
},
{
"id": "spinSpeed",
"label": "Spin Speed",
"type": "float",
"default": 7.0,
"min": 0.0,
"max": 20.0,
"step": 0.1
"default": 7,
"min": 0,
"max": 20,
"step": 0.1,
"description": "How quickly the swirl pattern rotates."
},
{
"id": "spinAmount",
"label": "Spin Amount",
"type": "float",
"default": 0.25,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Amount of radial twisting in the swirl."
},
{
"id": "spinEase",
"label": "Spin Ease",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 3.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 3,
"step": 0.01,
"description": "Changes how strongly the twist falls off from the center."
},
{
"id": "contrast",
@@ -47,59 +51,94 @@
"type": "float",
"default": 3.5,
"min": 0.5,
"max": 8.0,
"step": 0.05
"max": 8,
"step": 0.05,
"description": "Adjusts separation between dark and bright areas."
},
{
"id": "lighting",
"label": "Lighting",
"type": "float",
"default": 0.4,
"min": 0.0,
"min": 0,
"max": 1.5,
"step": 0.01
"step": 0.01,
"description": "Strength of the highlight/shadow modulation."
},
{
"id": "offset",
"label": "Offset",
"type": "vec2",
"default": [0.0, 0.0],
"min": [-1.0, -1.0],
"max": [1.0, 1.0],
"step": [0.001, 0.001]
"default": [
0,
0
],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the generated field in normalized coordinates."
},
{
"id": "colour1",
"label": "Colour 1",
"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",
"label": "Colour 2",
"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",
"label": "Colour 3",
"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",
"label": "Rotate Field",
"type": "bool",
"default": false
"default": false,
"description": "Rotates the whole generated field over time."
},
{
"id": "sourceMix",
"label": "Source Mix",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the generated effect with the incoming video."
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,55 +9,85 @@
"id": "lensFovDegrees",
"label": "Lens FOV",
"type": "float",
"default": 190.0,
"min": 1.0,
"max": 220.0,
"step": 0.1
"default": 190,
"min": 1,
"max": 220,
"step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
},
{
"id": "center",
"label": "Optical Center",
"type": "vec2",
"default": [0.5, 0.5],
"min": [0.0, 0.0],
"max": [1.0, 1.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.5
],
"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",
"label": "Fisheye Radius",
"type": "vec2",
"default": [0.5, 0.8889],
"min": [0.001, 0.001],
"max": [2.0, 2.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.8889
],
"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",
"label": "Yaw",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Rotates the virtual view horizontally."
},
{
"id": "pitchDegrees",
"label": "Pitch",
"type": "float",
"default": 0.0,
"min": -120.0,
"max": 120.0,
"step": 0.1
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Rotates the virtual view vertically."
},
{
"id": "rollDegrees",
"label": "Roll",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live roll rotation around the viewing axis."
},
{
"id": "fisheyeModel",
@@ -65,35 +95,56 @@
"type": "enum",
"default": "equidistant",
"options": [
{ "value": "equidistant", "label": "Equidistant" },
{ "value": "equisolid", "label": "Equisolid" },
{ "value": "stereographic", "label": "Stereographic" },
{ "value": "orthographic", "label": "Orthographic" }
]
{
"value": "equidistant",
"label": "Equidistant"
},
{
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
},
{
"id": "edgeFill",
"label": "Edge Fill",
"type": "float",
"default": 0.06,
"min": 0.0,
"min": 0,
"max": 0.3,
"step": 0.001
"step": 0.001,
"description": "Extends edge samples outward to cover small missing areas."
},
{
"id": "edgeBlur",
"label": "Edge Blur",
"type": "float",
"default": 0.018,
"min": 0.0,
"min": 0,
"max": 0.12,
"step": 0.001
"step": 0.001,
"description": "Softens the dilated edge fill."
},
{
"id": "outsideColor",
"label": "Outside 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",
"label": "Lens FOV",
"type": "float",
"default": 190.0,
"min": 1.0,
"max": 220.0,
"step": 0.1
"default": 190,
"min": 1,
"max": 220,
"step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
},
{
"id": "center",
"label": "Optical Center",
"type": "vec2",
"default": [0.5, 0.5],
"min": [0.0, 0.0],
"max": [1.0, 1.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.5
],
"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",
"label": "Fisheye Radius",
"type": "vec2",
"default": [0.5, 0.885],
"min": [0.001, 0.001],
"max": [2.0, 2.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.885
],
"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",
"label": "Virtual FOV",
"type": "float",
"default": 75.0,
"min": 1.0,
"max": 175.0,
"step": 0.1
"default": 75,
"min": 1,
"max": 175,
"step": 0.1,
"description": "Field of view of the generated virtual camera."
},
{
"id": "basePanDegrees",
"label": "Base Pan",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Permanent horizontal alignment offset before live pan."
},
{
"id": "baseTiltDegrees",
"label": "Base Tilt",
"type": "float",
"default": 0.0,
"min": -120.0,
"max": 120.0,
"step": 0.1
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Permanent vertical alignment offset before live tilt."
},
{
"id": "baseRollDegrees",
"label": "Base Roll",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Permanent roll alignment offset before live roll."
},
{
"id": "panDegrees",
"label": "Pan",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live horizontal view rotation."
},
{
"id": "tiltDegrees",
"label": "Tilt",
"type": "float",
"default": 0.0,
"min": -120.0,
"max": 120.0,
"step": 0.1
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Live vertical view rotation."
},
{
"id": "rollDegrees",
"label": "Roll",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live roll rotation around the viewing axis."
},
{
"id": "fisheyeModel",
@@ -101,11 +135,24 @@
"type": "enum",
"default": "equidistant",
"options": [
{ "value": "equidistant", "label": "Equidistant" },
{ "value": "equisolid", "label": "Equisolid" },
{ "value": "stereographic", "label": "Stereographic" },
{ "value": "orthographic", "label": "Orthographic" }
]
{
"value": "equidistant",
"label": "Equidistant"
},
{
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
},
{
"id": "outputProjection",
@@ -113,15 +160,28 @@
"type": "enum",
"default": "rectilinear",
"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",
"label": "Outside 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",
"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",
"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": [
{
"id": "radius",
"label": "Radius",
"type": "float",
"default": 2.0,
"min": 0.0,
"max": 8.0,
"step": 0.1
"default": 2,
"min": 0,
"max": 8,
"step": 0.1,
"description": "Blur radius in pixels for each separable pass."
},
{
"id": "strength",
"label": "Strength",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends between the original and blurred result."
},
{
"id": "samples",
"label": "Samples",
"type": "float",
"default": 2.0,
"min": 0.0,
"max": 25.0,
"step": 1.0
"default": 2,
"min": 0,
"max": 25,
"step": 1,
"description": "Number of taps per direction; higher values cost more GPU time."
}
]
}

View File

@@ -1,25 +1,22 @@
float4 shadeVideo(ShaderContext context)
float4 gaussianBlurDirection(ShaderContext context, float2 direction)
{
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
float blurRadius = max(radius, 0.0);
float2 sampleStep = texel * blurRadius;
float blurRadius = max(radius, 0.0) * saturate(strength);
float2 sampleStep = texel * blurRadius * direction;
int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5);
float4 center = sampleVideo(context.uv);
float4 blur = float4(0.0, 0.0, 0.0, 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 sigma = max(float(sampleRadius) * 0.5, 0.5);
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
float2 offset = float2(float(x), float(y)) * sampleStep;
blur += sampleVideo(context.uv + offset) * weight;
totalWeight += weight;
}
float distanceSquared = float(x * x);
float sigma = max(float(sampleRadius) * 0.5, 0.5);
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
float2 offset = float(x) * sampleStep;
blur += sampleVideo(context.uv + offset) * weight;
totalWeight += weight;
}
if (sampleRadius == 0)
@@ -29,7 +26,20 @@ float4 shadeVideo(ShaderContext context)
}
blur /= max(totalWeight, 0.0001);
float mixValue = saturate(strength);
return lerp(center, blur, mixValue);
return blur;
}
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.",
"category": "Keying",
"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": [
{
"id": "screenColor",
"label": "Screen Color",
"type": "color",
"default": [0.15, 0.85, 0.2, 1.0],
"min": [0.0, 0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0, 1.0],
"step": [0.01, 0.01, 0.01, 0.01]
"default": [
0.15,
0.85,
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",
@@ -21,7 +71,8 @@
"default": 0.24,
"min": 0.01,
"max": 0.8,
"step": 0.005
"step": 0.005,
"description": "Higher values keep more foreground; lower values remove more screen."
},
{
"id": "softness",
@@ -30,142 +81,238 @@
"default": 0.16,
"min": 0.001,
"max": 0.5,
"step": 0.005
"step": 0.005,
"description": "Feathers the transition between foreground and keyed screen."
},
{
"id": "screenBalance",
"label": "Screen Balance",
"type": "float",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.005
"min": 0,
"max": 1,
"step": 0.005,
"description": "Balances chroma-distance keying against color-direction keying."
},
{
"id": "screenPreBlur",
"label": "Screen PreBlur",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 8.0,
"step": 0.1
"default": 1,
"min": 0,
"max": 8,
"step": 0.1,
"description": "Blurs source color before matte generation to reduce noisy edges."
},
{
"id": "erodeDilate",
"label": "Erode/Dilate",
"type": "float",
"default": 0.0,
"default": 0,
"min": -0.3,
"max": 0.3,
"step": 0.005
"step": 0.005,
"description": "Negative erodes the matte; positive expands it."
},
{
"id": "matteBlur",
"label": "Matte Blur",
"type": "float",
"default": 1.25,
"min": 0.0,
"max": 6.0,
"step": 0.1
"min": 0,
"max": 6,
"step": 0.1,
"description": "Softens the generated matte after keying."
},
{
"id": "matteGamma",
"label": "Matte Gamma",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.25,
"max": 4.0,
"step": 0.01
"max": 4,
"step": 0.01,
"description": "Shapes midtone opacity in the matte."
},
{
"id": "matteContrast",
"label": "Matte Contrast",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.25,
"max": 4.0,
"step": 0.01
"max": 4,
"step": 0.01,
"description": "Increases or reduces matte separation around 50 percent alpha."
},
{
"id": "blackCleanup",
"label": "Black Cleanup",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.005
"default": 0,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Pushes semi-transparent dark matte areas toward transparent."
},
{
"id": "whiteCleanup",
"label": "White Cleanup",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.005
"default": 0,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Pushes semi-transparent light matte areas toward opaque."
},
{
"id": "despill",
"label": "Despill",
"type": "float",
"default": 0.45,
"min": 0.0,
"min": 0,
"max": 1.5,
"step": 0.01
"step": 0.01,
"description": "Removes screen-colored contamination from foreground edges."
},
{
"id": "despillBias",
"label": "Despill Bias",
"type": "float",
"default": 0.0,
"default": 0,
"min": -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",
"label": "Spill Tint",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0],
"min": [0.0, 0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0, 1.0],
"step": [0.01, 0.01, 0.01, 0.01]
"default": [
1,
1,
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",
"label": "Edge Recover",
"type": "float",
"default": 0.18,
"min": 0.0,
"max": 1.0,
"step": 0.005
"min": 0,
"max": 1,
"step": 0.005,
"description": "Adds color recovery along semi-transparent matte edges."
},
{
"id": "edgeColor",
"label": "Edge Color",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0],
"min": [0.0, 0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0, 1.0],
"step": [0.01, 0.01, 0.01, 0.01]
"default": [
1,
1,
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",
"label": "Clip Black",
"type": "float",
"default": 0.0,
"min": 0.0,
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.005
"step": 0.005,
"description": "Matte values below this become transparent."
},
{
"id": "clipWhite",
"label": "Clip White",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.5,
"max": 1.0,
"step": 0.005
"max": 1,
"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",
@@ -173,12 +320,28 @@
"type": "enum",
"default": "composite",
"options": [
{ "value": "composite", "label": "Composite" },
{ "value": "matte", "label": "Matte" },
{ "value": "spill", "label": "Spill" },
{ "value": "despill", "label": "Despill" },
{ "value": "status", "label": "Status" }
]
{
"value": "composite",
"label": "Composite"
},
{
"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);
}
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));
float blur = max(matteBlur, 0.0);
float aaRadius = max(blur, 0.65);
float centerAlpha = rawAlphaAt(uv, context);
float centerAlpha = matteAlphaAt(uv);
float alpha = centerAlpha * 0.30;
if (aaRadius > 0.0001)
@@ -64,51 +69,51 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
float2 halfRadius = radius * 0.5;
float alphaMin = 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv + radius, context);
sampleAlpha = matteAlphaAt(uv + radius);
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = rawAlphaAt(uv - radius, context);
sampleAlpha = matteAlphaAt(uv - radius);
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, 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;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
@@ -118,7 +123,7 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
}
else
{
alpha = rawAlphaAt(uv, context);
alpha = centerAlpha;
}
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
@@ -147,17 +152,43 @@ float3 despillColor(float3 color, float alpha)
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;
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);
float3 despilled = despillColor(color, alpha);
float cropMask = cropMaskAt(context.uv, context);
alpha *= cropMask;
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
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)
return float4(alpha, alpha, alpha, 1.0);
@@ -167,10 +198,15 @@ float4 shadeVideo(ShaderContext context)
return float4(despilled, 1.0);
if (viewMode == 4)
{
float rawAlpha = rawAlphaAt(context.uv, context);
float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
return float4(rawAlpha, alpha, spill, 1.0);
}
float3 premultiplied = saturate(despilled) * alpha;
return float4(premultiplied, alpha);
}
float4 shadeVideo(ShaderContext context)
{
return applyKey(context);
}

View File

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

View File

@@ -9,34 +9,59 @@
"id": "lift",
"label": "Lift",
"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",
"label": "Gamma",
"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",
"label": "Gain",
"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",
"label": "Offset",
"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",
"label": "Strength",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the grade with the original image."
}
]
}

View File

@@ -15,43 +15,48 @@
"id": "lutStrength",
"label": "LUT Strength",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends between the original image and the LUT result."
},
{
"id": "preExposure",
"label": "Pre Exposure",
"type": "float",
"default": 0.0,
"min": -4.0,
"max": 4.0,
"step": 0.01
"default": 0,
"min": -4,
"max": 4,
"step": 0.01,
"description": "Exposure offset applied before the LUT lookup."
},
{
"id": "postContrast",
"label": "Post Contrast",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 2,
"step": 0.01,
"description": "Contrast adjustment applied after the LUT lookup."
},
{
"id": "ditherAmount",
"label": "Output Dither",
"type": "float",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Adds subtle output dither to reduce visible banding."
},
{
"id": "clampInput",
"label": "Clamp Input",
"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",
"label": "Pixel Count",
"type": "vec2",
"default": [96.0, 54.0],
"min": [2.0, 2.0],
"max": [1920.0, 1080.0],
"step": [1.0, 1.0]
"default": [
96,
54
],
"min": [
2,
2
],
"max": [
1920,
1080
],
"step": [
1,
1
],
"description": "Number of pixel blocks across X and Y."
},
{
"id": "gridAmount",
"label": "Grid",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Visibility of the block grid lines."
},
{
"id": "gridColor",
"label": "Grid 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",
"entryPoint": "shadeVideo",
"parameters": [
{ "id": "showActionSafe", "label": "Action Safe", "type": "bool", "default": true },
{ "id": "showTitleSafe", "label": "Title Safe", "type": "bool", "default": true },
{ "id": "showCenter", "label": "Center Marks", "type": "bool", "default": true },
{
"id": "showActionSafe",
"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",
"label": "Line 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",
"label": "Line Opacity",
"type": "float",
"default": 0.65,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Overall visibility of the guide lines."
},
{
"id": "lineThicknessPixels",
"label": "Line Thickness",
"type": "float",
"default": 2.0,
"default": 2,
"min": 0.5,
"max": 12.0,
"step": 0.1
"max": 12,
"step": 0.1,
"description": "Guide line width in output pixels."
},
{
"id": "aspectMode",
@@ -38,20 +64,34 @@
"type": "enum",
"default": "none",
"options": [
{ "value": "none", "label": "None" },
{ "value": "239", "label": "2.39:1" },
{ "value": "185", "label": "1.85:1" },
{ "value": "square", "label": "1:1" }
]
{
"value": "none",
"label": "None"
},
{
"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",
"label": "Matte Opacity",
"type": "float",
"default": 0.35,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Opacity of the aspect-ratio matte outside the active image."
}
]
}

View File

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

View File

@@ -9,7 +9,13 @@
"id": "fillColor",
"label": "Fill",
"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",
"type": "float",
"default": 0.55,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Overall visibility of the temporal echoes."
},
{
"id": "decay",
"label": "Decay",
"type": "float",
"default": 0.72,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "How quickly older temporal echoes fade away."
},
{
"id": "frameStride",
"label": "Frame Stride",
"type": "float",
"default": 2.0,
"min": 1.0,
"max": 6.0,
"step": 1.0
"default": 2,
"min": 1,
"max": 6,
"step": 1,
"description": "Number of frames skipped between each echo sample."
},
{
"id": "echoTint",
"label": "Echo Tint",
"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",
"type": "float",
"default": 0.72,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Contribution of the current frame in the temporal blend."
},
{
"id": "trailMix",
"label": "Trail Mix",
"type": "float",
"default": 0.28,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Contribution of older frames in the temporal blend."
}
]
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More