Compare commits
56 Commits
v0.0.2
...
ebbc11bb34
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,15 +12,48 @@ on:
|
||||
jobs:
|
||||
native-windows:
|
||||
name: Native Windows Build And Tests
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
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
|
||||
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
|
||||
shell: powershell
|
||||
@@ -32,7 +65,7 @@ jobs:
|
||||
|
||||
ui-ubuntu:
|
||||
name: React UI Build
|
||||
runs-on: nubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -48,7 +81,7 @@ jobs:
|
||||
|
||||
package-windows:
|
||||
name: Windows Release Package
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
needs:
|
||||
- native-windows
|
||||
- ui-ubuntu
|
||||
@@ -57,6 +90,16 @@ jobs:
|
||||
- name: Checkout
|
||||
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
|
||||
shell: powershell
|
||||
working-directory: ui
|
||||
@@ -66,7 +109,30 @@ jobs:
|
||||
|
||||
- name: Configure Release
|
||||
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
|
||||
shell: powershell
|
||||
@@ -81,7 +147,8 @@ jobs:
|
||||
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
||||
|
||||
- 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:
|
||||
name: VideoShader-windows-release
|
||||
path: dist/VideoShader.zip
|
||||
|
||||
243
CMakeLists.txt
243
CMakeLists.txt
@@ -7,68 +7,132 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
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")
|
||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
|
||||
message(FATAL_ERROR "NVIDIA GPUDirect library not found under ${GPUDIRECT_DIR}")
|
||||
set(SLANG_RUNTIME_FILES
|
||||
"${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()
|
||||
|
||||
set(APP_SOURCES
|
||||
"${APP_DIR}/ControlServer.cpp"
|
||||
"${APP_DIR}/ControlServer.h"
|
||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
||||
"${APP_DIR}/GLExtensions.cpp"
|
||||
"${APP_DIR}/GLExtensions.h"
|
||||
"${APP_DIR}/control/ControlServer.cpp"
|
||||
"${APP_DIR}/control/ControlServer.h"
|
||||
"${APP_DIR}/control/OscServer.cpp"
|
||||
"${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}/decklink/DeckLinkDisplayMode.cpp"
|
||||
"${APP_DIR}/decklink/DeckLinkDisplayMode.h"
|
||||
"${APP_DIR}/decklink/DeckLinkFrameTransfer.cpp"
|
||||
"${APP_DIR}/decklink/DeckLinkFrameTransfer.h"
|
||||
"${APP_DIR}/decklink/DeckLinkSession.cpp"
|
||||
"${APP_DIR}/decklink/DeckLinkSession.h"
|
||||
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp"
|
||||
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.h"
|
||||
"${APP_DIR}/gl/GLExtensions.cpp"
|
||||
"${APP_DIR}/gl/GLExtensions.h"
|
||||
"${APP_DIR}/gl/GlobalParamsBuffer.cpp"
|
||||
"${APP_DIR}/gl/GlobalParamsBuffer.h"
|
||||
"${APP_DIR}/gl/GlRenderConstants.h"
|
||||
"${APP_DIR}/gl/GlScopedObjects.h"
|
||||
"${APP_DIR}/gl/GlShaderSources.cpp"
|
||||
"${APP_DIR}/gl/GlShaderSources.h"
|
||||
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/gl/OpenGLComposite.h"
|
||||
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
||||
"${APP_DIR}/gl/OpenGLRenderPass.cpp"
|
||||
"${APP_DIR}/gl/OpenGLRenderPass.h"
|
||||
"${APP_DIR}/gl/OpenGLRenderer.cpp"
|
||||
"${APP_DIR}/gl/OpenGLRenderer.h"
|
||||
"${APP_DIR}/gl/OpenGLVideoIOBridge.cpp"
|
||||
"${APP_DIR}/gl/OpenGLVideoIOBridge.h"
|
||||
"${APP_DIR}/gl/OpenGLShaderPrograms.cpp"
|
||||
"${APP_DIR}/gl/OpenGLShaderPrograms.h"
|
||||
"${APP_DIR}/gl/PngScreenshotWriter.cpp"
|
||||
"${APP_DIR}/gl/PngScreenshotWriter.h"
|
||||
"${APP_DIR}/gl/ShaderProgramCompiler.cpp"
|
||||
"${APP_DIR}/gl/ShaderProgramCompiler.h"
|
||||
"${APP_DIR}/gl/ShaderBuildQueue.cpp"
|
||||
"${APP_DIR}/gl/ShaderBuildQueue.h"
|
||||
"${APP_DIR}/gl/ShaderTextureBindings.cpp"
|
||||
"${APP_DIR}/gl/ShaderTextureBindings.h"
|
||||
"${APP_DIR}/gl/Std140Buffer.h"
|
||||
"${APP_DIR}/gl/TextRasterizer.cpp"
|
||||
"${APP_DIR}/gl/TextRasterizer.h"
|
||||
"${APP_DIR}/gl/TextureAssetLoader.cpp"
|
||||
"${APP_DIR}/gl/TextureAssetLoader.h"
|
||||
"${APP_DIR}/gl/TemporalHistoryBuffers.cpp"
|
||||
"${APP_DIR}/gl/TemporalHistoryBuffers.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
||||
"${APP_DIR}/NativeHandles.h"
|
||||
"${APP_DIR}/NativeSockets.h"
|
||||
"${APP_DIR}/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/OpenGLComposite.h"
|
||||
"${APP_DIR}/OscServer.cpp"
|
||||
"${APP_DIR}/OscServer.h"
|
||||
"${APP_DIR}/platform/NativeHandles.h"
|
||||
"${APP_DIR}/platform/NativeSockets.h"
|
||||
"${APP_DIR}/resource.h"
|
||||
"${APP_DIR}/RuntimeHost.cpp"
|
||||
"${APP_DIR}/RuntimeHost.h"
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/RuntimeJson.h"
|
||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/ShaderCompiler.h"
|
||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/ShaderPackageRegistry.h"
|
||||
"${APP_DIR}/ShaderTypes.h"
|
||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
||||
"${APP_DIR}/shader/ShaderTypes.h"
|
||||
"${APP_DIR}/stdafx.cpp"
|
||||
"${APP_DIR}/stdafx.h"
|
||||
"${APP_DIR}/targetver.h"
|
||||
"${APP_DIR}/VideoFrameTransfer.cpp"
|
||||
"${APP_DIR}/VideoFrameTransfer.h"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||
)
|
||||
|
||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
||||
|
||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${GPUDIRECT_DIR}/include"
|
||||
)
|
||||
|
||||
target_link_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${GPUDIRECT_DIR}/lib/x64"
|
||||
"${APP_DIR}/control"
|
||||
"${APP_DIR}/decklink"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
"${APP_DIR}/videoio"
|
||||
)
|
||||
|
||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
dvp.lib
|
||||
opengl32
|
||||
glu32
|
||||
Ws2_32
|
||||
Crypt32
|
||||
Advapi32
|
||||
Gdiplus
|
||||
Ole32
|
||||
Windowscodecs
|
||||
)
|
||||
|
||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
@@ -81,12 +145,13 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
add_executable(RuntimeJsonTests
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeJsonTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -96,14 +161,32 @@ endif()
|
||||
enable_testing()
|
||||
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
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -112,14 +195,31 @@ endif()
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(Std140BufferTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||
|
||||
add_executable(ShaderPackageRegistryTests
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -129,12 +229,14 @@ endif()
|
||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||
|
||||
add_executable(OscServerTests
|
||||
"${APP_DIR}/OscServer.cpp"
|
||||
"${APP_DIR}/control/OscServer.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(OscServerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/control"
|
||||
"${APP_DIR}/platform"
|
||||
)
|
||||
|
||||
target_link_libraries(OscServerTests PRIVATE
|
||||
@@ -147,17 +249,72 @@ endif()
|
||||
|
||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
||||
|
||||
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
||||
"$<TARGET_FILE_DIR:LoopThroughWithOpenGLCompositing>/dvp.dll"
|
||||
add_executable(VideoIOFormatTests
|
||||
"${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoIOFormatTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/decklink"
|
||||
"${APP_DIR}/videoio"
|
||||
)
|
||||
|
||||
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}/decklink"
|
||||
"${APP_DIR}/videoio"
|
||||
)
|
||||
|
||||
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}/decklink"
|
||||
"${APP_DIR}/videoio"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
||||
|
||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||
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 "."
|
||||
)
|
||||
|
||||
|
||||
64
README.md
64
README.md
@@ -20,21 +20,19 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
|
||||
- Windows with Visual Studio 2022 C++ tooling.
|
||||
- CMake 3.24 or newer.
|
||||
- Node.js and npm for the control UI.
|
||||
- Blackmagic DeckLink SDK 16.0 with the NVIDIA GPUDirect sample files available locally.
|
||||
- Slang compiler available under the repo/tooling paths expected by the runtime, or otherwise discoverable by the existing app setup.
|
||||
- Blackmagic Desktop Video drivers and a DeckLink device.
|
||||
- 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 SDK path:
|
||||
Default expected Slang path:
|
||||
|
||||
```text
|
||||
3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect
|
||||
3rdParty/slang-2026.8-windows-x86_64
|
||||
```
|
||||
|
||||
Override example:
|
||||
|
||||
```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
|
||||
@@ -75,14 +73,17 @@ The package folder will contain:
|
||||
```text
|
||||
dist/VideoShader/
|
||||
LoopThroughWithOpenGLCompositing.exe
|
||||
dvp.dll
|
||||
config/
|
||||
shaders/
|
||||
3rdParty/slang/bin/
|
||||
ui/dist/
|
||||
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:
|
||||
|
||||
@@ -109,7 +110,7 @@ Current native test coverage includes:
|
||||
|
||||
- JSON parsing and serialization.
|
||||
- Parameter normalization and preset filename safety.
|
||||
- Shader manifest parsing and package registry scanning.
|
||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
@@ -169,6 +170,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.
|
||||
|
||||
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
|
||||
|
||||
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
||||
@@ -187,9 +194,10 @@ Each shader package lives under:
|
||||
shaders/<id>/
|
||||
shader.json
|
||||
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
|
||||
|
||||
@@ -207,7 +215,37 @@ Only `runtime/templates/` and `runtime/README.md` are tracked.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
- Continue source cleanup/refactoring. Pass 2 done
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2
|
||||
- move to MSDF, typography rasterisation
|
||||
- better shader search UI, pass 1
|
||||
- More comprehensive greenscreen shader
|
||||
- linear compositing?
|
||||
- compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store
|
||||
- allow shaders to read other shaders data store based on name? or output over OSC
|
||||
- Mipmappong for shader declared textures
|
||||
- Multipass for shaders at request
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
`shader.json` is the runtime-facing description of the shader.
|
||||
@@ -73,6 +98,7 @@ Optional fields:
|
||||
- `category`: UI grouping label.
|
||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||
- `textures`: texture assets to load and expose as samplers.
|
||||
- `fonts`: packaged font assets for live text parameters.
|
||||
- `temporal`: history-buffer requirements.
|
||||
|
||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
@@ -80,6 +106,7 @@ Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
- `entryPoint`
|
||||
- parameter `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.
|
||||
|
||||
@@ -120,6 +147,9 @@ struct ShaderContext
|
||||
float2 inputResolution;
|
||||
float2 outputResolution;
|
||||
float time;
|
||||
float utcTimeSeconds;
|
||||
float utcOffsetSeconds;
|
||||
float startupRandom;
|
||||
float frameCount;
|
||||
float mixAmount;
|
||||
float bypass;
|
||||
@@ -135,12 +165,22 @@ Fields:
|
||||
- `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.
|
||||
- `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.
|
||||
- `mixAmount`: runtime mix amount.
|
||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||
- `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 host prefers 10-bit DeckLink YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
|
||||
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than the packed DeckLink byte formats.
|
||||
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
The wrapper provides:
|
||||
@@ -180,6 +220,8 @@ Supported types:
|
||||
| `color` | `float4` | `[r, g, b, a]` |
|
||||
| `bool` | `bool` | `true` or `false` |
|
||||
| `enum` | `int` | selected option index |
|
||||
| `text` | generated texture/helper | string |
|
||||
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
|
||||
|
||||
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:
|
||||
|
||||
- Float values are clamped to `min`/`max` if provided.
|
||||
- `vec2` must have exactly 2 numbers.
|
||||
- `color` must have exactly 4 numbers.
|
||||
- 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.
|
||||
|
||||
## Texture Assets
|
||||
@@ -323,6 +416,31 @@ return float4(logo.rgb * alpha, alpha);
|
||||
|
||||
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 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.
|
||||
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
||||
- 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:
|
||||
|
||||
@@ -384,6 +515,28 @@ float2 pixel = 1.0 / max(context.outputResolution, float2(1.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
|
||||
|
||||
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.
|
||||
- Remember enum globals are integer indexes, not strings.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -414,6 +568,7 @@ Before committing a new shader package:
|
||||
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
||||
- `shader.slang` implements the configured entry point.
|
||||
- Texture files referenced by `textures` exist.
|
||||
- Font files referenced by `fonts` exist.
|
||||
- Enum defaults are present in their `options`.
|
||||
- Temporal shaders handle short or empty history gracefully.
|
||||
- The app can reload and compile the shader without errors.
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
#include "resource.h"
|
||||
#include "OpenGLComposite.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <shellapi.h>
|
||||
#include <string>
|
||||
|
||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
||||
#endif
|
||||
@@ -65,6 +69,169 @@
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
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)
|
||||
{
|
||||
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 OpenGLComposite* pOpenGLComposite = NULL;
|
||||
static bool sInteractiveResize = false;
|
||||
static StatusStripControls sStatusStrip;
|
||||
|
||||
switch (message)
|
||||
{
|
||||
@@ -251,7 +419,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
if (pOpenGLComposite->Start())
|
||||
{
|
||||
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
||||
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
|
||||
@@ -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:
|
||||
try
|
||||
{
|
||||
@@ -300,7 +495,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
RECT 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);
|
||||
break;
|
||||
@@ -308,6 +507,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
case WM_SIZE:
|
||||
try
|
||||
{
|
||||
if (StatusStripCreated(sStatusStrip))
|
||||
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||
if (pOpenGLComposite)
|
||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||
}
|
||||
@@ -332,6 +533,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
pOpenGLComposite->paintGL();
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
RaiseStatusControls(sStatusStrip);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
@@ -356,6 +558,28 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
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:
|
||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
@@ -99,15 +99,11 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Midl>
|
||||
@@ -115,7 +111,7 @@
|
||||
</Midl>
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
@@ -125,21 +121,17 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
@@ -149,18 +141,13 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
</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 Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Midl>
|
||||
@@ -169,7 +156,7 @@
|
||||
<ClCompile>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
@@ -179,39 +166,60 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="GLExtensions.cpp" />
|
||||
<ClCompile Include="gl\GLExtensions.cpp" />
|
||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
|
||||
<ClCompile Include="OpenGLComposite.cpp" />
|
||||
<ClCompile Include="gl\OpenGLComposite.cpp" />
|
||||
<ClCompile Include="gl\OpenGLRenderPass.cpp" />
|
||||
<ClCompile Include="gl\OpenGLRenderer.cpp" />
|
||||
<ClCompile Include="gl\OpenGLShaderPrograms.cpp" />
|
||||
<ClCompile Include="gl\PngScreenshotWriter.cpp" />
|
||||
<ClCompile Include="gl\ShaderBuildQueue.cpp" />
|
||||
<ClCompile Include="gl\TemporalHistoryBuffers.cpp" />
|
||||
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp" />
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="VideoFrameTransfer.cpp" />
|
||||
<ClCompile Include="DeckLinkAPI_i.c" />
|
||||
<ClCompile Include="control\RuntimeServices.cpp" />
|
||||
<ClCompile Include="decklink\DeckLinkSession.cpp" />
|
||||
<ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp" />
|
||||
<ClCompile Include="runtime\RuntimeClock.cpp" />
|
||||
<ClCompile Include="videoio\VideoIOFormat.cpp" />
|
||||
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="GLExtensions.h" />
|
||||
<ClInclude Include="gl\GLExtensions.h" />
|
||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
|
||||
<ClInclude Include="OpenGLComposite.h" />
|
||||
<ClInclude Include="gl\OpenGLComposite.h" />
|
||||
<ClInclude Include="gl\OpenGLRenderPass.h" />
|
||||
<ClInclude Include="gl\OpenGLRenderer.h" />
|
||||
<ClInclude Include="gl\OpenGLShaderPrograms.h" />
|
||||
<ClInclude Include="gl\PngScreenshotWriter.h" />
|
||||
<ClInclude Include="gl\ShaderBuildQueue.h" />
|
||||
<ClInclude Include="gl\TemporalHistoryBuffers.h" />
|
||||
<ClInclude Include="gl\OpenGLVideoIOBridge.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="VideoFrameTransfer.h" />
|
||||
<ClInclude Include="control\RuntimeServices.h" />
|
||||
<ClInclude Include="decklink\DeckLinkSession.h" />
|
||||
<ClInclude Include="decklink\DeckLinkVideoIOFormat.h" />
|
||||
<ClInclude Include="runtime\RuntimeClock.h" />
|
||||
<ClInclude Include="videoio\VideoIOFormat.h" />
|
||||
<ClInclude Include="videoio\VideoIOTypes.h" />
|
||||
<ClInclude Include="videoio\VideoPlayoutScheduler.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||
|
||||
@@ -18,33 +18,90 @@
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="GLExtensions.cpp">
|
||||
<ClCompile Include="gl\GLExtensions.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="OpenGLComposite.cpp">
|
||||
<ClCompile Include="gl\OpenGLComposite.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\OpenGLRenderPass.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\OpenGLRenderer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\OpenGLShaderPrograms.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\PngScreenshotWriter.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\ShaderBuildQueue.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\TemporalHistoryBuffers.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="VideoFrameTransfer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DeckLinkAPI_i.c">
|
||||
<Filter>DeckLink API</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="control\RuntimeServices.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decklink\DeckLinkSession.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="runtime\RuntimeClock.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="videoio\VideoIOFormat.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="GLExtensions.h">
|
||||
<ClInclude Include="gl\GLExtensions.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="OpenGLComposite.h">
|
||||
<ClInclude Include="gl\OpenGLComposite.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\OpenGLRenderPass.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\OpenGLRenderer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\OpenGLShaderPrograms.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\PngScreenshotWriter.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\ShaderBuildQueue.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\TemporalHistoryBuffers.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\OpenGLVideoIOBridge.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
@@ -56,7 +113,25 @@
|
||||
<ClInclude Include="targetver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="VideoFrameTransfer.h">
|
||||
<ClInclude Include="control\RuntimeServices.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decklink\DeckLinkSession.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="decklink\DeckLinkVideoIOFormat.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="runtime\RuntimeClock.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoIOFormat.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoIOTypes.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="videoio\VideoPlayoutScheduler.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
|
||||
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
|
||||
{
|
||||
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
||||
|
||||
bool InitializeWinsock(std::string& error)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
@@ -165,9 +167,18 @@ void ControlServer::BroadcastState()
|
||||
|
||||
void ControlServer::ServerLoop()
|
||||
{
|
||||
DWORD lastStateBroadcastMs = GetTickCount();
|
||||
while (mRunning)
|
||||
{
|
||||
TryAcceptClient();
|
||||
|
||||
const DWORD nowMs = GetTickCount();
|
||||
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
||||
{
|
||||
BroadcastState();
|
||||
lastStateBroadcastMs = nowMs;
|
||||
}
|
||||
|
||||
Sleep(25);
|
||||
}
|
||||
}
|
||||
@@ -422,6 +433,11 @@ bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& ro
|
||||
{
|
||||
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&)> loadStackPreset;
|
||||
std::function<bool(std::string&)> reloadShader;
|
||||
std::function<bool(std::string&)> requestScreenshot;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -60,13 +60,19 @@
|
||||
#define GL_DYNAMIC_DRAW 0x88E8
|
||||
#define GL_UNIFORM_BUFFER 0x8A11
|
||||
#define GL_RGBA8 0x8058
|
||||
#define GL_RGBA16F 0x881A
|
||||
#define GL_TEXTURE0 0x84C0
|
||||
#define GL_ACTIVE_TEXTURE 0x84E0
|
||||
#define GL_ARRAY_BUFFER 0x8892
|
||||
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
||||
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
||||
#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
|
||||
#define GL_FRAGMENT_SHADER 0x8B30
|
||||
#define GL_VERTEX_SHADER 0x8B31
|
||||
#define GL_COMPILE_STATUS 0x8B81
|
||||
#define GL_LINK_STATUS 0x8B82
|
||||
#define GL_INVALID_INDEX 0xFFFFFFFFu
|
||||
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
|
||||
#define GL_RENDERBUFFER_EXT 0x8D41
|
||||
#define GL_FRAMEBUFFER_EXT 0x8D40
|
||||
#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;
|
||||
57
apps/LoopThroughWithOpenGLCompositing/gl/GlScopedObjects.h
Normal file
57
apps/LoopThroughWithOpenGLCompositing/gl/GlScopedObjects.h
Normal file
@@ -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;
|
||||
};
|
||||
141
apps/LoopThroughWithOpenGLCompositing/gl/GlShaderSources.cpp
Normal file
141
apps/LoopThroughWithOpenGLCompositing/gl/GlShaderSources.cpp
Normal file
@@ -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;
|
||||
102
apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp
Normal file
102
apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp
Normal file
@@ -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;
|
||||
};
|
||||
474
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
474
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkSession.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "GlRenderConstants.h"
|
||||
#include "OpenGLRenderPass.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>();
|
||||
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||
*mVideoIO,
|
||||
*mRenderer,
|
||||
*mRuntimeHost,
|
||||
pMutex,
|
||||
hGLDC,
|
||||
hGLRC,
|
||||
[this]() { renderEffect(); },
|
||||
[this]() { ProcessScreenshotRequest(); },
|
||||
[this]() { paintGL(); });
|
||||
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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
|
||||
|
||||
101
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
101
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#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 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<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;
|
||||
}
|
||||
169
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp
Normal file
169
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.cpp
Normal file
@@ -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);
|
||||
}
|
||||
47
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.h
Normal file
47
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderPass.h
Normal file
@@ -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;
|
||||
};
|
||||
330
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp
Normal file
330
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.cpp
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
109
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h
Normal file
109
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLRenderer.h
Normal file
@@ -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,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;
|
||||
};
|
||||
179
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.cpp
Normal file
179
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <gl/gl.h>
|
||||
|
||||
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint) :
|
||||
mVideoIO(videoIO),
|
||||
mRenderer(renderer),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mMutex(mutex),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc),
|
||||
mRenderEffect(renderEffect),
|
||||
mOutputReady(outputReady),
|
||||
mPaint(paint)
|
||||
{
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Draw the effect output to the off-screen framebuffer.
|
||||
const auto renderStartTime = std::chrono::steady_clock::now();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
mRenderEffect();
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
if (mOutputReady)
|
||||
mOutputReady();
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(mRenderer.OutputPackProgram());
|
||||
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
|
||||
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
|
||||
if (outputResolutionLocation >= 0)
|
||||
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
||||
if (activeWordsLocation >= 0)
|
||||
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
glFlush();
|
||||
const auto renderEndTime = std::chrono::steady_clock::now();
|
||||
const double frameBudgetMilliseconds = state.frameBudgetMilliseconds;
|
||||
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
||||
mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
||||
mRuntimeHost.TryAdvanceFrame();
|
||||
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes);
|
||||
}
|
||||
mPaint();
|
||||
|
||||
mVideoIO.EndOutputFrame(outputFrame);
|
||||
|
||||
mVideoIO.AccountForCompletionResult(completion.result);
|
||||
|
||||
// Schedule the next frame for playout
|
||||
mVideoIO.ScheduleOutputFrame(outputFrame);
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
class OpenGLRenderer;
|
||||
class RuntimeHost;
|
||||
|
||||
struct RenderPipelineFrameContext
|
||||
{
|
||||
VideoIOState videoState;
|
||||
VideoIOCompletion completion;
|
||||
};
|
||||
|
||||
class OpenGLVideoIOBridge
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using OutputReadyCallback = std::function<void()>;
|
||||
using PaintCallback = std::function<void()>;
|
||||
|
||||
OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint);
|
||||
|
||||
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
||||
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
||||
|
||||
private:
|
||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||
|
||||
VideoIODevice& mVideoIO;
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
CRITICAL_SECTION& mMutex;
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
RenderEffectCallback mRenderEffect;
|
||||
OutputReadyCallback mOutputReady;
|
||||
PaintCallback mPaint;
|
||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||
double mCompletionIntervalMilliseconds = 0.0;
|
||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||
uint64_t mLateFrameCount = 0;
|
||||
uint64_t mDroppedFrameCount = 0;
|
||||
uint64_t mFlushedFrameCount = 0;
|
||||
};
|
||||
137
apps/LoopThroughWithOpenGLCompositing/gl/PngScreenshotWriter.cpp
Normal file
137
apps/LoopThroughWithOpenGLCompositing/gl/PngScreenshotWriter.cpp
Normal file
@@ -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);
|
||||
|
||||
134
apps/LoopThroughWithOpenGLCompositing/gl/ShaderBuildQueue.cpp
Normal file
134
apps/LoopThroughWithOpenGLCompositing/gl/ShaderBuildQueue.cpp
Normal file
@@ -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;
|
||||
}
|
||||
57
apps/LoopThroughWithOpenGLCompositing/gl/ShaderBuildQueue.h
Normal file
57
apps/LoopThroughWithOpenGLCompositing/gl/ShaderBuildQueue.h
Normal file
@@ -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;
|
||||
};
|
||||
48
apps/LoopThroughWithOpenGLCompositing/gl/Std140Buffer.h
Normal file
48
apps/LoopThroughWithOpenGLCompositing/gl/Std140Buffer.h
Normal file
@@ -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,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;
|
||||
};
|
||||
243
apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp
Normal file
243
apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp
Normal file
@@ -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;
|
||||
}
|
||||
10
apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h
Normal file
10
apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h
Normal file
@@ -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);
|
||||
222
apps/LoopThroughWithOpenGLCompositing/gl/TextureAssetLoader.cpp
Normal file
222
apps/LoopThroughWithOpenGLCompositing/gl/TextureAssetLoader.cpp
Normal file
@@ -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 "RuntimeHost.h"
|
||||
|
||||
#include "RuntimeClock.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "ShaderCompiler.h"
|
||||
#include "ShaderPackageRegistry.h"
|
||||
@@ -7,6 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
@@ -53,6 +56,13 @@ bool MatchesControlKey(const std::string& candidate, const std::string& 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)
|
||||
{
|
||||
const std::string prefix = "layer-";
|
||||
@@ -133,6 +143,8 @@ std::string ShaderParameterTypeToString(ShaderParameterType type)
|
||||
case ShaderParameterType::Color: return "color";
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "enum";
|
||||
case ShaderParameterType::Text: return "text";
|
||||
case ShaderParameterType::Trigger: return "trigger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
@@ -179,6 +191,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
||||
type = ShaderParameterType::Enum;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "text")
|
||||
{
|
||||
type = ShaderParameterType::Text;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "trigger")
|
||||
{
|
||||
type = ShaderParameterType::Trigger;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -200,6 +222,24 @@ bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::
|
||||
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)
|
||||
{
|
||||
return manifestPath.string();
|
||||
@@ -379,6 +419,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
||||
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)
|
||||
{
|
||||
const JsonValue* temporalValue = nullptr;
|
||||
@@ -461,6 +544,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -543,6 +637,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
||||
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)
|
||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||
|
||||
@@ -578,6 +696,13 @@ RuntimeHost::RuntimeHost()
|
||||
mFrameBudgetMilliseconds(0.0),
|
||||
mRenderMilliseconds(0.0),
|
||||
mSmoothedRenderMilliseconds(0.0),
|
||||
mCompletionIntervalMilliseconds(0.0),
|
||||
mSmoothedCompletionIntervalMilliseconds(0.0),
|
||||
mMaxCompletionIntervalMilliseconds(0.0),
|
||||
mStartupRandom(GenerateStartupRandom()),
|
||||
mLateFrameCount(0),
|
||||
mDroppedFrameCount(0),
|
||||
mFlushedFrameCount(0),
|
||||
mServerPort(8080),
|
||||
mAutoReloadEnabled(true),
|
||||
mStartTime(std::chrono::steady_clock::now()),
|
||||
@@ -693,7 +818,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
||||
}
|
||||
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
|
||||
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;
|
||||
break;
|
||||
@@ -714,7 +840,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
||||
if (previous->second.first != active->second.shaderWriteTime ||
|
||||
previous->second.second != active->second.manifestWriteTime ||
|
||||
(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;
|
||||
}
|
||||
@@ -899,6 +1026,15 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
||||
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;
|
||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||
return false;
|
||||
@@ -945,6 +1081,15 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
||||
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;
|
||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||
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)
|
||||
{
|
||||
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;
|
||||
mSignalWidth = width;
|
||||
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,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable,
|
||||
externalKeyingRequested, externalKeyingActive, statusMessage);
|
||||
}
|
||||
|
||||
void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mDeckLinkOutputStatus.backendName = backendName;
|
||||
mDeckLinkOutputStatus.modelName = modelName;
|
||||
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
|
||||
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
|
||||
@@ -1067,6 +1235,21 @@ void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool sup
|
||||
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||
{
|
||||
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);
|
||||
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||
if (mSmoothedRenderMilliseconds <= 0.0)
|
||||
@@ -1075,12 +1258,48 @@ void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double ren
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
bool RuntimeHost::TryAdvanceFrame()
|
||||
{
|
||||
++mFrameCounter;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
||||
{
|
||||
try
|
||||
@@ -1123,7 +1342,39 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
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)
|
||||
{
|
||||
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||
@@ -1133,8 +1384,6 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
||||
RuntimeRenderState state;
|
||||
state.layerId = layer.id;
|
||||
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.bypass = layer.bypass ? 1.0 : 0.0;
|
||||
state.inputWidth = mSignalWidth;
|
||||
@@ -1143,6 +1392,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
||||
state.outputHeight = outputHeight;
|
||||
state.parameterDefinitions = shaderIt->second.parameters;
|
||||
state.textureAssets = shaderIt->second.textureAssets;
|
||||
state.fontAssets = shaderIt->second.fontAssets;
|
||||
state.isTemporal = shaderIt->second.temporal.enabled;
|
||||
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
||||
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
||||
@@ -1160,7 +1410,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
||||
states.push_back(state);
|
||||
}
|
||||
|
||||
return states;
|
||||
RefreshDynamicRenderStateFields(states);
|
||||
}
|
||||
|
||||
std::string RuntimeHost::BuildStateJson() const
|
||||
@@ -1401,12 +1651,14 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
||||
{
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
std::vector<std::string> packageOrder;
|
||||
std::vector<ShaderPackageStatus> packageStatuses;
|
||||
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
||||
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
|
||||
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||
return false;
|
||||
|
||||
mPackagesById.swap(packagesById);
|
||||
mPackageOrder.swap(packageOrder);
|
||||
mPackageStatuses.swap(packageStatuses);
|
||||
|
||||
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);
|
||||
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||
}
|
||||
@@ -1465,8 +1718,68 @@ void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, co
|
||||
{
|
||||
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);
|
||||
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));
|
||||
root.set("decklink", deckLink);
|
||||
|
||||
JsonValue videoIO = JsonValue::MakeObject();
|
||||
videoIO.set("backend", JsonValue(mDeckLinkOutputStatus.backendName));
|
||||
videoIO.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
|
||||
videoIO.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));
|
||||
videoIO.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying));
|
||||
videoIO.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable));
|
||||
videoIO.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested));
|
||||
videoIO.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive));
|
||||
videoIO.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||
root.set("videoIO", videoIO);
|
||||
|
||||
JsonValue performance = JsonValue::MakeObject();
|
||||
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
||||
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
||||
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
||||
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);
|
||||
|
||||
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();
|
||||
shader.set("id", JsonValue(shaderIt->second.id));
|
||||
shader.set("name", JsonValue(shaderIt->second.displayName));
|
||||
shader.set("description", JsonValue(shaderIt->second.description));
|
||||
shader.set("category", JsonValue(shaderIt->second.category));
|
||||
if (shaderIt->second.temporal.enabled)
|
||||
shader.set("id", JsonValue(status.id));
|
||||
shader.set("name", JsonValue(status.displayName));
|
||||
shader.set("description", JsonValue(status.description));
|
||||
shader.set("category", JsonValue(status.category));
|
||||
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();
|
||||
temporal.set("enabled", JsonValue(true));
|
||||
@@ -1657,6 +1988,7 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
||||
parameter.set("id", JsonValue(definition.id));
|
||||
parameter.set("label", JsonValue(definition.label));
|
||||
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
||||
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
|
||||
|
||||
if (!definition.minNumbers.empty())
|
||||
{
|
||||
@@ -1691,6 +2023,12 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
||||
}
|
||||
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);
|
||||
auto valueIt = layer.parameterValues.find(definition.id);
|
||||
@@ -1829,6 +2167,10 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
|
||||
return JsonValue(value.booleanValue);
|
||||
case ShaderParameterType::Enum:
|
||||
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:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Vec2:
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
@@ -35,13 +36,24 @@ public:
|
||||
|
||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||
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,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
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();
|
||||
bool TryAdvanceFrame();
|
||||
|
||||
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
std::string BuildStateJson() const;
|
||||
|
||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||
@@ -76,6 +88,7 @@ private:
|
||||
|
||||
struct DeckLinkOutputStatus
|
||||
{
|
||||
std::string backendName = "decklink";
|
||||
std::string modelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
@@ -109,6 +122,7 @@ private:
|
||||
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 ResolvePaths(std::string& error);
|
||||
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
JsonValue BuildStateValue() const;
|
||||
JsonValue SerializeLayerStackLocked() const;
|
||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||
@@ -120,6 +134,10 @@ private:
|
||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
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:
|
||||
mutable std::mutex mMutex;
|
||||
@@ -138,6 +156,7 @@ private:
|
||||
std::filesystem::path mPatchedGlslPath;
|
||||
std::map<std::string, ShaderPackage> mPackagesById;
|
||||
std::vector<std::string> mPackageOrder;
|
||||
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||
bool mReloadRequested;
|
||||
bool mCompileSucceeded;
|
||||
std::string mCompileMessage;
|
||||
@@ -148,11 +167,18 @@ private:
|
||||
double mFrameBudgetMilliseconds;
|
||||
double mRenderMilliseconds;
|
||||
double mSmoothedRenderMilliseconds;
|
||||
double mCompletionIntervalMilliseconds;
|
||||
double mSmoothedCompletionIntervalMilliseconds;
|
||||
double mMaxCompletionIntervalMilliseconds;
|
||||
double mStartupRandom;
|
||||
uint64_t mLateFrameCount;
|
||||
uint64_t mDroppedFrameCount;
|
||||
uint64_t mFlushedFrameCount;
|
||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||
unsigned short mServerPort;
|
||||
bool mAutoReloadEnabled;
|
||||
std::chrono::steady_clock::time_point mStartTime;
|
||||
std::chrono::steady_clock::time_point mLastScanTime;
|
||||
uint64_t mFrameCounter;
|
||||
std::atomic<uint64_t> mFrameCounter;
|
||||
uint64_t mNextLayerId;
|
||||
};
|
||||
@@ -36,6 +36,21 @@ std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
}
|
||||
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)
|
||||
@@ -82,6 +97,12 @@ ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition&
|
||||
case ShaderParameterType::Enum:
|
||||
value.enumValue = definition.defaultEnumValue;
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
value.numberValues = { 0.0, -1000000.0 };
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -164,6 +185,22 @@ bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definit
|
||||
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
|
||||
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;
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "NativeHandles.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <cctype>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
@@ -30,15 +31,36 @@ std::string SlangCBufferTypeForParameter(ShaderParameterType type)
|
||||
case ShaderParameterType::Color: return "float4";
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "int";
|
||||
case ShaderParameterType::Text: return "";
|
||||
case ShaderParameterType::Trigger: return "int";
|
||||
}
|
||||
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::ostringstream source;
|
||||
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";
|
||||
}
|
||||
return source.str();
|
||||
}
|
||||
|
||||
@@ -60,6 +82,44 @@ std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset
|
||||
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::ostringstream source;
|
||||
@@ -115,11 +175,14 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
||||
return false;
|
||||
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", mMaxTemporalHistoryFrames));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", mMaxTemporalHistoryFrames));
|
||||
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
||||
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, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", mMaxTemporalHistoryFrames));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", mMaxTemporalHistoryFrames));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
||||
return true;
|
||||
@@ -67,6 +67,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
||||
type = ShaderParameterType::Enum;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "text")
|
||||
{
|
||||
type = ShaderParameterType::Text;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "trigger")
|
||||
{
|
||||
type = ShaderParameterType::Trigger;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -283,6 +293,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
||||
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)
|
||||
{
|
||||
const JsonValue* temporalValue = nullptr;
|
||||
@@ -365,6 +418,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -447,6 +511,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
||||
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)
|
||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||
|
||||
@@ -471,6 +559,36 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
|
||||
|
||||
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)
|
||||
@@ -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();
|
||||
packageOrder.clear();
|
||||
packageStatuses.clear();
|
||||
|
||||
if (!std::filesystem::exists(shaderRoot))
|
||||
{
|
||||
@@ -500,19 +624,27 @@ bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::m
|
||||
|
||||
ShaderPackage shaderPackage;
|
||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
||||
return false;
|
||||
{
|
||||
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
|
||||
error.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
||||
{
|
||||
error = "Duplicate shader id found: " + shaderPackage.id;
|
||||
return false;
|
||||
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
|
||||
continue;
|
||||
}
|
||||
|
||||
packageOrder.push_back(shaderPackage.id);
|
||||
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
|
||||
packagesById[shaderPackage.id] = shaderPackage;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -544,6 +676,7 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||
}
|
||||
@@ -12,7 +12,12 @@ class ShaderPackageRegistry
|
||||
public:
|
||||
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;
|
||||
|
||||
private:
|
||||
@@ -11,7 +11,9 @@ enum class ShaderParameterType
|
||||
Vec2,
|
||||
Color,
|
||||
Boolean,
|
||||
Enum
|
||||
Enum,
|
||||
Text,
|
||||
Trigger
|
||||
};
|
||||
|
||||
struct ShaderParameterOption
|
||||
@@ -31,6 +33,9 @@ struct ShaderParameterDefinition
|
||||
std::vector<double> stepNumbers;
|
||||
bool defaultBoolean = false;
|
||||
std::string defaultEnumValue;
|
||||
std::string defaultTextValue;
|
||||
std::string fontId;
|
||||
unsigned maxLength = 64;
|
||||
std::vector<ShaderParameterOption> enumOptions;
|
||||
};
|
||||
|
||||
@@ -39,6 +44,7 @@ struct ShaderParameterValue
|
||||
std::vector<double> numberValues;
|
||||
bool booleanValue = false;
|
||||
std::string enumValue;
|
||||
std::string textValue;
|
||||
};
|
||||
|
||||
enum class TemporalHistorySource
|
||||
@@ -63,6 +69,13 @@ struct ShaderTextureAsset
|
||||
std::filesystem::file_time_type writeTime;
|
||||
};
|
||||
|
||||
struct ShaderFontAsset
|
||||
{
|
||||
std::string id;
|
||||
std::filesystem::path path;
|
||||
std::filesystem::file_time_type writeTime;
|
||||
};
|
||||
|
||||
struct ShaderPackage
|
||||
{
|
||||
std::string id;
|
||||
@@ -75,11 +88,22 @@ struct ShaderPackage
|
||||
std::filesystem::path manifestPath;
|
||||
std::vector<ShaderParameterDefinition> parameters;
|
||||
std::vector<ShaderTextureAsset> textureAssets;
|
||||
std::vector<ShaderFontAsset> fontAssets;
|
||||
TemporalSettings temporal;
|
||||
std::filesystem::file_time_type shaderWriteTime;
|
||||
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
|
||||
{
|
||||
std::string layerId;
|
||||
@@ -87,7 +111,11 @@ struct RuntimeRenderState
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
std::vector<ShaderTextureAsset> textureAssets;
|
||||
std::vector<ShaderFontAsset> fontAssets;
|
||||
double timeSeconds = 0.0;
|
||||
double utcTimeSeconds = 0.0;
|
||||
double utcOffsetSeconds = 0.0;
|
||||
double startupRandom = 0.0;
|
||||
double frameCount = 0.0;
|
||||
double mixAmount = 1.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;
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
For simple scalar controls, set the widget address and target directly:
|
||||
|
||||
@@ -214,6 +214,24 @@ paths:
|
||||
$ref: "#/components/responses/ActionOk"
|
||||
"400":
|
||||
$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:
|
||||
responses:
|
||||
ActionOk:
|
||||
@@ -474,7 +492,7 @@ components:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [float, vec2, color, bool, enum]
|
||||
enum: [float, vec2, color, bool, enum, text, trigger]
|
||||
min:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -11,6 +11,9 @@ struct ShaderContext
|
||||
float2 inputResolution;
|
||||
float2 outputResolution;
|
||||
float time;
|
||||
float utcTimeSeconds;
|
||||
float utcOffsetSeconds;
|
||||
float startupRandom;
|
||||
float frameCount;
|
||||
float mixAmount;
|
||||
float bypass;
|
||||
@@ -23,6 +26,9 @@ cbuffer GlobalParams
|
||||
float gTime;
|
||||
float2 gInputResolution;
|
||||
float2 gOutputResolution;
|
||||
float gUtcTimeSeconds;
|
||||
float gUtcOffsetSeconds;
|
||||
float gStartupRandom;
|
||||
float gFrameCount;
|
||||
float gMixAmount;
|
||||
float gBypass;
|
||||
@@ -32,6 +38,7 @@ cbuffer GlobalParams
|
||||
|
||||
Sampler2D<float4> gVideoInput;
|
||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
||||
{{TEXT_SAMPLERS}}
|
||||
float4 sampleVideo(float2 tc)
|
||||
{
|
||||
return gVideoInput.Sample(tc);
|
||||
@@ -67,6 +74,7 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
||||
}
|
||||
}
|
||||
|
||||
{{TEXT_HELPERS}}
|
||||
#include "{{USER_SHADER_INCLUDE}}"
|
||||
|
||||
[shader("fragment")]
|
||||
@@ -78,6 +86,9 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
||||
context.inputResolution = gInputResolution;
|
||||
context.outputResolution = gOutputResolution;
|
||||
context.time = gTime;
|
||||
context.utcTimeSeconds = gUtcTimeSeconds;
|
||||
context.utcOffsetSeconds = gUtcOffsetSeconds;
|
||||
context.startupRandom = gStartupRandom;
|
||||
context.frameCount = gFrameCount;
|
||||
context.mixAmount = gMixAmount;
|
||||
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",
|
||||
"name": "Black and White",
|
||||
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
|
||||
"category": "Built-in",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"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",
|
||||
"name": "Composition Guides",
|
||||
"description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.",
|
||||
"category": "Utility",
|
||||
"category": "Scopes & Guides",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -7,12 +7,14 @@ float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
||||
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 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;
|
||||
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);
|
||||
|
||||
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "dvd-bounce",
|
||||
"name": "DVD Bounce",
|
||||
"description": "A transparent bouncing DVD logo sprite that changes color on each screen hit.",
|
||||
"category": "Built-in",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"textures": [
|
||||
{
|
||||
|
||||
@@ -29,7 +29,8 @@ float4 shadeVideo(ShaderContext context)
|
||||
float2 velocityPx = float2(
|
||||
max(20.0, bounceSpeed * minDimension * 1.00),
|
||||
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(
|
||||
pingPong(motionPx.x, travelPx.x),
|
||||
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 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 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
||||
|
||||
|
||||
84
shaders/ether/shader.json
Normal file
84
shaders/ether/shader.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"id": "ether",
|
||||
"name": "Ether",
|
||||
"description": "Raymarched ether field. Original by nimitz 2014 (twitter: @stormoid), adapted from https://www.shadertoy.com/view/MsjSW3.",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "depth",
|
||||
"label": "Depth",
|
||||
"type": "float",
|
||||
"default": 2.5,
|
||||
"min": 0.2,
|
||||
"max": 8.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "density",
|
||||
"label": "Density",
|
||||
"type": "float",
|
||||
"default": 0.7,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"label": "Contrast",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.25,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.9, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
},
|
||||
{
|
||||
"id": "baseColor",
|
||||
"label": "Base Color",
|
||||
"type": "color",
|
||||
"default": [0.1, 0.3, 0.4, 1.0]
|
||||
},
|
||||
{
|
||||
"id": "energyColor",
|
||||
"label": "Energy Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 0.5, 0.6, 1.0]
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
41
shaders/ether/shader.slang
Normal file
41
shaders/ether/shader.slang
Normal file
@@ -0,0 +1,41 @@
|
||||
float2x2 rotation2(float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return float2x2(c, -s, s, c);
|
||||
}
|
||||
|
||||
float etherMap(float3 p, float time)
|
||||
{
|
||||
p.xz = mul(rotation2(time * 0.4), p.xz);
|
||||
p.xy = mul(rotation2(time * 0.3), p.xy);
|
||||
|
||||
float3 q = p * 2.0 + time;
|
||||
float wave = sin(q.x + sin(q.z + sin(q.y))) * 0.5;
|
||||
return length(p + float3(sin(time * 0.7), sin(time * 0.7), sin(time * 0.7))) * log(length(p) + 1.0) + wave - 1.0;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 fragCoord = context.uv * resolution;
|
||||
float2 p = fragCoord / resolution.y - offset;
|
||||
float seed = context.startupRandom;
|
||||
float time = context.time * speed + seed * 41.0;
|
||||
|
||||
float3 color = float3(0.0, 0.0, 0.0);
|
||||
float d = depth;
|
||||
|
||||
for (int i = 0; i <= 5; ++i)
|
||||
{
|
||||
float3 rayPosition = float3(seed * 0.7 - 0.35, 0.23 - seed * 0.46, 5.0) + normalize(float3(p, -1.0)) * d;
|
||||
float rz = etherMap(rayPosition, time);
|
||||
float f = clamp((rz - etherMap(rayPosition + float3(0.1, 0.1, 0.1), time)) * 0.5, -0.1, 1.0);
|
||||
float3 light = baseColor.rgb + energyColor.rgb * 5.0 * f;
|
||||
color = color * light + smoothstep(2.5, 0.0, rz) * density * light;
|
||||
d += min(rz, 1.0);
|
||||
}
|
||||
|
||||
color = pow(max(color * brightness, float3(0.0, 0.0, 0.0)), float3(1.0 / max(contrast, 0.001)));
|
||||
return saturate(lerp(float4(color, 1.0), context.sourceColor, sourceMix));
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "false-color",
|
||||
"name": "False Color",
|
||||
"description": "Maps luminance ranges to exposure-assist colors for camera and shader debugging.",
|
||||
"category": "Utility",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
99
shaders/fisheye-equirectangular-mirror/shader.json
Normal file
99
shaders/fisheye-equirectangular-mirror/shader.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"id": "fisheye-equirectangular-mirror",
|
||||
"name": "Fisheye Equirectangular Mirror",
|
||||
"description": "Unwraps a single width-filled 16:9 fisheye lens into a 360x180 equirectangular map by mirroring the rear hemisphere into the same fisheye source.",
|
||||
"category": "Projection",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "lensFovDegrees",
|
||||
"label": "Lens FOV",
|
||||
"type": "float",
|
||||
"default": 190.0,
|
||||
"min": 1.0,
|
||||
"max": 220.0,
|
||||
"step": 0.1
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Optical Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
},
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Fisheye Radius",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.8889],
|
||||
"min": [0.001, 0.001],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
},
|
||||
{
|
||||
"id": "yawDegrees",
|
||||
"label": "Yaw",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
},
|
||||
{
|
||||
"id": "pitchDegrees",
|
||||
"label": "Pitch",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
},
|
||||
{
|
||||
"id": "fisheyeModel",
|
||||
"label": "Fisheye Model",
|
||||
"type": "enum",
|
||||
"default": "equidistant",
|
||||
"options": [
|
||||
{ "value": "equidistant", "label": "Equidistant" },
|
||||
{ "value": "equisolid", "label": "Equisolid" },
|
||||
{ "value": "stereographic", "label": "Stereographic" },
|
||||
{ "value": "orthographic", "label": "Orthographic" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "edgeFill",
|
||||
"label": "Edge Fill",
|
||||
"type": "float",
|
||||
"default": 0.06,
|
||||
"min": 0.0,
|
||||
"max": 0.3,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "edgeBlur",
|
||||
"label": "Edge Blur",
|
||||
"type": "float",
|
||||
"default": 0.018,
|
||||
"min": 0.0,
|
||||
"max": 0.12,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
}
|
||||
]
|
||||
}
|
||||
127
shaders/fisheye-equirectangular-mirror/shader.slang
Normal file
127
shaders/fisheye-equirectangular-mirror/shader.slang
Normal file
@@ -0,0 +1,127 @@
|
||||
static const float PI = 3.14159265358979323846;
|
||||
static const float TWO_PI = 6.28318530717958647692;
|
||||
|
||||
float radiansFromDegrees(float degrees)
|
||||
{
|
||||
return degrees * (PI / 180.0);
|
||||
}
|
||||
|
||||
float3 rotateX(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(ray.x, c * ray.y - s * ray.z, s * ray.y + c * ray.z);
|
||||
}
|
||||
|
||||
float3 rotateY(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * ray.x + s * ray.z, ray.y, -s * ray.x + c * ray.z);
|
||||
}
|
||||
|
||||
float3 rotateZ(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * ray.x - s * ray.y, s * ray.x + c * ray.y, ray.z);
|
||||
}
|
||||
|
||||
float normalizedFisheyeRadius(float theta, float halfFov)
|
||||
{
|
||||
float safeHalfFov = max(halfFov, 0.0001);
|
||||
|
||||
if (fisheyeModel == 1)
|
||||
{
|
||||
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
||||
}
|
||||
else if (fisheyeModel == 2)
|
||||
{
|
||||
return tan(theta * 0.5) / max(tan(safeHalfFov * 0.5), 0.0001);
|
||||
}
|
||||
else if (fisheyeModel == 3)
|
||||
{
|
||||
return sin(theta) / max(sin(safeHalfFov), 0.0001);
|
||||
}
|
||||
|
||||
return theta / safeHalfFov;
|
||||
}
|
||||
|
||||
float3 equirectangularRay(float2 uv)
|
||||
{
|
||||
float longitude = (uv.x - 0.5) * TWO_PI;
|
||||
float latitude = (0.5 - uv.y) * PI;
|
||||
float latitudeCos = cos(latitude);
|
||||
|
||||
return normalize(float3(
|
||||
sin(longitude) * latitudeCos,
|
||||
sin(latitude),
|
||||
cos(longitude) * latitudeCos
|
||||
));
|
||||
}
|
||||
|
||||
float sourceUvOutsideDistance(float2 uv)
|
||||
{
|
||||
float2 lower = max(-uv, float2(0.0, 0.0));
|
||||
float2 upper = max(uv - 1.0, float2(0.0, 0.0));
|
||||
return max(max(lower.x, lower.y), max(upper.x, upper.y));
|
||||
}
|
||||
|
||||
float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
|
||||
{
|
||||
float outsideDistance = sourceUvOutsideDistance(sourceUv);
|
||||
if (outsideDistance <= 0.0)
|
||||
return sampleVideo(sourceUv);
|
||||
|
||||
float fillDistance = max(edgeFill, 0.0);
|
||||
if (outsideDistance > fillDistance)
|
||||
return outsideColor;
|
||||
|
||||
float2 clampedUv = saturate(sourceUv);
|
||||
float2 inward = clampedUv - sourceUv;
|
||||
float inwardLength = max(length(inward), 0.000001);
|
||||
inward /= inwardLength;
|
||||
|
||||
float blurDistance = max(edgeBlur, 0.0);
|
||||
float4 color = sampleVideo(clampedUv) * 0.32;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.75)) * 0.20;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.20)) * 0.14;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.75)) * 0.08;
|
||||
|
||||
float edgeFade = smoothstep(fillDistance * 0.78, fillDistance, outsideDistance);
|
||||
return lerp(color, outsideColor, edgeFade);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float3 ray = equirectangularRay(context.uv);
|
||||
|
||||
ray = rotateZ(ray, radiansFromDegrees(rollDegrees));
|
||||
ray = rotateX(ray, radiansFromDegrees(-pitchDegrees));
|
||||
ray = rotateY(ray, radiansFromDegrees(yawDegrees));
|
||||
|
||||
// Mirror the rear hemisphere into the front-facing fisheye image so one
|
||||
// circular lens source fills both halves of the equirectangular output.
|
||||
ray.z = abs(ray.z);
|
||||
ray = normalize(ray);
|
||||
|
||||
float halfFov = radiansFromDegrees(clamp(lensFovDegrees, 1.0, 220.0) * 0.5);
|
||||
float theta = acos(clamp(ray.z, -1.0, 1.0));
|
||||
if (theta > halfFov)
|
||||
return outsideColor;
|
||||
|
||||
float phi = atan2(ray.y, ray.x);
|
||||
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
||||
|
||||
float2 sourceUv = float2(
|
||||
center.x + cos(phi) * fisheyeRadius * radius.x,
|
||||
center.y - sin(phi) * fisheyeRadius * radius.y
|
||||
);
|
||||
|
||||
float2 guard = 0.5 / max(context.inputResolution, float2(1.0, 1.0));
|
||||
if (edgeFill <= 0.0 && (sourceUv.x < -guard.x || sourceUv.x > 1.0 + guard.x || sourceUv.y < -guard.y || sourceUv.y > 1.0 + guard.y))
|
||||
return outsideColor;
|
||||
|
||||
return sampleEdgeFilledVideo(sourceUv, context);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user