Compare commits
60 Commits
v0.0.2
...
07a5c91427
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a5c91427 | |||
| 53b980913b | |||
| 4e2ac4a091 | |||
| 3eb5bb5de3 | |||
| ebbc11bb34 | |||
| 6d5a606107 | |||
| 0831e18c2d | |||
| 05d0bcbedd | |||
| 6ea70d9497 | |||
| bc536bd751 | |||
| 7035cde8c8 | |||
| 5eff189bbf | |||
| c9fed70a60 | |||
| fb9122ecdc | |||
| bff27c42a7 | |||
| cea435b609 | |||
| f9ea2d6900 | |||
| 96e7e66b0d | |||
| e5221b329f | |||
| 70be7312b8 | |||
| b2f4d6677c | |||
| 08e039aebe | |||
| 6502344d0a | |||
| e59677c212 | |||
| 3dc7af6fc0 | |||
| ef829bf3ef | |||
| ff1b7519a0 | |||
| 414ef62479 | |||
| d2cf852eb2 | |||
| 73e0af5d2e | |||
| 99e8fb4681 | |||
| a58f8aaf43 | |||
| 515f58b848 | |||
| 02a8a64360 | |||
| a526887ff6 | |||
| d2ac369fdc | |||
| 2317a80ce5 | |||
| 3cb8d3cfad | |||
| 8b9e2916df | |||
| bbbc678c83 | |||
| 1b67777c4a | |||
| 5fd24b3f06 | |||
| 35f5a024fd | |||
| 6918306336 | |||
| 8ec87685b8 | |||
| 8c8028dd1f | |||
| 9e480db31c | |||
| 0bfffa6552 | |||
| 437199f3f0 | |||
| cf31c91831 | |||
| 7e4ab5cbd8 | |||
| 6ce09c0e9c | |||
| 62c3ded1f8 | |||
| 3e8b472f74 | |||
| fd0ebb8d40 | |||
| fcdc5bac6e | |||
| fecc936a14 | |||
| 536f65bf88 | |||
| ce5905373a | |||
| 119e49aec1 |
@@ -12,27 +12,60 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
native-windows:
|
native-windows:
|
||||||
name: Native Windows Build And Tests
|
name: Native Windows Build And Tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Configure Debug
|
- name: Configure Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --preset vs2022-x64-debug
|
run: |
|
||||||
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = $env:SLANG_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFiles = @(
|
||||||
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
|
(Join-Path $slangRoot "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
|
if ($missingFiles.Count -gt 0) {
|
||||||
|
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
|
cmake --preset vs2022-x64-debug -DSLANG_ROOT="$slangRoot"
|
||||||
|
|
||||||
- name: Build Debug
|
- name: Build Debug
|
||||||
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
|
||||||
|
|
||||||
ui-ubuntu:
|
ui-ubuntu:
|
||||||
name: React UI Build
|
name: React UI Build
|
||||||
runs-on: nubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -48,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
package-windows:
|
package-windows:
|
||||||
name: Windows Release Package
|
name: Windows Release Package
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
needs:
|
needs:
|
||||||
- native-windows
|
- native-windows
|
||||||
- ui-ubuntu
|
- ui-ubuntu
|
||||||
@@ -57,6 +90,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Build UI
|
- name: Build UI
|
||||||
shell: powershell
|
shell: powershell
|
||||||
working-directory: ui
|
working-directory: ui
|
||||||
@@ -66,7 +109,30 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure Release
|
- name: Configure Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --preset vs2022-x64-release
|
run: |
|
||||||
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = $env:SLANG_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFiles = @(
|
||||||
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
|
(Join-Path $slangRoot "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
|
if ($missingFiles.Count -gt 0) {
|
||||||
|
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
|
cmake --preset vs2022-x64-release -DSLANG_ROOT="$slangRoot"
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -81,7 +147,8 @@ jobs:
|
|||||||
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
||||||
|
|
||||||
- name: Upload Runtime Package
|
- name: Upload Runtime Package
|
||||||
uses: actions/upload-artifact@v4
|
# Gitea/GHES-compatible runners do not support the v4 artifact backend yet.
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: VideoShader-windows-release
|
name: VideoShader-windows-release
|
||||||
path: dist/VideoShader.zip
|
path: dist/VideoShader.zip
|
||||||
|
|||||||
275
CMakeLists.txt
275
CMakeLists.txt
@@ -7,68 +7,138 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
||||||
set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect" CACHE PATH "Path to the NVIDIA_GPUDirect sample directory from the Blackmagic DeckLink SDK")
|
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||||
|
|
||||||
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
||||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
|
set(SLANG_RUNTIME_FILES
|
||||||
message(FATAL_ERROR "NVIDIA GPUDirect library not found under ${GPUDIRECT_DIR}")
|
"${SLANG_ROOT}/bin/slangc.exe"
|
||||||
|
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
||||||
|
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach(SLANG_RUNTIME_FILE IN LISTS SLANG_RUNTIME_FILES)
|
||||||
|
if(NOT EXISTS "${SLANG_RUNTIME_FILE}")
|
||||||
|
message(FATAL_ERROR "Required Slang runtime file not found: ${SLANG_RUNTIME_FILE}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||||
|
if(NOT EXISTS "${SLANG_LICENSE_FILE}")
|
||||||
|
message(FATAL_ERROR "Slang license file not found: ${SLANG_LICENSE_FILE}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
"${APP_DIR}/ControlServer.cpp"
|
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
||||||
"${APP_DIR}/ControlServer.h"
|
"${APP_DIR}/control/ControlServer.cpp"
|
||||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
"${APP_DIR}/control/ControlServer.h"
|
||||||
"${APP_DIR}/GLExtensions.cpp"
|
"${APP_DIR}/control/OscServer.cpp"
|
||||||
"${APP_DIR}/GLExtensions.h"
|
"${APP_DIR}/control/OscServer.h"
|
||||||
|
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
||||||
|
"${APP_DIR}/control/RuntimeControlBridge.h"
|
||||||
|
"${APP_DIR}/control/RuntimeServices.cpp"
|
||||||
|
"${APP_DIR}/control/RuntimeServices.h"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
||||||
|
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
||||||
|
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
||||||
|
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/GlobalParamsBuffer.h"
|
||||||
|
"${APP_DIR}/gl/renderer/GlRenderConstants.h"
|
||||||
|
"${APP_DIR}/gl/renderer/GlScopedObjects.h"
|
||||||
|
"${APP_DIR}/gl/shader/GlShaderSources.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/GlShaderSources.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLComposite.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
||||||
|
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
||||||
|
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
|
||||||
|
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
|
||||||
|
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderBuildQueue.h"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/ShaderTextureBindings.h"
|
||||||
|
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
||||||
|
"${APP_DIR}/gl/shader/TextRasterizer.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/TextRasterizer.h"
|
||||||
|
"${APP_DIR}/gl/shader/TextureAssetLoader.cpp"
|
||||||
|
"${APP_DIR}/gl/shader/TextureAssetLoader.h"
|
||||||
|
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.h"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
||||||
"${APP_DIR}/NativeHandles.h"
|
"${APP_DIR}/platform/NativeHandles.h"
|
||||||
"${APP_DIR}/NativeSockets.h"
|
"${APP_DIR}/platform/NativeSockets.h"
|
||||||
"${APP_DIR}/OpenGLComposite.cpp"
|
|
||||||
"${APP_DIR}/OpenGLComposite.h"
|
|
||||||
"${APP_DIR}/OscServer.cpp"
|
|
||||||
"${APP_DIR}/OscServer.h"
|
|
||||||
"${APP_DIR}/resource.h"
|
"${APP_DIR}/resource.h"
|
||||||
"${APP_DIR}/RuntimeHost.cpp"
|
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||||
"${APP_DIR}/RuntimeHost.h"
|
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
"${APP_DIR}/RuntimeJson.h"
|
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.h"
|
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||||
"${APP_DIR}/ShaderCompiler.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
"${APP_DIR}/ShaderCompiler.h"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.h"
|
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||||
"${APP_DIR}/ShaderTypes.h"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
||||||
|
"${APP_DIR}/shader/ShaderTypes.h"
|
||||||
"${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}/VideoFrameTransfer.cpp"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
"${APP_DIR}/VideoFrameTransfer.h"
|
"${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})
|
||||||
|
|
||||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
"${GPUDIRECT_DIR}/include"
|
"${APP_DIR}/control"
|
||||||
)
|
"${APP_DIR}/gl"
|
||||||
|
"${APP_DIR}/gl/pipeline"
|
||||||
target_link_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
"${APP_DIR}/gl/renderer"
|
||||||
"${GPUDIRECT_DIR}/lib/x64"
|
"${APP_DIR}/gl/shader"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
dvp.lib
|
|
||||||
opengl32
|
opengl32
|
||||||
glu32
|
glu32
|
||||||
Ws2_32
|
Ws2_32
|
||||||
Crypt32
|
Crypt32
|
||||||
Advapi32
|
Advapi32
|
||||||
|
Gdiplus
|
||||||
|
Ole32
|
||||||
|
Windowscodecs
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
@@ -81,12 +151,13 @@ if(MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(RuntimeJsonTests
|
add_executable(RuntimeJsonTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RuntimeJsonTests PRIVATE
|
target_include_directories(RuntimeJsonTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -96,14 +167,32 @@ endif()
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||||
|
|
||||||
|
add_executable(RuntimeClockTests
|
||||||
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RuntimeClockTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||||
|
|
||||||
add_executable(RuntimeParameterUtilsTests
|
add_executable(RuntimeParameterUtilsTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -112,14 +201,32 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||||
|
|
||||||
|
add_executable(Std140BufferTests
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(Std140BufferTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/gl"
|
||||||
|
"${APP_DIR}/gl/shader"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(Std140BufferTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||||
|
|
||||||
add_executable(ShaderPackageRegistryTests
|
add_executable(ShaderPackageRegistryTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -128,13 +235,38 @@ 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}/OscServer.cpp"
|
"${APP_DIR}/control/OscServer.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(OscServerTests PRIVATE
|
target_include_directories(OscServerTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/control"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(OscServerTests PRIVATE
|
target_link_libraries(OscServerTests PRIVATE
|
||||||
@@ -147,17 +279,72 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
add_test(NAME OscServerTests COMMAND OscServerTests)
|
||||||
|
|
||||||
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
|
add_executable(VideoIOFormatTests
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
||||||
"${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
"$<TARGET_FILE_DIR:LoopThroughWithOpenGLCompositing>/dvp.dll"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(VideoIOFormatTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(VideoIOFormatTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
|
||||||
|
|
||||||
|
add_executable(VideoPlayoutSchedulerTests
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||||
|
|
||||||
|
add_executable(VideoIODeviceFakeTests
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(VideoIODeviceFakeTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
||||||
|
|
||||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||||
RUNTIME DESTINATION "."
|
RUNTIME DESTINATION "."
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
install(FILES ${SLANG_RUNTIME_FILES}
|
||||||
|
DESTINATION "3rdParty/slang/bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${SLANG_LICENSE_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "SLANG_LICENSE.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
|
||||||
DESTINATION "."
|
DESTINATION "."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Video Shader
|
# Video Shader
|
||||||
|
|
||||||
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI.
|
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
|
||||||
|
|
||||||
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
|
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
|
||||||
|
|
||||||
@@ -15,26 +15,32 @@ 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 DeckLink SDK 16.0 with the NVIDIA GPUDirect sample files available locally.
|
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
||||||
- Slang compiler available under the repo/tooling paths expected by the runtime, or otherwise discoverable by the existing app setup.
|
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
||||||
|
|
||||||
The Blackmagic/GPUDirect SDK should not be committed to this repository. `CMakeLists.txt` exposes `GPUDIRECT_DIR` as a cache path so local machines and CI runners can point at their installed SDK location.
|
Default expected Slang path:
|
||||||
|
|
||||||
Default expected SDK path:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect
|
3rdParty/slang-2026.8-windows-x86_64
|
||||||
```
|
```
|
||||||
|
|
||||||
Override example:
|
Override example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --preset vs2022-x64-debug -DGPUDIRECT_DIR="D:/SDKs/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect"
|
cmake --preset vs2022-x64-debug -DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
@@ -75,14 +81,19 @@ The package folder will contain:
|
|||||||
```text
|
```text
|
||||||
dist/VideoShader/
|
dist/VideoShader/
|
||||||
LoopThroughWithOpenGLCompositing.exe
|
LoopThroughWithOpenGLCompositing.exe
|
||||||
dvp.dll
|
|
||||||
config/
|
config/
|
||||||
shaders/
|
shaders/
|
||||||
|
3rdParty/slang/bin/
|
||||||
ui/dist/
|
ui/dist/
|
||||||
|
docs/
|
||||||
|
SHADER_CONTRACT.md
|
||||||
runtime/templates/
|
runtime/templates/
|
||||||
|
third_party_notices/
|
||||||
```
|
```
|
||||||
|
|
||||||
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
||||||
|
|
||||||
|
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It does not copy the full Slang release folder.
|
||||||
|
|
||||||
Create a zip for distribution:
|
Create a zip for distribution:
|
||||||
|
|
||||||
@@ -109,7 +120,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 and package registry scanning.
|
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||||
|
- Video I/O format helpers, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||||
|
- OSC packet parsing.
|
||||||
|
|
||||||
## Runtime Configuration
|
## Runtime Configuration
|
||||||
|
|
||||||
@@ -130,7 +143,7 @@ Current native test coverage includes:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
|
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
|
||||||
|
|
||||||
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
||||||
|
|
||||||
@@ -169,6 +182,12 @@ 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:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime/screenshots/
|
||||||
|
```
|
||||||
|
|
||||||
## OSC Control
|
## OSC Control
|
||||||
|
|
||||||
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
||||||
@@ -187,9 +206,10 @@ Each shader package lives under:
|
|||||||
shaders/<id>/
|
shaders/<id>/
|
||||||
shader.json
|
shader.json
|
||||||
shader.slang
|
shader.slang
|
||||||
|
optional-font-or-texture-assets
|
||||||
```
|
```
|
||||||
|
|
||||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, temporal history support, and the Slang entry point contract.
|
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.
|
||||||
|
|
||||||
## Generated Files
|
## Generated Files
|
||||||
|
|
||||||
@@ -200,6 +220,7 @@ Runtime-generated files are intentionally ignored:
|
|||||||
- `runtime/shader_cache/active_shader.frag`
|
- `runtime/shader_cache/active_shader.frag`
|
||||||
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
||||||
- `runtime/stack_presets/*.json`
|
- `runtime/stack_presets/*.json`
|
||||||
|
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
|
||||||
|
|
||||||
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
||||||
|
|
||||||
@@ -207,7 +228,36 @@ Only `runtime/templates/` and `runtime/README.md` are tracked.
|
|||||||
|
|
||||||
The Gitea workflow expects two act runners:
|
The Gitea workflow expects two act runners:
|
||||||
|
|
||||||
- `windows-latest`: builds the native app and runs native tests.
|
- `windows-2022`: builds the native app and runs native tests.
|
||||||
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
|
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
|
||||||
|
|
||||||
If your Windows runner stores the Blackmagic SDK outside the repo, configure `GPUDIRECT_DIR` in the runner environment or adjust the workflow configure command to pass `-DGPUDIRECT_DIR=...`.
|
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
|
||||||
|
|
||||||
|
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
||||||
|
|
||||||
|
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
|
||||||
|
|
||||||
|
Example runner paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D:\SDKs\slang-2026.8-windows-x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default under `3rdParty/`.
|
||||||
|
|
||||||
|
## Still Todo
|
||||||
|
|
||||||
|
- Audio.
|
||||||
|
- Genlock.
|
||||||
|
- Find a better UI library for React.
|
||||||
|
- Logs.
|
||||||
|
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
|
||||||
|
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||||
|
- Add WebView2
|
||||||
|
- MSDF typography rasterisation
|
||||||
|
- More shader-library organisation and filtering as the built-in library grows.
|
||||||
|
- linear compositing?
|
||||||
|
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
||||||
|
- allow shaders to read other shaders data store based on name? or output over OSC
|
||||||
|
- Mipmapping for shader-declared textures
|
||||||
|
- Multipass for shaders at request
|
||||||
|
|||||||
@@ -57,6 +57,31 @@ 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.
|
||||||
|
|
||||||
|
## Guidance For Shaders
|
||||||
|
|
||||||
|
When generating a new shader package, prefer matching the existing runtime contract over copying code verbatim from Shadertoy, GLSL sandbox sites, or WebGL demos.
|
||||||
|
|
||||||
|
Important rules:
|
||||||
|
|
||||||
|
- Generate a complete package: `shaders/<id>/shader.json` and `shaders/<id>/shader.slang`.
|
||||||
|
- Use `float4 shadeVideo(ShaderContext context)` unless the manifest explicitly sets a different `entryPoint`.
|
||||||
|
- Do not create `mainImage`, `main`, `fragColor`, `iResolution`, `iTime`, `iChannel0`, or a fragment shader attribute layout. The runtime wrapper provides the real fragment entry point.
|
||||||
|
- Replace Shadertoy `fragCoord` with `context.uv * context.outputResolution`.
|
||||||
|
- Replace `iResolution.xy` with `context.outputResolution`.
|
||||||
|
- Replace `iTime` with `context.time`.
|
||||||
|
- Replace `iFrame` with `context.frameCount`.
|
||||||
|
- Replace source-video `iChannel0` sampling with `sampleVideo(uv)` or `context.sourceColor`.
|
||||||
|
- Use Slang/HLSL names and syntax: `float2`, `float3`, `float4`, `float2x2`, `lerp`, `frac`, `saturate`, and `mul(matrix, vector)`.
|
||||||
|
- Do not use GLSL-only types/functions such as `vec2`, `vec3`, `vec4`, `mat2`, `mix`, `fract`, `mod`, `texture`, or `mainImage`.
|
||||||
|
- Keep parameter IDs, texture IDs, font IDs, and function entry points as valid shader identifiers: letters, numbers, and underscores only, starting with a letter or underscore.
|
||||||
|
- Add only controls that are actually used by the shader.
|
||||||
|
- Prefer a small number of clear controls with conservative defaults.
|
||||||
|
- Keep shaders deterministic unless randomness is an explicit feature. For stable process-level variation, use `context.startupRandom`; for per-pixel pseudo-randomness, hash from `uv`, pixel coordinates, `frameCount`, or trigger values.
|
||||||
|
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
|
||||||
|
- If the source license is unclear or incompatible, do not add the shader package.
|
||||||
|
|
||||||
|
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector.
|
||||||
|
|
||||||
## Manifest Fields
|
## Manifest Fields
|
||||||
|
|
||||||
`shader.json` is the runtime-facing description of the shader.
|
`shader.json` is the runtime-facing description of the shader.
|
||||||
@@ -73,6 +98,7 @@ Optional fields:
|
|||||||
- `category`: UI grouping label.
|
- `category`: UI grouping label.
|
||||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||||
- `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.
|
||||||
- `temporal`: history-buffer requirements.
|
- `temporal`: history-buffer requirements.
|
||||||
|
|
||||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||||
@@ -80,6 +106,7 @@ Shader-visible identifiers must be valid Slang-style identifiers:
|
|||||||
- `entryPoint`
|
- `entryPoint`
|
||||||
- parameter `id`
|
- parameter `id`
|
||||||
- texture `id`
|
- texture `id`
|
||||||
|
- font `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.
|
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.
|
||||||
|
|
||||||
@@ -120,6 +147,9 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
|
float startupRandom;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -133,14 +163,24 @@ 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.
|
||||||
|
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
||||||
|
- `startupRandom`: random `0..1` value generated once when the host process starts. It stays constant for the lifetime of the app and changes on the next launch.
|
||||||
- `frameCount`: incrementing frame counter.
|
- `frameCount`: incrementing frame counter.
|
||||||
- `mixAmount`: runtime mix amount.
|
- `mixAmount`: runtime mix amount.
|
||||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
||||||
|
|
||||||
|
Color/precision notes:
|
||||||
|
|
||||||
|
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
||||||
|
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
|
||||||
|
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
|
||||||
|
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
||||||
|
|
||||||
## Helper Functions
|
## Helper Functions
|
||||||
|
|
||||||
The wrapper provides:
|
The wrapper provides:
|
||||||
@@ -180,6 +220,8 @@ Supported types:
|
|||||||
| `color` | `float4` | `[r, g, b, a]` |
|
| `color` | `float4` | `[r, g, b, a]` |
|
||||||
| `bool` | `bool` | `true` or `false` |
|
| `bool` | `bool` | `true` or `false` |
|
||||||
| `enum` | `int` | selected option index |
|
| `enum` | `int` | selected option index |
|
||||||
|
| `text` | generated texture/helper | string |
|
||||||
|
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
|
||||||
|
|
||||||
Float example:
|
Float example:
|
||||||
|
|
||||||
@@ -278,12 +320,63 @@ else if (mode == 2)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Text example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [
|
||||||
|
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" }
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "titleText",
|
||||||
|
"label": "Title",
|
||||||
|
"type": "text",
|
||||||
|
"default": "LIVE",
|
||||||
|
"font": "inter",
|
||||||
|
"maxLength": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Text parameters are runtime-owned strings. They are not emitted as uniform values. Instead, the runtime renders the current string into a single-line SDF mask texture and the shader wrapper exposes helpers based on the parameter id:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float mask = sampleTitleText(textUv);
|
||||||
|
float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
|
||||||
|
```
|
||||||
|
|
||||||
|
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
|
||||||
|
|
||||||
|
Trigger example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "flash",
|
||||||
|
"label": "Flash",
|
||||||
|
"type": "trigger"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A trigger appears as a button in the control UI. Pressing it increments the shader-visible integer `flash` and records the runtime time in `flashTime`:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float age = context.time - flashTime;
|
||||||
|
float intensity = flash > 0 ? exp(-age * 5.0) : 0.0;
|
||||||
|
color.rgb += intensity;
|
||||||
|
```
|
||||||
|
|
||||||
|
Triggers are useful for one-shot shader reactions such as flashes, ripples, cuts, or randomized looks. They do not execute arbitrary CPU code; they only update uniforms consumed by the shader.
|
||||||
|
|
||||||
Parameter validation:
|
Parameter validation:
|
||||||
|
|
||||||
- Float values are clamped to `min`/`max` if provided.
|
- Float values are clamped to `min`/`max` if provided.
|
||||||
- `vec2` must have exactly 2 numbers.
|
- `vec2` must have exactly 2 numbers.
|
||||||
- `color` must have exactly 4 numbers.
|
- `color` must have exactly 4 numbers.
|
||||||
- Enum defaults must match one of the declared option values.
|
- Enum defaults must match one of the declared option values.
|
||||||
|
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
|
||||||
|
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
|
||||||
- Non-finite numeric values are rejected.
|
- Non-finite numeric values are rejected.
|
||||||
|
|
||||||
## Texture Assets
|
## Texture Assets
|
||||||
@@ -323,6 +416,31 @@ return float4(logo.rgb * alpha, alpha);
|
|||||||
|
|
||||||
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
||||||
|
|
||||||
|
## Font Assets
|
||||||
|
|
||||||
|
Declare packaged font assets in the manifest:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [
|
||||||
|
{
|
||||||
|
"id": "inter",
|
||||||
|
"path": "fonts/Inter-Regular.ttf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `id` must be a valid shader identifier.
|
||||||
|
- `path` is relative to the shader package directory.
|
||||||
|
- The file must exist when the manifest is loaded.
|
||||||
|
- Font asset changes trigger shader reload.
|
||||||
|
- V1 text layout is single-line; shaders position and scale the generated text texture themselves.
|
||||||
|
|
||||||
|
See `shaders/text-overlay/` for a complete live text example. The sample bundles Roboto Regular and includes its OFL license beside the font file.
|
||||||
|
|
||||||
## Temporal Shaders
|
## Temporal Shaders
|
||||||
|
|
||||||
Temporal shaders can request access to previous frames.
|
Temporal shaders can request access to previous frames.
|
||||||
@@ -376,6 +494,19 @@ See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples
|
|||||||
- Use `context.inputResolution` when sampling source video by input pixel size.
|
- Use `context.inputResolution` when sampling source video by input pixel size.
|
||||||
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
||||||
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
||||||
|
- For generated calibration charts, test patterns, gradients, and exposure ramps, state whether patch values are linear-light, display-referred gamma encoded, Rec.709 encoded, or intentionally artistic.
|
||||||
|
- For one-stop exposure patches, each patch should normally be `baseLevel * 2^patchIndex` before any display/tone encoding.
|
||||||
|
- For Rec.709 OETF encoding, use:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float rec709Oetf(float linearLevel)
|
||||||
|
{
|
||||||
|
float value = saturate(linearLevel);
|
||||||
|
if (value < 0.018)
|
||||||
|
return 4.5 * value;
|
||||||
|
return 1.099 * pow(value, 0.45) - 0.099;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Pixel-size example:
|
Pixel-size example:
|
||||||
|
|
||||||
@@ -384,6 +515,28 @@ float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
|
|||||||
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Animation And Timing Notes
|
||||||
|
|
||||||
|
- `context.time` is elapsed runtime time in seconds and is the default animation source for generative shaders.
|
||||||
|
- `context.frameCount` increments once per rendered output frame and is useful when an effect must be frame-locked.
|
||||||
|
- Avoid expensive CPU-like timing logic in the shader; animation should usually be a simple function of `context.time`, `context.frameCount`, trigger uniforms, or parameters.
|
||||||
|
- If a shader appears to judder only while animated, first test whether freezing its time removes the issue. That usually separates animation cadence issues from rendering or transfer issues.
|
||||||
|
- Do not add custom timer uniforms to the wrapper. Use the fields already in `ShaderContext`.
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
The app has to meet a fixed video frame cadence, so avoid shader code that only looks good in unconstrained browser demos.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Keep loops bounded with compile-time constants where possible.
|
||||||
|
- Avoid very high per-pixel raymarch counts by default. If a heavy loop is needed, expose a quality/steps control with a safe default.
|
||||||
|
- Prefer early exits only when they are simple; highly divergent branches can be expensive across a full frame.
|
||||||
|
- Avoid repeated texture sampling in large loops unless the effect really needs it.
|
||||||
|
- Use `context.outputResolution` carefully. A 1080p frame is over 2 million fragments; a tiny extra loop can become expensive.
|
||||||
|
- The UI render time may measure CPU command submission rather than true GPU execution time, so visual frame issues can still be GPU-related even when reported render time is small.
|
||||||
|
- Do not write debug files, allocate resources, or assume CPU-side work can happen from `shader.slang`. Shader code is GPU-only.
|
||||||
|
|
||||||
## Reload And Generated Files
|
## Reload And Generated Files
|
||||||
|
|
||||||
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
||||||
@@ -401,6 +554,7 @@ These files are ignored by git and are useful for debugging compiler output. If
|
|||||||
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
||||||
- Remember enum globals are integer indexes, not strings.
|
- Remember enum globals are integer indexes, not strings.
|
||||||
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
||||||
|
- Declare packaged fonts in `shader.json` when text parameters should use a specific font.
|
||||||
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
||||||
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
||||||
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
||||||
@@ -414,6 +568,7 @@ Before committing a new shader package:
|
|||||||
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
||||||
- `shader.slang` implements the configured entry point.
|
- `shader.slang` implements the configured entry point.
|
||||||
- Texture files referenced by `textures` exist.
|
- Texture files referenced by `textures` exist.
|
||||||
|
- Font files referenced by `fonts` exist.
|
||||||
- Enum defaults are present in their `options`.
|
- Enum defaults are present in their `options`.
|
||||||
- Temporal shaders handle short or empty history gracefully.
|
- Temporal shaders handle short or empty history gracefully.
|
||||||
- The app can reload and compile the shader without errors.
|
- The app can reload and compile the shader without errors.
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
||||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
||||||
#endif
|
#endif
|
||||||
@@ -65,6 +69,169 @@
|
|||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int kStatusPanelWidth = 680;
|
||||||
|
const int kStatusPanelHeight = 92;
|
||||||
|
const int kStatusPadding = 8;
|
||||||
|
const int kStatusLabelWidth = 58;
|
||||||
|
const int kStatusButtonWidth = 86;
|
||||||
|
const int kStatusRowHeight = 24;
|
||||||
|
const int kStatusGap = 6;
|
||||||
|
const UINT kCreateStatusStripMessage = WM_APP + 1;
|
||||||
|
|
||||||
|
enum StatusControlId
|
||||||
|
{
|
||||||
|
kControlUrlEditId = 2001,
|
||||||
|
kDocsUrlEditId = 2002,
|
||||||
|
kOscAddressEditId = 2003,
|
||||||
|
kOpenControlButtonId = 2004,
|
||||||
|
kOpenDocsButtonId = 2005
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StatusStripControls
|
||||||
|
{
|
||||||
|
HWND panel = NULL;
|
||||||
|
HWND controlLabel = NULL;
|
||||||
|
HWND controlUrl = NULL;
|
||||||
|
HWND openControl = NULL;
|
||||||
|
HWND docsLabel = NULL;
|
||||||
|
HWND docsUrl = NULL;
|
||||||
|
HWND openDocs = NULL;
|
||||||
|
HWND oscLabel = NULL;
|
||||||
|
HWND oscAddress = NULL;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool StatusStripCreated(const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
return controls.panel != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, DWORD exStyle, int controlId)
|
||||||
|
{
|
||||||
|
return CreateWindowExA(
|
||||||
|
exStyle,
|
||||||
|
className,
|
||||||
|
text,
|
||||||
|
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | style,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
parent,
|
||||||
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(controlId)),
|
||||||
|
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE)),
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateStatusStrip(HWND hWnd, StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_LEFT, WS_EX_CLIENTEDGE, 0);
|
||||||
|
controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0, 0);
|
||||||
|
controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kControlUrlEditId);
|
||||||
|
controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenControlButtonId);
|
||||||
|
controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0, 0);
|
||||||
|
controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kDocsUrlEditId);
|
||||||
|
controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenDocsButtonId);
|
||||||
|
controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0, 0);
|
||||||
|
controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kOscAddressEditId);
|
||||||
|
|
||||||
|
HFONT guiFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
||||||
|
HWND children[] = {
|
||||||
|
controls.controlLabel,
|
||||||
|
controls.controlUrl,
|
||||||
|
controls.openControl,
|
||||||
|
controls.docsLabel,
|
||||||
|
controls.docsUrl,
|
||||||
|
controls.openDocs,
|
||||||
|
controls.oscLabel,
|
||||||
|
controls.oscAddress
|
||||||
|
};
|
||||||
|
for (HWND child : children)
|
||||||
|
{
|
||||||
|
if (child)
|
||||||
|
SendMessage(child, WM_SETFONT, reinterpret_cast<WPARAM>(guiFont), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetWindowTextA(controls.controlUrl, "Starting control server...");
|
||||||
|
SetWindowTextA(controls.docsUrl, "Starting API docs...");
|
||||||
|
SetWindowTextA(controls.oscAddress, "Starting OSC listener...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RaiseStatusControls(const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(controls))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetWindowPos(controls.panel, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
HWND interactiveControls[] = {
|
||||||
|
controls.controlLabel,
|
||||||
|
controls.controlUrl,
|
||||||
|
controls.openControl,
|
||||||
|
controls.docsLabel,
|
||||||
|
controls.docsUrl,
|
||||||
|
controls.openDocs,
|
||||||
|
controls.oscLabel,
|
||||||
|
controls.oscAddress
|
||||||
|
};
|
||||||
|
for (HWND control : interactiveControls)
|
||||||
|
{
|
||||||
|
if (control)
|
||||||
|
SetWindowPos(control, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
RECT clientRect = {};
|
||||||
|
if (!GetClientRect(hWnd, &clientRect) || !controls.panel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int clientWidth = static_cast<int>(clientRect.right - clientRect.left);
|
||||||
|
const int clientHeight = static_cast<int>(clientRect.bottom - clientRect.top);
|
||||||
|
const int panelWidth = std::max(280, std::min(kStatusPanelWidth, clientWidth - (kStatusPadding * 2)));
|
||||||
|
const int panelHeight = kStatusPanelHeight;
|
||||||
|
const int panelLeft = kStatusPadding;
|
||||||
|
const int panelTop = std::max(kStatusPadding, clientHeight - panelHeight - kStatusPadding);
|
||||||
|
MoveWindow(controls.panel, panelLeft, panelTop, panelWidth, panelHeight, TRUE);
|
||||||
|
|
||||||
|
const int rowX = panelLeft + kStatusPadding;
|
||||||
|
const int editX = rowX + kStatusLabelWidth + kStatusGap;
|
||||||
|
const int buttonX = panelLeft + panelWidth - kStatusPadding - kStatusButtonWidth;
|
||||||
|
const int editWidth = std::max(80, buttonX - editX - kStatusGap);
|
||||||
|
const int oscWidth = std::max(80, panelLeft + panelWidth - editX - kStatusPadding);
|
||||||
|
const int row1 = panelTop + kStatusPadding;
|
||||||
|
const int row2 = row1 + kStatusRowHeight + kStatusGap;
|
||||||
|
const int row3 = row2 + kStatusRowHeight + kStatusGap;
|
||||||
|
|
||||||
|
MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE);
|
||||||
|
RaiseStatusControls(controls);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(controls))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str());
|
||||||
|
SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str());
|
||||||
|
SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenUrl(const char* url)
|
||||||
|
{
|
||||||
|
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ShowUnhandledExceptionMessage(const char* prefix)
|
void ShowUnhandledExceptionMessage(const char* prefix)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -203,6 +370,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
static HDC hDC = NULL; // Private GDI Device context
|
static HDC hDC = NULL; // Private GDI Device context
|
||||||
static OpenGLComposite* pOpenGLComposite = NULL;
|
static OpenGLComposite* pOpenGLComposite = NULL;
|
||||||
static bool sInteractiveResize = false;
|
static bool sInteractiveResize = false;
|
||||||
|
static StatusStripControls sStatusStrip;
|
||||||
|
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
@@ -251,7 +419,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
if (pOpenGLComposite->Start())
|
if (pOpenGLComposite->Start())
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failed to initialize - cleanup
|
// Failed to initialize - cleanup
|
||||||
@@ -268,6 +444,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case kCreateStatusStripMessage:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(sStatusStrip))
|
||||||
|
CreateStatusStrip(hWnd, sStatusStrip);
|
||||||
|
|
||||||
|
UpdateStatusStrip(sStatusStrip, *pOpenGLComposite);
|
||||||
|
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||||
|
RECT clientRect = {};
|
||||||
|
if (GetClientRect(hWnd, &clientRect))
|
||||||
|
{
|
||||||
|
pOpenGLComposite->resizeGL(
|
||||||
|
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||||
|
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -300,7 +495,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
RECT clientRect = {};
|
RECT clientRect = {};
|
||||||
if (GetClientRect(hWnd, &clientRect))
|
if (GetClientRect(hWnd, &clientRect))
|
||||||
pOpenGLComposite->resizeGL(static_cast<WORD>(clientRect.right - clientRect.left), static_cast<WORD>(clientRect.bottom - clientRect.top));
|
{
|
||||||
|
pOpenGLComposite->resizeGL(
|
||||||
|
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||||
|
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
InvalidateRect(hWnd, NULL, FALSE);
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
break;
|
break;
|
||||||
@@ -308,6 +507,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (StatusStripCreated(sStatusStrip))
|
||||||
|
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||||
if (pOpenGLComposite)
|
if (pOpenGLComposite)
|
||||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||||
}
|
}
|
||||||
@@ -332,6 +533,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
wglMakeCurrent(hDC, hRC);
|
wglMakeCurrent(hDC, hRC);
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL();
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
|
RaiseStatusControls(sStatusStrip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
@@ -356,6 +558,28 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_COMMAND:
|
||||||
|
switch (LOWORD(wParam))
|
||||||
|
{
|
||||||
|
case kOpenControlButtonId:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
std::string url = pOpenGLComposite->GetControlUrl();
|
||||||
|
OpenUrl(url.c_str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case kOpenDocsButtonId:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
std::string url = pOpenGLComposite->GetDocsUrl();
|
||||||
|
OpenUrl(url.c_str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
return (DefWindowProc(hWnd, message, wParam, lParam));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(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>
|
||||||
@@ -99,15 +99,11 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<TargetMachine>MachineX86</TargetMachine>
|
<TargetMachine>MachineX86</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<Midl>
|
<Midl>
|
||||||
@@ -115,7 +111,7 @@
|
|||||||
</Midl>
|
</Midl>
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>Disabled</Optimization>
|
<Optimization>Disabled</Optimization>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(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>
|
||||||
@@ -125,21 +121,17 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<TargetMachine>MachineX64</TargetMachine>
|
<TargetMachine>MachineX64</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>MaxSpeed</Optimization>
|
<Optimization>MaxSpeed</Optimization>
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(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>
|
||||||
@@ -149,18 +141,13 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
<TargetMachine>MachineX86</TargetMachine>
|
<TargetMachine>MachineX86</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Message>Copy nececssary DLLs to target directory</Message>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<Midl>
|
<Midl>
|
||||||
@@ -169,7 +156,7 @@
|
|||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>MaxSpeed</Optimization>
|
<Optimization>MaxSpeed</Optimization>
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(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>
|
||||||
@@ -179,39 +166,62 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
<TargetMachine>MachineX64</TargetMachine>
|
<TargetMachine>MachineX64</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="GLExtensions.cpp" />
|
<ClCompile Include="gl\renderer\GLExtensions.cpp" />
|
||||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
|
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
|
||||||
<ClCompile Include="OpenGLComposite.cpp" />
|
<ClCompile Include="gl\OpenGLComposite.cpp" />
|
||||||
|
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
|
||||||
|
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
|
||||||
|
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
|
||||||
|
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
|
||||||
|
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
|
||||||
|
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
|
||||||
|
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp" />
|
||||||
|
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.cpp" />
|
||||||
<ClCompile Include="stdafx.cpp">
|
<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="VideoFrameTransfer.cpp" />
|
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c" />
|
||||||
<ClCompile Include="DeckLinkAPI_i.c" />
|
<ClCompile Include="control\RuntimeServices.cpp" />
|
||||||
|
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp" />
|
||||||
|
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp" />
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp" />
|
||||||
|
<ClCompile Include="videoio\VideoIOFormat.cpp" />
|
||||||
|
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="GLExtensions.h" />
|
<ClInclude Include="gl\renderer\GLExtensions.h" />
|
||||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
|
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
|
||||||
<ClInclude Include="OpenGLComposite.h" />
|
<ClInclude Include="gl\OpenGLComposite.h" />
|
||||||
|
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
|
||||||
|
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
|
||||||
|
<ClInclude Include="gl\renderer\OpenGLRenderer.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="VideoFrameTransfer.h" />
|
<ClInclude Include="control\RuntimeServices.h" />
|
||||||
|
<ClInclude Include="videoio\decklink\DeckLinkSession.h" />
|
||||||
|
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h" />
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h" />
|
||||||
|
<ClInclude Include="videoio\VideoIOFormat.h" />
|
||||||
|
<ClInclude Include="videoio\VideoIOTypes.h" />
|
||||||
|
<ClInclude Include="videoio\VideoPlayoutScheduler.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Filter Include="Source Files">
|
<Filter Include="Source Files">
|
||||||
@@ -18,33 +18,96 @@
|
|||||||
</Filter>
|
</Filter>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="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">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="OpenGLComposite.cpp">
|
<ClCompile Include="gl\OpenGLComposite.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<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="VideoFrameTransfer.cpp">
|
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c">
|
||||||
|
<Filter>DeckLink API</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="control\RuntimeServices.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="DeckLinkAPI_i.c">
|
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp">
|
||||||
<Filter>DeckLink API</Filter>
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="videoio\VideoIOFormat.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="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">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="OpenGLComposite.h">
|
<ClInclude Include="gl\OpenGLComposite.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="gl\renderer\OpenGLRenderer.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<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">
|
||||||
@@ -56,7 +119,25 @@
|
|||||||
<ClInclude Include="targetver.h">
|
<ClInclude Include="targetver.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="VideoFrameTransfer.h">
|
<ClInclude Include="control\RuntimeServices.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="videoio\decklink\DeckLinkSession.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="videoio\VideoIOFormat.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="videoio\VideoIOTypes.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="videoio\VideoPlayoutScheduler.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,361 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __OPENGL_COMPOSITE_H__
|
|
||||||
#define __OPENGL_COMPOSITE_H__
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <process.h>
|
|
||||||
#include <tchar.h>
|
|
||||||
#include <gl/gl.h>
|
|
||||||
#include <gl/glu.h>
|
|
||||||
|
|
||||||
#include <objbase.h>
|
|
||||||
#include <atlbase.h>
|
|
||||||
#include <comutil.h>
|
|
||||||
#include "DeckLinkAPI_h.h"
|
|
||||||
|
|
||||||
#include "VideoFrameTransfer.h"
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
#include <deque>
|
|
||||||
|
|
||||||
class PlayoutDelegate;
|
|
||||||
class CaptureDelegate;
|
|
||||||
class PinnedMemoryAllocator;
|
|
||||||
class ControlServer;
|
|
||||||
class OscServer;
|
|
||||||
|
|
||||||
|
|
||||||
class OpenGLComposite
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
|
||||||
~OpenGLComposite();
|
|
||||||
|
|
||||||
bool InitDeckLink();
|
|
||||||
bool Start();
|
|
||||||
bool Stop();
|
|
||||||
bool ReloadShader();
|
|
||||||
std::string GetRuntimeStateJson() const;
|
|
||||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
|
||||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
|
||||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
|
||||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
|
||||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
|
||||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
|
||||||
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
|
||||||
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
|
||||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
|
||||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
|
||||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
|
||||||
void paintGL();
|
|
||||||
|
|
||||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
|
||||||
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void resizeWindow(int width, int height);
|
|
||||||
bool CheckOpenGLExtensions();
|
|
||||||
|
|
||||||
CaptureDelegate* mCaptureDelegate;
|
|
||||||
PlayoutDelegate* mPlayoutDelegate;
|
|
||||||
HWND hGLWnd;
|
|
||||||
HDC hGLDC;
|
|
||||||
HGLRC hGLRC;
|
|
||||||
CRITICAL_SECTION pMutex;
|
|
||||||
|
|
||||||
// DeckLink
|
|
||||||
IDeckLinkInput* mDLInput;
|
|
||||||
IDeckLinkOutput* mDLOutput;
|
|
||||||
IDeckLinkKeyer* mDLKeyer;
|
|
||||||
std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue;
|
|
||||||
PinnedMemoryAllocator* mPlayoutAllocator;
|
|
||||||
BMDTimeValue mFrameDuration;
|
|
||||||
BMDTimeScale mFrameTimescale;
|
|
||||||
unsigned mTotalPlayoutFrames;
|
|
||||||
unsigned mInputFrameWidth;
|
|
||||||
unsigned mInputFrameHeight;
|
|
||||||
unsigned mOutputFrameWidth;
|
|
||||||
unsigned mOutputFrameHeight;
|
|
||||||
std::string mInputDisplayModeName;
|
|
||||||
std::string mOutputDisplayModeName;
|
|
||||||
bool mHasNoInputSource;
|
|
||||||
std::string mDeckLinkOutputModelName;
|
|
||||||
bool mDeckLinkSupportsInternalKeying;
|
|
||||||
bool mDeckLinkSupportsExternalKeying;
|
|
||||||
bool mDeckLinkKeyerInterfaceAvailable;
|
|
||||||
bool mDeckLinkExternalKeyingActive;
|
|
||||||
std::string mDeckLinkStatusMessage;
|
|
||||||
|
|
||||||
// OpenGL data
|
|
||||||
bool mFastTransferExtensionAvailable;
|
|
||||||
GLuint mCaptureTexture;
|
|
||||||
GLuint mDecodedTexture;
|
|
||||||
GLuint mLayerTempTexture;
|
|
||||||
GLuint mFBOTexture;
|
|
||||||
GLuint mOutputTexture;
|
|
||||||
GLuint mUnpinnedTextureBuffer;
|
|
||||||
GLuint mDecodeFrameBuf;
|
|
||||||
GLuint mLayerTempFrameBuf;
|
|
||||||
GLuint mIdFrameBuf;
|
|
||||||
GLuint mOutputFrameBuf;
|
|
||||||
GLuint mIdColorBuf;
|
|
||||||
GLuint mIdDepthBuf;
|
|
||||||
GLuint mFullscreenVAO;
|
|
||||||
GLuint mGlobalParamsUBO;
|
|
||||||
GLuint mDecodeProgram;
|
|
||||||
GLuint mDecodeVertexShader;
|
|
||||||
GLuint mDecodeFragmentShader;
|
|
||||||
GLsizeiptr mGlobalParamsUBOSize;
|
|
||||||
int mViewWidth;
|
|
||||||
int mViewHeight;
|
|
||||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
|
||||||
std::unique_ptr<ControlServer> mControlServer;
|
|
||||||
std::unique_ptr<OscServer> mOscServer;
|
|
||||||
|
|
||||||
struct LayerProgram
|
|
||||||
{
|
|
||||||
struct TextureBinding
|
|
||||||
{
|
|
||||||
std::string samplerName;
|
|
||||||
std::filesystem::path sourcePath;
|
|
||||||
GLuint texture = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string layerId;
|
|
||||||
std::string shaderId;
|
|
||||||
GLuint program = 0;
|
|
||||||
GLuint vertexShader = 0;
|
|
||||||
GLuint fragmentShader = 0;
|
|
||||||
std::vector<TextureBinding> textureBindings;
|
|
||||||
};
|
|
||||||
std::vector<LayerProgram> mLayerPrograms;
|
|
||||||
|
|
||||||
struct HistorySlot
|
|
||||||
{
|
|
||||||
GLuint texture = 0;
|
|
||||||
GLuint framebuffer = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HistoryRing
|
|
||||||
{
|
|
||||||
std::vector<HistorySlot> slots;
|
|
||||||
std::size_t nextWriteIndex = 0;
|
|
||||||
std::size_t filledCount = 0;
|
|
||||||
unsigned effectiveLength = 0;
|
|
||||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryRing mSourceHistoryRing;
|
|
||||||
std::map<std::string, HistoryRing> mPreLayerHistoryByLayerId;
|
|
||||||
bool mTemporalHistoryNeedsReset;
|
|
||||||
|
|
||||||
bool InitOpenGLState();
|
|
||||||
bool compileLayerPrograms(int errorMessageSize, char* errorMessage);
|
|
||||||
bool compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
|
||||||
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
|
||||||
void destroyLayerPrograms();
|
|
||||||
void destroySingleLayerProgram(LayerProgram& layerProgram);
|
|
||||||
void destroyDecodeShaderProgram();
|
|
||||||
void renderDecodePass();
|
|
||||||
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
|
|
||||||
bool loadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
|
||||||
void bindLayerTextureAssets(const LayerProgram& layerProgram);
|
|
||||||
void renderEffect();
|
|
||||||
bool PollRuntimeChanges();
|
|
||||||
void broadcastRuntimeState();
|
|
||||||
bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
|
||||||
bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const;
|
|
||||||
bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error);
|
|
||||||
bool createHistoryRing(HistoryRing& ring, unsigned effectiveLength, TemporalHistorySource historySource, std::string& error);
|
|
||||||
void destroyHistoryRing(HistoryRing& ring);
|
|
||||||
void destroyTemporalHistoryResources();
|
|
||||||
void resetTemporalHistoryState();
|
|
||||||
void pushFramebufferToHistoryRing(GLuint sourceFramebuffer, HistoryRing& ring);
|
|
||||||
void bindHistorySamplers(const RuntimeRenderState& state, GLuint currentSourceTexture);
|
|
||||||
GLuint resolveHistoryTexture(const HistoryRing& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
|
||||||
unsigned sourceHistoryAvailableCount() const;
|
|
||||||
unsigned temporalHistoryAvailableCountForLayer(const std::string& layerId) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// PinnedMemoryAllocator
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class PinnedMemoryAllocator : public IDeckLinkVideoBufferAllocator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PinnedMemoryAllocator(HDC hdc, HGLRC hglrc, VideoFrameTransfer::Direction direction, unsigned cacheSize, unsigned bufferSize);
|
|
||||||
virtual ~PinnedMemoryAllocator();
|
|
||||||
|
|
||||||
bool transferFrame(void* address, GLuint gpuTexture);
|
|
||||||
void waitForTransferComplete(void* address);
|
|
||||||
unsigned bufferSize() { return mBufferSize; }
|
|
||||||
|
|
||||||
// IUnknown methods
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBufferAllocator methods
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE AllocateVideoBuffer (IDeckLinkVideoBuffer** allocatedBuffer) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void unPinAddress(void* address);
|
|
||||||
|
|
||||||
private:
|
|
||||||
HDC mHGLDC;
|
|
||||||
HGLRC mHGLRC;
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
VideoFrameTransfer::Direction mDirection;
|
|
||||||
std::map<void*, VideoFrameTransfer*> mFrameTransfer;
|
|
||||||
unsigned mBufferSize;
|
|
||||||
std::vector<void*> mFrameCache;
|
|
||||||
unsigned mFrameCacheSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// InputAllocatorPool
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class InputAllocatorPool : public IDeckLinkVideoBufferAllocatorProvider
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
InputAllocatorPool(HDC hdc, HGLRC hglrc);
|
|
||||||
|
|
||||||
// IUnknown interface
|
|
||||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
|
||||||
ULONG STDMETHODCALLTYPE Release() override;
|
|
||||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBufferAllocatorProvider interface
|
|
||||||
HRESULT STDMETHODCALLTYPE GetVideoBufferAllocator(
|
|
||||||
/* [in] */ unsigned int bufferSize,
|
|
||||||
/* [in] */ unsigned int width,
|
|
||||||
/* [in] */ unsigned int height,
|
|
||||||
/* [in] */ unsigned int rowBytes,
|
|
||||||
/* [in] */ BMDPixelFormat pixelFormat,
|
|
||||||
/* [out] */ IDeckLinkVideoBufferAllocator **allocator) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
std::map<unsigned int, CComPtr<PinnedMemoryAllocator> > mAllocatorBySize;
|
|
||||||
HDC mHDC;
|
|
||||||
HGLRC mHGLRC;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// DeckLinkVideoBuffer
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class DeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit DeckLinkVideoBuffer(std::shared_ptr<void>& buffer, PinnedMemoryAllocator* parent);
|
|
||||||
virtual ~DeckLinkVideoBuffer() = default;
|
|
||||||
|
|
||||||
// IUnknown interface
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBuffer interface
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE GetSize(uint64_t* size) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags flags) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags flags) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
CComPtr<PinnedMemoryAllocator> mParentAllocator; // Dual-purpose: allocator owns mem this points to, and to access transferFrame() via a QueryInterface
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
std::shared_ptr<void> mBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class CaptureDelegate : public IDeckLinkInputCallback
|
|
||||||
{
|
|
||||||
OpenGLComposite* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
CaptureDelegate (OpenGLComposite* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived (IDeckLinkVideoInputFrame *videoFrame, IDeckLinkAudioInputPacket *audioPacket);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged (BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Render Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
|
|
||||||
{
|
|
||||||
OpenGLComposite* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
PlayoutDelegate (OpenGLComposite* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped ();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // __OPENGL_COMPOSITE_H__
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "VideoFrameTransfer.h"
|
|
||||||
#include "NativeHandles.h"
|
|
||||||
|
|
||||||
|
|
||||||
#define DVP_CHECK(cmd) { \
|
|
||||||
DVPStatus hr = (cmd); \
|
|
||||||
if (DVP_STATUS_OK != hr) { \
|
|
||||||
OutputDebugStringA( #cmd " failed\n" ); \
|
|
||||||
ExitProcess(hr); \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Initialise static members
|
|
||||||
bool VideoFrameTransfer::mInitialized = false;
|
|
||||||
bool VideoFrameTransfer::mUseDvp = false;
|
|
||||||
unsigned VideoFrameTransfer::mWidth = 0;
|
|
||||||
unsigned VideoFrameTransfer::mHeight = 0;
|
|
||||||
GLuint VideoFrameTransfer::mCaptureTexture = 0;
|
|
||||||
|
|
||||||
// NVIDIA specific static members
|
|
||||||
DVPBufferHandle VideoFrameTransfer::mDvpCaptureTextureHandle = 0;
|
|
||||||
DVPBufferHandle VideoFrameTransfer::mDvpPlaybackTextureHandle = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mBufferAddrAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mBufferGpuStrideAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphoreAddrAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphoreAllocSize = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphorePayloadOffset = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphorePayloadSize = 0;
|
|
||||||
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::isNvidiaDvpAvailable()
|
|
||||||
{
|
|
||||||
// Look for supported graphics boards
|
|
||||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
|
||||||
if (renderer == NULL)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
|
|
||||||
return hasDvp;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::isAMDPinnedMemoryAvailable()
|
|
||||||
{
|
|
||||||
// GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported
|
|
||||||
const GLubyte* strExt = glGetString(GL_EXTENSIONS);
|
|
||||||
if (strExt == NULL)
|
|
||||||
{
|
|
||||||
// In a core profile context GL_EXTENSIONS is no longer queryable via glGetString().
|
|
||||||
// Treat this as "extension unavailable" for now; the fast-transfer path is optional.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL);
|
|
||||||
return hasAMDPinned;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::checkFastMemoryTransferAvailable()
|
|
||||||
{
|
|
||||||
return (isNvidiaDvpAvailable() || isAMDPinnedMemoryAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture)
|
|
||||||
{
|
|
||||||
if (mInitialized)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasDvp = isNvidiaDvpAvailable();
|
|
||||||
bool hasAMDPinned = isAMDPinnedMemoryAvailable();
|
|
||||||
|
|
||||||
if (!hasDvp && !hasAMDPinned)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mUseDvp = hasDvp;
|
|
||||||
mWidth = width;
|
|
||||||
mHeight = height;
|
|
||||||
mCaptureTexture = captureTexture;
|
|
||||||
|
|
||||||
if (! initializeMemoryLocking(mWidth * mHeight * 4)) // BGRA uses 4 bytes per pixel
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// DVP initialisation
|
|
||||||
DVP_CHECK(dvpInitGLContext(DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT));
|
|
||||||
DVP_CHECK(dvpGetRequiredConstantsGLCtx( &mBufferAddrAlignment, &mBufferGpuStrideAlignment,
|
|
||||||
&mSemaphoreAddrAlignment, &mSemaphoreAllocSize,
|
|
||||||
&mSemaphorePayloadOffset, &mSemaphorePayloadSize));
|
|
||||||
|
|
||||||
// Register textures with DVP
|
|
||||||
DVP_CHECK(dvpCreateGPUTextureGL(captureTexture, &mDvpCaptureTextureHandle));
|
|
||||||
DVP_CHECK(dvpCreateGPUTextureGL(playbackTexture, &mDvpPlaybackTextureHandle));
|
|
||||||
}
|
|
||||||
|
|
||||||
mInitialized = true;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::initializeMemoryLocking(unsigned memSize)
|
|
||||||
{
|
|
||||||
// Increase the process working set size to allow pinning of memory.
|
|
||||||
static SIZE_T dwMin = 0, dwMax = 0;
|
|
||||||
UniqueHandle processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, FALSE, GetCurrentProcessId()));
|
|
||||||
if (!processHandle.valid())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Retrieve the working set size of the process.
|
|
||||||
if (!dwMin && !GetProcessWorkingSetSize(processHandle.get(), &dwMin, &dwMax))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Allow for 80 frames to be locked
|
|
||||||
BOOL res = SetProcessWorkingSetSize(processHandle.get(), memSize * 80 + dwMin, memSize * 80 + (dwMax-dwMin));
|
|
||||||
if (!res)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncInfo sets up a semaphore which is shared between the GPU and CPU and used to
|
|
||||||
// synchronise access to DVP buffers.
|
|
||||||
struct SyncInfo
|
|
||||||
{
|
|
||||||
SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment);
|
|
||||||
~SyncInfo();
|
|
||||||
|
|
||||||
volatile uint32_t* mSem;
|
|
||||||
volatile uint32_t mReleaseValue;
|
|
||||||
volatile uint32_t mAcquireValue;
|
|
||||||
DVPSyncObjectHandle mDvpSync;
|
|
||||||
};
|
|
||||||
|
|
||||||
SyncInfo::SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment)
|
|
||||||
{
|
|
||||||
mSem = (uint32_t*)_aligned_malloc(semaphoreAllocSize, semaphoreAddrAlignment);
|
|
||||||
|
|
||||||
// Initialise
|
|
||||||
mSem[0] = 0;
|
|
||||||
mReleaseValue = 0;
|
|
||||||
mAcquireValue = 0;
|
|
||||||
|
|
||||||
// Setup DVP sync object and import it
|
|
||||||
DVPSyncObjectDesc syncObjectDesc;
|
|
||||||
syncObjectDesc.externalClientWaitFunc = NULL;
|
|
||||||
syncObjectDesc.sem = (uint32_t*)mSem;
|
|
||||||
|
|
||||||
DVP_CHECK(dvpImportSyncObject(&syncObjectDesc, &mDvpSync));
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncInfo::~SyncInfo()
|
|
||||||
{
|
|
||||||
DVP_CHECK(dvpFreeSyncObject(mDvpSync));
|
|
||||||
_aligned_free((void*)mSem);
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoFrameTransfer::VideoFrameTransfer(unsigned long memSize, void* address, Direction direction) :
|
|
||||||
mBuffer(address),
|
|
||||||
mMemSize(memSize),
|
|
||||||
mDirection(direction),
|
|
||||||
mExtSync(NULL),
|
|
||||||
mGpuSync(NULL),
|
|
||||||
mDvpSysMemHandle(0),
|
|
||||||
mBufferHandle(0)
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// Pin the memory
|
|
||||||
if (! VirtualLock(mBuffer, mMemSize))
|
|
||||||
throw std::runtime_error("Error pinning memory with VirtualLock");
|
|
||||||
|
|
||||||
// Create necessary sysmem and gpu sync objects
|
|
||||||
mExtSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
|
||||||
mGpuSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
|
||||||
|
|
||||||
// Register system memory buffers with DVP
|
|
||||||
DVPSysmemBufferDesc sysMemBuffersDesc;
|
|
||||||
sysMemBuffersDesc.width = mWidth;
|
|
||||||
sysMemBuffersDesc.height = mHeight;
|
|
||||||
sysMemBuffersDesc.stride = mWidth * 4;
|
|
||||||
sysMemBuffersDesc.format = DVP_BGRA;
|
|
||||||
sysMemBuffersDesc.type = DVP_UNSIGNED_BYTE;
|
|
||||||
sysMemBuffersDesc.size = mMemSize;
|
|
||||||
sysMemBuffersDesc.bufAddr = mBuffer;
|
|
||||||
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
// A UYVY 4:2:2 frame is transferred to the GPU, rather than RGB 4:4:4, so width is halved
|
|
||||||
sysMemBuffersDesc.width /= 2;
|
|
||||||
sysMemBuffersDesc.stride /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
DVP_CHECK(dvpCreateBuffer(&sysMemBuffersDesc, &mDvpSysMemHandle));
|
|
||||||
DVP_CHECK(dvpBindToGLCtx(mDvpSysMemHandle));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Create an OpenGL buffer handle to use for pinned memory
|
|
||||||
GLuint bufferHandle;
|
|
||||||
glGenBuffers(1, &bufferHandle);
|
|
||||||
|
|
||||||
// Pin memory by binding buffer to special AMD target.
|
|
||||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, bufferHandle);
|
|
||||||
|
|
||||||
// glBufferData() sets up the address so any OpenGL operation on this buffer will use system memory directly
|
|
||||||
// (assumes address is aligned to 4k boundary).
|
|
||||||
glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, mMemSize, address, GL_STREAM_DRAW);
|
|
||||||
GLenum result = glGetError();
|
|
||||||
if (result != GL_NO_ERROR)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Error pinning memory with glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, ...)");
|
|
||||||
}
|
|
||||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind buffer to target
|
|
||||||
|
|
||||||
mBufferHandle = bufferHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoFrameTransfer::~VideoFrameTransfer()
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
DVP_CHECK(dvpUnbindFromGLCtx(mDvpSysMemHandle));
|
|
||||||
DVP_CHECK(dvpDestroyBuffer(mDvpSysMemHandle));
|
|
||||||
|
|
||||||
delete mExtSync;
|
|
||||||
delete mGpuSync;
|
|
||||||
|
|
||||||
VirtualUnlock(mBuffer, mMemSize);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The buffer is un-pinned by the GPU when the buffer is deleted
|
|
||||||
glDeleteBuffers(1, &mBufferHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::performFrameTransfer()
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// NVIDIA DVP transfers
|
|
||||||
DVPStatus status;
|
|
||||||
|
|
||||||
mGpuSync->mReleaseValue++;
|
|
||||||
|
|
||||||
dvpBegin();
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
// Copy from system memory to GPU texture
|
|
||||||
dvpMapBufferWaitDVP(mDvpCaptureTextureHandle);
|
|
||||||
status = dvpMemcpyLined( mDvpSysMemHandle, mExtSync->mDvpSync, mExtSync->mAcquireValue, DVP_TIMEOUT_IGNORED,
|
|
||||||
mDvpCaptureTextureHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
|
||||||
dvpMapBufferEndDVP(mDvpCaptureTextureHandle);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Copy from GPU texture to system memory
|
|
||||||
dvpMapBufferWaitDVP(mDvpPlaybackTextureHandle);
|
|
||||||
status = dvpMemcpyLined( mDvpPlaybackTextureHandle, mExtSync->mDvpSync, mExtSync->mReleaseValue, DVP_TIMEOUT_IGNORED,
|
|
||||||
mDvpSysMemHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
|
||||||
dvpMapBufferEndDVP(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
dvpEnd();
|
|
||||||
|
|
||||||
return (status == DVP_STATUS_OK);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// AMD pinned memory transfers
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
glEnable(GL_TEXTURE_2D);
|
|
||||||
|
|
||||||
// Use a pinned buffer for the GL_PIXEL_UNPACK_BUFFER target
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mBufferHandle);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
|
||||||
|
|
||||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth/2, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
|
||||||
|
|
||||||
// Ensure pinned texture has been transferred to GPU before we draw with it
|
|
||||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
|
||||||
glDeleteSync(fence);
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
|
||||||
glDisable(GL_TEXTURE_2D);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Use a PIXEL PACK BUFFER to read back pixels
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, mBufferHandle);
|
|
||||||
glReadPixels(0, 0, mWidth, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
|
||||||
|
|
||||||
// Ensure GPU has processed all commands in the pipeline up to this point, before memory is read by the CPU
|
|
||||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
|
||||||
glDeleteSync(fence);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (glGetError() == GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::waitForTransferComplete()
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Block until buffer has completely transferred between GPU and CPU buffer
|
|
||||||
dvpBegin();
|
|
||||||
dvpSyncObjClientWaitComplete(mGpuSync->mDvpSync, DVP_TIMEOUT_IGNORED);
|
|
||||||
dvpEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::beginTextureInUse(Direction direction)
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (direction == CPUtoGPU)
|
|
||||||
dvpMapBufferWaitAPI(mDvpCaptureTextureHandle);
|
|
||||||
else
|
|
||||||
dvpMapBufferWaitAPI(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::endTextureInUse(Direction direction)
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (direction == CPUtoGPU)
|
|
||||||
dvpMapBufferEndAPI(mDvpCaptureTextureHandle);
|
|
||||||
else
|
|
||||||
dvpMapBufferEndAPI(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
#ifndef __VIDEO_FRAME_TRANSFER_H__
|
|
||||||
#define __VIDEO_FRAME_TRANSFER_H__
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
// NVIDIA GPU Direct For Video with OpenGL requires the following two headers.
|
|
||||||
// See the NVIDIA website to check if your graphics card is supported.
|
|
||||||
#include <DVPAPI.h>
|
|
||||||
#include <dvpapi_gl.h>
|
|
||||||
|
|
||||||
struct SyncInfo;
|
|
||||||
|
|
||||||
|
|
||||||
// Class for performing efficient frame memory transfers between the CPU and GPU,
|
|
||||||
// using NVIDIA and AMD extensions.
|
|
||||||
class VideoFrameTransfer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
enum Direction
|
|
||||||
{
|
|
||||||
CPUtoGPU,
|
|
||||||
GPUtoCPU
|
|
||||||
};
|
|
||||||
|
|
||||||
VideoFrameTransfer(unsigned long memSize, void* address, Direction direction);
|
|
||||||
~VideoFrameTransfer();
|
|
||||||
|
|
||||||
static bool checkFastMemoryTransferAvailable();
|
|
||||||
static bool initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture);
|
|
||||||
static void beginTextureInUse(Direction direction);
|
|
||||||
static void endTextureInUse(Direction direction);
|
|
||||||
|
|
||||||
bool performFrameTransfer();
|
|
||||||
void waitForTransferComplete();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static bool isNvidiaDvpAvailable();
|
|
||||||
static bool isAMDPinnedMemoryAvailable();
|
|
||||||
static bool initializeMemoryLocking(unsigned memSize);
|
|
||||||
|
|
||||||
void* mBuffer;
|
|
||||||
unsigned long mMemSize;
|
|
||||||
Direction mDirection;
|
|
||||||
static bool mInitialized;
|
|
||||||
static bool mUseDvp;
|
|
||||||
static unsigned mWidth;
|
|
||||||
static unsigned mHeight;
|
|
||||||
static GLuint mCaptureTexture;
|
|
||||||
|
|
||||||
// NVIDIA GPU Direct for Video support
|
|
||||||
SyncInfo* mExtSync;
|
|
||||||
SyncInfo* mGpuSync;
|
|
||||||
DVPBufferHandle mDvpSysMemHandle;
|
|
||||||
|
|
||||||
static DVPBufferHandle mDvpCaptureTextureHandle;
|
|
||||||
static DVPBufferHandle mDvpPlaybackTextureHandle;
|
|
||||||
static uint32_t mBufferAddrAlignment;
|
|
||||||
static uint32_t mBufferGpuStrideAlignment;
|
|
||||||
static uint32_t mSemaphoreAddrAlignment;
|
|
||||||
static uint32_t mSemaphoreAllocSize;
|
|
||||||
static uint32_t mSemaphorePayloadOffset;
|
|
||||||
static uint32_t mSemaphorePayloadSize;
|
|
||||||
|
|
||||||
// GPU buffer bound to the target GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD for pinned memory
|
|
||||||
GLuint mBufferHandle;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
||||||
|
|
||||||
bool InitializeWinsock(std::string& error)
|
bool InitializeWinsock(std::string& error)
|
||||||
{
|
{
|
||||||
WSADATA wsaData = {};
|
WSADATA wsaData = {};
|
||||||
@@ -165,9 +167,18 @@ void ControlServer::BroadcastState()
|
|||||||
|
|
||||||
void ControlServer::ServerLoop()
|
void ControlServer::ServerLoop()
|
||||||
{
|
{
|
||||||
|
DWORD lastStateBroadcastMs = GetTickCount();
|
||||||
while (mRunning)
|
while (mRunning)
|
||||||
{
|
{
|
||||||
TryAcceptClient();
|
TryAcceptClient();
|
||||||
|
|
||||||
|
const DWORD nowMs = GetTickCount();
|
||||||
|
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
||||||
|
{
|
||||||
|
BroadcastState();
|
||||||
|
lastStateBroadcastMs = nowMs;
|
||||||
|
}
|
||||||
|
|
||||||
Sleep(25);
|
Sleep(25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,6 +433,11 @@ bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& ro
|
|||||||
{
|
{
|
||||||
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
|
||||||
|
{
|
||||||
|
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ public:
|
|||||||
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
||||||
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
||||||
std::function<bool(std::string&)> reloadShader;
|
std::function<bool(std::string&)> reloadShader;
|
||||||
|
std::function<bool(std::string&)> requestScreenshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
ControlServer();
|
ControlServer();
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "RuntimeControlBridge.h"
|
||||||
|
|
||||||
|
#include "ControlServer.h"
|
||||||
|
#include "OpenGLComposite.h"
|
||||||
|
#include "OscServer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
bool StartRuntimeControlServices(
|
||||||
|
OpenGLComposite& composite,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
ControlServer& controlServer,
|
||||||
|
OscServer& oscServer,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
ControlServer::Callbacks callbacks;
|
||||||
|
callbacks.getStateJson = [&composite]() { return composite.GetRuntimeStateJson(); };
|
||||||
|
callbacks.addLayer = [&composite](const std::string& shaderId, std::string& actionError) { return composite.AddLayer(shaderId, actionError); };
|
||||||
|
callbacks.removeLayer = [&composite](const std::string& layerId, std::string& actionError) { return composite.RemoveLayer(layerId, actionError); };
|
||||||
|
callbacks.moveLayer = [&composite](const std::string& layerId, int direction, std::string& actionError) { return composite.MoveLayer(layerId, direction, actionError); };
|
||||||
|
callbacks.moveLayerToIndex = [&composite](const std::string& layerId, std::size_t targetIndex, std::string& actionError) { return composite.MoveLayerToIndex(layerId, targetIndex, actionError); };
|
||||||
|
callbacks.setLayerBypass = [&composite](const std::string& layerId, bool bypassed, std::string& actionError) { return composite.SetLayerBypass(layerId, bypassed, actionError); };
|
||||||
|
callbacks.setLayerShader = [&composite](const std::string& layerId, const std::string& shaderId, std::string& actionError) { return composite.SetLayerShader(layerId, shaderId, actionError); };
|
||||||
|
callbacks.updateLayerParameter = [&composite](const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& actionError) {
|
||||||
|
return composite.UpdateLayerParameterJson(layerId, parameterId, valueJson, actionError);
|
||||||
|
};
|
||||||
|
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
|
||||||
|
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
|
||||||
|
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
|
||||||
|
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
|
||||||
|
callbacks.reloadShader = [&composite](std::string& actionError) {
|
||||||
|
if (!composite.ReloadShader())
|
||||||
|
{
|
||||||
|
actionError = "Shader reload failed. See native app status for details.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error))
|
||||||
|
return false;
|
||||||
|
runtimeHost.SetServerPort(controlServer.GetPort());
|
||||||
|
|
||||||
|
OscServer::Callbacks oscCallbacks;
|
||||||
|
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
||||||
|
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
|
||||||
|
};
|
||||||
|
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscPort(), oscCallbacks, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class ControlServer;
|
||||||
|
class OpenGLComposite;
|
||||||
|
class OscServer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
bool StartRuntimeControlServices(
|
||||||
|
OpenGLComposite& composite,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
ControlServer& controlServer,
|
||||||
|
OscServer& oscServer,
|
||||||
|
std::string& error);
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include "RuntimeServices.h"
|
||||||
|
|
||||||
|
#include "ControlServer.h"
|
||||||
|
#include "OscServer.h"
|
||||||
|
#include "RuntimeControlBridge.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
RuntimeServices::RuntimeServices() :
|
||||||
|
mControlServer(std::make_unique<ControlServer>()),
|
||||||
|
mOscServer(std::make_unique<OscServer>()),
|
||||||
|
mPollRunning(false),
|
||||||
|
mRegistryChanged(false),
|
||||||
|
mReloadRequested(false),
|
||||||
|
mPollFailed(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeServices::~RuntimeServices()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
StartPolling(runtimeHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::Stop()
|
||||||
|
{
|
||||||
|
StopPolling();
|
||||||
|
|
||||||
|
if (mOscServer)
|
||||||
|
mOscServer->Stop();
|
||||||
|
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::BroadcastState()
|
||||||
|
{
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->BroadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
||||||
|
{
|
||||||
|
RuntimePollEvents events;
|
||||||
|
events.registryChanged = mRegistryChanged.exchange(false);
|
||||||
|
events.reloadRequested = mReloadRequested.exchange(false);
|
||||||
|
events.failed = mPollFailed.exchange(false);
|
||||||
|
|
||||||
|
if (events.failed)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||||
|
events.error = mPollError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::StartPolling(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
if (mPollRunning.exchange(true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::StopPolling()
|
||||||
|
{
|
||||||
|
if (!mPollRunning.exchange(false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mPollThread.joinable())
|
||||||
|
mPollThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
while (mPollRunning)
|
||||||
|
{
|
||||||
|
bool registryChanged = false;
|
||||||
|
bool reloadRequested = false;
|
||||||
|
std::string runtimeError;
|
||||||
|
if (!runtimeHost.PollFileChanges(registryChanged, reloadRequested, runtimeError))
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||||
|
mPollError = runtimeError;
|
||||||
|
}
|
||||||
|
mPollFailed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (registryChanged)
|
||||||
|
mRegistryChanged = true;
|
||||||
|
if (reloadRequested)
|
||||||
|
mReloadRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 25 && mPollRunning; ++i)
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
class ControlServer;
|
||||||
|
class OpenGLComposite;
|
||||||
|
class OscServer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
struct RuntimePollEvents
|
||||||
|
{
|
||||||
|
bool registryChanged = false;
|
||||||
|
bool reloadRequested = false;
|
||||||
|
bool failed = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeServices
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeServices();
|
||||||
|
~RuntimeServices();
|
||||||
|
|
||||||
|
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
||||||
|
void BeginPolling(RuntimeHost& runtimeHost);
|
||||||
|
void Stop();
|
||||||
|
void BroadcastState();
|
||||||
|
RuntimePollEvents ConsumePollEvents();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void StartPolling(RuntimeHost& runtimeHost);
|
||||||
|
void StopPolling();
|
||||||
|
void PollLoop(RuntimeHost& runtimeHost);
|
||||||
|
|
||||||
|
std::unique_ptr<ControlServer> mControlServer;
|
||||||
|
std::unique_ptr<OscServer> mOscServer;
|
||||||
|
std::thread mPollThread;
|
||||||
|
std::atomic<bool> mPollRunning;
|
||||||
|
std::atomic<bool> mRegistryChanged;
|
||||||
|
std::atomic<bool> mReloadRequested;
|
||||||
|
std::atomic<bool> mPollFailed;
|
||||||
|
std::mutex mPollErrorMutex;
|
||||||
|
std::string mPollError;
|
||||||
|
};
|
||||||
479
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
479
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
|
#include "OpenGLComposite.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
#include "OpenGLRenderPass.h"
|
||||||
|
#include "OpenGLRenderPipeline.h"
|
||||||
|
#include "OpenGLShaderPrograms.h"
|
||||||
|
#include "OpenGLVideoIOBridge.h"
|
||||||
|
#include "PngScreenshotWriter.h"
|
||||||
|
#include "RuntimeServices.h"
|
||||||
|
#include "ShaderBuildQueue.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||||
|
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
||||||
|
mVideoIO(std::make_unique<DeckLinkSession>()),
|
||||||
|
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||||
|
mUseCommittedLayerStates(false),
|
||||||
|
mScreenshotRequested(false)
|
||||||
|
{
|
||||||
|
InitializeCriticalSection(&pMutex);
|
||||||
|
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||||
|
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
|
||||||
|
*mRenderer,
|
||||||
|
*mRuntimeHost,
|
||||||
|
[this]() { renderEffect(); },
|
||||||
|
[this]() { ProcessScreenshotRequest(); },
|
||||||
|
[this]() { paintGL(); });
|
||||||
|
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||||
|
*mVideoIO,
|
||||||
|
*mRenderer,
|
||||||
|
*mRenderPipeline,
|
||||||
|
*mRuntimeHost,
|
||||||
|
pMutex,
|
||||||
|
hGLDC,
|
||||||
|
hGLRC);
|
||||||
|
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
|
||||||
|
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
|
||||||
|
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
|
||||||
|
mRuntimeServices = std::make_unique<RuntimeServices>();
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGLComposite::~OpenGLComposite()
|
||||||
|
{
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->Stop();
|
||||||
|
if (mShaderBuildQueue)
|
||||||
|
mShaderBuildQueue->Stop();
|
||||||
|
mVideoIO->ReleaseResources();
|
||||||
|
mRenderer->DestroyResources();
|
||||||
|
|
||||||
|
DeleteCriticalSection(&pMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::InitDeckLink()
|
||||||
|
{
|
||||||
|
return InitVideoIO();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::InitVideoIO()
|
||||||
|
{
|
||||||
|
VideoFormatSelection videoModes;
|
||||||
|
std::string initFailureReason;
|
||||||
|
|
||||||
|
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
|
||||||
|
{
|
||||||
|
std::string runtimeError;
|
||||||
|
if (!mRuntimeHost->Initialize(runtimeError))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
if (!ResolveConfiguredVideoFormats(
|
||||||
|
mRuntimeHost->GetInputVideoFormat(),
|
||||||
|
mRuntimeHost->GetInputFrameRate(),
|
||||||
|
mRuntimeHost->GetOutputVideoFormat(),
|
||||||
|
mRuntimeHost->GetOutputFrameRate(),
|
||||||
|
videoModes,
|
||||||
|
initFailureReason))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
|
||||||
|
{
|
||||||
|
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
|
||||||
|
? "This application requires the DeckLink drivers installed."
|
||||||
|
: "DeckLink initialization failed";
|
||||||
|
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason))
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
if (! CheckOpenGLExtensions())
|
||||||
|
{
|
||||||
|
initFailureReason = "OpenGL extension checks failed.";
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! InitOpenGLState())
|
||||||
|
{
|
||||||
|
initFailureReason = "OpenGL state initialization failed.";
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
|
||||||
|
? "DeckLink output device selected."
|
||||||
|
: ("Selected output device: " + mVideoIO->OutputModelName()));
|
||||||
|
|
||||||
|
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||||
|
if (mVideoIO->OutputFrameWidth() < 1920)
|
||||||
|
resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||||
|
else
|
||||||
|
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
|
||||||
|
|
||||||
|
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
|
||||||
|
{
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
if (!mVideoIO->HasInputDevice() && mRuntimeHost)
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||||
|
{
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishVideoIOStatus(mVideoIO->StatusMessage());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error:
|
||||||
|
if (!initFailureReason.empty())
|
||||||
|
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
||||||
|
mVideoIO->ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::paintGL()
|
||||||
|
{
|
||||||
|
if (!TryEnterCriticalSection(&pMutex))
|
||||||
|
{
|
||||||
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||||
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
LeaveCriticalSection(&pMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::resizeGL(WORD width, WORD height)
|
||||||
|
{
|
||||||
|
// We don't set the project or model matrices here since the window data is copied directly from
|
||||||
|
// an off-screen FBO in paintGL(). Just save the width and height for use in paintGL().
|
||||||
|
mRenderer->ResizeView(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::resizeWindow(int width, int height)
|
||||||
|
{
|
||||||
|
RECT r;
|
||||||
|
if (GetWindowRect(hGLWnd, &r))
|
||||||
|
{
|
||||||
|
SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!statusMessage.empty())
|
||||||
|
mVideoIO->SetStatusMessage(statusMessage);
|
||||||
|
|
||||||
|
mRuntimeHost->SetVideoIOStatus(
|
||||||
|
"decklink",
|
||||||
|
mVideoIO->OutputModelName(),
|
||||||
|
mVideoIO->SupportsInternalKeying(),
|
||||||
|
mVideoIO->SupportsExternalKeying(),
|
||||||
|
mVideoIO->KeyerInterfaceAvailable(),
|
||||||
|
mRuntimeHost->ExternalKeyingEnabled(),
|
||||||
|
mVideoIO->ExternalKeyingActive(),
|
||||||
|
mVideoIO->StatusMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::InitOpenGLState()
|
||||||
|
{
|
||||||
|
if (! ResolveGLExtensions())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::string runtimeError;
|
||||||
|
if (mRuntimeHost->GetRepoRoot().empty() && !mRuntimeHost->Initialize(runtimeError))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mRuntimeServices->Start(*this, *mRuntimeHost, runtimeError))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the runtime shader program generated from the active shader package.
|
||||||
|
char compilerErrorMessage[1024];
|
||||||
|
if (!mShaderPrograms->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mShaderPrograms->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||||
|
mUseCommittedLayerStates = false;
|
||||||
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
|
||||||
|
std::string rendererError;
|
||||||
|
if (!mRenderer->InitializeResources(
|
||||||
|
mVideoIO->InputFrameWidth(),
|
||||||
|
mVideoIO->InputFrameHeight(),
|
||||||
|
mVideoIO->CaptureTextureWidth(),
|
||||||
|
mVideoIO->OutputFrameWidth(),
|
||||||
|
mVideoIO->OutputFrameHeight(),
|
||||||
|
mVideoIO->OutputPackTextureWidth(),
|
||||||
|
rendererError))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::Start()
|
||||||
|
{
|
||||||
|
return mVideoIO->Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::Stop()
|
||||||
|
{
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->Stop();
|
||||||
|
|
||||||
|
const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
|
||||||
|
mVideoIO->Stop();
|
||||||
|
if (wasExternalKeyingActive)
|
||||||
|
PublishVideoIOStatus("External keying has been disabled.");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::ReloadShader()
|
||||||
|
{
|
||||||
|
if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||||
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
}
|
||||||
|
RequestShaderBuild();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||||
|
{
|
||||||
|
(void)error;
|
||||||
|
mScreenshotRequested.store(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::renderEffect()
|
||||||
|
{
|
||||||
|
ProcessRuntimePollResults();
|
||||||
|
|
||||||
|
const bool hasInputSource = mVideoIO->HasInputSource();
|
||||||
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
|
if (mUseCommittedLayerStates)
|
||||||
|
{
|
||||||
|
layerStates = mShaderPrograms->CommittedLayerStates();
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
|
}
|
||||||
|
else if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates))
|
||||||
|
{
|
||||||
|
mCachedLayerRenderStates = layerStates;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
layerStates = mCachedLayerRenderStates;
|
||||||
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
|
||||||
|
mRenderPass->Render(
|
||||||
|
hasInputSource,
|
||||||
|
layerStates,
|
||||||
|
mVideoIO->InputFrameWidth(),
|
||||||
|
mVideoIO->InputFrameHeight(),
|
||||||
|
mVideoIO->CaptureTextureWidth(),
|
||||||
|
mVideoIO->InputPixelFormat(),
|
||||||
|
historyCap,
|
||||||
|
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
||||||
|
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
||||||
|
},
|
||||||
|
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) {
|
||||||
|
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::ProcessScreenshotRequest()
|
||||||
|
{
|
||||||
|
if (!mScreenshotRequested.exchange(false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
|
||||||
|
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
|
||||||
|
if (width == 0 || height == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
|
||||||
|
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
|
||||||
|
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||||
|
for (unsigned y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
const unsigned sourceY = height - 1 - y;
|
||||||
|
std::copy(
|
||||||
|
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
|
||||||
|
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
|
||||||
|
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const std::filesystem::path outputPath = BuildScreenshotPath();
|
||||||
|
std::filesystem::create_directories(outputPath.parent_path());
|
||||||
|
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
|
||||||
|
}
|
||||||
|
catch (const std::exception& exception)
|
||||||
|
{
|
||||||
|
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
||||||
|
{
|
||||||
|
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
|
||||||
|
? mRuntimeHost->GetRuntimeRoot()
|
||||||
|
: std::filesystem::current_path();
|
||||||
|
|
||||||
|
const auto now = std::chrono::system_clock::now();
|
||||||
|
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
||||||
|
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||||
|
std::tm localTime = {};
|
||||||
|
localtime_s(&localTime, &nowTime);
|
||||||
|
|
||||||
|
std::ostringstream filename;
|
||||||
|
filename << "video-shader-toys-"
|
||||||
|
<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
|
||||||
|
<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
|
||||||
|
<< ".png";
|
||||||
|
|
||||||
|
return root / "screenshots" / filename.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::ProcessRuntimePollResults()
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost || !mRuntimeServices)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
|
||||||
|
if (events.failed)
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetCompileStatus(false, events.error);
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.registryChanged)
|
||||||
|
broadcastRuntimeState();
|
||||||
|
|
||||||
|
if (!events.reloadRequested)
|
||||||
|
{
|
||||||
|
PreparedShaderBuild readyBuild;
|
||||||
|
if (!mShaderBuildQueue || !mShaderBuildQueue->TryConsumeReadyBuild(readyBuild))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
char compilerErrorMessage[1024] = {};
|
||||||
|
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||||
|
mUseCommittedLayerStates = true;
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mUseCommittedLayerStates = false;
|
||||||
|
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||||
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||||
|
RequestShaderBuild();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::RequestShaderBuild()
|
||||||
|
{
|
||||||
|
if (!mShaderBuildQueue || !mVideoIO)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mUseCommittedLayerStates = true;
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::broadcastRuntimeState()
|
||||||
|
{
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->BroadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::resetTemporalHistoryState()
|
||||||
|
{
|
||||||
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::CheckOpenGLExtensions()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
103
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
103
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#ifndef __OPENGL_COMPOSITE_H__
|
||||||
|
#define __OPENGL_COMPOSITE_H__
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <process.h>
|
||||||
|
#include <tchar.h>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
#include <gl/glu.h>
|
||||||
|
|
||||||
|
#include <objbase.h>
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <comutil.h>
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
class VideoIODevice;
|
||||||
|
class OpenGLVideoIOBridge;
|
||||||
|
class OpenGLRenderPass;
|
||||||
|
class OpenGLRenderPipeline;
|
||||||
|
class OpenGLShaderPrograms;
|
||||||
|
class RuntimeServices;
|
||||||
|
class ShaderBuildQueue;
|
||||||
|
|
||||||
|
|
||||||
|
class OpenGLComposite
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
||||||
|
~OpenGLComposite();
|
||||||
|
|
||||||
|
bool InitDeckLink();
|
||||||
|
bool InitVideoIO();
|
||||||
|
bool Start();
|
||||||
|
bool Stop();
|
||||||
|
bool ReloadShader();
|
||||||
|
std::string GetRuntimeStateJson() const;
|
||||||
|
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||||
|
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||||
|
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||||
|
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||||
|
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||||
|
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||||
|
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||||
|
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||||
|
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||||
|
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||||
|
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||||
|
bool RequestScreenshot(std::string& error);
|
||||||
|
unsigned short GetControlServerPort() const;
|
||||||
|
unsigned short GetOscPort() const;
|
||||||
|
std::string GetControlUrl() const;
|
||||||
|
std::string GetDocsUrl() const;
|
||||||
|
std::string GetOscAddress() const;
|
||||||
|
|
||||||
|
void resizeGL(WORD width, WORD height);
|
||||||
|
void paintGL();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resizeWindow(int width, int height);
|
||||||
|
bool CheckOpenGLExtensions();
|
||||||
|
void PublishVideoIOStatus(const std::string& statusMessage);
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
|
||||||
|
HWND hGLWnd;
|
||||||
|
HDC hGLDC;
|
||||||
|
HGLRC hGLRC;
|
||||||
|
CRITICAL_SECTION pMutex;
|
||||||
|
|
||||||
|
std::unique_ptr<VideoIODevice> mVideoIO;
|
||||||
|
std::unique_ptr<OpenGLRenderer> mRenderer;
|
||||||
|
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||||
|
std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
|
||||||
|
std::unique_ptr<OpenGLRenderPass> mRenderPass;
|
||||||
|
std::unique_ptr<OpenGLRenderPipeline> mRenderPipeline;
|
||||||
|
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
|
||||||
|
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
||||||
|
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||||
|
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||||
|
std::atomic<bool> mUseCommittedLayerStates;
|
||||||
|
std::atomic<bool> mScreenshotRequested;
|
||||||
|
|
||||||
|
bool InitOpenGLState();
|
||||||
|
void renderEffect();
|
||||||
|
bool ProcessRuntimePollResults();
|
||||||
|
void RequestShaderBuild();
|
||||||
|
void ProcessScreenshotRequest();
|
||||||
|
std::filesystem::path BuildScreenshotPath() const;
|
||||||
|
void broadcastRuntimeState();
|
||||||
|
void resetTemporalHistoryState();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // __OPENGL_COMPOSITE_H__
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#include "OpenGLComposite.h"
|
||||||
|
|
||||||
|
std::string OpenGLComposite::GetRuntimeStateJson() const
|
||||||
|
{
|
||||||
|
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned short OpenGLComposite::GetControlServerPort() const
|
||||||
|
{
|
||||||
|
return mRuntimeHost ? mRuntimeHost->GetServerPort() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned short OpenGLComposite::GetOscPort() const
|
||||||
|
{
|
||||||
|
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OpenGLComposite::GetControlUrl() const
|
||||||
|
{
|
||||||
|
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OpenGLComposite::GetDocsUrl() const
|
||||||
|
{
|
||||||
|
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/docs";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OpenGLComposite::GetOscAddress() const
|
||||||
|
{
|
||||||
|
return "udp://127.0.0.1:" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->AddLayer(shaderId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->SetLayerBypass(layerId, bypassed, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->SetLayerShader(layerId, shaderId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
|
||||||
|
{
|
||||||
|
JsonValue parsedValue;
|
||||||
|
if (!ParseJson(valueJson, parsedValue, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!mRuntimeHost->UpdateLayerParameter(layerId, parameterId, parsedValue, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||||
|
{
|
||||||
|
JsonValue parsedValue;
|
||||||
|
if (!ParseJson(valueJson, parsedValue, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!mRuntimeHost->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->SaveStackPreset(presetName, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->LoadStackPreset(presetName, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReloadShader();
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class OpenGLRenderPass
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
||||||
|
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
|
||||||
|
|
||||||
|
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
|
||||||
|
|
||||||
|
void Render(
|
||||||
|
bool hasInputSource,
|
||||||
|
const std::vector<RuntimeRenderState>& layerStates,
|
||||||
|
unsigned inputFrameWidth,
|
||||||
|
unsigned inputFrameHeight,
|
||||||
|
unsigned captureTextureWidth,
|
||||||
|
VideoIOPixelFormat inputPixelFormat,
|
||||||
|
unsigned historyCap,
|
||||||
|
const TextBindingUpdater& updateTextBinding,
|
||||||
|
const GlobalParamsUpdater& updateGlobalParams);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat);
|
||||||
|
void RenderShaderProgram(
|
||||||
|
GLuint sourceTexture,
|
||||||
|
GLuint destinationFrameBuffer,
|
||||||
|
LayerProgram& layerProgram,
|
||||||
|
const RuntimeRenderState& state,
|
||||||
|
unsigned inputFrameWidth,
|
||||||
|
unsigned inputFrameHeight,
|
||||||
|
unsigned historyCap,
|
||||||
|
const TextBindingUpdater& updateTextBinding,
|
||||||
|
const GlobalParamsUpdater& updateGlobalParams);
|
||||||
|
void BindLayerTextureAssets(const LayerProgram& layerProgram);
|
||||||
|
void UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap);
|
||||||
|
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#include "OpenGLRenderPipeline.h"
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
OpenGLRenderPipeline::OpenGLRenderPipeline(
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
RenderEffectCallback renderEffect,
|
||||||
|
OutputReadyCallback outputReady,
|
||||||
|
PaintCallback paint) :
|
||||||
|
mRenderer(renderer),
|
||||||
|
mRuntimeHost(runtimeHost),
|
||||||
|
mRenderEffect(renderEffect),
|
||||||
|
mOutputReady(outputReady),
|
||||||
|
mPaint(paint)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||||
|
{
|
||||||
|
const VideoIOState& state = context.videoState;
|
||||||
|
|
||||||
|
const auto renderStartTime = std::chrono::steady_clock::now();
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||||
|
mRenderEffect();
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
if (mOutputReady)
|
||||||
|
mOutputReady();
|
||||||
|
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||||
|
PackOutputForV210(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::PackOutputForV210(const VideoIOState& state)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
|
||||||
|
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||||
|
glUseProgram(mRenderer.OutputPackProgram());
|
||||||
|
|
||||||
|
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
|
||||||
|
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
|
||||||
|
if (outputResolutionLocation >= 0)
|
||||||
|
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
||||||
|
if (activeWordsLocation >= 0)
|
||||||
|
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
||||||
|
{
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class OpenGLRenderer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
struct RenderPipelineFrameContext
|
||||||
|
{
|
||||||
|
VideoIOState videoState;
|
||||||
|
VideoIOCompletion completion;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpenGLRenderPipeline
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using RenderEffectCallback = std::function<void()>;
|
||||||
|
using OutputReadyCallback = std::function<void()>;
|
||||||
|
using PaintCallback = std::function<void()>;
|
||||||
|
|
||||||
|
OpenGLRenderPipeline(
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
RenderEffectCallback renderEffect,
|
||||||
|
OutputReadyCallback outputReady,
|
||||||
|
PaintCallback paint);
|
||||||
|
|
||||||
|
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void PackOutputForV210(const VideoIOState& state);
|
||||||
|
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
RenderEffectCallback mRenderEffect;
|
||||||
|
OutputReadyCallback mOutputReady;
|
||||||
|
PaintCallback mPaint;
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#include "OpenGLVideoIOBridge.h"
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
||||||
|
VideoIODevice& videoIO,
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
OpenGLRenderPipeline& renderPipeline,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
CRITICAL_SECTION& mutex,
|
||||||
|
HDC hdc,
|
||||||
|
HGLRC hglrc) :
|
||||||
|
mVideoIO(videoIO),
|
||||||
|
mRenderer(renderer),
|
||||||
|
mRenderPipeline(renderPipeline),
|
||||||
|
mRuntimeHost(runtimeHost),
|
||||||
|
mMutex(mutex),
|
||||||
|
mHdc(hdc),
|
||||||
|
mHglrc(hglrc)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
||||||
|
{
|
||||||
|
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
||||||
|
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||||
|
else
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
||||||
|
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
||||||
|
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||||
|
}
|
||||||
|
mLastPlayoutCompletionTime = now;
|
||||||
|
|
||||||
|
if (completionResult == VideoIOCompletionResult::DisplayedLate)
|
||||||
|
++mLateFrameCount;
|
||||||
|
else if (completionResult == VideoIOCompletionResult::Dropped)
|
||||||
|
++mDroppedFrameCount;
|
||||||
|
else if (completionResult == VideoIOCompletionResult::Flushed)
|
||||||
|
++mFlushedFrameCount;
|
||||||
|
|
||||||
|
mRuntimeHost.TrySetFramePacingStats(
|
||||||
|
mCompletionIntervalMilliseconds,
|
||||||
|
mSmoothedCompletionIntervalMilliseconds,
|
||||||
|
mMaxCompletionIntervalMilliseconds,
|
||||||
|
mLateFrameCount,
|
||||||
|
mDroppedFrameCount,
|
||||||
|
mFlushedFrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
||||||
|
{
|
||||||
|
const VideoIOState& state = mVideoIO.State();
|
||||||
|
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||||
|
|
||||||
|
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||||
|
return; // don't transfer texture when there's no input
|
||||||
|
|
||||||
|
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||||
|
|
||||||
|
EnterCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
|
||||||
|
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
||||||
|
|
||||||
|
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
|
||||||
|
if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||||
|
else
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||||
|
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
||||||
|
{
|
||||||
|
RecordFramePacing(completion.result);
|
||||||
|
|
||||||
|
EnterCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
VideoIOOutputFrame outputFrame;
|
||||||
|
if (!mVideoIO.BeginOutputFrame(outputFrame))
|
||||||
|
{
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const VideoIOState& state = mVideoIO.State();
|
||||||
|
RenderPipelineFrameContext frameContext;
|
||||||
|
frameContext.videoState = state;
|
||||||
|
frameContext.completion = completion;
|
||||||
|
|
||||||
|
// make GL context current in this thread
|
||||||
|
wglMakeCurrent(mHdc, mHglrc);
|
||||||
|
|
||||||
|
mRenderPipeline.RenderFrame(frameContext, outputFrame);
|
||||||
|
|
||||||
|
mVideoIO.EndOutputFrame(outputFrame);
|
||||||
|
|
||||||
|
mVideoIO.AccountForCompletionResult(completion.result);
|
||||||
|
|
||||||
|
// Schedule the next frame for playout
|
||||||
|
mVideoIO.ScheduleOutputFrame(outputFrame);
|
||||||
|
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenGLRenderPipeline.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
class OpenGLVideoIOBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OpenGLVideoIOBridge(
|
||||||
|
VideoIODevice& videoIO,
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
OpenGLRenderPipeline& renderPipeline,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
CRITICAL_SECTION& mutex,
|
||||||
|
HDC hdc,
|
||||||
|
HGLRC hglrc);
|
||||||
|
|
||||||
|
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
||||||
|
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||||
|
|
||||||
|
VideoIODevice& mVideoIO;
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
OpenGLRenderPipeline& mRenderPipeline;
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
CRITICAL_SECTION& mMutex;
|
||||||
|
HDC mHdc;
|
||||||
|
HGLRC mHglrc;
|
||||||
|
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||||
|
double mCompletionIntervalMilliseconds = 0.0;
|
||||||
|
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||||
|
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||||
|
uint64_t mLateFrameCount = 0;
|
||||||
|
uint64_t mDroppedFrameCount = 0;
|
||||||
|
uint64_t mFlushedFrameCount = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "PngScreenshotWriter.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <wincodec.h>
|
||||||
|
#include <atlbase.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string HResultToString(HRESULT hr)
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << "HRESULT 0x" << std::hex << static_cast<unsigned long>(hr);
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WritePngFile(
|
||||||
|
const std::filesystem::path& outputPath,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height,
|
||||||
|
const std::vector<unsigned char>& bgraPixels,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0 || bgraPixels.size() < static_cast<std::size_t>(width) * height * 4)
|
||||||
|
{
|
||||||
|
error = "Invalid screenshot dimensions or pixel buffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||||
|
const bool shouldUninitialize = SUCCEEDED(initializeResult);
|
||||||
|
if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE)
|
||||||
|
{
|
||||||
|
error = "CoInitializeEx failed: " + HResultToString(initializeResult);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICImagingFactory> factory;
|
||||||
|
HRESULT result = CoCreateInstance(
|
||||||
|
CLSID_WICImagingFactory,
|
||||||
|
nullptr,
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
IID_PPV_ARGS(&factory));
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "Could not create WIC imaging factory: " + HResultToString(result);
|
||||||
|
if (shouldUninitialize)
|
||||||
|
CoUninitialize();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICStream> stream;
|
||||||
|
result = factory->CreateStream(&stream);
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = stream->InitializeFromFilename(outputPath.wstring().c_str(), GENERIC_WRITE);
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "Could not open screenshot output file: " + HResultToString(result);
|
||||||
|
if (shouldUninitialize)
|
||||||
|
CoUninitialize();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICBitmapEncoder> encoder;
|
||||||
|
result = factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = encoder->Initialize(stream, WICBitmapEncoderNoCache);
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "Could not initialize PNG encoder: " + HResultToString(result);
|
||||||
|
if (shouldUninitialize)
|
||||||
|
CoUninitialize();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICBitmapFrameEncode> frame;
|
||||||
|
CComPtr<IPropertyBag2> propertyBag;
|
||||||
|
result = encoder->CreateNewFrame(&frame, &propertyBag);
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = frame->Initialize(propertyBag);
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = frame->SetSize(width, height);
|
||||||
|
|
||||||
|
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = frame->SetPixelFormat(&pixelFormat);
|
||||||
|
if (SUCCEEDED(result) && pixelFormat != GUID_WICPixelFormat32bppBGRA)
|
||||||
|
{
|
||||||
|
error = "PNG encoder did not accept BGRA pixel format.";
|
||||||
|
result = E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UINT stride = width * 4;
|
||||||
|
const UINT imageSize = stride * height;
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = frame->WritePixels(height, stride, imageSize, const_cast<BYTE*>(bgraPixels.data()));
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = frame->Commit();
|
||||||
|
if (SUCCEEDED(result))
|
||||||
|
result = encoder->Commit();
|
||||||
|
|
||||||
|
if (shouldUninitialize)
|
||||||
|
CoUninitialize();
|
||||||
|
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "Could not write screenshot PNG: " + HResultToString(result);
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove(outputPath, ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WritePngFileAsync(
|
||||||
|
const std::filesystem::path& outputPath,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height,
|
||||||
|
std::vector<unsigned char> rgbaPixels)
|
||||||
|
{
|
||||||
|
std::thread(
|
||||||
|
[outputPath, width, height, pixels = std::move(rgbaPixels)]() mutable
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index + 3 < pixels.size(); index += 4)
|
||||||
|
std::swap(pixels[index], pixels[index + 2]);
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
if (!WritePngFile(outputPath, width, height, pixels, error))
|
||||||
|
OutputDebugStringA(("Screenshot write failed: " + error + "\n").c_str());
|
||||||
|
else
|
||||||
|
OutputDebugStringA(("Screenshot written: " + outputPath.string() + "\n").c_str());
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
void WritePngFileAsync(
|
||||||
|
const std::filesystem::path& outputPath,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height,
|
||||||
|
std::vector<unsigned char> rgbaPixels);
|
||||||
|
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#include "TemporalHistoryBuffers.h"
|
||||||
|
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const
|
||||||
|
{
|
||||||
|
unsigned requiredUnits = kSourceHistoryTextureUnitBase;
|
||||||
|
for (const RuntimeRenderState& state : layerStates)
|
||||||
|
{
|
||||||
|
unsigned textTextureCount = 0;
|
||||||
|
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||||
|
{
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
++textTextureCount;
|
||||||
|
}
|
||||||
|
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
|
||||||
|
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + totalShaderTextures;
|
||||||
|
if (layerRequiredUnits > requiredUnits)
|
||||||
|
requiredUnits = layerRequiredUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint maxTextureUnits = 0;
|
||||||
|
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||||
|
const unsigned availableUnits = maxTextureUnits > 0 ? static_cast<unsigned>(maxTextureUnits) : 0u;
|
||||||
|
if (requiredUnits > availableUnits)
|
||||||
|
{
|
||||||
|
std::ostringstream message;
|
||||||
|
message << "The current history and shader texture asset configuration requires " << requiredUnits
|
||||||
|
<< " fragment texture units, but only " << maxTextureUnits << " are available.";
|
||||||
|
error = message.str();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TemporalHistoryBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||||
|
{
|
||||||
|
const bool sourceHistoryNeeded = std::any_of(layerStates.begin(), layerStates.end(),
|
||||||
|
[](const RuntimeRenderState& state) { return state.isTemporal && state.effectiveTemporalHistoryLength > 0; });
|
||||||
|
const unsigned sourceHistoryLength = sourceHistoryNeeded ? historyCap : 0;
|
||||||
|
|
||||||
|
if (sourceHistoryRing.effectiveLength != sourceHistoryLength)
|
||||||
|
{
|
||||||
|
if (!CreateRing(sourceHistoryRing, sourceHistoryLength, TemporalHistorySource::Source, frameWidth, frameHeight, error))
|
||||||
|
return false;
|
||||||
|
mNeedsReset = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> requiredPreLayerIds;
|
||||||
|
for (const RuntimeRenderState& state : layerStates)
|
||||||
|
{
|
||||||
|
if (!state.isTemporal || state.temporalHistorySource != TemporalHistorySource::PreLayerInput)
|
||||||
|
continue;
|
||||||
|
requiredPreLayerIds.insert(state.layerId);
|
||||||
|
auto historyIt = preLayerHistoryByLayerId.find(state.layerId);
|
||||||
|
if (historyIt == preLayerHistoryByLayerId.end() || historyIt->second.effectiveLength != state.effectiveTemporalHistoryLength)
|
||||||
|
{
|
||||||
|
Ring replacement;
|
||||||
|
if (!CreateRing(replacement, state.effectiveTemporalHistoryLength, TemporalHistorySource::PreLayerInput, frameWidth, frameHeight, error))
|
||||||
|
return false;
|
||||||
|
preLayerHistoryByLayerId[state.layerId] = std::move(replacement);
|
||||||
|
mNeedsReset = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = preLayerHistoryByLayerId.begin(); it != preLayerHistoryByLayerId.end();)
|
||||||
|
{
|
||||||
|
if (requiredPreLayerIds.find(it->first) == requiredPreLayerIds.end())
|
||||||
|
{
|
||||||
|
DestroyRing(it->second);
|
||||||
|
it = preLayerHistoryByLayerId.erase(it);
|
||||||
|
mNeedsReset = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mNeedsReset)
|
||||||
|
ResetState();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TemporalHistoryBuffers::CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||||
|
{
|
||||||
|
DestroyRing(ring);
|
||||||
|
ring.effectiveLength = effectiveLength;
|
||||||
|
ring.historySource = historySource;
|
||||||
|
if (effectiveLength == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ring.slots.resize(effectiveLength);
|
||||||
|
for (Slot& slot : ring.slots)
|
||||||
|
{
|
||||||
|
glGenTextures(1, &slot.texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, slot.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);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, frameWidth, frameHeight, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||||
|
|
||||||
|
glGenFramebuffers(1, &slot.framebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
||||||
|
const GLenum framebufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (framebufferStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Failed to initialize a temporal history framebuffer.";
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
DestroyRing(ring);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::DestroyRing(Ring& ring)
|
||||||
|
{
|
||||||
|
for (Slot& slot : ring.slots)
|
||||||
|
{
|
||||||
|
if (slot.framebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &slot.framebuffer);
|
||||||
|
if (slot.texture != 0)
|
||||||
|
glDeleteTextures(1, &slot.texture);
|
||||||
|
slot.framebuffer = 0;
|
||||||
|
slot.texture = 0;
|
||||||
|
}
|
||||||
|
ring.slots.clear();
|
||||||
|
ring.nextWriteIndex = 0;
|
||||||
|
ring.filledCount = 0;
|
||||||
|
ring.effectiveLength = 0;
|
||||||
|
ring.historySource = TemporalHistorySource::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::DestroyResources()
|
||||||
|
{
|
||||||
|
DestroyRing(sourceHistoryRing);
|
||||||
|
for (auto& historyEntry : preLayerHistoryByLayerId)
|
||||||
|
DestroyRing(historyEntry.second);
|
||||||
|
preLayerHistoryByLayerId.clear();
|
||||||
|
mNeedsReset = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::ResetState()
|
||||||
|
{
|
||||||
|
sourceHistoryRing.nextWriteIndex = 0;
|
||||||
|
sourceHistoryRing.filledCount = 0;
|
||||||
|
for (auto& historyEntry : preLayerHistoryByLayerId)
|
||||||
|
{
|
||||||
|
historyEntry.second.nextWriteIndex = 0;
|
||||||
|
historyEntry.second.filledCount = 0;
|
||||||
|
}
|
||||||
|
mNeedsReset = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, unsigned frameWidth, unsigned frameHeight)
|
||||||
|
{
|
||||||
|
if (ring.effectiveLength == 0 || ring.slots.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Slot& targetSlot = ring.slots[ring.nextWriteIndex];
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetSlot.framebuffer);
|
||||||
|
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
ring.nextWriteIndex = (ring.nextWriteIndex + 1) % ring.slots.size();
|
||||||
|
ring.filledCount = std::min<std::size_t>(ring.filledCount + 1, ring.slots.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||||
|
{
|
||||||
|
PushFramebuffer(sourceFramebuffer, sourceHistoryRing, frameWidth, frameHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||||
|
{
|
||||||
|
auto historyIt = preLayerHistoryByLayerId.find(layerId);
|
||||||
|
if (historyIt != preLayerHistoryByLayerId.end())
|
||||||
|
PushFramebuffer(sourceFramebuffer, historyIt->second, frameWidth, frameHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TemporalHistoryBuffers::BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap)
|
||||||
|
{
|
||||||
|
for (unsigned index = 0; index < historyCap; ++index)
|
||||||
|
{
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, ResolveTexture(sourceHistoryRing, currentSourceTexture, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint temporalBase = kSourceHistoryTextureUnitBase + historyCap;
|
||||||
|
const Ring* temporalRing = nullptr;
|
||||||
|
auto it = preLayerHistoryByLayerId.find(state.layerId);
|
||||||
|
if (it != preLayerHistoryByLayerId.end())
|
||||||
|
temporalRing = &it->second;
|
||||||
|
|
||||||
|
for (unsigned index = 0; index < historyCap; ++index)
|
||||||
|
{
|
||||||
|
glActiveTexture(GL_TEXTURE0 + temporalBase + index);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, temporalRing ? ResolveTexture(*temporalRing, currentSourceTexture, index) : currentSourceTexture);
|
||||||
|
}
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
|
||||||
|
{
|
||||||
|
if (ring.filledCount == 0 || ring.slots.empty())
|
||||||
|
return fallbackTexture;
|
||||||
|
|
||||||
|
const std::size_t clampedOffset = std::min<std::size_t>(framesAgo, ring.filledCount - 1);
|
||||||
|
const std::size_t newestIndex = (ring.nextWriteIndex + ring.slots.size() - 1) % ring.slots.size();
|
||||||
|
const std::size_t slotIndex = (newestIndex + ring.slots.size() - clampedOffset) % ring.slots.size();
|
||||||
|
return ring.slots[slotIndex].texture != 0 ? ring.slots[slotIndex].texture : fallbackTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned TemporalHistoryBuffers::SourceAvailableCount() const
|
||||||
|
{
|
||||||
|
return static_cast<unsigned>(sourceHistoryRing.filledCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned TemporalHistoryBuffers::AvailableCountForLayer(const std::string& layerId) const
|
||||||
|
{
|
||||||
|
auto it = preLayerHistoryByLayerId.find(layerId);
|
||||||
|
if (it == preLayerHistoryByLayerId.end())
|
||||||
|
return 0;
|
||||||
|
return static_cast<unsigned>(it->second.filledCount);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <gl/gl.h>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct RuntimeRenderState;
|
||||||
|
|
||||||
|
class TemporalHistoryBuffers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
GLuint texture = 0;
|
||||||
|
GLuint framebuffer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Ring
|
||||||
|
{
|
||||||
|
std::vector<Slot> slots;
|
||||||
|
std::size_t nextWriteIndex = 0;
|
||||||
|
std::size_t filledCount = 0;
|
||||||
|
unsigned effectiveLength = 0;
|
||||||
|
TemporalHistorySource historySource = TemporalHistorySource::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const;
|
||||||
|
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||||
|
bool CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||||
|
void DestroyRing(Ring& ring);
|
||||||
|
void DestroyResources();
|
||||||
|
void ResetState();
|
||||||
|
void PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, 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 BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap);
|
||||||
|
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
||||||
|
unsigned SourceAvailableCount() const;
|
||||||
|
unsigned AvailableCountForLayer(const std::string& layerId) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ring sourceHistoryRing;
|
||||||
|
std::map<std::string, Ring> preLayerHistoryByLayerId;
|
||||||
|
bool mNeedsReset = true;
|
||||||
|
};
|
||||||
@@ -60,13 +60,19 @@
|
|||||||
#define GL_DYNAMIC_DRAW 0x88E8
|
#define GL_DYNAMIC_DRAW 0x88E8
|
||||||
#define GL_UNIFORM_BUFFER 0x8A11
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
#define GL_RGBA8 0x8058
|
#define GL_RGBA8 0x8058
|
||||||
|
#define GL_RGBA16F 0x881A
|
||||||
|
#define GL_TEXTURE0 0x84C0
|
||||||
|
#define GL_ACTIVE_TEXTURE 0x84E0
|
||||||
#define GL_ARRAY_BUFFER 0x8892
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
||||||
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
||||||
|
#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
|
||||||
#define GL_FRAGMENT_SHADER 0x8B30
|
#define GL_FRAGMENT_SHADER 0x8B30
|
||||||
#define GL_VERTEX_SHADER 0x8B31
|
#define GL_VERTEX_SHADER 0x8B31
|
||||||
#define GL_COMPILE_STATUS 0x8B81
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
#define GL_LINK_STATUS 0x8B82
|
#define GL_LINK_STATUS 0x8B82
|
||||||
|
#define GL_INVALID_INDEX 0xFFFFFFFFu
|
||||||
|
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
|
||||||
#define GL_RENDERBUFFER_EXT 0x8D41
|
#define GL_RENDERBUFFER_EXT 0x8D41
|
||||||
#define GL_FRAMEBUFFER_EXT 0x8D40
|
#define GL_FRAMEBUFFER_EXT 0x8D40
|
||||||
#define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5
|
#define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
||||||
|
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||||
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
|
constexpr unsigned kPrerollFrameCount = 8;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
class ScopedGlShader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScopedGlShader(GLuint shader = 0) : mShader(shader) {}
|
||||||
|
~ScopedGlShader() { reset(); }
|
||||||
|
|
||||||
|
ScopedGlShader(const ScopedGlShader&) = delete;
|
||||||
|
ScopedGlShader& operator=(const ScopedGlShader&) = delete;
|
||||||
|
|
||||||
|
GLuint get() const { return mShader; }
|
||||||
|
GLuint release()
|
||||||
|
{
|
||||||
|
GLuint shader = mShader;
|
||||||
|
mShader = 0;
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
void reset(GLuint shader = 0)
|
||||||
|
{
|
||||||
|
if (mShader != 0)
|
||||||
|
glDeleteShader(mShader);
|
||||||
|
mShader = shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint mShader;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScopedGlProgram
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScopedGlProgram(GLuint program = 0) : mProgram(program) {}
|
||||||
|
~ScopedGlProgram() { reset(); }
|
||||||
|
|
||||||
|
ScopedGlProgram(const ScopedGlProgram&) = delete;
|
||||||
|
ScopedGlProgram& operator=(const ScopedGlProgram&) = delete;
|
||||||
|
|
||||||
|
GLuint get() const { return mProgram; }
|
||||||
|
GLuint release()
|
||||||
|
{
|
||||||
|
GLuint program = mProgram;
|
||||||
|
mProgram = 0;
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
void reset(GLuint program = 0)
|
||||||
|
{
|
||||||
|
if (mProgram != 0)
|
||||||
|
glDeleteProgram(mProgram);
|
||||||
|
mProgram = program;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint mProgram;
|
||||||
|
};
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void ConfigureByteFrameTexture(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_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)
|
||||||
|
{
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
|
||||||
|
glGenBuffers(1, &mTextureUploadBuffer);
|
||||||
|
|
||||||
|
glGenTextures(1, &mCaptureTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
|
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenTextures(1, &mDecodedTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
|
||||||
|
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenTextures(1, &mLayerTempTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mLayerTempTexture);
|
||||||
|
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenFramebuffers(1, &mDecodeFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mLayerTempFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mIdFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mOutputFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mOutputPackFrameBuf);
|
||||||
|
glGenRenderbuffers(1, &mIdColorBuf);
|
||||||
|
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||||
|
glGenVertexArrays(1, &mFullscreenVAO);
|
||||||
|
glGenBuffers(1, &mGlobalParamsUBO);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Cannot initialize decode framebuffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Cannot initialize layer framebuffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
glGenTextures(1, &mFBOTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mFBOTexture);
|
||||||
|
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||||
|
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Cannot initialize framebuffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &mOutputTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mOutputTexture);
|
||||||
|
ConfigureDisplayFrameTexture(outputFrameWidth, outputFrameHeight);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Cannot initialize output framebuffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &mOutputPackTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mOutputPackTexture);
|
||||||
|
ConfigureByteFrameTexture(outputPackTextureWidth, outputFrameHeight);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mOutputPackFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputPackTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Cannot initialize output pack framebuffer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindVertexArray(mFullscreenVAO);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, 1024, NULL, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
||||||
|
{
|
||||||
|
mDecodeProgram = program;
|
||||||
|
mDecodeVertexShader = vertexShader;
|
||||||
|
mDecodeFragmentShader = fragmentShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
||||||
|
{
|
||||||
|
mOutputPackProgram = program;
|
||||||
|
mOutputPackVertexShader = vertexShader;
|
||||||
|
mOutputPackFragmentShader = fragmentShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::ResizeView(int width, int height)
|
||||||
|
{
|
||||||
|
mViewWidth = width;
|
||||||
|
mViewHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight)
|
||||||
|
{
|
||||||
|
int destWidth = mViewWidth;
|
||||||
|
int destHeight = mViewHeight;
|
||||||
|
int destX = 0;
|
||||||
|
int destY = 0;
|
||||||
|
|
||||||
|
if (outputFrameWidth > 0 && outputFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0)
|
||||||
|
{
|
||||||
|
const double frameAspect = static_cast<double>(outputFrameWidth) / static_cast<double>(outputFrameHeight);
|
||||||
|
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
|
||||||
|
|
||||||
|
if (viewAspect > frameAspect)
|
||||||
|
{
|
||||||
|
destHeight = mViewHeight;
|
||||||
|
destWidth = static_cast<int>(destHeight * frameAspect + 0.5);
|
||||||
|
destX = (mViewWidth - destWidth) / 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destWidth = mViewWidth;
|
||||||
|
destHeight = static_cast<int>(destWidth / frameAspect + 0.5);
|
||||||
|
destY = (mViewHeight - destHeight) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glViewport(0, 0, mViewWidth, mViewHeight);
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glBlitFramebuffer(0, 0, outputFrameWidth, outputFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
|
||||||
|
SwapBuffers(hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::DestroyResources()
|
||||||
|
{
|
||||||
|
if (mFullscreenVAO != 0)
|
||||||
|
glDeleteVertexArrays(1, &mFullscreenVAO);
|
||||||
|
if (mGlobalParamsUBO != 0)
|
||||||
|
glDeleteBuffers(1, &mGlobalParamsUBO);
|
||||||
|
if (mDecodeFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mDecodeFrameBuf);
|
||||||
|
if (mLayerTempFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mLayerTempFrameBuf);
|
||||||
|
if (mIdFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mIdFrameBuf);
|
||||||
|
if (mOutputFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mOutputFrameBuf);
|
||||||
|
if (mOutputPackFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mOutputPackFrameBuf);
|
||||||
|
if (mIdColorBuf != 0)
|
||||||
|
glDeleteRenderbuffers(1, &mIdColorBuf);
|
||||||
|
if (mIdDepthBuf != 0)
|
||||||
|
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
||||||
|
if (mCaptureTexture != 0)
|
||||||
|
glDeleteTextures(1, &mCaptureTexture);
|
||||||
|
if (mDecodedTexture != 0)
|
||||||
|
glDeleteTextures(1, &mDecodedTexture);
|
||||||
|
if (mLayerTempTexture != 0)
|
||||||
|
glDeleteTextures(1, &mLayerTempTexture);
|
||||||
|
if (mFBOTexture != 0)
|
||||||
|
glDeleteTextures(1, &mFBOTexture);
|
||||||
|
if (mOutputTexture != 0)
|
||||||
|
glDeleteTextures(1, &mOutputTexture);
|
||||||
|
if (mOutputPackTexture != 0)
|
||||||
|
glDeleteTextures(1, &mOutputPackTexture);
|
||||||
|
if (mTextureUploadBuffer != 0)
|
||||||
|
glDeleteBuffers(1, &mTextureUploadBuffer);
|
||||||
|
|
||||||
|
mFullscreenVAO = 0;
|
||||||
|
mGlobalParamsUBO = 0;
|
||||||
|
mDecodeFrameBuf = 0;
|
||||||
|
mLayerTempFrameBuf = 0;
|
||||||
|
mIdFrameBuf = 0;
|
||||||
|
mOutputFrameBuf = 0;
|
||||||
|
mOutputPackFrameBuf = 0;
|
||||||
|
mIdColorBuf = 0;
|
||||||
|
mIdDepthBuf = 0;
|
||||||
|
mCaptureTexture = 0;
|
||||||
|
mDecodedTexture = 0;
|
||||||
|
mLayerTempTexture = 0;
|
||||||
|
mFBOTexture = 0;
|
||||||
|
mOutputTexture = 0;
|
||||||
|
mOutputPackTexture = 0;
|
||||||
|
mTextureUploadBuffer = 0;
|
||||||
|
mGlobalParamsUBOSize = 0;
|
||||||
|
|
||||||
|
mTemporalHistory.DestroyResources();
|
||||||
|
DestroyLayerPrograms();
|
||||||
|
DestroyDecodeShaderProgram();
|
||||||
|
DestroyOutputPackShaderProgram();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
||||||
|
{
|
||||||
|
for (LayerProgram::TextureBinding& binding : layerProgram.textureBindings)
|
||||||
|
{
|
||||||
|
if (binding.texture != 0)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &binding.texture);
|
||||||
|
binding.texture = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layerProgram.textureBindings.clear();
|
||||||
|
|
||||||
|
for (LayerProgram::TextBinding& binding : layerProgram.textBindings)
|
||||||
|
{
|
||||||
|
if (binding.texture != 0)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &binding.texture);
|
||||||
|
binding.texture = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layerProgram.textBindings.clear();
|
||||||
|
|
||||||
|
if (layerProgram.program != 0)
|
||||||
|
{
|
||||||
|
glDeleteProgram(layerProgram.program);
|
||||||
|
layerProgram.program = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerProgram.fragmentShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(layerProgram.fragmentShader);
|
||||||
|
layerProgram.fragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerProgram.vertexShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(layerProgram.vertexShader);
|
||||||
|
layerProgram.vertexShader = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::DestroyLayerPrograms()
|
||||||
|
{
|
||||||
|
for (LayerProgram& layerProgram : mLayerPrograms)
|
||||||
|
DestroySingleLayerProgram(layerProgram);
|
||||||
|
mLayerPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::DestroyDecodeShaderProgram()
|
||||||
|
{
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
{
|
||||||
|
glDeleteProgram(mDecodeProgram);
|
||||||
|
mDecodeProgram = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDecodeFragmentShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mDecodeFragmentShader);
|
||||||
|
mDecodeFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDecodeVertexShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mDecodeVertexShader);
|
||||||
|
mDecodeVertexShader = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderer::DestroyOutputPackShaderProgram()
|
||||||
|
{
|
||||||
|
if (mOutputPackProgram != 0)
|
||||||
|
{
|
||||||
|
glDeleteProgram(mOutputPackProgram);
|
||||||
|
mOutputPackProgram = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOutputPackFragmentShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mOutputPackFragmentShader);
|
||||||
|
mOutputPackFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOutputPackVertexShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mOutputPackVertexShader);
|
||||||
|
mOutputPackVertexShader = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
#include "TemporalHistoryBuffers.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class OpenGLRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct LayerProgram
|
||||||
|
{
|
||||||
|
struct TextureBinding
|
||||||
|
{
|
||||||
|
std::string samplerName;
|
||||||
|
std::filesystem::path sourcePath;
|
||||||
|
GLuint texture = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextBinding
|
||||||
|
{
|
||||||
|
std::string parameterId;
|
||||||
|
std::string samplerName;
|
||||||
|
std::string fontId;
|
||||||
|
GLuint texture = 0;
|
||||||
|
std::string renderedText;
|
||||||
|
unsigned renderedWidth = 0;
|
||||||
|
unsigned renderedHeight = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
GLuint shaderTextureBase = 0;
|
||||||
|
GLuint program = 0;
|
||||||
|
GLuint vertexShader = 0;
|
||||||
|
GLuint fragmentShader = 0;
|
||||||
|
std::vector<TextureBinding> textureBindings;
|
||||||
|
std::vector<TextBinding> textBindings;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLuint CaptureTexture() const { return mCaptureTexture; }
|
||||||
|
GLuint DecodedTexture() const { return mDecodedTexture; }
|
||||||
|
GLuint LayerTempTexture() const { return mLayerTempTexture; }
|
||||||
|
GLuint CompositeTexture() const { return mFBOTexture; }
|
||||||
|
GLuint OutputTexture() const { return mOutputTexture; }
|
||||||
|
GLuint OutputPackTexture() const { return mOutputPackTexture; }
|
||||||
|
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
|
||||||
|
GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; }
|
||||||
|
GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; }
|
||||||
|
GLuint CompositeFramebuffer() const { return mIdFrameBuf; }
|
||||||
|
GLuint OutputFramebuffer() const { return mOutputFrameBuf; }
|
||||||
|
GLuint OutputPackFramebuffer() const { return mOutputPackFrameBuf; }
|
||||||
|
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
|
||||||
|
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
||||||
|
GLuint DecodeProgram() const { return mDecodeProgram; }
|
||||||
|
GLuint OutputPackProgram() const { return mOutputPackProgram; }
|
||||||
|
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
||||||
|
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
||||||
|
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||||
|
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
||||||
|
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
||||||
|
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
||||||
|
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
||||||
|
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||||
|
void SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||||
|
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
|
||||||
|
void ResizeView(int width, int height);
|
||||||
|
void PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||||
|
void DestroyResources();
|
||||||
|
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||||
|
void DestroyLayerPrograms();
|
||||||
|
void DestroyDecodeShaderProgram();
|
||||||
|
void DestroyOutputPackShaderProgram();
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLuint mCaptureTexture = 0;
|
||||||
|
GLuint mDecodedTexture = 0;
|
||||||
|
GLuint mLayerTempTexture = 0;
|
||||||
|
GLuint mFBOTexture = 0;
|
||||||
|
GLuint mOutputTexture = 0;
|
||||||
|
GLuint mOutputPackTexture = 0;
|
||||||
|
GLuint mTextureUploadBuffer = 0;
|
||||||
|
GLuint mDecodeFrameBuf = 0;
|
||||||
|
GLuint mLayerTempFrameBuf = 0;
|
||||||
|
GLuint mIdFrameBuf = 0;
|
||||||
|
GLuint mOutputFrameBuf = 0;
|
||||||
|
GLuint mOutputPackFrameBuf = 0;
|
||||||
|
GLuint mIdColorBuf = 0;
|
||||||
|
GLuint mIdDepthBuf = 0;
|
||||||
|
GLuint mFullscreenVAO = 0;
|
||||||
|
GLuint mGlobalParamsUBO = 0;
|
||||||
|
GLuint mDecodeProgram = 0;
|
||||||
|
GLuint mDecodeVertexShader = 0;
|
||||||
|
GLuint mDecodeFragmentShader = 0;
|
||||||
|
GLuint mOutputPackProgram = 0;
|
||||||
|
GLuint mOutputPackVertexShader = 0;
|
||||||
|
GLuint mOutputPackFragmentShader = 0;
|
||||||
|
GLsizeiptr mGlobalParamsUBOSize = 0;
|
||||||
|
int mViewWidth = 0;
|
||||||
|
int mViewHeight = 0;
|
||||||
|
std::vector<LayerProgram> mLayerPrograms;
|
||||||
|
TemporalHistoryBuffers mTemporalHistory;
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#include "GlShaderSources.h"
|
||||||
|
|
||||||
|
const char* kFullscreenTriangleVertexShaderSource =
|
||||||
|
"#version 430 core\n"
|
||||||
|
"out vec2 vTexCoord;\n"
|
||||||
|
"void main()\n"
|
||||||
|
"{\n"
|
||||||
|
" vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n"
|
||||||
|
" vec2 texCoords[3] = vec2[3](vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0));\n"
|
||||||
|
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
|
||||||
|
" vTexCoord = texCoords[gl_VertexID];\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
const char* kDecodeFragmentShaderSource =
|
||||||
|
"#version 430 core\n"
|
||||||
|
"layout(binding = 2) uniform sampler2D uPackedVideoInput;\n"
|
||||||
|
"uniform vec2 uPackedVideoResolution;\n"
|
||||||
|
"uniform vec2 uDecodedVideoResolution;\n"
|
||||||
|
"uniform int uInputPixelFormat;\n"
|
||||||
|
"in vec2 vTexCoord;\n"
|
||||||
|
"layout(location = 0) out vec4 fragColor;\n"
|
||||||
|
"vec4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n"
|
||||||
|
"{\n"
|
||||||
|
" Y = (Y * 256.0 - 16.0) / 219.0;\n"
|
||||||
|
" Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||||
|
" Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||||
|
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
|
||||||
|
"}\n"
|
||||||
|
"vec4 rec709YCbCr10_2rgba(float Y, float Cb, float Cr, float a)\n"
|
||||||
|
"{\n"
|
||||||
|
" Y = (Y - 64.0) / 876.0;\n"
|
||||||
|
" Cb = (Cb - 64.0) / 896.0 - 0.5;\n"
|
||||||
|
" Cr = (Cr - 64.0) / 896.0 - 0.5;\n"
|
||||||
|
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
|
||||||
|
"}\n"
|
||||||
|
"uint loadV210Word(ivec2 coord)\n"
|
||||||
|
"{\n"
|
||||||
|
" vec4 b = round(texelFetch(uPackedVideoInput, coord, 0) * 255.0);\n"
|
||||||
|
" return uint(b.r) | (uint(b.g) << 8) | (uint(b.b) << 16) | (uint(b.a) << 24);\n"
|
||||||
|
"}\n"
|
||||||
|
"float v210Component(uint word, int index)\n"
|
||||||
|
"{\n"
|
||||||
|
" return float((word >> uint(index * 10)) & 1023u);\n"
|
||||||
|
"}\n"
|
||||||
|
"vec4 decodeUyvy8(ivec2 outputCoord, ivec2 packedSize)\n"
|
||||||
|
"{\n"
|
||||||
|
" ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, packedSize.x - 1), clamp(outputCoord.y, 0, packedSize.y - 1));\n"
|
||||||
|
" vec4 macroPixel = texelFetch(uPackedVideoInput, packedCoord, 0);\n"
|
||||||
|
" float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;\n"
|
||||||
|
" return rec709YCbCr2rgba(ySample, macroPixel.b, macroPixel.r, 1.0);\n"
|
||||||
|
"}\n"
|
||||||
|
"vec4 decodeV210(ivec2 outputCoord, ivec2 packedSize)\n"
|
||||||
|
"{\n"
|
||||||
|
" int group = outputCoord.x / 6;\n"
|
||||||
|
" int pixel = outputCoord.x - group * 6;\n"
|
||||||
|
" int wordBase = group * 4;\n"
|
||||||
|
" ivec2 rowBase = ivec2(wordBase, clamp(outputCoord.y, 0, packedSize.y - 1));\n"
|
||||||
|
" uint w0 = loadV210Word(ivec2(min(rowBase.x + 0, packedSize.x - 1), rowBase.y));\n"
|
||||||
|
" uint w1 = loadV210Word(ivec2(min(rowBase.x + 1, packedSize.x - 1), rowBase.y));\n"
|
||||||
|
" uint w2 = loadV210Word(ivec2(min(rowBase.x + 2, packedSize.x - 1), rowBase.y));\n"
|
||||||
|
" uint w3 = loadV210Word(ivec2(min(rowBase.x + 3, packedSize.x - 1), rowBase.y));\n"
|
||||||
|
" float y0 = v210Component(w0, 1);\n"
|
||||||
|
" float y1 = v210Component(w1, 0);\n"
|
||||||
|
" float y2 = v210Component(w1, 2);\n"
|
||||||
|
" float y3 = v210Component(w2, 1);\n"
|
||||||
|
" float y4 = v210Component(w3, 0);\n"
|
||||||
|
" float y5 = v210Component(w3, 2);\n"
|
||||||
|
" float cb0 = v210Component(w0, 0);\n"
|
||||||
|
" float cr0 = v210Component(w0, 2);\n"
|
||||||
|
" float cb2 = v210Component(w1, 1);\n"
|
||||||
|
" float cr2 = v210Component(w2, 0);\n"
|
||||||
|
" float cb4 = v210Component(w2, 2);\n"
|
||||||
|
" float cr4 = v210Component(w3, 1);\n"
|
||||||
|
" float ySample = pixel == 0 ? y0 : pixel == 1 ? y1 : pixel == 2 ? y2 : pixel == 3 ? y3 : pixel == 4 ? y4 : y5;\n"
|
||||||
|
" float cbSample = pixel < 2 ? cb0 : pixel < 4 ? cb2 : cb4;\n"
|
||||||
|
" float crSample = pixel < 2 ? cr0 : pixel < 4 ? cr2 : cr4;\n"
|
||||||
|
" return rec709YCbCr10_2rgba(ySample, cbSample, crSample, 1.0);\n"
|
||||||
|
"}\n"
|
||||||
|
"void main()\n"
|
||||||
|
"{\n"
|
||||||
|
" vec2 correctedUv = vec2(vTexCoord.x, 1.0 - vTexCoord.y);\n"
|
||||||
|
" ivec2 decodedSize = ivec2(max(uDecodedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||||
|
" ivec2 outputCoord = clamp(ivec2(correctedUv * vec2(decodedSize)), ivec2(0, 0), decodedSize - ivec2(1, 1));\n"
|
||||||
|
" ivec2 packedSize = ivec2(max(uPackedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||||
|
" fragColor = uInputPixelFormat == 1 ? decodeV210(outputCoord, packedSize) : decodeUyvy8(outputCoord, packedSize);\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
const char* kOutputPackFragmentShaderSource =
|
||||||
|
"#version 430 core\n"
|
||||||
|
"layout(binding = 0) uniform sampler2D uOutputRgb;\n"
|
||||||
|
"uniform vec2 uOutputVideoResolution;\n"
|
||||||
|
"uniform float uActiveV210Words;\n"
|
||||||
|
"in vec2 vTexCoord;\n"
|
||||||
|
"layout(location = 0) out vec4 fragColor;\n"
|
||||||
|
"vec3 rgbAt(int x, int y)\n"
|
||||||
|
"{\n"
|
||||||
|
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
||||||
|
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0).rgb, vec3(0.0), vec3(1.0));\n"
|
||||||
|
"}\n"
|
||||||
|
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
|
||||||
|
"{\n"
|
||||||
|
" float y = dot(rgb, vec3(0.2126, 0.7152, 0.0722));\n"
|
||||||
|
" float cb = (rgb.b - y) / 1.8556 + 0.5;\n"
|
||||||
|
" float cr = (rgb.r - y) / 1.5748 + 0.5;\n"
|
||||||
|
" return vec3(clamp(round(64.0 + y * 876.0), 64.0, 940.0), clamp(round(64.0 + cb * 896.0), 64.0, 960.0), clamp(round(64.0 + cr * 896.0), 64.0, 960.0));\n"
|
||||||
|
"}\n"
|
||||||
|
"uint makeWord(float a, float b, float c)\n"
|
||||||
|
"{\n"
|
||||||
|
" return (uint(a) & 1023u) | ((uint(b) & 1023u) << 10) | ((uint(c) & 1023u) << 20);\n"
|
||||||
|
"}\n"
|
||||||
|
"vec4 wordToBytes(uint word)\n"
|
||||||
|
"{\n"
|
||||||
|
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n"
|
||||||
|
"}\n"
|
||||||
|
"void main()\n"
|
||||||
|
"{\n"
|
||||||
|
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
|
||||||
|
" if (float(outCoord.x) >= uActiveV210Words)\n"
|
||||||
|
" {\n"
|
||||||
|
" fragColor = vec4(0.0);\n"
|
||||||
|
" return;\n"
|
||||||
|
" }\n"
|
||||||
|
" int group = outCoord.x / 4;\n"
|
||||||
|
" int wordIndex = outCoord.x - group * 4;\n"
|
||||||
|
" int pixelBase = group * 6;\n"
|
||||||
|
" int y = outCoord.y;\n"
|
||||||
|
" vec3 c0 = rgbToLegalYcbcr10(rgbAt(pixelBase + 0, y));\n"
|
||||||
|
" vec3 c1 = rgbToLegalYcbcr10(rgbAt(pixelBase + 1, y));\n"
|
||||||
|
" vec3 c2 = rgbToLegalYcbcr10(rgbAt(pixelBase + 2, y));\n"
|
||||||
|
" vec3 c3 = rgbToLegalYcbcr10(rgbAt(pixelBase + 3, y));\n"
|
||||||
|
" vec3 c4 = rgbToLegalYcbcr10(rgbAt(pixelBase + 4, y));\n"
|
||||||
|
" vec3 c5 = rgbToLegalYcbcr10(rgbAt(pixelBase + 5, y));\n"
|
||||||
|
" float cb0 = round((c0.y + c1.y) * 0.5);\n"
|
||||||
|
" float cr0 = round((c0.z + c1.z) * 0.5);\n"
|
||||||
|
" float cb2 = round((c2.y + c3.y) * 0.5);\n"
|
||||||
|
" float cr2 = round((c2.z + c3.z) * 0.5);\n"
|
||||||
|
" float cb4 = round((c4.y + c5.y) * 0.5);\n"
|
||||||
|
" float cr4 = round((c4.z + c5.z) * 0.5);\n"
|
||||||
|
" uint word = wordIndex == 0 ? makeWord(cb0, c0.x, cr0) : wordIndex == 1 ? makeWord(c1.x, cb2, c2.x) : wordIndex == 2 ? makeWord(cr2, c3.x, cb4) : makeWord(c4.x, cr4, c5.x);\n"
|
||||||
|
" fragColor = wordToBytes(word);\n"
|
||||||
|
"}\n";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
extern const char* kFullscreenTriangleVertexShaderSource;
|
||||||
|
extern const char* kDecodeFragmentShaderSource;
|
||||||
|
extern const char* kOutputPackFragmentShaderSource;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "GlobalParamsBuffer.h"
|
||||||
|
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
#include "Std140Buffer.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
|
||||||
|
mRenderer(renderer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> buffer;
|
||||||
|
buffer.reserve(512);
|
||||||
|
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.utcTimeSeconds));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.utcOffsetSeconds));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.startupRandom));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(state.bypass));
|
||||||
|
const unsigned effectiveSourceHistoryLength = availableSourceHistoryLength < state.effectiveTemporalHistoryLength
|
||||||
|
? availableSourceHistoryLength
|
||||||
|
: state.effectiveTemporalHistoryLength;
|
||||||
|
const unsigned effectiveTemporalHistoryLength = (state.temporalHistorySource == TemporalHistorySource::PreLayerInput)
|
||||||
|
? (availableTemporalHistoryLength < state.effectiveTemporalHistoryLength ? availableTemporalHistoryLength : state.effectiveTemporalHistoryLength)
|
||||||
|
: 0u;
|
||||||
|
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
|
||||||
|
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
|
||||||
|
|
||||||
|
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||||
|
{
|
||||||
|
auto valueIt = state.parameterValues.find(definition.id);
|
||||||
|
const ShaderParameterValue value = valueIt != state.parameterValues.end()
|
||||||
|
? valueIt->second
|
||||||
|
: ShaderParameterValue();
|
||||||
|
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast<float>(value.numberValues[0]));
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
AppendStd140Vec2(buffer,
|
||||||
|
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
|
||||||
|
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
AppendStd140Vec4(buffer,
|
||||||
|
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
|
||||||
|
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
|
||||||
|
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
|
||||||
|
value.numberValues.size() > 3 ? static_cast<float>(value.numberValues[3]) : 1.0f);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
AppendStd140Int(buffer, value.booleanValue ? 1 : 0);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
{
|
||||||
|
int selectedIndex = 0;
|
||||||
|
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
|
||||||
|
{
|
||||||
|
if (definition.enumOptions[optionIndex].value == value.enumValue)
|
||||||
|
{
|
||||||
|
selectedIndex = static_cast<int>(optionIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppendStd140Int(buffer, selectedIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||||
|
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.resize(AlignStd140(buffer.size(), 16), 0);
|
||||||
|
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, mRenderer.GlobalParamsUBO());
|
||||||
|
if (mRenderer.GlobalParamsUBOSize() != static_cast<GLsizeiptr>(buffer.size()))
|
||||||
|
{
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW);
|
||||||
|
mRenderer.SetGlobalParamsUBOSize(static_cast<GLsizeiptr>(buffer.size()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast<GLsizeiptr>(buffer.size()), buffer.data());
|
||||||
|
}
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mRenderer.GlobalParamsUBO());
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
class GlobalParamsBuffer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
||||||
|
|
||||||
|
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#include "OpenGLShaderPrograms.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
|
||||||
|
mRenderer(renderer),
|
||||||
|
mRuntimeHost(runtimeHost),
|
||||||
|
mGlobalParamsBuffer(renderer),
|
||||||
|
mCompiler(renderer, runtimeHost, mTextureBindings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost.GetLayerRenderStates(inputFrameWidth, inputFrameHeight);
|
||||||
|
std::string temporalError;
|
||||||
|
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||||
|
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(layerStates, historyCap, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mRenderer.TemporalHistory().EnsureResources(layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LayerProgram> newPrograms;
|
||||||
|
newPrograms.reserve(layerStates.size());
|
||||||
|
|
||||||
|
for (const RuntimeRenderState& state : layerStates)
|
||||||
|
{
|
||||||
|
LayerProgram layerProgram;
|
||||||
|
if (!mCompiler.CompileLayerProgram(state, layerProgram, errorMessageSize, errorMessage))
|
||||||
|
{
|
||||||
|
for (LayerProgram& program : newPrograms)
|
||||||
|
DestroySingleLayerProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newPrograms.push_back(layerProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyLayerPrograms();
|
||||||
|
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||||
|
mCommittedLayerStates = layerStates;
|
||||||
|
|
||||||
|
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||||
|
mRuntimeHost.ClearReloadRequest();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
if (!preparedBuild.succeeded)
|
||||||
|
{
|
||||||
|
CopyErrorMessage(preparedBuild.message, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string temporalError;
|
||||||
|
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||||
|
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.layerStates, historyCap, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<LayerProgram> newPrograms;
|
||||||
|
newPrograms.reserve(preparedBuild.layers.size());
|
||||||
|
|
||||||
|
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
|
||||||
|
{
|
||||||
|
LayerProgram layerProgram;
|
||||||
|
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.fragmentShaderSource, layerProgram, errorMessageSize, errorMessage))
|
||||||
|
{
|
||||||
|
for (LayerProgram& program : newPrograms)
|
||||||
|
DestroySingleLayerProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
newPrograms.push_back(layerProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyLayerPrograms();
|
||||||
|
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||||
|
mCommittedLayerStates = preparedBuild.layerStates;
|
||||||
|
|
||||||
|
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||||
|
mRuntimeHost.ClearReloadRequest();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
return mCompiler.CompileDecodeShader(errorMessageSize, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
return mCompiler.CompileOutputPackShader(errorMessageSize, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLShaderPrograms::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
||||||
|
{
|
||||||
|
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLShaderPrograms::DestroyLayerPrograms()
|
||||||
|
{
|
||||||
|
mRenderer.DestroyLayerPrograms();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLShaderPrograms::DestroyDecodeShaderProgram()
|
||||||
|
{
|
||||||
|
mRenderer.DestroyDecodeShaderProgram();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLShaderPrograms::ResetTemporalHistoryState()
|
||||||
|
{
|
||||||
|
mRenderer.TemporalHistory().ResetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
||||||
|
{
|
||||||
|
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
||||||
|
{
|
||||||
|
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GlobalParamsBuffer.h"
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
#include "ShaderBuildQueue.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
#include "ShaderProgramCompiler.h"
|
||||||
|
#include "ShaderTextureBindings.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class OpenGLShaderPrograms
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
|
||||||
|
OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost);
|
||||||
|
|
||||||
|
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||||
|
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||||
|
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||||
|
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
||||||
|
void DestroyLayerPrograms();
|
||||||
|
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||||
|
void DestroyDecodeShaderProgram();
|
||||||
|
void ResetTemporalHistoryState();
|
||||||
|
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
|
||||||
|
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||||
|
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
ShaderTextureBindings mTextureBindings;
|
||||||
|
GlobalParamsBuffer mGlobalParamsBuffer;
|
||||||
|
ShaderProgramCompiler mCompiler;
|
||||||
|
std::vector<RuntimeRenderState> mCommittedLayerStates;
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include "ShaderBuildQueue.h"
|
||||||
|
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderBuildQueue::ShaderBuildQueue(RuntimeHost& runtimeHost) :
|
||||||
|
mRuntimeHost(runtimeHost),
|
||||||
|
mWorkerThread([this]() { WorkerLoop(); })
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderBuildQueue::~ShaderBuildQueue()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderBuildQueue::RequestBuild(unsigned outputWidth, unsigned outputHeight)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mHasRequest = true;
|
||||||
|
++mRequestedGeneration;
|
||||||
|
mRequestedOutputWidth = outputWidth;
|
||||||
|
mRequestedOutputHeight = outputHeight;
|
||||||
|
mHasReadyBuild = false;
|
||||||
|
}
|
||||||
|
mCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderBuildQueue::TryConsumeReadyBuild(PreparedShaderBuild& build)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!mHasReadyBuild)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
build = std::move(mReadyBuild);
|
||||||
|
mReadyBuild = PreparedShaderBuild();
|
||||||
|
mHasReadyBuild = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderBuildQueue::Stop()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mStopping)
|
||||||
|
return;
|
||||||
|
mStopping = true;
|
||||||
|
}
|
||||||
|
mCondition.notify_one();
|
||||||
|
if (mWorkerThread.joinable())
|
||||||
|
mWorkerThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderBuildQueue::WorkerLoop()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
uint64_t generation = 0;
|
||||||
|
unsigned outputWidth = 0;
|
||||||
|
unsigned outputHeight = 0;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
mCondition.wait(lock, [this]() { return mStopping || mHasRequest; });
|
||||||
|
if (mStopping)
|
||||||
|
return;
|
||||||
|
|
||||||
|
generation = mRequestedGeneration;
|
||||||
|
outputWidth = mRequestedOutputWidth;
|
||||||
|
outputHeight = mRequestedOutputHeight;
|
||||||
|
mHasRequest = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
if (mCondition.wait_for(lock, kShaderBuildDebounce, [this, generation]() {
|
||||||
|
return mStopping || (mHasRequest && mRequestedGeneration != generation);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
if (mStopping)
|
||||||
|
return;
|
||||||
|
|
||||||
|
generation = mRequestedGeneration;
|
||||||
|
outputWidth = mRequestedOutputWidth;
|
||||||
|
outputHeight = mRequestedOutputHeight;
|
||||||
|
mHasRequest = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedShaderBuild build = Build(generation, outputWidth, outputHeight);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mStopping)
|
||||||
|
return;
|
||||||
|
if (generation != mRequestedGeneration)
|
||||||
|
continue;
|
||||||
|
mReadyBuild = std::move(build);
|
||||||
|
mHasReadyBuild = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight)
|
||||||
|
{
|
||||||
|
PreparedShaderBuild build;
|
||||||
|
build.generation = generation;
|
||||||
|
build.layerStates = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
|
||||||
|
build.layers.reserve(build.layerStates.size());
|
||||||
|
|
||||||
|
for (const RuntimeRenderState& state : build.layerStates)
|
||||||
|
{
|
||||||
|
PreparedLayerShader layer;
|
||||||
|
layer.state = state;
|
||||||
|
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, layer.fragmentShaderSource, build.message))
|
||||||
|
{
|
||||||
|
build.succeeded = false;
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
build.layers.push_back(std::move(layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
build.succeeded = true;
|
||||||
|
build.message = "Shader layers prepared successfully.";
|
||||||
|
return build;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
struct PreparedLayerShader
|
||||||
|
{
|
||||||
|
RuntimeRenderState state;
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PreparedShaderBuild
|
||||||
|
{
|
||||||
|
uint64_t generation = 0;
|
||||||
|
bool succeeded = false;
|
||||||
|
std::string message;
|
||||||
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
|
std::vector<PreparedLayerShader> layers;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ShaderBuildQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ShaderBuildQueue(RuntimeHost& runtimeHost);
|
||||||
|
~ShaderBuildQueue();
|
||||||
|
|
||||||
|
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
|
||||||
|
ShaderBuildQueue& operator=(const ShaderBuildQueue&) = delete;
|
||||||
|
|
||||||
|
void RequestBuild(unsigned outputWidth, unsigned outputHeight);
|
||||||
|
bool TryConsumeReadyBuild(PreparedShaderBuild& build);
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void WorkerLoop();
|
||||||
|
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
|
||||||
|
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
std::thread mWorkerThread;
|
||||||
|
std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
bool mStopping = false;
|
||||||
|
bool mHasRequest = false;
|
||||||
|
uint64_t mRequestedGeneration = 0;
|
||||||
|
unsigned mRequestedOutputWidth = 0;
|
||||||
|
unsigned mRequestedOutputHeight = 0;
|
||||||
|
bool mHasReadyBuild = false;
|
||||||
|
PreparedShaderBuild mReadyBuild;
|
||||||
|
};
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
#include "ShaderTextureBindings.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class ShaderProgramCompiler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
|
||||||
|
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
|
||||||
|
|
||||||
|
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||||
|
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||||
|
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||||
|
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
ShaderTextureBindings& mTextureBindings;
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#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());
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ShaderTextureBindings
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
|
||||||
|
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||||
|
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
||||||
|
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||||
|
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
inline std::size_t AlignStd140(std::size_t offset, std::size_t alignment)
|
||||||
|
{
|
||||||
|
const std::size_t mask = alignment - 1;
|
||||||
|
return (offset + mask) & ~mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename TValue>
|
||||||
|
inline void AppendStd140Value(std::vector<unsigned char>& buffer, std::size_t alignment, const TValue& value)
|
||||||
|
{
|
||||||
|
const std::size_t offset = AlignStd140(buffer.size(), alignment);
|
||||||
|
if (buffer.size() < offset + sizeof(TValue))
|
||||||
|
buffer.resize(offset + sizeof(TValue), 0);
|
||||||
|
std::memcpy(buffer.data() + offset, &value, sizeof(TValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void AppendStd140Float(std::vector<unsigned char>& buffer, float value)
|
||||||
|
{
|
||||||
|
AppendStd140Value(buffer, 4, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void AppendStd140Int(std::vector<unsigned char>& buffer, int value)
|
||||||
|
{
|
||||||
|
AppendStd140Value(buffer, 4, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void AppendStd140Vec2(std::vector<unsigned char>& buffer, float x, float y)
|
||||||
|
{
|
||||||
|
const std::size_t offset = AlignStd140(buffer.size(), 8);
|
||||||
|
if (buffer.size() < offset + sizeof(float) * 2)
|
||||||
|
buffer.resize(offset + sizeof(float) * 2, 0);
|
||||||
|
float values[2] = { x, y };
|
||||||
|
std::memcpy(buffer.data() + offset, values, sizeof(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void AppendStd140Vec4(std::vector<unsigned char>& buffer, float x, float y, float z, float w)
|
||||||
|
{
|
||||||
|
const std::size_t offset = AlignStd140(buffer.size(), 16);
|
||||||
|
if (buffer.size() < offset + sizeof(float) * 4)
|
||||||
|
buffer.resize(offset + sizeof(float) * 4, 0);
|
||||||
|
float values[4] = { x, y, z, w };
|
||||||
|
std::memcpy(buffer.data() + offset, values, sizeof(values));
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
#include "TextRasterizer.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr int kTextSdfSpread = 20;
|
||||||
|
constexpr float kTextFontPixelSize = 144.0f;
|
||||||
|
constexpr float kTextLayoutPadding = 48.0f;
|
||||||
|
constexpr float kSdfInfinity = 1.0e20f;
|
||||||
|
|
||||||
|
class GdiplusSession
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GdiplusSession()
|
||||||
|
{
|
||||||
|
Gdiplus::GdiplusStartupInput startupInput;
|
||||||
|
mStarted = Gdiplus::GdiplusStartup(&mToken, &startupInput, NULL) == Gdiplus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
~GdiplusSession()
|
||||||
|
{
|
||||||
|
if (mStarted)
|
||||||
|
Gdiplus::GdiplusShutdown(mToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
GdiplusSession(const GdiplusSession&) = delete;
|
||||||
|
GdiplusSession& operator=(const GdiplusSession&) = delete;
|
||||||
|
|
||||||
|
bool started() const { return mStarted; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ULONG_PTR mToken = 0;
|
||||||
|
bool mStarted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::wstring Utf8ToWide(const std::string& text)
|
||||||
|
{
|
||||||
|
if (text.empty())
|
||||||
|
return std::wstring();
|
||||||
|
const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0);
|
||||||
|
if (required <= 1)
|
||||||
|
return std::wstring();
|
||||||
|
std::wstring wide(static_cast<std::size_t>(required - 1), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required);
|
||||||
|
return wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DistanceTransform1D(const std::vector<float>& input, std::vector<float>& output, unsigned count)
|
||||||
|
{
|
||||||
|
std::vector<unsigned> locations(count, 0);
|
||||||
|
std::vector<float> boundaries(static_cast<std::size_t>(count) + 1, 0.0f);
|
||||||
|
|
||||||
|
unsigned segment = 0;
|
||||||
|
locations[0] = 0;
|
||||||
|
boundaries[0] = -kSdfInfinity;
|
||||||
|
boundaries[1] = kSdfInfinity;
|
||||||
|
|
||||||
|
for (unsigned q = 1; q < count; ++q)
|
||||||
|
{
|
||||||
|
float intersection = 0.0f;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
const unsigned location = locations[segment];
|
||||||
|
intersection =
|
||||||
|
((input[q] + static_cast<float>(q * q)) - (input[location] + static_cast<float>(location * location))) /
|
||||||
|
(2.0f * static_cast<float>(q) - 2.0f * static_cast<float>(location));
|
||||||
|
if (intersection > boundaries[segment] || segment == 0)
|
||||||
|
break;
|
||||||
|
--segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
++segment;
|
||||||
|
locations[segment] = q;
|
||||||
|
boundaries[segment] = intersection;
|
||||||
|
boundaries[segment + 1] = kSdfInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
segment = 0;
|
||||||
|
for (unsigned q = 0; q < count; ++q)
|
||||||
|
{
|
||||||
|
while (boundaries[segment + 1] < static_cast<float>(q))
|
||||||
|
++segment;
|
||||||
|
const unsigned location = locations[segment];
|
||||||
|
const float delta = static_cast<float>(q) - static_cast<float>(location);
|
||||||
|
output[q] = delta * delta + input[location];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> DistanceTransform2D(const std::vector<unsigned char>& targetMask, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<float> rowInput(width, 0.0f);
|
||||||
|
std::vector<float> rowOutput(width, 0.0f);
|
||||||
|
std::vector<float> columnInput(height, 0.0f);
|
||||||
|
std::vector<float> columnOutput(height, 0.0f);
|
||||||
|
std::vector<float> rowDistance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||||
|
std::vector<float> distance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||||
|
|
||||||
|
for (unsigned y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
for (unsigned x = 0; x < width; ++x)
|
||||||
|
rowInput[x] = targetMask[static_cast<std::size_t>(y) * width + x] ? 0.0f : kSdfInfinity;
|
||||||
|
DistanceTransform1D(rowInput, rowOutput, width);
|
||||||
|
for (unsigned x = 0; x < width; ++x)
|
||||||
|
rowDistance[static_cast<std::size_t>(y) * width + x] = rowOutput[x];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned x = 0; x < width; ++x)
|
||||||
|
{
|
||||||
|
for (unsigned y = 0; y < height; ++y)
|
||||||
|
columnInput[y] = rowDistance[static_cast<std::size_t>(y) * width + x];
|
||||||
|
DistanceTransform1D(columnInput, columnOutput, height);
|
||||||
|
for (unsigned y = 0; y < height; ++y)
|
||||||
|
distance[static_cast<std::size_t>(y) * width + x] = columnOutput[y];
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> BuildTextSdfTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> insideMask(static_cast<std::size_t>(width) * height, 0);
|
||||||
|
std::vector<unsigned char> outsideMask(static_cast<std::size_t>(width) * height, 0);
|
||||||
|
for (std::size_t index = 0; index < alpha.size(); ++index)
|
||||||
|
{
|
||||||
|
const bool inside = alpha[index] > 127;
|
||||||
|
insideMask[index] = inside ? 1 : 0;
|
||||||
|
outsideMask[index] = inside ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<float> distanceToInside = DistanceTransform2D(insideMask, width, height);
|
||||||
|
const std::vector<float> distanceToOutside = DistanceTransform2D(outsideMask, width, height);
|
||||||
|
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
|
||||||
|
|
||||||
|
for (unsigned y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
const unsigned flippedY = height - 1 - y;
|
||||||
|
for (unsigned x = 0; x < width; ++x)
|
||||||
|
{
|
||||||
|
const std::size_t source = static_cast<std::size_t>(y) * width + x;
|
||||||
|
const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]);
|
||||||
|
const float normalized = std::clamp(
|
||||||
|
0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2),
|
||||||
|
0.0f,
|
||||||
|
1.0f);
|
||||||
|
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
|
||||||
|
const std::size_t out = (static_cast<std::size_t>(flippedY) * width + x) * 4;
|
||||||
|
sdf[out + 0] = value;
|
||||||
|
sdf[out + 1] = value;
|
||||||
|
sdf[out + 2] = value;
|
||||||
|
sdf[out + 3] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error)
|
||||||
|
{
|
||||||
|
GdiplusSession gdiplus;
|
||||||
|
if (!gdiplus.started())
|
||||||
|
{
|
||||||
|
error = "Could not start GDI+ for text rendering.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gdiplus::PrivateFontCollection fontCollection;
|
||||||
|
Gdiplus::FontFamily fallbackFamily(L"Arial");
|
||||||
|
Gdiplus::FontFamily* fontFamily = &fallbackFamily;
|
||||||
|
std::unique_ptr<Gdiplus::FontFamily[]> families;
|
||||||
|
const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring();
|
||||||
|
if (!wideFontPath.empty())
|
||||||
|
{
|
||||||
|
if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok)
|
||||||
|
{
|
||||||
|
error = "Could not load packaged font file for text rendering: " + fontPath.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INT familyCount = fontCollection.GetFamilyCount();
|
||||||
|
if (familyCount <= 0)
|
||||||
|
{
|
||||||
|
error = "Packaged font did not contain a usable font family: " + fontPath.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
families.reset(new Gdiplus::FontFamily[familyCount]);
|
||||||
|
INT found = 0;
|
||||||
|
if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0)
|
||||||
|
{
|
||||||
|
error = "Could not read the packaged font family: " + fontPath.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fontFamily = &families[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB);
|
||||||
|
Gdiplus::Graphics graphics(&bitmap);
|
||||||
|
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
|
||||||
|
graphics.Clear(Gdiplus::Color(255, 0, 0, 0));
|
||||||
|
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
|
||||||
|
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
|
||||||
|
graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
|
||||||
|
Gdiplus::Font font(fontFamily, kTextFontPixelSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
|
||||||
|
Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
|
||||||
|
Gdiplus::StringFormat format;
|
||||||
|
format.SetAlignment(Gdiplus::StringAlignmentNear);
|
||||||
|
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
|
||||||
|
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces);
|
||||||
|
const Gdiplus::RectF layout(
|
||||||
|
kTextLayoutPadding,
|
||||||
|
0.0f,
|
||||||
|
static_cast<Gdiplus::REAL>(kTextTextureWidth) - (kTextLayoutPadding * 2.0f),
|
||||||
|
static_cast<Gdiplus::REAL>(kTextTextureHeight));
|
||||||
|
const std::wstring wideText = Utf8ToWide(text);
|
||||||
|
graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush);
|
||||||
|
|
||||||
|
std::vector<unsigned char> alpha(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight, 0);
|
||||||
|
for (unsigned y = 0; y < kTextTextureHeight; ++y)
|
||||||
|
{
|
||||||
|
for (unsigned x = 0; x < kTextTextureWidth; ++x)
|
||||||
|
{
|
||||||
|
Gdiplus::Color pixel;
|
||||||
|
bitmap.GetPixel(x, y, &pixel);
|
||||||
|
BYTE luminance = pixel.GetRed();
|
||||||
|
if (pixel.GetGreen() > luminance)
|
||||||
|
luminance = pixel.GetGreen();
|
||||||
|
if (pixel.GetBlue() > luminance)
|
||||||
|
luminance = pixel.GetBlue();
|
||||||
|
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
constexpr unsigned kTextTextureWidth = 4096;
|
||||||
|
constexpr unsigned kTextTextureHeight = 512;
|
||||||
|
|
||||||
|
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error);
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
#include "TextureAssetLoader.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <wincodec.h>
|
||||||
|
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef GL_RGBA32F
|
||||||
|
#define GL_RGBA32F 0x8814
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string LowercaseExtension(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
std::string extension = path.extension().string();
|
||||||
|
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||||
|
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadCubeTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
||||||
|
{
|
||||||
|
std::ifstream file(textureAsset.path);
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
error = "Could not open shader LUT asset: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned lutSize = 0;
|
||||||
|
std::vector<float> values;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line))
|
||||||
|
{
|
||||||
|
const std::size_t commentStart = line.find('#');
|
||||||
|
if (commentStart != std::string::npos)
|
||||||
|
line.resize(commentStart);
|
||||||
|
|
||||||
|
std::istringstream stream(line);
|
||||||
|
std::string firstToken;
|
||||||
|
if (!(stream >> firstToken))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (firstToken == "TITLE" || firstToken == "DOMAIN_MIN" || firstToken == "DOMAIN_MAX")
|
||||||
|
continue;
|
||||||
|
if (firstToken == "LUT_3D_SIZE")
|
||||||
|
{
|
||||||
|
stream >> lutSize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (firstToken == "LUT_1D_SIZE")
|
||||||
|
{
|
||||||
|
error = "Only 3D .cube LUT assets are supported: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float red = 0.0f;
|
||||||
|
float green = 0.0f;
|
||||||
|
float blue = 0.0f;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
red = std::stof(firstToken);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
error = "Unsupported .cube directive in shader LUT asset: " + firstToken;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(stream >> green >> blue))
|
||||||
|
{
|
||||||
|
error = "Malformed RGB entry in shader LUT asset: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
values.push_back(red);
|
||||||
|
values.push_back(green);
|
||||||
|
values.push_back(blue);
|
||||||
|
values.push_back(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lutSize == 0)
|
||||||
|
{
|
||||||
|
error = "Shader LUT asset is missing LUT_3D_SIZE: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t expectedFloats = static_cast<std::size_t>(lutSize) * lutSize * lutSize * 4;
|
||||||
|
if (values.size() != expectedFloats)
|
||||||
|
{
|
||||||
|
error = "Shader LUT asset entry count does not match LUT_3D_SIZE: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLsizei atlasWidth = static_cast<GLsizei>(lutSize * lutSize);
|
||||||
|
const GLsizei atlasHeight = static_cast<GLsizei>(lutSize);
|
||||||
|
glGenTextures(1, &textureId);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, atlasWidth, atlasHeight, 0, GL_RGBA, GL_FLOAT, values.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
||||||
|
{
|
||||||
|
textureId = 0;
|
||||||
|
if (LowercaseExtension(textureAsset.path) == ".cube")
|
||||||
|
return LoadCubeTextureAsset(textureAsset, textureId, error);
|
||||||
|
|
||||||
|
HRESULT comInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||||
|
const bool shouldUninitializeCom = (comInitResult == S_OK || comInitResult == S_FALSE);
|
||||||
|
if (FAILED(comInitResult) && comInitResult != RPC_E_CHANGED_MODE)
|
||||||
|
{
|
||||||
|
error = "Could not initialize COM to load shader texture assets.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICImagingFactory> imagingFactory;
|
||||||
|
HRESULT result = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&imagingFactory));
|
||||||
|
if (FAILED(result) || !imagingFactory)
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not create a WIC imaging factory to load shader texture assets.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICBitmapDecoder> bitmapDecoder;
|
||||||
|
result = imagingFactory->CreateDecoderFromFilename(textureAsset.path.wstring().c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &bitmapDecoder);
|
||||||
|
if (FAILED(result) || !bitmapDecoder)
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not open shader texture asset: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICBitmapFrameDecode> bitmapFrame;
|
||||||
|
result = bitmapDecoder->GetFrame(0, &bitmapFrame);
|
||||||
|
if (FAILED(result) || !bitmapFrame)
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not decode the first frame of shader texture asset: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IWICFormatConverter> formatConverter;
|
||||||
|
result = imagingFactory->CreateFormatConverter(&formatConverter);
|
||||||
|
if (FAILED(result) || !formatConverter)
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not create a WIC format converter for shader texture asset: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = formatConverter->Initialize(bitmapFrame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not convert shader texture asset to BGRA: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT width = 0;
|
||||||
|
UINT height = 0;
|
||||||
|
result = formatConverter->GetSize(&width, &height);
|
||||||
|
if (FAILED(result) || width == 0 || height == 0)
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Shader texture asset has an invalid size: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UINT stride = width * 4;
|
||||||
|
std::vector<unsigned char> pixels(static_cast<std::size_t>(stride) * static_cast<std::size_t>(height));
|
||||||
|
result = formatConverter->CopyPixels(NULL, stride, static_cast<UINT>(pixels.size()), pixels.data());
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
error = "Could not read shader texture pixels: " + textureAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> flippedPixels(pixels.size());
|
||||||
|
for (UINT row = 0; row < height; ++row)
|
||||||
|
{
|
||||||
|
const std::size_t srcOffset = static_cast<std::size_t>(row) * stride;
|
||||||
|
const std::size_t dstOffset = static_cast<std::size_t>(height - 1 - row) * stride;
|
||||||
|
std::memcpy(flippedPixels.data() + dstOffset, pixels.data() + srcOffset, stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &textureId);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||||
|
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_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_BGRA, GL_UNSIGNED_BYTE, flippedPixels.data());
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
if (shouldUninitializeCom)
|
||||||
|
CoUninitialize();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#include "RuntimeClock.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool ToUtcTime(std::time_t time, std::tm& utcTime)
|
||||||
|
{
|
||||||
|
return gmtime_s(&utcTime, &time) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ToLocalTime(std::time_t time, std::tm& localTime)
|
||||||
|
{
|
||||||
|
return localtime_s(&localTime, &time) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeClockSnapshot GetRuntimeClockSnapshot()
|
||||||
|
{
|
||||||
|
return MakeRuntimeClockSnapshot(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now)
|
||||||
|
{
|
||||||
|
RuntimeClockSnapshot snapshot;
|
||||||
|
|
||||||
|
std::tm utcTime = {};
|
||||||
|
if (!ToUtcTime(now, utcTime))
|
||||||
|
return snapshot;
|
||||||
|
|
||||||
|
snapshot.utcTimeSeconds =
|
||||||
|
static_cast<double>(utcTime.tm_hour * 3600 + utcTime.tm_min * 60 + utcTime.tm_sec);
|
||||||
|
|
||||||
|
std::tm localTime = {};
|
||||||
|
if (!ToLocalTime(now, localTime))
|
||||||
|
return snapshot;
|
||||||
|
|
||||||
|
utcTime.tm_isdst = localTime.tm_isdst;
|
||||||
|
const std::time_t localAsTime = std::mktime(&localTime);
|
||||||
|
const std::time_t utcAsLocalTime = std::mktime(&utcTime);
|
||||||
|
if (localAsTime != static_cast<std::time_t>(-1) && utcAsLocalTime != static_cast<std::time_t>(-1))
|
||||||
|
snapshot.utcOffsetSeconds = std::difftime(localAsTime, utcAsLocalTime);
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
12
apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h
Normal file
12
apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
struct RuntimeClockSnapshot
|
||||||
|
{
|
||||||
|
double utcTimeSeconds = 0.0;
|
||||||
|
double utcOffsetSeconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
RuntimeClockSnapshot GetRuntimeClockSnapshot();
|
||||||
|
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now);
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include "RuntimeClock.h"
|
||||||
#include "RuntimeParameterUtils.h"
|
#include "RuntimeParameterUtils.h"
|
||||||
#include "ShaderCompiler.h"
|
#include "ShaderCompiler.h"
|
||||||
#include "ShaderPackageRegistry.h"
|
#include "ShaderPackageRegistry.h"
|
||||||
@@ -7,6 +9,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@@ -53,6 +56,13 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
|||||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double GenerateStartupRandom()
|
||||||
|
{
|
||||||
|
std::random_device randomDevice;
|
||||||
|
std::uniform_real_distribution<double> distribution(0.0, 1.0);
|
||||||
|
return distribution(randomDevice);
|
||||||
|
}
|
||||||
|
|
||||||
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||||
{
|
{
|
||||||
const std::string prefix = "layer-";
|
const std::string prefix = "layer-";
|
||||||
@@ -133,6 +143,8 @@ std::string ShaderParameterTypeToString(ShaderParameterType type)
|
|||||||
case ShaderParameterType::Color: return "color";
|
case ShaderParameterType::Color: return "color";
|
||||||
case ShaderParameterType::Boolean: return "bool";
|
case ShaderParameterType::Boolean: return "bool";
|
||||||
case ShaderParameterType::Enum: return "enum";
|
case ShaderParameterType::Enum: return "enum";
|
||||||
|
case ShaderParameterType::Text: return "text";
|
||||||
|
case ShaderParameterType::Trigger: return "trigger";
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -179,6 +191,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
|||||||
type = ShaderParameterType::Enum;
|
type = ShaderParameterType::Enum;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (typeName == "text")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeName == "trigger")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Trigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +222,24 @@ bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right)
|
||||||
|
{
|
||||||
|
if (left.size() != right.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < left.size(); ++index)
|
||||||
|
{
|
||||||
|
if (left[index].id != right[index].id ||
|
||||||
|
left[index].path != right[index].path ||
|
||||||
|
left[index].writeTime != right[index].writeTime)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
||||||
{
|
{
|
||||||
return manifestPath.string();
|
return manifestPath.string();
|
||||||
@@ -379,6 +419,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
|
{
|
||||||
|
const JsonValue* fontsValue = nullptr;
|
||||||
|
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
||||||
|
return false;
|
||||||
|
if (!fontsValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (const JsonValue& fontJson : fontsValue->asArray())
|
||||||
|
{
|
||||||
|
if (!fontJson.isObject())
|
||||||
|
{
|
||||||
|
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fontId;
|
||||||
|
std::string fontPath;
|
||||||
|
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
||||||
|
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
||||||
|
{
|
||||||
|
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ShaderFontAsset fontAsset;
|
||||||
|
fontAsset.id = fontId;
|
||||||
|
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
||||||
|
if (!std::filesystem::exists(fontAsset.path))
|
||||||
|
{
|
||||||
|
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
||||||
|
shaderPackage.fontAssets.push_back(fontAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
{
|
{
|
||||||
const JsonValue* temporalValue = nullptr;
|
const JsonValue* temporalValue = nullptr;
|
||||||
@@ -461,6 +544,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (!defaultValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter default must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.defaultTextValue = defaultValue->asString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +637,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (const JsonValue* fontValue = parameterJson.find("font"))
|
||||||
|
{
|
||||||
|
if (!fontValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter 'font' must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.fontId = fontValue->asString();
|
||||||
|
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
||||||
|
{
|
||||||
|
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
||||||
|
{
|
||||||
|
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
if (definition.type == ShaderParameterType::Enum)
|
||||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||||
|
|
||||||
@@ -578,6 +696,13 @@ RuntimeHost::RuntimeHost()
|
|||||||
mFrameBudgetMilliseconds(0.0),
|
mFrameBudgetMilliseconds(0.0),
|
||||||
mRenderMilliseconds(0.0),
|
mRenderMilliseconds(0.0),
|
||||||
mSmoothedRenderMilliseconds(0.0),
|
mSmoothedRenderMilliseconds(0.0),
|
||||||
|
mCompletionIntervalMilliseconds(0.0),
|
||||||
|
mSmoothedCompletionIntervalMilliseconds(0.0),
|
||||||
|
mMaxCompletionIntervalMilliseconds(0.0),
|
||||||
|
mStartupRandom(GenerateStartupRandom()),
|
||||||
|
mLateFrameCount(0),
|
||||||
|
mDroppedFrameCount(0),
|
||||||
|
mFlushedFrameCount(0),
|
||||||
mServerPort(8080),
|
mServerPort(8080),
|
||||||
mAutoReloadEnabled(true),
|
mAutoReloadEnabled(true),
|
||||||
mStartTime(std::chrono::steady_clock::now()),
|
mStartTime(std::chrono::steady_clock::now()),
|
||||||
@@ -693,7 +818,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
}
|
}
|
||||||
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
|
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
|
||||||
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
|
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
|
||||||
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets))
|
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) ||
|
||||||
|
!FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets))
|
||||||
{
|
{
|
||||||
registryChanged = true;
|
registryChanged = true;
|
||||||
break;
|
break;
|
||||||
@@ -714,7 +840,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
if (previous->second.first != active->second.shaderWriteTime ||
|
if (previous->second.first != active->second.shaderWriteTime ||
|
||||||
previous->second.second != active->second.manifestWriteTime ||
|
previous->second.second != active->second.manifestWriteTime ||
|
||||||
(previousPackage != previousPackages.end() &&
|
(previousPackage != previousPackages.end() &&
|
||||||
!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets)))
|
(!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) ||
|
||||||
|
!FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets))))
|
||||||
{
|
{
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
}
|
}
|
||||||
@@ -899,6 +1026,15 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = layer->parameterValues[parameterId];
|
||||||
|
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();
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue normalized;
|
ShaderParameterValue normalized;
|
||||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -945,6 +1081,15 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = matchedLayer->parameterValues[parameterIt->id];
|
||||||
|
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();
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue normalized;
|
ShaderParameterValue normalized;
|
||||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -1045,6 +1190,21 @@ void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message)
|
|||||||
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetSignalStatusLocked(hasSignal, width, height, modeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetSignalStatusLocked(hasSignal, width, height, modeName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
|
{
|
||||||
mHasSignal = hasSignal;
|
mHasSignal = hasSignal;
|
||||||
mSignalWidth = width;
|
mSignalWidth = width;
|
||||||
mSignalHeight = height;
|
mSignalHeight = height;
|
||||||
@@ -1053,8 +1213,16 @@ void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned heigh
|
|||||||
|
|
||||||
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable,
|
||||||
|
externalKeyingRequested, externalKeyingActive, statusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mDeckLinkOutputStatus.backendName = backendName;
|
||||||
mDeckLinkOutputStatus.modelName = modelName;
|
mDeckLinkOutputStatus.modelName = modelName;
|
||||||
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
|
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
|
||||||
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
|
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
|
||||||
@@ -1067,6 +1235,21 @@ void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool sup
|
|||||||
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetPerformanceStatsLocked(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetPerformanceStatsLocked(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
|
{
|
||||||
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
||||||
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||||
if (mSmoothedRenderMilliseconds <= 0.0)
|
if (mSmoothedRenderMilliseconds <= 0.0)
|
||||||
@@ -1075,12 +1258,48 @@ void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double ren
|
|||||||
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::AdvanceFrame()
|
void RuntimeHost::SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetFramePacingStatsLocked(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
||||||
|
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetFramePacingStatsLocked(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
||||||
|
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
|
{
|
||||||
|
mCompletionIntervalMilliseconds = std::max(completionIntervalMilliseconds, 0.0);
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = std::max(smoothedCompletionIntervalMilliseconds, 0.0);
|
||||||
|
mMaxCompletionIntervalMilliseconds = std::max(maxCompletionIntervalMilliseconds, 0.0);
|
||||||
|
mLateFrameCount = lateFrameCount;
|
||||||
|
mDroppedFrameCount = droppedFrameCount;
|
||||||
|
mFlushedFrameCount = flushedFrameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::AdvanceFrame()
|
||||||
|
{
|
||||||
++mFrameCounter;
|
++mFrameCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TryAdvanceFrame()
|
||||||
|
{
|
||||||
|
++mFrameCounter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1123,7 +1342,39 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
std::vector<RuntimeRenderState> states;
|
std::vector<RuntimeRenderState> states;
|
||||||
|
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
states.clear();
|
||||||
|
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
|
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||||
|
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
const double frameCount = static_cast<double>(mFrameCounter.load(std::memory_order_relaxed));
|
||||||
|
|
||||||
|
for (RuntimeRenderState& state : states)
|
||||||
|
{
|
||||||
|
state.timeSeconds = timeSeconds;
|
||||||
|
state.utcTimeSeconds = clock.utcTimeSeconds;
|
||||||
|
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
||||||
|
state.startupRandom = mStartupRandom;
|
||||||
|
state.frameCount = frameCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
for (const LayerPersistentState& layer : mPersistentState.layers)
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(layer.shaderId);
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||||
@@ -1133,8 +1384,6 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
RuntimeRenderState state;
|
RuntimeRenderState state;
|
||||||
state.layerId = layer.id;
|
state.layerId = layer.id;
|
||||||
state.shaderId = layer.shaderId;
|
state.shaderId = layer.shaderId;
|
||||||
state.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
|
||||||
state.frameCount = static_cast<double>(mFrameCounter);
|
|
||||||
state.mixAmount = 1.0;
|
state.mixAmount = 1.0;
|
||||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||||
state.inputWidth = mSignalWidth;
|
state.inputWidth = mSignalWidth;
|
||||||
@@ -1143,6 +1392,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
state.outputHeight = outputHeight;
|
state.outputHeight = outputHeight;
|
||||||
state.parameterDefinitions = shaderIt->second.parameters;
|
state.parameterDefinitions = shaderIt->second.parameters;
|
||||||
state.textureAssets = shaderIt->second.textureAssets;
|
state.textureAssets = shaderIt->second.textureAssets;
|
||||||
|
state.fontAssets = shaderIt->second.fontAssets;
|
||||||
state.isTemporal = shaderIt->second.temporal.enabled;
|
state.isTemporal = shaderIt->second.temporal.enabled;
|
||||||
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
||||||
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
||||||
@@ -1160,7 +1410,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
states.push_back(state);
|
states.push_back(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return states;
|
RefreshDynamicRenderStateFields(states);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string RuntimeHost::BuildStateJson() const
|
std::string RuntimeHost::BuildStateJson() const
|
||||||
@@ -1401,12 +1651,14 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
{
|
{
|
||||||
std::map<std::string, ShaderPackage> packagesById;
|
std::map<std::string, ShaderPackage> packagesById;
|
||||||
std::vector<std::string> packageOrder;
|
std::vector<std::string> packageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> packageStatuses;
|
||||||
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
||||||
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
|
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mPackagesById.swap(packagesById);
|
mPackagesById.swap(packagesById);
|
||||||
mPackageOrder.swap(packageOrder);
|
mPackageOrder.swap(packageOrder);
|
||||||
|
mPackageStatuses.swap(packageStatuses);
|
||||||
|
|
||||||
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
||||||
{
|
{
|
||||||
@@ -1447,6 +1699,7 @@ bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath,
|
|||||||
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) &&
|
||||||
|
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||||
}
|
}
|
||||||
@@ -1465,8 +1718,68 @@ void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, co
|
|||||||
{
|
{
|
||||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||||
{
|
{
|
||||||
if (layerState.parameterValues.find(definition.id) == layerState.parameterValues.end())
|
auto valueIt = layerState.parameterValues.find(definition.id);
|
||||||
|
if (valueIt == layerState.parameterValues.end())
|
||||||
|
{
|
||||||
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonValue valueJson;
|
||||||
|
bool shouldNormalize = true;
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
if (valueIt->second.numberValues.empty())
|
||||||
|
shouldNormalize = false;
|
||||||
|
else
|
||||||
|
valueJson = JsonValue(valueIt->second.numberValues.front());
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
valueJson = JsonValue::MakeArray();
|
||||||
|
for (double number : valueIt->second.numberValues)
|
||||||
|
valueJson.pushBack(JsonValue(number));
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
valueJson = JsonValue(valueIt->second.booleanValue);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
valueJson = JsonValue(valueIt->second.enumValue);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
{
|
||||||
|
const std::string textValue = !valueIt->second.textValue.empty()
|
||||||
|
? valueIt->second.textValue
|
||||||
|
: valueIt->second.enumValue;
|
||||||
|
if (textValue.empty())
|
||||||
|
{
|
||||||
|
valueIt->second = DefaultValueForDefinition(definition);
|
||||||
|
shouldNormalize = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valueJson = JsonValue(textValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
if (valueIt->second.numberValues.empty())
|
||||||
|
valueJson = JsonValue(0.0);
|
||||||
|
else
|
||||||
|
valueJson = JsonValue(std::max(0.0, std::floor(valueIt->second.numberValues.front())));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldNormalize)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ShaderParameterValue normalizedValue;
|
||||||
|
std::string normalizeError;
|
||||||
|
if (NormalizeAndValidateValue(definition, valueJson, normalizedValue, normalizeError))
|
||||||
|
valueIt->second = normalizedValue;
|
||||||
|
else
|
||||||
|
valueIt->second = DefaultValueForDefinition(definition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1584,26 +1897,44 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||||
root.set("decklink", deckLink);
|
root.set("decklink", deckLink);
|
||||||
|
|
||||||
|
JsonValue videoIO = JsonValue::MakeObject();
|
||||||
|
videoIO.set("backend", JsonValue(mDeckLinkOutputStatus.backendName));
|
||||||
|
videoIO.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
|
||||||
|
videoIO.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));
|
||||||
|
videoIO.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying));
|
||||||
|
videoIO.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable));
|
||||||
|
videoIO.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested));
|
||||||
|
videoIO.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive));
|
||||||
|
videoIO.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||||
|
root.set("videoIO", videoIO);
|
||||||
|
|
||||||
JsonValue performance = JsonValue::MakeObject();
|
JsonValue performance = JsonValue::MakeObject();
|
||||||
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
||||||
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
||||||
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
||||||
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
||||||
|
performance.set("completionIntervalMs", JsonValue(mCompletionIntervalMilliseconds));
|
||||||
|
performance.set("smoothedCompletionIntervalMs", JsonValue(mSmoothedCompletionIntervalMilliseconds));
|
||||||
|
performance.set("maxCompletionIntervalMs", JsonValue(mMaxCompletionIntervalMilliseconds));
|
||||||
|
performance.set("lateFrameCount", JsonValue(static_cast<double>(mLateFrameCount)));
|
||||||
|
performance.set("droppedFrameCount", JsonValue(static_cast<double>(mDroppedFrameCount)));
|
||||||
|
performance.set("flushedFrameCount", JsonValue(static_cast<double>(mFlushedFrameCount)));
|
||||||
root.set("performance", performance);
|
root.set("performance", performance);
|
||||||
|
|
||||||
JsonValue shaderLibrary = JsonValue::MakeArray();
|
JsonValue shaderLibrary = JsonValue::MakeArray();
|
||||||
for (const std::string& shaderId : mPackageOrder)
|
for (const ShaderPackageStatus& status : mPackageStatuses)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(shaderId);
|
|
||||||
if (shaderIt == mPackagesById.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
JsonValue shader = JsonValue::MakeObject();
|
JsonValue shader = JsonValue::MakeObject();
|
||||||
shader.set("id", JsonValue(shaderIt->second.id));
|
shader.set("id", JsonValue(status.id));
|
||||||
shader.set("name", JsonValue(shaderIt->second.displayName));
|
shader.set("name", JsonValue(status.displayName));
|
||||||
shader.set("description", JsonValue(shaderIt->second.description));
|
shader.set("description", JsonValue(status.description));
|
||||||
shader.set("category", JsonValue(shaderIt->second.category));
|
shader.set("category", JsonValue(status.category));
|
||||||
if (shaderIt->second.temporal.enabled)
|
shader.set("available", JsonValue(status.available));
|
||||||
|
if (!status.available)
|
||||||
|
shader.set("error", JsonValue(status.error));
|
||||||
|
|
||||||
|
auto shaderIt = mPackagesById.find(status.id);
|
||||||
|
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.temporal.enabled)
|
||||||
{
|
{
|
||||||
JsonValue temporal = JsonValue::MakeObject();
|
JsonValue temporal = JsonValue::MakeObject();
|
||||||
temporal.set("enabled", JsonValue(true));
|
temporal.set("enabled", JsonValue(true));
|
||||||
@@ -1657,6 +1988,7 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|||||||
parameter.set("id", JsonValue(definition.id));
|
parameter.set("id", JsonValue(definition.id));
|
||||||
parameter.set("label", JsonValue(definition.label));
|
parameter.set("label", JsonValue(definition.label));
|
||||||
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
||||||
|
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
|
||||||
|
|
||||||
if (!definition.minNumbers.empty())
|
if (!definition.minNumbers.empty())
|
||||||
{
|
{
|
||||||
@@ -1691,6 +2023,12 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|||||||
}
|
}
|
||||||
parameter.set("options", options);
|
parameter.set("options", options);
|
||||||
}
|
}
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
parameter.set("maxLength", JsonValue(static_cast<double>(definition.maxLength)));
|
||||||
|
if (!definition.fontId.empty())
|
||||||
|
parameter.set("font", JsonValue(definition.fontId));
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||||
auto valueIt = layer.parameterValues.find(definition.id);
|
auto valueIt = layer.parameterValues.find(definition.id);
|
||||||
@@ -1829,6 +2167,10 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
|
|||||||
return JsonValue(value.booleanValue);
|
return JsonValue(value.booleanValue);
|
||||||
case ShaderParameterType::Enum:
|
case ShaderParameterType::Enum:
|
||||||
return JsonValue(value.enumValue);
|
return JsonValue(value.enumValue);
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
return JsonValue(value.textValue);
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
case ShaderParameterType::Float:
|
case ShaderParameterType::Float:
|
||||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
case ShaderParameterType::Vec2:
|
case ShaderParameterType::Vec2:
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "RuntimeJson.h"
|
#include "RuntimeJson.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -35,13 +36,24 @@ 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);
|
||||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||||
|
void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
|
bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
void AdvanceFrame();
|
void AdvanceFrame();
|
||||||
|
bool TryAdvanceFrame();
|
||||||
|
|
||||||
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, 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;
|
||||||
|
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||||
std::string BuildStateJson() const;
|
std::string BuildStateJson() const;
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||||
@@ -76,6 +88,7 @@ private:
|
|||||||
|
|
||||||
struct DeckLinkOutputStatus
|
struct DeckLinkOutputStatus
|
||||||
{
|
{
|
||||||
|
std::string backendName = "decklink";
|
||||||
std::string modelName;
|
std::string modelName;
|
||||||
bool supportsInternalKeying = false;
|
bool supportsInternalKeying = false;
|
||||||
bool supportsExternalKeying = false;
|
bool supportsExternalKeying = false;
|
||||||
@@ -109,6 +122,7 @@ private:
|
|||||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||||
bool ResolvePaths(std::string& error);
|
bool ResolvePaths(std::string& error);
|
||||||
|
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||||
JsonValue BuildStateValue() const;
|
JsonValue BuildStateValue() const;
|
||||||
JsonValue SerializeLayerStackLocked() const;
|
JsonValue SerializeLayerStackLocked() const;
|
||||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||||
@@ -120,6 +134,10 @@ private:
|
|||||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||||
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 SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
@@ -138,6 +156,7 @@ private:
|
|||||||
std::filesystem::path mPatchedGlslPath;
|
std::filesystem::path mPatchedGlslPath;
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
std::vector<std::string> mPackageOrder;
|
std::vector<std::string> mPackageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||||
bool mReloadRequested;
|
bool mReloadRequested;
|
||||||
bool mCompileSucceeded;
|
bool mCompileSucceeded;
|
||||||
std::string mCompileMessage;
|
std::string mCompileMessage;
|
||||||
@@ -148,11 +167,18 @@ private:
|
|||||||
double mFrameBudgetMilliseconds;
|
double mFrameBudgetMilliseconds;
|
||||||
double mRenderMilliseconds;
|
double mRenderMilliseconds;
|
||||||
double mSmoothedRenderMilliseconds;
|
double mSmoothedRenderMilliseconds;
|
||||||
|
double mCompletionIntervalMilliseconds;
|
||||||
|
double mSmoothedCompletionIntervalMilliseconds;
|
||||||
|
double mMaxCompletionIntervalMilliseconds;
|
||||||
|
double mStartupRandom;
|
||||||
|
uint64_t mLateFrameCount;
|
||||||
|
uint64_t mDroppedFrameCount;
|
||||||
|
uint64_t mFlushedFrameCount;
|
||||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||||
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;
|
||||||
uint64_t mFrameCounter;
|
std::atomic<uint64_t> mFrameCounter;
|
||||||
uint64_t mNextLayerId;
|
uint64_t mNextLayerId;
|
||||||
};
|
};
|
||||||
@@ -36,6 +36,21 @@ std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
|||||||
}
|
}
|
||||||
return numbers;
|
return numbers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
|
||||||
|
{
|
||||||
|
std::string normalized;
|
||||||
|
normalized.reserve(std::min<std::size_t>(text.size(), maxLength));
|
||||||
|
for (unsigned char ch : text)
|
||||||
|
{
|
||||||
|
if (ch < 32 || ch > 126)
|
||||||
|
continue;
|
||||||
|
if (normalized.size() >= maxLength)
|
||||||
|
break;
|
||||||
|
normalized.push_back(static_cast<char>(ch));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MakeSafePresetFileStem(const std::string& presetName)
|
std::string MakeSafePresetFileStem(const std::string& presetName)
|
||||||
@@ -82,6 +97,12 @@ ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition&
|
|||||||
case ShaderParameterType::Enum:
|
case ShaderParameterType::Enum:
|
||||||
value.enumValue = definition.defaultEnumValue;
|
value.enumValue = definition.defaultEnumValue;
|
||||||
break;
|
break;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
value.numberValues = { 0.0, -1000000.0 };
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -164,6 +185,22 @@ bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definit
|
|||||||
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
|
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
if (!value.isString())
|
||||||
|
{
|
||||||
|
error = "Expected string value for text parameter '" + definition.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
normalizedValue.textValue = NormalizeTextValue(value.asString(), definition.maxLength);
|
||||||
|
return true;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
if (!value.isNumber() && !value.isBoolean())
|
||||||
|
{
|
||||||
|
error = "Expected numeric or boolean value for trigger parameter '" + definition.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
normalizedValue.numberValues = { value.isNumber() ? std::max(0.0, std::floor(value.asNumber())) : 0.0, -1000000.0 };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "NativeHandles.h"
|
#include "NativeHandles.h"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <cctype>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -30,15 +31,36 @@ std::string SlangCBufferTypeForParameter(ShaderParameterType type)
|
|||||||
case ShaderParameterType::Color: return "float4";
|
case ShaderParameterType::Color: return "float4";
|
||||||
case ShaderParameterType::Boolean: return "bool";
|
case ShaderParameterType::Boolean: return "bool";
|
||||||
case ShaderParameterType::Enum: return "int";
|
case ShaderParameterType::Enum: return "int";
|
||||||
|
case ShaderParameterType::Text: return "";
|
||||||
|
case ShaderParameterType::Trigger: return "int";
|
||||||
}
|
}
|
||||||
return "float";
|
return "float";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string CapitalizeIdentifier(const std::string& identifier)
|
||||||
|
{
|
||||||
|
if (identifier.empty())
|
||||||
|
return identifier;
|
||||||
|
std::string text = identifier;
|
||||||
|
text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
|
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
{
|
{
|
||||||
std::ostringstream source;
|
std::ostringstream source;
|
||||||
for (const ShaderParameterDefinition& definition : parameters)
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
if (definition.type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
source << "\tint " << definition.id << ";\n";
|
||||||
|
source << "\tfloat " << definition.id << "Time;\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
|
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
|
||||||
|
}
|
||||||
return source.str();
|
return source.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +82,44 @@ std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset
|
|||||||
return source.str();
|
return source.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
|
{
|
||||||
|
std::ostringstream source;
|
||||||
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type != ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
source << "Sampler2D<float4> " << definition.id << "Texture;\n";
|
||||||
|
}
|
||||||
|
if (source.tellp() > 0)
|
||||||
|
source << "\n";
|
||||||
|
return source.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
|
{
|
||||||
|
std::ostringstream source;
|
||||||
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type != ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
const std::string suffix = CapitalizeIdentifier(definition.id);
|
||||||
|
source
|
||||||
|
<< "float sample" << suffix << "(float2 uv)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
|
||||||
|
<< "\t\treturn 0.0;\n"
|
||||||
|
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
|
||||||
|
<< "}\n\n"
|
||||||
|
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
|
||||||
|
<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
|
||||||
|
<< "}\n\n";
|
||||||
|
}
|
||||||
|
return source.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
|
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
|
||||||
{
|
{
|
||||||
std::ostringstream source;
|
std::ostringstream source;
|
||||||
@@ -115,11 +175,14 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
|
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", mMaxTemporalHistoryFrames));
|
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
||||||
return true;
|
return true;
|
||||||
@@ -127,6 +190,18 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
|||||||
|
|
||||||
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
|
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
|
||||||
{
|
{
|
||||||
|
char slangRootBuffer[MAX_PATH] = {};
|
||||||
|
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer)));
|
||||||
|
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
|
||||||
|
{
|
||||||
|
std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe";
|
||||||
|
if (std::filesystem::exists(candidate))
|
||||||
|
{
|
||||||
|
compilerPath = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
|
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
|
||||||
if (!std::filesystem::exists(thirdPartyRoot))
|
if (!std::filesystem::exists(thirdPartyRoot))
|
||||||
{
|
{
|
||||||
@@ -67,6 +67,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
|||||||
type = ShaderParameterType::Enum;
|
type = ShaderParameterType::Enum;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (typeName == "text")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeName == "trigger")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Trigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +293,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
|
{
|
||||||
|
const JsonValue* fontsValue = nullptr;
|
||||||
|
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
||||||
|
return false;
|
||||||
|
if (!fontsValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (const JsonValue& fontJson : fontsValue->asArray())
|
||||||
|
{
|
||||||
|
if (!fontJson.isObject())
|
||||||
|
{
|
||||||
|
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fontId;
|
||||||
|
std::string fontPath;
|
||||||
|
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
||||||
|
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
||||||
|
{
|
||||||
|
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ShaderFontAsset fontAsset;
|
||||||
|
fontAsset.id = fontId;
|
||||||
|
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
||||||
|
if (!std::filesystem::exists(fontAsset.path))
|
||||||
|
{
|
||||||
|
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
||||||
|
shaderPackage.fontAssets.push_back(fontAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
{
|
{
|
||||||
const JsonValue* temporalValue = nullptr;
|
const JsonValue* temporalValue = nullptr;
|
||||||
@@ -365,6 +418,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (!defaultValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter default must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.defaultTextValue = defaultValue->asString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +511,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (const JsonValue* fontValue = parameterJson.find("font"))
|
||||||
|
{
|
||||||
|
if (!fontValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter 'font' must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.fontId = fontValue->asString();
|
||||||
|
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
||||||
|
{
|
||||||
|
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
||||||
|
{
|
||||||
|
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
if (definition.type == ShaderParameterType::Enum)
|
||||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||||
|
|
||||||
@@ -471,6 +559,36 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string UniqueUnavailableShaderId(const std::filesystem::path& manifestPath, const std::string& parsedId)
|
||||||
|
{
|
||||||
|
const std::string fallbackId = manifestPath.parent_path().filename().string();
|
||||||
|
const std::string baseId = parsedId.empty() ? fallbackId : parsedId;
|
||||||
|
return baseId + "@invalid:" + fallbackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackageStatus BuildUnavailableStatus(const std::filesystem::path& manifestPath, const ShaderPackage& partialPackage, const std::string& packageError)
|
||||||
|
{
|
||||||
|
ShaderPackageStatus status;
|
||||||
|
status.id = UniqueUnavailableShaderId(manifestPath, partialPackage.id);
|
||||||
|
status.displayName = !partialPackage.displayName.empty() ? partialPackage.displayName : manifestPath.parent_path().filename().string();
|
||||||
|
status.description = partialPackage.description;
|
||||||
|
status.category = !partialPackage.category.empty() ? partialPackage.category : "Unavailable";
|
||||||
|
status.available = false;
|
||||||
|
status.error = packageError;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackageStatus BuildAvailableStatus(const ShaderPackage& shaderPackage)
|
||||||
|
{
|
||||||
|
ShaderPackageStatus status;
|
||||||
|
status.id = shaderPackage.id;
|
||||||
|
status.displayName = shaderPackage.displayName;
|
||||||
|
status.description = shaderPackage.description;
|
||||||
|
status.category = shaderPackage.category;
|
||||||
|
status.available = true;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
||||||
@@ -478,10 +596,16 @@ ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
|
bool ShaderPackageRegistry::Scan(
|
||||||
|
const std::filesystem::path& shaderRoot,
|
||||||
|
std::map<std::string, ShaderPackage>& packagesById,
|
||||||
|
std::vector<std::string>& packageOrder,
|
||||||
|
std::vector<ShaderPackageStatus>& packageStatuses,
|
||||||
|
std::string& error) const
|
||||||
{
|
{
|
||||||
packagesById.clear();
|
packagesById.clear();
|
||||||
packageOrder.clear();
|
packageOrder.clear();
|
||||||
|
packageStatuses.clear();
|
||||||
|
|
||||||
if (!std::filesystem::exists(shaderRoot))
|
if (!std::filesystem::exists(shaderRoot))
|
||||||
{
|
{
|
||||||
@@ -500,19 +624,27 @@ bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::m
|
|||||||
|
|
||||||
ShaderPackage shaderPackage;
|
ShaderPackage shaderPackage;
|
||||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
if (!ParseManifest(manifestPath, shaderPackage, error))
|
||||||
return false;
|
{
|
||||||
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
|
||||||
|
error.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
||||||
{
|
{
|
||||||
error = "Duplicate shader id found: " + shaderPackage.id;
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
packageOrder.push_back(shaderPackage.id);
|
packageOrder.push_back(shaderPackage.id);
|
||||||
|
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
|
||||||
packagesById[shaderPackage.id] = shaderPackage;
|
packagesById[shaderPackage.id] = shaderPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(packageOrder.begin(), packageOrder.end());
|
std::sort(packageOrder.begin(), packageOrder.end());
|
||||||
|
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
|
||||||
|
return left.displayName < right.displayName;
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +676,7 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
|||||||
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) &&
|
||||||
|
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,12 @@ class ShaderPackageRegistry
|
|||||||
public:
|
public:
|
||||||
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
|
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
|
||||||
|
|
||||||
bool Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const;
|
bool Scan(
|
||||||
|
const std::filesystem::path& shaderRoot,
|
||||||
|
std::map<std::string, ShaderPackage>& packagesById,
|
||||||
|
std::vector<std::string>& packageOrder,
|
||||||
|
std::vector<ShaderPackageStatus>& packageStatuses,
|
||||||
|
std::string& error) const;
|
||||||
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -11,7 +11,9 @@ enum class ShaderParameterType
|
|||||||
Vec2,
|
Vec2,
|
||||||
Color,
|
Color,
|
||||||
Boolean,
|
Boolean,
|
||||||
Enum
|
Enum,
|
||||||
|
Text,
|
||||||
|
Trigger
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShaderParameterOption
|
struct ShaderParameterOption
|
||||||
@@ -31,6 +33,9 @@ struct ShaderParameterDefinition
|
|||||||
std::vector<double> stepNumbers;
|
std::vector<double> stepNumbers;
|
||||||
bool defaultBoolean = false;
|
bool defaultBoolean = false;
|
||||||
std::string defaultEnumValue;
|
std::string defaultEnumValue;
|
||||||
|
std::string defaultTextValue;
|
||||||
|
std::string fontId;
|
||||||
|
unsigned maxLength = 64;
|
||||||
std::vector<ShaderParameterOption> enumOptions;
|
std::vector<ShaderParameterOption> enumOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +44,7 @@ struct ShaderParameterValue
|
|||||||
std::vector<double> numberValues;
|
std::vector<double> numberValues;
|
||||||
bool booleanValue = false;
|
bool booleanValue = false;
|
||||||
std::string enumValue;
|
std::string enumValue;
|
||||||
|
std::string textValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TemporalHistorySource
|
enum class TemporalHistorySource
|
||||||
@@ -63,6 +69,13 @@ struct ShaderTextureAsset
|
|||||||
std::filesystem::file_time_type writeTime;
|
std::filesystem::file_time_type writeTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ShaderFontAsset
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::filesystem::path path;
|
||||||
|
std::filesystem::file_time_type writeTime;
|
||||||
|
};
|
||||||
|
|
||||||
struct ShaderPackage
|
struct ShaderPackage
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -75,11 +88,22 @@ struct ShaderPackage
|
|||||||
std::filesystem::path manifestPath;
|
std::filesystem::path manifestPath;
|
||||||
std::vector<ShaderParameterDefinition> parameters;
|
std::vector<ShaderParameterDefinition> parameters;
|
||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
TemporalSettings temporal;
|
TemporalSettings temporal;
|
||||||
std::filesystem::file_time_type shaderWriteTime;
|
std::filesystem::file_time_type shaderWriteTime;
|
||||||
std::filesystem::file_time_type manifestWriteTime;
|
std::filesystem::file_time_type manifestWriteTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ShaderPackageStatus
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string displayName;
|
||||||
|
std::string description;
|
||||||
|
std::string category;
|
||||||
|
bool available = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
struct RuntimeRenderState
|
struct RuntimeRenderState
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
@@ -87,7 +111,11 @@ struct RuntimeRenderState
|
|||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
double timeSeconds = 0.0;
|
double timeSeconds = 0.0;
|
||||||
|
double utcTimeSeconds = 0.0;
|
||||||
|
double utcOffsetSeconds = 0.0;
|
||||||
|
double startupRandom = 0.0;
|
||||||
double frameCount = 0.0;
|
double frameCount = 0.0;
|
||||||
double mixAmount = 1.0;
|
double mixAmount = 1.0;
|
||||||
double bypass = 0.0;
|
double bypass = 0.0;
|
||||||
159
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.cpp
Normal file
159
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.cpp
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#ifdef min
|
||||||
|
#undef min
|
||||||
|
#endif
|
||||||
|
#ifdef max
|
||||||
|
#undef max
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
uint16_t Clamp10(int value, int minimum, int maximum)
|
||||||
|
{
|
||||||
|
return static_cast<uint16_t>(std::max(minimum, std::min(maximum, value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t MakeV210Word(uint16_t a, uint16_t b, uint16_t c)
|
||||||
|
{
|
||||||
|
return (static_cast<uint32_t>(a) & 0x3ffu)
|
||||||
|
| ((static_cast<uint32_t>(b) & 0x3ffu) << 10)
|
||||||
|
| ((static_cast<uint32_t>(c) & 0x3ffu) << 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StoreWord(std::array<uint8_t, 16>& bytes, std::size_t wordIndex, uint32_t word)
|
||||||
|
{
|
||||||
|
const std::size_t offset = wordIndex * 4;
|
||||||
|
bytes[offset + 0] = static_cast<uint8_t>(word & 0xffu);
|
||||||
|
bytes[offset + 1] = static_cast<uint8_t>((word >> 8) & 0xffu);
|
||||||
|
bytes[offset + 2] = static_cast<uint8_t>((word >> 16) & 0xffu);
|
||||||
|
bytes[offset + 3] = static_cast<uint8_t>((word >> 24) & 0xffu);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t LoadWord(const std::array<uint8_t, 16>& bytes, std::size_t wordIndex)
|
||||||
|
{
|
||||||
|
const std::size_t offset = wordIndex * 4;
|
||||||
|
return static_cast<uint32_t>(bytes[offset + 0])
|
||||||
|
| (static_cast<uint32_t>(bytes[offset + 1]) << 8)
|
||||||
|
| (static_cast<uint32_t>(bytes[offset + 2]) << 16)
|
||||||
|
| (static_cast<uint32_t>(bytes[offset + 3]) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Component(uint32_t word, unsigned index)
|
||||||
|
{
|
||||||
|
return static_cast<uint16_t>((word >> (index * 10)) & 0x3ffu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case VideoIOPixelFormat::V210:
|
||||||
|
return "10-bit YUV v210";
|
||||||
|
case VideoIOPixelFormat::Bgra8:
|
||||||
|
return "8-bit BGRA";
|
||||||
|
case VideoIOPixelFormat::Uyvy8:
|
||||||
|
default:
|
||||||
|
return "8-bit YUV UYVY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
return format == VideoIOPixelFormat::V210;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
|
||||||
|
{
|
||||||
|
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case VideoIOPixelFormat::Uyvy8:
|
||||||
|
return 2u;
|
||||||
|
case VideoIOPixelFormat::Bgra8:
|
||||||
|
return 4u;
|
||||||
|
case VideoIOPixelFormat::V210:
|
||||||
|
default:
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
|
||||||
|
{
|
||||||
|
if (format == VideoIOPixelFormat::V210)
|
||||||
|
return MinimumV210RowBytes(frameWidth);
|
||||||
|
return frameWidth * VideoIOBytesPerPixel(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
|
||||||
|
{
|
||||||
|
return (rowBytes + 3u) / 4u;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned MinimumV210RowBytes(unsigned frameWidth)
|
||||||
|
{
|
||||||
|
return ((frameWidth + 5u) / 6u) * 16u;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned ActiveV210WordsForWidth(unsigned frameWidth)
|
||||||
|
{
|
||||||
|
return ((frameWidth + 5u) / 6u) * 4u;
|
||||||
|
}
|
||||||
|
|
||||||
|
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue)
|
||||||
|
{
|
||||||
|
red = std::max(0.0f, std::min(1.0f, red));
|
||||||
|
green = std::max(0.0f, std::min(1.0f, green));
|
||||||
|
blue = std::max(0.0f, std::min(1.0f, blue));
|
||||||
|
|
||||||
|
const float y = 0.2126f * red + 0.7152f * green + 0.0722f * blue;
|
||||||
|
const float cb = (blue - y) / 1.8556f + 0.5f;
|
||||||
|
const float cr = (red - y) / 1.5748f + 0.5f;
|
||||||
|
|
||||||
|
V210CodeValues values;
|
||||||
|
values.y = Clamp10(static_cast<int>(std::lround(64.0f + y * 876.0f)), 64, 940);
|
||||||
|
values.cb = Clamp10(static_cast<int>(std::lround(64.0f + cb * 896.0f)), 64, 960);
|
||||||
|
values.cr = Clamp10(static_cast<int>(std::lround(64.0f + cr * 896.0f)), 64, 960);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block)
|
||||||
|
{
|
||||||
|
std::array<uint8_t, 16> bytes = {};
|
||||||
|
StoreWord(bytes, 0, MakeV210Word(block.cb[0], block.y[0], block.cr[0]));
|
||||||
|
StoreWord(bytes, 1, MakeV210Word(block.y[1], block.cb[1], block.y[2]));
|
||||||
|
StoreWord(bytes, 2, MakeV210Word(block.cr[1], block.y[3], block.cb[2]));
|
||||||
|
StoreWord(bytes, 3, MakeV210Word(block.y[4], block.cr[2], block.y[5]));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
V210SixPixelBlock UnpackV210Block(const std::array<uint8_t, 16>& bytes)
|
||||||
|
{
|
||||||
|
const uint32_t word0 = LoadWord(bytes, 0);
|
||||||
|
const uint32_t word1 = LoadWord(bytes, 1);
|
||||||
|
const uint32_t word2 = LoadWord(bytes, 2);
|
||||||
|
const uint32_t word3 = LoadWord(bytes, 3);
|
||||||
|
|
||||||
|
V210SixPixelBlock block;
|
||||||
|
block.cb[0] = Component(word0, 0);
|
||||||
|
block.y[0] = Component(word0, 1);
|
||||||
|
block.cr[0] = Component(word0, 2);
|
||||||
|
block.y[1] = Component(word1, 0);
|
||||||
|
block.cb[1] = Component(word1, 1);
|
||||||
|
block.y[2] = Component(word1, 2);
|
||||||
|
block.cr[1] = Component(word2, 0);
|
||||||
|
block.y[3] = Component(word2, 1);
|
||||||
|
block.cb[2] = Component(word2, 2);
|
||||||
|
block.y[4] = Component(word3, 0);
|
||||||
|
block.cr[2] = Component(word3, 1);
|
||||||
|
block.y[5] = Component(word3, 2);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class VideoIOPixelFormat
|
||||||
|
{
|
||||||
|
Uyvy8,
|
||||||
|
V210,
|
||||||
|
Bgra8
|
||||||
|
};
|
||||||
|
|
||||||
|
struct V210CodeValues
|
||||||
|
{
|
||||||
|
uint16_t y = 64;
|
||||||
|
uint16_t cb = 512;
|
||||||
|
uint16_t cr = 512;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct V210SixPixelBlock
|
||||||
|
{
|
||||||
|
std::array<uint16_t, 6> y = {};
|
||||||
|
std::array<uint16_t, 3> cb = {};
|
||||||
|
std::array<uint16_t, 3> cr = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* VideoIOPixelFormatName(VideoIOPixelFormat format);
|
||||||
|
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format);
|
||||||
|
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
|
||||||
|
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
|
||||||
|
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
|
||||||
|
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
|
||||||
|
unsigned MinimumV210RowBytes(unsigned frameWidth);
|
||||||
|
unsigned ActiveV210WordsForWidth(unsigned frameWidth);
|
||||||
|
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue);
|
||||||
|
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block);
|
||||||
|
V210SixPixelBlock UnpackV210Block(const std::array<uint8_t, 16>& bytes);
|
||||||
137
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h
Normal file
137
apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
enum class VideoIOBackend
|
||||||
|
{
|
||||||
|
DeckLink
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class VideoIOCompletionResult
|
||||||
|
{
|
||||||
|
Completed,
|
||||||
|
DisplayedLate,
|
||||||
|
Dropped,
|
||||||
|
Flushed,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOConfig
|
||||||
|
{
|
||||||
|
VideoFormatSelection videoModes;
|
||||||
|
bool externalKeyingEnabled = false;
|
||||||
|
bool preferTenBit = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOState
|
||||||
|
{
|
||||||
|
FrameSize inputFrameSize;
|
||||||
|
FrameSize outputFrameSize;
|
||||||
|
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
unsigned inputFrameRowBytes = 0;
|
||||||
|
unsigned outputFrameRowBytes = 0;
|
||||||
|
unsigned captureTextureWidth = 0;
|
||||||
|
unsigned outputPackTextureWidth = 0;
|
||||||
|
std::string inputDisplayModeName = "1080p59.94";
|
||||||
|
std::string outputDisplayModeName = "1080p59.94";
|
||||||
|
std::string outputModelName;
|
||||||
|
std::string statusMessage;
|
||||||
|
std::string formatStatusMessage;
|
||||||
|
bool hasInputDevice = false;
|
||||||
|
bool hasInputSource = false;
|
||||||
|
bool supportsInternalKeying = false;
|
||||||
|
bool supportsExternalKeying = false;
|
||||||
|
bool keyerInterfaceAvailable = false;
|
||||||
|
bool externalKeyingActive = false;
|
||||||
|
double frameBudgetMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOFrame
|
||||||
|
{
|
||||||
|
void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
bool hasNoInputSource = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOOutputFrame
|
||||||
|
{
|
||||||
|
void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
void* nativeFrame = nullptr;
|
||||||
|
void* nativeBuffer = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOCompletion
|
||||||
|
{
|
||||||
|
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoIOScheduleTime
|
||||||
|
{
|
||||||
|
int64_t streamTime = 0;
|
||||||
|
int64_t duration = 0;
|
||||||
|
int64_t timeScale = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VideoIODevice
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
|
||||||
|
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
||||||
|
|
||||||
|
virtual ~VideoIODevice() = default;
|
||||||
|
virtual void ReleaseResources() = 0;
|
||||||
|
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
||||||
|
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
||||||
|
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
||||||
|
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
||||||
|
virtual bool Start() = 0;
|
||||||
|
virtual bool Stop() = 0;
|
||||||
|
virtual const VideoIOState& State() const = 0;
|
||||||
|
virtual VideoIOState& MutableState() = 0;
|
||||||
|
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||||
|
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||||
|
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
||||||
|
virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0;
|
||||||
|
|
||||||
|
bool HasInputDevice() const { return State().hasInputDevice; }
|
||||||
|
bool HasInputSource() const { return State().hasInputSource; }
|
||||||
|
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
|
||||||
|
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
|
||||||
|
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
|
||||||
|
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
|
||||||
|
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
|
||||||
|
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
|
||||||
|
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
|
||||||
|
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
|
||||||
|
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
|
||||||
|
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
|
||||||
|
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
|
||||||
|
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
|
||||||
|
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
|
||||||
|
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
|
||||||
|
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
||||||
|
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
||||||
|
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
||||||
|
const std::string& OutputModelName() const { return State().outputModelName; }
|
||||||
|
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
|
||||||
|
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
|
||||||
|
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
|
||||||
|
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
||||||
|
const std::string& StatusMessage() const { return State().statusMessage; }
|
||||||
|
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
||||||
|
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#include "VideoPlayoutScheduler.h"
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
||||||
|
{
|
||||||
|
mFrameDuration = frameDuration;
|
||||||
|
mTimeScale = timeScale;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::Reset()
|
||||||
|
{
|
||||||
|
mScheduledFrameIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||||
|
{
|
||||||
|
VideoIOScheduleTime time;
|
||||||
|
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
|
||||||
|
time.duration = mFrameDuration;
|
||||||
|
time.timeScale = mTimeScale;
|
||||||
|
time.frameIndex = mScheduledFrameIndex;
|
||||||
|
++mScheduledFrameIndex;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
|
||||||
|
{
|
||||||
|
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
|
||||||
|
mScheduledFrameIndex += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
||||||
|
{
|
||||||
|
return mTimeScale != 0
|
||||||
|
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
||||||
|
: 0.0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class VideoPlayoutScheduler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void Configure(int64_t frameDuration, int64_t timeScale);
|
||||||
|
void Reset();
|
||||||
|
VideoIOScheduleTime NextScheduleTime();
|
||||||
|
void AccountForCompletionResult(VideoIOCompletionResult result);
|
||||||
|
double FrameBudgetMilliseconds() const;
|
||||||
|
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||||
|
int64_t TimeScale() const { return mTimeScale; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int64_t mFrameDuration = 0;
|
||||||
|
int64_t mTimeScale = 0;
|
||||||
|
uint64_t mScheduledFrameIndex = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
std::string NormalizeModeToken(const std::string& value)
|
||||||
|
{
|
||||||
|
std::string normalized;
|
||||||
|
for (unsigned char ch : value)
|
||||||
|
{
|
||||||
|
if (std::isalnum(ch))
|
||||||
|
normalized.push_back(static_cast<char>(std::tolower(ch)));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName)
|
||||||
|
{
|
||||||
|
VideoFormat videoMode;
|
||||||
|
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
displayMode = videoMode.displayMode;
|
||||||
|
displayModeName = videoMode.displayName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
|
||||||
|
{
|
||||||
|
const std::string formatToken = NormalizeModeToken(videoFormat);
|
||||||
|
const std::string frameToken = NormalizeModeToken(frameRate);
|
||||||
|
const std::string combinedToken = formatToken + frameToken;
|
||||||
|
|
||||||
|
struct ModeOption
|
||||||
|
{
|
||||||
|
const char* token;
|
||||||
|
BMDDisplayMode mode;
|
||||||
|
const char* displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ModeOption options[] =
|
||||||
|
{
|
||||||
|
{ "720p50", bmdModeHD720p50, "720p50" },
|
||||||
|
{ "hd720p50", bmdModeHD720p50, "720p50" },
|
||||||
|
{ "720p5994", bmdModeHD720p5994, "720p59.94" },
|
||||||
|
{ "hd720p5994", bmdModeHD720p5994, "720p59.94" },
|
||||||
|
{ "720p60", bmdModeHD720p60, "720p60" },
|
||||||
|
{ "hd720p60", bmdModeHD720p60, "720p60" },
|
||||||
|
{ "1080i50", bmdModeHD1080i50, "1080i50" },
|
||||||
|
{ "hd1080i50", bmdModeHD1080i50, "1080i50" },
|
||||||
|
{ "1080i5994", bmdModeHD1080i5994, "1080i59.94" },
|
||||||
|
{ "hd1080i5994", bmdModeHD1080i5994, "1080i59.94" },
|
||||||
|
{ "1080i60", bmdModeHD1080i6000, "1080i60" },
|
||||||
|
{ "hd1080i60", bmdModeHD1080i6000, "1080i60" },
|
||||||
|
{ "1080p2398", bmdModeHD1080p2398, "1080p23.98" },
|
||||||
|
{ "hd1080p2398", bmdModeHD1080p2398, "1080p23.98" },
|
||||||
|
{ "1080p24", bmdModeHD1080p24, "1080p24" },
|
||||||
|
{ "hd1080p24", bmdModeHD1080p24, "1080p24" },
|
||||||
|
{ "1080p25", bmdModeHD1080p25, "1080p25" },
|
||||||
|
{ "hd1080p25", bmdModeHD1080p25, "1080p25" },
|
||||||
|
{ "1080p2997", bmdModeHD1080p2997, "1080p29.97" },
|
||||||
|
{ "hd1080p2997", bmdModeHD1080p2997, "1080p29.97" },
|
||||||
|
{ "1080p30", bmdModeHD1080p30, "1080p30" },
|
||||||
|
{ "hd1080p30", bmdModeHD1080p30, "1080p30" },
|
||||||
|
{ "1080p50", bmdModeHD1080p50, "1080p50" },
|
||||||
|
{ "hd1080p50", bmdModeHD1080p50, "1080p50" },
|
||||||
|
{ "1080p5994", bmdModeHD1080p5994, "1080p59.94" },
|
||||||
|
{ "hd1080p5994", bmdModeHD1080p5994, "1080p59.94" },
|
||||||
|
{ "1080p60", bmdModeHD1080p6000, "1080p60" },
|
||||||
|
{ "hd1080p60", bmdModeHD1080p6000, "1080p60" },
|
||||||
|
{ "2160p2398", bmdMode4K2160p2398, "2160p23.98" },
|
||||||
|
{ "4k2160p2398", bmdMode4K2160p2398, "2160p23.98" },
|
||||||
|
{ "2160p24", bmdMode4K2160p24, "2160p24" },
|
||||||
|
{ "4k2160p24", bmdMode4K2160p24, "2160p24" },
|
||||||
|
{ "2160p25", bmdMode4K2160p25, "2160p25" },
|
||||||
|
{ "4k2160p25", bmdMode4K2160p25, "2160p25" },
|
||||||
|
{ "2160p2997", bmdMode4K2160p2997, "2160p29.97" },
|
||||||
|
{ "4k2160p2997", bmdMode4K2160p2997, "2160p29.97" },
|
||||||
|
{ "2160p30", bmdMode4K2160p30, "2160p30" },
|
||||||
|
{ "4k2160p30", bmdMode4K2160p30, "2160p30" },
|
||||||
|
{ "2160p50", bmdMode4K2160p50, "2160p50" },
|
||||||
|
{ "4k2160p50", bmdMode4K2160p50, "2160p50" },
|
||||||
|
{ "2160p5994", bmdMode4K2160p5994, "2160p59.94" },
|
||||||
|
{ "4k2160p5994", bmdMode4K2160p5994, "2160p59.94" },
|
||||||
|
{ "2160p60", bmdMode4K2160p60, "2160p60" },
|
||||||
|
{ "4k2160p60", bmdMode4K2160p60, "2160p60" }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ModeOption& option : options)
|
||||||
|
{
|
||||||
|
if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token))
|
||||||
|
{
|
||||||
|
videoMode.displayMode = option.mode;
|
||||||
|
videoMode.displayName = option.displayName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolveConfiguredVideoFormats(
|
||||||
|
const std::string& inputVideoFormat,
|
||||||
|
const std::string& inputFrameRate,
|
||||||
|
const std::string& outputVideoFormat,
|
||||||
|
const std::string& outputFrameRate,
|
||||||
|
VideoFormatSelection& videoModes,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
|
||||||
|
{
|
||||||
|
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
|
inputVideoFormat + " / " + inputFrameRate;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
|
||||||
|
{
|
||||||
|
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
|
||||||
|
outputVideoFormat + " / " + outputFrameRate;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
|
||||||
|
{
|
||||||
|
if (!iterator || !foundMode)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
*foundMode = NULL;
|
||||||
|
IDeckLinkDisplayMode* candidate = NULL;
|
||||||
|
while (iterator->Next(&candidate) == S_OK)
|
||||||
|
{
|
||||||
|
if (candidate->GetDisplayMode() == targetMode)
|
||||||
|
{
|
||||||
|
*foundMode = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate->Release();
|
||||||
|
candidate = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#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);
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#include "DeckLinkFrameTransfer.h"
|
||||||
|
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// DeckLink Capture Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
|
||||||
|
m_pOwner(pOwner),
|
||||||
|
mRefCount(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT CaptureDelegate::QueryInterface(REFIID, LPVOID* ppv)
|
||||||
|
{
|
||||||
|
*ppv = NULL;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG CaptureDelegate::AddRef()
|
||||||
|
{
|
||||||
|
return InterlockedIncrement(&mRefCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG CaptureDelegate::Release()
|
||||||
|
{
|
||||||
|
int newCount = InterlockedDecrement(&mRefCount);
|
||||||
|
if (newCount == 0)
|
||||||
|
delete this;
|
||||||
|
return newCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket*)
|
||||||
|
{
|
||||||
|
if (!inputFrame)
|
||||||
|
{
|
||||||
|
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
|
||||||
|
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// DeckLink Playout Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
|
||||||
|
m_pOwner(pOwner),
|
||||||
|
mRefCount(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT PlayoutDelegate::QueryInterface(REFIID, LPVOID* ppv)
|
||||||
|
{
|
||||||
|
*ppv = NULL;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG PlayoutDelegate::AddRef()
|
||||||
|
{
|
||||||
|
return InterlockedIncrement(&mRefCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG PlayoutDelegate::Release()
|
||||||
|
{
|
||||||
|
int newCount = InterlockedDecrement(&mRefCount);
|
||||||
|
if (newCount == 0)
|
||||||
|
delete this;
|
||||||
|
return newCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case bmdOutputFrameDisplayedLate:
|
||||||
|
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Displayed Late\n");
|
||||||
|
break;
|
||||||
|
case bmdOutputFrameDropped:
|
||||||
|
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Dropped\n");
|
||||||
|
break;
|
||||||
|
case bmdOutputFrameCompleted:
|
||||||
|
case bmdOutputFrameFlushed:
|
||||||
|
// Don't log bmdOutputFrameFlushed result since it is expected when Stop() is called
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT PlayoutDelegate::ScheduledPlaybackHasStopped()
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
|
||||||
|
class DeckLinkSession;
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// Capture Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
class CaptureDelegate : public IDeckLinkInputCallback
|
||||||
|
{
|
||||||
|
DeckLinkSession* m_pOwner;
|
||||||
|
LONG mRefCount;
|
||||||
|
|
||||||
|
public:
|
||||||
|
CaptureDelegate(DeckLinkSession* pOwner);
|
||||||
|
|
||||||
|
// IUnknown needs only a dummy implementation
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
||||||
|
virtual ULONG STDMETHODCALLTYPE AddRef();
|
||||||
|
virtual ULONG STDMETHODCALLTYPE Release();
|
||||||
|
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket);
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// Render Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
|
||||||
|
{
|
||||||
|
DeckLinkSession* m_pOwner;
|
||||||
|
LONG mRefCount;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PlayoutDelegate(DeckLinkSession* pOwner);
|
||||||
|
|
||||||
|
// IUnknown needs only a dummy implementation
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
||||||
|
virtual ULONG STDMETHODCALLTYPE AddRef();
|
||||||
|
virtual ULONG STDMETHODCALLTYPE Release();
|
||||||
|
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped();
|
||||||
|
};
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
#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 VideoFormatSelection& videoModes, std::string& error)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
||||||
|
CComPtr<IDeckLinkDisplayMode> inputMode;
|
||||||
|
CComPtr<IDeckLinkDisplayMode> outputMode;
|
||||||
|
|
||||||
|
mState.inputDisplayModeName = videoModes.input.displayName;
|
||||||
|
mState.outputDisplayModeName = videoModes.output.displayName;
|
||||||
|
|
||||||
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
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.outputModelName = modelName;
|
||||||
|
mState.supportsInternalKeying = deviceSupportsInternalKeying;
|
||||||
|
mState.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 VideoFormatSelection& videoModes, std::string& error)
|
||||||
|
{
|
||||||
|
if (!output)
|
||||||
|
{
|
||||||
|
error = "Expected an Output DeckLink device";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mState.formatStatusMessage.clear();
|
||||||
|
|
||||||
|
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.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, videoModes.output.displayMode, bmdFormat10BitYUV);
|
||||||
|
mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8;
|
||||||
|
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, const VideoFormat& inputVideoMode, 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(inputVideoMode.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(inputVideoMode.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, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||||
|
{
|
||||||
|
mOutputFrameCallback = std::move(callback);
|
||||||
|
|
||||||
|
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)
|
||||||
|
mState.keyerInterfaceAvailable = true;
|
||||||
|
|
||||||
|
if (externalKeyingEnabled)
|
||||||
|
{
|
||||||
|
if (!mState.supportsExternalKeying)
|
||||||
|
{
|
||||||
|
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
||||||
|
}
|
||||||
|
else if (!mState.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.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "DeckLinkFrameTransfer.h"
|
||||||
|
#include "DeckLinkVideoIOFormat.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutScheduler.h"
|
||||||
|
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class OpenGLComposite;
|
||||||
|
|
||||||
|
class DeckLinkSession : public VideoIODevice
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkSession() = default;
|
||||||
|
~DeckLinkSession();
|
||||||
|
|
||||||
|
void ReleaseResources() override;
|
||||||
|
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
|
||||||
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) override;
|
||||||
|
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
||||||
|
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
||||||
|
bool Start() override;
|
||||||
|
bool Stop() override;
|
||||||
|
|
||||||
|
bool HasInputDevice() const { return mState.hasInputDevice; }
|
||||||
|
bool HasInputSource() const { return mState.hasInputSource; }
|
||||||
|
void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
|
||||||
|
bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
|
||||||
|
const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
|
||||||
|
const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
|
||||||
|
unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
|
||||||
|
unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
|
||||||
|
unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
|
||||||
|
unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
|
||||||
|
VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
|
||||||
|
VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
|
||||||
|
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
|
||||||
|
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
|
||||||
|
unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
|
||||||
|
unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
|
||||||
|
unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
|
||||||
|
unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
|
||||||
|
const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
|
||||||
|
const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
|
||||||
|
const std::string& OutputModelName() const { return mState.outputModelName; }
|
||||||
|
bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
|
||||||
|
bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
|
||||||
|
bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
|
||||||
|
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
|
||||||
|
const std::string& StatusMessage() const { return mState.statusMessage; }
|
||||||
|
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
||||||
|
const VideoIOState& State() const override { return mState; }
|
||||||
|
VideoIOState& MutableState() override { return mState; }
|
||||||
|
double FrameBudgetMilliseconds() const;
|
||||||
|
void AccountForCompletionResult(VideoIOCompletionResult completionResult) override;
|
||||||
|
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
|
||||||
|
void EndOutputFrame(VideoIOOutputFrame& frame) override;
|
||||||
|
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
|
||||||
|
void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||||
|
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
|
private:
|
||||||
|
CComPtr<CaptureDelegate> captureDelegate;
|
||||||
|
CComPtr<PlayoutDelegate> playoutDelegate;
|
||||||
|
CComPtr<IDeckLinkInput> input;
|
||||||
|
CComPtr<IDeckLinkOutput> output;
|
||||||
|
CComPtr<IDeckLinkKeyer> keyer;
|
||||||
|
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||||
|
VideoIOState mState;
|
||||||
|
VideoPlayoutScheduler mScheduler;
|
||||||
|
InputFrameCallback mInputFrameCallback;
|
||||||
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#include "DeckLinkVideoIOFormat.h"
|
||||||
|
|
||||||
|
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case VideoIOPixelFormat::V210:
|
||||||
|
return bmdFormat10BitYUV;
|
||||||
|
case VideoIOPixelFormat::Bgra8:
|
||||||
|
return bmdFormat8BitBGRA;
|
||||||
|
case VideoIOPixelFormat::Uyvy8:
|
||||||
|
default:
|
||||||
|
return bmdFormat8BitYUV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
|
||||||
|
{
|
||||||
|
if (format == bmdFormat10BitYUV)
|
||||||
|
return VideoIOPixelFormat::V210;
|
||||||
|
if (format == bmdFormat8BitBGRA)
|
||||||
|
return VideoIOPixelFormat::Bgra8;
|
||||||
|
return VideoIOPixelFormat::Uyvy8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
|
||||||
|
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
|
||||||
@@ -70,6 +70,12 @@ Examples:
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/VideoShaderToys/trigger-flash/flash 1
|
||||||
|
```
|
||||||
|
|
||||||
## Open Stage Control
|
## Open Stage Control
|
||||||
|
|
||||||
For simple scalar controls, set the widget address and target directly:
|
For simple scalar controls, set the widget address and target directly:
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ paths:
|
|||||||
$ref: "#/components/responses/ActionOk"
|
$ref: "#/components/responses/ActionOk"
|
||||||
"400":
|
"400":
|
||||||
$ref: "#/components/responses/ActionError"
|
$ref: "#/components/responses/ActionError"
|
||||||
|
/api/screenshot:
|
||||||
|
post:
|
||||||
|
tags: [Runtime]
|
||||||
|
summary: Queue a PNG screenshot of the final output render target
|
||||||
|
description: Captures the next completed output render target and writes it under `runtime/screenshots/`.
|
||||||
|
operationId: queueScreenshot
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ActionOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/ActionError"
|
||||||
components:
|
components:
|
||||||
responses:
|
responses:
|
||||||
ActionOk:
|
ActionOk:
|
||||||
@@ -340,6 +358,8 @@ components:
|
|||||||
$ref: "#/components/schemas/VideoStatus"
|
$ref: "#/components/schemas/VideoStatus"
|
||||||
decklink:
|
decklink:
|
||||||
$ref: "#/components/schemas/DeckLinkStatus"
|
$ref: "#/components/schemas/DeckLinkStatus"
|
||||||
|
videoIO:
|
||||||
|
$ref: "#/components/schemas/VideoIOStatus"
|
||||||
performance:
|
performance:
|
||||||
$ref: "#/components/schemas/PerformanceStatus"
|
$ref: "#/components/schemas/PerformanceStatus"
|
||||||
shaders:
|
shaders:
|
||||||
@@ -397,6 +417,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
DeckLinkStatus:
|
DeckLinkStatus:
|
||||||
type: object
|
type: object
|
||||||
|
deprecated: true
|
||||||
|
description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
|
||||||
properties:
|
properties:
|
||||||
modelName:
|
modelName:
|
||||||
type: string
|
type: string
|
||||||
@@ -412,6 +434,26 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
statusMessage:
|
statusMessage:
|
||||||
type: string
|
type: string
|
||||||
|
VideoIOStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
example: decklink
|
||||||
|
modelName:
|
||||||
|
type: string
|
||||||
|
supportsInternalKeying:
|
||||||
|
type: boolean
|
||||||
|
supportsExternalKeying:
|
||||||
|
type: boolean
|
||||||
|
keyerInterfaceAvailable:
|
||||||
|
type: boolean
|
||||||
|
externalKeyingRequested:
|
||||||
|
type: boolean
|
||||||
|
externalKeyingActive:
|
||||||
|
type: boolean
|
||||||
|
statusMessage:
|
||||||
|
type: string
|
||||||
PerformanceStatus:
|
PerformanceStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -474,7 +516,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
enum: [float, vec2, color, bool, enum]
|
enum: [float, vec2, color, bool, enum, text, trigger]
|
||||||
min:
|
min:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Generated files:
|
|||||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
||||||
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
|
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
|
||||||
- `stack_presets/*.json`: user-saved layer stack presets.
|
- `stack_presets/*.json`: user-saved layer stack presets.
|
||||||
|
- `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.
|
||||||
|
|
||||||
Git policy:
|
Git policy:
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
|
float startupRandom;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -23,6 +26,9 @@ cbuffer GlobalParams
|
|||||||
float gTime;
|
float gTime;
|
||||||
float2 gInputResolution;
|
float2 gInputResolution;
|
||||||
float2 gOutputResolution;
|
float2 gOutputResolution;
|
||||||
|
float gUtcTimeSeconds;
|
||||||
|
float gUtcOffsetSeconds;
|
||||||
|
float gStartupRandom;
|
||||||
float gFrameCount;
|
float gFrameCount;
|
||||||
float gMixAmount;
|
float gMixAmount;
|
||||||
float gBypass;
|
float gBypass;
|
||||||
@@ -32,6 +38,7 @@ cbuffer GlobalParams
|
|||||||
|
|
||||||
Sampler2D<float4> gVideoInput;
|
Sampler2D<float4> gVideoInput;
|
||||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
||||||
|
{{TEXT_SAMPLERS}}
|
||||||
float4 sampleVideo(float2 tc)
|
float4 sampleVideo(float2 tc)
|
||||||
{
|
{
|
||||||
return gVideoInput.Sample(tc);
|
return gVideoInput.Sample(tc);
|
||||||
@@ -67,6 +74,7 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{TEXT_HELPERS}}
|
||||||
#include "{{USER_SHADER_INCLUDE}}"
|
#include "{{USER_SHADER_INCLUDE}}"
|
||||||
|
|
||||||
[shader("fragment")]
|
[shader("fragment")]
|
||||||
@@ -78,6 +86,9 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
|||||||
context.inputResolution = gInputResolution;
|
context.inputResolution = gInputResolution;
|
||||||
context.outputResolution = gOutputResolution;
|
context.outputResolution = gOutputResolution;
|
||||||
context.time = gTime;
|
context.time = gTime;
|
||||||
|
context.utcTimeSeconds = gUtcTimeSeconds;
|
||||||
|
context.utcOffsetSeconds = gUtcOffsetSeconds;
|
||||||
|
context.startupRandom = gStartupRandom;
|
||||||
context.frameCount = gFrameCount;
|
context.frameCount = gFrameCount;
|
||||||
context.mixAmount = gMixAmount;
|
context.mixAmount = gMixAmount;
|
||||||
context.bypass = gBypass;
|
context.bypass = gBypass;
|
||||||
|
|||||||
46
shaders/anamorphic-desqueeze/shader.json
Normal file
46
shaders/anamorphic-desqueeze/shader.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"id": "anamorphic-desqueeze",
|
||||||
|
"name": "Anamorphic Desqueeze",
|
||||||
|
"description": "Desqueezes anamorphic footage by 1.3x, 1.33x, 1.5x, or 2x with fit or fill framing.",
|
||||||
|
"category": "Transform",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "desqueezeFactor",
|
||||||
|
"label": "Desqueeze",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "x1_33",
|
||||||
|
"options": [
|
||||||
|
{ "value": "x1_3", "label": "1.3x" },
|
||||||
|
{ "value": "x1_33", "label": "1.33x" },
|
||||||
|
{ "value": "x1_5", "label": "1.5x" },
|
||||||
|
{ "value": "x2_0", "label": "2x" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "framing",
|
||||||
|
"label": "Framing",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "fit",
|
||||||
|
"options": [
|
||||||
|
{ "value": "fit", "label": "Fit" },
|
||||||
|
{ "value": "fill", "label": "Fill" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pan",
|
||||||
|
"label": "Pan",
|
||||||
|
"type": "vec2",
|
||||||
|
"default": [0.0, 0.0],
|
||||||
|
"min": [-1.0, -1.0],
|
||||||
|
"max": [1.0, 1.0],
|
||||||
|
"step": [0.001, 0.001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "outsideColor",
|
||||||
|
"label": "Outside Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.0, 0.0, 0.0, 1.0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
float selectedDesqueezeFactor()
|
||||||
|
{
|
||||||
|
if (desqueezeFactor == 0)
|
||||||
|
return 1.3;
|
||||||
|
if (desqueezeFactor == 1)
|
||||||
|
return 1.3333333;
|
||||||
|
if (desqueezeFactor == 2)
|
||||||
|
return 1.5;
|
||||||
|
return 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float factor = selectedDesqueezeFactor();
|
||||||
|
float2 centered = context.uv - 0.5;
|
||||||
|
|
||||||
|
if (framing == 0)
|
||||||
|
{
|
||||||
|
centered.y *= factor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
centered.x /= factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 sourceUv = centered + 0.5 - pan;
|
||||||
|
bool inside = sourceUv.x >= 0.0 && sourceUv.x <= 1.0 && sourceUv.y >= 0.0 && sourceUv.y <= 1.0;
|
||||||
|
if (!inside)
|
||||||
|
return outsideColor;
|
||||||
|
|
||||||
|
return sampleVideo(sourceUv);
|
||||||
|
}
|
||||||
105
shaders/balatro-swirl/shader.json
Normal file
105
shaders/balatro-swirl/shader.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"id": "balatro-swirl",
|
||||||
|
"name": "Balatro Swirl",
|
||||||
|
"description": "Animated painterly swirl background. Original by localthunk (https://www.playbalatro.com), adapted from https://www.shadertoy.com/view/XXtBRr.",
|
||||||
|
"category": "Generative",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "spinRotation",
|
||||||
|
"label": "Spin Rotation",
|
||||||
|
"type": "float",
|
||||||
|
"default": -2.0,
|
||||||
|
"min": -8.0,
|
||||||
|
"max": 8.0,
|
||||||
|
"step": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spinSpeed",
|
||||||
|
"label": "Spin Speed",
|
||||||
|
"type": "float",
|
||||||
|
"default": 7.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 20.0,
|
||||||
|
"step": 0.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spinAmount",
|
||||||
|
"label": "Spin Amount",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.25,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spinEase",
|
||||||
|
"label": "Spin Ease",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 3.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contrast",
|
||||||
|
"label": "Contrast",
|
||||||
|
"type": "float",
|
||||||
|
"default": 3.5,
|
||||||
|
"min": 0.5,
|
||||||
|
"max": 8.0,
|
||||||
|
"step": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lighting",
|
||||||
|
"label": "Lighting",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.4,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.5,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "offset",
|
||||||
|
"label": "Offset",
|
||||||
|
"type": "vec2",
|
||||||
|
"default": [0.0, 0.0],
|
||||||
|
"min": [-1.0, -1.0],
|
||||||
|
"max": [1.0, 1.0],
|
||||||
|
"step": [0.001, 0.001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colour1",
|
||||||
|
"label": "Colour 1",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.871, 0.267, 0.231, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colour2",
|
||||||
|
"label": "Colour 2",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.0, 0.42, 0.706, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "colour3",
|
||||||
|
"label": "Colour 3",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.086, 0.137, 0.145, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "isRotate",
|
||||||
|
"label": "Rotate Field",
|
||||||
|
"type": "bool",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sourceMix",
|
||||||
|
"label": "Source Mix",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
shaders/balatro-swirl/shader.slang
Normal file
48
shaders/balatro-swirl/shader.slang
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float seed)
|
||||||
|
{
|
||||||
|
const float pi = 3.14159265359;
|
||||||
|
float safeScreenLength = max(length(screenSize), 1.0);
|
||||||
|
float2 seedOffset = float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||||
|
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
|
||||||
|
float uvLength = length(uv);
|
||||||
|
|
||||||
|
float speed = spinRotation * spinEase * 0.2;
|
||||||
|
if (isRotate)
|
||||||
|
speed = time * speed;
|
||||||
|
speed += 302.2 + seed * 6.2831853;
|
||||||
|
|
||||||
|
float newPixelAngle = atan2(uv.y, uv.x) + speed - spinEase * 20.0 * (spinAmount * uvLength + (1.0 - spinAmount));
|
||||||
|
float2 mid = (screenSize / safeScreenLength) * 0.5;
|
||||||
|
uv = float2(uvLength * cos(newPixelAngle) + mid.x, uvLength * sin(newPixelAngle) + mid.y) - mid;
|
||||||
|
|
||||||
|
uv *= 30.0;
|
||||||
|
speed = (time + seed * 17.0) * spinSpeed;
|
||||||
|
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
|
||||||
|
uv += 0.5 * float2(cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121), sin(uv2.x - 0.113 * speed));
|
||||||
|
float warp = cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);
|
||||||
|
uv -= float2(warp, warp);
|
||||||
|
}
|
||||||
|
|
||||||
|
float contrastMod = 0.25 * contrast + 0.5 * spinAmount + 1.2;
|
||||||
|
float paintRes = min(2.0, max(0.0, length(uv) * 0.035 * contrastMod));
|
||||||
|
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
|
||||||
|
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
|
||||||
|
float c3p = 1.0 - min(1.0, c1p + c2p);
|
||||||
|
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);
|
||||||
|
float4 base = (0.3 / safeContrast) * colour1;
|
||||||
|
float4 paint = colour1 * c1p + colour2 * c2p + float4(c3p * colour3.rgb, c3p * colour1.a);
|
||||||
|
return base + (1.0 - 0.3 / safeContrast) * paint + float4(light, light, light, light);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 screenSize = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
|
float4 swirl = balatroSwirl(screenSize, context.uv * screenSize, context.time, context.startupRandom);
|
||||||
|
return saturate(lerp(swirl, context.sourceColor, sourceMix));
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "black-and-white",
|
"id": "black-and-white",
|
||||||
"name": "Black and White",
|
"name": "Black and White",
|
||||||
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
|
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
|
||||||
"category": "Built-in",
|
"category": "Color",
|
||||||
"entryPoint": "shadeVideo",
|
"entryPoint": "shadeVideo",
|
||||||
"parameters": []
|
"parameters": []
|
||||||
}
|
}
|
||||||
|
|||||||
15
shaders/broken-shader-example/shader.json
Normal file
15
shaders/broken-shader-example/shader.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"id": "broken-shader-example",
|
||||||
|
"name": "Broken Shader Example",
|
||||||
|
"description": "Intentionally invalid shader package used to verify that bad shaders appear as errors without blocking the app.",
|
||||||
|
"category": "Diagnostics",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "badToggle",
|
||||||
|
"label": "Bad Toggle",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
shaders/broken-shader-example/shader.slang
Normal file
4
shaders/broken-shader-example/shader.slang
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
return context.sourceColor;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "composition-guides",
|
"id": "composition-guides",
|
||||||
"name": "Composition Guides",
|
"name": "Composition Guides",
|
||||||
"description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.",
|
"description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.",
|
||||||
"category": "Utility",
|
"category": "Scopes & Guides",
|
||||||
"entryPoint": "shadeVideo",
|
"entryPoint": "shadeVideo",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
{
|
{
|
||||||
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
||||||
float2 blockId = floor(context.uv * blocks);
|
float2 blockId = floor(context.uv * blocks);
|
||||||
float n = hash21(blockId + floor(context.time * 8.0));
|
float seed = context.startupRandom * 4096.0;
|
||||||
|
float frameSeed = floor(context.time * 8.0);
|
||||||
|
float n = hash21(blockId + float2(frameSeed + seed, frameSeed + seed * 0.17));
|
||||||
float historyFrame = floor(lerp(1.0, 7.0, n));
|
float historyFrame = floor(lerp(1.0, 7.0, n));
|
||||||
|
|
||||||
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y), floor(context.time * 5.0)));
|
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y) + seed * 0.37, floor(context.time * 5.0) + seed * 0.13));
|
||||||
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
|
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
|
||||||
float2 offset = float2(tear, (hash21(blockId + 19.0) * 2.0 - 1.0) * amount * 0.025);
|
float2 offset = float2(tear, (hash21(blockId + float2(19.0 + seed, 19.0 + seed * 0.31)) * 2.0 - 1.0) * amount * 0.025);
|
||||||
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
|
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
|
||||||
|
|
||||||
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"id": "dvd-bounce",
|
"id": "dvd-bounce",
|
||||||
"name": "DVD Bounce",
|
"name": "DVD Bounce",
|
||||||
"description": "A transparent bouncing DVD logo sprite that changes color on each screen hit.",
|
"description": "A transparent bouncing DVD logo sprite that changes color on each screen hit.",
|
||||||
"category": "Built-in",
|
"category": "Generative",
|
||||||
"entryPoint": "shadeVideo",
|
"entryPoint": "shadeVideo",
|
||||||
"textures": [
|
"textures": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 velocityPx = float2(
|
float2 velocityPx = float2(
|
||||||
max(20.0, bounceSpeed * minDimension * 1.00),
|
max(20.0, bounceSpeed * minDimension * 1.00),
|
||||||
max(24.0, bounceSpeed * minDimension * 0.77));
|
max(24.0, bounceSpeed * minDimension * 0.77));
|
||||||
float2 motionPx = context.time * velocityPx;
|
float seed = context.startupRandom;
|
||||||
|
float2 motionPx = (float2(context.time, context.time) + float2(seed * 32.7, seed * 40.3)) * velocityPx;
|
||||||
float2 centerPx = minCenterPx + float2(
|
float2 centerPx = minCenterPx + float2(
|
||||||
pingPong(motionPx.x, travelPx.x),
|
pingPong(motionPx.x, travelPx.x),
|
||||||
pingPong(motionPx.y, travelPx.y));
|
pingPong(motionPx.y, travelPx.y));
|
||||||
@@ -38,7 +39,7 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
|
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
|
||||||
int totalHits = max(0, xHits + yHits);
|
int totalHits = max(0, xHits + yHits);
|
||||||
|
|
||||||
float hue = frac(0.09 + float(totalHits) * 0.173);
|
float hue = frac(0.09 + seed * 0.71 + float(totalHits) * 0.173);
|
||||||
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
|
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
|
||||||
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user