30 Commits

Author SHA1 Message Date
Aiden
0c16665610 Revert "Decklink separation"
Some checks failed
CI / Windows Release Package (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
This reverts commit 46f2f1ece5.
2026-05-09 16:47:33 +10:00
Aiden
46f2f1ece5 Decklink separation 2026-05-09 14:42:11 +10:00
Aiden
4ffbb97abf Video backend
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Successful in 2m54s
2026-05-09 14:15:49 +10:00
Aiden
98f5cbe309 preview changes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m41s
2026-05-09 13:53:00 +10:00
Aiden
93d856b3b6 CPU optimisations
Some checks failed
CI / React UI Build (push) Successful in 37s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-09 13:50:27 +10:00
6ea6971dd6 more shaders and updates/changes
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m35s
2026-05-08 20:32:19 +10:00
163d70e9bd Annotations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 20:01:22 +10:00
8afef5065a Update README.md
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-08 19:14:31 +10:00
27bf2ae45c doc updates
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 18:49:27 +10:00
1ea44ba3ae fix typo
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m19s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 18:43:48 +10:00
0af9a72937 removed redundant code
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:40:56 +10:00
d650cac857 control layout updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:28:28 +10:00
a0cc86f189 description updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:11:26 +10:00
f322abf79a updates
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:07:45 +10:00
eede6938cb Update multipass shader test
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 17:41:53 +10:00
ad24a20fdb Multi pass test
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-08 17:40:09 +10:00
5ae43513a7 annotations
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 17:35:48 +10:00
cc23e73d51 Removed uneeded code 2026-05-08 17:33:57 +10:00
f85abef237 Multi pass
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:28:48 +10:00
596d370f43 Add manifest support for pass declarations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:19:30 +10:00
87cb55b80b Layer program split
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Successful in 2m26s
2026-05-08 17:10:29 +10:00
f458eb0130 Texture binding 2026-05-08 17:04:28 +10:00
7d8f9a39d1 render target pool
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m15s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:59:43 +10:00
5b6e30ad13 Render class 2026-05-08 16:55:16 +10:00
07a5c91427 shader validation checks
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 16:46:03 +10:00
53b980913b docs update 2026-05-08 16:42:23 +10:00
4e2ac4a091 re organisation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m42s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:38:47 +10:00
3eb5bb5de3 Splitting out rendering 2026-05-08 16:33:55 +10:00
ebbc11bb34 Decklink abstraction
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m41s
CI / Windows Release Package (push) Successful in 2m20s
2026-05-08 16:27:40 +10:00
6d5a606107 Greenscreen adjsutments
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m22s
2026-05-08 16:11:43 +10:00
147 changed files with 6972 additions and 2833 deletions

View File

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

View File

@@ -31,7 +31,7 @@ if(NOT EXISTS "${SLANG_LICENSE_FILE}")
endif() endif()
set(APP_SOURCES set(APP_SOURCES
"${APP_DIR}/DeckLinkAPI_i.c" "${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
"${APP_DIR}/control/ControlServer.cpp" "${APP_DIR}/control/ControlServer.cpp"
"${APP_DIR}/control/ControlServer.h" "${APP_DIR}/control/ControlServer.h"
"${APP_DIR}/control/OscServer.cpp" "${APP_DIR}/control/OscServer.cpp"
@@ -40,48 +40,58 @@ set(APP_SOURCES
"${APP_DIR}/control/RuntimeControlBridge.h" "${APP_DIR}/control/RuntimeControlBridge.h"
"${APP_DIR}/control/RuntimeServices.cpp" "${APP_DIR}/control/RuntimeServices.cpp"
"${APP_DIR}/control/RuntimeServices.h" "${APP_DIR}/control/RuntimeServices.h"
"${APP_DIR}/decklink/DeckLinkDisplayMode.cpp" "${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
"${APP_DIR}/decklink/DeckLinkDisplayMode.h" "${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
"${APP_DIR}/decklink/DeckLinkFrameTransfer.cpp" "${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
"${APP_DIR}/decklink/DeckLinkFrameTransfer.h" "${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
"${APP_DIR}/decklink/DeckLinkSession.cpp" "${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/decklink/DeckLinkSession.h" "${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
"${APP_DIR}/decklink/VideoIOFormat.cpp" "${APP_DIR}/videoio/decklink/DeckLinkSession.h"
"${APP_DIR}/decklink/VideoIOFormat.h" "${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/gl/GLExtensions.cpp" "${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/gl/GLExtensions.h" "${APP_DIR}/videoio/VideoIOBackendFactory.cpp"
"${APP_DIR}/gl/GlobalParamsBuffer.cpp" "${APP_DIR}/videoio/VideoIOBackendFactory.h"
"${APP_DIR}/gl/GlobalParamsBuffer.h" "${APP_DIR}/videoio/VideoIOConfig.cpp"
"${APP_DIR}/gl/GlRenderConstants.h" "${APP_DIR}/videoio/VideoIOConfig.h"
"${APP_DIR}/gl/GlScopedObjects.h" "${APP_DIR}/gl/renderer/GLExtensions.cpp"
"${APP_DIR}/gl/GlShaderSources.cpp" "${APP_DIR}/gl/renderer/GLExtensions.h"
"${APP_DIR}/gl/GlShaderSources.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.cpp"
"${APP_DIR}/gl/OpenGLComposite.h" "${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp" "${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/OpenGLDeckLinkBridge.cpp" "${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/OpenGLDeckLinkBridge.h" "${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
"${APP_DIR}/gl/OpenGLRenderPass.cpp" "${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
"${APP_DIR}/gl/OpenGLRenderPass.h" "${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
"${APP_DIR}/gl/OpenGLRenderer.cpp" "${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
"${APP_DIR}/gl/OpenGLRenderer.h" "${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
"${APP_DIR}/gl/OpenGLShaderPrograms.cpp" "${APP_DIR}/gl/renderer/OpenGLRenderer.h"
"${APP_DIR}/gl/OpenGLShaderPrograms.h" "${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
"${APP_DIR}/gl/PngScreenshotWriter.cpp" "${APP_DIR}/gl/renderer/RenderTargetPool.h"
"${APP_DIR}/gl/PngScreenshotWriter.h" "${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
"${APP_DIR}/gl/ShaderProgramCompiler.cpp" "${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
"${APP_DIR}/gl/ShaderProgramCompiler.h" "${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
"${APP_DIR}/gl/ShaderBuildQueue.cpp" "${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
"${APP_DIR}/gl/ShaderBuildQueue.h" "${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
"${APP_DIR}/gl/ShaderTextureBindings.cpp" "${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
"${APP_DIR}/gl/ShaderTextureBindings.h" "${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
"${APP_DIR}/gl/Std140Buffer.h" "${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
"${APP_DIR}/gl/TextRasterizer.cpp" "${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
"${APP_DIR}/gl/TextRasterizer.h" "${APP_DIR}/gl/shader/ShaderBuildQueue.h"
"${APP_DIR}/gl/TextureAssetLoader.cpp" "${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
"${APP_DIR}/gl/TextureAssetLoader.h" "${APP_DIR}/gl/shader/ShaderTextureBindings.h"
"${APP_DIR}/gl/TemporalHistoryBuffers.cpp" "${APP_DIR}/gl/shader/Std140Buffer.h"
"${APP_DIR}/gl/TemporalHistoryBuffers.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.cpp"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h" "${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc" "${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
@@ -104,6 +114,11 @@ set(APP_SOURCES
"${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.cpp"
"${APP_DIR}/stdafx.h" "${APP_DIR}/stdafx.h"
"${APP_DIR}/targetver.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}) add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
@@ -111,11 +126,15 @@ add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}" "${APP_DIR}"
"${APP_DIR}/control" "${APP_DIR}/control"
"${APP_DIR}/decklink"
"${APP_DIR}/gl" "${APP_DIR}/gl"
"${APP_DIR}/gl/pipeline"
"${APP_DIR}/gl/renderer"
"${APP_DIR}/gl/shader"
"${APP_DIR}/platform" "${APP_DIR}/platform"
"${APP_DIR}/runtime" "${APP_DIR}/runtime"
"${APP_DIR}/shader" "${APP_DIR}/shader"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
) )
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
@@ -189,6 +208,35 @@ endif()
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests) add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
add_executable(RuntimeHostVideoIOStateTests
"${APP_DIR}/runtime/RuntimeHost.cpp"
"${APP_DIR}/runtime/RuntimeClock.cpp"
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${APP_DIR}/videoio/VideoIOConfig.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeHostVideoIOStateTests.cpp"
)
target_include_directories(RuntimeHostVideoIOStateTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
"${APP_DIR}/videoio"
)
target_link_libraries(RuntimeHostVideoIOStateTests PRIVATE
Advapi32
)
if(MSVC)
target_compile_options(RuntimeHostVideoIOStateTests PRIVATE /W3)
endif()
add_test(NAME RuntimeHostVideoIOStateTests COMMAND RuntimeHostVideoIOStateTests)
add_executable(Std140BufferTests add_executable(Std140BufferTests
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
) )
@@ -196,6 +244,7 @@ add_executable(Std140BufferTests
target_include_directories(Std140BufferTests PRIVATE target_include_directories(Std140BufferTests PRIVATE
"${APP_DIR}" "${APP_DIR}"
"${APP_DIR}/gl" "${APP_DIR}/gl"
"${APP_DIR}/gl/shader"
) )
if(MSVC) if(MSVC)
@@ -222,6 +271,29 @@ endif()
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests) add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
add_executable(ShaderSlangValidationTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
)
target_include_directories(ShaderSlangValidationTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
)
if(MSVC)
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
endif()
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
set_tests_properties(ShaderSlangValidationTests PROPERTIES
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
)
add_executable(OscServerTests add_executable(OscServerTests
"${APP_DIR}/control/OscServer.cpp" "${APP_DIR}/control/OscServer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
@@ -244,13 +316,15 @@ endif()
add_test(NAME OscServerTests COMMAND OscServerTests) add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(VideoIOFormatTests 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" "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
) )
target_include_directories(VideoIOFormatTests PRIVATE target_include_directories(VideoIOFormatTests PRIVATE
"${APP_DIR}" "${APP_DIR}"
"${APP_DIR}/decklink" "${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
) )
if(MSVC) if(MSVC)
@@ -259,6 +333,78 @@ endif()
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests) 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/VideoIOConfig.cpp"
"${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)
add_executable(VideoIOBackendFactoryTests
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/videoio/VideoIOBackendFactory.cpp"
"${APP_DIR}/videoio/VideoIOBackendFactory.h"
"${APP_DIR}/videoio/VideoIOConfig.cpp"
"${APP_DIR}/videoio/VideoIOConfig.h"
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOBackendFactoryTests.cpp"
)
target_include_directories(VideoIOBackendFactoryTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/gl/renderer"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
target_link_libraries(VideoIOBackendFactoryTests PRIVATE
Ole32
)
if(MSVC)
target_compile_options(VideoIOBackendFactoryTests PRIVATE /W3)
endif()
add_test(NAME VideoIOBackendFactoryTests COMMAND VideoIOBackendFactoryTests)
install(TARGETS LoopThroughWithOpenGLCompositing install(TARGETS LoopThroughWithOpenGLCompositing
RUNTIME DESTINATION "." RUNTIME DESTINATION "."
) )

View File

@@ -1,8 +1,8 @@
# Video Shader # Video Shader
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI. Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
## Repository Layout ## Repository Layout
@@ -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. - `tests/`: focused native tests for pure runtime logic.
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build. - `.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 ## Requirements
- Windows with Visual Studio 2022 C++ tooling. - Windows with Visual Studio 2022 C++ tooling.
- CMake 3.24 or newer. - CMake 3.24 or newer.
- Node.js and npm for the control UI. - Node.js and npm for the control UI.
- Blackmagic Desktop Video drivers and a DeckLink device. - Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`. - Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
Default expected Slang path: Default expected Slang path:
@@ -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 native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development.
The control UI provides:
- A searchable shader library for adding layers.
- Compact parameter rows with inline descriptions and OSC copy controls.
- Stack save/recall presets.
- Manual shader reload.
- Screenshot capture from the final output render target.
## Package ## Package
Build the UI, build the native Release target, then install into a portable runtime folder: Build the UI, build the native Release target, then install into a portable runtime folder:
@@ -77,6 +93,8 @@ dist/VideoShader/
shaders/ shaders/
3rdParty/slang/bin/ 3rdParty/slang/bin/
ui/dist/ ui/dist/
docs/
SHADER_CONTRACT.md
runtime/templates/ runtime/templates/
third_party_notices/ third_party_notices/
``` ```
@@ -111,6 +129,9 @@ Current native test coverage includes:
- JSON parsing and serialization. - JSON parsing and serialization.
- Parameter normalization and preset filename safety. - Parameter normalization and preset filename safety.
- Shader manifest parsing, temporal manifest validation, and package registry scanning. - Shader manifest parsing, temporal manifest validation, and package registry scanning.
- Video I/O format helpers, v210/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 ## 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. 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. Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
The control UI also has a Screenshot button. It queues a capture of the final output render target and writes a PNG under: The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, queues shader compilation, refreshes shader availability/errors, and keeps the previous working shader stack running if a changed shader fails to compile.
Each parameter row also includes a small **OSC** button. Clicking it copies that parameter's OSC route to the clipboard.
The control UI also has a **Screenshot** button. It queues a capture of the final output render target and writes a PNG under:
```text ```text
runtime/screenshots/ runtime/screenshots/
@@ -194,10 +219,11 @@ Each shader package lives under:
shaders/<id>/ shaders/<id>/
shader.json shader.json
shader.slang shader.slang
optional-extra-pass.slang
optional-font-or-texture-assets optional-font-or-texture-assets
``` ```
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
## Generated Files ## Generated Files
@@ -208,6 +234,7 @@ Runtime-generated files are intentionally ignored:
- `runtime/shader_cache/active_shader.frag` - `runtime/shader_cache/active_shader.frag`
- `runtime/runtime_state.json` autosaved latest stack and parameter state. - `runtime/runtime_state.json` autosaved latest stack and parameter state.
- `runtime/stack_presets/*.json` - `runtime/stack_presets/*.json`
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
Only `runtime/templates/` and `runtime/README.md` are tracked. Only `runtime/templates/` and `runtime/README.md` are tracked.
@@ -236,16 +263,15 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
- Audio. - Audio.
- Genlock. - Genlock.
- Find a better UI library for react.
- Logs. - 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) - Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
- Add WebView2 - Add WebView2 for an embedded native control surface.
- move to MSDF, typography rasterisation - MSDF typography rasterisation
- better shader search UI, pass 1 - More shader-library organisation and filtering as the built-in library grows.
- More comprehensive greenscreen shader - Optional linear-light compositing mode.
- linear compositing? - compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
- compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store
- allow shaders to read other shaders data store based on name? or output over OSC - allow shaders to read other shaders data store based on name? or output over OSC
- Mipmappong for shader declared textures - Mipmapping for shader-declared textures
- unwrap a fish eyelens and mirror it and map it to equirectangulr for environmnet map purposes - Anotate included shaders
- allow 3 vector exposed controls

View File

@@ -55,7 +55,7 @@ float4 shadeVideo(ShaderContext context)
} }
``` ```
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. You can also use **Reload shaders** in the control UI to manually rescan the shader library.
## Guidance For Shaders ## Guidance For Shaders
@@ -80,7 +80,7 @@ Important rules:
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation. - If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
- If the source license is unclear or incompatible, do not add the shader package. - If the source license is unclear or incompatible, do not add the shader package.
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. CI also runs shader validation, so every available package in `shaders/` should compile successfully. Intentionally broken examples should stay visibly marked as broken rather than pretending to be production shaders.
## Manifest Fields ## Manifest Fields
@@ -97,10 +97,13 @@ Optional fields:
- `description`: display/help text for the shader library. - `description`: display/help text for the shader library.
- `category`: UI grouping label. - `category`: UI grouping label.
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`. - `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
- `textures`: texture assets to load and expose as samplers. - `textures`: texture assets to load and expose as samplers.
- `fonts`: packaged font assets for live text parameters. - `fonts`: packaged font assets for live text parameters.
- `temporal`: history-buffer requirements. - `temporal`: history-buffer requirements.
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
Shader-visible identifiers must be valid Slang-style identifiers: Shader-visible identifiers must be valid Slang-style identifiers:
- `entryPoint` - `entryPoint`
@@ -110,6 +113,87 @@ Shader-visible identifiers must be valid Slang-style identifiers:
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID. Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
## Render Passes
Most shaders should omit `passes`. The runtime then creates one implicit pass:
```json
{
"id": "main",
"source": "shader.slang",
"entryPoint": "shadeVideo",
"output": "layerOutput"
}
```
Advanced shaders may declare explicit passes. All passes may live in one `.slang` file by using different `entryPoint` values, or they may be split across multiple source files:
```json
{
"passes": [
{
"id": "blurX",
"source": "blur-x.slang",
"entryPoint": "blurHorizontal",
"inputs": ["layerInput"],
"output": "blurredX"
},
{
"id": "final",
"source": "final.slang",
"entryPoint": "finish",
"inputs": ["blurredX"],
"output": "layerOutput"
}
]
}
```
Pass fields:
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
- `source`: required Slang source path relative to the package directory.
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
- `inputs`: optional list of named inputs. The first input is used as the pass input texture.
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
Pass input names:
- `layerInput`: the input to this layer, before any of its passes run.
- `previousPass`: the previous pass output in this layer. If there is no previous pass, this falls back to `layerInput`.
- Any earlier pass `id` or `output` name from the same layer.
If `inputs` is omitted, the first pass samples `layerInput` and later passes sample `previousPass`.
Single-file multipass example:
```json
{
"passes": [
{
"id": "mask",
"source": "shader.slang",
"entryPoint": "makeMask",
"output": "maskBuffer"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finish",
"inputs": ["maskBuffer"],
"output": "layerOutput"
}
]
}
```
Pass output names:
- `layerOutput`: the final visible output of this layer.
- Any other name creates an intermediate 16-bit float render target that later passes may sample.
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
## Slang Entry Point ## Slang Entry Point
Your shader file must implement the manifest `entryPoint`. Your shader file must implement the manifest `entryPoint`.
@@ -163,7 +247,7 @@ Fields:
- `uv`: normalized texture coordinates, usually `0..1`. - `uv`: normalized texture coordinates, usually `0..1`.
- `sourceColor`: decoded RGBA source video at `uv`. - `sourceColor`: decoded RGBA source video at `uv`.
- `inputResolution`: decoded input video resolution in pixels. - `inputResolution`: decoded input video resolution in pixels.
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode. - `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured video I/O output mode.
- `time`: elapsed runtime time in seconds. - `time`: elapsed runtime time in seconds.
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight. - `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day. - `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
@@ -177,8 +261,8 @@ Fields:
Color/precision notes: Color/precision notes:
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB. - `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
- The host prefers 10-bit DeckLink YUV capture and output when the card/mode supports it, with automatic 8-bit fallback. - The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback. 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 the packed DeckLink byte formats. - Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior. - Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
## Helper Functions ## Helper Functions
@@ -547,6 +631,8 @@ When a shader compiles, the runtime writes generated files under `runtime/shader
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader. These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
For multipass shaders, these files reflect the most recently compiled pass. If a package has several passes, the reported compile error and pass name are usually more useful than assuming the cache contains the first pass.
## Common Pitfalls ## Common Pitfalls
- Do not use hyphens in parameter IDs, texture IDs, or entry point names. - Do not use hyphens in parameter IDs, texture IDs, or entry point names.

View File

@@ -412,10 +412,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
break; break;
} }
// Setup OpenGL and DeckLink capture and playout object // Setup OpenGL and video I/O capture/playout object
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC); pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
if (pOpenGLComposite->InitDeckLink()) if (pOpenGLComposite->InitializeVideoIO())
{ {
wglMakeCurrent( NULL, NULL ); wglMakeCurrent( NULL, NULL );
if (pOpenGLComposite->Start()) if (pOpenGLComposite->Start())
@@ -423,11 +423,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0); PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
break; // success break; // success
} }
MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR); MessageBoxA(NULL, "The OpenGL/video I/O runtime initialized, but playout failed to start. See the previous start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
} }
else else
{ {
MessageBoxA(NULL, "The OpenGL/DeckLink runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR); MessageBoxA(NULL, "The OpenGL/video I/O runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
} }
// Failed to initialize - cleanup // Failed to initialize - cleanup
@@ -438,7 +438,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
} }
catch (...) catch (...)
{ {
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime."); ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/video I/O runtime.");
PostMessage(hWnd, WM_CLOSE, 0, 0); PostMessage(hWnd, WM_CLOSE, 0, 0);
break; break;
} }
@@ -474,7 +474,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
} }
catch (...) catch (...)
{ {
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime."); ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/video I/O runtime.");
} }
// Deselect the current rendering context and delete it // Deselect the current rendering context and delete it

View File

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

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<Filter Include="Source Files"> <Filter Include="Source Files">
@@ -18,7 +18,7 @@
</Filter> </Filter>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="gl\GLExtensions.cpp"> <ClCompile Include="gl\renderer\GLExtensions.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp"> <ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
@@ -27,45 +27,60 @@
<ClCompile Include="gl\OpenGLComposite.cpp"> <ClCompile Include="gl\OpenGLComposite.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\OpenGLRenderPass.cpp"> <ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\OpenGLRenderer.cpp"> <ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\OpenGLShaderPrograms.cpp"> <ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\PngScreenshotWriter.cpp"> <ClCompile Include="gl\renderer\RenderTargetPool.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\ShaderBuildQueue.cpp"> <ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </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> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="DeckLinkAPI_i.c"> <ClCompile Include="videoio\decklink\DeckLinkAPI_i.c">
<Filter>DeckLink API</Filter> <Filter>DeckLink API</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="control\RuntimeServices.cpp"> <ClCompile Include="control\RuntimeServices.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="decklink\DeckLinkSession.cpp"> <ClCompile Include="videoio\decklink\DeckLinkSession.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="decklink\VideoIOFormat.cpp"> <ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="runtime\RuntimeClock.cpp"> <ClCompile Include="runtime\RuntimeClock.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="videoio\VideoIOFormat.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="gl\GLExtensions.h"> <ClInclude Include="gl\renderer\GLExtensions.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="LoopThroughWithOpenGLCompositing.h"> <ClInclude Include="LoopThroughWithOpenGLCompositing.h">
@@ -74,22 +89,34 @@
<ClInclude Include="gl\OpenGLComposite.h"> <ClInclude Include="gl\OpenGLComposite.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\OpenGLRenderPass.h"> <ClInclude Include="gl\pipeline\OpenGLRenderPass.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\OpenGLRenderer.h"> <ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\OpenGLShaderPrograms.h"> <ClInclude Include="gl\pipeline\RenderPassDescriptor.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\PngScreenshotWriter.h"> <ClInclude Include="gl\renderer\OpenGLRenderer.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\ShaderBuildQueue.h"> <ClInclude Include="gl\renderer\RenderTargetPool.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </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> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="resource.h"> <ClInclude Include="resource.h">
@@ -104,15 +131,24 @@
<ClInclude Include="control\RuntimeServices.h"> <ClInclude Include="control\RuntimeServices.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="decklink\DeckLinkSession.h"> <ClInclude Include="videoio\decklink\DeckLinkSession.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="decklink\VideoIOFormat.h"> <ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="runtime\RuntimeClock.h"> <ClInclude Include="runtime\RuntimeClock.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </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>
<ItemGroup> <ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico"> <Image Include="LoopThroughWithOpenGLCompositing.ico">

View File

@@ -501,6 +501,9 @@ bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& pa
void ControlServer::BroadcastStateLocked() void ControlServer::BroadcastStateLocked()
{ {
if (mClients.empty())
return;
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"; const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
for (auto it = mClients.begin(); it != mClients.end();) for (auto it = mClients.begin(); it != mClients.end();)
{ {

View File

@@ -1,47 +0,0 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include <string>
struct FrameSize
{
unsigned width = 0;
unsigned height = 0;
bool IsEmpty() const { return width == 0 || height == 0; }
};
inline bool operator==(const FrameSize& left, const FrameSize& right)
{
return left.width == right.width && left.height == right.height;
}
inline bool operator!=(const FrameSize& left, const FrameSize& right)
{
return !(left == right);
}
struct VideoFormat
{
BMDDisplayMode displayMode = bmdModeHD1080p5994;
std::string displayName = "1080p59.94";
};
struct VideoFormatSelection
{
VideoFormat input;
VideoFormat output;
};
std::string NormalizeModeToken(const std::string& value);
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName);
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode);
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error);
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);

View File

@@ -1,518 +0,0 @@
#include "DeckLinkSession.h"
#include "GlRenderConstants.h"
#include <atlbase.h>
#include <cstdio>
#include <cstring>
#include <new>
#include <sstream>
#include <vector>
namespace
{
std::string BstrToUtf8(BSTR value)
{
if (value == nullptr)
return std::string();
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
if (requiredBytes <= 1)
return std::string();
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
return std::string();
return std::string(utf8Name.data());
}
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (input == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = input->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoInputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (output == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
}
DeckLinkSession::~DeckLinkSession()
{
ReleaseResources();
}
void DeckLinkSession::ReleaseResources()
{
if (input != nullptr)
input->SetCallback(nullptr);
captureDelegate.Release();
input.Release();
if (output != nullptr)
output->SetScheduledFrameCompletionCallback(nullptr);
if (keyer != nullptr)
{
keyer->Disable();
externalKeyingActive = false;
}
keyer.Release();
playoutDelegate.Release();
outputVideoFrameQueue.clear();
output.Release();
}
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
CComPtr<IDeckLinkIterator> deckLinkIterator;
CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode;
inputDisplayModeName = videoModes.input.displayName;
outputDisplayModeName = videoModes.output.displayName;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
if (FAILED(result))
{
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
return false;
}
CComPtr<IDeckLink> deckLink;
while (deckLinkIterator->Next(&deckLink) == S_OK)
{
int64_t duplexMode;
bool deviceSupportsInternalKeying = false;
bool deviceSupportsExternalKeying = false;
std::string modelName;
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
{
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
deckLink.Release();
continue;
}
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
BOOL attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
deviceSupportsInternalKeying = (attributeFlag != FALSE);
attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
deviceSupportsExternalKeying = (attributeFlag != FALSE);
CComBSTR modelNameBstr;
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
modelName = BstrToUtf8(modelNameBstr);
if (result != S_OK || duplexMode == bmdDuplexInactive)
{
deckLink.Release();
continue;
}
bool inputUsed = false;
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
inputUsed = true;
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
{
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
output.Release();
else
{
outputModelName = modelName;
supportsInternalKeying = deviceSupportsInternalKeying;
supportsExternalKeying = deviceSupportsExternalKeying;
}
}
deckLink.Release();
if (output && input)
break;
}
if (!output)
{
error = "Expected an Output DeckLink device";
ReleaseResources();
return false;
}
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
{
error = "Cannot get input Display Mode Iterator.";
ReleaseResources();
return false;
}
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
{
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
ReleaseResources();
return false;
}
inputDisplayModeIterator.Release();
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
{
error = "Cannot get output Display Mode Iterator.";
ReleaseResources();
return false;
}
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
{
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
ReleaseResources();
return false;
}
outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
inputFrameSize = inputMode
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
: outputFrameSize;
if (!input)
inputDisplayModeName = "No input - black frame";
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
inputFrameRowBytes = inputFrameSize.width * 2u;
outputFrameRowBytes = outputFrameSize.width * 4u;
captureTextureWidth = inputFrameSize.width / 2u;
outputPackTextureWidth = outputFrameSize.width;
return true;
}
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error)
{
if (!output)
{
error = "Expected an Output DeckLink device";
return false;
}
formatStatusMessage.clear();
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
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. ";
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. ";
int deckLinkOutputRowBytes = 0;
if (output->RowBytesForPixelFormat(OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA, 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;
if (InputIsTenBit())
{
int deckLinkInputRowBytes = 0;
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
else
inputFrameRowBytes = MinimumV210RowBytes(inputFrameSize.width);
}
else
{
inputFrameRowBytes = inputFrameSize.width * 2u;
}
captureTextureWidth = InputIsTenBit()
? PackedTextureWidthFromRowBytes(inputFrameRowBytes)
: 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();
return true;
}
bool DeckLinkSession::ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error)
{
(void)hdc;
(void)hglrc;
if (!input)
{
hasNoInputSource = true;
inputDisplayModeName = "No input - black frame";
return true;
}
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(inputPixelFormat);
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
if (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;
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")
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
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";
return true;
}
input_enabled:
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(owner));
if (captureDelegate == nullptr)
{
error = "DeckLink input setup failed while creating the capture callback.";
return false;
}
if (input->SetCallback(captureDelegate) != S_OK)
{
error = "DeckLink input setup failed while installing the capture callback.";
return false;
}
return true;
}
bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
(void)hdc;
(void)hglrc;
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
{
error = "DeckLink output setup failed while enabling video output.";
return false;
}
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
keyerInterfaceAvailable = true;
if (externalKeyingEnabled)
{
if (!supportsExternalKeying)
{
statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
}
else if (!keyerInterfaceAvailable)
{
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.";
}
else
{
externalKeyingActive = true;
statusMessage = "External keying is active on the selected DeckLink output.";
}
}
else if (supportsExternalKeying)
{
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)
{
error = "DeckLink output setup failed while creating an output video frame.";
return false;
}
outputVideoFrameQueue.push_back(outputFrame);
}
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(owner));
if (playoutDelegate == nullptr)
{
error = "DeckLink output setup failed while creating the playout callback.";
return false;
}
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
{
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
return false;
}
if (!formatStatusMessage.empty())
statusMessage = statusMessage.empty() ? formatStatusMessage : formatStatusMessage + " " + statusMessage;
return true;
}
double DeckLinkSession::FrameBudgetMilliseconds() const
{
return frameTimescale != 0
? (static_cast<double>(frameDuration) * 1000.0) / static_cast<double>(frameTimescale)
: 0.0;
}
IDeckLinkMutableVideoFrame* DeckLinkSession::RotateOutputFrame()
{
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)
return false;
totalPlayoutFrames++;
return true;
}
bool DeckLinkSession::Start()
{
totalPlayoutFrames = 0;
if (!output)
{
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameQueue.empty())
{
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
for (unsigned i = 0; i < kPrerollFrameCount; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
{
MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
{
MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
void* pFrame;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK)
{
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
totalPlayoutFrames++;
}
if (input)
{
if (input->StartStreams() != S_OK)
{
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
}
if (output->StartScheduledPlayback(0, frameTimescale, 1.0) != S_OK)
{
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
bool DeckLinkSession::Stop()
{
if (keyer != nullptr)
{
keyer->Disable();
externalKeyingActive = false;
}
if (input)
{
input->StopStreams();
input->DisableVideoInput();
}
if (output)
{
output->StopScheduledPlayback(0, NULL, 0);
output->DisableVideoOutput();
}
return true;
}

View File

@@ -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;
};

View File

@@ -1,14 +1,14 @@
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h"
#include "OpenGLComposite.h" #include "OpenGLComposite.h"
#include "GLExtensions.h" #include "GLExtensions.h"
#include "GlRenderConstants.h" #include "GlRenderConstants.h"
#include "OpenGLDeckLinkBridge.h"
#include "OpenGLRenderPass.h" #include "OpenGLRenderPass.h"
#include "OpenGLRenderPipeline.h"
#include "OpenGLShaderPrograms.h" #include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h" #include "PngScreenshotWriter.h"
#include "RuntimeServices.h" #include "RuntimeServices.h"
#include "ShaderBuildQueue.h" #include "ShaderBuildQueue.h"
#include "VideoIOBackendFactory.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
@@ -22,23 +22,26 @@
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC), hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mDeckLink(std::make_unique<DeckLinkSession>()),
mRenderer(std::make_unique<OpenGLRenderer>()), mRenderer(std::make_unique<OpenGLRenderer>()),
mUseCommittedLayerStates(false), mUseCommittedLayerStates(false),
mScreenshotRequested(false) mScreenshotRequested(false)
{ {
InitializeCriticalSection(&pMutex); InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>(); mRuntimeHost = std::make_unique<RuntimeHost>();
mDeckLinkBridge = std::make_unique<OpenGLDeckLinkBridge>( mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
*mDeckLink,
*mRenderer, *mRenderer,
*mRuntimeHost, *mRuntimeHost,
pMutex,
hGLDC,
hGLRC,
[this]() { renderEffect(); }, [this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); }, [this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(); }); [this]() { paintGL(); });
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
nullptr,
*mRenderer,
*mRenderPipeline,
*mRuntimeHost,
pMutex,
hGLDC,
hGLRC);
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer); mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost); mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost); mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
@@ -51,15 +54,15 @@ OpenGLComposite::~OpenGLComposite()
mRuntimeServices->Stop(); mRuntimeServices->Stop();
if (mShaderBuildQueue) if (mShaderBuildQueue)
mShaderBuildQueue->Stop(); mShaderBuildQueue->Stop();
mDeckLink->ReleaseResources(); if (mVideoIO)
mVideoIO->ReleaseResources();
mRenderer->DestroyResources(); mRenderer->DestroyResources();
DeleteCriticalSection(&pMutex); DeleteCriticalSection(&pMutex);
} }
bool OpenGLComposite::InitDeckLink() bool OpenGLComposite::InitializeVideoIO()
{ {
VideoFormatSelection videoModes;
std::string initFailureReason; std::string initFailureReason;
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty()) if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
@@ -72,30 +75,31 @@ bool OpenGLComposite::InitDeckLink()
} }
} }
if (mRuntimeHost) if (!mRuntimeHost)
{ {
if (!ResolveConfiguredVideoFormats( initFailureReason = "Runtime host is not available.";
mRuntimeHost->GetInputVideoFormat(), MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
mRuntimeHost->GetInputFrameRate(), return false;
mRuntimeHost->GetOutputVideoFormat(),
mRuntimeHost->GetOutputFrameRate(),
videoModes,
initFailureReason))
{
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
return false;
}
} }
if (!mDeckLink->DiscoverDevicesAndModes(videoModes, initFailureReason)) const VideoIOConfiguration videoIOConfig = mRuntimeHost->GetVideoIOConfiguration();
mVideoIO = CreateVideoIODevice(videoIOConfig.backendId, initFailureReason);
if (!mVideoIO)
{
MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
return false;
}
mVideoIOBridge->SetVideoIODevice(mVideoIO.get());
if (!mVideoIO->DiscoverDevicesAndModes(videoIOConfig, initFailureReason))
{ {
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application." const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
? "This application requires the DeckLink drivers installed." ? "This application requires the selected video I/O drivers installed."
: "DeckLink initialization failed"; : "Video I/O initialization failed";
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR); MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false; return false;
} }
if (!mDeckLink->SelectPreferredFormats(videoModes, initFailureReason)) if (!mVideoIO->SelectPreferredFormats(videoIOConfig, initFailureReason))
goto error; goto error;
if (! CheckOpenGLExtensions()) if (! CheckOpenGLExtensions())
@@ -110,50 +114,53 @@ bool OpenGLComposite::InitDeckLink()
goto error; goto error;
} }
PublishDeckLinkOutputStatus(mDeckLink->OutputModelName().empty() PublishVideoIOStatus(mVideoIO->DeviceName().empty()
? "DeckLink output device selected." ? "Video I/O output device selected."
: ("Selected output device: " + mDeckLink->OutputModelName())); : ("Selected output device: " + mVideoIO->DeviceName()));
// Resize window to match output video frame, but scale large formats down by half for viewing. // Resize window to match output video frame, but scale large formats down by half for viewing.
if (mDeckLink->OutputFrameWidth() < 1920) if (mVideoIO->OutputFrameWidth() < 1920)
resizeWindow(mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
else 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); }, initFailureReason))
{ {
goto error; 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); }, initFailureReason))
{ {
goto error; goto error;
} }
PublishDeckLinkOutputStatus(mDeckLink->StatusMessage()); PublishVideoIOStatus(mVideoIO->StatusMessage());
return true; return true;
error: error:
if (!initFailureReason.empty()) if (!initFailureReason.empty())
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR); MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
mDeckLink->ReleaseResources(); mVideoIO->ReleaseResources();
return false; return false;
} }
void OpenGLComposite::paintGL() void OpenGLComposite::paintGL()
{ {
if (!mVideoIO)
return;
if (!TryEnterCriticalSection(&pMutex)) if (!TryEnterCriticalSection(&pMutex))
{ {
ValidateRect(hGLWnd, NULL); ValidateRect(hGLWnd, NULL);
return; return;
} }
mRenderer->PresentToWindow(hGLDC, mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
ValidateRect(hGLWnd, NULL); ValidateRect(hGLWnd, NULL);
LeaveCriticalSection(&pMutex); LeaveCriticalSection(&pMutex);
} }
@@ -174,22 +181,15 @@ void OpenGLComposite::resizeWindow(int width, int height)
} }
} }
void OpenGLComposite::PublishDeckLinkOutputStatus(const std::string& statusMessage) void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
{ {
if (!mRuntimeHost) if (!mRuntimeHost || !mVideoIO)
return; return;
if (!statusMessage.empty()) if (!statusMessage.empty())
mDeckLink->SetStatusMessage(statusMessage); mVideoIO->SetStatusMessage(statusMessage);
mRuntimeHost->SetDeckLinkOutputStatus( mRuntimeHost->SetVideoIOStatus(mVideoIO->State());
mDeckLink->OutputModelName(),
mDeckLink->SupportsInternalKeying(),
mDeckLink->SupportsExternalKeying(),
mDeckLink->KeyerInterfaceAvailable(),
mRuntimeHost->ExternalKeyingEnabled(),
mDeckLink->ExternalKeyingActive(),
mDeckLink->StatusMessage());
} }
bool OpenGLComposite::InitOpenGLState() bool OpenGLComposite::InitOpenGLState()
@@ -223,7 +223,7 @@ bool OpenGLComposite::InitOpenGLState()
return false; 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); MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
return false; return false;
@@ -234,12 +234,12 @@ bool OpenGLComposite::InitOpenGLState()
std::string rendererError; std::string rendererError;
if (!mRenderer->InitializeResources( if (!mRenderer->InitializeResources(
mDeckLink->InputFrameWidth(), mVideoIO->InputFrameWidth(),
mDeckLink->InputFrameHeight(), mVideoIO->InputFrameHeight(),
mDeckLink->CaptureTextureWidth(), mVideoIO->CaptureTextureWidth(),
mDeckLink->OutputFrameWidth(), mVideoIO->OutputFrameWidth(),
mDeckLink->OutputFrameHeight(), mVideoIO->OutputFrameHeight(),
mDeckLink->OutputPackTextureWidth(), mVideoIO->OutputPackTextureWidth(),
rendererError)) rendererError))
{ {
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK); MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
@@ -251,20 +251,9 @@ bool OpenGLComposite::InitOpenGLState()
return true; 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() bool OpenGLComposite::Start()
{ {
return mDeckLink->Start(); return mVideoIO->Start();
} }
bool OpenGLComposite::Stop() bool OpenGLComposite::Stop()
@@ -272,10 +261,10 @@ bool OpenGLComposite::Stop()
if (mRuntimeServices) if (mRuntimeServices)
mRuntimeServices->Stop(); mRuntimeServices->Stop();
const bool wasExternalKeyingActive = mDeckLink->ExternalKeyingActive(); const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
mDeckLink->Stop(); mVideoIO->Stop();
if (wasExternalKeyingActive) if (wasExternalKeyingActive)
PublishDeckLinkOutputStatus("External keying has been disabled."); PublishVideoIOStatus("External keying has been disabled.");
return true; return true;
} }
@@ -303,7 +292,7 @@ void OpenGLComposite::renderEffect()
{ {
ProcessRuntimePollResults(); ProcessRuntimePollResults();
const bool hasInputSource = mDeckLink->HasInputSource(); const bool hasInputSource = mVideoIO->HasInputSource();
std::vector<RuntimeRenderState> layerStates; std::vector<RuntimeRenderState> layerStates;
if (mUseCommittedLayerStates) if (mUseCommittedLayerStates)
{ {
@@ -313,24 +302,44 @@ void OpenGLComposite::renderEffect()
} }
else if (mRuntimeHost) else if (mRuntimeHost)
{ {
if (mRuntimeHost->TryGetLayerRenderStates(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), layerStates)) const unsigned renderWidth = mVideoIO->InputFrameWidth();
{ const unsigned renderHeight = mVideoIO->InputFrameHeight();
mCachedLayerRenderStates = layerStates; const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
} const bool renderStateCacheValid =
else !mCachedLayerRenderStates.empty() &&
mCachedRenderStateVersion == renderStateVersion &&
mCachedRenderStateWidth == renderWidth &&
mCachedRenderStateHeight == renderHeight;
if (renderStateCacheValid)
{ {
layerStates = mCachedLayerRenderStates; layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates); mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
} }
else
{
if (mRuntimeHost->TryGetLayerRenderStates(renderWidth, renderHeight, layerStates))
{
mCachedLayerRenderStates = layerStates;
mCachedRenderStateVersion = renderStateVersion;
mCachedRenderStateWidth = renderWidth;
mCachedRenderStateHeight = renderHeight;
}
else
{
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
}
} }
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0; const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
mRenderPass->Render( mRenderPass->Render(
hasInputSource, hasInputSource,
layerStates, layerStates,
mDeckLink->InputFrameWidth(), mVideoIO->InputFrameWidth(),
mDeckLink->InputFrameHeight(), mVideoIO->InputFrameHeight(),
mDeckLink->CaptureTextureWidth(), mVideoIO->CaptureTextureWidth(),
mDeckLink->InputPixelFormat(), mVideoIO->InputPixelFormat(),
historyCap, historyCap,
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) { [this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error); return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
@@ -345,8 +354,8 @@ void OpenGLComposite::ProcessScreenshotRequest()
if (!mScreenshotRequested.exchange(false)) if (!mScreenshotRequested.exchange(false))
return; return;
const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0; const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0; const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
if (width == 0 || height == 0) if (width == 0 || height == 0)
return; return;
@@ -426,7 +435,7 @@ bool OpenGLComposite::ProcessRuntimePollResults()
return true; return true;
char compilerErrorMessage[1024] = {}; 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); mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
mUseCommittedLayerStates = true; mUseCommittedLayerStates = true;
@@ -449,13 +458,13 @@ bool OpenGLComposite::ProcessRuntimePollResults()
void OpenGLComposite::RequestShaderBuild() void OpenGLComposite::RequestShaderBuild()
{ {
if (!mShaderBuildQueue || !mDeckLink) if (!mShaderBuildQueue || !mVideoIO)
return; return;
mUseCommittedLayerStates = true; mUseCommittedLayerStates = true;
if (mRuntimeHost) if (mRuntimeHost)
mRuntimeHost->ClearReloadRequest(); mRuntimeHost->ClearReloadRequest();
mShaderBuildQueue->RequestBuild(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight()); mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
} }
void OpenGLComposite::broadcastRuntimeState() void OpenGLComposite::broadcastRuntimeState()

View File

@@ -10,7 +10,6 @@
#include <objbase.h> #include <objbase.h>
#include <atlbase.h> #include <atlbase.h>
#include <comutil.h> #include <comutil.h>
#include "DeckLinkAPI_h.h"
#include "GLExtensions.h" #include "GLExtensions.h"
#include "OpenGLRenderer.h" #include "OpenGLRenderer.h"
@@ -25,9 +24,10 @@
#include <vector> #include <vector>
#include <deque> #include <deque>
class DeckLinkSession; class VideoIODevice;
class OpenGLDeckLinkBridge; class OpenGLVideoIOBridge;
class OpenGLRenderPass; class OpenGLRenderPass;
class OpenGLRenderPipeline;
class OpenGLShaderPrograms; class OpenGLShaderPrograms;
class RuntimeServices; class RuntimeServices;
class ShaderBuildQueue; class ShaderBuildQueue;
@@ -39,7 +39,7 @@ public:
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC); OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
~OpenGLComposite(); ~OpenGLComposite();
bool InitDeckLink(); bool InitializeVideoIO();
bool Start(); bool Start();
bool Stop(); bool Stop();
bool ReloadShader(); bool ReloadShader();
@@ -65,13 +65,10 @@ public:
void resizeGL(WORD width, WORD height); void resizeGL(WORD width, WORD height);
void paintGL(); void paintGL();
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
private: private:
void resizeWindow(int width, int height); void resizeWindow(int width, int height);
bool CheckOpenGLExtensions(); bool CheckOpenGLExtensions();
void PublishDeckLinkOutputStatus(const std::string& statusMessage); void PublishVideoIOStatus(const std::string& statusMessage);
using LayerProgram = OpenGLRenderer::LayerProgram; using LayerProgram = OpenGLRenderer::LayerProgram;
HWND hGLWnd; HWND hGLWnd;
@@ -79,15 +76,19 @@ private:
HGLRC hGLRC; HGLRC hGLRC;
CRITICAL_SECTION pMutex; CRITICAL_SECTION pMutex;
std::unique_ptr<DeckLinkSession> mDeckLink; std::unique_ptr<VideoIODevice> mVideoIO;
std::unique_ptr<OpenGLRenderer> mRenderer; std::unique_ptr<OpenGLRenderer> mRenderer;
std::unique_ptr<RuntimeHost> mRuntimeHost; std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<OpenGLDeckLinkBridge> mDeckLinkBridge; std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
std::unique_ptr<OpenGLRenderPass> mRenderPass; std::unique_ptr<OpenGLRenderPass> mRenderPass;
std::unique_ptr<OpenGLRenderPipeline> mRenderPipeline;
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms; std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue; std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
std::unique_ptr<RuntimeServices> mRuntimeServices; std::unique_ptr<RuntimeServices> mRuntimeServices;
std::vector<RuntimeRenderState> mCachedLayerRenderStates; std::vector<RuntimeRenderState> mCachedLayerRenderStates;
uint64_t mCachedRenderStateVersion = 0;
unsigned mCachedRenderStateWidth = 0;
unsigned mCachedRenderStateHeight = 0;
std::atomic<bool> mUseCommittedLayerStates; std::atomic<bool> mUseCommittedLayerStates;
std::atomic<bool> mScreenshotRequested; std::atomic<bool> mScreenshotRequested;

View File

@@ -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);
}

View File

@@ -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;
};

View File

@@ -1,169 +0,0 @@
#include "OpenGLRenderPass.h"
#include "GlRenderConstants.h"
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
mRenderer(renderer)
{
}
void OpenGLRenderPass::Render(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (hasInputSource)
{
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
}
else
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
if (layerStates.empty() || layerPrograms.empty())
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
}
else
{
GLuint sourceTexture = mRenderer.DecodedTexture();
GLuint sourceFrameBuffer = mRenderer.DecodeFramebuffer();
for (std::size_t index = 0; index < layerStates.size() && index < layerPrograms.size(); ++index)
{
const std::size_t remaining = layerStates.size() - index;
const bool writeToMain = (remaining % 2) == 1;
RenderShaderProgram(
sourceTexture,
writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(),
layerPrograms[index],
layerStates[index],
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
if (layerStates[index].temporalHistorySource == TemporalHistorySource::PreLayerInput)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(layerStates[index].layerId, sourceFrameBuffer, inputFrameWidth, inputFrameHeight);
sourceTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
sourceFrameBuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
}
}
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.DecodeProgram());
const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution");
const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution");
const GLint inputPixelFormatLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uInputPixelFormat");
if (packedResolutionLocation >= 0)
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
if (decodedResolutionLocation >= 0)
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
if (inputPixelFormatLocation >= 0)
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
void OpenGLRenderPass::RenderShaderProgram(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
LayerProgram& layerProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings)
{
std::string textError;
if (!updateTextBinding(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, sourceTexture);
mRenderer.TemporalHistory().BindSamplers(state, sourceTexture, historyCap);
BindLayerTextureAssets(layerProgram);
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(layerProgram.program);
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
UnbindLayerTextureAssets(layerProgram, historyCap);
}
void OpenGLRenderPass::BindLayerTextureAssets(const LayerProgram& layerProgram)
{
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < layerProgram.textureBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textureBindings[index].texture);
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(layerProgram.textureBindings.size());
for (std::size_t index = 0; index < layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + textTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, layerProgram.textBindings[index].texture);
}
glActiveTexture(GL_TEXTURE0);
}
void OpenGLRenderPass::UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap)
{
for (unsigned index = 0; index < historyCap; ++index)
{
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + historyCap + index);
glBindTexture(GL_TEXTURE_2D, 0);
}
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < layerProgram.textureBindings.size() + layerProgram.textBindings.size(); ++index)
{
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
glBindTexture(GL_TEXTURE_2D, 0);
}
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}

View File

@@ -1,244 +0,0 @@
#include "ShaderProgramCompiler.h"
#include "GlRenderConstants.h"
#include "GlScopedObjects.h"
#include "GlShaderSources.h"
#include <cstring>
#include <vector>
namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
return;
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
}
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mTextureBindings(textureBindings)
{
}
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::string fragmentShaderSource;
std::string loadError;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
return CompilePreparedLayerProgram(state, fragmentShaderSource, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
std::string loadError;
std::vector<LayerProgram::TextureBinding> textureBindings;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings);
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
const GLuint shaderTextureBase = state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
glUseProgram(newProgram.get());
const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput");
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index)
{
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(newProgram.get(), sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(newProgram.get(), temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
}
for (std::size_t index = 0; index < textureBindings.size(); ++index)
{
const GLint textureSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textureBindings[index].samplerName);
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(textureBindings.size());
for (std::size_t index = 0; index < textBindings.size(); ++index)
{
const GLint textSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textBindings[index].samplerName);
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
glUseProgram(0);
layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId;
layerProgram.shaderTextureBase = shaderTextureBase;
layerProgram.program = newProgram.release();
layerProgram.vertexShader = newVertexShader.release();
layerProgram.fragmentShader = newFragmentShader.release();
layerProgram.textureBindings.swap(textureBindings);
layerProgram.textBindings.swap(textBindings);
return true;
}
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kDecodeFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
mRenderer.DestroyDecodeShaderProgram();
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kOutputPackFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
glUseProgram(newProgram.get());
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
if (outputSamplerLocation >= 0)
glUniform1i(outputSamplerLocation, 0);
glUseProgram(0);
mRenderer.DestroyOutputPackShaderProgram();
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}

View File

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

View File

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

View File

@@ -0,0 +1,276 @@
#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 = mRenderer.DecodePackedResolutionLocation();
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
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 = mPassScratch;
passes.clear();
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
std::size_t descriptorCount = 0;
for (std::size_t index = 0; index < passCount; ++index)
descriptorCount += layerPrograms[index].passes.size();
passes.reserve(descriptorCount);
GLuint sourceTexture = mRenderer.DecodedTexture();
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
for (std::size_t index = 0; index < passCount; ++index)
{
const RuntimeRenderState& state = layerStates[index];
LayerProgram& layerProgram = layerPrograms[index];
if (layerProgram.passes.empty())
continue;
// Preserve the original two-target layer ping-pong. Intermediate passes
// inside this layer are routed through pooled temporary targets instead.
const std::size_t remaining = layerStates.size() - index;
const bool writeToMain = (remaining % 2) == 1;
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
const GLuint layerInputTexture = sourceTexture;
const GLuint layerInputFramebuffer = sourceFramebuffer;
GLuint previousPassTexture = layerInputTexture;
GLuint previousPassFramebuffer = layerInputFramebuffer;
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
std::size_t temporaryTargetIndex = 0;
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
{
PassProgram& passProgram = layerProgram.passes[passIndex];
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
GLuint passSourceTexture = previousPassTexture;
GLuint passSourceFramebuffer = previousPassFramebuffer;
if (!passProgram.inputNames.empty())
{
// v1 multipass uses the first declared input as gVideoInput.
// Later inputs are parsed for forward compatibility.
const std::string& inputName = passProgram.inputNames.front();
if (inputName == "layerInput")
{
passSourceTexture = layerInputTexture;
passSourceFramebuffer = layerInputFramebuffer;
}
else if (inputName == "previousPass")
{
passSourceTexture = previousPassTexture;
passSourceFramebuffer = previousPassFramebuffer;
}
else
{
auto namedOutputIt = namedOutputs.find(inputName);
if (namedOutputIt != namedOutputs.end())
{
passSourceTexture = namedOutputIt->second.first;
passSourceFramebuffer = namedOutputIt->second.second;
}
}
}
GLuint passDestinationTexture = layerOutputTexture;
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
RenderPassOutputTarget outputTarget = layerOutputTarget;
if (!writesLayerOutput)
{
// Temporary targets are reserved when the shader stack is
// committed, avoiding texture allocation during playback.
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
{
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
++temporaryTargetIndex;
passDestinationTexture = temporaryTarget.texture;
passDestinationFramebuffer = temporaryTarget.framebuffer;
outputTarget = RenderPassOutputTarget::Temporary;
}
}
RenderPassDescriptor pass;
pass.kind = RenderPassKind::LayerEffect;
pass.outputTarget = outputTarget;
pass.passIndex = passes.size();
pass.passId = passProgram.passId;
pass.layerId = state.layerId;
pass.shaderId = state.shaderId;
pass.sourceTexture = passSourceTexture;
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
pass.destinationTexture = passDestinationTexture;
pass.destinationFramebuffer = passDestinationFramebuffer;
pass.layerProgram = &layerProgram;
pass.passProgram = &passProgram;
pass.layerState = &state;
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
passes.push_back(pass);
// A later pass can reference either the explicit output name or the
// pass id, which keeps small manifests pleasant to write.
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
previousPassTexture = passDestinationTexture;
previousPassFramebuffer = passDestinationFramebuffer;
}
sourceTexture = layerOutputTexture;
sourceFramebuffer = layerOutputFramebuffer;
}
return passes;
}
void OpenGLRenderPass::RenderLayerPass(
const RenderPassDescriptor& pass,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
if (pass.passProgram == nullptr || pass.layerState == nullptr)
return;
RenderShaderProgram(
pass.sourceTexture,
pass.destinationFramebuffer,
*pass.passProgram,
*pass.layerState,
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
if (pass.capturePreLayerHistory)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderShaderProgram(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
{
std::string textError;
if (!updateTextBinding(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(passProgram.program);
// The UBO is shared by every pass in a layer; texture routing is what
// changes from pass to pass.
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
}

View File

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

View File

@@ -0,0 +1,279 @@
#include "OpenGLRenderPipeline.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "VideoIOFormat.h"
#include <cstring>
#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)
{
}
OpenGLRenderPipeline::~OpenGLRenderPipeline()
{
ResetAsyncReadbackState();
}
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 = mRenderer.OutputPackResolutionLocation();
const GLint activeWordsLocation = mRenderer.OutputPackActiveWordsLocation();
const GLint packFormatLocation = mRenderer.OutputPackFormatLocation();
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);
}
bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
{
if (requiredBytes == 0)
return false;
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
return true;
ResetAsyncReadbackState();
mAsyncReadbackBytes = requiredBytes;
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
glGenBuffers(1, &slot.pixelPackBuffer);
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
slot.sizeBytes = requiredBytes;
slot.inFlight = false;
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
return true;
}
void OpenGLRenderPipeline::ResetAsyncReadbackState()
{
FlushAsyncReadbackPipeline();
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
slot.sizeBytes = 0;
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
{
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
if (slot.pixelPackBuffer != 0)
{
glDeleteBuffers(1, &slot.pixelPackBuffer);
slot.pixelPackBuffer = 0;
}
}
}
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
mAsyncReadbackBytes = 0;
}
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
{
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
if (slot.fence != nullptr)
{
glDeleteSync(slot.fence);
slot.fence = nullptr;
}
slot.inFlight = false;
}
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
}
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
{
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
const GLenum format = usePackedOutput ? GL_RGBA : GL_BGRA;
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
if (requiredBytes == 0)
return;
if (mAsyncReadbackBytes != requiredBytes
|| mAsyncReadbackFormat != format
|| mAsyncReadbackType != type
|| mAsyncReadbackFramebuffer != framebuffer)
{
mAsyncReadbackFormat = format;
mAsyncReadbackType = type;
mAsyncReadbackFramebuffer = framebuffer;
if (!EnsureAsyncReadbackBuffers(requiredBytes))
return;
}
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
if (slot.fence != nullptr)
{
glDeleteSync(slot.fence);
slot.fence = nullptr;
}
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
slot.inFlight = slot.fence != nullptr;
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
}
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
{
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
return false;
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackReadIndex];
if (!slot.inFlight || slot.fence == nullptr || slot.pixelPackBuffer == 0)
return false;
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
return false;
glDeleteSync(slot.fence);
slot.fence = nullptr;
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
if (mappedBytes == nullptr)
{
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
slot.inFlight = false;
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
return false;
}
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
slot.inFlight = false;
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
CacheOutputFrame(outputFrame);
return true;
}
void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFrame)
{
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
return;
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
mCachedOutputFrame.resize(byteCount);
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
}
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
{
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (usePackedOutput)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
}
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, destinationBytes);
}
}
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
{
if (TryConsumeAsyncReadback(outputFrame, 500000))
{
QueueAsyncReadback(state);
return;
}
// If async readback misses the playout deadline, prefer a fresh synchronous
// frame over reusing stale cached output, then restart the async pipeline.
if (outputFrame.bytes != nullptr)
{
ReadOutputFrameSynchronously(state, outputFrame.bytes);
CacheOutputFrame(outputFrame);
}
FlushAsyncReadbackPipeline();
QueueAsyncReadback(state);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include "GLExtensions.h"
#include "VideoIOTypes.h"
#include <array>
#include <functional>
#include <vector>
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);
~OpenGLRenderPipeline();
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
private:
struct AsyncReadbackSlot
{
GLuint pixelPackBuffer = 0;
GLsync fence = nullptr;
std::size_t sizeBytes = 0;
bool inFlight = false;
};
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
void ResetAsyncReadbackState();
void FlushAsyncReadbackPipeline();
void QueueAsyncReadback(const VideoIOState& state);
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
void PackOutputFor10Bit(const VideoIOState& state);
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
RenderEffectCallback mRenderEffect;
OutputReadyCallback mOutputReady;
PaintCallback mPaint;
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
std::size_t mAsyncReadbackWriteIndex = 0;
std::size_t mAsyncReadbackReadIndex = 0;
std::size_t mAsyncReadbackBytes = 0;
GLenum mAsyncReadbackFormat = GL_BGRA;
GLenum mAsyncReadbackType = GL_UNSIGNED_INT_8_8_8_8_REV;
GLuint mAsyncReadbackFramebuffer = 0;
std::vector<unsigned char> mCachedOutputFrame;
};

View File

@@ -0,0 +1,135 @@
#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::SetVideoIODevice(VideoIODevice* videoIO)
{
mVideoIO = videoIO;
}
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)
{
if (mVideoIO == nullptr)
return;
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)
{
if (mVideoIO == nullptr)
return;
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);
}

View File

@@ -0,0 +1,46 @@
#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 SetVideoIODevice(VideoIODevice* videoIO);
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;
};

View File

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

View File

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

View File

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

View File

@@ -62,6 +62,8 @@ PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLDELETEBUFFERSPROC glDeleteBuffers; PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLBINDBUFFERPROC glBindBuffer; PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBUFFERDATAPROC glBufferData; PFNGLBUFFERDATAPROC glBufferData;
PFNGLMAPBUFFERPROC glMapBuffer;
PFNGLUNMAPBUFFERPROC glUnmapBuffer;
PFNGLBUFFERSUBDATAPROC glBufferSubData; PFNGLBUFFERSUBDATAPROC glBufferSubData;
PFNGLBINDBUFFERBASEPROC glBindBufferBase; PFNGLBINDBUFFERBASEPROC glBindBufferBase;
PFNGLACTIVETEXTUREPROC glActiveTexture; PFNGLACTIVETEXTUREPROC glActiveTexture;
@@ -131,6 +133,8 @@ bool ResolveGLExtensions()
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers"); glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer"); glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData"); glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
glMapBuffer = (PFNGLMAPBUFFERPROC) wglGetProcAddress("glMapBuffer");
glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) wglGetProcAddress("glUnmapBuffer");
glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData"); glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData");
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase"); glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase");
glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture"); glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture");
@@ -176,6 +180,8 @@ bool ResolveGLExtensions()
&& glDeleteBuffers && glDeleteBuffers
&& glBindBuffer && glBindBuffer
&& glBufferData && glBufferData
&& glMapBuffer
&& glUnmapBuffer
&& glBufferSubData && glBufferSubData
&& glBindBufferBase && glBindBufferBase
&& glActiveTexture && glActiveTexture

View File

@@ -89,6 +89,11 @@
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160 #define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001 #define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_ALREADY_SIGNALED 0x911A
#define GL_TIMEOUT_EXPIRED 0x911B
#define GL_CONDITION_SATISFIED 0x911C
#define GL_WAIT_FAILED 0x911D
#define GL_READ_ONLY 0x88B8
typedef struct __GLsync *GLsync; typedef struct __GLsync *GLsync;
typedef unsigned __int64 GLuint64; typedef unsigned __int64 GLuint64;
@@ -100,6 +105,8 @@ typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers); typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers); typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage); typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERPROC) (GLenum target, GLenum access);
typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target);
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader); typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader); typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void); typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
@@ -159,6 +166,8 @@ extern PFNGLGENBUFFERSPROC glGenBuffers;
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers; extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
extern PFNGLBINDBUFFERPROC glBindBuffer; extern PFNGLBINDBUFFERPROC glBindBuffer;
extern PFNGLBUFFERDATAPROC glBufferData; extern PFNGLBUFFERDATAPROC glBufferData;
extern PFNGLMAPBUFFERPROC glMapBuffer;
extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
extern PFNGLBUFFERSUBDATAPROC glBufferSubData; extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase; extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
extern PFNGLACTIVETEXTUREPROC glActiveTexture; extern PFNGLACTIVETEXTUREPROC glActiveTexture;

View File

@@ -12,15 +12,6 @@ namespace
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
} }
void ConfigureDisplayFrameTexture(unsigned width, unsigned height)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
}
} }
bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error) bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error)
@@ -35,80 +26,32 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight); ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glGenTextures(1, &mDecodedTexture);
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0);
glGenTextures(1, &mLayerTempTexture);
glBindTexture(GL_TEXTURE_2D, mLayerTempTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &mDecodeFrameBuf);
glGenFramebuffers(1, &mLayerTempFrameBuf);
glGenFramebuffers(1, &mIdFrameBuf);
glGenFramebuffers(1, &mOutputFrameBuf);
glGenFramebuffers(1, &mOutputPackFrameBuf);
glGenRenderbuffers(1, &mIdColorBuf); glGenRenderbuffers(1, &mIdColorBuf);
glGenRenderbuffers(1, &mIdDepthBuf); glGenRenderbuffers(1, &mIdDepthBuf);
glGenVertexArrays(1, &mFullscreenVAO); glGenVertexArrays(1, &mFullscreenVAO);
glGenBuffers(1, &mGlobalParamsUBO); glGenBuffers(1, &mGlobalParamsUBO);
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf); if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize decode framebuffer.";
return false; return false;
} if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
return false;
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf); if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize layer framebuffer.";
return false; return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
glGenTextures(1, &mFBOTexture);
glBindTexture(GL_TEXTURE_2D, mFBOTexture);
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf); glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{ {
error = "Cannot initialize framebuffer."; error = "Cannot initialize framebuffer.";
return false; return false;
} }
glGenTextures(1, &mOutputTexture); if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
glBindTexture(GL_TEXTURE_2D, mOutputTexture);
ConfigureDisplayFrameTexture(outputFrameWidth, outputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize output framebuffer.";
return false; return false;
} if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
glGenTextures(1, &mOutputPackTexture);
glBindTexture(GL_TEXTURE_2D, mOutputPackTexture);
ConfigureByteFrameTexture(outputPackTextureWidth, outputFrameHeight);
glBindFramebuffer(GL_FRAMEBUFFER, mOutputPackFrameBuf);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputPackTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize output pack framebuffer.";
return false; return false;
}
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0); glBindRenderbuffer(GL_RENDERBUFFER, 0);
@@ -128,6 +71,9 @@ void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader,
mDecodeProgram = program; mDecodeProgram = program;
mDecodeVertexShader = vertexShader; mDecodeVertexShader = vertexShader;
mDecodeFragmentShader = fragmentShader; mDecodeFragmentShader = fragmentShader;
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
} }
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader) void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
@@ -135,6 +81,14 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
mOutputPackProgram = program; mOutputPackProgram = program;
mOutputPackVertexShader = vertexShader; mOutputPackVertexShader = vertexShader;
mOutputPackFragmentShader = fragmentShader; mOutputPackFragmentShader = fragmentShader;
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
}
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
{
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
} }
void OpenGLRenderer::ResizeView(int width, int height) void OpenGLRenderer::ResizeView(int width, int height)
@@ -169,7 +123,7 @@ void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigne
} }
} }
glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf); glBindFramebuffer(GL_READ_FRAMEBUFFER, OutputFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST); glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, mViewWidth, mViewHeight); glViewport(0, 0, mViewWidth, mViewHeight);
@@ -186,50 +140,21 @@ void OpenGLRenderer::DestroyResources()
glDeleteVertexArrays(1, &mFullscreenVAO); glDeleteVertexArrays(1, &mFullscreenVAO);
if (mGlobalParamsUBO != 0) if (mGlobalParamsUBO != 0)
glDeleteBuffers(1, &mGlobalParamsUBO); glDeleteBuffers(1, &mGlobalParamsUBO);
if (mDecodeFrameBuf != 0)
glDeleteFramebuffers(1, &mDecodeFrameBuf);
if (mLayerTempFrameBuf != 0)
glDeleteFramebuffers(1, &mLayerTempFrameBuf);
if (mIdFrameBuf != 0)
glDeleteFramebuffers(1, &mIdFrameBuf);
if (mOutputFrameBuf != 0)
glDeleteFramebuffers(1, &mOutputFrameBuf);
if (mOutputPackFrameBuf != 0)
glDeleteFramebuffers(1, &mOutputPackFrameBuf);
if (mIdColorBuf != 0) if (mIdColorBuf != 0)
glDeleteRenderbuffers(1, &mIdColorBuf); glDeleteRenderbuffers(1, &mIdColorBuf);
if (mIdDepthBuf != 0) if (mIdDepthBuf != 0)
glDeleteRenderbuffers(1, &mIdDepthBuf); glDeleteRenderbuffers(1, &mIdDepthBuf);
if (mCaptureTexture != 0) if (mCaptureTexture != 0)
glDeleteTextures(1, &mCaptureTexture); glDeleteTextures(1, &mCaptureTexture);
if (mDecodedTexture != 0)
glDeleteTextures(1, &mDecodedTexture);
if (mLayerTempTexture != 0)
glDeleteTextures(1, &mLayerTempTexture);
if (mFBOTexture != 0)
glDeleteTextures(1, &mFBOTexture);
if (mOutputTexture != 0)
glDeleteTextures(1, &mOutputTexture);
if (mOutputPackTexture != 0)
glDeleteTextures(1, &mOutputPackTexture);
if (mTextureUploadBuffer != 0) if (mTextureUploadBuffer != 0)
glDeleteBuffers(1, &mTextureUploadBuffer); glDeleteBuffers(1, &mTextureUploadBuffer);
mRenderTargets.Destroy();
mFullscreenVAO = 0; mFullscreenVAO = 0;
mGlobalParamsUBO = 0; mGlobalParamsUBO = 0;
mDecodeFrameBuf = 0;
mLayerTempFrameBuf = 0;
mIdFrameBuf = 0;
mOutputFrameBuf = 0;
mOutputPackFrameBuf = 0;
mIdColorBuf = 0; mIdColorBuf = 0;
mIdDepthBuf = 0; mIdDepthBuf = 0;
mCaptureTexture = 0; mCaptureTexture = 0;
mDecodedTexture = 0;
mLayerTempTexture = 0;
mFBOTexture = 0;
mOutputTexture = 0;
mOutputPackTexture = 0;
mTextureUploadBuffer = 0; mTextureUploadBuffer = 0;
mGlobalParamsUBOSize = 0; mGlobalParamsUBOSize = 0;
@@ -241,43 +166,47 @@ void OpenGLRenderer::DestroyResources()
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram) void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
{ {
for (LayerProgram::TextureBinding& binding : layerProgram.textureBindings) for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
{ {
if (binding.texture != 0) for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
{ {
glDeleteTextures(1, &binding.texture); if (binding.texture != 0)
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(); layerProgram.passes.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;
}
} }
void OpenGLRenderer::DestroyLayerPrograms() void OpenGLRenderer::DestroyLayerPrograms()
@@ -294,6 +223,9 @@ void OpenGLRenderer::DestroyDecodeShaderProgram()
glDeleteProgram(mDecodeProgram); glDeleteProgram(mDecodeProgram);
mDecodeProgram = 0; mDecodeProgram = 0;
} }
mDecodePackedResolutionLocation = -1;
mDecodeDecodedResolutionLocation = -1;
mDecodeInputPixelFormatLocation = -1;
if (mDecodeFragmentShader != 0) if (mDecodeFragmentShader != 0)
{ {
@@ -315,6 +247,9 @@ void OpenGLRenderer::DestroyOutputPackShaderProgram()
glDeleteProgram(mOutputPackProgram); glDeleteProgram(mOutputPackProgram);
mOutputPackProgram = 0; mOutputPackProgram = 0;
} }
mOutputPackResolutionLocation = -1;
mOutputPackActiveWordsLocation = -1;
mOutputPackFormatLocation = -1;
if (mOutputPackFragmentShader != 0) if (mOutputPackFragmentShader != 0)
{ {

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "GLExtensions.h" #include "GLExtensions.h"
#include "RenderTargetPool.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include "TemporalHistoryBuffers.h" #include "TemporalHistoryBuffers.h"
@@ -36,35 +37,53 @@ public:
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
GLuint shaderTextureBase = 0;
GLuint program = 0; struct PassProgram
GLuint vertexShader = 0; {
GLuint fragmentShader = 0; std::string passId;
std::vector<TextureBinding> textureBindings; std::vector<std::string> inputNames;
std::vector<TextBinding> textBindings; 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 CaptureTexture() const { return mCaptureTexture; }
GLuint DecodedTexture() const { return mDecodedTexture; } GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
GLuint LayerTempTexture() const { return mLayerTempTexture; } GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
GLuint CompositeTexture() const { return mFBOTexture; } GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
GLuint OutputTexture() const { return mOutputTexture; } GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
GLuint OutputPackTexture() const { return mOutputPackTexture; } GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; } GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; } GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; } GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
GLuint CompositeFramebuffer() const { return mIdFrameBuf; } GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
GLuint OutputFramebuffer() const { return mOutputFrameBuf; } GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
GLuint OutputPackFramebuffer() const { return mOutputPackFrameBuf; } GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
GLuint FullscreenVertexArray() const { return mFullscreenVAO; } GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; } GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
GLuint DecodeProgram() const { return mDecodeProgram; } GLuint DecodeProgram() const { return mDecodeProgram; }
GLuint OutputPackProgram() const { return mOutputPackProgram; } GLuint OutputPackProgram() const { return mOutputPackProgram; }
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; } GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; } void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); } void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; } std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; } const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; } TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; } const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader); void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
@@ -80,17 +99,7 @@ public:
private: private:
GLuint mCaptureTexture = 0; GLuint mCaptureTexture = 0;
GLuint mDecodedTexture = 0;
GLuint mLayerTempTexture = 0;
GLuint mFBOTexture = 0;
GLuint mOutputTexture = 0;
GLuint mOutputPackTexture = 0;
GLuint mTextureUploadBuffer = 0; GLuint mTextureUploadBuffer = 0;
GLuint mDecodeFrameBuf = 0;
GLuint mLayerTempFrameBuf = 0;
GLuint mIdFrameBuf = 0;
GLuint mOutputFrameBuf = 0;
GLuint mOutputPackFrameBuf = 0;
GLuint mIdColorBuf = 0; GLuint mIdColorBuf = 0;
GLuint mIdDepthBuf = 0; GLuint mIdDepthBuf = 0;
GLuint mFullscreenVAO = 0; GLuint mFullscreenVAO = 0;
@@ -98,12 +107,19 @@ private:
GLuint mDecodeProgram = 0; GLuint mDecodeProgram = 0;
GLuint mDecodeVertexShader = 0; GLuint mDecodeVertexShader = 0;
GLuint mDecodeFragmentShader = 0; GLuint mDecodeFragmentShader = 0;
GLint mDecodePackedResolutionLocation = -1;
GLint mDecodeDecodedResolutionLocation = -1;
GLint mDecodeInputPixelFormatLocation = -1;
GLuint mOutputPackProgram = 0; GLuint mOutputPackProgram = 0;
GLuint mOutputPackVertexShader = 0; GLuint mOutputPackVertexShader = 0;
GLuint mOutputPackFragmentShader = 0; GLuint mOutputPackFragmentShader = 0;
GLint mOutputPackResolutionLocation = -1;
GLint mOutputPackActiveWordsLocation = -1;
GLint mOutputPackFormatLocation = -1;
GLsizeiptr mGlobalParamsUBOSize = 0; GLsizeiptr mGlobalParamsUBOSize = 0;
int mViewWidth = 0; int mViewWidth = 0;
int mViewHeight = 0; int mViewHeight = 0;
std::vector<LayerProgram> mLayerPrograms; std::vector<LayerProgram> mLayerPrograms;
RenderTargetPool mRenderTargets;
TemporalHistoryBuffers mTemporalHistory; TemporalHistoryBuffers mTemporalHistory;
}; };

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,8 @@ GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
{ {
std::vector<unsigned char> buffer; std::vector<unsigned char>& buffer = mScratchBuffer;
buffer.clear();
buffer.reserve(512); buffer.reserve(512);
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds)); AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));

View File

@@ -3,6 +3,8 @@
#include "OpenGLRenderer.h" #include "OpenGLRenderer.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include <vector>
class GlobalParamsBuffer class GlobalParamsBuffer
{ {
public: public:
@@ -12,4 +14,5 @@ public:
private: private:
OpenGLRenderer& mRenderer; OpenGLRenderer& mRenderer;
std::vector<unsigned char> mScratchBuffer;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,223 @@
#include "ShaderTextureBindings.h"
#include "GlRenderConstants.h"
#include "TextRasterizer.h"
#include "TextureAssetLoader.h"
#include <algorithm>
#include <filesystem>
namespace
{
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
auto valueIt = state.parameterValues.find(parameterId);
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
if (!definition.fontId.empty())
{
for (const ShaderFontAsset& fontAsset : state.fontAssets)
{
if (fontAsset.id == definition.fontId)
return &fontAsset;
}
}
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
}
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
return ::LoadTextureAsset(textureAsset, textureId, error);
}
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
{
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
LayerProgram::TextBinding textBinding;
textBinding.parameterId = definition.id;
textBinding.samplerName = definition.id + "Texture";
textBinding.fontId = definition.fontId;
glGenTextures(1, &textBinding.texture);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBindings.push_back(textBinding);
}
}
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
const std::string text = TextValueForBinding(state, textBinding.parameterId);
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
return true;
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
if (definitionIt == state.parameterDefinitions.end())
return true;
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
std::filesystem::path fontPath;
if (fontAsset)
fontPath = fontAsset->path;
std::vector<unsigned char> sdf;
if (!RasterizeTextSdf(text, fontPath, sdf, error))
return false;
GLint previousActiveTexture = 0;
GLint previousUnpackBuffer = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
glActiveTexture(GL_TEXTURE0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
textBinding.renderedText = text;
textBinding.renderedWidth = kTextTextureWidth;
textBinding.renderedHeight = kTextTextureHeight;
return true;
}
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
{
GLint location = glGetUniformLocation(program, samplerName.c_str());
if (location >= 0)
return location;
return glGetUniformLocation(program, (samplerName + "_0").c_str());
}
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
{
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
}
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
{
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index)
{
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
}
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
}
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const
{
RuntimeTextureBindingPlan plan;
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"sourceHistory",
"gSourceHistory" + std::to_string(index),
sourceHistoryTextures[index],
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
});
}
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"temporalHistory",
"gTemporalHistory" + std::to_string(index),
temporalHistoryTextures[index],
temporalBase + static_cast<GLuint>(index)
});
}
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
plan.bindings.push_back({
"shaderTexture",
textureBinding.samplerName,
textureBinding.texture,
shaderTextureBase + static_cast<GLuint>(index)
});
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
plan.bindings.push_back({
"textTexture",
textBinding.samplerName,
textBinding.texture,
textTextureBase + static_cast<GLuint>(index)
});
}
return plan;
}
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, binding.texture);
}
glActiveTexture(GL_TEXTURE0);
}
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
}
glActiveTexture(GL_TEXTURE0);
}

View File

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

View File

@@ -82,17 +82,6 @@ bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
return true; return true;
} }
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
{ {
return std::filesystem::exists(candidate / "config" / "runtime-host.json") && return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
@@ -629,6 +618,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error)) if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false; return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) || if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
@@ -849,6 +841,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
} }
reloadRequested = mReloadRequested; reloadRequested = mReloadRequested;
if (registryChanged || reloadRequested)
MarkRenderStateDirtyLocked();
return true; return true;
} }
catch (const std::exception& exception) catch (const std::exception& exception)
@@ -892,6 +886,7 @@ bool RuntimeHost::AddLayer(const std::string& shaderId, std::string& error)
EnsureLayerDefaultsLocked(layer, shaderIt->second); EnsureLayerDefaultsLocked(layer, shaderIt->second);
mPersistentState.layers.push_back(layer); mPersistentState.layers.push_back(layer);
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -908,6 +903,7 @@ bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error)
mPersistentState.layers.erase(it); mPersistentState.layers.erase(it);
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -929,6 +925,7 @@ bool RuntimeHost::MoveLayer(const std::string& layerId, int direction, std::stri
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]); std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -957,6 +954,7 @@ bool RuntimeHost::MoveLayerToIndex(const std::string& layerId, std::size_t targe
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex)); mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer); mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -972,6 +970,7 @@ bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std:
layer->bypass = bypassed; layer->bypass = bypassed;
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -996,6 +995,7 @@ bool RuntimeHost::SetLayerShader(const std::string& layerId, const std::string&
layer->parameterValues.clear(); layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second); EnsureLayerDefaultsLocked(*layer, shaderIt->second);
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -1032,6 +1032,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0]; const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count(); const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
value.numberValues = { previousCount + 1.0, triggerTime }; value.numberValues = { previousCount + 1.0, triggerTime };
MarkRenderStateDirtyLocked();
return true; return true;
} }
@@ -1040,6 +1041,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
return false; return false;
layer->parameterValues[parameterId] = normalized; layer->parameterValues[parameterId] = normalized;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -1087,6 +1089,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0]; const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count(); const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
value.numberValues = { previousCount + 1.0, triggerTime }; value.numberValues = { previousCount + 1.0, triggerTime };
MarkRenderStateDirtyLocked();
return true; return true;
} }
@@ -1095,6 +1098,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
return false; return false;
matchedLayer->parameterValues[parameterIt->id] = normalized; matchedLayer->parameterValues[parameterIt->id] = normalized;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -1118,6 +1122,7 @@ bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string&
layer->parameterValues.clear(); layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second); EnsureLayerDefaultsLocked(*layer, shaderIt->second);
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -1177,6 +1182,7 @@ bool RuntimeHost::LoadStackPreset(const std::string& presetName, std::string& er
mPersistentState.layers = nextLayers; mPersistentState.layers = nextLayers;
mReloadRequested = true; mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error); return SavePersistentState(error);
} }
@@ -1205,23 +1211,37 @@ bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned he
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName) void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{ {
const bool changed = mHasSignal != hasSignal ||
mSignalWidth != width ||
mSignalHeight != height ||
mSignalModeName != modeName;
mHasSignal = hasSignal; mHasSignal = hasSignal;
mSignalWidth = width; mSignalWidth = width;
mSignalHeight = height; mSignalHeight = height;
mSignalModeName = modeName; mSignalModeName = modeName;
if (changed)
MarkRenderStateDirtyLocked();
} }
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, void RuntimeHost::MarkRenderStateDirtyLocked()
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) {
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::SetVideoIOStatus(const VideoIOState& state)
{ {
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
mDeckLinkOutputStatus.modelName = modelName; mVideoIOStatus.backendId = state.backendId;
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying; mVideoIOStatus.deviceName = state.deviceName;
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying; mVideoIOStatus.capabilities = state.capabilities;
mDeckLinkOutputStatus.keyerInterfaceAvailable = keyerInterfaceAvailable; mVideoIOStatus.hasInputDevice = state.hasInputDevice;
mDeckLinkOutputStatus.externalKeyingRequested = externalKeyingRequested; mVideoIOStatus.hasInputSource = state.hasInputSource;
mDeckLinkOutputStatus.externalKeyingActive = externalKeyingActive; mVideoIOStatus.inputDisplayModeName = state.inputDisplayModeName;
mDeckLinkOutputStatus.statusMessage = statusMessage; mVideoIOStatus.outputDisplayModeName = state.outputDisplayModeName;
mVideoIOStatus.externalKeyingRequested = state.externalKeyingRequested;
mVideoIOStatus.externalKeyingActive = state.externalKeyingActive;
mVideoIOStatus.statusMessage = state.statusMessage;
mVideoIOStatus.formatStatusMessage = state.formatStatusMessage;
} }
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds) void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
@@ -1292,7 +1312,7 @@ bool RuntimeHost::TryAdvanceFrame()
return true; return true;
} }
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error) bool RuntimeHost::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error)
{ {
try try
{ {
@@ -1316,16 +1336,30 @@ bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std
} }
ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames); ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames);
return compiler.BuildLayerFragmentShaderSource(shaderPackage, fragmentShaderSource, error); // Compile every declared pass while the caller remains backend-neutral.
// The GL layer decides how the resulting pass sources are routed.
passSources.clear();
passSources.reserve(shaderPackage.passes.size());
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
ShaderPassBuildSource passSource;
passSource.passId = pass.id;
passSource.inputNames = pass.inputNames;
passSource.outputName = pass.outputName;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error))
return false;
passSources.push_back(std::move(passSource));
}
return true;
} }
catch (const std::exception& exception) catch (const std::exception& exception)
{ {
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what(); error = std::string("RuntimeHost::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
return false; return false;
} }
catch (...) catch (...)
{ {
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception."; error = "RuntimeHost::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
return false; return false;
} }
} }
@@ -1442,61 +1476,67 @@ bool RuntimeHost::LoadConfig(std::string& error)
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames)); const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue); mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
} }
if (const JsonValue* videoBackendValue = configJson.find("videoBackend"))
{
VideoIOBackendId backendId = mConfig.videoIO.backendId;
if (videoBackendValue->isString() && ParseVideoIOBackendId(videoBackendValue->asString(), backendId))
mConfig.videoIO.backendId = backendId;
}
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying); mConfig.videoIO.externalKeyingEnabled = enableExternalKeyingValue->asBoolean(mConfig.videoIO.externalKeyingEnabled);
if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
{ {
if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
{ {
mConfig.inputVideoFormat = videoFormatValue->asString(); mConfig.videoIO.inputMode.videoFormat = videoFormatValue->asString();
mConfig.outputVideoFormat = videoFormatValue->asString(); mConfig.videoIO.outputMode.videoFormat = videoFormatValue->asString();
} }
} }
if (const JsonValue* frameRateValue = configJson.find("frameRate")) if (const JsonValue* frameRateValue = configJson.find("frameRate"))
{ {
if (frameRateValue->isString() && !frameRateValue->asString().empty()) if (frameRateValue->isString() && !frameRateValue->asString().empty())
{ {
mConfig.inputFrameRate = frameRateValue->asString(); mConfig.videoIO.inputMode.frameRate = frameRateValue->asString();
mConfig.outputFrameRate = frameRateValue->asString(); mConfig.videoIO.outputMode.frameRate = frameRateValue->asString();
} }
else if (frameRateValue->isNumber()) else if (frameRateValue->isNumber())
{ {
std::ostringstream stream; std::ostringstream stream;
stream << frameRateValue->asNumber(); stream << frameRateValue->asNumber();
mConfig.inputFrameRate = stream.str(); mConfig.videoIO.inputMode.frameRate = stream.str();
mConfig.outputFrameRate = stream.str(); mConfig.videoIO.outputMode.frameRate = stream.str();
} }
} }
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat")) if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
{ {
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty()) if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
mConfig.inputVideoFormat = inputVideoFormatValue->asString(); mConfig.videoIO.inputMode.videoFormat = inputVideoFormatValue->asString();
} }
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate")) if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
{ {
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty()) if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
mConfig.inputFrameRate = inputFrameRateValue->asString(); mConfig.videoIO.inputMode.frameRate = inputFrameRateValue->asString();
else if (inputFrameRateValue->isNumber()) else if (inputFrameRateValue->isNumber())
{ {
std::ostringstream stream; std::ostringstream stream;
stream << inputFrameRateValue->asNumber(); stream << inputFrameRateValue->asNumber();
mConfig.inputFrameRate = stream.str(); mConfig.videoIO.inputMode.frameRate = stream.str();
} }
} }
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat")) if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
{ {
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty()) if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
mConfig.outputVideoFormat = outputVideoFormatValue->asString(); mConfig.videoIO.outputMode.videoFormat = outputVideoFormatValue->asString();
} }
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate")) if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
{ {
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty()) if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
mConfig.outputFrameRate = outputFrameRateValue->asString(); mConfig.videoIO.outputMode.frameRate = outputFrameRateValue->asString();
else if (outputFrameRateValue->isNumber()) else if (outputFrameRateValue->isNumber())
{ {
std::ostringstream stream; std::ostringstream stream;
stream << outputFrameRateValue->asNumber(); stream << outputFrameRateValue->asNumber();
mConfig.outputFrameRate = stream.str(); mConfig.videoIO.outputMode.frameRate = stream.str();
} }
} }
@@ -1660,42 +1700,11 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
++it; ++it;
} }
MarkRenderStateDirtyLocked();
return true; return true;
} }
bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
if (!manifestJson.isObject())
{
error = "Shader manifest root must be an object: " + manifestPath.string();
return false;
}
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false;
if (!std::filesystem::exists(shaderPackage.shaderPath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
{ {
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error); return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
@@ -1859,11 +1868,12 @@ JsonValue RuntimeHost::BuildStateValue() const
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort))); app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("autoReload", JsonValue(mAutoReloadEnabled));
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames))); app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying)); app.set("videoBackend", JsonValue(VideoIOBackendName(mConfig.videoIO.backendId)));
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat)); app.set("enableExternalKeying", JsonValue(mConfig.videoIO.externalKeyingEnabled));
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate)); app.set("inputVideoFormat", JsonValue(mConfig.videoIO.inputMode.videoFormat));
app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat)); app.set("inputFrameRate", JsonValue(mConfig.videoIO.inputMode.frameRate));
app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate)); app.set("outputVideoFormat", JsonValue(mConfig.videoIO.outputMode.videoFormat));
app.set("outputFrameRate", JsonValue(mConfig.videoIO.outputMode.frameRate));
root.set("app", app); root.set("app", app);
JsonValue runtime = JsonValue::MakeObject(); JsonValue runtime = JsonValue::MakeObject();
@@ -1879,15 +1889,23 @@ JsonValue RuntimeHost::BuildStateValue() const
video.set("modeName", JsonValue(mSignalModeName)); video.set("modeName", JsonValue(mSignalModeName));
root.set("video", video); root.set("video", video);
JsonValue deckLink = JsonValue::MakeObject(); JsonValue videoIO = JsonValue::MakeObject();
deckLink.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName)); videoIO.set("backend", JsonValue(VideoIOBackendName(mVideoIOStatus.backendId)));
deckLink.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying)); videoIO.set("deviceName", JsonValue(mVideoIOStatus.deviceName));
deckLink.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying)); videoIO.set("hasInputDevice", JsonValue(mVideoIOStatus.hasInputDevice));
deckLink.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable)); videoIO.set("hasInputSource", JsonValue(mVideoIOStatus.hasInputSource));
deckLink.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested)); videoIO.set("inputModeName", JsonValue(mVideoIOStatus.inputDisplayModeName));
deckLink.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive)); videoIO.set("outputModeName", JsonValue(mVideoIOStatus.outputDisplayModeName));
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage)); JsonValue capabilities = JsonValue::MakeObject();
root.set("decklink", deckLink); capabilities.set("supportsInternalKeying", JsonValue(mVideoIOStatus.capabilities.supportsInternalKeying));
capabilities.set("supportsExternalKeying", JsonValue(mVideoIOStatus.capabilities.supportsExternalKeying));
capabilities.set("keyerInterfaceAvailable", JsonValue(mVideoIOStatus.capabilities.keyerInterfaceAvailable));
videoIO.set("capabilities", capabilities);
videoIO.set("externalKeyingRequested", JsonValue(mVideoIOStatus.externalKeyingRequested));
videoIO.set("externalKeyingActive", JsonValue(mVideoIOStatus.externalKeyingActive));
videoIO.set("statusMessage", JsonValue(mVideoIOStatus.statusMessage));
videoIO.set("formatStatusMessage", JsonValue(mVideoIOStatus.formatStatusMessage));
root.set("videoIO", videoIO);
JsonValue performance = JsonValue::MakeObject(); JsonValue performance = JsonValue::MakeObject();
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds)); performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
@@ -1968,6 +1986,8 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
JsonValue parameter = JsonValue::MakeObject(); JsonValue parameter = JsonValue::MakeObject();
parameter.set("id", JsonValue(definition.id)); parameter.set("id", JsonValue(definition.id));
parameter.set("label", JsonValue(definition.label)); parameter.set("label", JsonValue(definition.label));
if (!definition.description.empty())
parameter.set("description", JsonValue(definition.description));
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type))); parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition))); parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));

View File

@@ -2,6 +2,7 @@
#include "RuntimeJson.h" #include "RuntimeJson.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include "VideoIOTypes.h"
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
@@ -9,6 +10,7 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <utility>
#include <vector> #include <vector>
class RuntimeHost class RuntimeHost
@@ -37,8 +39,7 @@ public:
void SetCompileStatus(bool succeeded, const std::string& message); void SetCompileStatus(bool succeeded, const std::string& message);
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, void SetVideoIOStatus(const VideoIOState& state);
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
@@ -48,11 +49,12 @@ public:
void AdvanceFrame(); void AdvanceFrame();
bool TryAdvanceFrame(); bool TryAdvanceFrame();
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error); bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const; std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const; bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const; void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
std::string BuildStateJson() const; std::string BuildStateJson() const;
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; } const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
const std::filesystem::path& GetUiRoot() const { return mUiRoot; } const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
@@ -61,11 +63,8 @@ public:
unsigned short GetServerPort() const { return mServerPort; } unsigned short GetServerPort() const { return mServerPort; }
unsigned short GetOscPort() const { return mConfig.oscPort; } unsigned short GetOscPort() const { return mConfig.oscPort; }
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; } bool ExternalKeyingEnabled() const { return mConfig.videoIO.externalKeyingEnabled; }
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; } VideoIOConfiguration GetVideoIOConfiguration() const { return mConfig.videoIO; }
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; }
void SetServerPort(unsigned short port); void SetServerPort(unsigned short port);
bool AutoReloadEnabled() const { return mAutoReloadEnabled; } bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
@@ -77,22 +76,22 @@ private:
unsigned short oscPort = 9000; unsigned short oscPort = 9000;
bool autoReload = true; bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4; unsigned maxTemporalHistoryFrames = 4;
bool enableExternalKeying = false; VideoIOConfiguration videoIO;
std::string inputVideoFormat = "1080p";
std::string inputFrameRate = "59.94";
std::string outputVideoFormat = "1080p";
std::string outputFrameRate = "59.94";
}; };
struct DeckLinkOutputStatus struct VideoIOStatusSnapshot
{ {
std::string modelName; VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
bool supportsInternalKeying = false; std::string deviceName;
bool supportsExternalKeying = false; VideoIOCapabilities capabilities;
bool keyerInterfaceAvailable = false; bool hasInputDevice = false;
bool hasInputSource = false;
std::string inputDisplayModeName = "1080p59.94";
std::string outputDisplayModeName = "1080p59.94";
bool externalKeyingRequested = false; bool externalKeyingRequested = false;
bool externalKeyingActive = false; bool externalKeyingActive = false;
std::string statusMessage; std::string statusMessage;
std::string formatStatusMessage;
}; };
struct LayerPersistentState struct LayerPersistentState
@@ -112,7 +111,6 @@ private:
bool LoadPersistentState(std::string& error); bool LoadPersistentState(std::string& error);
bool SavePersistentState(std::string& error) const; bool SavePersistentState(std::string& error) const;
bool ScanShaderPackages(std::string& error); bool ScanShaderPackages(std::string& error);
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const; bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const; ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const; void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
@@ -132,6 +130,7 @@ private:
const LayerPersistentState* FindLayerById(const std::string& layerId) const; const LayerPersistentState* FindLayerById(const std::string& layerId) const;
std::string GenerateLayerId(); std::string GenerateLayerId();
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void MarkRenderStateDirtyLocked();
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds); void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount); double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
@@ -171,11 +170,12 @@ private:
uint64_t mLateFrameCount; uint64_t mLateFrameCount;
uint64_t mDroppedFrameCount; uint64_t mDroppedFrameCount;
uint64_t mFlushedFrameCount; uint64_t mFlushedFrameCount;
DeckLinkOutputStatus mDeckLinkOutputStatus; VideoIOStatusSnapshot mVideoIOStatus;
unsigned short mServerPort; unsigned short mServerPort;
bool mAutoReloadEnabled; bool mAutoReloadEnabled;
std::chrono::steady_clock::time_point mStartTime; std::chrono::steady_clock::time_point mStartTime;
std::chrono::steady_clock::time_point mLastScanTime; std::chrono::steady_clock::time_point mLastScanTime;
std::atomic<uint64_t> mFrameCounter; std::atomic<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
uint64_t mNextLayerId; uint64_t mNextLayerId;
}; };

View File

@@ -661,3 +661,14 @@ std::string SerializeJson(const JsonValue& value, bool pretty)
SerializeJsonImpl(value, output, pretty, 0); SerializeJsonImpl(value, output, pretty, 0);
return output.str(); return output.str();
} }
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}

View File

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

View File

@@ -26,17 +26,6 @@ bool IsFiniteNumber(double value)
return std::isfinite(value) != 0; return std::isfinite(value) != 0;
} }
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
std::string NormalizeTextValue(const std::string& text, unsigned maxLength) std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
{ {
std::string normalized; std::string normalized;

View File

@@ -143,10 +143,10 @@ ShaderCompiler::ShaderCompiler(
{ {
} }
bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
{ {
std::string wrapperSource; std::string wrapperSource;
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error)) if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
return false; return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error)) if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false; return false;
@@ -167,7 +167,7 @@ bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderP
return true; return true;
} }
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
{ {
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in"; const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
wrapperSource = ReadTextFile(templatePath, error); wrapperSource = ReadTextFile(templatePath, error);
@@ -183,13 +183,25 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters)); wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount)); wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount)); wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string()); wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)"); wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
return true; return true;
} }
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{ {
char slangRootBuffer[MAX_PATH] = {};
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer)));
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
{
std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty"; std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyRoot)) if (!std::filesystem::exists(thirdPartyRoot))
{ {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
#include "VideoIOBackendFactory.h"
#include "DeckLinkSession.h"
#include "VideoIOTypes.h"
std::unique_ptr<VideoIODevice> CreateVideoIODevice(VideoIOBackendId backendId, std::string& error)
{
switch (backendId)
{
case VideoIOBackendId::DeckLink:
return std::make_unique<DeckLinkSession>();
}
error = "Unsupported video I/O backend.";
return nullptr;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "VideoIOConfig.h"
#include <memory>
#include <string>
class VideoIODevice;
std::unique_ptr<VideoIODevice> CreateVideoIODevice(VideoIOBackendId backendId, std::string& error);

View File

@@ -0,0 +1,35 @@
#include "VideoIOConfig.h"
#include <algorithm>
#include <cctype>
namespace
{
std::string NormalizeToken(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return value;
}
}
const char* VideoIOBackendName(VideoIOBackendId backendId)
{
switch (backendId)
{
case VideoIOBackendId::DeckLink:
return "decklink";
}
return "unknown";
}
bool ParseVideoIOBackendId(const std::string& value, VideoIOBackendId& backendId)
{
const std::string normalized = NormalizeToken(value);
if (normalized.empty() || normalized == "decklink")
{
backendId = VideoIOBackendId::DeckLink;
return true;
}
return false;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <string>
enum class VideoIOBackendId
{
DeckLink
};
const char* VideoIOBackendName(VideoIOBackendId backendId);
bool ParseVideoIOBackendId(const std::string& value, VideoIOBackendId& backendId);
struct FrameSize
{
unsigned width = 0;
unsigned height = 0;
bool IsEmpty() const { return width == 0 || height == 0; }
};
inline bool operator==(const FrameSize& left, const FrameSize& right)
{
return left.width == right.width && left.height == right.height;
}
inline bool operator!=(const FrameSize& left, const FrameSize& right)
{
return !(left == right);
}
struct VideoIOModeConfiguration
{
std::string videoFormat = "1080p";
std::string frameRate = "59.94";
};
struct VideoIOConfiguration
{
VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
VideoIOModeConfiguration inputMode;
VideoIOModeConfiguration outputMode;
bool externalKeyingEnabled = false;
bool preferTenBit = true;
};

View File

@@ -50,22 +50,23 @@ uint16_t Component(uint32_t word, unsigned index)
const char* VideoIOPixelFormatName(VideoIOPixelFormat format) 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) bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
{ {
return format == VideoIOPixelFormat::V210; return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
}
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
{
return format == VideoIOPixelFormat::V210 ? bmdFormat10BitYUV : bmdFormat8BitYUV;
}
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
{
return format == bmdFormat10BitYUV ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
} }
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported) VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
@@ -73,6 +74,31 @@ VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8; 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) unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
{ {
return (rowBytes + 3u) / 4u; return (rowBytes + 3u) / 4u;
@@ -83,6 +109,11 @@ unsigned MinimumV210RowBytes(unsigned frameWidth)
return ((frameWidth + 5u) / 6u) * 16u; return ((frameWidth + 5u) / 6u) * 16u;
} }
unsigned MinimumYuva10RowBytes(unsigned frameWidth)
{
return ((frameWidth + 63u) / 64u) * 256u;
}
unsigned ActiveV210WordsForWidth(unsigned frameWidth) unsigned ActiveV210WordsForWidth(unsigned frameWidth)
{ {
return ((frameWidth + 5u) / 6u) * 4u; return ((frameWidth + 5u) / 6u) * 4u;

View File

@@ -1,14 +1,14 @@
#pragma once #pragma once
#include "DeckLinkAPI_h.h"
#include <array> #include <array>
#include <cstdint> #include <cstdint>
enum class VideoIOPixelFormat enum class VideoIOPixelFormat
{ {
Uyvy8, Uyvy8,
V210 V210,
Yuva10,
Bgra8
}; };
struct V210CodeValues struct V210CodeValues
@@ -27,11 +27,12 @@ struct V210SixPixelBlock
const char* VideoIOPixelFormatName(VideoIOPixelFormat format); const char* VideoIOPixelFormatName(VideoIOPixelFormat format);
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format); bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format);
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported); VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes); unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
unsigned MinimumV210RowBytes(unsigned frameWidth); unsigned MinimumV210RowBytes(unsigned frameWidth);
unsigned MinimumYuva10RowBytes(unsigned frameWidth);
unsigned ActiveV210WordsForWidth(unsigned frameWidth); unsigned ActiveV210WordsForWidth(unsigned frameWidth);
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue); V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue);
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block); std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block);

View File

@@ -0,0 +1,134 @@
#pragma once
#include "VideoIOConfig.h"
#include "VideoIOFormat.h"
#include <cstdint>
#include <functional>
#include <string>
struct VideoIOCapabilities
{
bool supportsInternalKeying = false;
bool supportsExternalKeying = false;
bool keyerInterfaceAvailable = false;
};
enum class VideoIOCompletionResult
{
Completed,
DisplayedLate,
Dropped,
Flushed,
Unknown
};
struct VideoIOState
{
VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
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 deviceName;
std::string statusMessage;
std::string formatStatusMessage;
bool hasInputDevice = false;
bool hasInputSource = false;
VideoIOCapabilities capabilities;
bool externalKeyingRequested = 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 VideoIOBackendId BackendId() const = 0;
virtual void ReleaseResources() = 0;
virtual bool DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error) = 0;
virtual bool SelectPreferredFormats(const VideoIOConfiguration& config, std::string& error) = 0;
virtual bool ConfigureInput(InputFrameCallback callback, std::string& error) = 0;
virtual bool ConfigureOutput(OutputFrameCallback callback, 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& DeviceName() const { return State().deviceName; }
bool SupportsInternalKeying() const { return State().capabilities.supportsInternalKeying; }
bool SupportsExternalKeying() const { return State().capabilities.supportsExternalKeying; }
bool KeyerInterfaceAvailable() const { return State().capabilities.keyerInterfaceAvailable; }
bool ExternalKeyingRequested() const { return State().externalKeyingRequested; }
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; }
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -13,10 +13,10 @@ std::string NormalizeModeToken(const std::string& value)
return normalized; return normalized;
} }
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName) bool ResolveConfiguredDeckLinkDisplayMode(const VideoIOModeConfiguration& mode, BMDDisplayMode& displayMode, std::string& displayModeName)
{ {
VideoFormat videoMode; DeckLinkVideoMode videoMode;
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode)) if (!ResolveConfiguredDeckLinkVideoMode(mode, videoMode))
return false; return false;
displayMode = videoMode.displayMode; displayMode = videoMode.displayMode;
@@ -24,10 +24,10 @@ bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::str
return true; return true;
} }
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode) bool ResolveConfiguredDeckLinkVideoMode(const VideoIOModeConfiguration& mode, DeckLinkVideoMode& videoMode)
{ {
const std::string formatToken = NormalizeModeToken(videoFormat); const std::string formatToken = NormalizeModeToken(mode.videoFormat);
const std::string frameToken = NormalizeModeToken(frameRate); const std::string frameToken = NormalizeModeToken(mode.frameRate);
const std::string combinedToken = formatToken + frameToken; const std::string combinedToken = formatToken + frameToken;
struct ModeOption struct ModeOption
@@ -98,25 +98,22 @@ bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::str
return false; return false;
} }
bool ResolveConfiguredVideoFormats( bool ResolveConfiguredDeckLinkVideoModes(
const std::string& inputVideoFormat, const VideoIOConfiguration& config,
const std::string& inputFrameRate, DeckLinkVideoModeSelection& videoModes,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error) std::string& error)
{ {
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input)) if (!ResolveConfiguredDeckLinkVideoMode(config.inputMode, videoModes.input))
{ {
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " + error = "Unsupported DeckLink input mode in config/runtime-host.json: " +
inputVideoFormat + " / " + inputFrameRate; config.inputMode.videoFormat + " / " + config.inputMode.frameRate;
return false; return false;
} }
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output)) if (!ResolveConfiguredDeckLinkVideoMode(config.outputMode, videoModes.output))
{ {
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " + error = "Unsupported DeckLink output mode in config/runtime-host.json: " +
outputVideoFormat + " / " + outputFrameRate; config.outputMode.videoFormat + " / " + config.outputMode.frameRate;
return false; return false;
} }

View File

@@ -0,0 +1,27 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "VideoIOConfig.h"
#include <string>
struct DeckLinkVideoMode
{
BMDDisplayMode displayMode = bmdModeHD1080p5994;
std::string displayName = "1080p59.94";
};
struct DeckLinkVideoModeSelection
{
DeckLinkVideoMode input;
DeckLinkVideoMode output;
};
std::string NormalizeModeToken(const std::string& value);
bool ResolveConfiguredDeckLinkDisplayMode(const VideoIOModeConfiguration& mode, BMDDisplayMode& displayMode, std::string& displayModeName);
bool ResolveConfiguredDeckLinkVideoMode(const VideoIOModeConfiguration& mode, DeckLinkVideoMode& videoMode);
bool ResolveConfiguredDeckLinkVideoModes(
const VideoIOConfiguration& config,
DeckLinkVideoModeSelection& videoModes,
std::string& error);
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);

View File

@@ -1,11 +1,11 @@
#include "DeckLinkFrameTransfer.h" #include "DeckLinkFrameTransfer.h"
#include "OpenGLComposite.h" #include "DeckLinkSession.h"
//////////////////////////////////////////// ////////////////////////////////////////////
// DeckLink Capture Delegate Class // DeckLink Capture Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
CaptureDelegate::CaptureDelegate(OpenGLComposite* pOwner) : CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner), m_pOwner(pOwner),
mRefCount(1) mRefCount(1)
{ {
@@ -39,7 +39,7 @@ HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputF
} }
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource; bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
m_pOwner->VideoFrameArrived(inputFrame, hasNoInputSource); m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
return S_OK; return S_OK;
} }
@@ -51,7 +51,7 @@ HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvent
//////////////////////////////////////////// ////////////////////////////////////////////
// DeckLink Playout Delegate Class // DeckLink Playout Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
PlayoutDelegate::PlayoutDelegate(OpenGLComposite* pOwner) : PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner), m_pOwner(pOwner),
mRefCount(1) mRefCount(1)
{ {
@@ -94,7 +94,7 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n"); OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
} }
m_pOwner->PlayoutFrameCompleted(completedFrame, result); m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
return S_OK; return S_OK;
} }

View File

@@ -6,18 +6,18 @@
#include "DeckLinkAPI_h.h" #include "DeckLinkAPI_h.h"
class OpenGLComposite; class DeckLinkSession;
//////////////////////////////////////////// ////////////////////////////////////////////
// Capture Delegate Class // Capture Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
class CaptureDelegate : public IDeckLinkInputCallback class CaptureDelegate : public IDeckLinkInputCallback
{ {
OpenGLComposite* m_pOwner; DeckLinkSession* m_pOwner;
LONG mRefCount; LONG mRefCount;
public: public:
CaptureDelegate(OpenGLComposite* pOwner); CaptureDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation // IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
@@ -33,11 +33,11 @@ public:
//////////////////////////////////////////// ////////////////////////////////////////////
class PlayoutDelegate : public IDeckLinkVideoOutputCallback class PlayoutDelegate : public IDeckLinkVideoOutputCallback
{ {
OpenGLComposite* m_pOwner; DeckLinkSession* m_pOwner;
LONG mRefCount; LONG mRefCount;
public: public:
PlayoutDelegate(OpenGLComposite* pOwner); PlayoutDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation // IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);

View File

@@ -0,0 +1,626 @@
#include "DeckLinkSession.h"
#include "GlRenderConstants.h"
#include <atlbase.h>
#include <cstdio>
#include <cstring>
#include <new>
#include <sstream>
#include <utility>
#include <vector>
namespace
{
std::string BstrToUtf8(BSTR value)
{
if (value == nullptr)
return std::string();
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
if (requiredBytes <= 1)
return std::string();
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
return std::string();
return std::string(utf8Name.data());
}
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (input == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = input->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoInputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (output == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
}
DeckLinkSession::~DeckLinkSession()
{
ReleaseResources();
}
void DeckLinkSession::ReleaseResources()
{
if (input != nullptr)
input->SetCallback(nullptr);
captureDelegate.Release();
input.Release();
if (output != nullptr)
output->SetScheduledFrameCompletionCallback(nullptr);
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
keyer.Release();
playoutDelegate.Release();
outputVideoFrameQueue.clear();
output.Release();
}
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error)
{
CComPtr<IDeckLinkIterator> deckLinkIterator;
CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode;
mState.backendId = BackendId();
mState.externalKeyingRequested = config.externalKeyingEnabled;
if (!ResolveConfiguredDeckLinkVideoModes(config, mConfiguredModes, error))
return false;
mState.inputDisplayModeName = mConfiguredModes.input.displayName;
mState.outputDisplayModeName = mConfiguredModes.output.displayName;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
if (FAILED(result))
{
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
return false;
}
CComPtr<IDeckLink> deckLink;
while (deckLinkIterator->Next(&deckLink) == S_OK)
{
int64_t duplexMode;
bool deviceSupportsInternalKeying = false;
bool deviceSupportsExternalKeying = false;
std::string modelName;
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
{
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
deckLink.Release();
continue;
}
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
BOOL attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
deviceSupportsInternalKeying = (attributeFlag != FALSE);
attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
deviceSupportsExternalKeying = (attributeFlag != FALSE);
CComBSTR modelNameBstr;
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
modelName = BstrToUtf8(modelNameBstr);
if (result != S_OK || duplexMode == bmdDuplexInactive)
{
deckLink.Release();
continue;
}
bool inputUsed = false;
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
inputUsed = true;
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
{
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
output.Release();
else
{
mState.deviceName = modelName;
mState.capabilities.supportsInternalKeying = deviceSupportsInternalKeying;
mState.capabilities.supportsExternalKeying = deviceSupportsExternalKeying;
}
}
deckLink.Release();
if (output && input)
break;
}
if (!output)
{
error = "Expected an Output DeckLink device";
ReleaseResources();
return false;
}
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
{
error = "Cannot get input Display Mode Iterator.";
ReleaseResources();
return false;
}
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, mConfiguredModes.input.displayMode, &inputMode))
{
error = "Cannot get specified input BMDDisplayMode for configured mode: " + mConfiguredModes.input.displayName;
ReleaseResources();
return false;
}
inputDisplayModeIterator.Release();
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
{
error = "Cannot get output Display Mode Iterator.";
ReleaseResources();
return false;
}
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, mConfiguredModes.output.displayMode, &outputMode))
{
error = "Cannot get specified output BMDDisplayMode for configured mode: " + mConfiguredModes.output.displayName;
ReleaseResources();
return false;
}
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()) }
: mState.outputFrameSize;
if (!input)
mState.inputDisplayModeName = "No input - black frame";
BMDTimeValue frameDuration = 0;
BMDTimeScale frameTimescale = 0;
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
mScheduler.Configure(frameDuration, frameTimescale);
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
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 VideoIOConfiguration& config, std::string& error)
{
if (!output)
{
error = "Expected an Output DeckLink device";
return false;
}
mState.formatStatusMessage.clear();
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, mConfiguredModes.input.displayMode, bmdFormat10BitYUV);
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
if (input != nullptr && !inputTenBitSupported)
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, mConfiguredModes.output.displayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, mConfiguredModes.output.displayMode, bmdFormat10BitYUVA);
mState.outputPixelFormat = config.externalKeyingEnabled
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
if (config.externalKeyingEnabled && outputTenBitYuvaSupported)
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
else if (config.externalKeyingEnabled)
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(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
{
error = "DeckLink output setup failed while calculating output row bytes.";
return false;
}
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
mState.outputPackTextureWidth = OutputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
: mState.outputFrameSize.width;
if (InputIsTenBit())
{
int deckLinkInputRowBytes = 0;
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
else
mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
}
else
{
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
}
mState.captureTextureWidth = InputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
: mState.inputFrameSize.width / 2u;
std::ostringstream status;
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(InputFrameCallback callback, std::string& error)
{
mInputFrameCallback = std::move(callback);
if (!input)
{
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
if (input->EnableVideoInput(mConfiguredModes.input.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
{
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
if (input->EnableVideoInput(mConfiguredModes.input.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
{
std::ostringstream status;
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
mState.formatStatusMessage = status.str();
goto input_enabled;
}
}
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
input.Release();
mState.hasInputDevice = false;
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
input_enabled:
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
if (captureDelegate == nullptr)
{
error = "DeckLink input setup failed while creating the capture callback.";
return false;
}
if (input->SetCallback(captureDelegate) != S_OK)
{
error = "DeckLink input setup failed while installing the capture callback.";
return false;
}
return true;
}
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, std::string& error)
{
mOutputFrameCallback = std::move(callback);
if (output->EnableVideoOutput(mConfiguredModes.output.displayMode, bmdVideoOutputFlagDefault) != S_OK)
{
error = "DeckLink output setup failed while enabling video output.";
return false;
}
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
mState.capabilities.keyerInterfaceAvailable = true;
if (mState.externalKeyingRequested)
{
if (!mState.capabilities.supportsExternalKeying)
{
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
}
else if (!mState.capabilities.keyerInterfaceAvailable)
{
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)
{
mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
}
else
{
mState.externalKeyingActive = true;
mState.statusMessage = "External keying is active on the selected DeckLink output.";
}
}
else if (mState.capabilities.supportsExternalKeying)
{
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 = 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;
}
outputVideoFrameQueue.push_back(outputFrame);
}
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
if (playoutDelegate == nullptr)
{
error = "DeckLink output setup failed while creating the playout callback.";
return false;
}
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
{
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
return false;
}
if (!mState.formatStatusMessage.empty())
mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
return true;
}
double DeckLinkSession::FrameBudgetMilliseconds() const
{
return mScheduler.FrameBudgetMilliseconds();
}
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
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;
return true;
}
bool DeckLinkSession::Start()
{
mScheduler.Reset();
if (!output)
{
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameQueue.empty())
{
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
for (unsigned i = 0; i < kPrerollFrameCount; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
{
MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
{
MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
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;
}
}
if (input)
{
if (input->StartStreams() != S_OK)
{
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
}
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;
}
return true;
}
bool DeckLinkSession::Stop()
{
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
if (input)
{
input->StopStreams();
input->DisableVideoInput();
}
if (output)
{
output->StopScheduledPlayback(0, NULL, 0);
output->DisableVideoOutput();
}
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);
}

View File

@@ -0,0 +1,53 @@
#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();
VideoIOBackendId BackendId() const override { return VideoIOBackendId::DeckLink; }
void ReleaseResources() override;
bool DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error) override;
bool SelectPreferredFormats(const VideoIOConfiguration& config, std::string& error) override;
bool ConfigureInput(InputFrameCallback callback, std::string& error) override;
bool ConfigureOutput(OutputFrameCallback callback, std::string& error) override;
bool Start() override;
bool Stop() override;
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;
DeckLinkVideoModeSelection mConfiguredModes;
};

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "VideoIOFormat.h"
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);

View File

@@ -2,6 +2,7 @@
"shaderLibrary": "shaders", "shaderLibrary": "shaders",
"serverPort": 8080, "serverPort": 8080,
"oscPort": 9000, "oscPort": 9000,
"videoBackend": "decklink",
"inputVideoFormat": "1080p", "inputVideoFormat": "1080p",
"inputFrameRate": "59.94", "inputFrameRate": "59.94",
"outputVideoFormat": "1080p", "outputVideoFormat": "1080p",

View File

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

View File

@@ -201,6 +201,7 @@ paths:
post: post:
tags: [Runtime] tags: [Runtime]
summary: Reload shaders summary: Reload shaders
description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
operationId: reloadShaders operationId: reloadShaders
requestBody: requestBody:
required: false required: false
@@ -358,6 +359,8 @@ components:
$ref: "#/components/schemas/VideoStatus" $ref: "#/components/schemas/VideoStatus"
decklink: decklink:
$ref: "#/components/schemas/DeckLinkStatus" $ref: "#/components/schemas/DeckLinkStatus"
videoIO:
$ref: "#/components/schemas/VideoIOStatus"
performance: performance:
$ref: "#/components/schemas/PerformanceStatus" $ref: "#/components/schemas/PerformanceStatus"
shaders: shaders:
@@ -415,6 +418,8 @@ components:
type: string type: string
DeckLinkStatus: DeckLinkStatus:
type: object type: object
deprecated: true
description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
properties: properties:
modelName: modelName:
type: string type: string
@@ -430,6 +435,26 @@ components:
type: boolean type: boolean
statusMessage: statusMessage:
type: string type: string
VideoIOStatus:
type: object
properties:
backend:
type: string
example: decklink
modelName:
type: string
supportsInternalKeying:
type: boolean
supportsExternalKeying:
type: boolean
keyerInterfaceAvailable:
type: boolean
externalKeyingRequested:
type: boolean
externalKeyingActive:
type: boolean
statusMessage:
type: string
PerformanceStatus: PerformanceStatus:
type: object type: object
properties: properties:
@@ -441,6 +466,18 @@ components:
type: number type: number
budgetUsedPercent: budgetUsedPercent:
type: number type: number
completionIntervalMs:
type: number
smoothedCompletionIntervalMs:
type: number
maxCompletionIntervalMs:
type: number
lateFrameCount:
type: number
droppedFrameCount:
type: number
flushedFrameCount:
type: number
ShaderSummary: ShaderSummary:
type: object type: object
properties: properties:
@@ -452,6 +489,12 @@ components:
type: string type: string
category: category:
type: string type: string
available:
type: boolean
description: False when the shader package exists but failed manifest or compile validation.
error:
type: string
description: Error text for unavailable shader packages.
temporal: temporal:
$ref: "#/components/schemas/TemporalState" $ref: "#/components/schemas/TemporalState"
TemporalState: TemporalState:
@@ -490,9 +533,21 @@ components:
type: string type: string
label: label:
type: string type: string
description:
type: string
description: Short helper text shown under the parameter label in the control UI.
type: type:
type: string type: string
enum: [float, vec2, color, bool, enum, text, trigger] enum: [float, vec2, color, bool, enum, text, trigger]
defaultValue:
description: Default parameter value from the shader manifest.
oneOf:
- type: number
- type: boolean
- type: string
- type: array
items:
type: number
min: min:
type: array type: array
items: items:
@@ -509,6 +564,12 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ParameterOption" $ref: "#/components/schemas/ParameterOption"
maxLength:
type: number
description: Maximum length for text parameters.
font:
type: string
description: Font asset id used by text parameters, when declared.
value: value:
description: Current parameter value. description: Current parameter value.
oneOf: oneOf:

View File

@@ -14,11 +14,12 @@ Packaged documentation:
Generated files: Generated files:
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer. - `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the most recently compiled shader pass.
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`. - `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc` for the most recently compiled pass.
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path. - `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path for the most recently compiled pass.
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup. - `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
- `stack_presets/*.json`: user-saved layer stack presets. - `stack_presets/*.json`: user-saved layer stack presets.
- `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.
Git policy: Git policy:

View File

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

View File

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

View File

@@ -6,6 +6,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset; float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
float uvLength = length(uv); float uvLength = length(uv);
// First warp: convert to polar space and twist the angle more near the
// center, creating the large spiral motion.
float speed = spinRotation * spinEase * 0.2; float speed = spinRotation * spinEase * 0.2;
if (isRotate) if (isRotate)
speed = time * speed; speed = time * speed;
@@ -19,6 +21,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
speed = (time + seed * 17.0) * spinSpeed; speed = (time + seed * 17.0) * spinSpeed;
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y); float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
// Second warp: a short iterative feedback loop turns the spiral into
// painterly bands while preserving a fixed compile-time loop bound.
for (int i = 0; i < 5; ++i) for (int i = 0; i < 5; ++i)
{ {
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv; uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
@@ -32,6 +36,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes)); float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes)); float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
float c3p = 1.0 - min(1.0, c1p + c2p); float c3p = 1.0 - min(1.0, c1p + c2p);
// Three soft band weights drive the palette; lighting rides on the brightest
// bands so the swirl keeps dimensional highlights.
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0); float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
float safeContrast = max(contrast, 0.001); float safeContrast = max(contrast, 0.001);

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
{
"id": "crt-bulge",
"name": "CRT Bulge",
"description": "Warps the image like convex CRT glass, with optional rounded screen edges and vignette darkening.",
"category": "Distortion",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "bulgeAmount",
"label": "Bulge",
"type": "float",
"default": -0.04,
"min": -0.5,
"max": 0.8,
"step": 0.01,
"description": "Positive values swell the center outward; negative values pinch it inward."
},
{
"id": "zoom",
"label": "Zoom",
"type": "float",
"default": 1.04,
"min": 0.5,
"max": 2,
"step": 0.01,
"description": "Scales the source before distortion, useful for hiding warped edges."
},
{
"id": "edgeRoundness",
"label": "Edge Roundness",
"type": "float",
"default": 0.08,
"min": 0,
"max": 0.35,
"step": 0.01,
"description": "Rounds the visible screen corners like older CRT glass."
},
{
"id": "edgeFeather",
"label": "Edge Feather",
"type": "float",
"default": 2,
"min": 0,
"max": 24,
"step": 0.1,
"description": "Softens the rounded screen edge in pixels."
},
{
"id": "sourceEdgeFeather",
"label": "Source Edge Feather",
"type": "float",
"default": 1.5,
"min": 0,
"max": 16,
"step": 0.1,
"description": "Antialiases warped source edges when the distortion reveals outside-frame pixels."
},
{
"id": "vignetteAmount",
"label": "Vignette",
"type": "float",
"default": 0.18,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Darkens the glass toward the screen edges."
},
{
"id": "edgeMode",
"label": "Edge Mode",
"type": "enum",
"default": "black",
"options": [
{
"value": "black",
"label": "Black"
},
{
"value": "clamp",
"label": "Clamp"
},
{
"value": "mirror",
"label": "Mirror"
}
],
"description": "Chooses how warped samples outside the source frame are filled."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [
0,
0,
0,
1
],
"description": "Color used outside the curved screen or source frame."
}
]
}

View File

@@ -0,0 +1,71 @@
float mirroredCoordinate(float coordinate)
{
float wrapped = frac(coordinate * 0.5) * 2.0;
return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
}
float roundedBoxMask(float2 point, float2 halfSize, float radius, float feather)
{
float2 distanceToEdge = abs(point) - (halfSize - radius);
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))) - radius;
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
float signedDistance = outsideDistance + insideDistance;
return 1.0 - smoothstep(0.0, max(feather, 0.00001), signedDistance);
}
float sourceBoundsMask(float2 uv, float2 resolution)
{
float2 pixel = 1.0 / max(resolution, float2(1.0, 1.0));
float2 feather = pixel * max(sourceEdgeFeather, 0.0);
float left = smoothstep(0.0, max(feather.x, 0.00001), uv.x);
float right = 1.0 - smoothstep(1.0 - max(feather.x, 0.00001), 1.0, uv.x);
float top = smoothstep(0.0, max(feather.y, 0.00001), uv.y);
float bottom = 1.0 - smoothstep(1.0 - max(feather.y, 0.00001), 1.0, uv.y);
return saturate(left * right * top * bottom);
}
float2 applyBulge(float2 uv, float2 resolution)
{
float2 centered = uv * 2.0 - 1.0;
float aspect = resolution.x / max(resolution.y, 1.0);
float2 aspectCentered = float2(centered.x * aspect, centered.y);
float radiusSq = dot(aspectCentered, aspectCentered);
float amount = clamp(bulgeAmount, -0.95, 0.95);
float scale = 1.0 / max(1.0 + amount * radiusSq, 0.05);
return centered * scale / max(zoom, 0.001) * 0.5 + 0.5;
}
float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
{
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
if (edgeMode == 1)
return sampleVideo(clamp(uv, 0.0, 1.0));
if (edgeMode == 2)
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
float edgeMask = sourceBoundsMask(uv, resolution);
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
return lerp(outsideColor, color, edgeMask);
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float2 sourceUv = applyBulge(context.uv, resolution);
bool insideSource = false;
float4 color = sampleWarped(sourceUv, resolution, insideSource);
float2 centered = context.uv * 2.0 - 1.0;
float feather = max(edgeFeather, 0.0) / min(resolution.x, resolution.y);
float screenMask = roundedBoxMask(centered, float2(1.0, 1.0), saturate(edgeRoundness), feather);
color = lerp(outsideColor, color, screenMask);
float2 aspectCentered = float2(centered.x * resolution.x / max(resolution.y, 1.0), centered.y);
float edgeDistance = saturate(length(aspectCentered) * 0.72);
float vignette = lerp(1.0, 1.0 - saturate(vignetteAmount), smoothstep(0.35, 1.05, edgeDistance));
color.rgb *= vignette;
return saturate(color);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
{ {
float safeHalfFov = max(halfFov, 0.0001); float safeHalfFov = max(halfFov, 0.0001);
// Match common fisheye projection families while keeping the selected FOV
// normalized to the same source-image radius.
if (fisheyeModel == 1) if (fisheyeModel == 1)
{ {
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001); return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
@@ -49,6 +51,7 @@ float normalizedFisheyeRadius(float theta, float halfFov)
float3 equirectangularRay(float2 uv) float3 equirectangularRay(float2 uv)
{ {
// Convert equirectangular UVs into longitude/latitude on the unit sphere.
float longitude = (uv.x - 0.5) * TWO_PI; float longitude = (uv.x - 0.5) * TWO_PI;
float latitude = (0.5 - uv.y) * PI; float latitude = (0.5 - uv.y) * PI;
float latitudeCos = cos(latitude); float latitudeCos = cos(latitude);
@@ -82,6 +85,8 @@ float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
float inwardLength = max(length(inward), 0.000001); float inwardLength = max(length(inward), 0.000001);
inward /= inwardLength; inward /= inwardLength;
// Outside the fisheye image, sample back inward from the nearest edge so the
// fill looks like stretched lens content instead of a hard color plate.
float blurDistance = max(edgeBlur, 0.0); float blurDistance = max(edgeBlur, 0.0);
float4 color = sampleVideo(clampedUv) * 0.32; float4 color = sampleVideo(clampedUv) * 0.32;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26; color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
@@ -114,6 +119,7 @@ float4 shadeVideo(ShaderContext context)
float phi = atan2(ray.y, ray.x); float phi = atan2(ray.y, ray.x);
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov); float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
// Project the mirrored sphere ray back into the circular fisheye source.
float2 sourceUv = float2( float2 sourceUv = float2(
center.x + cos(phi) * fisheyeRadius * radius.x, center.x + cos(phi) * fisheyeRadius * radius.x,
center.y - sin(phi) * fisheyeRadius * radius.y center.y - sin(phi) * fisheyeRadius * radius.y

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