Compare commits
48 Commits
fd0ebb8d40
...
v0.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -12,15 +12,48 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
native-windows:
|
native-windows:
|
||||||
name: Native Windows Build And Tests
|
name: Native Windows Build And Tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Configure Debug
|
- name: Configure Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --preset vs2022-x64-debug
|
run: |
|
||||||
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = $env:SLANG_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFiles = @(
|
||||||
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
|
(Join-Path $slangRoot "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
|
if ($missingFiles.Count -gt 0) {
|
||||||
|
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
|
cmake --preset vs2022-x64-debug -DSLANG_ROOT="$slangRoot"
|
||||||
|
|
||||||
- name: Build Debug
|
- name: Build Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -32,7 +65,7 @@ jobs:
|
|||||||
|
|
||||||
ui-ubuntu:
|
ui-ubuntu:
|
||||||
name: React UI Build
|
name: React UI Build
|
||||||
runs-on: nubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -48,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
package-windows:
|
package-windows:
|
||||||
name: Windows Release Package
|
name: Windows Release Package
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
needs:
|
needs:
|
||||||
- native-windows
|
- native-windows
|
||||||
- ui-ubuntu
|
- ui-ubuntu
|
||||||
@@ -57,6 +90,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Visual Studio ATL
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||||
|
if ($atlHeaders.Count -eq 0) {
|
||||||
|
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||||
|
|
||||||
- name: Build UI
|
- name: Build UI
|
||||||
shell: powershell
|
shell: powershell
|
||||||
working-directory: ui
|
working-directory: ui
|
||||||
@@ -66,7 +109,30 @@ jobs:
|
|||||||
|
|
||||||
- name: Configure Release
|
- name: Configure Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --preset vs2022-x64-release
|
run: |
|
||||||
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = $env:SLANG_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
|
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredFiles = @(
|
||||||
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
|
(Join-Path $slangRoot "LICENSE")
|
||||||
|
)
|
||||||
|
|
||||||
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
|
if ($missingFiles.Count -gt 0) {
|
||||||
|
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
|
cmake --preset vs2022-x64-release -DSLANG_ROOT="$slangRoot"
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -81,7 +147,8 @@ jobs:
|
|||||||
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
||||||
|
|
||||||
- name: Upload Runtime Package
|
- name: Upload Runtime Package
|
||||||
uses: actions/upload-artifact@v4
|
# Gitea/GHES-compatible runners do not support the v4 artifact backend yet.
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: VideoShader-windows-release
|
name: VideoShader-windows-release
|
||||||
path: dist/VideoShader.zip
|
path: dist/VideoShader.zip
|
||||||
|
|||||||
184
CMakeLists.txt
184
CMakeLists.txt
@@ -7,17 +7,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
||||||
set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect" CACHE PATH "Path to the NVIDIA_GPUDirect sample directory from the Blackmagic DeckLink SDK")
|
|
||||||
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||||
|
|
||||||
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
||||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
|
|
||||||
message(FATAL_ERROR "NVIDIA GPUDirect library not found under ${GPUDIRECT_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(SLANG_RUNTIME_FILES
|
set(SLANG_RUNTIME_FILES
|
||||||
"${SLANG_ROOT}/bin/slangc.exe"
|
"${SLANG_ROOT}/bin/slangc.exe"
|
||||||
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
||||||
@@ -36,57 +31,102 @@ if(NOT EXISTS "${SLANG_LICENSE_FILE}")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(APP_SOURCES
|
set(APP_SOURCES
|
||||||
"${APP_DIR}/ControlServer.cpp"
|
|
||||||
"${APP_DIR}/ControlServer.h"
|
|
||||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
"${APP_DIR}/DeckLinkAPI_i.c"
|
||||||
"${APP_DIR}/GLExtensions.cpp"
|
"${APP_DIR}/control/ControlServer.cpp"
|
||||||
"${APP_DIR}/GLExtensions.h"
|
"${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/VideoIOFormat.cpp"
|
||||||
|
"${APP_DIR}/decklink/VideoIOFormat.h"
|
||||||
|
"${APP_DIR}/gl/GLExtensions.cpp"
|
||||||
|
"${APP_DIR}/gl/GLExtensions.h"
|
||||||
|
"${APP_DIR}/gl/GlobalParamsBuffer.cpp"
|
||||||
|
"${APP_DIR}/gl/GlobalParamsBuffer.h"
|
||||||
|
"${APP_DIR}/gl/GlRenderConstants.h"
|
||||||
|
"${APP_DIR}/gl/GlScopedObjects.h"
|
||||||
|
"${APP_DIR}/gl/GlShaderSources.cpp"
|
||||||
|
"${APP_DIR}/gl/GlShaderSources.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLComposite.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLDeckLinkBridge.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLDeckLinkBridge.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLRenderPass.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLRenderPass.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLRenderer.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLRenderer.h"
|
||||||
|
"${APP_DIR}/gl/OpenGLShaderPrograms.cpp"
|
||||||
|
"${APP_DIR}/gl/OpenGLShaderPrograms.h"
|
||||||
|
"${APP_DIR}/gl/PngScreenshotWriter.cpp"
|
||||||
|
"${APP_DIR}/gl/PngScreenshotWriter.h"
|
||||||
|
"${APP_DIR}/gl/ShaderProgramCompiler.cpp"
|
||||||
|
"${APP_DIR}/gl/ShaderProgramCompiler.h"
|
||||||
|
"${APP_DIR}/gl/ShaderBuildQueue.cpp"
|
||||||
|
"${APP_DIR}/gl/ShaderBuildQueue.h"
|
||||||
|
"${APP_DIR}/gl/ShaderTextureBindings.cpp"
|
||||||
|
"${APP_DIR}/gl/ShaderTextureBindings.h"
|
||||||
|
"${APP_DIR}/gl/Std140Buffer.h"
|
||||||
|
"${APP_DIR}/gl/TextRasterizer.cpp"
|
||||||
|
"${APP_DIR}/gl/TextRasterizer.h"
|
||||||
|
"${APP_DIR}/gl/TextureAssetLoader.cpp"
|
||||||
|
"${APP_DIR}/gl/TextureAssetLoader.h"
|
||||||
|
"${APP_DIR}/gl/TemporalHistoryBuffers.cpp"
|
||||||
|
"${APP_DIR}/gl/TemporalHistoryBuffers.h"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
||||||
"${APP_DIR}/NativeHandles.h"
|
"${APP_DIR}/platform/NativeHandles.h"
|
||||||
"${APP_DIR}/NativeSockets.h"
|
"${APP_DIR}/platform/NativeSockets.h"
|
||||||
"${APP_DIR}/OpenGLComposite.cpp"
|
|
||||||
"${APP_DIR}/OpenGLComposite.h"
|
|
||||||
"${APP_DIR}/OscServer.cpp"
|
|
||||||
"${APP_DIR}/OscServer.h"
|
|
||||||
"${APP_DIR}/resource.h"
|
"${APP_DIR}/resource.h"
|
||||||
"${APP_DIR}/RuntimeHost.cpp"
|
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||||
"${APP_DIR}/RuntimeHost.h"
|
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
"${APP_DIR}/RuntimeJson.h"
|
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.h"
|
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||||
"${APP_DIR}/ShaderCompiler.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
"${APP_DIR}/ShaderCompiler.h"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.h"
|
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||||
"${APP_DIR}/ShaderTypes.h"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
||||||
|
"${APP_DIR}/shader/ShaderTypes.h"
|
||||||
"${APP_DIR}/stdafx.cpp"
|
"${APP_DIR}/stdafx.cpp"
|
||||||
"${APP_DIR}/stdafx.h"
|
"${APP_DIR}/stdafx.h"
|
||||||
"${APP_DIR}/targetver.h"
|
"${APP_DIR}/targetver.h"
|
||||||
"${APP_DIR}/VideoFrameTransfer.cpp"
|
|
||||||
"${APP_DIR}/VideoFrameTransfer.h"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
||||||
|
|
||||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
"${GPUDIRECT_DIR}/include"
|
"${APP_DIR}/control"
|
||||||
)
|
"${APP_DIR}/decklink"
|
||||||
|
"${APP_DIR}/gl"
|
||||||
target_link_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
"${APP_DIR}/platform"
|
||||||
"${GPUDIRECT_DIR}/lib/x64"
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
dvp.lib
|
|
||||||
opengl32
|
opengl32
|
||||||
glu32
|
glu32
|
||||||
Ws2_32
|
Ws2_32
|
||||||
Crypt32
|
Crypt32
|
||||||
Advapi32
|
Advapi32
|
||||||
|
Gdiplus
|
||||||
|
Ole32
|
||||||
|
Windowscodecs
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||||
@@ -99,12 +139,13 @@ if(MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(RuntimeJsonTests
|
add_executable(RuntimeJsonTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RuntimeJsonTests PRIVATE
|
target_include_directories(RuntimeJsonTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -114,14 +155,32 @@ endif()
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||||
|
|
||||||
|
add_executable(RuntimeClockTests
|
||||||
|
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RuntimeClockTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||||
|
|
||||||
add_executable(RuntimeParameterUtilsTests
|
add_executable(RuntimeParameterUtilsTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -130,14 +189,31 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||||
|
|
||||||
|
add_executable(Std140BufferTests
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(Std140BufferTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/gl"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(Std140BufferTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||||
|
|
||||||
add_executable(ShaderPackageRegistryTests
|
add_executable(ShaderPackageRegistryTests
|
||||||
"${APP_DIR}/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
@@ -147,12 +223,14 @@ endif()
|
|||||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||||
|
|
||||||
add_executable(OscServerTests
|
add_executable(OscServerTests
|
||||||
"${APP_DIR}/OscServer.cpp"
|
"${APP_DIR}/control/OscServer.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(OscServerTests PRIVATE
|
target_include_directories(OscServerTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/control"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(OscServerTests PRIVATE
|
target_link_libraries(OscServerTests PRIVATE
|
||||||
@@ -165,20 +243,26 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
add_test(NAME OscServerTests COMMAND OscServerTests)
|
||||||
|
|
||||||
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
|
add_executable(VideoIOFormatTests
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
"${APP_DIR}/decklink/VideoIOFormat.cpp"
|
||||||
"${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
|
||||||
"$<TARGET_FILE_DIR:LoopThroughWithOpenGLCompositing>/dvp.dll"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_include_directories(VideoIOFormatTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(VideoIOFormatTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
|
||||||
|
|
||||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||||
RUNTIME DESTINATION "."
|
RUNTIME DESTINATION "."
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
|
||||||
DESTINATION "."
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES ${SLANG_RUNTIME_FILES}
|
install(FILES ${SLANG_RUNTIME_FILES}
|
||||||
DESTINATION "3rdParty/slang/bin"
|
DESTINATION "3rdParty/slang/bin"
|
||||||
)
|
)
|
||||||
@@ -188,6 +272,10 @@ install(FILES "${SLANG_LICENSE_FILE}"
|
|||||||
RENAME "SLANG_LICENSE.txt"
|
RENAME "SLANG_LICENSE.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
|
||||||
|
DESTINATION "."
|
||||||
|
)
|
||||||
|
|
||||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/config/"
|
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/config/"
|
||||||
DESTINATION "config"
|
DESTINATION "config"
|
||||||
)
|
)
|
||||||
|
|||||||
66
README.md
66
README.md
@@ -20,23 +20,9 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
|
|||||||
- Windows with Visual Studio 2022 C++ tooling.
|
- Windows with Visual Studio 2022 C++ tooling.
|
||||||
- CMake 3.24 or newer.
|
- CMake 3.24 or newer.
|
||||||
- Node.js and npm for the control UI.
|
- Node.js and npm for the control UI.
|
||||||
- Blackmagic DeckLink SDK 16.0 with the NVIDIA GPUDirect sample files available locally.
|
- Blackmagic Desktop Video drivers and a DeckLink device.
|
||||||
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
|
||||||
3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect
|
|
||||||
```
|
|
||||||
|
|
||||||
Override example:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cmake --preset vs2022-x64-debug -DGPUDIRECT_DIR="D:/SDKs/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect"
|
|
||||||
```
|
|
||||||
|
|
||||||
Default expected Slang path:
|
Default expected Slang path:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -87,7 +73,6 @@ The package folder will contain:
|
|||||||
```text
|
```text
|
||||||
dist/VideoShader/
|
dist/VideoShader/
|
||||||
LoopThroughWithOpenGLCompositing.exe
|
LoopThroughWithOpenGLCompositing.exe
|
||||||
dvp.dll
|
|
||||||
config/
|
config/
|
||||||
shaders/
|
shaders/
|
||||||
3rdParty/slang/bin/
|
3rdParty/slang/bin/
|
||||||
@@ -125,7 +110,7 @@ Current native test coverage includes:
|
|||||||
|
|
||||||
- JSON parsing and serialization.
|
- JSON parsing and serialization.
|
||||||
- Parameter normalization and preset filename safety.
|
- Parameter normalization and preset filename safety.
|
||||||
- Shader manifest parsing and package registry scanning.
|
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||||
|
|
||||||
## Runtime Configuration
|
## Runtime Configuration
|
||||||
|
|
||||||
@@ -185,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.
|
Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
|
||||||
|
|
||||||
|
The control UI also has a Screenshot button. It queues a capture of the final output render target and writes a PNG under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime/screenshots/
|
||||||
|
```
|
||||||
|
|
||||||
## OSC Control
|
## OSC Control
|
||||||
|
|
||||||
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
||||||
@@ -203,9 +194,10 @@ Each shader package lives under:
|
|||||||
shaders/<id>/
|
shaders/<id>/
|
||||||
shader.json
|
shader.json
|
||||||
shader.slang
|
shader.slang
|
||||||
|
optional-font-or-texture-assets
|
||||||
```
|
```
|
||||||
|
|
||||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, temporal history support, and the Slang entry point contract.
|
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license.
|
||||||
|
|
||||||
## Generated Files
|
## Generated Files
|
||||||
|
|
||||||
@@ -223,15 +215,37 @@ Only `runtime/templates/` and `runtime/README.md` are tracked.
|
|||||||
|
|
||||||
The Gitea workflow expects two act runners:
|
The Gitea workflow expects two act runners:
|
||||||
|
|
||||||
- `windows-latest`: builds the native app and runs native tests.
|
- `windows-2022`: builds the native app and runs native tests.
|
||||||
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
|
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
|
||||||
|
|
||||||
If your Windows runner stores the Blackmagic SDK outside the repo, configure `GPUDIRECT_DIR` in the runner environment or adjust the workflow configure command to pass `-DGPUDIRECT_DIR=...`.
|
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
|
||||||
|
|
||||||
|
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
||||||
|
|
||||||
## Still todo
|
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`.
|
||||||
Audio
|
|
||||||
Fonts
|
Example runner paths:
|
||||||
genlock
|
|
||||||
Logs
|
```text
|
||||||
anamorphic desqueeze
|
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
|
||||||
|
- unwrap a fish eyelens and mirror it and map it to equirectangulr for environmnet map purposes
|
||||||
|
|||||||
@@ -57,6 +57,31 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
|
|
||||||
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically.
|
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically.
|
||||||
|
|
||||||
|
## Guidance For Shaders
|
||||||
|
|
||||||
|
When generating a new shader package, prefer matching the existing runtime contract over copying code verbatim from Shadertoy, GLSL sandbox sites, or WebGL demos.
|
||||||
|
|
||||||
|
Important rules:
|
||||||
|
|
||||||
|
- Generate a complete package: `shaders/<id>/shader.json` and `shaders/<id>/shader.slang`.
|
||||||
|
- Use `float4 shadeVideo(ShaderContext context)` unless the manifest explicitly sets a different `entryPoint`.
|
||||||
|
- Do not create `mainImage`, `main`, `fragColor`, `iResolution`, `iTime`, `iChannel0`, or a fragment shader attribute layout. The runtime wrapper provides the real fragment entry point.
|
||||||
|
- Replace Shadertoy `fragCoord` with `context.uv * context.outputResolution`.
|
||||||
|
- Replace `iResolution.xy` with `context.outputResolution`.
|
||||||
|
- Replace `iTime` with `context.time`.
|
||||||
|
- Replace `iFrame` with `context.frameCount`.
|
||||||
|
- Replace source-video `iChannel0` sampling with `sampleVideo(uv)` or `context.sourceColor`.
|
||||||
|
- Use Slang/HLSL names and syntax: `float2`, `float3`, `float4`, `float2x2`, `lerp`, `frac`, `saturate`, and `mul(matrix, vector)`.
|
||||||
|
- Do not use GLSL-only types/functions such as `vec2`, `vec3`, `vec4`, `mat2`, `mix`, `fract`, `mod`, `texture`, or `mainImage`.
|
||||||
|
- Keep parameter IDs, texture IDs, font IDs, and function entry points as valid shader identifiers: letters, numbers, and underscores only, starting with a letter or underscore.
|
||||||
|
- Add only controls that are actually used by the shader.
|
||||||
|
- Prefer a small number of clear controls with conservative defaults.
|
||||||
|
- Keep shaders deterministic unless randomness is an explicit feature. For stable process-level variation, use `context.startupRandom`; for per-pixel pseudo-randomness, hash from `uv`, pixel coordinates, `frameCount`, or trigger values.
|
||||||
|
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
|
||||||
|
- If the source license is unclear or incompatible, do not add the shader package.
|
||||||
|
|
||||||
|
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector.
|
||||||
|
|
||||||
## Manifest Fields
|
## Manifest Fields
|
||||||
|
|
||||||
`shader.json` is the runtime-facing description of the shader.
|
`shader.json` is the runtime-facing description of the shader.
|
||||||
@@ -73,6 +98,7 @@ Optional fields:
|
|||||||
- `category`: UI grouping label.
|
- `category`: UI grouping label.
|
||||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||||
- `textures`: texture assets to load and expose as samplers.
|
- `textures`: texture assets to load and expose as samplers.
|
||||||
|
- `fonts`: packaged font assets for live text parameters.
|
||||||
- `temporal`: history-buffer requirements.
|
- `temporal`: history-buffer requirements.
|
||||||
|
|
||||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||||
@@ -80,6 +106,7 @@ Shader-visible identifiers must be valid Slang-style identifiers:
|
|||||||
- `entryPoint`
|
- `entryPoint`
|
||||||
- parameter `id`
|
- parameter `id`
|
||||||
- texture `id`
|
- texture `id`
|
||||||
|
- font `id`
|
||||||
|
|
||||||
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
|
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
|
||||||
|
|
||||||
@@ -120,6 +147,9 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
|
float startupRandom;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -135,12 +165,22 @@ Fields:
|
|||||||
- `inputResolution`: decoded input video resolution in pixels.
|
- `inputResolution`: decoded input video resolution in pixels.
|
||||||
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
||||||
- `time`: elapsed runtime time in seconds.
|
- `time`: elapsed runtime time in seconds.
|
||||||
|
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
|
||||||
|
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
||||||
|
- `startupRandom`: random `0..1` value generated once when the host process starts. It stays constant for the lifetime of the app and changes on the next launch.
|
||||||
- `frameCount`: incrementing frame counter.
|
- `frameCount`: incrementing frame counter.
|
||||||
- `mixAmount`: runtime mix amount.
|
- `mixAmount`: runtime mix amount.
|
||||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
||||||
|
|
||||||
|
Color/precision notes:
|
||||||
|
|
||||||
|
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
||||||
|
- The 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
|
## Helper Functions
|
||||||
|
|
||||||
The wrapper provides:
|
The wrapper provides:
|
||||||
@@ -180,6 +220,8 @@ Supported types:
|
|||||||
| `color` | `float4` | `[r, g, b, a]` |
|
| `color` | `float4` | `[r, g, b, a]` |
|
||||||
| `bool` | `bool` | `true` or `false` |
|
| `bool` | `bool` | `true` or `false` |
|
||||||
| `enum` | `int` | selected option index |
|
| `enum` | `int` | selected option index |
|
||||||
|
| `text` | generated texture/helper | string |
|
||||||
|
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
|
||||||
|
|
||||||
Float example:
|
Float example:
|
||||||
|
|
||||||
@@ -278,12 +320,63 @@ else if (mode == 2)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Text example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [
|
||||||
|
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" }
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "titleText",
|
||||||
|
"label": "Title",
|
||||||
|
"type": "text",
|
||||||
|
"default": "LIVE",
|
||||||
|
"font": "inter",
|
||||||
|
"maxLength": 64
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Text parameters are runtime-owned strings. They are not emitted as uniform values. Instead, the runtime renders the current string into a single-line SDF mask texture and the shader wrapper exposes helpers based on the parameter id:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float mask = sampleTitleText(textUv);
|
||||||
|
float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
|
||||||
|
```
|
||||||
|
|
||||||
|
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
|
||||||
|
|
||||||
|
Trigger example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "flash",
|
||||||
|
"label": "Flash",
|
||||||
|
"type": "trigger"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A trigger appears as a button in the control UI. Pressing it increments the shader-visible integer `flash` and records the runtime time in `flashTime`:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float age = context.time - flashTime;
|
||||||
|
float intensity = flash > 0 ? exp(-age * 5.0) : 0.0;
|
||||||
|
color.rgb += intensity;
|
||||||
|
```
|
||||||
|
|
||||||
|
Triggers are useful for one-shot shader reactions such as flashes, ripples, cuts, or randomized looks. They do not execute arbitrary CPU code; they only update uniforms consumed by the shader.
|
||||||
|
|
||||||
Parameter validation:
|
Parameter validation:
|
||||||
|
|
||||||
- Float values are clamped to `min`/`max` if provided.
|
- Float values are clamped to `min`/`max` if provided.
|
||||||
- `vec2` must have exactly 2 numbers.
|
- `vec2` must have exactly 2 numbers.
|
||||||
- `color` must have exactly 4 numbers.
|
- `color` must have exactly 4 numbers.
|
||||||
- Enum defaults must match one of the declared option values.
|
- Enum defaults must match one of the declared option values.
|
||||||
|
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
|
||||||
|
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
|
||||||
- Non-finite numeric values are rejected.
|
- Non-finite numeric values are rejected.
|
||||||
|
|
||||||
## Texture Assets
|
## Texture Assets
|
||||||
@@ -323,6 +416,31 @@ return float4(logo.rgb * alpha, alpha);
|
|||||||
|
|
||||||
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
||||||
|
|
||||||
|
## Font Assets
|
||||||
|
|
||||||
|
Declare packaged font assets in the manifest:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [
|
||||||
|
{
|
||||||
|
"id": "inter",
|
||||||
|
"path": "fonts/Inter-Regular.ttf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `id` must be a valid shader identifier.
|
||||||
|
- `path` is relative to the shader package directory.
|
||||||
|
- The file must exist when the manifest is loaded.
|
||||||
|
- Font asset changes trigger shader reload.
|
||||||
|
- V1 text layout is single-line; shaders position and scale the generated text texture themselves.
|
||||||
|
|
||||||
|
See `shaders/text-overlay/` for a complete live text example. The sample bundles Roboto Regular and includes its OFL license beside the font file.
|
||||||
|
|
||||||
## Temporal Shaders
|
## Temporal Shaders
|
||||||
|
|
||||||
Temporal shaders can request access to previous frames.
|
Temporal shaders can request access to previous frames.
|
||||||
@@ -376,6 +494,19 @@ See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples
|
|||||||
- Use `context.inputResolution` when sampling source video by input pixel size.
|
- Use `context.inputResolution` when sampling source video by input pixel size.
|
||||||
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
||||||
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
||||||
|
- For generated calibration charts, test patterns, gradients, and exposure ramps, state whether patch values are linear-light, display-referred gamma encoded, Rec.709 encoded, or intentionally artistic.
|
||||||
|
- For one-stop exposure patches, each patch should normally be `baseLevel * 2^patchIndex` before any display/tone encoding.
|
||||||
|
- For Rec.709 OETF encoding, use:
|
||||||
|
|
||||||
|
```slang
|
||||||
|
float rec709Oetf(float linearLevel)
|
||||||
|
{
|
||||||
|
float value = saturate(linearLevel);
|
||||||
|
if (value < 0.018)
|
||||||
|
return 4.5 * value;
|
||||||
|
return 1.099 * pow(value, 0.45) - 0.099;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Pixel-size example:
|
Pixel-size example:
|
||||||
|
|
||||||
@@ -384,6 +515,28 @@ float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
|
|||||||
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Animation And Timing Notes
|
||||||
|
|
||||||
|
- `context.time` is elapsed runtime time in seconds and is the default animation source for generative shaders.
|
||||||
|
- `context.frameCount` increments once per rendered output frame and is useful when an effect must be frame-locked.
|
||||||
|
- Avoid expensive CPU-like timing logic in the shader; animation should usually be a simple function of `context.time`, `context.frameCount`, trigger uniforms, or parameters.
|
||||||
|
- If a shader appears to judder only while animated, first test whether freezing its time removes the issue. That usually separates animation cadence issues from rendering or transfer issues.
|
||||||
|
- Do not add custom timer uniforms to the wrapper. Use the fields already in `ShaderContext`.
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
The app has to meet a fixed video frame cadence, so avoid shader code that only looks good in unconstrained browser demos.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Keep loops bounded with compile-time constants where possible.
|
||||||
|
- Avoid very high per-pixel raymarch counts by default. If a heavy loop is needed, expose a quality/steps control with a safe default.
|
||||||
|
- Prefer early exits only when they are simple; highly divergent branches can be expensive across a full frame.
|
||||||
|
- Avoid repeated texture sampling in large loops unless the effect really needs it.
|
||||||
|
- Use `context.outputResolution` carefully. A 1080p frame is over 2 million fragments; a tiny extra loop can become expensive.
|
||||||
|
- The UI render time may measure CPU command submission rather than true GPU execution time, so visual frame issues can still be GPU-related even when reported render time is small.
|
||||||
|
- Do not write debug files, allocate resources, or assume CPU-side work can happen from `shader.slang`. Shader code is GPU-only.
|
||||||
|
|
||||||
## Reload And Generated Files
|
## Reload And Generated Files
|
||||||
|
|
||||||
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
||||||
@@ -401,6 +554,7 @@ These files are ignored by git and are useful for debugging compiler output. If
|
|||||||
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
||||||
- Remember enum globals are integer indexes, not strings.
|
- Remember enum globals are integer indexes, not strings.
|
||||||
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
||||||
|
- Declare packaged fonts in `shader.json` when text parameters should use a specific font.
|
||||||
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
||||||
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
||||||
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
||||||
@@ -414,6 +568,7 @@ Before committing a new shader package:
|
|||||||
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
||||||
- `shader.slang` implements the configured entry point.
|
- `shader.slang` implements the configured entry point.
|
||||||
- Texture files referenced by `textures` exist.
|
- Texture files referenced by `textures` exist.
|
||||||
|
- Font files referenced by `fonts` exist.
|
||||||
- Enum defaults are present in their `options`.
|
- Enum defaults are present in their `options`.
|
||||||
- Temporal shaders handle short or empty history gracefully.
|
- Temporal shaders handle short or empty history gracefully.
|
||||||
- The app can reload and compile the shader without errors.
|
- The app can reload and compile the shader without errors.
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <shellapi.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
||||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
||||||
#endif
|
#endif
|
||||||
@@ -65,6 +69,169 @@
|
|||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int kStatusPanelWidth = 680;
|
||||||
|
const int kStatusPanelHeight = 92;
|
||||||
|
const int kStatusPadding = 8;
|
||||||
|
const int kStatusLabelWidth = 58;
|
||||||
|
const int kStatusButtonWidth = 86;
|
||||||
|
const int kStatusRowHeight = 24;
|
||||||
|
const int kStatusGap = 6;
|
||||||
|
const UINT kCreateStatusStripMessage = WM_APP + 1;
|
||||||
|
|
||||||
|
enum StatusControlId
|
||||||
|
{
|
||||||
|
kControlUrlEditId = 2001,
|
||||||
|
kDocsUrlEditId = 2002,
|
||||||
|
kOscAddressEditId = 2003,
|
||||||
|
kOpenControlButtonId = 2004,
|
||||||
|
kOpenDocsButtonId = 2005
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StatusStripControls
|
||||||
|
{
|
||||||
|
HWND panel = NULL;
|
||||||
|
HWND controlLabel = NULL;
|
||||||
|
HWND controlUrl = NULL;
|
||||||
|
HWND openControl = NULL;
|
||||||
|
HWND docsLabel = NULL;
|
||||||
|
HWND docsUrl = NULL;
|
||||||
|
HWND openDocs = NULL;
|
||||||
|
HWND oscLabel = NULL;
|
||||||
|
HWND oscAddress = NULL;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool StatusStripCreated(const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
return controls.panel != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, DWORD exStyle, int controlId)
|
||||||
|
{
|
||||||
|
return CreateWindowExA(
|
||||||
|
exStyle,
|
||||||
|
className,
|
||||||
|
text,
|
||||||
|
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | style,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
parent,
|
||||||
|
reinterpret_cast<HMENU>(static_cast<INT_PTR>(controlId)),
|
||||||
|
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE)),
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateStatusStrip(HWND hWnd, StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_LEFT, WS_EX_CLIENTEDGE, 0);
|
||||||
|
controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0, 0);
|
||||||
|
controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kControlUrlEditId);
|
||||||
|
controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenControlButtonId);
|
||||||
|
controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0, 0);
|
||||||
|
controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kDocsUrlEditId);
|
||||||
|
controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenDocsButtonId);
|
||||||
|
controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0, 0);
|
||||||
|
controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kOscAddressEditId);
|
||||||
|
|
||||||
|
HFONT guiFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
||||||
|
HWND children[] = {
|
||||||
|
controls.controlLabel,
|
||||||
|
controls.controlUrl,
|
||||||
|
controls.openControl,
|
||||||
|
controls.docsLabel,
|
||||||
|
controls.docsUrl,
|
||||||
|
controls.openDocs,
|
||||||
|
controls.oscLabel,
|
||||||
|
controls.oscAddress
|
||||||
|
};
|
||||||
|
for (HWND child : children)
|
||||||
|
{
|
||||||
|
if (child)
|
||||||
|
SendMessage(child, WM_SETFONT, reinterpret_cast<WPARAM>(guiFont), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetWindowTextA(controls.controlUrl, "Starting control server...");
|
||||||
|
SetWindowTextA(controls.docsUrl, "Starting API docs...");
|
||||||
|
SetWindowTextA(controls.oscAddress, "Starting OSC listener...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RaiseStatusControls(const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(controls))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetWindowPos(controls.panel, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
|
||||||
|
HWND interactiveControls[] = {
|
||||||
|
controls.controlLabel,
|
||||||
|
controls.controlUrl,
|
||||||
|
controls.openControl,
|
||||||
|
controls.docsLabel,
|
||||||
|
controls.docsUrl,
|
||||||
|
controls.openDocs,
|
||||||
|
controls.oscLabel,
|
||||||
|
controls.oscAddress
|
||||||
|
};
|
||||||
|
for (HWND control : interactiveControls)
|
||||||
|
{
|
||||||
|
if (control)
|
||||||
|
SetWindowPos(control, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls)
|
||||||
|
{
|
||||||
|
RECT clientRect = {};
|
||||||
|
if (!GetClientRect(hWnd, &clientRect) || !controls.panel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int clientWidth = static_cast<int>(clientRect.right - clientRect.left);
|
||||||
|
const int clientHeight = static_cast<int>(clientRect.bottom - clientRect.top);
|
||||||
|
const int panelWidth = std::max(280, std::min(kStatusPanelWidth, clientWidth - (kStatusPadding * 2)));
|
||||||
|
const int panelHeight = kStatusPanelHeight;
|
||||||
|
const int panelLeft = kStatusPadding;
|
||||||
|
const int panelTop = std::max(kStatusPadding, clientHeight - panelHeight - kStatusPadding);
|
||||||
|
MoveWindow(controls.panel, panelLeft, panelTop, panelWidth, panelHeight, TRUE);
|
||||||
|
|
||||||
|
const int rowX = panelLeft + kStatusPadding;
|
||||||
|
const int editX = rowX + kStatusLabelWidth + kStatusGap;
|
||||||
|
const int buttonX = panelLeft + panelWidth - kStatusPadding - kStatusButtonWidth;
|
||||||
|
const int editWidth = std::max(80, buttonX - editX - kStatusGap);
|
||||||
|
const int oscWidth = std::max(80, panelLeft + panelWidth - editX - kStatusPadding);
|
||||||
|
const int row1 = panelTop + kStatusPadding;
|
||||||
|
const int row2 = row1 + kStatusRowHeight + kStatusGap;
|
||||||
|
const int row3 = row2 + kStatusRowHeight + kStatusGap;
|
||||||
|
|
||||||
|
MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||||
|
MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE);
|
||||||
|
RaiseStatusControls(controls);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(controls))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str());
|
||||||
|
SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str());
|
||||||
|
SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenUrl(const char* url)
|
||||||
|
{
|
||||||
|
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ShowUnhandledExceptionMessage(const char* prefix)
|
void ShowUnhandledExceptionMessage(const char* prefix)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -203,6 +370,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
static HDC hDC = NULL; // Private GDI Device context
|
static HDC hDC = NULL; // Private GDI Device context
|
||||||
static OpenGLComposite* pOpenGLComposite = NULL;
|
static OpenGLComposite* pOpenGLComposite = NULL;
|
||||||
static bool sInteractiveResize = false;
|
static bool sInteractiveResize = false;
|
||||||
|
static StatusStripControls sStatusStrip;
|
||||||
|
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
@@ -251,7 +419,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
if (pOpenGLComposite->Start())
|
if (pOpenGLComposite->Start())
|
||||||
|
{
|
||||||
|
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
||||||
break; // success
|
break; // success
|
||||||
|
}
|
||||||
MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
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
|
else
|
||||||
@@ -273,6 +444,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case kCreateStatusStripMessage:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
if (!StatusStripCreated(sStatusStrip))
|
||||||
|
CreateStatusStrip(hWnd, sStatusStrip);
|
||||||
|
|
||||||
|
UpdateStatusStrip(sStatusStrip, *pOpenGLComposite);
|
||||||
|
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||||
|
RECT clientRect = {};
|
||||||
|
if (GetClientRect(hWnd, &clientRect))
|
||||||
|
{
|
||||||
|
pOpenGLComposite->resizeGL(
|
||||||
|
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||||
|
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -305,7 +495,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
RECT clientRect = {};
|
RECT clientRect = {};
|
||||||
if (GetClientRect(hWnd, &clientRect))
|
if (GetClientRect(hWnd, &clientRect))
|
||||||
pOpenGLComposite->resizeGL(static_cast<WORD>(clientRect.right - clientRect.left), static_cast<WORD>(clientRect.bottom - clientRect.top));
|
{
|
||||||
|
pOpenGLComposite->resizeGL(
|
||||||
|
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||||
|
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
InvalidateRect(hWnd, NULL, FALSE);
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
break;
|
break;
|
||||||
@@ -313,6 +507,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (StatusStripCreated(sStatusStrip))
|
||||||
|
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||||
if (pOpenGLComposite)
|
if (pOpenGLComposite)
|
||||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||||
}
|
}
|
||||||
@@ -337,6 +533,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
wglMakeCurrent(hDC, hRC);
|
wglMakeCurrent(hDC, hRC);
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL();
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
|
RaiseStatusControls(sStatusStrip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
@@ -361,6 +558,28 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_COMMAND:
|
||||||
|
switch (LOWORD(wParam))
|
||||||
|
{
|
||||||
|
case kOpenControlButtonId:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
std::string url = pOpenGLComposite->GetControlUrl();
|
||||||
|
OpenUrl(url.c_str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case kOpenDocsButtonId:
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
std::string url = pOpenGLComposite->GetDocsUrl();
|
||||||
|
OpenUrl(url.c_str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
return (DefWindowProc(hWnd, message, wParam, lParam));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>Disabled</Optimization>
|
<Optimization>Disabled</Optimization>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||||
@@ -99,15 +99,11 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<TargetMachine>MachineX86</TargetMachine>
|
<TargetMachine>MachineX86</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<Midl>
|
<Midl>
|
||||||
@@ -115,7 +111,7 @@
|
|||||||
</Midl>
|
</Midl>
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>Disabled</Optimization>
|
<Optimization>Disabled</Optimization>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||||
@@ -125,21 +121,17 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<TargetMachine>MachineX64</TargetMachine>
|
<TargetMachine>MachineX64</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>MaxSpeed</Optimization>
|
<Optimization>MaxSpeed</Optimization>
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
@@ -149,18 +141,13 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
<TargetMachine>MachineX86</TargetMachine>
|
<TargetMachine>MachineX86</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Message>Copy nececssary DLLs to target directory</Message>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<Midl>
|
<Midl>
|
||||||
@@ -169,7 +156,7 @@
|
|||||||
<ClCompile>
|
<ClCompile>
|
||||||
<Optimization>MaxSpeed</Optimization>
|
<Optimization>MaxSpeed</Optimization>
|
||||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
@@ -179,39 +166,53 @@
|
|||||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
<TargetMachine>MachineX64</TargetMachine>
|
<TargetMachine>MachineX64</TargetMachine>
|
||||||
</Link>
|
</Link>
|
||||||
<PostBuildEvent>
|
|
||||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="GLExtensions.cpp" />
|
<ClCompile Include="gl\GLExtensions.cpp" />
|
||||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.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="stdafx.cpp">
|
<ClCompile Include="stdafx.cpp">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="VideoFrameTransfer.cpp" />
|
|
||||||
<ClCompile Include="DeckLinkAPI_i.c" />
|
<ClCompile Include="DeckLinkAPI_i.c" />
|
||||||
|
<ClCompile Include="control\RuntimeServices.cpp" />
|
||||||
|
<ClCompile Include="decklink\DeckLinkSession.cpp" />
|
||||||
|
<ClCompile Include="decklink\VideoIOFormat.cpp" />
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="GLExtensions.h" />
|
<ClInclude Include="gl\GLExtensions.h" />
|
||||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.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="resource.h" />
|
<ClInclude Include="resource.h" />
|
||||||
<ClInclude Include="stdafx.h" />
|
<ClInclude Include="stdafx.h" />
|
||||||
<ClInclude Include="targetver.h" />
|
<ClInclude Include="targetver.h" />
|
||||||
<ClInclude Include="VideoFrameTransfer.h" />
|
<ClInclude Include="control\RuntimeServices.h" />
|
||||||
|
<ClInclude Include="decklink\DeckLinkSession.h" />
|
||||||
|
<ClInclude Include="decklink\VideoIOFormat.h" />
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||||
|
|||||||
@@ -18,33 +18,78 @@
|
|||||||
</Filter>
|
</Filter>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="GLExtensions.cpp">
|
<ClCompile Include="gl\GLExtensions.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
|
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="OpenGLComposite.cpp">
|
<ClCompile Include="gl\OpenGLComposite.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="gl\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>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="stdafx.cpp">
|
<ClCompile Include="stdafx.cpp">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="VideoFrameTransfer.cpp">
|
|
||||||
<Filter>Source Files</Filter>
|
|
||||||
</ClCompile>
|
|
||||||
<ClCompile Include="DeckLinkAPI_i.c">
|
<ClCompile Include="DeckLinkAPI_i.c">
|
||||||
<Filter>DeckLink API</Filter>
|
<Filter>DeckLink API</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="control\RuntimeServices.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="decklink\DeckLinkSession.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="decklink\VideoIOFormat.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="runtime\RuntimeClock.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="GLExtensions.h">
|
<ClInclude Include="gl\GLExtensions.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
|
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="OpenGLComposite.h">
|
<ClInclude Include="gl\OpenGLComposite.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="gl\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>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="resource.h">
|
<ClInclude Include="resource.h">
|
||||||
@@ -56,7 +101,16 @@
|
|||||||
<ClInclude Include="targetver.h">
|
<ClInclude Include="targetver.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="VideoFrameTransfer.h">
|
<ClInclude Include="control\RuntimeServices.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="decklink\DeckLinkSession.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="decklink\VideoIOFormat.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="runtime\RuntimeClock.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,361 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef __OPENGL_COMPOSITE_H__
|
|
||||||
#define __OPENGL_COMPOSITE_H__
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <process.h>
|
|
||||||
#include <tchar.h>
|
|
||||||
#include <gl/gl.h>
|
|
||||||
#include <gl/glu.h>
|
|
||||||
|
|
||||||
#include <objbase.h>
|
|
||||||
#include <atlbase.h>
|
|
||||||
#include <comutil.h>
|
|
||||||
#include "DeckLinkAPI_h.h"
|
|
||||||
|
|
||||||
#include "VideoFrameTransfer.h"
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
#include <deque>
|
|
||||||
|
|
||||||
class PlayoutDelegate;
|
|
||||||
class CaptureDelegate;
|
|
||||||
class PinnedMemoryAllocator;
|
|
||||||
class ControlServer;
|
|
||||||
class OscServer;
|
|
||||||
|
|
||||||
|
|
||||||
class OpenGLComposite
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
|
||||||
~OpenGLComposite();
|
|
||||||
|
|
||||||
bool InitDeckLink();
|
|
||||||
bool Start();
|
|
||||||
bool Stop();
|
|
||||||
bool ReloadShader();
|
|
||||||
std::string GetRuntimeStateJson() const;
|
|
||||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
|
||||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
|
||||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
|
||||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
|
||||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
|
||||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
|
||||||
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
|
||||||
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
|
||||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
|
||||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
|
||||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
|
||||||
void paintGL();
|
|
||||||
|
|
||||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
|
||||||
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void resizeWindow(int width, int height);
|
|
||||||
bool CheckOpenGLExtensions();
|
|
||||||
|
|
||||||
CaptureDelegate* mCaptureDelegate;
|
|
||||||
PlayoutDelegate* mPlayoutDelegate;
|
|
||||||
HWND hGLWnd;
|
|
||||||
HDC hGLDC;
|
|
||||||
HGLRC hGLRC;
|
|
||||||
CRITICAL_SECTION pMutex;
|
|
||||||
|
|
||||||
// DeckLink
|
|
||||||
IDeckLinkInput* mDLInput;
|
|
||||||
IDeckLinkOutput* mDLOutput;
|
|
||||||
IDeckLinkKeyer* mDLKeyer;
|
|
||||||
std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue;
|
|
||||||
PinnedMemoryAllocator* mPlayoutAllocator;
|
|
||||||
BMDTimeValue mFrameDuration;
|
|
||||||
BMDTimeScale mFrameTimescale;
|
|
||||||
unsigned mTotalPlayoutFrames;
|
|
||||||
unsigned mInputFrameWidth;
|
|
||||||
unsigned mInputFrameHeight;
|
|
||||||
unsigned mOutputFrameWidth;
|
|
||||||
unsigned mOutputFrameHeight;
|
|
||||||
std::string mInputDisplayModeName;
|
|
||||||
std::string mOutputDisplayModeName;
|
|
||||||
bool mHasNoInputSource;
|
|
||||||
std::string mDeckLinkOutputModelName;
|
|
||||||
bool mDeckLinkSupportsInternalKeying;
|
|
||||||
bool mDeckLinkSupportsExternalKeying;
|
|
||||||
bool mDeckLinkKeyerInterfaceAvailable;
|
|
||||||
bool mDeckLinkExternalKeyingActive;
|
|
||||||
std::string mDeckLinkStatusMessage;
|
|
||||||
|
|
||||||
// OpenGL data
|
|
||||||
bool mFastTransferExtensionAvailable;
|
|
||||||
GLuint mCaptureTexture;
|
|
||||||
GLuint mDecodedTexture;
|
|
||||||
GLuint mLayerTempTexture;
|
|
||||||
GLuint mFBOTexture;
|
|
||||||
GLuint mOutputTexture;
|
|
||||||
GLuint mUnpinnedTextureBuffer;
|
|
||||||
GLuint mDecodeFrameBuf;
|
|
||||||
GLuint mLayerTempFrameBuf;
|
|
||||||
GLuint mIdFrameBuf;
|
|
||||||
GLuint mOutputFrameBuf;
|
|
||||||
GLuint mIdColorBuf;
|
|
||||||
GLuint mIdDepthBuf;
|
|
||||||
GLuint mFullscreenVAO;
|
|
||||||
GLuint mGlobalParamsUBO;
|
|
||||||
GLuint mDecodeProgram;
|
|
||||||
GLuint mDecodeVertexShader;
|
|
||||||
GLuint mDecodeFragmentShader;
|
|
||||||
GLsizeiptr mGlobalParamsUBOSize;
|
|
||||||
int mViewWidth;
|
|
||||||
int mViewHeight;
|
|
||||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
|
||||||
std::unique_ptr<ControlServer> mControlServer;
|
|
||||||
std::unique_ptr<OscServer> mOscServer;
|
|
||||||
|
|
||||||
struct LayerProgram
|
|
||||||
{
|
|
||||||
struct TextureBinding
|
|
||||||
{
|
|
||||||
std::string samplerName;
|
|
||||||
std::filesystem::path sourcePath;
|
|
||||||
GLuint texture = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string layerId;
|
|
||||||
std::string shaderId;
|
|
||||||
GLuint program = 0;
|
|
||||||
GLuint vertexShader = 0;
|
|
||||||
GLuint fragmentShader = 0;
|
|
||||||
std::vector<TextureBinding> textureBindings;
|
|
||||||
};
|
|
||||||
std::vector<LayerProgram> mLayerPrograms;
|
|
||||||
|
|
||||||
struct HistorySlot
|
|
||||||
{
|
|
||||||
GLuint texture = 0;
|
|
||||||
GLuint framebuffer = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HistoryRing
|
|
||||||
{
|
|
||||||
std::vector<HistorySlot> slots;
|
|
||||||
std::size_t nextWriteIndex = 0;
|
|
||||||
std::size_t filledCount = 0;
|
|
||||||
unsigned effectiveLength = 0;
|
|
||||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryRing mSourceHistoryRing;
|
|
||||||
std::map<std::string, HistoryRing> mPreLayerHistoryByLayerId;
|
|
||||||
bool mTemporalHistoryNeedsReset;
|
|
||||||
|
|
||||||
bool InitOpenGLState();
|
|
||||||
bool compileLayerPrograms(int errorMessageSize, char* errorMessage);
|
|
||||||
bool compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
|
||||||
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
|
||||||
void destroyLayerPrograms();
|
|
||||||
void destroySingleLayerProgram(LayerProgram& layerProgram);
|
|
||||||
void destroyDecodeShaderProgram();
|
|
||||||
void renderDecodePass();
|
|
||||||
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
|
|
||||||
bool loadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
|
||||||
void bindLayerTextureAssets(const LayerProgram& layerProgram);
|
|
||||||
void renderEffect();
|
|
||||||
bool PollRuntimeChanges();
|
|
||||||
void broadcastRuntimeState();
|
|
||||||
bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
|
||||||
bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const;
|
|
||||||
bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error);
|
|
||||||
bool createHistoryRing(HistoryRing& ring, unsigned effectiveLength, TemporalHistorySource historySource, std::string& error);
|
|
||||||
void destroyHistoryRing(HistoryRing& ring);
|
|
||||||
void destroyTemporalHistoryResources();
|
|
||||||
void resetTemporalHistoryState();
|
|
||||||
void pushFramebufferToHistoryRing(GLuint sourceFramebuffer, HistoryRing& ring);
|
|
||||||
void bindHistorySamplers(const RuntimeRenderState& state, GLuint currentSourceTexture);
|
|
||||||
GLuint resolveHistoryTexture(const HistoryRing& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
|
||||||
unsigned sourceHistoryAvailableCount() const;
|
|
||||||
unsigned temporalHistoryAvailableCountForLayer(const std::string& layerId) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// PinnedMemoryAllocator
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class PinnedMemoryAllocator : public IDeckLinkVideoBufferAllocator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
PinnedMemoryAllocator(HDC hdc, HGLRC hglrc, VideoFrameTransfer::Direction direction, unsigned cacheSize, unsigned bufferSize);
|
|
||||||
virtual ~PinnedMemoryAllocator();
|
|
||||||
|
|
||||||
bool transferFrame(void* address, GLuint gpuTexture);
|
|
||||||
void waitForTransferComplete(void* address);
|
|
||||||
unsigned bufferSize() { return mBufferSize; }
|
|
||||||
|
|
||||||
// IUnknown methods
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBufferAllocator methods
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE AllocateVideoBuffer (IDeckLinkVideoBuffer** allocatedBuffer) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
void unPinAddress(void* address);
|
|
||||||
|
|
||||||
private:
|
|
||||||
HDC mHGLDC;
|
|
||||||
HGLRC mHGLRC;
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
VideoFrameTransfer::Direction mDirection;
|
|
||||||
std::map<void*, VideoFrameTransfer*> mFrameTransfer;
|
|
||||||
unsigned mBufferSize;
|
|
||||||
std::vector<void*> mFrameCache;
|
|
||||||
unsigned mFrameCacheSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// InputAllocatorPool
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class InputAllocatorPool : public IDeckLinkVideoBufferAllocatorProvider
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
InputAllocatorPool(HDC hdc, HGLRC hglrc);
|
|
||||||
|
|
||||||
// IUnknown interface
|
|
||||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
|
||||||
ULONG STDMETHODCALLTYPE Release() override;
|
|
||||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBufferAllocatorProvider interface
|
|
||||||
HRESULT STDMETHODCALLTYPE GetVideoBufferAllocator(
|
|
||||||
/* [in] */ unsigned int bufferSize,
|
|
||||||
/* [in] */ unsigned int width,
|
|
||||||
/* [in] */ unsigned int height,
|
|
||||||
/* [in] */ unsigned int rowBytes,
|
|
||||||
/* [in] */ BMDPixelFormat pixelFormat,
|
|
||||||
/* [out] */ IDeckLinkVideoBufferAllocator **allocator) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
std::map<unsigned int, CComPtr<PinnedMemoryAllocator> > mAllocatorBySize;
|
|
||||||
HDC mHDC;
|
|
||||||
HGLRC mHGLRC;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// DeckLinkVideoBuffer
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class DeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit DeckLinkVideoBuffer(std::shared_ptr<void>& buffer, PinnedMemoryAllocator* parent);
|
|
||||||
virtual ~DeckLinkVideoBuffer() = default;
|
|
||||||
|
|
||||||
// IUnknown interface
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
|
||||||
|
|
||||||
// IDeckLinkVideoBuffer interface
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE GetSize(uint64_t* size) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags flags) override;
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags flags) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
CComPtr<PinnedMemoryAllocator> mParentAllocator; // Dual-purpose: allocator owns mem this points to, and to access transferFrame() via a QueryInterface
|
|
||||||
std::atomic<ULONG> mRefCount;
|
|
||||||
std::shared_ptr<void> mBuffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class CaptureDelegate : public IDeckLinkInputCallback
|
|
||||||
{
|
|
||||||
OpenGLComposite* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
CaptureDelegate (OpenGLComposite* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived (IDeckLinkVideoInputFrame *videoFrame, IDeckLinkAudioInputPacket *audioPacket);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged (BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Render Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
|
|
||||||
{
|
|
||||||
OpenGLComposite* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
PlayoutDelegate (OpenGLComposite* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped ();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // __OPENGL_COMPOSITE_H__
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "VideoFrameTransfer.h"
|
|
||||||
#include "NativeHandles.h"
|
|
||||||
|
|
||||||
|
|
||||||
#define DVP_CHECK(cmd) { \
|
|
||||||
DVPStatus hr = (cmd); \
|
|
||||||
if (DVP_STATUS_OK != hr) { \
|
|
||||||
OutputDebugStringA( #cmd " failed\n" ); \
|
|
||||||
ExitProcess(hr); \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Initialise static members
|
|
||||||
bool VideoFrameTransfer::mInitialized = false;
|
|
||||||
bool VideoFrameTransfer::mUseDvp = false;
|
|
||||||
unsigned VideoFrameTransfer::mWidth = 0;
|
|
||||||
unsigned VideoFrameTransfer::mHeight = 0;
|
|
||||||
GLuint VideoFrameTransfer::mCaptureTexture = 0;
|
|
||||||
|
|
||||||
// NVIDIA specific static members
|
|
||||||
DVPBufferHandle VideoFrameTransfer::mDvpCaptureTextureHandle = 0;
|
|
||||||
DVPBufferHandle VideoFrameTransfer::mDvpPlaybackTextureHandle = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mBufferAddrAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mBufferGpuStrideAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphoreAddrAlignment = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphoreAllocSize = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphorePayloadOffset = 0;
|
|
||||||
uint32_t VideoFrameTransfer::mSemaphorePayloadSize = 0;
|
|
||||||
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::isNvidiaDvpAvailable()
|
|
||||||
{
|
|
||||||
// Look for supported graphics boards
|
|
||||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
|
||||||
if (renderer == NULL)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
|
|
||||||
return hasDvp;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::isAMDPinnedMemoryAvailable()
|
|
||||||
{
|
|
||||||
// GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported
|
|
||||||
const GLubyte* strExt = glGetString(GL_EXTENSIONS);
|
|
||||||
if (strExt == NULL)
|
|
||||||
{
|
|
||||||
// In a core profile context GL_EXTENSIONS is no longer queryable via glGetString().
|
|
||||||
// Treat this as "extension unavailable" for now; the fast-transfer path is optional.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL);
|
|
||||||
return hasAMDPinned;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::checkFastMemoryTransferAvailable()
|
|
||||||
{
|
|
||||||
return (isNvidiaDvpAvailable() || isAMDPinnedMemoryAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture)
|
|
||||||
{
|
|
||||||
if (mInitialized)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasDvp = isNvidiaDvpAvailable();
|
|
||||||
bool hasAMDPinned = isAMDPinnedMemoryAvailable();
|
|
||||||
|
|
||||||
if (!hasDvp && !hasAMDPinned)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mUseDvp = hasDvp;
|
|
||||||
mWidth = width;
|
|
||||||
mHeight = height;
|
|
||||||
mCaptureTexture = captureTexture;
|
|
||||||
|
|
||||||
if (! initializeMemoryLocking(mWidth * mHeight * 4)) // BGRA uses 4 bytes per pixel
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// DVP initialisation
|
|
||||||
DVP_CHECK(dvpInitGLContext(DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT));
|
|
||||||
DVP_CHECK(dvpGetRequiredConstantsGLCtx( &mBufferAddrAlignment, &mBufferGpuStrideAlignment,
|
|
||||||
&mSemaphoreAddrAlignment, &mSemaphoreAllocSize,
|
|
||||||
&mSemaphorePayloadOffset, &mSemaphorePayloadSize));
|
|
||||||
|
|
||||||
// Register textures with DVP
|
|
||||||
DVP_CHECK(dvpCreateGPUTextureGL(captureTexture, &mDvpCaptureTextureHandle));
|
|
||||||
DVP_CHECK(dvpCreateGPUTextureGL(playbackTexture, &mDvpPlaybackTextureHandle));
|
|
||||||
}
|
|
||||||
|
|
||||||
mInitialized = true;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::initializeMemoryLocking(unsigned memSize)
|
|
||||||
{
|
|
||||||
// Increase the process working set size to allow pinning of memory.
|
|
||||||
static SIZE_T dwMin = 0, dwMax = 0;
|
|
||||||
UniqueHandle processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, FALSE, GetCurrentProcessId()));
|
|
||||||
if (!processHandle.valid())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Retrieve the working set size of the process.
|
|
||||||
if (!dwMin && !GetProcessWorkingSetSize(processHandle.get(), &dwMin, &dwMax))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Allow for 80 frames to be locked
|
|
||||||
BOOL res = SetProcessWorkingSetSize(processHandle.get(), memSize * 80 + dwMin, memSize * 80 + (dwMax-dwMin));
|
|
||||||
if (!res)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncInfo sets up a semaphore which is shared between the GPU and CPU and used to
|
|
||||||
// synchronise access to DVP buffers.
|
|
||||||
struct SyncInfo
|
|
||||||
{
|
|
||||||
SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment);
|
|
||||||
~SyncInfo();
|
|
||||||
|
|
||||||
volatile uint32_t* mSem;
|
|
||||||
volatile uint32_t mReleaseValue;
|
|
||||||
volatile uint32_t mAcquireValue;
|
|
||||||
DVPSyncObjectHandle mDvpSync;
|
|
||||||
};
|
|
||||||
|
|
||||||
SyncInfo::SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment)
|
|
||||||
{
|
|
||||||
mSem = (uint32_t*)_aligned_malloc(semaphoreAllocSize, semaphoreAddrAlignment);
|
|
||||||
|
|
||||||
// Initialise
|
|
||||||
mSem[0] = 0;
|
|
||||||
mReleaseValue = 0;
|
|
||||||
mAcquireValue = 0;
|
|
||||||
|
|
||||||
// Setup DVP sync object and import it
|
|
||||||
DVPSyncObjectDesc syncObjectDesc;
|
|
||||||
syncObjectDesc.externalClientWaitFunc = NULL;
|
|
||||||
syncObjectDesc.sem = (uint32_t*)mSem;
|
|
||||||
|
|
||||||
DVP_CHECK(dvpImportSyncObject(&syncObjectDesc, &mDvpSync));
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncInfo::~SyncInfo()
|
|
||||||
{
|
|
||||||
DVP_CHECK(dvpFreeSyncObject(mDvpSync));
|
|
||||||
_aligned_free((void*)mSem);
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoFrameTransfer::VideoFrameTransfer(unsigned long memSize, void* address, Direction direction) :
|
|
||||||
mBuffer(address),
|
|
||||||
mMemSize(memSize),
|
|
||||||
mDirection(direction),
|
|
||||||
mExtSync(NULL),
|
|
||||||
mGpuSync(NULL),
|
|
||||||
mDvpSysMemHandle(0),
|
|
||||||
mBufferHandle(0)
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// Pin the memory
|
|
||||||
if (! VirtualLock(mBuffer, mMemSize))
|
|
||||||
throw std::runtime_error("Error pinning memory with VirtualLock");
|
|
||||||
|
|
||||||
// Create necessary sysmem and gpu sync objects
|
|
||||||
mExtSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
|
||||||
mGpuSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
|
||||||
|
|
||||||
// Register system memory buffers with DVP
|
|
||||||
DVPSysmemBufferDesc sysMemBuffersDesc;
|
|
||||||
sysMemBuffersDesc.width = mWidth;
|
|
||||||
sysMemBuffersDesc.height = mHeight;
|
|
||||||
sysMemBuffersDesc.stride = mWidth * 4;
|
|
||||||
sysMemBuffersDesc.format = DVP_BGRA;
|
|
||||||
sysMemBuffersDesc.type = DVP_UNSIGNED_BYTE;
|
|
||||||
sysMemBuffersDesc.size = mMemSize;
|
|
||||||
sysMemBuffersDesc.bufAddr = mBuffer;
|
|
||||||
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
// A UYVY 4:2:2 frame is transferred to the GPU, rather than RGB 4:4:4, so width is halved
|
|
||||||
sysMemBuffersDesc.width /= 2;
|
|
||||||
sysMemBuffersDesc.stride /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
DVP_CHECK(dvpCreateBuffer(&sysMemBuffersDesc, &mDvpSysMemHandle));
|
|
||||||
DVP_CHECK(dvpBindToGLCtx(mDvpSysMemHandle));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Create an OpenGL buffer handle to use for pinned memory
|
|
||||||
GLuint bufferHandle;
|
|
||||||
glGenBuffers(1, &bufferHandle);
|
|
||||||
|
|
||||||
// Pin memory by binding buffer to special AMD target.
|
|
||||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, bufferHandle);
|
|
||||||
|
|
||||||
// glBufferData() sets up the address so any OpenGL operation on this buffer will use system memory directly
|
|
||||||
// (assumes address is aligned to 4k boundary).
|
|
||||||
glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, mMemSize, address, GL_STREAM_DRAW);
|
|
||||||
GLenum result = glGetError();
|
|
||||||
if (result != GL_NO_ERROR)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Error pinning memory with glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, ...)");
|
|
||||||
}
|
|
||||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind buffer to target
|
|
||||||
|
|
||||||
mBufferHandle = bufferHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoFrameTransfer::~VideoFrameTransfer()
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
DVP_CHECK(dvpUnbindFromGLCtx(mDvpSysMemHandle));
|
|
||||||
DVP_CHECK(dvpDestroyBuffer(mDvpSysMemHandle));
|
|
||||||
|
|
||||||
delete mExtSync;
|
|
||||||
delete mGpuSync;
|
|
||||||
|
|
||||||
VirtualUnlock(mBuffer, mMemSize);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The buffer is un-pinned by the GPU when the buffer is deleted
|
|
||||||
glDeleteBuffers(1, &mBufferHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoFrameTransfer::performFrameTransfer()
|
|
||||||
{
|
|
||||||
if (mUseDvp)
|
|
||||||
{
|
|
||||||
// NVIDIA DVP transfers
|
|
||||||
DVPStatus status;
|
|
||||||
|
|
||||||
mGpuSync->mReleaseValue++;
|
|
||||||
|
|
||||||
dvpBegin();
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
// Copy from system memory to GPU texture
|
|
||||||
dvpMapBufferWaitDVP(mDvpCaptureTextureHandle);
|
|
||||||
status = dvpMemcpyLined( mDvpSysMemHandle, mExtSync->mDvpSync, mExtSync->mAcquireValue, DVP_TIMEOUT_IGNORED,
|
|
||||||
mDvpCaptureTextureHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
|
||||||
dvpMapBufferEndDVP(mDvpCaptureTextureHandle);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Copy from GPU texture to system memory
|
|
||||||
dvpMapBufferWaitDVP(mDvpPlaybackTextureHandle);
|
|
||||||
status = dvpMemcpyLined( mDvpPlaybackTextureHandle, mExtSync->mDvpSync, mExtSync->mReleaseValue, DVP_TIMEOUT_IGNORED,
|
|
||||||
mDvpSysMemHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
|
||||||
dvpMapBufferEndDVP(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
dvpEnd();
|
|
||||||
|
|
||||||
return (status == DVP_STATUS_OK);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// AMD pinned memory transfers
|
|
||||||
if (mDirection == CPUtoGPU)
|
|
||||||
{
|
|
||||||
glEnable(GL_TEXTURE_2D);
|
|
||||||
|
|
||||||
// Use a pinned buffer for the GL_PIXEL_UNPACK_BUFFER target
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mBufferHandle);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
|
||||||
|
|
||||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth/2, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
|
||||||
|
|
||||||
// Ensure pinned texture has been transferred to GPU before we draw with it
|
|
||||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
|
||||||
glDeleteSync(fence);
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
|
||||||
glDisable(GL_TEXTURE_2D);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Use a PIXEL PACK BUFFER to read back pixels
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, mBufferHandle);
|
|
||||||
glReadPixels(0, 0, mWidth, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
|
||||||
|
|
||||||
// Ensure GPU has processed all commands in the pipeline up to this point, before memory is read by the CPU
|
|
||||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
|
||||||
glDeleteSync(fence);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (glGetError() == GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::waitForTransferComplete()
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Block until buffer has completely transferred between GPU and CPU buffer
|
|
||||||
dvpBegin();
|
|
||||||
dvpSyncObjClientWaitComplete(mGpuSync->mDvpSync, DVP_TIMEOUT_IGNORED);
|
|
||||||
dvpEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::beginTextureInUse(Direction direction)
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (direction == CPUtoGPU)
|
|
||||||
dvpMapBufferWaitAPI(mDvpCaptureTextureHandle);
|
|
||||||
else
|
|
||||||
dvpMapBufferWaitAPI(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoFrameTransfer::endTextureInUse(Direction direction)
|
|
||||||
{
|
|
||||||
if (!mUseDvp)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (direction == CPUtoGPU)
|
|
||||||
dvpMapBufferEndAPI(mDvpCaptureTextureHandle);
|
|
||||||
else
|
|
||||||
dvpMapBufferEndAPI(mDvpPlaybackTextureHandle);
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/* -LICENSE-START-
|
|
||||||
** Copyright (c) 2012 Blackmagic Design
|
|
||||||
**
|
|
||||||
** Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
** obtaining a copy of the software and accompanying documentation (the
|
|
||||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
|
||||||
** and transmit the Software, and to prepare derivative works of the Software,
|
|
||||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
|
||||||
** accordance with:
|
|
||||||
**
|
|
||||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
|
||||||
** Agreement for the Software Development Kit ("EULA") available at
|
|
||||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
|
||||||
**
|
|
||||||
** (2) if the Software is obtained from any third party, such licensing terms
|
|
||||||
** as notified by that third party,
|
|
||||||
**
|
|
||||||
** and all subject to the following:
|
|
||||||
**
|
|
||||||
** (3) the copyright notices in the Software and this entire statement,
|
|
||||||
** including the above license grant, this restriction and the following
|
|
||||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
|
||||||
** part, and all derivative works of the Software, unless such copies or
|
|
||||||
** derivative works are solely in the form of machine-executable object code
|
|
||||||
** generated by a source language processor.
|
|
||||||
**
|
|
||||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
** DEALINGS IN THE SOFTWARE.
|
|
||||||
**
|
|
||||||
** A copy of the Software is available free of charge at
|
|
||||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
|
||||||
**
|
|
||||||
** -LICENSE-END-
|
|
||||||
*/
|
|
||||||
#ifndef __VIDEO_FRAME_TRANSFER_H__
|
|
||||||
#define __VIDEO_FRAME_TRANSFER_H__
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
// NVIDIA GPU Direct For Video with OpenGL requires the following two headers.
|
|
||||||
// See the NVIDIA website to check if your graphics card is supported.
|
|
||||||
#include <DVPAPI.h>
|
|
||||||
#include <dvpapi_gl.h>
|
|
||||||
|
|
||||||
struct SyncInfo;
|
|
||||||
|
|
||||||
|
|
||||||
// Class for performing efficient frame memory transfers between the CPU and GPU,
|
|
||||||
// using NVIDIA and AMD extensions.
|
|
||||||
class VideoFrameTransfer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
enum Direction
|
|
||||||
{
|
|
||||||
CPUtoGPU,
|
|
||||||
GPUtoCPU
|
|
||||||
};
|
|
||||||
|
|
||||||
VideoFrameTransfer(unsigned long memSize, void* address, Direction direction);
|
|
||||||
~VideoFrameTransfer();
|
|
||||||
|
|
||||||
static bool checkFastMemoryTransferAvailable();
|
|
||||||
static bool initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture);
|
|
||||||
static void beginTextureInUse(Direction direction);
|
|
||||||
static void endTextureInUse(Direction direction);
|
|
||||||
|
|
||||||
bool performFrameTransfer();
|
|
||||||
void waitForTransferComplete();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static bool isNvidiaDvpAvailable();
|
|
||||||
static bool isAMDPinnedMemoryAvailable();
|
|
||||||
static bool initializeMemoryLocking(unsigned memSize);
|
|
||||||
|
|
||||||
void* mBuffer;
|
|
||||||
unsigned long mMemSize;
|
|
||||||
Direction mDirection;
|
|
||||||
static bool mInitialized;
|
|
||||||
static bool mUseDvp;
|
|
||||||
static unsigned mWidth;
|
|
||||||
static unsigned mHeight;
|
|
||||||
static GLuint mCaptureTexture;
|
|
||||||
|
|
||||||
// NVIDIA GPU Direct for Video support
|
|
||||||
SyncInfo* mExtSync;
|
|
||||||
SyncInfo* mGpuSync;
|
|
||||||
DVPBufferHandle mDvpSysMemHandle;
|
|
||||||
|
|
||||||
static DVPBufferHandle mDvpCaptureTextureHandle;
|
|
||||||
static DVPBufferHandle mDvpPlaybackTextureHandle;
|
|
||||||
static uint32_t mBufferAddrAlignment;
|
|
||||||
static uint32_t mBufferGpuStrideAlignment;
|
|
||||||
static uint32_t mSemaphoreAddrAlignment;
|
|
||||||
static uint32_t mSemaphoreAllocSize;
|
|
||||||
static uint32_t mSemaphorePayloadOffset;
|
|
||||||
static uint32_t mSemaphorePayloadSize;
|
|
||||||
|
|
||||||
// GPU buffer bound to the target GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD for pinned memory
|
|
||||||
GLuint mBufferHandle;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -433,6 +433,11 @@ bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& ro
|
|||||||
{
|
{
|
||||||
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
|
||||||
|
{
|
||||||
|
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ public:
|
|||||||
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
||||||
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
||||||
std::function<bool(std::string&)> reloadShader;
|
std::function<bool(std::string&)> reloadShader;
|
||||||
|
std::function<bool(std::string&)> requestScreenshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
ControlServer();
|
ControlServer();
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "RuntimeControlBridge.h"
|
||||||
|
|
||||||
|
#include "ControlServer.h"
|
||||||
|
#include "OpenGLComposite.h"
|
||||||
|
#include "OscServer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
bool StartRuntimeControlServices(
|
||||||
|
OpenGLComposite& composite,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
ControlServer& controlServer,
|
||||||
|
OscServer& oscServer,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
ControlServer::Callbacks callbacks;
|
||||||
|
callbacks.getStateJson = [&composite]() { return composite.GetRuntimeStateJson(); };
|
||||||
|
callbacks.addLayer = [&composite](const std::string& shaderId, std::string& actionError) { return composite.AddLayer(shaderId, actionError); };
|
||||||
|
callbacks.removeLayer = [&composite](const std::string& layerId, std::string& actionError) { return composite.RemoveLayer(layerId, actionError); };
|
||||||
|
callbacks.moveLayer = [&composite](const std::string& layerId, int direction, std::string& actionError) { return composite.MoveLayer(layerId, direction, actionError); };
|
||||||
|
callbacks.moveLayerToIndex = [&composite](const std::string& layerId, std::size_t targetIndex, std::string& actionError) { return composite.MoveLayerToIndex(layerId, targetIndex, actionError); };
|
||||||
|
callbacks.setLayerBypass = [&composite](const std::string& layerId, bool bypassed, std::string& actionError) { return composite.SetLayerBypass(layerId, bypassed, actionError); };
|
||||||
|
callbacks.setLayerShader = [&composite](const std::string& layerId, const std::string& shaderId, std::string& actionError) { return composite.SetLayerShader(layerId, shaderId, actionError); };
|
||||||
|
callbacks.updateLayerParameter = [&composite](const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& actionError) {
|
||||||
|
return composite.UpdateLayerParameterJson(layerId, parameterId, valueJson, actionError);
|
||||||
|
};
|
||||||
|
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
|
||||||
|
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
|
||||||
|
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
|
||||||
|
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
|
||||||
|
callbacks.reloadShader = [&composite](std::string& actionError) {
|
||||||
|
if (!composite.ReloadShader())
|
||||||
|
{
|
||||||
|
actionError = "Shader reload failed. See native app status for details.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error))
|
||||||
|
return false;
|
||||||
|
runtimeHost.SetServerPort(controlServer.GetPort());
|
||||||
|
|
||||||
|
OscServer::Callbacks oscCallbacks;
|
||||||
|
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
||||||
|
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
|
||||||
|
};
|
||||||
|
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscPort(), oscCallbacks, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class ControlServer;
|
||||||
|
class OpenGLComposite;
|
||||||
|
class OscServer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
bool StartRuntimeControlServices(
|
||||||
|
OpenGLComposite& composite,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
ControlServer& controlServer,
|
||||||
|
OscServer& oscServer,
|
||||||
|
std::string& error);
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include "RuntimeServices.h"
|
||||||
|
|
||||||
|
#include "ControlServer.h"
|
||||||
|
#include "OscServer.h"
|
||||||
|
#include "RuntimeControlBridge.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
RuntimeServices::RuntimeServices() :
|
||||||
|
mControlServer(std::make_unique<ControlServer>()),
|
||||||
|
mOscServer(std::make_unique<OscServer>()),
|
||||||
|
mPollRunning(false),
|
||||||
|
mRegistryChanged(false),
|
||||||
|
mReloadRequested(false),
|
||||||
|
mPollFailed(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeServices::~RuntimeServices()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
StartPolling(runtimeHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::Stop()
|
||||||
|
{
|
||||||
|
StopPolling();
|
||||||
|
|
||||||
|
if (mOscServer)
|
||||||
|
mOscServer->Stop();
|
||||||
|
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::BroadcastState()
|
||||||
|
{
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->BroadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
||||||
|
{
|
||||||
|
RuntimePollEvents events;
|
||||||
|
events.registryChanged = mRegistryChanged.exchange(false);
|
||||||
|
events.reloadRequested = mReloadRequested.exchange(false);
|
||||||
|
events.failed = mPollFailed.exchange(false);
|
||||||
|
|
||||||
|
if (events.failed)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||||
|
events.error = mPollError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::StartPolling(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
if (mPollRunning.exchange(true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::StopPolling()
|
||||||
|
{
|
||||||
|
if (!mPollRunning.exchange(false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mPollThread.joinable())
|
||||||
|
mPollThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
|
||||||
|
{
|
||||||
|
while (mPollRunning)
|
||||||
|
{
|
||||||
|
bool registryChanged = false;
|
||||||
|
bool reloadRequested = false;
|
||||||
|
std::string runtimeError;
|
||||||
|
if (!runtimeHost.PollFileChanges(registryChanged, reloadRequested, runtimeError))
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||||
|
mPollError = runtimeError;
|
||||||
|
}
|
||||||
|
mPollFailed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (registryChanged)
|
||||||
|
mRegistryChanged = true;
|
||||||
|
if (reloadRequested)
|
||||||
|
mReloadRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 25 && mPollRunning; ++i)
|
||||||
|
Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
class ControlServer;
|
||||||
|
class OpenGLComposite;
|
||||||
|
class OscServer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
struct RuntimePollEvents
|
||||||
|
{
|
||||||
|
bool registryChanged = false;
|
||||||
|
bool reloadRequested = false;
|
||||||
|
bool failed = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeServices
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeServices();
|
||||||
|
~RuntimeServices();
|
||||||
|
|
||||||
|
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
||||||
|
void BeginPolling(RuntimeHost& runtimeHost);
|
||||||
|
void Stop();
|
||||||
|
void BroadcastState();
|
||||||
|
RuntimePollEvents ConsumePollEvents();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void StartPolling(RuntimeHost& runtimeHost);
|
||||||
|
void StopPolling();
|
||||||
|
void PollLoop(RuntimeHost& runtimeHost);
|
||||||
|
|
||||||
|
std::unique_ptr<ControlServer> mControlServer;
|
||||||
|
std::unique_ptr<OscServer> mOscServer;
|
||||||
|
std::thread mPollThread;
|
||||||
|
std::atomic<bool> mPollRunning;
|
||||||
|
std::atomic<bool> mRegistryChanged;
|
||||||
|
std::atomic<bool> mReloadRequested;
|
||||||
|
std::atomic<bool> mPollFailed;
|
||||||
|
std::mutex mPollErrorMutex;
|
||||||
|
std::string mPollError;
|
||||||
|
};
|
||||||
@@ -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 "OpenGLComposite.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// DeckLink Capture Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
CaptureDelegate::CaptureDelegate(OpenGLComposite* 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->VideoFrameArrived(inputFrame, hasNoInputSource);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// DeckLink Playout Delegate Class
|
||||||
|
////////////////////////////////////////////
|
||||||
|
PlayoutDelegate::PlayoutDelegate(OpenGLComposite* 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->PlayoutFrameCompleted(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 OpenGLComposite;
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// 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();
|
||||||
|
};
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
#include "DeckLinkSession.h"
|
||||||
|
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <new>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string BstrToUtf8(BSTR value)
|
||||||
|
{
|
||||||
|
if (value == nullptr)
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
|
||||||
|
if (requiredBytes <= 1)
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
|
||||||
|
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
return std::string(utf8Name.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
if (input == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
BOOL supported = FALSE;
|
||||||
|
BMDDisplayMode actualMode = bmdModeUnknown;
|
||||||
|
const HRESULT result = input->DoesSupportVideoMode(
|
||||||
|
bmdVideoConnectionUnspecified,
|
||||||
|
displayMode,
|
||||||
|
pixelFormat,
|
||||||
|
bmdNoVideoInputConversion,
|
||||||
|
bmdSupportedVideoModeDefault,
|
||||||
|
&actualMode,
|
||||||
|
&supported);
|
||||||
|
return result == S_OK && supported != FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
BOOL supported = FALSE;
|
||||||
|
BMDDisplayMode actualMode = bmdModeUnknown;
|
||||||
|
const HRESULT result = output->DoesSupportVideoMode(
|
||||||
|
bmdVideoConnectionUnspecified,
|
||||||
|
displayMode,
|
||||||
|
pixelFormat,
|
||||||
|
bmdNoVideoOutputConversion,
|
||||||
|
bmdSupportedVideoModeDefault,
|
||||||
|
&actualMode,
|
||||||
|
&supported);
|
||||||
|
return result == S_OK && supported != FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkSession::~DeckLinkSession()
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::ReleaseResources()
|
||||||
|
{
|
||||||
|
if (input != nullptr)
|
||||||
|
input->SetCallback(nullptr);
|
||||||
|
captureDelegate.Release();
|
||||||
|
input.Release();
|
||||||
|
|
||||||
|
if (output != nullptr)
|
||||||
|
output->SetScheduledFrameCompletionCallback(nullptr);
|
||||||
|
|
||||||
|
if (keyer != nullptr)
|
||||||
|
{
|
||||||
|
keyer->Disable();
|
||||||
|
externalKeyingActive = false;
|
||||||
|
}
|
||||||
|
keyer.Release();
|
||||||
|
|
||||||
|
playoutDelegate.Release();
|
||||||
|
outputVideoFrameQueue.clear();
|
||||||
|
output.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
||||||
|
CComPtr<IDeckLinkDisplayMode> inputMode;
|
||||||
|
CComPtr<IDeckLinkDisplayMode> outputMode;
|
||||||
|
|
||||||
|
inputDisplayModeName = videoModes.input.displayName;
|
||||||
|
outputDisplayModeName = videoModes.output.displayName;
|
||||||
|
|
||||||
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IDeckLink> deckLink;
|
||||||
|
while (deckLinkIterator->Next(&deckLink) == S_OK)
|
||||||
|
{
|
||||||
|
int64_t duplexMode;
|
||||||
|
bool deviceSupportsInternalKeying = false;
|
||||||
|
bool deviceSupportsExternalKeying = false;
|
||||||
|
std::string modelName;
|
||||||
|
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
|
||||||
|
|
||||||
|
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
|
||||||
|
{
|
||||||
|
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
|
||||||
|
deckLink.Release();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
|
||||||
|
BOOL attributeFlag = FALSE;
|
||||||
|
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
|
||||||
|
deviceSupportsInternalKeying = (attributeFlag != FALSE);
|
||||||
|
attributeFlag = FALSE;
|
||||||
|
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
|
||||||
|
deviceSupportsExternalKeying = (attributeFlag != FALSE);
|
||||||
|
CComBSTR modelNameBstr;
|
||||||
|
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
|
||||||
|
modelName = BstrToUtf8(modelNameBstr);
|
||||||
|
|
||||||
|
if (result != S_OK || duplexMode == bmdDuplexInactive)
|
||||||
|
{
|
||||||
|
deckLink.Release();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inputUsed = false;
|
||||||
|
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
|
||||||
|
inputUsed = true;
|
||||||
|
|
||||||
|
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
|
||||||
|
{
|
||||||
|
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
|
||||||
|
output.Release();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputModelName = modelName;
|
||||||
|
supportsInternalKeying = deviceSupportsInternalKeying;
|
||||||
|
supportsExternalKeying = deviceSupportsExternalKeying;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deckLink.Release();
|
||||||
|
|
||||||
|
if (output && input)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output)
|
||||||
|
{
|
||||||
|
error = "Expected an Output DeckLink device";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
|
||||||
|
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
|
||||||
|
{
|
||||||
|
error = "Cannot get input Display Mode Iterator.";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
|
||||||
|
{
|
||||||
|
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inputDisplayModeIterator.Release();
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
|
||||||
|
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
|
||||||
|
{
|
||||||
|
error = "Cannot get output Display Mode Iterator.";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
|
||||||
|
{
|
||||||
|
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
||||||
|
inputFrameSize = inputMode
|
||||||
|
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
|
||||||
|
: outputFrameSize;
|
||||||
|
if (!input)
|
||||||
|
inputDisplayModeName = "No input - black frame";
|
||||||
|
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
||||||
|
|
||||||
|
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||||
|
outputFrameRowBytes = outputFrameSize.width * 4u;
|
||||||
|
captureTextureWidth = inputFrameSize.width / 2u;
|
||||||
|
outputPackTextureWidth = outputFrameSize.width;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error)
|
||||||
|
{
|
||||||
|
if (!output)
|
||||||
|
{
|
||||||
|
error = "Expected an Output DeckLink device";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatStatusMessage.clear();
|
||||||
|
|
||||||
|
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
|
||||||
|
inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
||||||
|
if (input != nullptr && !inputTenBitSupported)
|
||||||
|
formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
||||||
|
|
||||||
|
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
|
||||||
|
outputPixelFormat = ChoosePreferredVideoIOFormat(outputTenBitSupported);
|
||||||
|
if (!outputTenBitSupported)
|
||||||
|
formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
|
||||||
|
|
||||||
|
int deckLinkOutputRowBytes = 0;
|
||||||
|
if (output->RowBytesForPixelFormat(OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA, outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink output setup failed while calculating output row bytes.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
|
||||||
|
outputPackTextureWidth = OutputIsTenBit()
|
||||||
|
? PackedTextureWidthFromRowBytes(outputFrameRowBytes)
|
||||||
|
: outputFrameSize.width;
|
||||||
|
|
||||||
|
if (InputIsTenBit())
|
||||||
|
{
|
||||||
|
int deckLinkInputRowBytes = 0;
|
||||||
|
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
|
||||||
|
inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
|
||||||
|
else
|
||||||
|
inputFrameRowBytes = MinimumV210RowBytes(inputFrameSize.width);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||||
|
}
|
||||||
|
captureTextureWidth = InputIsTenBit()
|
||||||
|
? PackedTextureWidthFromRowBytes(inputFrameRowBytes)
|
||||||
|
: inputFrameSize.width / 2u;
|
||||||
|
|
||||||
|
std::ostringstream status;
|
||||||
|
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(inputPixelFormat) : "none")
|
||||||
|
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") << ".";
|
||||||
|
if (!formatStatusMessage.empty())
|
||||||
|
status << " " << formatStatusMessage;
|
||||||
|
formatStatusMessage = status.str();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error)
|
||||||
|
{
|
||||||
|
(void)hdc;
|
||||||
|
(void)hglrc;
|
||||||
|
|
||||||
|
if (!input)
|
||||||
|
{
|
||||||
|
hasNoInputSource = true;
|
||||||
|
inputDisplayModeName = "No input - black frame";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(inputPixelFormat);
|
||||||
|
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||||
|
{
|
||||||
|
if (inputPixelFormat == VideoIOPixelFormat::V210)
|
||||||
|
{
|
||||||
|
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
|
||||||
|
inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
inputFrameRowBytes = inputFrameSize.width * 2u;
|
||||||
|
captureTextureWidth = inputFrameSize.width / 2u;
|
||||||
|
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
||||||
|
{
|
||||||
|
std::ostringstream status;
|
||||||
|
status << "DeckLink formats: capture " << VideoIOPixelFormatName(inputPixelFormat)
|
||||||
|
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA")
|
||||||
|
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
|
||||||
|
formatStatusMessage = status.str();
|
||||||
|
goto input_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
|
||||||
|
input.Release();
|
||||||
|
hasNoInputSource = true;
|
||||||
|
inputDisplayModeName = "No input - black frame";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
input_enabled:
|
||||||
|
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(owner));
|
||||||
|
if (captureDelegate == nullptr)
|
||||||
|
{
|
||||||
|
error = "DeckLink input setup failed while creating the capture callback.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (input->SetCallback(captureDelegate) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink input setup failed while installing the capture callback.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||||
|
{
|
||||||
|
(void)hdc;
|
||||||
|
(void)hglrc;
|
||||||
|
|
||||||
|
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink output setup failed while enabling video output.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
|
||||||
|
keyerInterfaceAvailable = true;
|
||||||
|
|
||||||
|
if (externalKeyingEnabled)
|
||||||
|
{
|
||||||
|
if (!supportsExternalKeying)
|
||||||
|
{
|
||||||
|
statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
||||||
|
}
|
||||||
|
else if (!keyerInterfaceAvailable)
|
||||||
|
{
|
||||||
|
statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
||||||
|
}
|
||||||
|
else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
|
||||||
|
{
|
||||||
|
statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
externalKeyingActive = true;
|
||||||
|
statusMessage = "External keying is active on the selected DeckLink output.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (supportsExternalKeying)
|
||||||
|
{
|
||||||
|
statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
|
||||||
|
|
||||||
|
const BMDPixelFormat deckLinkOutputPixelFormat = OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA;
|
||||||
|
if (output->CreateVideoFrame(outputFrameSize.width, outputFrameSize.height, outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink output setup failed while creating an output video frame.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputVideoFrameQueue.push_back(outputFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(owner));
|
||||||
|
if (playoutDelegate == nullptr)
|
||||||
|
{
|
||||||
|
error = "DeckLink output setup failed while creating the playout callback.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formatStatusMessage.empty())
|
||||||
|
statusMessage = statusMessage.empty() ? formatStatusMessage : formatStatusMessage + " " + statusMessage;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double DeckLinkSession::FrameBudgetMilliseconds() const
|
||||||
|
{
|
||||||
|
return frameTimescale != 0
|
||||||
|
? (static_cast<double>(frameDuration) * 1000.0) / static_cast<double>(frameTimescale)
|
||||||
|
: 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkMutableVideoFrame* DeckLinkSession::RotateOutputFrame()
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
|
||||||
|
outputVideoFrameQueue.push_back(outputVideoFrame);
|
||||||
|
outputVideoFrameQueue.pop_front();
|
||||||
|
return outputVideoFrame.p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult)
|
||||||
|
{
|
||||||
|
if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped)
|
||||||
|
totalPlayoutFrames += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
|
{
|
||||||
|
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
totalPlayoutFrames++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::Start()
|
||||||
|
{
|
||||||
|
totalPlayoutFrames = 0;
|
||||||
|
if (!output)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (outputVideoFrameQueue.empty())
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < kPrerollFrameCount; i++)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
|
||||||
|
outputVideoFrameQueue.push_back(outputVideoFrame);
|
||||||
|
outputVideoFrameQueue.pop_front();
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
|
||||||
|
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* pFrame;
|
||||||
|
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
|
||||||
|
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * outputFrameSize.height);
|
||||||
|
|
||||||
|
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
||||||
|
|
||||||
|
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPlayoutFrames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input)
|
||||||
|
{
|
||||||
|
if (input->StartStreams() != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (output->StartScheduledPlayback(0, frameTimescale, 1.0) != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::Stop()
|
||||||
|
{
|
||||||
|
if (keyer != nullptr)
|
||||||
|
{
|
||||||
|
keyer->Disable();
|
||||||
|
externalKeyingActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input)
|
||||||
|
{
|
||||||
|
input->StopStreams();
|
||||||
|
input->DisableVideoInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output)
|
||||||
|
{
|
||||||
|
output->StopScheduledPlayback(0, NULL, 0);
|
||||||
|
output->DisableVideoOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "DeckLinkFrameTransfer.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <atlbase.h>
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class OpenGLComposite;
|
||||||
|
|
||||||
|
class DeckLinkSession
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkSession() = default;
|
||||||
|
~DeckLinkSession();
|
||||||
|
|
||||||
|
void ReleaseResources();
|
||||||
|
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
||||||
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error);
|
||||||
|
bool ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error);
|
||||||
|
bool ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
|
||||||
|
bool Start();
|
||||||
|
bool Stop();
|
||||||
|
|
||||||
|
bool HasInputDevice() const { return input != nullptr; }
|
||||||
|
bool HasInputSource() const { return !hasNoInputSource; }
|
||||||
|
void SetInputSourceMissing(bool missing) { hasNoInputSource = missing; }
|
||||||
|
bool InputOutputDimensionsDiffer() const { return inputFrameSize != outputFrameSize; }
|
||||||
|
const FrameSize& InputFrameSize() const { return inputFrameSize; }
|
||||||
|
const FrameSize& OutputFrameSize() const { return outputFrameSize; }
|
||||||
|
unsigned InputFrameWidth() const { return inputFrameSize.width; }
|
||||||
|
unsigned InputFrameHeight() const { return inputFrameSize.height; }
|
||||||
|
unsigned OutputFrameWidth() const { return outputFrameSize.width; }
|
||||||
|
unsigned OutputFrameHeight() const { return outputFrameSize.height; }
|
||||||
|
VideoIOPixelFormat InputPixelFormat() const { return inputPixelFormat; }
|
||||||
|
VideoIOPixelFormat OutputPixelFormat() const { return outputPixelFormat; }
|
||||||
|
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(inputPixelFormat); }
|
||||||
|
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(outputPixelFormat); }
|
||||||
|
unsigned InputFrameRowBytes() const { return inputFrameRowBytes; }
|
||||||
|
unsigned OutputFrameRowBytes() const { return outputFrameRowBytes; }
|
||||||
|
unsigned CaptureTextureWidth() const { return captureTextureWidth; }
|
||||||
|
unsigned OutputPackTextureWidth() const { return outputPackTextureWidth; }
|
||||||
|
const std::string& FormatStatusMessage() const { return formatStatusMessage; }
|
||||||
|
const std::string& InputDisplayModeName() const { return inputDisplayModeName; }
|
||||||
|
const std::string& OutputModelName() const { return outputModelName; }
|
||||||
|
bool SupportsInternalKeying() const { return supportsInternalKeying; }
|
||||||
|
bool SupportsExternalKeying() const { return supportsExternalKeying; }
|
||||||
|
bool KeyerInterfaceAvailable() const { return keyerInterfaceAvailable; }
|
||||||
|
bool ExternalKeyingActive() const { return externalKeyingActive; }
|
||||||
|
const std::string& StatusMessage() const { return statusMessage; }
|
||||||
|
void SetStatusMessage(const std::string& message) { statusMessage = message; }
|
||||||
|
double FrameBudgetMilliseconds() const;
|
||||||
|
IDeckLinkMutableVideoFrame* RotateOutputFrame();
|
||||||
|
void AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
||||||
|
bool ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
|
||||||
|
private:
|
||||||
|
CComPtr<CaptureDelegate> captureDelegate;
|
||||||
|
CComPtr<PlayoutDelegate> playoutDelegate;
|
||||||
|
CComPtr<IDeckLinkInput> input;
|
||||||
|
CComPtr<IDeckLinkOutput> output;
|
||||||
|
CComPtr<IDeckLinkKeyer> keyer;
|
||||||
|
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||||
|
BMDTimeValue frameDuration = 0;
|
||||||
|
BMDTimeScale frameTimescale = 0;
|
||||||
|
unsigned totalPlayoutFrames = 0;
|
||||||
|
FrameSize inputFrameSize;
|
||||||
|
FrameSize outputFrameSize;
|
||||||
|
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
unsigned inputFrameRowBytes = 0;
|
||||||
|
unsigned outputFrameRowBytes = 0;
|
||||||
|
unsigned captureTextureWidth = 0;
|
||||||
|
unsigned outputPackTextureWidth = 0;
|
||||||
|
std::string inputDisplayModeName = "1080p59.94";
|
||||||
|
std::string outputDisplayModeName = "1080p59.94";
|
||||||
|
bool hasNoInputSource = true;
|
||||||
|
std::string outputModelName;
|
||||||
|
bool supportsInternalKeying = false;
|
||||||
|
bool supportsExternalKeying = false;
|
||||||
|
bool keyerInterfaceAvailable = false;
|
||||||
|
bool externalKeyingActive = false;
|
||||||
|
std::string statusMessage;
|
||||||
|
std::string formatStatusMessage;
|
||||||
|
};
|
||||||
139
apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.cpp
Normal file
139
apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.cpp
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#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)
|
||||||
|
{
|
||||||
|
return format == VideoIOPixelFormat::V210 ? "10-bit YUV v210" : "8-bit YUV UYVY";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
return format == VideoIOPixelFormat::V210;
|
||||||
|
}
|
||||||
|
|
||||||
|
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
|
||||||
|
{
|
||||||
|
return format == VideoIOPixelFormat::V210 ? bmdFormat10BitYUV : bmdFormat8BitYUV;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
|
||||||
|
{
|
||||||
|
return format == bmdFormat10BitYUV ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
|
||||||
|
{
|
||||||
|
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class VideoIOPixelFormat
|
||||||
|
{
|
||||||
|
Uyvy8,
|
||||||
|
V210
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
|
||||||
|
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
|
||||||
|
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
|
||||||
|
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);
|
||||||
@@ -60,13 +60,19 @@
|
|||||||
#define GL_DYNAMIC_DRAW 0x88E8
|
#define GL_DYNAMIC_DRAW 0x88E8
|
||||||
#define GL_UNIFORM_BUFFER 0x8A11
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
#define GL_RGBA8 0x8058
|
#define GL_RGBA8 0x8058
|
||||||
|
#define GL_RGBA16F 0x881A
|
||||||
|
#define GL_TEXTURE0 0x84C0
|
||||||
|
#define GL_ACTIVE_TEXTURE 0x84E0
|
||||||
#define GL_ARRAY_BUFFER 0x8892
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
#define GL_PIXEL_PACK_BUFFER 0x88EB
|
||||||
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
|
||||||
|
#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
|
||||||
#define GL_FRAGMENT_SHADER 0x8B30
|
#define GL_FRAGMENT_SHADER 0x8B30
|
||||||
#define GL_VERTEX_SHADER 0x8B31
|
#define GL_VERTEX_SHADER 0x8B31
|
||||||
#define GL_COMPILE_STATUS 0x8B81
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
#define GL_LINK_STATUS 0x8B82
|
#define GL_LINK_STATUS 0x8B82
|
||||||
|
#define GL_INVALID_INDEX 0xFFFFFFFFu
|
||||||
|
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
|
||||||
#define GL_RENDERBUFFER_EXT 0x8D41
|
#define GL_RENDERBUFFER_EXT 0x8D41
|
||||||
#define GL_FRAMEBUFFER_EXT 0x8D40
|
#define GL_FRAMEBUFFER_EXT 0x8D40
|
||||||
#define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5
|
#define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
||||||
|
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||||
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
|
constexpr unsigned kPrerollFrameCount = 8;
|
||||||
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;
|
||||||
|
};
|
||||||
479
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
479
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
|
#include "OpenGLComposite.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "GlRenderConstants.h"
|
||||||
|
#include "OpenGLDeckLinkBridge.h"
|
||||||
|
#include "OpenGLRenderPass.h"
|
||||||
|
#include "OpenGLShaderPrograms.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),
|
||||||
|
mDeckLink(std::make_unique<DeckLinkSession>()),
|
||||||
|
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||||
|
mUseCommittedLayerStates(false),
|
||||||
|
mScreenshotRequested(false)
|
||||||
|
{
|
||||||
|
InitializeCriticalSection(&pMutex);
|
||||||
|
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||||
|
mDeckLinkBridge = std::make_unique<OpenGLDeckLinkBridge>(
|
||||||
|
*mDeckLink,
|
||||||
|
*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();
|
||||||
|
mDeckLink->ReleaseResources();
|
||||||
|
mRenderer->DestroyResources();
|
||||||
|
|
||||||
|
DeleteCriticalSection(&pMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::InitDeckLink()
|
||||||
|
{
|
||||||
|
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 (!mDeckLink->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 (!mDeckLink->SelectPreferredFormats(videoModes, initFailureReason))
|
||||||
|
goto error;
|
||||||
|
|
||||||
|
if (! CheckOpenGLExtensions())
|
||||||
|
{
|
||||||
|
initFailureReason = "OpenGL extension checks failed.";
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! InitOpenGLState())
|
||||||
|
{
|
||||||
|
initFailureReason = "OpenGL state initialization failed.";
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishDeckLinkOutputStatus(mDeckLink->OutputModelName().empty()
|
||||||
|
? "DeckLink output device selected."
|
||||||
|
: ("Selected output device: " + mDeckLink->OutputModelName()));
|
||||||
|
|
||||||
|
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||||
|
if (mDeckLink->OutputFrameWidth() < 1920)
|
||||||
|
resizeWindow(mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight());
|
||||||
|
else
|
||||||
|
resizeWindow(mDeckLink->OutputFrameWidth() / 2, mDeckLink->OutputFrameHeight() / 2);
|
||||||
|
|
||||||
|
if (!mDeckLink->ConfigureInput(this, hGLDC, hGLRC, videoModes.input, initFailureReason))
|
||||||
|
{
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
if (!mDeckLink->HasInputDevice() && mRuntimeHost)
|
||||||
|
{
|
||||||
|
mRuntimeHost->SetSignalStatus(false, mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), mDeckLink->InputDisplayModeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mDeckLink->ConfigureOutput(this, hGLDC, hGLRC, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||||
|
{
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishDeckLinkOutputStatus(mDeckLink->StatusMessage());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error:
|
||||||
|
if (!initFailureReason.empty())
|
||||||
|
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
||||||
|
mDeckLink->ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::paintGL()
|
||||||
|
{
|
||||||
|
if (!TryEnterCriticalSection(&pMutex))
|
||||||
|
{
|
||||||
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRenderer->PresentToWindow(hGLDC, mDeckLink->OutputFrameWidth(), mDeckLink->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::PublishDeckLinkOutputStatus(const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!statusMessage.empty())
|
||||||
|
mDeckLink->SetStatusMessage(statusMessage);
|
||||||
|
|
||||||
|
mRuntimeHost->SetDeckLinkOutputStatus(
|
||||||
|
mDeckLink->OutputModelName(),
|
||||||
|
mDeckLink->SupportsInternalKeying(),
|
||||||
|
mDeckLink->SupportsExternalKeying(),
|
||||||
|
mDeckLink->KeyerInterfaceAvailable(),
|
||||||
|
mRuntimeHost->ExternalKeyingEnabled(),
|
||||||
|
mDeckLink->ExternalKeyingActive(),
|
||||||
|
mDeckLink->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(mDeckLink->InputFrameWidth(), mDeckLink->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(
|
||||||
|
mDeckLink->InputFrameWidth(),
|
||||||
|
mDeckLink->InputFrameHeight(),
|
||||||
|
mDeckLink->CaptureTextureWidth(),
|
||||||
|
mDeckLink->OutputFrameWidth(),
|
||||||
|
mDeckLink->OutputFrameHeight(),
|
||||||
|
mDeckLink->OutputPackTextureWidth(),
|
||||||
|
rendererError))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeckLink delegates still target OpenGLComposite; the bridge owns the per-frame work.
|
||||||
|
void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||||
|
{
|
||||||
|
mDeckLinkBridge->VideoFrameArrived(inputFrame, hasNoInputSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||||
|
{
|
||||||
|
mDeckLinkBridge->PlayoutFrameCompleted(completedFrame, completionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::Start()
|
||||||
|
{
|
||||||
|
return mDeckLink->Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::Stop()
|
||||||
|
{
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->Stop();
|
||||||
|
|
||||||
|
const bool wasExternalKeyingActive = mDeckLink->ExternalKeyingActive();
|
||||||
|
mDeckLink->Stop();
|
||||||
|
if (wasExternalKeyingActive)
|
||||||
|
PublishDeckLinkOutputStatus("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 = mDeckLink->HasInputSource();
|
||||||
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
|
if (mUseCommittedLayerStates)
|
||||||
|
{
|
||||||
|
layerStates = mShaderPrograms->CommittedLayerStates();
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
|
}
|
||||||
|
else if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
if (mRuntimeHost->TryGetLayerRenderStates(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), layerStates))
|
||||||
|
{
|
||||||
|
mCachedLayerRenderStates = layerStates;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
layerStates = mCachedLayerRenderStates;
|
||||||
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
|
||||||
|
mRenderPass->Render(
|
||||||
|
hasInputSource,
|
||||||
|
layerStates,
|
||||||
|
mDeckLink->InputFrameWidth(),
|
||||||
|
mDeckLink->InputFrameHeight(),
|
||||||
|
mDeckLink->CaptureTextureWidth(),
|
||||||
|
mDeckLink->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 = mDeckLink ? mDeckLink->OutputFrameWidth() : 0;
|
||||||
|
const unsigned height = mDeckLink ? mDeckLink->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, mDeckLink->InputFrameWidth(), mDeckLink->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 || !mDeckLink)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mUseCommittedLayerStates = true;
|
||||||
|
if (mRuntimeHost)
|
||||||
|
mRuntimeHost->ClearReloadRequest();
|
||||||
|
mShaderBuildQueue->RequestBuild(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::broadcastRuntimeState()
|
||||||
|
{
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->BroadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::resetTemporalHistoryState()
|
||||||
|
{
|
||||||
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::CheckOpenGLExtensions()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
104
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
104
apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#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 "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 DeckLinkSession;
|
||||||
|
class OpenGLDeckLinkBridge;
|
||||||
|
class OpenGLRenderPass;
|
||||||
|
class OpenGLShaderPrograms;
|
||||||
|
class RuntimeServices;
|
||||||
|
class ShaderBuildQueue;
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
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();
|
||||||
|
|
||||||
|
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||||
|
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resizeWindow(int width, int height);
|
||||||
|
bool CheckOpenGLExtensions();
|
||||||
|
void PublishDeckLinkOutputStatus(const std::string& statusMessage);
|
||||||
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
|
||||||
|
HWND hGLWnd;
|
||||||
|
HDC hGLDC;
|
||||||
|
HGLRC hGLRC;
|
||||||
|
CRITICAL_SECTION pMutex;
|
||||||
|
|
||||||
|
std::unique_ptr<DeckLinkSession> mDeckLink;
|
||||||
|
std::unique_ptr<OpenGLRenderer> mRenderer;
|
||||||
|
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||||
|
std::unique_ptr<OpenGLDeckLinkBridge> mDeckLinkBridge;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
#include "OpenGLDeckLinkBridge.h"
|
||||||
|
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
|
#include "OpenGLRenderer.h"
|
||||||
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
|
||||||
|
DeckLinkSession& deckLink,
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
CRITICAL_SECTION& mutex,
|
||||||
|
HDC hdc,
|
||||||
|
HGLRC hglrc,
|
||||||
|
RenderEffectCallback renderEffect,
|
||||||
|
OutputReadyCallback outputReady,
|
||||||
|
PaintCallback paint) :
|
||||||
|
mDeckLink(deckLink),
|
||||||
|
mRenderer(renderer),
|
||||||
|
mRuntimeHost(runtimeHost),
|
||||||
|
mMutex(mutex),
|
||||||
|
mHdc(hdc),
|
||||||
|
mHglrc(hglrc),
|
||||||
|
mRenderEffect(renderEffect),
|
||||||
|
mOutputReady(outputReady),
|
||||||
|
mPaint(paint)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult completionResult)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
||||||
|
{
|
||||||
|
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
||||||
|
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||||
|
else
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
||||||
|
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
||||||
|
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||||
|
}
|
||||||
|
mLastPlayoutCompletionTime = now;
|
||||||
|
|
||||||
|
if (completionResult == bmdOutputFrameDisplayedLate)
|
||||||
|
++mLateFrameCount;
|
||||||
|
else if (completionResult == bmdOutputFrameDropped)
|
||||||
|
++mDroppedFrameCount;
|
||||||
|
else if (completionResult == bmdOutputFrameFlushed)
|
||||||
|
++mFlushedFrameCount;
|
||||||
|
|
||||||
|
mRuntimeHost.TrySetFramePacingStats(
|
||||||
|
mCompletionIntervalMilliseconds,
|
||||||
|
mSmoothedCompletionIntervalMilliseconds,
|
||||||
|
mMaxCompletionIntervalMilliseconds,
|
||||||
|
mLateFrameCount,
|
||||||
|
mDroppedFrameCount,
|
||||||
|
mFlushedFrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
||||||
|
{
|
||||||
|
mDeckLink.SetInputSourceMissing(hasNoInputSource);
|
||||||
|
mRuntimeHost.TrySetSignalStatus(!hasNoInputSource, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), mDeckLink.InputDisplayModeName());
|
||||||
|
|
||||||
|
if (!mDeckLink.HasInputSource())
|
||||||
|
return; // don't transfer texture when there's no input
|
||||||
|
|
||||||
|
long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight();
|
||||||
|
IDeckLinkVideoBuffer* inputFrameBuffer = NULL;
|
||||||
|
void* videoPixels;
|
||||||
|
|
||||||
|
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||||
|
{
|
||||||
|
inputFrameBuffer->Release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inputFrameBuffer->GetBytes(&videoPixels);
|
||||||
|
|
||||||
|
EnterCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
|
||||||
|
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
||||||
|
|
||||||
|
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
|
||||||
|
if (mDeckLink.InputPixelFormat() == VideoIOPixelFormat::V210)
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||||
|
else
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||||
|
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||||
|
inputFrameBuffer->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||||
|
{
|
||||||
|
(void)completedFrame;
|
||||||
|
|
||||||
|
RecordFramePacing(completionResult);
|
||||||
|
|
||||||
|
EnterCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
// Get the first frame from the queue
|
||||||
|
IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink.RotateOutputFrame();
|
||||||
|
|
||||||
|
// make GL context current in this thread
|
||||||
|
wglMakeCurrent(mHdc, mHglrc);
|
||||||
|
|
||||||
|
// Draw the effect output to the off-screen framebuffer.
|
||||||
|
const auto renderStartTime = std::chrono::steady_clock::now();
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||||
|
mRenderEffect();
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
glBlitFramebuffer(0, 0, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), 0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
if (mOutputReady)
|
||||||
|
mOutputReady();
|
||||||
|
if (mDeckLink.OutputIsTenBit())
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
glViewport(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight());
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
|
||||||
|
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||||
|
glUseProgram(mRenderer.OutputPackProgram());
|
||||||
|
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
|
||||||
|
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
|
||||||
|
if (outputResolutionLocation >= 0)
|
||||||
|
glUniform2f(outputResolutionLocation, static_cast<float>(mDeckLink.OutputFrameWidth()), static_cast<float>(mDeckLink.OutputFrameHeight()));
|
||||||
|
if (activeWordsLocation >= 0)
|
||||||
|
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(mDeckLink.OutputFrameWidth())));
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
glFlush();
|
||||||
|
const auto renderEndTime = std::chrono::steady_clock::now();
|
||||||
|
const double frameBudgetMilliseconds = mDeckLink.FrameBudgetMilliseconds();
|
||||||
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
||||||
|
mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
mRuntimeHost.TryAdvanceFrame();
|
||||||
|
|
||||||
|
IDeckLinkVideoBuffer* outputVideoFrameBuffer;
|
||||||
|
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
||||||
|
{
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
||||||
|
{
|
||||||
|
outputVideoFrameBuffer->Release();
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* pFrame;
|
||||||
|
outputVideoFrameBuffer->GetBytes(&pFrame);
|
||||||
|
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
if (mDeckLink.OutputIsTenBit())
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
glReadPixels(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, pFrame);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
glReadPixels(0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame);
|
||||||
|
}
|
||||||
|
mPaint();
|
||||||
|
|
||||||
|
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
||||||
|
outputVideoFrameBuffer->Release();
|
||||||
|
|
||||||
|
mDeckLink.AccountForCompletionResult(completionResult);
|
||||||
|
|
||||||
|
// Schedule the next frame for playout
|
||||||
|
mDeckLink.ScheduleOutputFrame(outputVideoFrame);
|
||||||
|
|
||||||
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
|
||||||
|
LeaveCriticalSection(&mMutex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class DeckLinkSession;
|
||||||
|
class OpenGLRenderer;
|
||||||
|
class RuntimeHost;
|
||||||
|
|
||||||
|
class OpenGLDeckLinkBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using RenderEffectCallback = std::function<void()>;
|
||||||
|
using OutputReadyCallback = std::function<void()>;
|
||||||
|
using PaintCallback = std::function<void()>;
|
||||||
|
|
||||||
|
OpenGLDeckLinkBridge(
|
||||||
|
DeckLinkSession& deckLink,
|
||||||
|
OpenGLRenderer& renderer,
|
||||||
|
RuntimeHost& runtimeHost,
|
||||||
|
CRITICAL_SECTION& mutex,
|
||||||
|
HDC hdc,
|
||||||
|
HGLRC hglrc,
|
||||||
|
RenderEffectCallback renderEffect,
|
||||||
|
OutputReadyCallback outputReady,
|
||||||
|
PaintCallback paint);
|
||||||
|
|
||||||
|
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||||
|
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RecordFramePacing(BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
|
DeckLinkSession& mDeckLink;
|
||||||
|
OpenGLRenderer& mRenderer;
|
||||||
|
RuntimeHost& mRuntimeHost;
|
||||||
|
CRITICAL_SECTION& mMutex;
|
||||||
|
HDC mHdc;
|
||||||
|
HGLRC mHglrc;
|
||||||
|
RenderEffectCallback mRenderEffect;
|
||||||
|
OutputReadyCallback mOutputReady;
|
||||||
|
PaintCallback mPaint;
|
||||||
|
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||||
|
double mCompletionIntervalMilliseconds = 0.0;
|
||||||
|
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||||
|
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||||
|
uint64_t mLateFrameCount = 0;
|
||||||
|
uint64_t mDroppedFrameCount = 0;
|
||||||
|
uint64_t mFlushedFrameCount = 0;
|
||||||
|
};
|
||||||
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;
|
||||||
|
};
|
||||||
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 "stdafx.h"
|
||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
|
#include "RuntimeClock.h"
|
||||||
#include "RuntimeParameterUtils.h"
|
#include "RuntimeParameterUtils.h"
|
||||||
#include "ShaderCompiler.h"
|
#include "ShaderCompiler.h"
|
||||||
#include "ShaderPackageRegistry.h"
|
#include "ShaderPackageRegistry.h"
|
||||||
@@ -7,6 +9,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
@@ -53,6 +56,13 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
|||||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double GenerateStartupRandom()
|
||||||
|
{
|
||||||
|
std::random_device randomDevice;
|
||||||
|
std::uniform_real_distribution<double> distribution(0.0, 1.0);
|
||||||
|
return distribution(randomDevice);
|
||||||
|
}
|
||||||
|
|
||||||
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||||
{
|
{
|
||||||
const std::string prefix = "layer-";
|
const std::string prefix = "layer-";
|
||||||
@@ -133,6 +143,8 @@ std::string ShaderParameterTypeToString(ShaderParameterType type)
|
|||||||
case ShaderParameterType::Color: return "color";
|
case ShaderParameterType::Color: return "color";
|
||||||
case ShaderParameterType::Boolean: return "bool";
|
case ShaderParameterType::Boolean: return "bool";
|
||||||
case ShaderParameterType::Enum: return "enum";
|
case ShaderParameterType::Enum: return "enum";
|
||||||
|
case ShaderParameterType::Text: return "text";
|
||||||
|
case ShaderParameterType::Trigger: return "trigger";
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -179,6 +191,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
|||||||
type = ShaderParameterType::Enum;
|
type = ShaderParameterType::Enum;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (typeName == "text")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeName == "trigger")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Trigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +222,24 @@ bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right)
|
||||||
|
{
|
||||||
|
if (left.size() != right.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < left.size(); ++index)
|
||||||
|
{
|
||||||
|
if (left[index].id != right[index].id ||
|
||||||
|
left[index].path != right[index].path ||
|
||||||
|
left[index].writeTime != right[index].writeTime)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
||||||
{
|
{
|
||||||
return manifestPath.string();
|
return manifestPath.string();
|
||||||
@@ -379,6 +419,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
|
{
|
||||||
|
const JsonValue* fontsValue = nullptr;
|
||||||
|
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
||||||
|
return false;
|
||||||
|
if (!fontsValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (const JsonValue& fontJson : fontsValue->asArray())
|
||||||
|
{
|
||||||
|
if (!fontJson.isObject())
|
||||||
|
{
|
||||||
|
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fontId;
|
||||||
|
std::string fontPath;
|
||||||
|
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
||||||
|
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
||||||
|
{
|
||||||
|
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ShaderFontAsset fontAsset;
|
||||||
|
fontAsset.id = fontId;
|
||||||
|
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
||||||
|
if (!std::filesystem::exists(fontAsset.path))
|
||||||
|
{
|
||||||
|
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
||||||
|
shaderPackage.fontAssets.push_back(fontAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
{
|
{
|
||||||
const JsonValue* temporalValue = nullptr;
|
const JsonValue* temporalValue = nullptr;
|
||||||
@@ -461,6 +544,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (!defaultValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter default must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.defaultTextValue = defaultValue->asString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +637,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (const JsonValue* fontValue = parameterJson.find("font"))
|
||||||
|
{
|
||||||
|
if (!fontValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter 'font' must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.fontId = fontValue->asString();
|
||||||
|
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
||||||
|
{
|
||||||
|
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
||||||
|
{
|
||||||
|
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
if (definition.type == ShaderParameterType::Enum)
|
||||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||||
|
|
||||||
@@ -578,6 +696,13 @@ RuntimeHost::RuntimeHost()
|
|||||||
mFrameBudgetMilliseconds(0.0),
|
mFrameBudgetMilliseconds(0.0),
|
||||||
mRenderMilliseconds(0.0),
|
mRenderMilliseconds(0.0),
|
||||||
mSmoothedRenderMilliseconds(0.0),
|
mSmoothedRenderMilliseconds(0.0),
|
||||||
|
mCompletionIntervalMilliseconds(0.0),
|
||||||
|
mSmoothedCompletionIntervalMilliseconds(0.0),
|
||||||
|
mMaxCompletionIntervalMilliseconds(0.0),
|
||||||
|
mStartupRandom(GenerateStartupRandom()),
|
||||||
|
mLateFrameCount(0),
|
||||||
|
mDroppedFrameCount(0),
|
||||||
|
mFlushedFrameCount(0),
|
||||||
mServerPort(8080),
|
mServerPort(8080),
|
||||||
mAutoReloadEnabled(true),
|
mAutoReloadEnabled(true),
|
||||||
mStartTime(std::chrono::steady_clock::now()),
|
mStartTime(std::chrono::steady_clock::now()),
|
||||||
@@ -693,7 +818,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
}
|
}
|
||||||
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
|
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
|
||||||
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
|
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
|
||||||
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets))
|
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) ||
|
||||||
|
!FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets))
|
||||||
{
|
{
|
||||||
registryChanged = true;
|
registryChanged = true;
|
||||||
break;
|
break;
|
||||||
@@ -714,7 +840,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
if (previous->second.first != active->second.shaderWriteTime ||
|
if (previous->second.first != active->second.shaderWriteTime ||
|
||||||
previous->second.second != active->second.manifestWriteTime ||
|
previous->second.second != active->second.manifestWriteTime ||
|
||||||
(previousPackage != previousPackages.end() &&
|
(previousPackage != previousPackages.end() &&
|
||||||
!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets)))
|
(!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) ||
|
||||||
|
!FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets))))
|
||||||
{
|
{
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
}
|
}
|
||||||
@@ -899,6 +1026,15 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = layer->parameterValues[parameterId];
|
||||||
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue normalized;
|
ShaderParameterValue normalized;
|
||||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -945,6 +1081,15 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = matchedLayer->parameterValues[parameterIt->id];
|
||||||
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue normalized;
|
ShaderParameterValue normalized;
|
||||||
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -1045,6 +1190,21 @@ void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message)
|
|||||||
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetSignalStatusLocked(hasSignal, width, height, modeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetSignalStatusLocked(hasSignal, width, height, modeName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
|
{
|
||||||
mHasSignal = hasSignal;
|
mHasSignal = hasSignal;
|
||||||
mSignalWidth = width;
|
mSignalWidth = width;
|
||||||
mSignalHeight = height;
|
mSignalHeight = height;
|
||||||
@@ -1067,6 +1227,21 @@ void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool sup
|
|||||||
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetPerformanceStatsLocked(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetPerformanceStatsLocked(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
|
{
|
||||||
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
||||||
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||||
if (mSmoothedRenderMilliseconds <= 0.0)
|
if (mSmoothedRenderMilliseconds <= 0.0)
|
||||||
@@ -1075,12 +1250,48 @@ void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double ren
|
|||||||
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::AdvanceFrame()
|
void RuntimeHost::SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SetFramePacingStatsLocked(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
||||||
|
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SetFramePacingStatsLocked(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
||||||
|
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||||
|
{
|
||||||
|
mCompletionIntervalMilliseconds = std::max(completionIntervalMilliseconds, 0.0);
|
||||||
|
mSmoothedCompletionIntervalMilliseconds = std::max(smoothedCompletionIntervalMilliseconds, 0.0);
|
||||||
|
mMaxCompletionIntervalMilliseconds = std::max(maxCompletionIntervalMilliseconds, 0.0);
|
||||||
|
mLateFrameCount = lateFrameCount;
|
||||||
|
mDroppedFrameCount = droppedFrameCount;
|
||||||
|
mFlushedFrameCount = flushedFrameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::AdvanceFrame()
|
||||||
|
{
|
||||||
++mFrameCounter;
|
++mFrameCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TryAdvanceFrame()
|
||||||
|
{
|
||||||
|
++mFrameCounter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1123,7 +1334,39 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
std::vector<RuntimeRenderState> states;
|
std::vector<RuntimeRenderState> states;
|
||||||
|
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
states.clear();
|
||||||
|
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
|
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||||
|
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
const double frameCount = static_cast<double>(mFrameCounter.load(std::memory_order_relaxed));
|
||||||
|
|
||||||
|
for (RuntimeRenderState& state : states)
|
||||||
|
{
|
||||||
|
state.timeSeconds = timeSeconds;
|
||||||
|
state.utcTimeSeconds = clock.utcTimeSeconds;
|
||||||
|
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
||||||
|
state.startupRandom = mStartupRandom;
|
||||||
|
state.frameCount = frameCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
for (const LayerPersistentState& layer : mPersistentState.layers)
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(layer.shaderId);
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||||
@@ -1133,8 +1376,6 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
RuntimeRenderState state;
|
RuntimeRenderState state;
|
||||||
state.layerId = layer.id;
|
state.layerId = layer.id;
|
||||||
state.shaderId = layer.shaderId;
|
state.shaderId = layer.shaderId;
|
||||||
state.timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
|
||||||
state.frameCount = static_cast<double>(mFrameCounter);
|
|
||||||
state.mixAmount = 1.0;
|
state.mixAmount = 1.0;
|
||||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||||
state.inputWidth = mSignalWidth;
|
state.inputWidth = mSignalWidth;
|
||||||
@@ -1143,6 +1384,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
state.outputHeight = outputHeight;
|
state.outputHeight = outputHeight;
|
||||||
state.parameterDefinitions = shaderIt->second.parameters;
|
state.parameterDefinitions = shaderIt->second.parameters;
|
||||||
state.textureAssets = shaderIt->second.textureAssets;
|
state.textureAssets = shaderIt->second.textureAssets;
|
||||||
|
state.fontAssets = shaderIt->second.fontAssets;
|
||||||
state.isTemporal = shaderIt->second.temporal.enabled;
|
state.isTemporal = shaderIt->second.temporal.enabled;
|
||||||
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
||||||
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
||||||
@@ -1160,7 +1402,7 @@ std::vector<RuntimeRenderState> RuntimeHost::GetLayerRenderStates(unsigned outpu
|
|||||||
states.push_back(state);
|
states.push_back(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return states;
|
RefreshDynamicRenderStateFields(states);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string RuntimeHost::BuildStateJson() const
|
std::string RuntimeHost::BuildStateJson() const
|
||||||
@@ -1401,12 +1643,14 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
{
|
{
|
||||||
std::map<std::string, ShaderPackage> packagesById;
|
std::map<std::string, ShaderPackage> packagesById;
|
||||||
std::vector<std::string> packageOrder;
|
std::vector<std::string> packageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> packageStatuses;
|
||||||
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
|
||||||
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, error))
|
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mPackagesById.swap(packagesById);
|
mPackagesById.swap(packagesById);
|
||||||
mPackageOrder.swap(packageOrder);
|
mPackageOrder.swap(packageOrder);
|
||||||
|
mPackageStatuses.swap(packageStatuses);
|
||||||
|
|
||||||
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
|
||||||
{
|
{
|
||||||
@@ -1447,6 +1691,7 @@ bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath,
|
|||||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||||
|
|
||||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
|
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||||
}
|
}
|
||||||
@@ -1465,8 +1710,68 @@ void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, co
|
|||||||
{
|
{
|
||||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||||
{
|
{
|
||||||
if (layerState.parameterValues.find(definition.id) == layerState.parameterValues.end())
|
auto valueIt = layerState.parameterValues.find(definition.id);
|
||||||
|
if (valueIt == layerState.parameterValues.end())
|
||||||
|
{
|
||||||
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonValue valueJson;
|
||||||
|
bool shouldNormalize = true;
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
if (valueIt->second.numberValues.empty())
|
||||||
|
shouldNormalize = false;
|
||||||
|
else
|
||||||
|
valueJson = JsonValue(valueIt->second.numberValues.front());
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
valueJson = JsonValue::MakeArray();
|
||||||
|
for (double number : valueIt->second.numberValues)
|
||||||
|
valueJson.pushBack(JsonValue(number));
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
valueJson = JsonValue(valueIt->second.booleanValue);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
valueJson = JsonValue(valueIt->second.enumValue);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
{
|
||||||
|
const std::string textValue = !valueIt->second.textValue.empty()
|
||||||
|
? valueIt->second.textValue
|
||||||
|
: valueIt->second.enumValue;
|
||||||
|
if (textValue.empty())
|
||||||
|
{
|
||||||
|
valueIt->second = DefaultValueForDefinition(definition);
|
||||||
|
shouldNormalize = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valueJson = JsonValue(textValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
if (valueIt->second.numberValues.empty())
|
||||||
|
valueJson = JsonValue(0.0);
|
||||||
|
else
|
||||||
|
valueJson = JsonValue(std::max(0.0, std::floor(valueIt->second.numberValues.front())));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldNormalize)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ShaderParameterValue normalizedValue;
|
||||||
|
std::string normalizeError;
|
||||||
|
if (NormalizeAndValidateValue(definition, valueJson, normalizedValue, normalizeError))
|
||||||
|
valueIt->second = normalizedValue;
|
||||||
|
else
|
||||||
|
valueIt->second = DefaultValueForDefinition(definition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1589,21 +1894,28 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
||||||
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
||||||
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
||||||
|
performance.set("completionIntervalMs", JsonValue(mCompletionIntervalMilliseconds));
|
||||||
|
performance.set("smoothedCompletionIntervalMs", JsonValue(mSmoothedCompletionIntervalMilliseconds));
|
||||||
|
performance.set("maxCompletionIntervalMs", JsonValue(mMaxCompletionIntervalMilliseconds));
|
||||||
|
performance.set("lateFrameCount", JsonValue(static_cast<double>(mLateFrameCount)));
|
||||||
|
performance.set("droppedFrameCount", JsonValue(static_cast<double>(mDroppedFrameCount)));
|
||||||
|
performance.set("flushedFrameCount", JsonValue(static_cast<double>(mFlushedFrameCount)));
|
||||||
root.set("performance", performance);
|
root.set("performance", performance);
|
||||||
|
|
||||||
JsonValue shaderLibrary = JsonValue::MakeArray();
|
JsonValue shaderLibrary = JsonValue::MakeArray();
|
||||||
for (const std::string& shaderId : mPackageOrder)
|
for (const ShaderPackageStatus& status : mPackageStatuses)
|
||||||
{
|
{
|
||||||
auto shaderIt = mPackagesById.find(shaderId);
|
|
||||||
if (shaderIt == mPackagesById.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
JsonValue shader = JsonValue::MakeObject();
|
JsonValue shader = JsonValue::MakeObject();
|
||||||
shader.set("id", JsonValue(shaderIt->second.id));
|
shader.set("id", JsonValue(status.id));
|
||||||
shader.set("name", JsonValue(shaderIt->second.displayName));
|
shader.set("name", JsonValue(status.displayName));
|
||||||
shader.set("description", JsonValue(shaderIt->second.description));
|
shader.set("description", JsonValue(status.description));
|
||||||
shader.set("category", JsonValue(shaderIt->second.category));
|
shader.set("category", JsonValue(status.category));
|
||||||
if (shaderIt->second.temporal.enabled)
|
shader.set("available", JsonValue(status.available));
|
||||||
|
if (!status.available)
|
||||||
|
shader.set("error", JsonValue(status.error));
|
||||||
|
|
||||||
|
auto shaderIt = mPackagesById.find(status.id);
|
||||||
|
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.temporal.enabled)
|
||||||
{
|
{
|
||||||
JsonValue temporal = JsonValue::MakeObject();
|
JsonValue temporal = JsonValue::MakeObject();
|
||||||
temporal.set("enabled", JsonValue(true));
|
temporal.set("enabled", JsonValue(true));
|
||||||
@@ -1692,6 +2004,12 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|||||||
}
|
}
|
||||||
parameter.set("options", options);
|
parameter.set("options", options);
|
||||||
}
|
}
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
parameter.set("maxLength", JsonValue(static_cast<double>(definition.maxLength)));
|
||||||
|
if (!definition.fontId.empty())
|
||||||
|
parameter.set("font", JsonValue(definition.fontId));
|
||||||
|
}
|
||||||
|
|
||||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||||
auto valueIt = layer.parameterValues.find(definition.id);
|
auto valueIt = layer.parameterValues.find(definition.id);
|
||||||
@@ -1830,6 +2148,10 @@ JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition&
|
|||||||
return JsonValue(value.booleanValue);
|
return JsonValue(value.booleanValue);
|
||||||
case ShaderParameterType::Enum:
|
case ShaderParameterType::Enum:
|
||||||
return JsonValue(value.enumValue);
|
return JsonValue(value.enumValue);
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
return JsonValue(value.textValue);
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
case ShaderParameterType::Float:
|
case ShaderParameterType::Float:
|
||||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
case ShaderParameterType::Vec2:
|
case ShaderParameterType::Vec2:
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "RuntimeJson.h"
|
#include "RuntimeJson.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -35,13 +36,22 @@ public:
|
|||||||
|
|
||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
|
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
|
bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
void AdvanceFrame();
|
void AdvanceFrame();
|
||||||
|
bool TryAdvanceFrame();
|
||||||
|
|
||||||
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
||||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||||
|
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||||
|
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||||
std::string BuildStateJson() const;
|
std::string BuildStateJson() const;
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||||
@@ -109,6 +119,7 @@ private:
|
|||||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||||
bool ResolvePaths(std::string& error);
|
bool ResolvePaths(std::string& error);
|
||||||
|
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||||
JsonValue BuildStateValue() const;
|
JsonValue BuildStateValue() const;
|
||||||
JsonValue SerializeLayerStackLocked() const;
|
JsonValue SerializeLayerStackLocked() const;
|
||||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||||
@@ -120,6 +131,10 @@ private:
|
|||||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||||
std::string GenerateLayerId();
|
std::string GenerateLayerId();
|
||||||
|
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
|
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
|
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
@@ -138,6 +153,7 @@ private:
|
|||||||
std::filesystem::path mPatchedGlslPath;
|
std::filesystem::path mPatchedGlslPath;
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
std::vector<std::string> mPackageOrder;
|
std::vector<std::string> mPackageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||||
bool mReloadRequested;
|
bool mReloadRequested;
|
||||||
bool mCompileSucceeded;
|
bool mCompileSucceeded;
|
||||||
std::string mCompileMessage;
|
std::string mCompileMessage;
|
||||||
@@ -148,11 +164,18 @@ private:
|
|||||||
double mFrameBudgetMilliseconds;
|
double mFrameBudgetMilliseconds;
|
||||||
double mRenderMilliseconds;
|
double mRenderMilliseconds;
|
||||||
double mSmoothedRenderMilliseconds;
|
double mSmoothedRenderMilliseconds;
|
||||||
|
double mCompletionIntervalMilliseconds;
|
||||||
|
double mSmoothedCompletionIntervalMilliseconds;
|
||||||
|
double mMaxCompletionIntervalMilliseconds;
|
||||||
|
double mStartupRandom;
|
||||||
|
uint64_t mLateFrameCount;
|
||||||
|
uint64_t mDroppedFrameCount;
|
||||||
|
uint64_t mFlushedFrameCount;
|
||||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||||
unsigned short mServerPort;
|
unsigned short mServerPort;
|
||||||
bool mAutoReloadEnabled;
|
bool mAutoReloadEnabled;
|
||||||
std::chrono::steady_clock::time_point mStartTime;
|
std::chrono::steady_clock::time_point mStartTime;
|
||||||
std::chrono::steady_clock::time_point mLastScanTime;
|
std::chrono::steady_clock::time_point mLastScanTime;
|
||||||
uint64_t mFrameCounter;
|
std::atomic<uint64_t> mFrameCounter;
|
||||||
uint64_t mNextLayerId;
|
uint64_t mNextLayerId;
|
||||||
};
|
};
|
||||||
@@ -36,6 +36,21 @@ std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
|||||||
}
|
}
|
||||||
return numbers;
|
return numbers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
|
||||||
|
{
|
||||||
|
std::string normalized;
|
||||||
|
normalized.reserve(std::min<std::size_t>(text.size(), maxLength));
|
||||||
|
for (unsigned char ch : text)
|
||||||
|
{
|
||||||
|
if (ch < 32 || ch > 126)
|
||||||
|
continue;
|
||||||
|
if (normalized.size() >= maxLength)
|
||||||
|
break;
|
||||||
|
normalized.push_back(static_cast<char>(ch));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MakeSafePresetFileStem(const std::string& presetName)
|
std::string MakeSafePresetFileStem(const std::string& presetName)
|
||||||
@@ -82,6 +97,12 @@ ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition&
|
|||||||
case ShaderParameterType::Enum:
|
case ShaderParameterType::Enum:
|
||||||
value.enumValue = definition.defaultEnumValue;
|
value.enumValue = definition.defaultEnumValue;
|
||||||
break;
|
break;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
value.numberValues = { 0.0, -1000000.0 };
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -164,6 +185,22 @@ bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definit
|
|||||||
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
|
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
if (!value.isString())
|
||||||
|
{
|
||||||
|
error = "Expected string value for text parameter '" + definition.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
normalizedValue.textValue = NormalizeTextValue(value.asString(), definition.maxLength);
|
||||||
|
return true;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
if (!value.isNumber() && !value.isBoolean())
|
||||||
|
{
|
||||||
|
error = "Expected numeric or boolean value for trigger parameter '" + definition.id + "'.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
normalizedValue.numberValues = { value.isNumber() ? std::max(0.0, std::floor(value.asNumber())) : 0.0, -1000000.0 };
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "NativeHandles.h"
|
#include "NativeHandles.h"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <cctype>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -30,15 +31,36 @@ std::string SlangCBufferTypeForParameter(ShaderParameterType type)
|
|||||||
case ShaderParameterType::Color: return "float4";
|
case ShaderParameterType::Color: return "float4";
|
||||||
case ShaderParameterType::Boolean: return "bool";
|
case ShaderParameterType::Boolean: return "bool";
|
||||||
case ShaderParameterType::Enum: return "int";
|
case ShaderParameterType::Enum: return "int";
|
||||||
|
case ShaderParameterType::Text: return "";
|
||||||
|
case ShaderParameterType::Trigger: return "int";
|
||||||
}
|
}
|
||||||
return "float";
|
return "float";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string CapitalizeIdentifier(const std::string& identifier)
|
||||||
|
{
|
||||||
|
if (identifier.empty())
|
||||||
|
return identifier;
|
||||||
|
std::string text = identifier;
|
||||||
|
text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
|
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
{
|
{
|
||||||
std::ostringstream source;
|
std::ostringstream source;
|
||||||
for (const ShaderParameterDefinition& definition : parameters)
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
if (definition.type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
source << "\tint " << definition.id << ";\n";
|
||||||
|
source << "\tfloat " << definition.id << "Time;\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
|
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
|
||||||
|
}
|
||||||
return source.str();
|
return source.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +82,44 @@ std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset
|
|||||||
return source.str();
|
return source.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
|
{
|
||||||
|
std::ostringstream source;
|
||||||
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type != ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
source << "Sampler2D<float4> " << definition.id << "Texture;\n";
|
||||||
|
}
|
||||||
|
if (source.tellp() > 0)
|
||||||
|
source << "\n";
|
||||||
|
return source.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
|
||||||
|
{
|
||||||
|
std::ostringstream source;
|
||||||
|
for (const ShaderParameterDefinition& definition : parameters)
|
||||||
|
{
|
||||||
|
if (definition.type != ShaderParameterType::Text)
|
||||||
|
continue;
|
||||||
|
const std::string suffix = CapitalizeIdentifier(definition.id);
|
||||||
|
source
|
||||||
|
<< "float sample" << suffix << "(float2 uv)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
|
||||||
|
<< "\t\treturn 0.0;\n"
|
||||||
|
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
|
||||||
|
<< "}\n\n"
|
||||||
|
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
|
||||||
|
<< "{\n"
|
||||||
|
<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
|
||||||
|
<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
|
||||||
|
<< "}\n\n";
|
||||||
|
}
|
||||||
|
return source.str();
|
||||||
|
}
|
||||||
|
|
||||||
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
|
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
|
||||||
{
|
{
|
||||||
std::ostringstream source;
|
std::ostringstream source;
|
||||||
@@ -115,11 +175,14 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
|
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", mMaxTemporalHistoryFrames));
|
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", mMaxTemporalHistoryFrames));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
||||||
return true;
|
return true;
|
||||||
@@ -67,6 +67,16 @@ bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType&
|
|||||||
type = ShaderParameterType::Enum;
|
type = ShaderParameterType::Enum;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (typeName == "text")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Text;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeName == "trigger")
|
||||||
|
{
|
||||||
|
type = ShaderParameterType::Trigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +293,49 @@ bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPack
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
|
{
|
||||||
|
const JsonValue* fontsValue = nullptr;
|
||||||
|
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
||||||
|
return false;
|
||||||
|
if (!fontsValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
for (const JsonValue& fontJson : fontsValue->asArray())
|
||||||
|
{
|
||||||
|
if (!fontJson.isObject())
|
||||||
|
{
|
||||||
|
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fontId;
|
||||||
|
std::string fontPath;
|
||||||
|
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
||||||
|
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
||||||
|
{
|
||||||
|
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ShaderFontAsset fontAsset;
|
||||||
|
fontAsset.id = fontId;
|
||||||
|
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
||||||
|
if (!std::filesystem::exists(fontAsset.path))
|
||||||
|
{
|
||||||
|
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
||||||
|
shaderPackage.fontAssets.push_back(fontAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
{
|
{
|
||||||
const JsonValue* temporalValue = nullptr;
|
const JsonValue* temporalValue = nullptr;
|
||||||
@@ -365,6 +418,17 @@ bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefini
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (!defaultValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter default must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.defaultTextValue = defaultValue->asString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +511,30 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (definition.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
if (const JsonValue* fontValue = parameterJson.find("font"))
|
||||||
|
{
|
||||||
|
if (!fontValue->isString())
|
||||||
|
{
|
||||||
|
error = "Text parameter 'font' must be a string for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.fontId = fontValue->asString();
|
||||||
|
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
||||||
|
{
|
||||||
|
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
||||||
|
{
|
||||||
|
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
if (definition.type == ShaderParameterType::Enum)
|
||||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||||
|
|
||||||
@@ -471,6 +559,36 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string UniqueUnavailableShaderId(const std::filesystem::path& manifestPath, const std::string& parsedId)
|
||||||
|
{
|
||||||
|
const std::string fallbackId = manifestPath.parent_path().filename().string();
|
||||||
|
const std::string baseId = parsedId.empty() ? fallbackId : parsedId;
|
||||||
|
return baseId + "@invalid:" + fallbackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackageStatus BuildUnavailableStatus(const std::filesystem::path& manifestPath, const ShaderPackage& partialPackage, const std::string& packageError)
|
||||||
|
{
|
||||||
|
ShaderPackageStatus status;
|
||||||
|
status.id = UniqueUnavailableShaderId(manifestPath, partialPackage.id);
|
||||||
|
status.displayName = !partialPackage.displayName.empty() ? partialPackage.displayName : manifestPath.parent_path().filename().string();
|
||||||
|
status.description = partialPackage.description;
|
||||||
|
status.category = !partialPackage.category.empty() ? partialPackage.category : "Unavailable";
|
||||||
|
status.available = false;
|
||||||
|
status.error = packageError;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackageStatus BuildAvailableStatus(const ShaderPackage& shaderPackage)
|
||||||
|
{
|
||||||
|
ShaderPackageStatus status;
|
||||||
|
status.id = shaderPackage.id;
|
||||||
|
status.displayName = shaderPackage.displayName;
|
||||||
|
status.description = shaderPackage.description;
|
||||||
|
status.category = shaderPackage.category;
|
||||||
|
status.available = true;
|
||||||
|
return status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
||||||
@@ -478,10 +596,16 @@ ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
|
bool ShaderPackageRegistry::Scan(
|
||||||
|
const std::filesystem::path& shaderRoot,
|
||||||
|
std::map<std::string, ShaderPackage>& packagesById,
|
||||||
|
std::vector<std::string>& packageOrder,
|
||||||
|
std::vector<ShaderPackageStatus>& packageStatuses,
|
||||||
|
std::string& error) const
|
||||||
{
|
{
|
||||||
packagesById.clear();
|
packagesById.clear();
|
||||||
packageOrder.clear();
|
packageOrder.clear();
|
||||||
|
packageStatuses.clear();
|
||||||
|
|
||||||
if (!std::filesystem::exists(shaderRoot))
|
if (!std::filesystem::exists(shaderRoot))
|
||||||
{
|
{
|
||||||
@@ -500,19 +624,27 @@ bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::m
|
|||||||
|
|
||||||
ShaderPackage shaderPackage;
|
ShaderPackage shaderPackage;
|
||||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
if (!ParseManifest(manifestPath, shaderPackage, error))
|
||||||
return false;
|
{
|
||||||
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
|
||||||
|
error.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
||||||
{
|
{
|
||||||
error = "Duplicate shader id found: " + shaderPackage.id;
|
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
|
||||||
return false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
packageOrder.push_back(shaderPackage.id);
|
packageOrder.push_back(shaderPackage.id);
|
||||||
|
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
|
||||||
packagesById[shaderPackage.id] = shaderPackage;
|
packagesById[shaderPackage.id] = shaderPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(packageOrder.begin(), packageOrder.end());
|
std::sort(packageOrder.begin(), packageOrder.end());
|
||||||
|
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
|
||||||
|
return left.displayName < right.displayName;
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +676,7 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
|||||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||||
|
|
||||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
|
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,12 @@ class ShaderPackageRegistry
|
|||||||
public:
|
public:
|
||||||
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
|
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
|
||||||
|
|
||||||
bool Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const;
|
bool Scan(
|
||||||
|
const std::filesystem::path& shaderRoot,
|
||||||
|
std::map<std::string, ShaderPackage>& packagesById,
|
||||||
|
std::vector<std::string>& packageOrder,
|
||||||
|
std::vector<ShaderPackageStatus>& packageStatuses,
|
||||||
|
std::string& error) const;
|
||||||
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -11,7 +11,9 @@ enum class ShaderParameterType
|
|||||||
Vec2,
|
Vec2,
|
||||||
Color,
|
Color,
|
||||||
Boolean,
|
Boolean,
|
||||||
Enum
|
Enum,
|
||||||
|
Text,
|
||||||
|
Trigger
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShaderParameterOption
|
struct ShaderParameterOption
|
||||||
@@ -31,6 +33,9 @@ struct ShaderParameterDefinition
|
|||||||
std::vector<double> stepNumbers;
|
std::vector<double> stepNumbers;
|
||||||
bool defaultBoolean = false;
|
bool defaultBoolean = false;
|
||||||
std::string defaultEnumValue;
|
std::string defaultEnumValue;
|
||||||
|
std::string defaultTextValue;
|
||||||
|
std::string fontId;
|
||||||
|
unsigned maxLength = 64;
|
||||||
std::vector<ShaderParameterOption> enumOptions;
|
std::vector<ShaderParameterOption> enumOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +44,7 @@ struct ShaderParameterValue
|
|||||||
std::vector<double> numberValues;
|
std::vector<double> numberValues;
|
||||||
bool booleanValue = false;
|
bool booleanValue = false;
|
||||||
std::string enumValue;
|
std::string enumValue;
|
||||||
|
std::string textValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TemporalHistorySource
|
enum class TemporalHistorySource
|
||||||
@@ -63,6 +69,13 @@ struct ShaderTextureAsset
|
|||||||
std::filesystem::file_time_type writeTime;
|
std::filesystem::file_time_type writeTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ShaderFontAsset
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::filesystem::path path;
|
||||||
|
std::filesystem::file_time_type writeTime;
|
||||||
|
};
|
||||||
|
|
||||||
struct ShaderPackage
|
struct ShaderPackage
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -75,11 +88,22 @@ struct ShaderPackage
|
|||||||
std::filesystem::path manifestPath;
|
std::filesystem::path manifestPath;
|
||||||
std::vector<ShaderParameterDefinition> parameters;
|
std::vector<ShaderParameterDefinition> parameters;
|
||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
TemporalSettings temporal;
|
TemporalSettings temporal;
|
||||||
std::filesystem::file_time_type shaderWriteTime;
|
std::filesystem::file_time_type shaderWriteTime;
|
||||||
std::filesystem::file_time_type manifestWriteTime;
|
std::filesystem::file_time_type manifestWriteTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ShaderPackageStatus
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string displayName;
|
||||||
|
std::string description;
|
||||||
|
std::string category;
|
||||||
|
bool available = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
struct RuntimeRenderState
|
struct RuntimeRenderState
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
@@ -87,7 +111,11 @@ struct RuntimeRenderState
|
|||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
double timeSeconds = 0.0;
|
double timeSeconds = 0.0;
|
||||||
|
double utcTimeSeconds = 0.0;
|
||||||
|
double utcOffsetSeconds = 0.0;
|
||||||
|
double startupRandom = 0.0;
|
||||||
double frameCount = 0.0;
|
double frameCount = 0.0;
|
||||||
double mixAmount = 1.0;
|
double mixAmount = 1.0;
|
||||||
double bypass = 0.0;
|
double bypass = 0.0;
|
||||||
@@ -70,6 +70,12 @@ Examples:
|
|||||||
|
|
||||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
||||||
|
|
||||||
|
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/VideoShaderToys/trigger-flash/flash 1
|
||||||
|
```
|
||||||
|
|
||||||
## Open Stage Control
|
## Open Stage Control
|
||||||
|
|
||||||
For simple scalar controls, set the widget address and target directly:
|
For simple scalar controls, set the widget address and target directly:
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ paths:
|
|||||||
$ref: "#/components/responses/ActionOk"
|
$ref: "#/components/responses/ActionOk"
|
||||||
"400":
|
"400":
|
||||||
$ref: "#/components/responses/ActionError"
|
$ref: "#/components/responses/ActionError"
|
||||||
|
/api/screenshot:
|
||||||
|
post:
|
||||||
|
tags: [Runtime]
|
||||||
|
summary: Queue a PNG screenshot of the final output render target
|
||||||
|
description: Captures the next completed output render target and writes it under `runtime/screenshots/`.
|
||||||
|
operationId: queueScreenshot
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ActionOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/ActionError"
|
||||||
components:
|
components:
|
||||||
responses:
|
responses:
|
||||||
ActionOk:
|
ActionOk:
|
||||||
@@ -474,7 +492,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
enum: [float, vec2, color, bool, enum]
|
enum: [float, vec2, color, bool, enum, text, trigger]
|
||||||
min:
|
min:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ struct ShaderContext
|
|||||||
float2 inputResolution;
|
float2 inputResolution;
|
||||||
float2 outputResolution;
|
float2 outputResolution;
|
||||||
float time;
|
float time;
|
||||||
|
float utcTimeSeconds;
|
||||||
|
float utcOffsetSeconds;
|
||||||
|
float startupRandom;
|
||||||
float frameCount;
|
float frameCount;
|
||||||
float mixAmount;
|
float mixAmount;
|
||||||
float bypass;
|
float bypass;
|
||||||
@@ -23,6 +26,9 @@ cbuffer GlobalParams
|
|||||||
float gTime;
|
float gTime;
|
||||||
float2 gInputResolution;
|
float2 gInputResolution;
|
||||||
float2 gOutputResolution;
|
float2 gOutputResolution;
|
||||||
|
float gUtcTimeSeconds;
|
||||||
|
float gUtcOffsetSeconds;
|
||||||
|
float gStartupRandom;
|
||||||
float gFrameCount;
|
float gFrameCount;
|
||||||
float gMixAmount;
|
float gMixAmount;
|
||||||
float gBypass;
|
float gBypass;
|
||||||
@@ -32,6 +38,7 @@ cbuffer GlobalParams
|
|||||||
|
|
||||||
Sampler2D<float4> gVideoInput;
|
Sampler2D<float4> gVideoInput;
|
||||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
||||||
|
{{TEXT_SAMPLERS}}
|
||||||
float4 sampleVideo(float2 tc)
|
float4 sampleVideo(float2 tc)
|
||||||
{
|
{
|
||||||
return gVideoInput.Sample(tc);
|
return gVideoInput.Sample(tc);
|
||||||
@@ -67,6 +74,7 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{TEXT_HELPERS}}
|
||||||
#include "{{USER_SHADER_INCLUDE}}"
|
#include "{{USER_SHADER_INCLUDE}}"
|
||||||
|
|
||||||
[shader("fragment")]
|
[shader("fragment")]
|
||||||
@@ -78,6 +86,9 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
|||||||
context.inputResolution = gInputResolution;
|
context.inputResolution = gInputResolution;
|
||||||
context.outputResolution = gOutputResolution;
|
context.outputResolution = gOutputResolution;
|
||||||
context.time = gTime;
|
context.time = gTime;
|
||||||
|
context.utcTimeSeconds = gUtcTimeSeconds;
|
||||||
|
context.utcOffsetSeconds = gUtcOffsetSeconds;
|
||||||
|
context.startupRandom = gStartupRandom;
|
||||||
context.frameCount = gFrameCount;
|
context.frameCount = gFrameCount;
|
||||||
context.mixAmount = gMixAmount;
|
context.mixAmount = gMixAmount;
|
||||||
context.bypass = gBypass;
|
context.bypass = gBypass;
|
||||||
|
|||||||
46
shaders/anamorphic-desqueeze/shader.json
Normal file
46
shaders/anamorphic-desqueeze/shader.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"id": "anamorphic-desqueeze",
|
||||||
|
"name": "Anamorphic Desqueeze",
|
||||||
|
"description": "Desqueezes anamorphic footage by 1.3x, 1.33x, 1.5x, or 2x with fit or fill framing.",
|
||||||
|
"category": "Transform",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "desqueezeFactor",
|
||||||
|
"label": "Desqueeze",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "x1_33",
|
||||||
|
"options": [
|
||||||
|
{ "value": "x1_3", "label": "1.3x" },
|
||||||
|
{ "value": "x1_33", "label": "1.33x" },
|
||||||
|
{ "value": "x1_5", "label": "1.5x" },
|
||||||
|
{ "value": "x2_0", "label": "2x" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "framing",
|
||||||
|
"label": "Framing",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "fit",
|
||||||
|
"options": [
|
||||||
|
{ "value": "fit", "label": "Fit" },
|
||||||
|
{ "value": "fill", "label": "Fill" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pan",
|
||||||
|
"label": "Pan",
|
||||||
|
"type": "vec2",
|
||||||
|
"default": [0.0, 0.0],
|
||||||
|
"min": [-1.0, -1.0],
|
||||||
|
"max": [1.0, 1.0],
|
||||||
|
"step": [0.001, 0.001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "outsideColor",
|
||||||
|
"label": "Outside Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [0.0, 0.0, 0.0, 1.0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
float selectedDesqueezeFactor()
|
||||||
|
{
|
||||||
|
if (desqueezeFactor == 0)
|
||||||
|
return 1.3;
|
||||||
|
if (desqueezeFactor == 1)
|
||||||
|
return 1.3333333;
|
||||||
|
if (desqueezeFactor == 2)
|
||||||
|
return 1.5;
|
||||||
|
return 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float factor = selectedDesqueezeFactor();
|
||||||
|
float2 centered = context.uv - 0.5;
|
||||||
|
|
||||||
|
if (framing == 0)
|
||||||
|
{
|
||||||
|
centered.y *= factor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
centered.x /= factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 sourceUv = centered + 0.5 - pan;
|
||||||
|
bool inside = sourceUv.x >= 0.0 && sourceUv.x <= 1.0 && sourceUv.y >= 0.0 && sourceUv.y <= 1.0;
|
||||||
|
if (!inside)
|
||||||
|
return outsideColor;
|
||||||
|
|
||||||
|
return sampleVideo(sourceUv);
|
||||||
|
}
|
||||||
@@ -41,15 +41,6 @@
|
|||||||
"max": 3.0,
|
"max": 3.0,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pixelFilter",
|
|
||||||
"label": "Pixel Filter",
|
|
||||||
"type": "float",
|
|
||||||
"default": 745.0,
|
|
||||||
"min": 120.0,
|
|
||||||
"max": 1600.0,
|
|
||||||
"step": 1.0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "contrast",
|
"id": "contrast",
|
||||||
"label": "Contrast",
|
"label": "Contrast",
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time)
|
float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float seed)
|
||||||
{
|
{
|
||||||
const float pi = 3.14159265359;
|
const float pi = 3.14159265359;
|
||||||
float safePixelFilter = max(pixelFilter, 1.0);
|
|
||||||
float safeScreenLength = max(length(screenSize), 1.0);
|
float safeScreenLength = max(length(screenSize), 1.0);
|
||||||
float pixelSize = safeScreenLength / safePixelFilter;
|
float2 seedOffset = float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||||
float2 uv = (floor(screenCoords * (1.0 / pixelSize)) * pixelSize - 0.5 * screenSize) / safeScreenLength - offset;
|
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
|
||||||
float uvLength = length(uv);
|
float uvLength = length(uv);
|
||||||
|
|
||||||
float speed = spinRotation * spinEase * 0.2;
|
float speed = spinRotation * spinEase * 0.2;
|
||||||
if (isRotate)
|
if (isRotate)
|
||||||
speed = time * speed;
|
speed = time * speed;
|
||||||
speed += 302.2;
|
speed += 302.2 + seed * 6.2831853;
|
||||||
|
|
||||||
float newPixelAngle = atan2(uv.y, uv.x) + speed - spinEase * 20.0 * (spinAmount * uvLength + (1.0 - spinAmount));
|
float newPixelAngle = atan2(uv.y, uv.x) + speed - spinEase * 20.0 * (spinAmount * uvLength + (1.0 - spinAmount));
|
||||||
float2 mid = (screenSize / safeScreenLength) * 0.5;
|
float2 mid = (screenSize / safeScreenLength) * 0.5;
|
||||||
uv = float2(uvLength * cos(newPixelAngle) + mid.x, uvLength * sin(newPixelAngle) + mid.y) - mid;
|
uv = float2(uvLength * cos(newPixelAngle) + mid.x, uvLength * sin(newPixelAngle) + mid.y) - mid;
|
||||||
|
|
||||||
uv *= 30.0;
|
uv *= 30.0;
|
||||||
speed = time * spinSpeed;
|
speed = (time + seed * 17.0) * spinSpeed;
|
||||||
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
||||||
|
|
||||||
for (int i = 0; i < 5; ++i)
|
for (int i = 0; i < 5; ++i)
|
||||||
@@ -44,6 +43,6 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time)
|
|||||||
float4 shadeVideo(ShaderContext context)
|
float4 shadeVideo(ShaderContext context)
|
||||||
{
|
{
|
||||||
float2 screenSize = max(context.outputResolution, float2(1.0, 1.0));
|
float2 screenSize = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
float4 swirl = balatroSwirl(screenSize, context.uv * screenSize, context.time);
|
float4 swirl = balatroSwirl(screenSize, context.uv * screenSize, context.time, context.startupRandom);
|
||||||
return saturate(lerp(swirl, context.sourceColor, sourceMix));
|
return saturate(lerp(swirl, context.sourceColor, sourceMix));
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -7,12 +7,14 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
{
|
{
|
||||||
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
||||||
float2 blockId = floor(context.uv * blocks);
|
float2 blockId = floor(context.uv * blocks);
|
||||||
float n = hash21(blockId + floor(context.time * 8.0));
|
float seed = context.startupRandom * 4096.0;
|
||||||
|
float frameSeed = floor(context.time * 8.0);
|
||||||
|
float n = hash21(blockId + float2(frameSeed + seed, frameSeed + seed * 0.17));
|
||||||
float historyFrame = floor(lerp(1.0, 7.0, n));
|
float historyFrame = floor(lerp(1.0, 7.0, n));
|
||||||
|
|
||||||
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y), floor(context.time * 5.0)));
|
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y) + seed * 0.37, floor(context.time * 5.0) + seed * 0.13));
|
||||||
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
|
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
|
||||||
float2 offset = float2(tear, (hash21(blockId + 19.0) * 2.0 - 1.0) * amount * 0.025);
|
float2 offset = float2(tear, (hash21(blockId + float2(19.0 + seed, 19.0 + seed * 0.31)) * 2.0 - 1.0) * amount * 0.025);
|
||||||
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
|
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
|
||||||
|
|
||||||
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 velocityPx = float2(
|
float2 velocityPx = float2(
|
||||||
max(20.0, bounceSpeed * minDimension * 1.00),
|
max(20.0, bounceSpeed * minDimension * 1.00),
|
||||||
max(24.0, bounceSpeed * minDimension * 0.77));
|
max(24.0, bounceSpeed * minDimension * 0.77));
|
||||||
float2 motionPx = context.time * velocityPx;
|
float seed = context.startupRandom;
|
||||||
|
float2 motionPx = (float2(context.time, context.time) + float2(seed * 32.7, seed * 40.3)) * velocityPx;
|
||||||
float2 centerPx = minCenterPx + float2(
|
float2 centerPx = minCenterPx + float2(
|
||||||
pingPong(motionPx.x, travelPx.x),
|
pingPong(motionPx.x, travelPx.x),
|
||||||
pingPong(motionPx.y, travelPx.y));
|
pingPong(motionPx.y, travelPx.y));
|
||||||
@@ -38,7 +39,7 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
|
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
|
||||||
int totalHits = max(0, xHits + yHits);
|
int totalHits = max(0, xHits + yHits);
|
||||||
|
|
||||||
float hue = frac(0.09 + float(totalHits) * 0.173);
|
float hue = frac(0.09 + seed * 0.71 + float(totalHits) * 0.173);
|
||||||
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
|
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
|
||||||
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
float2 fragCoord = context.uv * resolution;
|
float2 fragCoord = context.uv * resolution;
|
||||||
float2 p = fragCoord / resolution.y - offset;
|
float2 p = fragCoord / resolution.y - offset;
|
||||||
float time = context.time * speed;
|
float seed = context.startupRandom;
|
||||||
|
float time = context.time * speed + seed * 41.0;
|
||||||
|
|
||||||
float3 color = float3(0.0, 0.0, 0.0);
|
float3 color = float3(0.0, 0.0, 0.0);
|
||||||
float d = depth;
|
float d = depth;
|
||||||
|
|
||||||
for (int i = 0; i <= 5; ++i)
|
for (int i = 0; i <= 5; ++i)
|
||||||
{
|
{
|
||||||
float3 rayPosition = float3(0.0, 0.0, 5.0) + normalize(float3(p, -1.0)) * d;
|
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 rz = etherMap(rayPosition, time);
|
||||||
float f = clamp((rz - etherMap(rayPosition + float3(0.1, 0.1, 0.1), time)) * 0.5, -0.1, 1.0);
|
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;
|
float3 light = baseColor.rgb + energyColor.rgb * 5.0 * f;
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
54
shaders/happy-accident/shader.json
Normal file
54
shaders/happy-accident/shader.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"id": "happy-accident",
|
||||||
|
"name": "Happy Accident",
|
||||||
|
"description": "Raymarched generative line field. CC0 original 'Clearly a bug' adapted from https://www.shadertoy.com/view/33cGDj.",
|
||||||
|
"category": "Generative",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "speed",
|
||||||
|
"label": "Speed",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 4.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scale",
|
||||||
|
"label": "Scale",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.25,
|
||||||
|
"max": 3.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "raySteps",
|
||||||
|
"label": "Ray Steps",
|
||||||
|
"type": "float",
|
||||||
|
"default": 77.0,
|
||||||
|
"min": 8.0,
|
||||||
|
"max": 77.0,
|
||||||
|
"step": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "intensity",
|
||||||
|
"label": "Intensity",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.1,
|
||||||
|
"max": 4.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sourceMix",
|
||||||
|
"label": "Source Mix",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
63
shaders/happy-accident/shader.slang
Normal file
63
shaders/happy-accident/shader.slang
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
float happyNoise(float2 p)
|
||||||
|
{
|
||||||
|
return frac(dot(p, sin(p))) - 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2x2 rotateAroundZ(float angle)
|
||||||
|
{
|
||||||
|
float c = cos(angle);
|
||||||
|
float s = sin(angle);
|
||||||
|
return float2x2(c, s, -s, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
float2x2 happyAccidentMatrix(float3 originalPosition, float timeCos)
|
||||||
|
{
|
||||||
|
return float2x2(
|
||||||
|
cos(originalPosition.x),
|
||||||
|
sin(originalPosition.y),
|
||||||
|
-sin(originalPosition.z),
|
||||||
|
timeCos);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
|
float2 fragCoord = context.uv * resolution;
|
||||||
|
float2 normalizedCoord = (fragCoord - 0.5 * resolution) / resolution.y / max(scale, 0.001);
|
||||||
|
float seed = context.startupRandom;
|
||||||
|
float time = context.time * speed + seed * 53.0;
|
||||||
|
float timeCos = cos(0.1 * time);
|
||||||
|
float3 direction = normalize(float3(normalizedCoord, 1.0));
|
||||||
|
float3 origin = float3(seed * 0.4 - 0.2, 0.2 - seed * 0.4, time);
|
||||||
|
|
||||||
|
float edgeAmount = saturate(length(normalizedCoord) * 0.55);
|
||||||
|
float z = happyNoise(fragCoord + seed * resolution.yx) * lerp(0.22, 0.08, edgeAmount);
|
||||||
|
float distanceToSurface = 0.0;
|
||||||
|
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
float clampedSteps = clamp(raySteps, 1.0, 77.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 77; ++i)
|
||||||
|
{
|
||||||
|
if (float(i) >= clampedSteps)
|
||||||
|
break;
|
||||||
|
|
||||||
|
z += 0.6 * distanceToSurface;
|
||||||
|
|
||||||
|
float3 position = origin + z * direction;
|
||||||
|
float3 originalPosition = position;
|
||||||
|
|
||||||
|
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
|
||||||
|
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
|
||||||
|
|
||||||
|
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
|
||||||
|
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
|
||||||
|
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
|
||||||
|
|
||||||
|
position = abs(frac(position) - 0.5);
|
||||||
|
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
|
||||||
|
accumulated += palette.w * palette / distanceToSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 color = float4(tanh((accumulated.rgb * intensity) / 20000.0), 1.0);
|
||||||
|
return saturate(lerp(color, context.sourceColor, sourceMix));
|
||||||
|
}
|
||||||
57
shaders/lut-apply/shader.json
Normal file
57
shaders/lut-apply/shader.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"id": "lut-apply",
|
||||||
|
"name": "3D LUT Apply",
|
||||||
|
"description": "Applies the packaged 33-point .cube LUT to the incoming video using tetrahedral interpolation and optional output dithering.",
|
||||||
|
"category": "Color",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"id": "lutTexture",
|
||||||
|
"path": "test-lut.cube"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "lutStrength",
|
||||||
|
"label": "LUT Strength",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "preExposure",
|
||||||
|
"label": "Pre Exposure",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": -4.0,
|
||||||
|
"max": 4.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "postContrast",
|
||||||
|
"label": "Post Contrast",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 2.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ditherAmount",
|
||||||
|
"label": "Output Dither",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.5,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clampInput",
|
||||||
|
"label": "Clamp Input",
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
77
shaders/lut-apply/shader.slang
Normal file
77
shaders/lut-apply/shader.slang
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
static const float LUT_SIZE = 33.0;
|
||||||
|
static const float LUT_LAST_INDEX = 32.0;
|
||||||
|
|
||||||
|
float3 sampleLutCell(float3 index)
|
||||||
|
{
|
||||||
|
float r = floor(index.r + 0.5);
|
||||||
|
float g = floor(index.g + 0.5);
|
||||||
|
float b = floor(index.b + 0.5);
|
||||||
|
|
||||||
|
float atlasWidth = LUT_SIZE * LUT_SIZE;
|
||||||
|
float2 lutUv;
|
||||||
|
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
|
||||||
|
lutUv.y = (g + 0.5) / LUT_SIZE;
|
||||||
|
return lutTexture.Sample(lutUv).rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applyLut33(float3 color)
|
||||||
|
{
|
||||||
|
float3 lutCoord = saturate(color) * LUT_LAST_INDEX;
|
||||||
|
float3 baseIndex = floor(lutCoord);
|
||||||
|
float3 nextIndex = min(baseIndex + 1.0, LUT_LAST_INDEX);
|
||||||
|
float3 blend = lutCoord - baseIndex;
|
||||||
|
|
||||||
|
float3 c000 = sampleLutCell(float3(baseIndex.r, baseIndex.g, baseIndex.b));
|
||||||
|
float3 c100 = sampleLutCell(float3(nextIndex.r, baseIndex.g, baseIndex.b));
|
||||||
|
float3 c010 = sampleLutCell(float3(baseIndex.r, nextIndex.g, baseIndex.b));
|
||||||
|
float3 c110 = sampleLutCell(float3(nextIndex.r, nextIndex.g, baseIndex.b));
|
||||||
|
float3 c001 = sampleLutCell(float3(baseIndex.r, baseIndex.g, nextIndex.b));
|
||||||
|
float3 c101 = sampleLutCell(float3(nextIndex.r, baseIndex.g, nextIndex.b));
|
||||||
|
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
|
||||||
|
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
|
||||||
|
|
||||||
|
if (blend.r > blend.g)
|
||||||
|
{
|
||||||
|
if (blend.g > blend.b)
|
||||||
|
return c000 + blend.r * (c100 - c000) + blend.g * (c110 - c100) + blend.b * (c111 - c110);
|
||||||
|
if (blend.r > blend.b)
|
||||||
|
return c000 + blend.r * (c100 - c000) + blend.b * (c101 - c100) + blend.g * (c111 - c101);
|
||||||
|
return c000 + blend.b * (c001 - c000) + blend.r * (c101 - c001) + blend.g * (c111 - c101);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blend.b > blend.g)
|
||||||
|
return c000 + blend.b * (c001 - c000) + blend.g * (c011 - c001) + blend.r * (c111 - c011);
|
||||||
|
if (blend.b > blend.r)
|
||||||
|
return c000 + blend.g * (c010 - c000) + blend.b * (c011 - c010) + blend.r * (c111 - c011);
|
||||||
|
return c000 + blend.g * (c010 - c000) + blend.r * (c110 - c010) + blend.b * (c111 - c110);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash12(float2 value)
|
||||||
|
{
|
||||||
|
float3 p = frac(float3(value.xyx) * 0.1031);
|
||||||
|
p += dot(p, p.yzx + 33.33);
|
||||||
|
return frac((p.x + p.y) * p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 outputDither(float2 pixel)
|
||||||
|
{
|
||||||
|
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
|
||||||
|
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
|
||||||
|
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));
|
||||||
|
return float3(r, g, b) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float4 source = context.sourceColor;
|
||||||
|
float3 inputColor = source.rgb * pow(2.0, preExposure);
|
||||||
|
if (clampInput)
|
||||||
|
inputColor = saturate(inputColor);
|
||||||
|
|
||||||
|
float3 lutColor = applyLut33(inputColor);
|
||||||
|
float3 graded = lerp(inputColor, lutColor, lutStrength);
|
||||||
|
graded = (graded - 0.5) * postContrast + 0.5;
|
||||||
|
graded += outputDither(context.uv * context.outputResolution) * ditherAmount;
|
||||||
|
|
||||||
|
return float4(saturate(graded), source.a);
|
||||||
|
}
|
||||||
35940
shaders/lut-apply/test-lut.cube
Normal file
35940
shaders/lut-apply/test-lut.cube
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,14 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float safeScale = max(scale, 0.001);
|
float safeScale = max(scale, 0.001);
|
||||||
float safeRingRadius = max(ringRadius, 0.001);
|
float safeRingRadius = max(ringRadius, 0.001);
|
||||||
float safeTightness = max(tightness, 0.001);
|
float safeTightness = max(tightness, 0.001);
|
||||||
float time = context.time * speed;
|
float seed = context.startupRandom;
|
||||||
|
float time = context.time * speed + seed * 37.0;
|
||||||
|
|
||||||
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
|
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
|
||||||
p -= center;
|
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||||
|
|
||||||
float iterator = 0.2;
|
float iterator = 0.2;
|
||||||
float2 diagonal = float2(-1.0, 1.0);
|
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
|
||||||
float2 blackholeCenter = p - iterator * diagonal;
|
float2 blackholeCenter = p - iterator * diagonal;
|
||||||
float gravity = iterator * strength / max(dot(blackholeCenter, blackholeCenter), 0.0001);
|
float gravity = iterator * strength / max(dot(blackholeCenter, blackholeCenter), 0.0001);
|
||||||
float2 skew = diagonal / (0.1 + gravity);
|
float2 skew = diagonal / (0.1 + gravity);
|
||||||
|
|||||||
8
shaders/smpte-color-bars/shader.json
Normal file
8
shaders/smpte-color-bars/shader.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"id": "smpte-color-bars",
|
||||||
|
"name": "SMPTE Color Bars",
|
||||||
|
"description": "Generates a procedural SMPTE RP 219-style 16:9 color bar test pattern matching the common Wikimedia 1920x1080 reference layout.",
|
||||||
|
"category": "Calibration",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
90
shaders/smpte-color-bars/shader.slang
Normal file
90
shaders/smpte-color-bars/shader.slang
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
float3 hexColor(float r, float g, float b)
|
||||||
|
{
|
||||||
|
return float3(r, g, b) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 smpteTop(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0)
|
||||||
|
return hexColor(102.0, 102.0, 102.0);
|
||||||
|
if (x < 445.0)
|
||||||
|
return hexColor(191.0, 191.0, 191.0);
|
||||||
|
if (x < 651.0)
|
||||||
|
return hexColor(191.0, 191.0, 0.0);
|
||||||
|
if (x < 857.0)
|
||||||
|
return hexColor(0.0, 191.0, 191.0);
|
||||||
|
if (x < 1063.0)
|
||||||
|
return hexColor(0.0, 191.0, 0.0);
|
||||||
|
if (x < 1269.0)
|
||||||
|
return hexColor(191.0, 0.0, 191.0);
|
||||||
|
if (x < 1475.0)
|
||||||
|
return hexColor(191.0, 0.0, 0.0);
|
||||||
|
if (x < 1680.0)
|
||||||
|
return hexColor(0.0, 0.0, 191.0);
|
||||||
|
return hexColor(102.0, 102.0, 102.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 smpteMiddleA(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0)
|
||||||
|
return hexColor(0.0, 255.0, 255.0);
|
||||||
|
if (x < 445.0)
|
||||||
|
return hexColor(0.0, 63.0, 105.0);
|
||||||
|
if (x < 1680.0)
|
||||||
|
return hexColor(191.0, 191.0, 191.0);
|
||||||
|
return hexColor(0.0, 0.0, 255.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 smpteMiddleB(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0)
|
||||||
|
return hexColor(255.0, 255.0, 0.0);
|
||||||
|
if (x < 445.0)
|
||||||
|
return hexColor(65.0, 0.0, 119.0);
|
||||||
|
if (x < 1475.0)
|
||||||
|
{
|
||||||
|
float ramp = saturate((x - 445.0) / (1475.0 - 445.0));
|
||||||
|
return float3(ramp, ramp, ramp);
|
||||||
|
}
|
||||||
|
if (x < 1680.0)
|
||||||
|
return float3(1.0, 1.0, 1.0);
|
||||||
|
return hexColor(255.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 smpteBottom(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0)
|
||||||
|
return hexColor(38.0, 38.0, 38.0);
|
||||||
|
if (x < 549.0)
|
||||||
|
return float3(0.0, 0.0, 0.0);
|
||||||
|
if (x < 960.0)
|
||||||
|
return float3(1.0, 1.0, 1.0);
|
||||||
|
if (x < 1268.0)
|
||||||
|
return float3(0.0, 0.0, 0.0);
|
||||||
|
if (x < 1337.0)
|
||||||
|
return hexColor(5.0, 5.0, 5.0);
|
||||||
|
if (x < 1405.0)
|
||||||
|
return float3(0.0, 0.0, 0.0);
|
||||||
|
if (x < 1474.0)
|
||||||
|
return hexColor(10.0, 10.0, 10.0);
|
||||||
|
if (x < 1680.0)
|
||||||
|
return float3(0.0, 0.0, 0.0);
|
||||||
|
return hexColor(38.0, 38.0, 38.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 uv = saturate(context.uv);
|
||||||
|
float2 pixel = float2(uv.x, 1.0 - uv.y) * float2(1920.0, 1080.0);
|
||||||
|
|
||||||
|
if (pixel.y < 630.0)
|
||||||
|
return float4(smpteTop(pixel.x), 1.0);
|
||||||
|
|
||||||
|
if (pixel.y < 720.0)
|
||||||
|
return float4(smpteMiddleA(pixel.x), 1.0);
|
||||||
|
|
||||||
|
if (pixel.y < 810.0)
|
||||||
|
return float4(smpteMiddleB(pixel.x), 1.0);
|
||||||
|
|
||||||
|
return float4(smpteBottom(pixel.x), 1.0);
|
||||||
|
}
|
||||||
15
shaders/solid-color/shader.json
Normal file
15
shaders/solid-color/shader.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"id": "solid-color",
|
||||||
|
"name": "Solid Color",
|
||||||
|
"description": "Fills the frame with a single user-selected color.",
|
||||||
|
"category": "Color",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "fillColor",
|
||||||
|
"label": "Fill",
|
||||||
|
"type": "color",
|
||||||
|
"default": [1.0, 1.0, 1.0, 1.0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4
shaders/solid-color/shader.slang
Normal file
4
shaders/solid-color/shader.slang
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
return saturate(fillColor);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user