Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bf2ae45c | |||
| 1ea44ba3ae | |||
| 0af9a72937 | |||
| d650cac857 | |||
| a0cc86f189 | |||
| f322abf79a | |||
| eede6938cb | |||
| ad24a20fdb | |||
| 5ae43513a7 | |||
| cc23e73d51 | |||
| f85abef237 | |||
| 596d370f43 | |||
| 87cb55b80b | |||
| f458eb0130 | |||
| 7d8f9a39d1 | |||
| 5b6e30ad13 | |||
| 07a5c91427 | |||
| 53b980913b | |||
| 4e2ac4a091 | |||
| 3eb5bb5de3 | |||
| ebbc11bb34 | |||
| 6d5a606107 |
@@ -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
|
||||
|
||||
|
||||
161
CMakeLists.txt
161
CMakeLists.txt
@@ -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/VideoIOFormat.cpp"
|
||||
"${APP_DIR}/decklink/VideoIOFormat.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/OpenGLDeckLinkBridge.cpp"
|
||||
"${APP_DIR}/gl/OpenGLDeckLinkBridge.h"
|
||||
"${APP_DIR}/gl/OpenGLRenderPass.cpp"
|
||||
"${APP_DIR}/gl/OpenGLRenderPass.h"
|
||||
"${APP_DIR}/gl/OpenGLRenderer.cpp"
|
||||
"${APP_DIR}/gl/OpenGLRenderer.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"
|
||||
@@ -104,6 +110,11 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/stdafx.cpp"
|
||||
"${APP_DIR}/stdafx.h"
|
||||
"${APP_DIR}/targetver.h"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||
)
|
||||
|
||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
||||
@@ -111,11 +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
|
||||
@@ -196,6 +211,7 @@ add_executable(Std140BufferTests
|
||||
target_include_directories(Std140BufferTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/gl/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -222,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"
|
||||
@@ -244,13 +283,15 @@ endif()
|
||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
||||
|
||||
add_executable(VideoIOFormatTests
|
||||
"${APP_DIR}/decklink/VideoIOFormat.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)
|
||||
@@ -259,6 +300,40 @@ endif()
|
||||
|
||||
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
|
||||
|
||||
add_executable(VideoPlayoutSchedulerTests
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||
|
||||
add_executable(VideoIODeviceFakeTests
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoIODeviceFakeTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
||||
|
||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||
RUNTIME DESTINATION "."
|
||||
)
|
||||
|
||||
56
README.md
56
README.md
@@ -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
|
||||
- unwrap a fish eyelens and mirror it and map it to equirectangulr for environmnet map purposes
|
||||
- Mipmapping for shader-declared textures
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;%(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;%(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;%(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;%(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,44 +175,56 @@
|
||||
</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\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\VideoIOFormat.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\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\VideoIOFormat.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" />
|
||||
<ClInclude Include="videoio\VideoPlayoutScheduler.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||
|
||||
@@ -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,45 +27,60 @@
|
||||
<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\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\VideoIOFormat.cpp">
|
||||
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="runtime\RuntimeClock.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="videoio\VideoIOFormat.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="gl\GLExtensions.h">
|
||||
<ClInclude Include="gl\renderer\GLExtensions.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
|
||||
@@ -74,22 +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\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">
|
||||
@@ -104,15 +131,24 @@
|
||||
<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\VideoIOFormat.h">
|
||||
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="runtime\RuntimeClock.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoIOFormat.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoIOTypes.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoPlayoutScheduler.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="LoopThroughWithOpenGLCompositing.ico">
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkFrameTransfer.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
class OpenGLComposite;
|
||||
|
||||
class DeckLinkSession
|
||||
{
|
||||
public:
|
||||
DeckLinkSession() = default;
|
||||
~DeckLinkSession();
|
||||
|
||||
void ReleaseResources();
|
||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error);
|
||||
bool ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error);
|
||||
bool ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
|
||||
bool Start();
|
||||
bool Stop();
|
||||
|
||||
bool HasInputDevice() const { return input != nullptr; }
|
||||
bool HasInputSource() const { return !hasNoInputSource; }
|
||||
void SetInputSourceMissing(bool missing) { hasNoInputSource = missing; }
|
||||
bool InputOutputDimensionsDiffer() const { return inputFrameSize != outputFrameSize; }
|
||||
const FrameSize& InputFrameSize() const { return inputFrameSize; }
|
||||
const FrameSize& OutputFrameSize() const { return outputFrameSize; }
|
||||
unsigned InputFrameWidth() const { return inputFrameSize.width; }
|
||||
unsigned InputFrameHeight() const { return inputFrameSize.height; }
|
||||
unsigned OutputFrameWidth() const { return outputFrameSize.width; }
|
||||
unsigned OutputFrameHeight() const { return outputFrameSize.height; }
|
||||
VideoIOPixelFormat InputPixelFormat() const { return inputPixelFormat; }
|
||||
VideoIOPixelFormat OutputPixelFormat() const { return outputPixelFormat; }
|
||||
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(inputPixelFormat); }
|
||||
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(outputPixelFormat); }
|
||||
unsigned InputFrameRowBytes() const { return inputFrameRowBytes; }
|
||||
unsigned OutputFrameRowBytes() const { return outputFrameRowBytes; }
|
||||
unsigned CaptureTextureWidth() const { return captureTextureWidth; }
|
||||
unsigned OutputPackTextureWidth() const { return outputPackTextureWidth; }
|
||||
const std::string& FormatStatusMessage() const { return formatStatusMessage; }
|
||||
const std::string& InputDisplayModeName() const { return inputDisplayModeName; }
|
||||
const std::string& OutputModelName() const { return outputModelName; }
|
||||
bool SupportsInternalKeying() const { return supportsInternalKeying; }
|
||||
bool SupportsExternalKeying() const { return supportsExternalKeying; }
|
||||
bool KeyerInterfaceAvailable() const { return keyerInterfaceAvailable; }
|
||||
bool ExternalKeyingActive() const { return externalKeyingActive; }
|
||||
const std::string& StatusMessage() const { return statusMessage; }
|
||||
void SetStatusMessage(const std::string& message) { statusMessage = message; }
|
||||
double FrameBudgetMilliseconds() const;
|
||||
IDeckLinkMutableVideoFrame* RotateOutputFrame();
|
||||
void AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
||||
bool ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||
|
||||
private:
|
||||
CComPtr<CaptureDelegate> captureDelegate;
|
||||
CComPtr<PlayoutDelegate> playoutDelegate;
|
||||
CComPtr<IDeckLinkInput> input;
|
||||
CComPtr<IDeckLinkOutput> output;
|
||||
CComPtr<IDeckLinkKeyer> keyer;
|
||||
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||
BMDTimeValue frameDuration = 0;
|
||||
BMDTimeScale frameTimescale = 0;
|
||||
unsigned totalPlayoutFrames = 0;
|
||||
FrameSize inputFrameSize;
|
||||
FrameSize outputFrameSize;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
unsigned inputFrameRowBytes = 0;
|
||||
unsigned outputFrameRowBytes = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
unsigned outputPackTextureWidth = 0;
|
||||
std::string inputDisplayModeName = "1080p59.94";
|
||||
std::string outputDisplayModeName = "1080p59.94";
|
||||
bool hasNoInputSource = true;
|
||||
std::string outputModelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingActive = false;
|
||||
std::string statusMessage;
|
||||
std::string formatStatusMessage;
|
||||
};
|
||||
@@ -3,9 +3,10 @@
|
||||
#include "OpenGLComposite.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "GlRenderConstants.h"
|
||||
#include "OpenGLDeckLinkBridge.h"
|
||||
#include "OpenGLRenderPass.h"
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
#include "PngScreenshotWriter.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
@@ -22,23 +23,27 @@
|
||||
|
||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
||||
mDeckLink(std::make_unique<DeckLinkSession>()),
|
||||
mVideoIO(std::make_unique<DeckLinkSession>()),
|
||||
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||
mUseCommittedLayerStates(false),
|
||||
mScreenshotRequested(false)
|
||||
{
|
||||
InitializeCriticalSection(&pMutex);
|
||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||
mDeckLinkBridge = std::make_unique<OpenGLDeckLinkBridge>(
|
||||
*mDeckLink,
|
||||
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);
|
||||
@@ -51,13 +56,18 @@ OpenGLComposite::~OpenGLComposite()
|
||||
mRuntimeServices->Stop();
|
||||
if (mShaderBuildQueue)
|
||||
mShaderBuildQueue->Stop();
|
||||
mDeckLink->ReleaseResources();
|
||||
mVideoIO->ReleaseResources();
|
||||
mRenderer->DestroyResources();
|
||||
|
||||
DeleteCriticalSection(&pMutex);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitDeckLink()
|
||||
{
|
||||
return InitVideoIO();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitVideoIO()
|
||||
{
|
||||
VideoFormatSelection videoModes;
|
||||
std::string initFailureReason;
|
||||
@@ -87,7 +97,7 @@ bool OpenGLComposite::InitDeckLink()
|
||||
}
|
||||
}
|
||||
|
||||
if (!mDeckLink->DiscoverDevicesAndModes(videoModes, initFailureReason))
|
||||
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
|
||||
{
|
||||
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
|
||||
? "This application requires the DeckLink drivers installed."
|
||||
@@ -95,7 +105,8 @@ bool OpenGLComposite::InitDeckLink()
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
if (!mDeckLink->SelectPreferredFormats(videoModes, initFailureReason))
|
||||
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
|
||||
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
|
||||
goto error;
|
||||
|
||||
if (! CheckOpenGLExtensions())
|
||||
@@ -110,38 +121,38 @@ bool OpenGLComposite::InitDeckLink()
|
||||
goto error;
|
||||
}
|
||||
|
||||
PublishDeckLinkOutputStatus(mDeckLink->OutputModelName().empty()
|
||||
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
|
||||
? "DeckLink output device selected."
|
||||
: ("Selected output device: " + mDeckLink->OutputModelName()));
|
||||
: ("Selected output device: " + mVideoIO->OutputModelName()));
|
||||
|
||||
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||
if (mDeckLink->OutputFrameWidth() < 1920)
|
||||
resizeWindow(mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight());
|
||||
if (mVideoIO->OutputFrameWidth() < 1920)
|
||||
resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||
else
|
||||
resizeWindow(mDeckLink->OutputFrameWidth() / 2, mDeckLink->OutputFrameHeight() / 2);
|
||||
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
|
||||
|
||||
if (!mDeckLink->ConfigureInput(this, hGLDC, hGLRC, videoModes.input, initFailureReason))
|
||||
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
if (!mDeckLink->HasInputDevice() && mRuntimeHost)
|
||||
if (!mVideoIO->HasInputDevice() && mRuntimeHost)
|
||||
{
|
||||
mRuntimeHost->SetSignalStatus(false, mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), mDeckLink->InputDisplayModeName());
|
||||
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
||||
}
|
||||
|
||||
if (!mDeckLink->ConfigureOutput(this, hGLDC, hGLRC, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
|
||||
PublishDeckLinkOutputStatus(mDeckLink->StatusMessage());
|
||||
PublishVideoIOStatus(mVideoIO->StatusMessage());
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
if (!initFailureReason.empty())
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
||||
mDeckLink->ReleaseResources();
|
||||
mVideoIO->ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -153,7 +164,7 @@ void OpenGLComposite::paintGL()
|
||||
return;
|
||||
}
|
||||
|
||||
mRenderer->PresentToWindow(hGLDC, mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight());
|
||||
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||
ValidateRect(hGLWnd, NULL);
|
||||
LeaveCriticalSection(&pMutex);
|
||||
}
|
||||
@@ -174,22 +185,23 @@ void OpenGLComposite::resizeWindow(int width, int height)
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLComposite::PublishDeckLinkOutputStatus(const std::string& statusMessage)
|
||||
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
||||
{
|
||||
if (!mRuntimeHost)
|
||||
return;
|
||||
|
||||
if (!statusMessage.empty())
|
||||
mDeckLink->SetStatusMessage(statusMessage);
|
||||
mVideoIO->SetStatusMessage(statusMessage);
|
||||
|
||||
mRuntimeHost->SetDeckLinkOutputStatus(
|
||||
mDeckLink->OutputModelName(),
|
||||
mDeckLink->SupportsInternalKeying(),
|
||||
mDeckLink->SupportsExternalKeying(),
|
||||
mDeckLink->KeyerInterfaceAvailable(),
|
||||
mRuntimeHost->SetVideoIOStatus(
|
||||
"decklink",
|
||||
mVideoIO->OutputModelName(),
|
||||
mVideoIO->SupportsInternalKeying(),
|
||||
mVideoIO->SupportsExternalKeying(),
|
||||
mVideoIO->KeyerInterfaceAvailable(),
|
||||
mRuntimeHost->ExternalKeyingEnabled(),
|
||||
mDeckLink->ExternalKeyingActive(),
|
||||
mDeckLink->StatusMessage());
|
||||
mVideoIO->ExternalKeyingActive(),
|
||||
mVideoIO->StatusMessage());
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitOpenGLState()
|
||||
@@ -223,7 +235,7 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mShaderPrograms->CompileLayerPrograms(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
@@ -234,12 +246,12 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
|
||||
std::string rendererError;
|
||||
if (!mRenderer->InitializeResources(
|
||||
mDeckLink->InputFrameWidth(),
|
||||
mDeckLink->InputFrameHeight(),
|
||||
mDeckLink->CaptureTextureWidth(),
|
||||
mDeckLink->OutputFrameWidth(),
|
||||
mDeckLink->OutputFrameHeight(),
|
||||
mDeckLink->OutputPackTextureWidth(),
|
||||
mVideoIO->InputFrameWidth(),
|
||||
mVideoIO->InputFrameHeight(),
|
||||
mVideoIO->CaptureTextureWidth(),
|
||||
mVideoIO->OutputFrameWidth(),
|
||||
mVideoIO->OutputFrameHeight(),
|
||||
mVideoIO->OutputPackTextureWidth(),
|
||||
rendererError))
|
||||
{
|
||||
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
|
||||
@@ -251,20 +263,9 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
return true;
|
||||
}
|
||||
|
||||
// DeckLink delegates still target OpenGLComposite; the bridge owns the per-frame work.
|
||||
void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||
{
|
||||
mDeckLinkBridge->VideoFrameArrived(inputFrame, hasNoInputSource);
|
||||
}
|
||||
|
||||
void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||
{
|
||||
mDeckLinkBridge->PlayoutFrameCompleted(completedFrame, completionResult);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Start()
|
||||
{
|
||||
return mDeckLink->Start();
|
||||
return mVideoIO->Start();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Stop()
|
||||
@@ -272,10 +273,10 @@ bool OpenGLComposite::Stop()
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->Stop();
|
||||
|
||||
const bool wasExternalKeyingActive = mDeckLink->ExternalKeyingActive();
|
||||
mDeckLink->Stop();
|
||||
const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
|
||||
mVideoIO->Stop();
|
||||
if (wasExternalKeyingActive)
|
||||
PublishDeckLinkOutputStatus("External keying has been disabled.");
|
||||
PublishVideoIOStatus("External keying has been disabled.");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -303,7 +304,7 @@ void OpenGLComposite::renderEffect()
|
||||
{
|
||||
ProcessRuntimePollResults();
|
||||
|
||||
const bool hasInputSource = mDeckLink->HasInputSource();
|
||||
const bool hasInputSource = mVideoIO->HasInputSource();
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
if (mUseCommittedLayerStates)
|
||||
{
|
||||
@@ -313,7 +314,7 @@ void OpenGLComposite::renderEffect()
|
||||
}
|
||||
else if (mRuntimeHost)
|
||||
{
|
||||
if (mRuntimeHost->TryGetLayerRenderStates(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), layerStates))
|
||||
if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates))
|
||||
{
|
||||
mCachedLayerRenderStates = layerStates;
|
||||
}
|
||||
@@ -327,10 +328,10 @@ void OpenGLComposite::renderEffect()
|
||||
mRenderPass->Render(
|
||||
hasInputSource,
|
||||
layerStates,
|
||||
mDeckLink->InputFrameWidth(),
|
||||
mDeckLink->InputFrameHeight(),
|
||||
mDeckLink->CaptureTextureWidth(),
|
||||
mDeckLink->InputPixelFormat(),
|
||||
mVideoIO->InputFrameWidth(),
|
||||
mVideoIO->InputFrameHeight(),
|
||||
mVideoIO->CaptureTextureWidth(),
|
||||
mVideoIO->InputPixelFormat(),
|
||||
historyCap,
|
||||
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
||||
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
||||
@@ -345,8 +346,8 @@ void OpenGLComposite::ProcessScreenshotRequest()
|
||||
if (!mScreenshotRequested.exchange(false))
|
||||
return;
|
||||
|
||||
const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0;
|
||||
const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0;
|
||||
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
|
||||
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
@@ -426,7 +427,7 @@ bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
return true;
|
||||
|
||||
char compilerErrorMessage[1024] = {};
|
||||
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||
mUseCommittedLayerStates = true;
|
||||
@@ -449,13 +450,13 @@ bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
|
||||
void OpenGLComposite::RequestShaderBuild()
|
||||
{
|
||||
if (!mShaderBuildQueue || !mDeckLink)
|
||||
if (!mShaderBuildQueue || !mVideoIO)
|
||||
return;
|
||||
|
||||
mUseCommittedLayerStates = true;
|
||||
if (mRuntimeHost)
|
||||
mRuntimeHost->ClearReloadRequest();
|
||||
mShaderBuildQueue->RequestBuild(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight());
|
||||
mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
|
||||
}
|
||||
|
||||
void OpenGLComposite::broadcastRuntimeState()
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <objbase.h>
|
||||
#include <atlbase.h>
|
||||
#include <comutil.h>
|
||||
#include "DeckLinkAPI_h.h"
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
@@ -25,9 +24,10 @@
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
class DeckLinkSession;
|
||||
class OpenGLDeckLinkBridge;
|
||||
class VideoIODevice;
|
||||
class OpenGLVideoIOBridge;
|
||||
class OpenGLRenderPass;
|
||||
class OpenGLRenderPipeline;
|
||||
class OpenGLShaderPrograms;
|
||||
class RuntimeServices;
|
||||
class ShaderBuildQueue;
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
~OpenGLComposite();
|
||||
|
||||
bool InitDeckLink();
|
||||
bool InitVideoIO();
|
||||
bool Start();
|
||||
bool Stop();
|
||||
bool ReloadShader();
|
||||
@@ -65,13 +66,10 @@ public:
|
||||
void resizeGL(WORD width, WORD height);
|
||||
void paintGL();
|
||||
|
||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
||||
|
||||
private:
|
||||
void resizeWindow(int width, int height);
|
||||
bool CheckOpenGLExtensions();
|
||||
void PublishDeckLinkOutputStatus(const std::string& statusMessage);
|
||||
void PublishVideoIOStatus(const std::string& statusMessage);
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
|
||||
HWND hGLWnd;
|
||||
@@ -79,11 +77,12 @@ private:
|
||||
HGLRC hGLRC;
|
||||
CRITICAL_SECTION pMutex;
|
||||
|
||||
std::unique_ptr<DeckLinkSession> mDeckLink;
|
||||
std::unique_ptr<VideoIODevice> mVideoIO;
|
||||
std::unique_ptr<OpenGLRenderer> mRenderer;
|
||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||
std::unique_ptr<OpenGLDeckLinkBridge> mDeckLinkBridge;
|
||||
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;
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#include "OpenGLDeckLinkBridge.h"
|
||||
|
||||
#include "DeckLinkSession.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <gl/gl.h>
|
||||
|
||||
OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
|
||||
DeckLinkSession& deckLink,
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint) :
|
||||
mDeckLink(deckLink),
|
||||
mRenderer(renderer),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mMutex(mutex),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc),
|
||||
mRenderEffect(renderEffect),
|
||||
mOutputReady(outputReady),
|
||||
mPaint(paint)
|
||||
{
|
||||
}
|
||||
|
||||
void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult completionResult)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
||||
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
||||
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
else
|
||||
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
||||
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
||||
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
}
|
||||
mLastPlayoutCompletionTime = now;
|
||||
|
||||
if (completionResult == bmdOutputFrameDisplayedLate)
|
||||
++mLateFrameCount;
|
||||
else if (completionResult == bmdOutputFrameDropped)
|
||||
++mDroppedFrameCount;
|
||||
else if (completionResult == bmdOutputFrameFlushed)
|
||||
++mFlushedFrameCount;
|
||||
|
||||
mRuntimeHost.TrySetFramePacingStats(
|
||||
mCompletionIntervalMilliseconds,
|
||||
mSmoothedCompletionIntervalMilliseconds,
|
||||
mMaxCompletionIntervalMilliseconds,
|
||||
mLateFrameCount,
|
||||
mDroppedFrameCount,
|
||||
mFlushedFrameCount);
|
||||
}
|
||||
|
||||
void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||
{
|
||||
mDeckLink.SetInputSourceMissing(hasNoInputSource);
|
||||
mRuntimeHost.TrySetSignalStatus(!hasNoInputSource, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), mDeckLink.InputDisplayModeName());
|
||||
|
||||
if (!mDeckLink.HasInputSource())
|
||||
return; // don't transfer texture when there's no input
|
||||
|
||||
long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight();
|
||||
IDeckLinkVideoBuffer* inputFrameBuffer = NULL;
|
||||
void* videoPixels;
|
||||
|
||||
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
|
||||
return;
|
||||
|
||||
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||
{
|
||||
inputFrameBuffer->Release();
|
||||
return;
|
||||
}
|
||||
inputFrameBuffer->GetBytes(&videoPixels);
|
||||
|
||||
EnterCriticalSection(&mMutex);
|
||||
|
||||
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
|
||||
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW);
|
||||
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
||||
|
||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
|
||||
if (mDeckLink.InputPixelFormat() == VideoIOPixelFormat::V210)
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
else
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
|
||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||
inputFrameBuffer->Release();
|
||||
}
|
||||
|
||||
void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||
{
|
||||
(void)completedFrame;
|
||||
|
||||
RecordFramePacing(completionResult);
|
||||
|
||||
EnterCriticalSection(&mMutex);
|
||||
|
||||
// Get the first frame from the queue
|
||||
IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink.RotateOutputFrame();
|
||||
|
||||
// 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, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), 0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
if (mOutputReady)
|
||||
mOutputReady();
|
||||
if (mDeckLink.OutputIsTenBit())
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glViewport(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight());
|
||||
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>(mDeckLink.OutputFrameWidth()), static_cast<float>(mDeckLink.OutputFrameHeight()));
|
||||
if (activeWordsLocation >= 0)
|
||||
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(mDeckLink.OutputFrameWidth())));
|
||||
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 = mDeckLink.FrameBudgetMilliseconds();
|
||||
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
||||
mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
||||
mRuntimeHost.TryAdvanceFrame();
|
||||
|
||||
IDeckLinkVideoBuffer* outputVideoFrameBuffer;
|
||||
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
||||
{
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
LeaveCriticalSection(&mMutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
||||
{
|
||||
outputVideoFrameBuffer->Release();
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
LeaveCriticalSection(&mMutex);
|
||||
return;
|
||||
}
|
||||
|
||||
void* pFrame;
|
||||
outputVideoFrameBuffer->GetBytes(&pFrame);
|
||||
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
if (mDeckLink.OutputIsTenBit())
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glReadPixels(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, pFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
glReadPixels(0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame);
|
||||
}
|
||||
mPaint();
|
||||
|
||||
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
||||
outputVideoFrameBuffer->Release();
|
||||
|
||||
mDeckLink.AccountForCompletionResult(completionResult);
|
||||
|
||||
// Schedule the next frame for playout
|
||||
mDeckLink.ScheduleOutputFrame(outputVideoFrame);
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
class DeckLinkSession;
|
||||
class OpenGLRenderer;
|
||||
class RuntimeHost;
|
||||
|
||||
class OpenGLDeckLinkBridge
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using OutputReadyCallback = std::function<void()>;
|
||||
using PaintCallback = std::function<void()>;
|
||||
|
||||
OpenGLDeckLinkBridge(
|
||||
DeckLinkSession& deckLink,
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint);
|
||||
|
||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
||||
|
||||
private:
|
||||
void RecordFramePacing(BMDOutputFrameCompletionResult completionResult);
|
||||
|
||||
DeckLinkSession& mDeckLink;
|
||||
OpenGLRenderer& mRenderer;
|
||||
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;
|
||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||
uint64_t mLateFrameCount = 0;
|
||||
uint64_t mDroppedFrameCount = 0;
|
||||
uint64_t mFlushedFrameCount = 0;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <gl/gl.h>
|
||||
|
||||
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
OpenGLRenderPipeline& renderPipeline,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc) :
|
||||
mVideoIO(videoIO),
|
||||
mRenderer(renderer),
|
||||
mRenderPipeline(renderPipeline),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mMutex(mutex),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc)
|
||||
{
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
||||
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
||||
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
else
|
||||
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
||||
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
||||
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
}
|
||||
mLastPlayoutCompletionTime = now;
|
||||
|
||||
if (completionResult == VideoIOCompletionResult::DisplayedLate)
|
||||
++mLateFrameCount;
|
||||
else if (completionResult == VideoIOCompletionResult::Dropped)
|
||||
++mDroppedFrameCount;
|
||||
else if (completionResult == VideoIOCompletionResult::Flushed)
|
||||
++mFlushedFrameCount;
|
||||
|
||||
mRuntimeHost.TrySetFramePacingStats(
|
||||
mCompletionIntervalMilliseconds,
|
||||
mSmoothedCompletionIntervalMilliseconds,
|
||||
mMaxCompletionIntervalMilliseconds,
|
||||
mLateFrameCount,
|
||||
mDroppedFrameCount,
|
||||
mFlushedFrameCount);
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
||||
{
|
||||
const VideoIOState& state = mVideoIO.State();
|
||||
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||
|
||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||
return; // don't transfer texture when there's no input
|
||||
|
||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||
|
||||
EnterCriticalSection(&mMutex);
|
||||
|
||||
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
|
||||
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW);
|
||||
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
||||
|
||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
|
||||
if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
else
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
||||
{
|
||||
RecordFramePacing(completion.result);
|
||||
|
||||
EnterCriticalSection(&mMutex);
|
||||
|
||||
VideoIOOutputFrame outputFrame;
|
||||
if (!mVideoIO.BeginOutputFrame(outputFrame))
|
||||
{
|
||||
LeaveCriticalSection(&mMutex);
|
||||
return;
|
||||
}
|
||||
const VideoIOState& state = mVideoIO.State();
|
||||
RenderPipelineFrameContext frameContext;
|
||||
frameContext.videoState = state;
|
||||
frameContext.completion = completion;
|
||||
|
||||
// make GL context current in this thread
|
||||
wglMakeCurrent(mHdc, mHglrc);
|
||||
|
||||
mRenderPipeline.RenderFrame(frameContext, outputFrame);
|
||||
|
||||
mVideoIO.EndOutputFrame(outputFrame);
|
||||
|
||||
mVideoIO.AccountForCompletionResult(completion.result);
|
||||
|
||||
// Schedule the next frame for playout
|
||||
mVideoIO.ScheduleOutputFrame(outputFrame);
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
class RuntimeHost;
|
||||
|
||||
class OpenGLVideoIOBridge
|
||||
{
|
||||
public:
|
||||
OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
OpenGLRenderPipeline& renderPipeline,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc);
|
||||
|
||||
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
||||
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
||||
|
||||
private:
|
||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||
|
||||
VideoIODevice& mVideoIO;
|
||||
OpenGLRenderer& mRenderer;
|
||||
OpenGLRenderPipeline& mRenderPipeline;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
CRITICAL_SECTION& mMutex;
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||
double mCompletionIntervalMilliseconds = 0.0;
|
||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||
uint64_t mLateFrameCount = 0;
|
||||
uint64_t mDroppedFrameCount = 0;
|
||||
uint64_t mFlushedFrameCount = 0;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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())
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -14,7 +14,7 @@ class RuntimeHost;
|
||||
struct PreparedLayerShader
|
||||
{
|
||||
RuntimeRenderState state;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<ShaderPassBuildSource> passes;
|
||||
};
|
||||
|
||||
struct PreparedShaderBuild
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) ||
|
||||
@@ -1213,8 +1205,16 @@ void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned
|
||||
|
||||
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable,
|
||||
externalKeyingRequested, externalKeyingActive, statusMessage);
|
||||
}
|
||||
|
||||
void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mDeckLinkOutputStatus.backendName = backendName;
|
||||
mDeckLinkOutputStatus.modelName = modelName;
|
||||
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
|
||||
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
|
||||
@@ -1292,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
|
||||
{
|
||||
@@ -1316,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;
|
||||
}
|
||||
}
|
||||
@@ -1663,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);
|
||||
@@ -1889,6 +1870,17 @@ JsonValue RuntimeHost::BuildStateValue() const
|
||||
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||
root.set("decklink", deckLink);
|
||||
|
||||
JsonValue videoIO = JsonValue::MakeObject();
|
||||
videoIO.set("backend", JsonValue(mDeckLinkOutputStatus.backendName));
|
||||
videoIO.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
|
||||
videoIO.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));
|
||||
videoIO.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying));
|
||||
videoIO.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable));
|
||||
videoIO.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested));
|
||||
videoIO.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive));
|
||||
videoIO.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||
root.set("videoIO", videoIO);
|
||||
|
||||
JsonValue performance = JsonValue::MakeObject();
|
||||
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
||||
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
||||
@@ -1968,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)));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost
|
||||
@@ -39,6 +40,8 @@ public:
|
||||
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
@@ -48,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;
|
||||
@@ -86,6 +89,7 @@ private:
|
||||
|
||||
struct DeckLinkOutputStatus
|
||||
{
|
||||
std::string backendName = "decklink";
|
||||
std::string modelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
@@ -112,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,22 +50,23 @@ uint16_t Component(uint32_t word, unsigned index)
|
||||
|
||||
const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
|
||||
{
|
||||
return format == VideoIOPixelFormat::V210 ? "10-bit YUV v210" : "8-bit YUV UYVY";
|
||||
switch (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:
|
||||
default:
|
||||
return "8-bit YUV UYVY";
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
|
||||
{
|
||||
return format == VideoIOPixelFormat::V210;
|
||||
}
|
||||
|
||||
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
|
||||
{
|
||||
return format == VideoIOPixelFormat::V210 ? bmdFormat10BitYUV : bmdFormat8BitYUV;
|
||||
}
|
||||
|
||||
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
|
||||
{
|
||||
return format == bmdFormat10BitYUV ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
|
||||
return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
|
||||
}
|
||||
|
||||
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
|
||||
@@ -73,6 +74,31 @@ VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
|
||||
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
|
||||
}
|
||||
|
||||
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case VideoIOPixelFormat::Uyvy8:
|
||||
return 2u;
|
||||
case VideoIOPixelFormat::Bgra8:
|
||||
return 4u;
|
||||
case VideoIOPixelFormat::Yuva10:
|
||||
return 4u;
|
||||
case VideoIOPixelFormat::V210:
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
|
||||
{
|
||||
if (format == VideoIOPixelFormat::V210)
|
||||
return MinimumV210RowBytes(frameWidth);
|
||||
if (format == VideoIOPixelFormat::Yuva10)
|
||||
return MinimumYuva10RowBytes(frameWidth);
|
||||
return frameWidth * VideoIOBytesPerPixel(format);
|
||||
}
|
||||
|
||||
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
|
||||
{
|
||||
return (rowBytes + 3u) / 4u;
|
||||
@@ -83,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;
|
||||
@@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
enum class VideoIOPixelFormat
|
||||
{
|
||||
Uyvy8,
|
||||
V210
|
||||
V210,
|
||||
Yuva10,
|
||||
Bgra8
|
||||
};
|
||||
|
||||
struct V210CodeValues
|
||||
@@ -27,11 +27,12 @@ struct V210SixPixelBlock
|
||||
|
||||
const char* VideoIOPixelFormatName(VideoIOPixelFormat format);
|
||||
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format);
|
||||
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
|
||||
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
|
||||
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
|
||||
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);
|
||||
137
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h
Normal file
137
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h
Normal file
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
enum class VideoIOBackend
|
||||
{
|
||||
DeckLink
|
||||
};
|
||||
|
||||
enum class VideoIOCompletionResult
|
||||
{
|
||||
Completed,
|
||||
DisplayedLate,
|
||||
Dropped,
|
||||
Flushed,
|
||||
Unknown
|
||||
};
|
||||
|
||||
struct VideoIOConfig
|
||||
{
|
||||
VideoFormatSelection videoModes;
|
||||
bool externalKeyingEnabled = false;
|
||||
bool preferTenBit = true;
|
||||
};
|
||||
|
||||
struct VideoIOState
|
||||
{
|
||||
FrameSize inputFrameSize;
|
||||
FrameSize outputFrameSize;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned inputFrameRowBytes = 0;
|
||||
unsigned outputFrameRowBytes = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
unsigned outputPackTextureWidth = 0;
|
||||
std::string inputDisplayModeName = "1080p59.94";
|
||||
std::string outputDisplayModeName = "1080p59.94";
|
||||
std::string outputModelName;
|
||||
std::string statusMessage;
|
||||
std::string formatStatusMessage;
|
||||
bool hasInputDevice = false;
|
||||
bool hasInputSource = false;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingActive = false;
|
||||
double frameBudgetMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct VideoIOFrame
|
||||
{
|
||||
void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
bool hasNoInputSource = false;
|
||||
};
|
||||
|
||||
struct VideoIOOutputFrame
|
||||
{
|
||||
void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
void* nativeFrame = nullptr;
|
||||
void* nativeBuffer = nullptr;
|
||||
};
|
||||
|
||||
struct VideoIOCompletion
|
||||
{
|
||||
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
||||
};
|
||||
|
||||
struct VideoIOScheduleTime
|
||||
{
|
||||
int64_t streamTime = 0;
|
||||
int64_t duration = 0;
|
||||
int64_t timeScale = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
class VideoIODevice
|
||||
{
|
||||
public:
|
||||
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
|
||||
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
||||
|
||||
virtual ~VideoIODevice() = default;
|
||||
virtual void ReleaseResources() = 0;
|
||||
virtual bool DiscoverDevicesAndModes(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;
|
||||
virtual bool Stop() = 0;
|
||||
virtual const VideoIOState& State() const = 0;
|
||||
virtual VideoIOState& MutableState() = 0;
|
||||
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
||||
virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0;
|
||||
|
||||
bool HasInputDevice() const { return State().hasInputDevice; }
|
||||
bool HasInputSource() const { return State().hasInputSource; }
|
||||
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
|
||||
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
|
||||
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
|
||||
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
|
||||
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
|
||||
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
|
||||
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
|
||||
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
|
||||
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
|
||||
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
|
||||
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
|
||||
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
|
||||
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
|
||||
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
|
||||
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
||||
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
||||
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
||||
const std::string& OutputModelName() const { return State().outputModelName; }
|
||||
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
|
||||
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
|
||||
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
|
||||
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
||||
const std::string& StatusMessage() const { return State().statusMessage; }
|
||||
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
||||
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "VideoPlayoutScheduler.h"
|
||||
|
||||
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
||||
{
|
||||
mFrameDuration = frameDuration;
|
||||
mTimeScale = timeScale;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::Reset()
|
||||
{
|
||||
mScheduledFrameIndex = 0;
|
||||
}
|
||||
|
||||
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||
{
|
||||
VideoIOScheduleTime time;
|
||||
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
|
||||
time.duration = mFrameDuration;
|
||||
time.timeScale = mTimeScale;
|
||||
time.frameIndex = mScheduledFrameIndex;
|
||||
++mScheduledFrameIndex;
|
||||
return time;
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
|
||||
{
|
||||
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
|
||||
mScheduledFrameIndex += 2;
|
||||
}
|
||||
|
||||
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
||||
{
|
||||
return mTimeScale != 0
|
||||
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
||||
: 0.0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class VideoPlayoutScheduler
|
||||
{
|
||||
public:
|
||||
void Configure(int64_t frameDuration, int64_t timeScale);
|
||||
void Reset();
|
||||
VideoIOScheduleTime NextScheduleTime();
|
||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
||||
double FrameBudgetMilliseconds() const;
|
||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||
int64_t TimeScale() const { return mTimeScale; }
|
||||
|
||||
private:
|
||||
int64_t mFrameDuration = 0;
|
||||
int64_t mTimeScale = 0;
|
||||
uint64_t mScheduledFrameIndex = 0;
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "DeckLinkFrameTransfer.h"
|
||||
|
||||
#include "OpenGLComposite.h"
|
||||
#include "DeckLinkSession.h"
|
||||
|
||||
////////////////////////////////////////////
|
||||
// DeckLink Capture Delegate Class
|
||||
////////////////////////////////////////////
|
||||
CaptureDelegate::CaptureDelegate(OpenGLComposite* pOwner) :
|
||||
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
|
||||
m_pOwner(pOwner),
|
||||
mRefCount(1)
|
||||
{
|
||||
@@ -39,7 +39,7 @@ HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputF
|
||||
}
|
||||
|
||||
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
|
||||
m_pOwner->VideoFrameArrived(inputFrame, hasNoInputSource);
|
||||
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvent
|
||||
////////////////////////////////////////////
|
||||
// DeckLink Playout Delegate Class
|
||||
////////////////////////////////////////////
|
||||
PlayoutDelegate::PlayoutDelegate(OpenGLComposite* pOwner) :
|
||||
PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
|
||||
m_pOwner(pOwner),
|
||||
mRefCount(1)
|
||||
{
|
||||
@@ -94,7 +94,7 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF
|
||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
||||
}
|
||||
|
||||
m_pOwner->PlayoutFrameCompleted(completedFrame, result);
|
||||
m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
|
||||
class OpenGLComposite;
|
||||
class DeckLinkSession;
|
||||
|
||||
////////////////////////////////////////////
|
||||
// Capture Delegate Class
|
||||
////////////////////////////////////////////
|
||||
class CaptureDelegate : public IDeckLinkInputCallback
|
||||
{
|
||||
OpenGLComposite* m_pOwner;
|
||||
DeckLinkSession* m_pOwner;
|
||||
LONG mRefCount;
|
||||
|
||||
public:
|
||||
CaptureDelegate(OpenGLComposite* pOwner);
|
||||
CaptureDelegate(DeckLinkSession* pOwner);
|
||||
|
||||
// IUnknown needs only a dummy implementation
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
||||
@@ -33,11 +33,11 @@ public:
|
||||
////////////////////////////////////////////
|
||||
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
|
||||
{
|
||||
OpenGLComposite* m_pOwner;
|
||||
DeckLinkSession* m_pOwner;
|
||||
LONG mRefCount;
|
||||
|
||||
public:
|
||||
PlayoutDelegate(OpenGLComposite* pOwner);
|
||||
PlayoutDelegate(DeckLinkSession* pOwner);
|
||||
|
||||
// IUnknown needs only a dummy implementation
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
@@ -82,7 +83,7 @@ void DeckLinkSession::ReleaseResources()
|
||||
if (keyer != nullptr)
|
||||
{
|
||||
keyer->Disable();
|
||||
externalKeyingActive = false;
|
||||
mState.externalKeyingActive = false;
|
||||
}
|
||||
keyer.Release();
|
||||
|
||||
@@ -97,8 +98,8 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
||||
CComPtr<IDeckLinkDisplayMode> inputMode;
|
||||
CComPtr<IDeckLinkDisplayMode> outputMode;
|
||||
|
||||
inputDisplayModeName = videoModes.input.displayName;
|
||||
outputDisplayModeName = videoModes.output.displayName;
|
||||
mState.inputDisplayModeName = videoModes.input.displayName;
|
||||
mState.outputDisplayModeName = videoModes.output.displayName;
|
||||
|
||||
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
||||
if (FAILED(result))
|
||||
@@ -150,9 +151,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
||||
output.Release();
|
||||
else
|
||||
{
|
||||
outputModelName = modelName;
|
||||
supportsInternalKeying = deviceSupportsInternalKeying;
|
||||
supportsExternalKeying = deviceSupportsExternalKeying;
|
||||
mState.outputModelName = modelName;
|
||||
mState.supportsInternalKeying = deviceSupportsInternalKeying;
|
||||
mState.supportsExternalKeying = deviceSupportsExternalKeying;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,23 +201,29 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
||||
return false;
|
||||
}
|
||||
|
||||
outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
||||
inputFrameSize = inputMode
|
||||
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
||||
mState.inputFrameSize = inputMode
|
||||
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
|
||||
: outputFrameSize;
|
||||
: mState.outputFrameSize;
|
||||
if (!input)
|
||||
inputDisplayModeName = "No input - black frame";
|
||||
mState.inputDisplayModeName = "No input - black frame";
|
||||
BMDTimeValue frameDuration = 0;
|
||||
BMDTimeScale frameTimescale = 0;
|
||||
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
||||
mScheduler.Configure(frameDuration, frameTimescale);
|
||||
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
|
||||
|
||||
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||
outputFrameRowBytes = outputFrameSize.width * 4u;
|
||||
captureTextureWidth = inputFrameSize.width / 2u;
|
||||
outputPackTextureWidth = outputFrameSize.width;
|
||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
||||
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
|
||||
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
||||
mState.outputPackTextureWidth = mState.outputFrameSize.width;
|
||||
mState.hasInputDevice = input != nullptr;
|
||||
mState.hasInputSource = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error)
|
||||
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
||||
{
|
||||
if (!output)
|
||||
{
|
||||
@@ -224,95 +231,102 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
|
||||
return false;
|
||||
}
|
||||
|
||||
formatStatusMessage.clear();
|
||||
mState.formatStatusMessage.clear();
|
||||
|
||||
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
|
||||
inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
||||
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
||||
if (input != nullptr && !inputTenBitSupported)
|
||||
formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
||||
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
||||
|
||||
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
|
||||
outputPixelFormat = ChoosePreferredVideoIOFormat(outputTenBitSupported);
|
||||
if (!outputTenBitSupported)
|
||||
formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
|
||||
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;
|
||||
if (output->RowBytesForPixelFormat(OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA, outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
|
||||
if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
|
||||
{
|
||||
error = "DeckLink output setup failed while calculating output row bytes.";
|
||||
return false;
|
||||
}
|
||||
outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
|
||||
outputPackTextureWidth = OutputIsTenBit()
|
||||
? PackedTextureWidthFromRowBytes(outputFrameRowBytes)
|
||||
: outputFrameSize.width;
|
||||
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
|
||||
mState.outputPackTextureWidth = OutputIsTenBit()
|
||||
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
|
||||
: mState.outputFrameSize.width;
|
||||
|
||||
if (InputIsTenBit())
|
||||
{
|
||||
int deckLinkInputRowBytes = 0;
|
||||
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
|
||||
inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
|
||||
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
|
||||
mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
|
||||
else
|
||||
inputFrameRowBytes = MinimumV210RowBytes(inputFrameSize.width);
|
||||
mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
||||
}
|
||||
captureTextureWidth = InputIsTenBit()
|
||||
? PackedTextureWidthFromRowBytes(inputFrameRowBytes)
|
||||
: inputFrameSize.width / 2u;
|
||||
mState.captureTextureWidth = InputIsTenBit()
|
||||
? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
|
||||
: mState.inputFrameSize.width / 2u;
|
||||
|
||||
std::ostringstream status;
|
||||
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(inputPixelFormat) : "none")
|
||||
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") << ".";
|
||||
if (!formatStatusMessage.empty())
|
||||
status << " " << formatStatusMessage;
|
||||
formatStatusMessage = status.str();
|
||||
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
|
||||
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
|
||||
if (!mState.formatStatusMessage.empty())
|
||||
status << " " << mState.formatStatusMessage;
|
||||
mState.formatStatusMessage = status.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error)
|
||||
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
|
||||
{
|
||||
(void)hdc;
|
||||
(void)hglrc;
|
||||
mInputFrameCallback = std::move(callback);
|
||||
|
||||
if (!input)
|
||||
{
|
||||
hasNoInputSource = true;
|
||||
inputDisplayModeName = "No input - black frame";
|
||||
mState.hasInputSource = false;
|
||||
mState.inputDisplayModeName = "No input - black frame";
|
||||
return true;
|
||||
}
|
||||
|
||||
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(inputPixelFormat);
|
||||
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
|
||||
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||
{
|
||||
if (inputPixelFormat == VideoIOPixelFormat::V210)
|
||||
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
|
||||
{
|
||||
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
|
||||
inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||
captureTextureWidth = inputFrameSize.width / 2u;
|
||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
||||
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
||||
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
||||
{
|
||||
std::ostringstream status;
|
||||
status << "DeckLink formats: capture " << VideoIOPixelFormatName(inputPixelFormat)
|
||||
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA")
|
||||
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
|
||||
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
|
||||
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
|
||||
formatStatusMessage = status.str();
|
||||
mState.formatStatusMessage = status.str();
|
||||
goto input_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
|
||||
input.Release();
|
||||
hasNoInputSource = true;
|
||||
inputDisplayModeName = "No input - black frame";
|
||||
mState.hasInputDevice = false;
|
||||
mState.hasInputSource = false;
|
||||
mState.inputDisplayModeName = "No input - black frame";
|
||||
return true;
|
||||
}
|
||||
|
||||
input_enabled:
|
||||
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(owner));
|
||||
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
|
||||
if (captureDelegate == nullptr)
|
||||
{
|
||||
error = "DeckLink input setup failed while creating the capture callback.";
|
||||
@@ -327,10 +341,9 @@ input_enabled:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||
{
|
||||
(void)hdc;
|
||||
(void)hglrc;
|
||||
mOutputFrameCallback = std::move(callback);
|
||||
|
||||
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
|
||||
{
|
||||
@@ -339,39 +352,39 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl
|
||||
}
|
||||
|
||||
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
|
||||
keyerInterfaceAvailable = true;
|
||||
mState.keyerInterfaceAvailable = true;
|
||||
|
||||
if (externalKeyingEnabled)
|
||||
{
|
||||
if (!supportsExternalKeying)
|
||||
if (!mState.supportsExternalKeying)
|
||||
{
|
||||
statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
||||
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
||||
}
|
||||
else if (!keyerInterfaceAvailable)
|
||||
else if (!mState.keyerInterfaceAvailable)
|
||||
{
|
||||
statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
||||
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
||||
}
|
||||
else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
|
||||
{
|
||||
statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
|
||||
mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
|
||||
}
|
||||
else
|
||||
{
|
||||
externalKeyingActive = true;
|
||||
statusMessage = "External keying is active on the selected DeckLink output.";
|
||||
mState.externalKeyingActive = true;
|
||||
mState.statusMessage = "External keying is active on the selected DeckLink output.";
|
||||
}
|
||||
}
|
||||
else if (supportsExternalKeying)
|
||||
else if (mState.supportsExternalKeying)
|
||||
{
|
||||
statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
|
||||
|
||||
const BMDPixelFormat deckLinkOutputPixelFormat = OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA;
|
||||
if (output->CreateVideoFrame(outputFrameSize.width, outputFrameSize.height, outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
|
||||
const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
|
||||
if (output->CreateVideoFrame(mState.outputFrameSize.width, mState.outputFrameSize.height, mState.outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
|
||||
{
|
||||
error = "DeckLink output setup failed while creating an output video frame.";
|
||||
return false;
|
||||
@@ -380,7 +393,7 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl
|
||||
outputVideoFrameQueue.push_back(outputFrame);
|
||||
}
|
||||
|
||||
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(owner));
|
||||
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
|
||||
if (playoutDelegate == nullptr)
|
||||
{
|
||||
error = "DeckLink output setup failed while creating the playout callback.";
|
||||
@@ -393,45 +406,73 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formatStatusMessage.empty())
|
||||
statusMessage = statusMessage.empty() ? formatStatusMessage : formatStatusMessage + " " + statusMessage;
|
||||
if (!mState.formatStatusMessage.empty())
|
||||
mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
double DeckLinkSession::FrameBudgetMilliseconds() const
|
||||
{
|
||||
return frameTimescale != 0
|
||||
? (static_cast<double>(frameDuration) * 1000.0) / static_cast<double>(frameTimescale)
|
||||
: 0.0;
|
||||
return mScheduler.FrameBudgetMilliseconds();
|
||||
}
|
||||
|
||||
IDeckLinkMutableVideoFrame* DeckLinkSession::RotateOutputFrame()
|
||||
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
||||
{
|
||||
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
|
||||
outputVideoFrameQueue.push_back(outputVideoFrame);
|
||||
outputVideoFrameQueue.pop_front();
|
||||
return outputVideoFrame.p;
|
||||
}
|
||||
|
||||
void DeckLinkSession::AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult)
|
||||
{
|
||||
if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped)
|
||||
totalPlayoutFrames += 2;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||
{
|
||||
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK)
|
||||
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
|
||||
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
||||
return false;
|
||||
|
||||
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
||||
return false;
|
||||
|
||||
void* pFrame = nullptr;
|
||||
outputVideoFrameBuffer->GetBytes(&pFrame);
|
||||
|
||||
frame.bytes = pFrame;
|
||||
frame.rowBytes = outputVideoFrame->GetRowBytes();
|
||||
frame.width = mState.outputFrameSize.width;
|
||||
frame.height = mState.outputFrameSize.height;
|
||||
frame.pixelFormat = mState.outputPixelFormat;
|
||||
frame.nativeFrame = outputVideoFrame.p;
|
||||
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
|
||||
{
|
||||
IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
|
||||
if (outputVideoFrameBuffer != nullptr)
|
||||
{
|
||||
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
||||
outputVideoFrameBuffer->Release();
|
||||
}
|
||||
frame.nativeBuffer = nullptr;
|
||||
frame.bytes = nullptr;
|
||||
}
|
||||
|
||||
void DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult)
|
||||
{
|
||||
mScheduler.AccountForCompletionResult(completionResult);
|
||||
}
|
||||
|
||||
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
||||
{
|
||||
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
if (outputVideoFrame == nullptr || output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
|
||||
return false;
|
||||
|
||||
totalPlayoutFrames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::Start()
|
||||
{
|
||||
totalPlayoutFrames = 0;
|
||||
mScheduler.Reset();
|
||||
if (!output)
|
||||
{
|
||||
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||
@@ -462,19 +503,18 @@ bool DeckLinkSession::Start()
|
||||
return false;
|
||||
}
|
||||
|
||||
void* pFrame;
|
||||
void* pFrame = nullptr;
|
||||
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
|
||||
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * outputFrameSize.height);
|
||||
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
|
||||
|
||||
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
||||
|
||||
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK)
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
if (output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
|
||||
{
|
||||
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
totalPlayoutFrames++;
|
||||
}
|
||||
|
||||
if (input)
|
||||
@@ -485,7 +525,7 @@ bool DeckLinkSession::Start()
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (output->StartScheduledPlayback(0, frameTimescale, 1.0) != S_OK)
|
||||
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
|
||||
{
|
||||
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
@@ -499,7 +539,7 @@ bool DeckLinkSession::Stop()
|
||||
if (keyer != nullptr)
|
||||
{
|
||||
keyer->Disable();
|
||||
externalKeyingActive = false;
|
||||
mState.externalKeyingActive = false;
|
||||
}
|
||||
|
||||
if (input)
|
||||
@@ -516,3 +556,66 @@ bool DeckLinkSession::Stop()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||
{
|
||||
mState.hasInputSource = !hasNoInputSource;
|
||||
if (hasNoInputSource || mInputFrameCallback == nullptr)
|
||||
{
|
||||
VideoIOFrame frame;
|
||||
frame.width = mState.inputFrameSize.width;
|
||||
frame.height = mState.inputFrameSize.height;
|
||||
frame.pixelFormat = mState.inputPixelFormat;
|
||||
frame.hasNoInputSource = hasNoInputSource;
|
||||
if (mInputFrameCallback)
|
||||
mInputFrameCallback(frame);
|
||||
return;
|
||||
}
|
||||
|
||||
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
||||
void* videoPixels = nullptr;
|
||||
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
|
||||
return;
|
||||
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||
return;
|
||||
|
||||
inputFrameBuffer->GetBytes(&videoPixels);
|
||||
|
||||
VideoIOFrame frame;
|
||||
frame.bytes = videoPixels;
|
||||
frame.rowBytes = inputFrame->GetRowBytes();
|
||||
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
|
||||
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
|
||||
frame.pixelFormat = mState.inputPixelFormat;
|
||||
frame.hasNoInputSource = hasNoInputSource;
|
||||
mInputFrameCallback(frame);
|
||||
|
||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||
}
|
||||
|
||||
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, BMDOutputFrameCompletionResult completionResult)
|
||||
{
|
||||
if (!mOutputFrameCallback)
|
||||
return;
|
||||
|
||||
VideoIOCompletion completion;
|
||||
switch (completionResult)
|
||||
{
|
||||
case bmdOutputFrameDisplayedLate:
|
||||
completion.result = VideoIOCompletionResult::DisplayedLate;
|
||||
break;
|
||||
case bmdOutputFrameDropped:
|
||||
completion.result = VideoIOCompletionResult::Dropped;
|
||||
break;
|
||||
case bmdOutputFrameFlushed:
|
||||
completion.result = VideoIOCompletionResult::Flushed;
|
||||
break;
|
||||
case bmdOutputFrameCompleted:
|
||||
completion.result = VideoIOCompletionResult::Completed;
|
||||
break;
|
||||
default:
|
||||
completion.result = VideoIOCompletionResult::Unknown;
|
||||
break;
|
||||
}
|
||||
mOutputFrameCallback(completion);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkFrameTransfer.h"
|
||||
#include "DeckLinkVideoIOFormat.h"
|
||||
#include "VideoIOFormat.h"
|
||||
#include "VideoIOTypes.h"
|
||||
#include "VideoPlayoutScheduler.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
class OpenGLComposite;
|
||||
|
||||
class DeckLinkSession : public VideoIODevice
|
||||
{
|
||||
public:
|
||||
DeckLinkSession() = default;
|
||||
~DeckLinkSession();
|
||||
|
||||
void ReleaseResources() override;
|
||||
bool DiscoverDevicesAndModes(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;
|
||||
bool Stop() override;
|
||||
|
||||
bool HasInputDevice() const { return mState.hasInputDevice; }
|
||||
bool HasInputSource() const { return mState.hasInputSource; }
|
||||
void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
|
||||
bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
|
||||
const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
|
||||
const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
|
||||
unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
|
||||
unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
|
||||
unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
|
||||
unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
|
||||
VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
|
||||
VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
|
||||
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
|
||||
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
|
||||
unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
|
||||
unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
|
||||
unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
|
||||
unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
|
||||
const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
|
||||
const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
|
||||
const std::string& OutputModelName() const { return mState.outputModelName; }
|
||||
bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
|
||||
bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
|
||||
bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
|
||||
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
|
||||
const std::string& StatusMessage() const { return mState.statusMessage; }
|
||||
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
||||
const VideoIOState& State() const override { return mState; }
|
||||
VideoIOState& MutableState() override { return mState; }
|
||||
double FrameBudgetMilliseconds() const;
|
||||
void AccountForCompletionResult(VideoIOCompletionResult completionResult) override;
|
||||
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
|
||||
void EndOutputFrame(VideoIOOutputFrame& frame) override;
|
||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
|
||||
void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
||||
|
||||
private:
|
||||
CComPtr<CaptureDelegate> captureDelegate;
|
||||
CComPtr<PlayoutDelegate> playoutDelegate;
|
||||
CComPtr<IDeckLinkInput> input;
|
||||
CComPtr<IDeckLinkOutput> output;
|
||||
CComPtr<IDeckLinkKeyer> keyer;
|
||||
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||
VideoIOState mState;
|
||||
VideoPlayoutScheduler mScheduler;
|
||||
InputFrameCallback mInputFrameCallback;
|
||||
OutputFrameCallback mOutputFrameCallback;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "DeckLinkVideoIOFormat.h"
|
||||
|
||||
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case VideoIOPixelFormat::V210:
|
||||
return bmdFormat10BitYUV;
|
||||
case VideoIOPixelFormat::Yuva10:
|
||||
return bmdFormat10BitYUVA;
|
||||
case VideoIOPixelFormat::Bgra8:
|
||||
return bmdFormat8BitBGRA;
|
||||
case VideoIOPixelFormat::Uyvy8:
|
||||
default:
|
||||
return bmdFormat8BitYUV;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
|
||||
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,90 +1,347 @@
|
||||
{
|
||||
"id": "greenscreen-key",
|
||||
"name": "Greenscreen Key",
|
||||
"description": "Keys out a green screen background and outputs transparent alpha for compositing.",
|
||||
"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",
|
||||
"label": "Threshold",
|
||||
"label": "Screen Gain",
|
||||
"type": "float",
|
||||
"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",
|
||||
"label": "Softness",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"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": "edgeSoftness",
|
||||
"label": "Edge Softness",
|
||||
"id": "screenBalance",
|
||||
"label": "Screen Balance",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.0,
|
||||
"max": 0.4,
|
||||
"step": 0.005
|
||||
"default": 0.5,
|
||||
"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,
|
||||
"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,
|
||||
"max": 6,
|
||||
"step": 0.1,
|
||||
"description": "Softens the generated matte after keying."
|
||||
},
|
||||
{
|
||||
"id": "matteGamma",
|
||||
"label": "Matte Gamma",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Shapes midtone opacity in the matte."
|
||||
},
|
||||
{
|
||||
"id": "matteContrast",
|
||||
"label": "Matte Contrast",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Increases or reduces matte separation around 50 percent alpha."
|
||||
},
|
||||
{
|
||||
"id": "blackCleanup",
|
||||
"label": "Black Cleanup",
|
||||
"type": "float",
|
||||
"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,
|
||||
"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": "edgeBoost",
|
||||
"label": "Edge Boost",
|
||||
"id": "despillBias",
|
||||
"label": "Despill Bias",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": -0.2,
|
||||
"max": 0.3,
|
||||
"step": 0.005
|
||||
"default": 0,
|
||||
"min": -0.5,
|
||||
"max": 0.5,
|
||||
"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,
|
||||
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,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Adds color recovery along semi-transparent matte edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeColor",
|
||||
"label": "Edge Color",
|
||||
"type": "color",
|
||||
"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",
|
||||
"label": "View",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"description": "Debug output mode for inspecting matte, spill, and despill stages."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,32 +9,204 @@ float luma709(float3 color)
|
||||
return dot(color, float3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float2 chroma709(float3 color)
|
||||
{
|
||||
float y = luma709(color);
|
||||
return float2((color.b - y) * 0.5647, (color.r - y) * 0.7132);
|
||||
}
|
||||
|
||||
float3 matteSampleColor(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float blur = max(screenPreBlur, 0.0);
|
||||
float3 center = saturate(sampleVideo(saturate(uv)).rgb);
|
||||
if (blur <= 0.0001)
|
||||
return center;
|
||||
|
||||
float2 radius = pixel * blur;
|
||||
float3 color = center * 0.36;
|
||||
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv - float2(radius.x, 0.0))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv + float2(0.0, radius.y))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv - float2(0.0, radius.y))).rgb) * 0.16;
|
||||
return color;
|
||||
}
|
||||
|
||||
float keyDistanceAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float3 color = matteSampleColor(uv, context);
|
||||
float3 keyColor = saturate(screenColor.rgb);
|
||||
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
|
||||
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
|
||||
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
|
||||
}
|
||||
|
||||
float rawAlphaAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float keyDistance = keyDistanceAt(uv, context);
|
||||
float matteCenter = threshold + erodeDilate;
|
||||
float matteFeather = max(softness, 0.0005);
|
||||
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, keyDistance);
|
||||
return saturate(alpha);
|
||||
}
|
||||
|
||||
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 = matteAlphaAt(uv);
|
||||
float alpha = centerAlpha * 0.30;
|
||||
|
||||
if (aaRadius > 0.0001)
|
||||
{
|
||||
float2 radius = pixel * aaRadius;
|
||||
float2 halfRadius = radius * 0.5;
|
||||
float alphaMin = centerAlpha;
|
||||
float alphaMax = centerAlpha;
|
||||
float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
|
||||
alpha = lerp(alpha, alphaMin, saturate(blackCleanup));
|
||||
alpha = lerp(alpha, alphaMax, saturate(whiteCleanup));
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha = centerAlpha;
|
||||
}
|
||||
|
||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
|
||||
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
|
||||
return saturate(alpha);
|
||||
}
|
||||
|
||||
float spillAmountForColor(float3 color)
|
||||
{
|
||||
float3 keyColor = saturate(screenColor.rgb);
|
||||
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
|
||||
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
|
||||
return saturate(keyComponent - opposingComponent + despillBias);
|
||||
}
|
||||
|
||||
float3 despillColor(float3 color, float alpha)
|
||||
{
|
||||
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
|
||||
float spill = spillAmountForColor(color) * despill * (1.0 - alpha * 0.35);
|
||||
float neutral = luma709(color);
|
||||
float3 neutralized = color - keyColor * spill;
|
||||
neutralized = max(neutralized, float3(0.0, 0.0, 0.0));
|
||||
neutralized = lerp(neutralized, float3(neutral, neutral, neutral), spill * 0.18);
|
||||
neutralized = lerp(neutralized, neutralized * saturate(spillTint.rgb), saturate(spill));
|
||||
return saturate(neutralized);
|
||||
}
|
||||
|
||||
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 = rawAlphaAt(context.uv, context);
|
||||
return float4(color, alpha);
|
||||
}
|
||||
|
||||
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
|
||||
float3 sampleColor = safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001)));
|
||||
float4 refineMatte(ShaderContext context)
|
||||
{
|
||||
float4 raw = sampleVideo(context.uv);
|
||||
float alpha = refinedAlphaFromMatte(context.uv, context);
|
||||
return float4(saturate(raw.rgb), alpha);
|
||||
}
|
||||
|
||||
float chromaDistance = length(sampleColor - keyColor);
|
||||
float matteCenter = threshold - erodeDilate;
|
||||
float matteFeather = max(softness + edgeSoftness, 0.0005);
|
||||
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, chromaDistance);
|
||||
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;
|
||||
|
||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||
alpha = saturate(alpha + edgeBoost);
|
||||
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
|
||||
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
|
||||
|
||||
float greenExcess = max(0.0, color.g - max(color.r, color.b));
|
||||
float spillReduction = greenExcess * despill;
|
||||
|
||||
float3 despilled = color;
|
||||
despilled.g = max(0.0, despilled.g - spillReduction);
|
||||
|
||||
float neutral = luma709(despilled);
|
||||
despilled.rb += spillReduction * 0.25;
|
||||
despilled = lerp(float3(neutral, neutral, neutral), despilled, 0.92);
|
||||
if (viewMode == 1)
|
||||
return float4(alpha, alpha, alpha, 1.0);
|
||||
if (viewMode == 2)
|
||||
return float4(spill, spill * 0.55, 0.0, 1.0);
|
||||
if (viewMode == 3)
|
||||
return float4(despilled, 1.0);
|
||||
if (viewMode == 4)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
49
shaders/multipass-test/shader.json
Normal file
49
shaders/multipass-test/shader.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
37
shaders/multipass-test/shader.slang
Normal file
37
shaders/multipass-test/shader.slang
Normal 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);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user