Compare commits
186 Commits
120f899b0d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dfba5dbc40 | |||
| 659fbef1a5 | |||
| 8a6bb81a37 | |||
| b7ce079a26 | |||
| 171b790fa3 | |||
|
|
27690c3afa | ||
|
|
a6d2ee385e | ||
|
|
fce3c3c5ae | ||
|
|
56883439a6 | ||
|
|
fbc2851ccb | ||
|
|
067c606092 | ||
|
|
0f3db3ba1b | ||
|
|
04e0802ef2 | ||
|
|
aa33d72b6e | ||
|
|
216a561ede | ||
|
|
2b995ac058 | ||
|
|
8ffc011ca0 | ||
|
|
f0f8b080ca | ||
|
|
d0b1f63524 | ||
| cfb796756d | |||
| e88a4a1f3b | |||
| 4bfabaca88 | |||
| 16548d54d3 | |||
| b96b32b441 | |||
| 3c3b1d68ff | |||
| c35ca8d61c | |||
| be9f3b4e8b | |||
| af448c338c | |||
| 283f38dddb | |||
| c5f0a9df0e | |||
| 80c6fd2434 | |||
| f8c3c60611 | |||
| 93aa8fa981 | |||
| 4e6b37304f | |||
| cec8b76f61 | |||
| 521b3cb09e | |||
| 3bd6aeb52f | |||
| e006fcc6ee | |||
| 2058f94193 | |||
| 6e8f18e24c | |||
| 2fdb1741f9 | |||
| d831b418d7 | |||
| e6faaee1ca | |||
| 64a6125c3f | |||
| 315cbda9d1 | |||
| 9787ca5f27 | |||
| 7bf5464fd2 | |||
| b7e7452567 | |||
| 35801601a5 | |||
| 0b6a2300ea | |||
| 084e60cbe0 | |||
| f2ff69fe90 | |||
| 4ddb5b6428 | |||
| 108edc096e | |||
| 269dbd0079 | |||
| 4096e9c26a | |||
| f9aac85e5f | |||
| 5cf1a09e75 | |||
| 3fc78d5bb8 | |||
| 5c46eaf18a | |||
| d68cf9b1a0 | |||
| bda9a9dc22 | |||
| c2de2c3738 | |||
| dc247ab58d | |||
| df0a77ef01 | |||
| 09efe2d6a0 | |||
| a9eeed30cf | |||
| e43ac21b2f | |||
| 081364e764 | |||
| f589b1e1fe | |||
| 7e17315e74 | |||
| bfaa3f5e0e | |||
| 1d4eb7a34c | |||
| f461a05c65 | |||
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 | ||
|
|
3e45bba54b | ||
|
|
fd4b70ec9c | ||
|
|
ce28904891 | ||
|
|
2c5e925b97 | ||
|
|
957c0be05a | ||
|
|
0a8b335048 | ||
|
|
6e32941675 | ||
|
|
5fb4607d8c | ||
|
|
f43b6f6519 | ||
|
|
dfd49fd0e3 | ||
|
|
1429b2e660 | ||
|
|
02b221f481 | ||
|
|
6a33bd02ab | ||
|
|
da7e1a93f6 | ||
|
|
334693f28c | ||
|
|
c5fd8e72b4 | ||
|
|
95b4a54326 | ||
|
|
d07ea1f63a | ||
|
|
1ddcf5d621 | ||
|
|
38d729b346 | ||
|
|
4b62627479 | ||
|
|
430cf0733d | ||
|
|
b44504500a | ||
|
|
bc690e2a87 | ||
|
|
9938a6cc26 | ||
|
|
79f7ac6c86 | ||
|
|
44b198b14d | ||
|
|
511b67c9bc | ||
|
|
c0d7e84495 | ||
|
|
4ea829af85 | ||
|
|
e0ca548ef5 | ||
|
|
2531d871e8 | ||
|
|
709d3d3fa4 | ||
|
|
ea31d0ca13 | ||
|
|
f1f4e3421b | ||
|
|
ac729dc2b9 | ||
|
|
bf23cd880a | ||
|
|
9e3412712c | ||
|
|
a434a88108 | ||
|
|
c5cead6003 | ||
|
|
f8adbbe0fe | ||
|
|
0a7954e879 | ||
|
|
f288455709 | ||
|
|
50d5880835 | ||
|
|
52eaf16a8c | ||
|
|
6b0638336a | ||
|
|
0da6ad6802 | ||
|
|
dd3cd6b66c | ||
|
|
1d08dec5fe | ||
|
|
0d57920bc1 | ||
|
|
1629dbc77a | ||
|
|
205c90e52e | ||
|
|
ab38bfad24 | ||
|
|
68503256dc | ||
|
|
a91cc91a21 | ||
|
|
a530325fa1 | ||
|
|
d332dceb5b | ||
|
|
79855d788c | ||
|
|
ff10b66d1d | ||
|
|
fdcc38c6ae | ||
|
|
718e4dcadd | ||
|
|
7740fe209c | ||
|
|
77590f4a62 | ||
|
|
e8a3805fff | ||
|
|
99fd903144 | ||
|
|
761df3b2d0 | ||
|
|
f141d20026 | ||
|
|
bfc32c4a1e | ||
|
|
20476bdf63 | ||
|
|
0ec5a4cfed | ||
|
|
539fcd3351 | ||
|
|
ebc10a9925 | ||
|
|
e5c5920ccd | ||
|
|
3b641dd07a | ||
|
|
e00e2574ed | ||
|
|
e459155d51 | ||
|
|
06f3dd4942 | ||
|
|
0808171677 | ||
|
|
00b6ad4c36 | ||
|
|
d4f6a4a268 | ||
|
|
6e600be112 | ||
|
|
a9b08f7f27 | ||
|
|
ccfc0237fd | ||
|
|
b3705d96cc | ||
|
|
5503ce85a9 | ||
|
|
41677b71ec | ||
|
|
9cbb5d8004 | ||
|
|
cbf1b541dc | ||
|
|
5cbdbd6813 | ||
|
|
b2369c418b | ||
|
|
c4883d3413 | ||
|
|
53e78890a8 | ||
|
|
36b398ea95 | ||
|
|
ba4643dfa3 | ||
|
|
27dbb55f7b | ||
|
|
f6b26bf28b | ||
|
|
861593123d | ||
|
|
34c145e80b | ||
|
|
a24cdc0630 |
@@ -7,6 +7,9 @@ on:
|
|||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
|
||||||
|
- cron: "0 14 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -15,8 +18,53 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Manual Checkout With LFS Submodules
|
||||||
uses: actions/checkout@v4
|
shell: powershell
|
||||||
|
env:
|
||||||
|
GITEA_PAT: ${{ secrets.TOKEN }}
|
||||||
|
run: |
|
||||||
|
function Invoke-Git {
|
||||||
|
& git @args
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-GiteaTokenToUrl([string]$url) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:GITEA_PAT)) {
|
||||||
|
return $url
|
||||||
|
}
|
||||||
|
return $url -replace "^https://", "https://aiden:$($env:GITEA_PAT)@"
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverUrl = "${{ gitea.server_url }}".TrimEnd("/")
|
||||||
|
$repository = "${{ gitea.repository }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($serverUrl)) {
|
||||||
|
$serverUrl = "https://git.f-40.com"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repository)) {
|
||||||
|
$repository = "aiden/video-shader-toys"
|
||||||
|
}
|
||||||
|
$repoUrl = Add-GiteaTokenToUrl "$serverUrl/$repository.git"
|
||||||
|
|
||||||
|
Invoke-Git init .
|
||||||
|
Invoke-Git lfs install --local
|
||||||
|
Invoke-Git remote add origin "$repoUrl"
|
||||||
|
Invoke-Git fetch --depth=1 origin "${{ gitea.ref }}"
|
||||||
|
Invoke-Git checkout FETCH_HEAD
|
||||||
|
|
||||||
|
Invoke-Git lfs pull origin "${{ gitea.ref }}"
|
||||||
|
Invoke-Git submodule sync --recursive
|
||||||
|
$submoduleUrl = Invoke-Git config --file .gitmodules --get submodule.video-io-3rdParty.url
|
||||||
|
$submoduleUrl = Add-GiteaTokenToUrl "$submoduleUrl"
|
||||||
|
Invoke-Git config submodule.video-io-3rdParty.url "$submoduleUrl"
|
||||||
|
Invoke-Git submodule update --init --recursive --depth=1
|
||||||
|
Invoke-Git -C video-io-3rdParty lfs pull
|
||||||
|
|
||||||
|
Write-Host "Parent LFS files:"
|
||||||
|
Invoke-Git lfs ls-files
|
||||||
|
Write-Host "Submodule LFS files:"
|
||||||
|
Invoke-Git -C video-io-3rdParty lfs ls-files
|
||||||
|
|
||||||
- name: Verify Visual Studio ATL
|
- name: Verify Visual Studio ATL
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -31,37 +79,81 @@ jobs:
|
|||||||
- name: Configure Debug
|
- name: Configure Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
|
$thirdPartyRoot = "${{ vars.THIRD_PARTY_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = $env:THIRD_PARTY_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = Join-Path $PWD "video-io-3rdParty"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = Join-Path $PWD "3rdParty"
|
||||||
|
}
|
||||||
|
|
||||||
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
$slangRoot = $env:SLANG_ROOT
|
$slangRoot = $env:SLANG_ROOT
|
||||||
}
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
$slangRoot = Join-Path $thirdPartyRoot "slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$msdfAtlasGenRoot = "${{ vars.MSDF_ATLAS_GEN_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
|
||||||
|
$msdfAtlasGenRoot = $env:MSDF_ATLAS_GEN_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
|
||||||
|
$msdfAtlasGenRoot = Join-Path $thirdPartyRoot "msdf-atlas-gen"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ndiSdkRoot = "${{ vars.NDI_SDK_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
|
||||||
|
$ndiSdkRoot = $env:NDI_SDK_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
|
||||||
|
$ndiSdkRoot = Join-Path $thirdPartyRoot "NDI 6 SDK"
|
||||||
|
}
|
||||||
|
|
||||||
|
$deckLinkSdkRoot = "${{ vars.DECKLINK_SDK_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
|
||||||
|
$deckLinkSdkRoot = $env:DECKLINK_SDK_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
|
||||||
|
$deckLinkSdkRoot = Join-Path $thirdPartyRoot "Blackmagic DeckLink SDK 16.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
$requiredFiles = @(
|
$requiredFiles = @(
|
||||||
(Join-Path $slangRoot "bin\slangc.exe"),
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
(Join-Path $slangRoot "LICENSE")
|
(Join-Path $slangRoot "LICENSE"),
|
||||||
|
(Join-Path $msdfAtlasGenRoot "msdf-atlas-gen.exe"),
|
||||||
|
(Join-Path $ndiSdkRoot "Include\Processing.NDI.Lib.h"),
|
||||||
|
(Join-Path $ndiSdkRoot "Lib\x64\Processing.NDI.Lib.x64.lib"),
|
||||||
|
(Join-Path $ndiSdkRoot "Bin\x64\Processing.NDI.Lib.x64.dll"),
|
||||||
|
(Join-Path $deckLinkSdkRoot "Win\include\DeckLinkAPI.idl")
|
||||||
)
|
)
|
||||||
|
|
||||||
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
if ($missingFiles.Count -gt 0) {
|
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 ', ')"
|
Write-Error "Missing native third-party dependencies. Initialize the private video-io-3rdParty submodule, set THIRD_PARTY_ROOT, or configure per-SDK repository variables. Missing: $($missingFiles -join ', ')"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
|
||||||
Write-Host "Using SLANG_ROOT=$slangRoot"
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
cmake --preset vs2022-x64-debug -DSLANG_ROOT="$slangRoot"
|
Write-Host "Using MSDF_ATLAS_GEN_ROOT=$msdfAtlasGenRoot"
|
||||||
|
Write-Host "Using NDI_SDK_ROOT=$ndiSdkRoot"
|
||||||
|
Write-Host "Using DECKLINK_SDK_ROOT=$deckLinkSdkRoot"
|
||||||
|
cmake --preset vs2022-x64-debug -DTHIRD_PARTY_ROOT="$thirdPartyRoot" -DSLANG_ROOT="$slangRoot" -DMSDF_ATLAS_GEN_ROOT="$msdfAtlasGenRoot" -DNDI_SDK_ROOT="$ndiSdkRoot" -DDECKLINK_SDK_ROOT="$deckLinkSdkRoot"
|
||||||
|
|
||||||
- name: Build Debug
|
- name: Build Debug
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --build --preset build-debug
|
run: cmake --build --preset build-debug --parallel
|
||||||
|
|
||||||
- name: Run Native Tests And Shader Validation
|
- name: Run Native Tests And Shader Validation
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --build --preset build-debug --target RUN_TESTS
|
run: cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||||
|
|
||||||
ui-ubuntu:
|
ui-ubuntu:
|
||||||
name: React UI Build
|
name: React UI Build
|
||||||
@@ -82,13 +174,59 @@ jobs:
|
|||||||
package-windows:
|
package-windows:
|
||||||
name: Windows Release Package
|
name: Windows Release Package
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||||
needs:
|
needs:
|
||||||
- native-windows
|
- native-windows
|
||||||
- ui-ubuntu
|
- ui-ubuntu
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Manual Checkout With LFS Submodules
|
||||||
uses: actions/checkout@v4
|
shell: powershell
|
||||||
|
env:
|
||||||
|
GITEA_PAT: ${{ secrets.TOKEN }}
|
||||||
|
run: |
|
||||||
|
function Invoke-Git {
|
||||||
|
& git @args
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-GiteaTokenToUrl([string]$url) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:GITEA_PAT)) {
|
||||||
|
return $url
|
||||||
|
}
|
||||||
|
return $url -replace "^https://", "https://aiden:$($env:GITEA_PAT)@"
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverUrl = "${{ gitea.server_url }}".TrimEnd("/")
|
||||||
|
$repository = "${{ gitea.repository }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($serverUrl)) {
|
||||||
|
$serverUrl = "https://git.f-40.com"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repository)) {
|
||||||
|
$repository = "aiden/video-shader-toys"
|
||||||
|
}
|
||||||
|
$repoUrl = Add-GiteaTokenToUrl "$serverUrl/$repository.git"
|
||||||
|
|
||||||
|
Invoke-Git init .
|
||||||
|
Invoke-Git lfs install --local
|
||||||
|
Invoke-Git remote add origin "$repoUrl"
|
||||||
|
Invoke-Git fetch --depth=1 origin "${{ gitea.ref }}"
|
||||||
|
Invoke-Git checkout FETCH_HEAD
|
||||||
|
|
||||||
|
Invoke-Git lfs pull origin "${{ gitea.ref }}"
|
||||||
|
Invoke-Git submodule sync --recursive
|
||||||
|
$submoduleUrl = Invoke-Git config --file .gitmodules --get submodule.video-io-3rdParty.url
|
||||||
|
$submoduleUrl = Add-GiteaTokenToUrl "$submoduleUrl"
|
||||||
|
Invoke-Git config submodule.video-io-3rdParty.url "$submoduleUrl"
|
||||||
|
Invoke-Git submodule update --init --recursive --depth=1
|
||||||
|
Invoke-Git -C video-io-3rdParty lfs pull
|
||||||
|
|
||||||
|
Write-Host "Parent LFS files:"
|
||||||
|
Invoke-Git lfs ls-files
|
||||||
|
Write-Host "Submodule LFS files:"
|
||||||
|
Invoke-Git -C video-io-3rdParty lfs ls-files
|
||||||
|
|
||||||
- name: Verify Visual Studio ATL
|
- name: Verify Visual Studio ATL
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -110,33 +248,77 @@ jobs:
|
|||||||
- name: Configure Release
|
- name: Configure Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
|
$thirdPartyRoot = "${{ vars.THIRD_PARTY_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = $env:THIRD_PARTY_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = Join-Path $PWD "video-io-3rdParty"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $thirdPartyRoot)) {
|
||||||
|
$thirdPartyRoot = Join-Path $PWD "3rdParty"
|
||||||
|
}
|
||||||
|
|
||||||
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
$slangRoot = "${{ vars.SLANG_ROOT }}"
|
||||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
$slangRoot = $env:SLANG_ROOT
|
$slangRoot = $env:SLANG_ROOT
|
||||||
}
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||||
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
|
$slangRoot = Join-Path $thirdPartyRoot "slang-2026.8-windows-x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
$msdfAtlasGenRoot = "${{ vars.MSDF_ATLAS_GEN_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
|
||||||
|
$msdfAtlasGenRoot = $env:MSDF_ATLAS_GEN_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
|
||||||
|
$msdfAtlasGenRoot = Join-Path $thirdPartyRoot "msdf-atlas-gen"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ndiSdkRoot = "${{ vars.NDI_SDK_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
|
||||||
|
$ndiSdkRoot = $env:NDI_SDK_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
|
||||||
|
$ndiSdkRoot = Join-Path $thirdPartyRoot "NDI 6 SDK"
|
||||||
|
}
|
||||||
|
|
||||||
|
$deckLinkSdkRoot = "${{ vars.DECKLINK_SDK_ROOT }}"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
|
||||||
|
$deckLinkSdkRoot = $env:DECKLINK_SDK_ROOT
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
|
||||||
|
$deckLinkSdkRoot = Join-Path $thirdPartyRoot "Blackmagic DeckLink SDK 16.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
$requiredFiles = @(
|
$requiredFiles = @(
|
||||||
(Join-Path $slangRoot "bin\slangc.exe"),
|
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||||
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
(Join-Path $slangRoot "bin\slang-compiler.dll"),
|
||||||
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
(Join-Path $slangRoot "bin\slang-glslang.dll"),
|
||||||
(Join-Path $slangRoot "LICENSE")
|
(Join-Path $slangRoot "LICENSE"),
|
||||||
|
(Join-Path $msdfAtlasGenRoot "msdf-atlas-gen.exe"),
|
||||||
|
(Join-Path $ndiSdkRoot "Include\Processing.NDI.Lib.h"),
|
||||||
|
(Join-Path $ndiSdkRoot "Lib\x64\Processing.NDI.Lib.x64.lib"),
|
||||||
|
(Join-Path $ndiSdkRoot "Bin\x64\Processing.NDI.Lib.x64.dll"),
|
||||||
|
(Join-Path $deckLinkSdkRoot "Win\include\DeckLinkAPI.idl")
|
||||||
)
|
)
|
||||||
|
|
||||||
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
|
||||||
if ($missingFiles.Count -gt 0) {
|
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 ', ')"
|
Write-Error "Missing native third-party dependencies. Initialize the private video-io-3rdParty submodule, set THIRD_PARTY_ROOT, or configure per-SDK repository variables. Missing: $($missingFiles -join ', ')"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
|
||||||
Write-Host "Using SLANG_ROOT=$slangRoot"
|
Write-Host "Using SLANG_ROOT=$slangRoot"
|
||||||
cmake --preset vs2022-x64-release -DSLANG_ROOT="$slangRoot"
|
Write-Host "Using MSDF_ATLAS_GEN_ROOT=$msdfAtlasGenRoot"
|
||||||
|
Write-Host "Using NDI_SDK_ROOT=$ndiSdkRoot"
|
||||||
|
Write-Host "Using DECKLINK_SDK_ROOT=$deckLinkSdkRoot"
|
||||||
|
cmake --preset vs2022-x64-release -DTHIRD_PARTY_ROOT="$thirdPartyRoot" -DSLANG_ROOT="$slangRoot" -DMSDF_ATLAS_GEN_ROOT="$msdfAtlasGenRoot" -DNDI_SDK_ROOT="$ndiSdkRoot" -DDECKLINK_SDK_ROOT="$deckLinkSdkRoot"
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: cmake --build --preset build-release
|
run: cmake --build --preset build-release --parallel
|
||||||
|
|
||||||
- name: Install Runtime Package
|
- name: Install Runtime Package
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "video-io-3rdParty"]
|
||||||
|
path = video-io-3rdParty
|
||||||
|
url = https://git.f-40.com/aiden/video-io-3rdParty.git
|
||||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -2,16 +2,21 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Debug LoopThroughWithOpenGLCompositing",
|
"name": "Debug RenderCadenceCompositor",
|
||||||
"type": "cppvsdbg",
|
"type": "cppvsdbg",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
|
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
|
||||||
"args": [],
|
"args": [],
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
"cwd": "${workspaceFolder}",
|
||||||
"environment": [],
|
"environment": [],
|
||||||
"console": "internalConsole",
|
"console": "externalTerminal",
|
||||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"requireExactSource": true,
|
||||||
|
"logging": {
|
||||||
|
"moduleLoad": true
|
||||||
|
},
|
||||||
|
"preLaunchTask": "Build RenderCadenceCompositor Debug x64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"/config/runtime-host.json",
|
||||||
|
"/config/runtime-host.*.json"
|
||||||
|
],
|
||||||
|
"url": "./config/runtime-host.schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@@ -2,42 +2,72 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Build LoopThroughWithOpenGLCompositing Debug x64",
|
"label": "Configure Debug x64",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
"command": "cmake",
|
||||||
|
"args": [
|
||||||
|
"--preset",
|
||||||
|
"vs2022-x64-debug"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build RenderCadenceCompositor Debug x64",
|
||||||
|
"type": "process",
|
||||||
|
"command": "cmake",
|
||||||
"args": [
|
"args": [
|
||||||
"--build",
|
"--build",
|
||||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||||
"--config",
|
"--config",
|
||||||
"Debug",
|
"Debug",
|
||||||
"--target",
|
"--target",
|
||||||
"LoopThroughWithOpenGLCompositing"
|
"RenderCadenceCompositor",
|
||||||
|
"--parallel"
|
||||||
],
|
],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
|
"dependsOn": "Configure Debug x64",
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Build LoopThroughWithOpenGLCompositing Release x64",
|
"label": "Build RenderCadenceCompositor Release x64",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
"command": "cmake",
|
||||||
"args": [
|
"args": [
|
||||||
"--build",
|
"--build",
|
||||||
"${workspaceFolder}\\build\\vs2022-x64-release",
|
"${workspaceFolder}\\build\\vs2022-x64-release",
|
||||||
"--config",
|
"--config",
|
||||||
"Release",
|
"Release",
|
||||||
"--target",
|
"--target",
|
||||||
"LoopThroughWithOpenGLCompositing"
|
"RenderCadenceCompositor",
|
||||||
|
"--parallel"
|
||||||
],
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
|
"label": "Run Native Tests Debug x64",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
"command": "cmake",
|
||||||
|
"args": [
|
||||||
|
"--build",
|
||||||
|
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||||
|
"--config",
|
||||||
|
"Debug",
|
||||||
|
"--target",
|
||||||
|
"RUN_TESTS",
|
||||||
|
"--parallel"
|
||||||
|
],
|
||||||
|
"group": "test",
|
||||||
|
"dependsOn": "Configure Debug x64",
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Clean Debug x64",
|
||||||
|
"type": "process",
|
||||||
|
"command": "cmake",
|
||||||
"args": [
|
"args": [
|
||||||
"--build",
|
"--build",
|
||||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||||
|
|||||||
613
CMakeLists.txt
613
CMakeLists.txt
@@ -1,368 +1,315 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
project(video_shader LANGUAGES C CXX RC)
|
project(video_shader LANGUAGES C CXX)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
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(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||||
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(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||||
|
|
||||||
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
set(LEGACY_THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty")
|
||||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
set(DEFAULT_THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/video-io-3rdParty")
|
||||||
|
if(NOT EXISTS "${DEFAULT_THIRD_PARTY_ROOT}")
|
||||||
|
set(DEFAULT_THIRD_PARTY_ROOT "${LEGACY_THIRD_PARTY_ROOT}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(THIRD_PARTY_ROOT "${DEFAULT_THIRD_PARTY_ROOT}" CACHE PATH "Path to the third-party SDK bundle")
|
||||||
|
if(EXISTS "${DEFAULT_THIRD_PARTY_ROOT}" AND THIRD_PARTY_ROOT STREQUAL LEGACY_THIRD_PARTY_ROOT AND NOT EXISTS "${THIRD_PARTY_ROOT}")
|
||||||
|
set(THIRD_PARTY_ROOT "${DEFAULT_THIRD_PARTY_ROOT}" CACHE PATH "Path to the third-party SDK bundle" FORCE)
|
||||||
|
endif()
|
||||||
|
set(SLANG_ROOT "${THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||||
|
set(MSDF_ATLAS_GEN_ROOT "${THIRD_PARTY_ROOT}/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
|
||||||
|
set(NDI_SDK_ROOT "${THIRD_PARTY_ROOT}/NDI 6 SDK" CACHE PATH "Path to the NDI SDK")
|
||||||
|
set(DECKLINK_SDK_ROOT "${THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0" CACHE PATH "Path to the Blackmagic DeckLink SDK")
|
||||||
|
|
||||||
|
set(LEGACY_SLANG_ROOT "${LEGACY_THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64")
|
||||||
|
set(LEGACY_MSDF_ATLAS_GEN_ROOT "${LEGACY_THIRD_PARTY_ROOT}/msdf-atlas-gen")
|
||||||
|
set(LEGACY_NDI_SDK_ROOT "${LEGACY_THIRD_PARTY_ROOT}/NDI 6 SDK")
|
||||||
|
set(LEGACY_DECKLINK_SDK_ROOT "${LEGACY_THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0")
|
||||||
|
if(EXISTS "${DEFAULT_THIRD_PARTY_ROOT}")
|
||||||
|
if(SLANG_ROOT STREQUAL LEGACY_SLANG_ROOT AND NOT EXISTS "${SLANG_ROOT}")
|
||||||
|
set(SLANG_ROOT "${THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe" FORCE)
|
||||||
|
endif()
|
||||||
|
if(MSDF_ATLAS_GEN_ROOT STREQUAL LEGACY_MSDF_ATLAS_GEN_ROOT AND NOT EXISTS "${MSDF_ATLAS_GEN_ROOT}")
|
||||||
|
set(MSDF_ATLAS_GEN_ROOT "${THIRD_PARTY_ROOT}/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release" FORCE)
|
||||||
|
endif()
|
||||||
|
if(NDI_SDK_ROOT STREQUAL LEGACY_NDI_SDK_ROOT AND NOT EXISTS "${NDI_SDK_ROOT}")
|
||||||
|
set(NDI_SDK_ROOT "${THIRD_PARTY_ROOT}/NDI 6 SDK" CACHE PATH "Path to the NDI SDK" FORCE)
|
||||||
|
endif()
|
||||||
|
if(DECKLINK_SDK_ROOT STREQUAL LEGACY_DECKLINK_SDK_ROOT AND NOT EXISTS "${DECKLINK_SDK_ROOT}")
|
||||||
|
set(DECKLINK_SDK_ROOT "${THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0" CACHE PATH "Path to the Blackmagic DeckLink SDK" FORCE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(VIDEO_SHADER_INCLUDE_DIRS
|
||||||
|
"${SRC_DIR}"
|
||||||
|
"${SRC_DIR}/app"
|
||||||
|
"${SRC_DIR}/control"
|
||||||
|
"${SRC_DIR}/control/http"
|
||||||
|
"${SRC_DIR}/frames"
|
||||||
|
"${SRC_DIR}/json"
|
||||||
|
"${SRC_DIR}/logging"
|
||||||
|
"${SRC_DIR}/platform"
|
||||||
|
"${SRC_DIR}/preview"
|
||||||
|
"${SRC_DIR}/render"
|
||||||
|
"${SRC_DIR}/render/readback"
|
||||||
|
"${SRC_DIR}/render/thread"
|
||||||
|
"${SRC_DIR}/render/runtime"
|
||||||
|
"${SRC_DIR}/runtime"
|
||||||
|
"${SRC_DIR}/runtime/catalog"
|
||||||
|
"${SRC_DIR}/runtime/layers"
|
||||||
|
"${SRC_DIR}/runtime/shader"
|
||||||
|
"${SRC_DIR}/runtime/state"
|
||||||
|
"${SRC_DIR}/runtime/text"
|
||||||
|
"${SRC_DIR}/shader"
|
||||||
|
"${SRC_DIR}/telemetry"
|
||||||
|
"${SRC_DIR}/video"
|
||||||
|
"${SRC_DIR}/video/core"
|
||||||
|
"${SRC_DIR}/video/decklink"
|
||||||
|
"${SRC_DIR}/video/ndi"
|
||||||
|
"${SRC_DIR}/video/playout"
|
||||||
|
)
|
||||||
|
|
||||||
|
function(video_shader_target_defaults target)
|
||||||
|
target_include_directories(${target} PRIVATE ${VIDEO_SHADER_INCLUDE_DIRS})
|
||||||
|
target_compile_definitions(${target} PRIVATE _UNICODE UNICODE)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(${target} PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(video_shader_files_exist out_var)
|
||||||
|
set(missing_files)
|
||||||
|
foreach(file IN LISTS ARGN)
|
||||||
|
if(NOT EXISTS "${file}")
|
||||||
|
list(APPEND missing_files "${file}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
set(${out_var} "${missing_files}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(add_video_shader_test name)
|
||||||
|
add_executable(${name} ${ARGN})
|
||||||
|
video_shader_target_defaults(${name})
|
||||||
|
add_test(NAME ${name} COMMAND ${name})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(RUNTIME_JSON_SOURCES
|
||||||
|
"${SRC_DIR}/runtime/state/RuntimeJson.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(RUNTIME_PARAMETER_SOURCES
|
||||||
|
${RUNTIME_JSON_SOURCES}
|
||||||
|
"${SRC_DIR}/runtime/state/RuntimeParameterUtils.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(RUNTIME_STATE_SOURCES
|
||||||
|
${RUNTIME_JSON_SOURCES}
|
||||||
|
"${SRC_DIR}/runtime/state/RuntimeStatePersistence.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(RUNTIME_LAYER_SOURCES
|
||||||
|
"${SRC_DIR}/runtime/layers/RuntimeLayerModel.cpp"
|
||||||
|
"${SRC_DIR}/runtime/layers/RuntimeLayerReload.cpp"
|
||||||
|
"${SRC_DIR}/runtime/layers/RuntimeLayerSnapshot.cpp"
|
||||||
|
"${SRC_DIR}/runtime/layers/RuntimeLayerStateRestore.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(RUNTIME_TEXT_SOURCES
|
||||||
|
"${SRC_DIR}/runtime/text/FontAtlasBuilder.cpp"
|
||||||
|
"${SRC_DIR}/runtime/text/FontAtlasImageLoader.cpp"
|
||||||
|
"${SRC_DIR}/runtime/text/FontAtlasMetadata.cpp"
|
||||||
|
"${SRC_DIR}/runtime/text/FontAtlasProcess.cpp"
|
||||||
|
"${SRC_DIR}/runtime/text/RuntimeTextTextureComposer.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(RUNTIME_CATALOG_SOURCES
|
||||||
|
"${SRC_DIR}/runtime/catalog/SupportedShaderCatalog.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SHADER_MANIFEST_SOURCES
|
||||||
|
"${SRC_DIR}/shader/ShaderManifestAssets.cpp"
|
||||||
|
"${SRC_DIR}/shader/ShaderManifestParameters.cpp"
|
||||||
|
"${SRC_DIR}/shader/ShaderManifestParser.cpp"
|
||||||
|
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${SRC_DIR}/shader/ShaderUiPath.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(VIDEO_MODE_SOURCES
|
||||||
|
"${SRC_DIR}/video/core/VideoMode.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
set(VIDEO_FORMAT_SOURCES
|
||||||
|
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
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"
|
||||||
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
||||||
)
|
)
|
||||||
|
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||||
|
set(MSDF_ATLAS_GEN_EXE_FILE "${MSDF_ATLAS_GEN_ROOT}/msdf-atlas-gen.exe")
|
||||||
|
file(GLOB MSDF_ATLAS_GEN_DLL_FILES CONFIGURE_DEPENDS
|
||||||
|
"${MSDF_ATLAS_GEN_ROOT}/*.dll"
|
||||||
|
)
|
||||||
|
set(MSDF_ATLAS_GEN_RUNTIME_FILES
|
||||||
|
"${MSDF_ATLAS_GEN_EXE_FILE}"
|
||||||
|
${MSDF_ATLAS_GEN_DLL_FILES}
|
||||||
|
)
|
||||||
|
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
|
||||||
|
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
|
||||||
|
set(NDI_INCLUDE_DIR "${NDI_SDK_ROOT}/Include")
|
||||||
|
set(NDI_RUNTIME_DLL "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.x64.dll")
|
||||||
|
set(NDI_IMPORT_LIB "${NDI_SDK_ROOT}/Lib/x64/Processing.NDI.Lib.x64.lib")
|
||||||
|
set(NDI_LICENSE_FILE "${NDI_SDK_ROOT}/NDI SDK License Agreement.pdf")
|
||||||
|
set(NDI_NOTICES_FILE "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.Licenses.txt")
|
||||||
|
set(DECKLINK_SDK_IDL_FILE "${DECKLINK_SDK_ROOT}/Win/include/DeckLinkAPI.idl")
|
||||||
|
set(DECKLINK_SDK_LICENSE_FILE "${DECKLINK_SDK_ROOT}/End User License Agreement.pdf")
|
||||||
|
|
||||||
foreach(SLANG_RUNTIME_FILE IN LISTS SLANG_RUNTIME_FILES)
|
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
||||||
if(NOT EXISTS "${SLANG_RUNTIME_FILE}")
|
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
||||||
message(FATAL_ERROR "Required Slang runtime file not found: ${SLANG_RUNTIME_FILE}")
|
"${SRC_DIR}/video/decklink/DeckLinkAPI_i.c"
|
||||||
|
"${SRC_DIR}/video/decklink/DeckLinkAPI_h.h"
|
||||||
|
)
|
||||||
|
|
||||||
|
video_shader_files_exist(RENDER_CADENCE_APP_MISSING_FILES ${RENDER_CADENCE_APP_REQUIRED_FILES})
|
||||||
|
if(EXISTS "${DECKLINK_SDK_IDL_FILE}")
|
||||||
|
message(STATUS "Blackmagic DeckLink SDK found: ${DECKLINK_SDK_ROOT}")
|
||||||
|
else()
|
||||||
|
message(STATUS "Blackmagic DeckLink SDK not found at ${DECKLINK_SDK_ROOT}; using checked-in DeckLink API shim files only")
|
||||||
|
endif()
|
||||||
|
if(RENDER_CADENCE_APP_MISSING_FILES)
|
||||||
|
message(STATUS "RenderCadenceCompositor target skipped; required app entry or external DeckLink SDK shim files are missing:")
|
||||||
|
foreach(missing_file IN LISTS RENDER_CADENCE_APP_MISSING_FILES)
|
||||||
|
message(STATUS " ${missing_file}")
|
||||||
|
endforeach()
|
||||||
|
else()
|
||||||
|
file(GLOB_RECURSE RENDER_CADENCE_APP_SOURCES CONFIGURE_DEPENDS
|
||||||
|
"${SRC_DIR}/*.c"
|
||||||
|
"${SRC_DIR}/*.cpp"
|
||||||
|
"${SRC_DIR}/*.h"
|
||||||
|
)
|
||||||
|
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||||
|
video_shader_target_defaults(RenderCadenceCompositor)
|
||||||
|
if(EXISTS "${NDI_INCLUDE_DIR}" AND EXISTS "${NDI_IMPORT_LIB}")
|
||||||
|
target_include_directories(RenderCadenceCompositor PRIVATE "${NDI_INCLUDE_DIR}")
|
||||||
|
target_link_libraries(RenderCadenceCompositor PRIVATE "${NDI_IMPORT_LIB}")
|
||||||
|
if(EXISTS "${NDI_RUNTIME_DLL}")
|
||||||
|
add_custom_command(TARGET RenderCadenceCompositor POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${NDI_RUNTIME_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:RenderCadenceCompositor>/Processing.NDI.Lib.x64.dll"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
message(STATUS "NDI SDK headers/import library not found; NDI backends will not build correctly: ${NDI_SDK_ROOT}")
|
||||||
|
endif()
|
||||||
|
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||||
|
opengl32
|
||||||
|
Ole32
|
||||||
|
Windowscodecs
|
||||||
|
Ws2_32
|
||||||
|
)
|
||||||
|
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET RenderCadenceCompositor)
|
||||||
|
install(TARGETS RenderCadenceCompositor
|
||||||
|
RUNTIME DESTINATION "."
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(slang_runtime_file IN LISTS SLANG_RUNTIME_FILES)
|
||||||
|
if(EXISTS "${slang_runtime_file}")
|
||||||
|
install(FILES "${slang_runtime_file}"
|
||||||
|
DESTINATION "3rdParty/slang/bin"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "Slang runtime file not found and will not be installed: ${slang_runtime_file}")
|
||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
foreach(msdf_runtime_file IN LISTS MSDF_ATLAS_GEN_RUNTIME_FILES)
|
||||||
if(NOT EXISTS "${SLANG_LICENSE_FILE}")
|
if(EXISTS "${msdf_runtime_file}")
|
||||||
message(FATAL_ERROR "Slang license file not found: ${SLANG_LICENSE_FILE}")
|
install(FILES "${msdf_runtime_file}"
|
||||||
|
DESTINATION "3rdParty/msdf-atlas-gen"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "msdf-atlas-gen runtime file not found and will not be installed: ${msdf_runtime_file}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if(EXISTS "${MSDF_ATLAS_GEN_LICENSE_FILE}")
|
||||||
|
install(FILES "${MSDF_ATLAS_GEN_LICENSE_FILE}"
|
||||||
|
DESTINATION "third_party_notices"
|
||||||
|
RENAME "MSDF_ATLAS_GEN_LICENSE.txt"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "msdf-atlas-gen license file not found: ${MSDF_ATLAS_GEN_LICENSE_FILE}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(APP_SOURCES
|
if(EXISTS "${MSDF_ATLAS_GEN_README_FILE}")
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
install(FILES "${MSDF_ATLAS_GEN_README_FILE}"
|
||||||
"${APP_DIR}/control/ControlServer.cpp"
|
DESTINATION "third_party_notices"
|
||||||
"${APP_DIR}/control/ControlServer.h"
|
RENAME "MSDF_ATLAS_GEN_README.md"
|
||||||
"${APP_DIR}/control/ControlServices.cpp"
|
)
|
||||||
"${APP_DIR}/control/ControlServices.h"
|
else()
|
||||||
"${APP_DIR}/control/OscServer.cpp"
|
message(STATUS "msdf-atlas-gen readme file not found: ${MSDF_ATLAS_GEN_README_FILE}")
|
||||||
"${APP_DIR}/control/OscServer.h"
|
|
||||||
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
|
||||||
"${APP_DIR}/control/RuntimeControlBridge.h"
|
|
||||||
"${APP_DIR}/control/RuntimeServices.cpp"
|
|
||||||
"${APP_DIR}/control/RuntimeServices.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
|
||||||
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
|
||||||
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
|
||||||
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/GlobalParamsBuffer.h"
|
|
||||||
"${APP_DIR}/gl/renderer/GlRenderConstants.h"
|
|
||||||
"${APP_DIR}/gl/renderer/GlScopedObjects.h"
|
|
||||||
"${APP_DIR}/gl/shader/GlShaderSources.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/GlShaderSources.h"
|
|
||||||
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
|
||||||
"${APP_DIR}/gl/OpenGLComposite.h"
|
|
||||||
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
|
||||||
"${APP_DIR}/gl/RenderEngine.cpp"
|
|
||||||
"${APP_DIR}/gl/RenderEngine.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.h"
|
|
||||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
|
||||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
|
||||||
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
|
||||||
"${APP_DIR}/gl/renderer/RenderTargetPool.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
|
|
||||||
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderBuildQueue.h"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/ShaderTextureBindings.h"
|
|
||||||
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
|
||||||
"${APP_DIR}/gl/shader/TextRasterizer.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/TextRasterizer.h"
|
|
||||||
"${APP_DIR}/gl/shader/TextureAssetLoader.cpp"
|
|
||||||
"${APP_DIR}/gl/shader/TextureAssetLoader.h"
|
|
||||||
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.cpp"
|
|
||||||
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.h"
|
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
|
||||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
|
||||||
"${APP_DIR}/platform/NativeHandles.h"
|
|
||||||
"${APP_DIR}/platform/NativeSockets.h"
|
|
||||||
"${APP_DIR}/resource.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeHost.h"
|
|
||||||
"${APP_DIR}/runtime/HealthTelemetry.cpp"
|
|
||||||
"${APP_DIR}/runtime/HealthTelemetry.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeCoordinator.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeCoordinator.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeSnapshotProvider.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeSnapshotProvider.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeClock.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeJson.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
|
||||||
"${APP_DIR}/runtime/RuntimeStore.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeStore.h"
|
|
||||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
|
||||||
"${APP_DIR}/shader/ShaderTypes.h"
|
|
||||||
"${APP_DIR}/stdafx.cpp"
|
|
||||||
"${APP_DIR}/stdafx.h"
|
|
||||||
"${APP_DIR}/targetver.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
|
||||||
"${APP_DIR}/videoio/VideoBackend.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoBackend.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
|
||||||
|
|
||||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/control"
|
|
||||||
"${APP_DIR}/gl"
|
|
||||||
"${APP_DIR}/gl/pipeline"
|
|
||||||
"${APP_DIR}/gl/renderer"
|
|
||||||
"${APP_DIR}/gl/shader"
|
|
||||||
"${APP_DIR}/platform"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
"${APP_DIR}/shader"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
"${APP_DIR}/videoio/decklink"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
|
||||||
opengl32
|
|
||||||
glu32
|
|
||||||
Ws2_32
|
|
||||||
Crypt32
|
|
||||||
Advapi32
|
|
||||||
Gdiplus
|
|
||||||
Ole32
|
|
||||||
Windowscodecs
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
|
||||||
_UNICODE
|
|
||||||
UNICODE
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(RuntimeJsonTests
|
if(EXISTS "${SLANG_LICENSE_FILE}")
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
install(FILES "${SLANG_LICENSE_FILE}"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
DESTINATION "third_party_notices"
|
||||||
)
|
RENAME "SLANG_LICENSE.txt"
|
||||||
|
)
|
||||||
target_include_directories(RuntimeJsonTests PRIVATE
|
else()
|
||||||
"${APP_DIR}"
|
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(RuntimeJsonTests PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_testing()
|
if(EXISTS "${NDI_RUNTIME_DLL}")
|
||||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
install(FILES "${NDI_RUNTIME_DLL}"
|
||||||
|
DESTINATION "."
|
||||||
add_executable(RuntimeClockTests
|
)
|
||||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
else()
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
message(STATUS "NDI runtime DLL not found and will not be installed: ${NDI_RUNTIME_DLL}")
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(RuntimeClockTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
if(EXISTS "${NDI_LICENSE_FILE}")
|
||||||
|
install(FILES "${NDI_LICENSE_FILE}"
|
||||||
add_executable(RuntimeParameterUtilsTests
|
DESTINATION "third_party_notices"
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
RENAME "NDI_SDK_LICENSE_AGREEMENT.pdf"
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
)
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
else()
|
||||||
)
|
message(STATUS "NDI license file not found: ${NDI_LICENSE_FILE}")
|
||||||
|
|
||||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
"${APP_DIR}/shader"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
if(EXISTS "${NDI_NOTICES_FILE}")
|
||||||
|
install(FILES "${NDI_NOTICES_FILE}"
|
||||||
add_executable(Std140BufferTests
|
DESTINATION "third_party_notices"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
RENAME "NDI_RUNTIME_LICENSES.txt"
|
||||||
)
|
)
|
||||||
|
else()
|
||||||
target_include_directories(Std140BufferTests PRIVATE
|
message(STATUS "NDI runtime notices file not found: ${NDI_NOTICES_FILE}")
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/gl"
|
|
||||||
"${APP_DIR}/gl/shader"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(Std140BufferTests PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
if(EXISTS "${DECKLINK_SDK_LICENSE_FILE}")
|
||||||
|
install(FILES "${DECKLINK_SDK_LICENSE_FILE}"
|
||||||
add_executable(ShaderPackageRegistryTests
|
DESTINATION "third_party_notices"
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
RENAME "BLACKMAGIC_DECKLINK_SDK_EULA.pdf"
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
)
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
else()
|
||||||
)
|
message(STATUS "Blackmagic DeckLink SDK license file not found: ${DECKLINK_SDK_LICENSE_FILE}")
|
||||||
|
|
||||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
"${APP_DIR}/shader"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
|
||||||
|
|
||||||
add_executable(ShaderSlangValidationTests
|
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(ShaderSlangValidationTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/platform"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
"${APP_DIR}/shader"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
|
|
||||||
set_tests_properties(ShaderSlangValidationTests PROPERTIES
|
|
||||||
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(OscServerTests
|
|
||||||
"${APP_DIR}/control/OscServer.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(OscServerTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/control"
|
|
||||||
"${APP_DIR}/platform"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(OscServerTests PRIVATE
|
|
||||||
Ws2_32
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(OscServerTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
|
||||||
|
|
||||||
add_executable(VideoIOFormatTests
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(VideoIOFormatTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
"${APP_DIR}/videoio/decklink"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(VideoIOFormatTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
|
|
||||||
|
|
||||||
add_executable(VideoPlayoutSchedulerTests
|
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
"${APP_DIR}/videoio/decklink"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
|
||||||
|
|
||||||
add_executable(VideoIODeviceFakeTests
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(VideoIODeviceFakeTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
"${APP_DIR}/videoio/decklink"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
|
||||||
|
|
||||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
|
||||||
RUNTIME DESTINATION "."
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES ${SLANG_RUNTIME_FILES}
|
|
||||||
DESTINATION "3rdParty/slang/bin"
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "${SLANG_LICENSE_FILE}"
|
|
||||||
DESTINATION "third_party_notices"
|
|
||||||
RENAME "SLANG_LICENSE.txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||||
DESTINATION "."
|
DESTINATION "."
|
||||||
)
|
)
|
||||||
@@ -392,5 +339,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
|
|||||||
DESTINATION "docs"
|
DESTINATION "docs"
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})
|
|
||||||
|
|||||||
224
README.md
224
README.md
@@ -1,27 +1,37 @@
|
|||||||
# Video Shader
|
# Video Shader
|
||||||
|
|
||||||
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
|
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink and NDI backends, Slang shader packages, and a local React control UI.
|
||||||
|
|
||||||
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
|
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
|
||||||
|
|
||||||
## Repository Layout
|
## Repository Layout
|
||||||
|
|
||||||
- `apps/LoopThroughWithOpenGLCompositing/`: native C++ host app.
|
- `src/`: native C++ host app.
|
||||||
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
|
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
|
||||||
- `ui/`: Vite/React control UI.
|
- `ui/`: Vite/React control UI.
|
||||||
- `config/runtime-host.json`: runtime configuration.
|
- `config/runtime-host.json`: runtime configuration.
|
||||||
- `runtime/templates/`: tracked shader wrapper templates.
|
- `runtime/templates/`: tracked shader wrapper templates.
|
||||||
- `runtime/`: ignored generated runtime cache/state output. See `runtime/README.md`.
|
- `runtime/`: ignored generated runtime cache/state output. See `runtime/README.md`.
|
||||||
- `tests/`: focused native tests for pure runtime logic.
|
- `tests/`: focused native tests for pure runtime logic.
|
||||||
|
- `docs/CURRENT_SYSTEM_ARCHITECTURE.md`: current architecture notes for the cadence/video I/O host.
|
||||||
|
- `docs/RENDER_CADENCE_GOLDEN_RULES.md`: guardrails for changes that touch render cadence, runtime work, and video I/O.
|
||||||
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
|
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
|
||||||
|
|
||||||
Native app internals are grouped by boundary:
|
Native app internals are grouped by boundary:
|
||||||
|
|
||||||
- `videoio/`: backend-neutral video I/O contracts, formats, and playout timing.
|
- `app/`: startup/shutdown orchestration, runtime-content controller boundary, config, preview, telemetry, and HTTP hookup.
|
||||||
- `videoio/decklink/`: DeckLink-specific device adapter, callbacks, and SDK bindings.
|
- `control/`: HTTP/WebSocket server, command parsing, and runtime-state JSON presentation.
|
||||||
- `gl/renderer/`: low-level OpenGL resources and extension helpers.
|
- `frames/`: system-memory frame exchange and input mailbox handoff.
|
||||||
- `gl/pipeline/`: frame pipeline, render passes, video I/O bridge, preview/readback, and screenshots.
|
- `render/`: render thread, readback, runtime render scene, and shared-context shader program preparation.
|
||||||
- `gl/shader/`: shader compilation, texture/text assets, UBO packing, and shader program ownership.
|
- `runtime/`: shader catalog support, layer model, Slang build bridge, font atlas build, and runtime-state persistence.
|
||||||
|
- `shader/`: shader package parsing and Slang compilation helpers.
|
||||||
|
- `video/core/`: backend-neutral video IO handoff contracts, mode descriptions, pixel formats, and output scheduling thread.
|
||||||
|
- `video/decklink/`: DeckLink input/output backend.
|
||||||
|
- `video/ndi/`: NDI input/output backend.
|
||||||
|
- `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers.
|
||||||
|
- `video/legacy/`: older backend pipeline pieces kept separate while the new edge model settles.
|
||||||
|
|
||||||
|
The runtime shader stack is plugged into the host through `src/app/RuntimeContentController.h`. The checked-in app wires `ShaderRuntimeContentController` into that slot; a fork can provide another runtime-content controller for a larger render engine while keeping the cadence/video I/O shell.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -29,27 +39,80 @@ Native app internals are grouped by boundary:
|
|||||||
- CMake 3.24 or newer.
|
- CMake 3.24 or newer.
|
||||||
- Node.js and npm for the control UI.
|
- Node.js and npm for the control UI.
|
||||||
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
||||||
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
|
- Blackmagic DeckLink SDK 16.0 for DeckLink SDK reference files and redistribution notices.
|
||||||
|
- NDI 6 SDK for NDI input/output builds. CMake expects `Include/`, `Lib/x64/Processing.NDI.Lib.x64.lib`, `Bin/x64/Processing.NDI.Lib.x64.dll`, and the NDI license/notice files.
|
||||||
|
- Slang 2026.8 Windows x86_64 binary release with `bin/slangc.exe`, `bin/slang-compiler.dll`, `bin/slang-glslang.dll`, and `LICENSE`.
|
||||||
|
- `msdf-atlas-gen` Windows binary release with `msdf-atlas-gen.exe`, `LICENSE.txt`, `README.md`, and any adjacent runtime DLLs for font atlas generation.
|
||||||
|
|
||||||
Default expected Slang path:
|
### Third-party SDK bundle
|
||||||
|
|
||||||
|
Org members can initialize the private SDK bundle submodule:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
When present, CMake defaults to this private bundle:
|
||||||
|
|
||||||
|
```text
|
||||||
|
video-io-3rdParty/
|
||||||
|
Blackmagic DeckLink SDK 16.0/
|
||||||
|
NDI 6 SDK/
|
||||||
|
slang-2026.8-windows-x86_64/
|
||||||
|
msdf-atlas-gen/
|
||||||
|
```
|
||||||
|
|
||||||
|
The parent repository is public, but this bundle is private. External builders need to obtain the SDKs from their vendors and place them in an ignored local `3rdParty/` folder with the same layout, or pass explicit CMake paths.
|
||||||
|
|
||||||
|
Fallback local Slang path:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
3rdParty/slang-2026.8-windows-x86_64
|
3rdParty/slang-2026.8-windows-x86_64
|
||||||
```
|
```
|
||||||
|
|
||||||
Override example:
|
Fallback local `msdf-atlas-gen` path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3rdParty/msdf-atlas-gen
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback local NDI SDK path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3rdParty/NDI 6 SDK
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback local Blackmagic DeckLink SDK path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3rdParty/Blackmagic DeckLink SDK 16.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Single-root override example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --preset vs2022-x64-debug -DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64"
|
cmake --preset vs2022-x64-debug -DTHIRD_PARTY_ROOT="D:/SDKs/video-io-3rdParty"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Individual override example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --preset vs2022-x64-debug `
|
||||||
|
-DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64" `
|
||||||
|
-DMSDF_ATLAS_GEN_ROOT="D:/SDKs/msdf-atlas-gen" `
|
||||||
|
-DNDI_SDK_ROOT="D:/SDKs/NDI 6 SDK" `
|
||||||
|
-DDECKLINK_SDK_ROOT="D:/SDKs/Blackmagic DeckLink SDK 16.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
At runtime, Slang compilation follows the same root order: `SLANG_ROOT`, `THIRD_PARTY_ROOT`, repo `video-io-3rdParty/`, then repo or packaged `3rdParty/`. The packaged layout uses `3rdParty/slang/bin/slangc.exe`; development bundles can use `slang-2026.8-windows-x86_64/bin/slangc.exe`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Configure and build the native app:
|
Configure and build the native app:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --preset vs2022-x64-debug
|
cmake --preset vs2022-x64-debug
|
||||||
cmake --build --preset build-debug
|
cmake --build --preset build-debug --parallel
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the React control UI:
|
Build the React control UI:
|
||||||
@@ -65,10 +128,11 @@ The native app serves `ui/dist` when it exists, otherwise it falls back to the s
|
|||||||
The control UI provides:
|
The control UI provides:
|
||||||
|
|
||||||
- A searchable shader library for adding layers.
|
- A searchable shader library for adding layers.
|
||||||
- Compact parameter rows with inline descriptions and OSC copy controls.
|
- Compact parameter rows with inline descriptions and intended OSC route copy controls.
|
||||||
- Stack save/recall presets.
|
- Shader-declared custom Web Component control panels with default-control fallback.
|
||||||
- Manual shader reload.
|
- Manual shader reload.
|
||||||
- Screenshot capture from the final output render target.
|
- Host config editing, save, restart request, and NDI input source discovery.
|
||||||
|
- Compact video I/O and render cadence status.
|
||||||
|
|
||||||
## Package
|
## Package
|
||||||
|
|
||||||
@@ -80,7 +144,7 @@ npm ci
|
|||||||
npm run build
|
npm run build
|
||||||
cd ..
|
cd ..
|
||||||
cmake --preset vs2022-x64-release
|
cmake --preset vs2022-x64-release
|
||||||
cmake --build --preset build-release
|
cmake --build --preset build-release --parallel
|
||||||
cmake --install build/vs2022-x64-release --config Release --prefix dist/VideoShader
|
cmake --install build/vs2022-x64-release --config Release --prefix dist/VideoShader
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -88,10 +152,12 @@ The package folder will contain:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
dist/VideoShader/
|
dist/VideoShader/
|
||||||
LoopThroughWithOpenGLCompositing.exe
|
RenderCadenceCompositor.exe
|
||||||
|
Processing.NDI.Lib.x64.dll
|
||||||
config/
|
config/
|
||||||
shaders/
|
shaders/
|
||||||
3rdParty/slang/bin/
|
3rdParty/slang/bin/
|
||||||
|
3rdParty/msdf-atlas-gen/
|
||||||
ui/dist/
|
ui/dist/
|
||||||
docs/
|
docs/
|
||||||
SHADER_CONTRACT.md
|
SHADER_CONTRACT.md
|
||||||
@@ -99,9 +165,9 @@ dist/VideoShader/
|
|||||||
third_party_notices/
|
third_party_notices/
|
||||||
```
|
```
|
||||||
|
|
||||||
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
You can run `RenderCadenceCompositor.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `3rdParty/msdf-atlas-gen/msdf-atlas-gen.exe`, `Processing.NDI.Lib.x64.dll`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
||||||
|
|
||||||
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It does not copy the full Slang release folder.
|
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It also copies `msdf-atlas-gen.exe`, any adjacent `msdf-atlas-gen` DLLs, `Processing.NDI.Lib.x64.dll`, and the `third_party_notices/MSDF_ATLAS_GEN_LICENSE.txt`, `third_party_notices/MSDF_ATLAS_GEN_README.md`, `third_party_notices/NDI_SDK_LICENSE_AGREEMENT.pdf`, and `third_party_notices/NDI_RUNTIME_LICENSES.txt` notice files. It does not copy full third-party release folders.
|
||||||
|
|
||||||
Create a zip for distribution:
|
Create a zip for distribution:
|
||||||
|
|
||||||
@@ -114,7 +180,7 @@ Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip
|
|||||||
Run native tests:
|
Run native tests:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cmake --build --preset build-debug --target RUN_TESTS
|
cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the UI production build check:
|
Run the UI production build check:
|
||||||
@@ -130,7 +196,6 @@ Current native test coverage includes:
|
|||||||
- Parameter normalization and preset filename safety.
|
- Parameter normalization and preset filename safety.
|
||||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||||
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||||
- OSC packet parsing.
|
|
||||||
- Slang validation for every available shader package.
|
- Slang validation for every available shader package.
|
||||||
|
|
||||||
## Runtime Configuration
|
## Runtime Configuration
|
||||||
@@ -144,19 +209,36 @@ Current native test coverage includes:
|
|||||||
"oscBindAddress": "127.0.0.1",
|
"oscBindAddress": "127.0.0.1",
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"oscSmoothing": 0.18,
|
"oscSmoothing": 0.18,
|
||||||
"inputVideoFormat": "1080p",
|
"runtimeShaderId": "",
|
||||||
"inputFrameRate": "59.94",
|
"input": {
|
||||||
"outputVideoFormat": "1080p",
|
"backend": "decklink",
|
||||||
"outputFrameRate": "59.94",
|
"device": "default",
|
||||||
|
"resolution": "1080p",
|
||||||
|
"frameRate": "59.94"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"backend": "decklink",
|
||||||
|
"device": "default",
|
||||||
|
"resolution": "1080p",
|
||||||
|
"frameRate": "59.94",
|
||||||
|
"pixelFormat": "auto",
|
||||||
|
"keying": {
|
||||||
|
"external": false,
|
||||||
|
"alphaRequired": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"autoReload": true,
|
"autoReload": true,
|
||||||
"maxTemporalHistoryFrames": 12,
|
"maxTemporalHistoryFrames": 12,
|
||||||
"enableExternalKeying": true
|
"previewEnabled": false,
|
||||||
|
"previewFps": 59.94
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
|
`input.backend` and `output.backend` select the concrete video I/O backend. Today the app supports `decklink`, `ndi`, and `none`; future backends such as Spout or file playback can be added behind the same factory boundary. `device` is a backend-neutral selector placeholder: DeckLink still chooses the first compatible device, NDI input uses it as the source selector, and NDI output uses it as the sender name.
|
||||||
|
|
||||||
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
`input.resolution`/`input.frameRate` select the video capture mode. `output.resolution`/`output.frameRate` select the playout mode through a backend-neutral mode description; the current DeckLink backend maps that mode to a `BMDDisplayMode` at the DeckLink boundary. Supported modes still depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
|
||||||
|
|
||||||
|
The checked-in config uses the nested `input` and `output` objects as the supported shape. When `input.backend` is `ndi`, the host-config editor uses NDI discovery to offer source-name suggestions in the `input.device` field while still allowing manual entry. The control UI presents `output.keying.external` and `output.keying.alphaRequired` as one **Output alpha** control; DeckLink maps that to external keying, while NDI only uses it to request an alpha-carrying system-frame format.
|
||||||
|
|
||||||
The control UI is available at:
|
The control UI is available at:
|
||||||
|
|
||||||
@@ -164,11 +246,43 @@ The control UI is available at:
|
|||||||
http://127.0.0.1:<serverPort>
|
http://127.0.0.1:<serverPort>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime State And Presets
|
`/api/state` exposes backend-neutral output telemetry in `videoOutput`. Use `videoOutput.enabled`, `videoOutput.backend`, and `videoOutput.scheduleFailures` for portable status. Backend-specific counters live in `videoOutput.backendMetrics`.
|
||||||
|
|
||||||
The current layer stack is autosaved to `runtime/runtime_state.json` whenever layers, shader assignments, bypass state, ordering, or parameter values change. On startup, the host reloads that file before compiling the stack, so the last working stack should come back automatically.
|
### Borderless Window / Fullscreen Output Plan
|
||||||
|
|
||||||
Manual stack presets are still available from the control UI and are saved under `runtime/stack_presets/*.json`. Presets are useful for named looks, while `runtime_state.json` is the latest working state for the local machine.
|
The preview window is a best-effort diagnostic view and is allowed to drop or reuse frames without disturbing cadence. A borderless window or fullscreen display output should be implemented as a real `output.backend`, separate from preview, so it can target full configured resolution and frame rate.
|
||||||
|
|
||||||
|
Rough plan:
|
||||||
|
|
||||||
|
- Add a `window` video output backend behind the existing `IVideoOutput` factory, selectable with `"output": { "backend": "window", ... }`.
|
||||||
|
- Give it its own device/display selection, fullscreen/borderless mode, window position, vsync policy, and optional monitor refresh validation in `runtime-host.json`.
|
||||||
|
- Consume `SystemFrameExchange` frames like the other output backends, on an output-owned thread, without blocking the render thread.
|
||||||
|
- Upload/present frames through a dedicated GL/DX window context or shared-context path, with a bounded mailbox so late window presentation never back-pressures render cadence.
|
||||||
|
- Report backend-neutral telemetry through `videoOutput` and put display/window-specific details under `videoOutput.backendMetrics`.
|
||||||
|
- Keep the existing preview window as an operator confidence monitor only, not as the full-quality display output path.
|
||||||
|
|
||||||
|
### OCIO / Linear 16-Bit Float Pipeline Plan
|
||||||
|
|
||||||
|
The render backend should move toward a true linear-light `RGBA16F` pipeline, with OpenColorIO used for explicit color transforms at the video I/O and display edges. Shader packages should be able to assume a stable working space instead of guessing whether input pixels are display-referred, log, Rec.709, HDR, or already linear.
|
||||||
|
|
||||||
|
Rough plan:
|
||||||
|
|
||||||
|
- Add OCIO as an optional third-party dependency and package its required runtime DLLs, config files, and license notices with Release builds.
|
||||||
|
- Extend `runtime-host.json` with color settings for input, working, output, and preview/display transforms, including OCIO config path, input color space, working color space, output color space, view, look, exposure, and gamma.
|
||||||
|
- Convert captured/input frames into the configured linear working space before shader layers sample them.
|
||||||
|
- Allocate runtime render targets, temporal history, feedback buffers, and intermediate layer buffers as `RGBA16F` by default, with explicit fallbacks for unsupported hardware.
|
||||||
|
- Keep shader parameter uploads and texture assets explicit about color intent: data textures stay data, image/video textures can opt into OCIO or known transfer conversion.
|
||||||
|
- Apply the output transform only at backend edges: DeckLink/NDI/window output, screenshots, and preview each get the correct display/output transform for their target.
|
||||||
|
- Prebuild OCIO GPU shader/lut resources off the render thread and commit prepared resources to the render thread only when ready, following the render cadence golden rules.
|
||||||
|
- Add test coverage for config parsing, color-pipeline defaults, fallback behavior, and shader/runtime state reporting.
|
||||||
|
|
||||||
|
## Runtime State
|
||||||
|
|
||||||
|
The current layer stack is autosaved to `runtime/runtime_state.json` whenever durable UI/API layer changes are accepted: add/remove, shader assignment, bypass state, ordering, parameter updates, parameter reset, and reload compatibility refreshes. Saves are debounced and written on a background worker, with a final flush during shutdown.
|
||||||
|
|
||||||
|
On startup, the host tries to reload `runtime/runtime_state.json` before compiling the stack. Valid saved layers are rebuilt in saved order, with shader id, bypass state, and parameter values restored. Missing shader packages are skipped, invalid saved parameter values fall back to shader defaults, and if the file is missing or unusable the app falls back to the optional configured startup shader. The checked-in config leaves `runtimeShaderId` empty, so a fresh host uses the simple fallback renderer until layers are added or a saved stack exists.
|
||||||
|
|
||||||
|
Manual stack preset and screenshot routes are still present in the UI/OpenAPI surface, but they are not implemented by the current native command path yet. `runtime_state.json` is the supported latest-working-state mechanism for now.
|
||||||
|
|
||||||
## Control API
|
## Control API
|
||||||
|
|
||||||
@@ -191,13 +305,15 @@ A Swagger UI page is available at:
|
|||||||
http://127.0.0.1:<serverPort>/docs
|
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, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
|
||||||
|
|
||||||
The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, queues shader compilation, refreshes shader availability/errors, and keeps the previous working shader stack running if a changed shader fails to compile.
|
The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, refreshes shader availability/errors, updates active layer parameter definitions from changed manifests, and queues recompilation for every catalog-valid layer in the active stack. Missing shader packages are marked failed, and the previous working render stack remains active where possible until replacement builds commit successfully.
|
||||||
|
|
||||||
Each parameter row also includes a small **OSC** button. Clicking it copies that parameter's OSC route to the clipboard.
|
Each parameter row still exposes the intended OSC route in the UI. The native host has an OSC service stub that reports the configured bind/port in state, but it does not open a UDP listener or dispatch OSC messages yet.
|
||||||
|
|
||||||
The control UI also has a **Screenshot** button. It queues a capture of the final output render target and writes a PNG under:
|
The control UI currently still shows preset and screenshot controls from the intended route surface. Those endpoints return an unimplemented action result in the native host until their backend paths are wired.
|
||||||
|
|
||||||
|
The reserved screenshot output directory is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
runtime/screenshots/
|
runtime/screenshots/
|
||||||
@@ -205,13 +321,15 @@ runtime/screenshots/
|
|||||||
|
|
||||||
## OSC Control
|
## OSC Control
|
||||||
|
|
||||||
The native host also listens for OSC parameter control on the configured `oscBindAddress` and `oscPort`:
|
OSC fields are present in `config/runtime-host.json` and `/api/state` for compatibility with the intended control surface, but the current native host does not start an OSC listener yet.
|
||||||
|
|
||||||
|
The intended route shape is:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, `/VideoShaderToys/VHS/intensity` updates the `intensity` parameter on the first matching `VHS` layer. The listener accepts float, integer, string, and boolean OSC values, and validates them through the same shader parameter path as the REST API. OSC updates are coalesced and applied once per render tick, UI state broadcasts are throttled, and OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. `oscSmoothing` adds a small per-frame easing amount for numeric OSC controls such as floats, `vec2`, and `color`, while booleans, enums, text, and triggers stay immediate. The default bind address is `127.0.0.1`; set `oscBindAddress` to `0.0.0.0` to accept OSC on all IPv4 interfaces. See `docs/OSC_CONTROL.md` for details.
|
For now, use the REST layer parameter endpoints or the control UI for live parameter changes. Future OSC-driven parameter changes should stay out of autosave unless an explicit persistence policy is added.
|
||||||
|
|
||||||
## Shader Packages
|
## Shader Packages
|
||||||
|
|
||||||
@@ -225,7 +343,19 @@ shaders/<id>/
|
|||||||
optional-font-or-texture-assets
|
optional-font-or-texture-assets
|
||||||
```
|
```
|
||||||
|
|
||||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
|
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, optional custom UI metadata, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
|
||||||
|
|
||||||
|
Shader packages can optionally declare a custom control panel:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"ui": {
|
||||||
|
"type": "webComponent",
|
||||||
|
"entry": "ui/controls.js",
|
||||||
|
"tag": "my-shader-controls"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The host validates that metadata, exposes it in `/api/state`, and serves the module from `/shader-assets/{shaderId}/...`. Custom controls receive the current layer, declared parameters, `setParameter(id, value)`, and `requestReset()`. They still update the same manifest-declared parameters as the default React controls.
|
||||||
|
|
||||||
## Generated Files
|
## Generated Files
|
||||||
|
|
||||||
@@ -235,8 +365,8 @@ Runtime-generated files are intentionally ignored:
|
|||||||
- `runtime/shader_cache/active_shader.raw.frag`
|
- `runtime/shader_cache/active_shader.raw.frag`
|
||||||
- `runtime/shader_cache/active_shader.frag`
|
- `runtime/shader_cache/active_shader.frag`
|
||||||
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
||||||
- `runtime/stack_presets/*.json`
|
- `runtime/stack_presets/*.json` reserved manual preset output; preset routes are not implemented in the native host yet.
|
||||||
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
|
- `runtime/screenshots/*.png` reserved screenshot output; screenshot capture is not implemented in the native host yet.
|
||||||
|
|
||||||
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
Only `runtime/templates/` and `runtime/README.md` are tracked.
|
||||||
|
|
||||||
@@ -249,7 +379,11 @@ The Gitea workflow expects two act runners:
|
|||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
|
- `THIRD_PARTY_ROOT`: path to a bundle containing the expected SDK folder layout.
|
||||||
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
|
||||||
|
- `MSDF_ATLAS_GEN_ROOT`: path to the `msdf-atlas-gen` binary release folder containing `msdf-atlas-gen.exe`.
|
||||||
|
- `NDI_SDK_ROOT`: path to the NDI SDK folder containing `Include/`, `Lib/x64/`, and `Bin/x64/`.
|
||||||
|
- `DECKLINK_SDK_ROOT`: path to the Blackmagic DeckLink SDK folder containing `Win/include/DeckLinkAPI.idl`.
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
@@ -257,19 +391,20 @@ Example runner paths:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
D:\SDKs\slang-2026.8-windows-x86_64
|
D:\SDKs\slang-2026.8-windows-x86_64
|
||||||
|
D:\SDKs\msdf-atlas-gen
|
||||||
|
D:\SDKs\NDI 6 SDK
|
||||||
|
D:\SDKs\Blackmagic DeckLink SDK 16.0
|
||||||
```
|
```
|
||||||
|
|
||||||
If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default under `3rdParty/`.
|
If these variables are not set, CMake first looks under the private `video-io-3rdParty/` submodule and then falls back to repo-local defaults under ignored `3rdParty/`.
|
||||||
|
|
||||||
## Still Todo
|
## Still Todo
|
||||||
|
|
||||||
- Audio.
|
- Audio.
|
||||||
- Genlock.
|
- Genlock.
|
||||||
- Logs.
|
- Logs.
|
||||||
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
|
|
||||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||||
- Add WebView2 for an embedded native control surface.
|
- Add WebView2 for an embedded native control surface.
|
||||||
- MSDF typography rasterisation
|
|
||||||
- More shader-library organisation and filtering as the built-in library grows.
|
- More shader-library organisation and filtering as the built-in library grows.
|
||||||
- Optional linear-light compositing mode.
|
- Optional linear-light compositing mode.
|
||||||
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
||||||
@@ -278,3 +413,6 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
|
|||||||
- Anotate included shaders
|
- Anotate included shaders
|
||||||
- allow 3 vector exposed controls
|
- allow 3 vector exposed controls
|
||||||
- add nearest sampling to the extra shader pass
|
- add nearest sampling to the extra shader pass
|
||||||
|
- Add Aja input and output (Assuming i can get a hold of an aja card)
|
||||||
|
- Add bluefish input and output (Assuming again card acess)
|
||||||
|
- Endpoint to show OSC paths seperatly instead of a part of the control UI
|
||||||
|
|||||||
@@ -1,589 +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-
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
// LoopThroughWithOpenGLCompositing.cpp
|
|
||||||
// LoopThroughWithOpenGLCompositing
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "stdafx.h"
|
|
||||||
#include "resource.h"
|
|
||||||
#include "OpenGLComposite.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <shellapi.h>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
|
||||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
|
||||||
#endif
|
|
||||||
#ifndef WGL_CONTEXT_MINOR_VERSION_ARB
|
|
||||||
#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
|
|
||||||
#endif
|
|
||||||
#ifndef WGL_CONTEXT_PROFILE_MASK_ARB
|
|
||||||
#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
|
|
||||||
#endif
|
|
||||||
#ifndef WGL_CONTEXT_CORE_PROFILE_BIT_ARB
|
|
||||||
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define MAX_LOADSTRING 100
|
|
||||||
|
|
||||||
// Declaration for Window procedure
|
|
||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
|
||||||
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
const int kStatusPanelWidth = 680;
|
|
||||||
const int kStatusPanelHeight = 92;
|
|
||||||
const int kStatusPadding = 8;
|
|
||||||
const int kStatusLabelWidth = 58;
|
|
||||||
const int kStatusButtonWidth = 86;
|
|
||||||
const int kStatusRowHeight = 24;
|
|
||||||
const int kStatusGap = 6;
|
|
||||||
const UINT kCreateStatusStripMessage = WM_APP + 1;
|
|
||||||
|
|
||||||
enum StatusControlId
|
|
||||||
{
|
|
||||||
kControlUrlEditId = 2001,
|
|
||||||
kDocsUrlEditId = 2002,
|
|
||||||
kOscAddressEditId = 2003,
|
|
||||||
kOpenControlButtonId = 2004,
|
|
||||||
kOpenDocsButtonId = 2005
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StatusStripControls
|
|
||||||
{
|
|
||||||
HWND panel = NULL;
|
|
||||||
HWND controlLabel = NULL;
|
|
||||||
HWND controlUrl = NULL;
|
|
||||||
HWND openControl = NULL;
|
|
||||||
HWND docsLabel = NULL;
|
|
||||||
HWND docsUrl = NULL;
|
|
||||||
HWND openDocs = NULL;
|
|
||||||
HWND oscLabel = NULL;
|
|
||||||
HWND oscAddress = NULL;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool StatusStripCreated(const StatusStripControls& controls)
|
|
||||||
{
|
|
||||||
return controls.panel != NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, DWORD exStyle, int controlId)
|
|
||||||
{
|
|
||||||
return CreateWindowExA(
|
|
||||||
exStyle,
|
|
||||||
className,
|
|
||||||
text,
|
|
||||||
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | style,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
parent,
|
|
||||||
reinterpret_cast<HMENU>(static_cast<INT_PTR>(controlId)),
|
|
||||||
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE)),
|
|
||||||
NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CreateStatusStrip(HWND hWnd, StatusStripControls& controls)
|
|
||||||
{
|
|
||||||
controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_LEFT, WS_EX_CLIENTEDGE, 0);
|
|
||||||
controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0, 0);
|
|
||||||
controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kControlUrlEditId);
|
|
||||||
controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenControlButtonId);
|
|
||||||
controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0, 0);
|
|
||||||
controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kDocsUrlEditId);
|
|
||||||
controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenDocsButtonId);
|
|
||||||
controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0, 0);
|
|
||||||
controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kOscAddressEditId);
|
|
||||||
|
|
||||||
HFONT guiFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
|
||||||
HWND children[] = {
|
|
||||||
controls.controlLabel,
|
|
||||||
controls.controlUrl,
|
|
||||||
controls.openControl,
|
|
||||||
controls.docsLabel,
|
|
||||||
controls.docsUrl,
|
|
||||||
controls.openDocs,
|
|
||||||
controls.oscLabel,
|
|
||||||
controls.oscAddress
|
|
||||||
};
|
|
||||||
for (HWND child : children)
|
|
||||||
{
|
|
||||||
if (child)
|
|
||||||
SendMessage(child, WM_SETFONT, reinterpret_cast<WPARAM>(guiFont), TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetWindowTextA(controls.controlUrl, "Starting control server...");
|
|
||||||
SetWindowTextA(controls.docsUrl, "Starting API docs...");
|
|
||||||
SetWindowTextA(controls.oscAddress, "Starting OSC listener...");
|
|
||||||
}
|
|
||||||
|
|
||||||
void RaiseStatusControls(const StatusStripControls& controls)
|
|
||||||
{
|
|
||||||
if (!StatusStripCreated(controls))
|
|
||||||
return;
|
|
||||||
|
|
||||||
SetWindowPos(controls.panel, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
||||||
|
|
||||||
HWND interactiveControls[] = {
|
|
||||||
controls.controlLabel,
|
|
||||||
controls.controlUrl,
|
|
||||||
controls.openControl,
|
|
||||||
controls.docsLabel,
|
|
||||||
controls.docsUrl,
|
|
||||||
controls.openDocs,
|
|
||||||
controls.oscLabel,
|
|
||||||
controls.oscAddress
|
|
||||||
};
|
|
||||||
for (HWND control : interactiveControls)
|
|
||||||
{
|
|
||||||
if (control)
|
|
||||||
SetWindowPos(control, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls)
|
|
||||||
{
|
|
||||||
RECT clientRect = {};
|
|
||||||
if (!GetClientRect(hWnd, &clientRect) || !controls.panel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const int clientWidth = static_cast<int>(clientRect.right - clientRect.left);
|
|
||||||
const int clientHeight = static_cast<int>(clientRect.bottom - clientRect.top);
|
|
||||||
const int panelWidth = std::max(280, std::min(kStatusPanelWidth, clientWidth - (kStatusPadding * 2)));
|
|
||||||
const int panelHeight = kStatusPanelHeight;
|
|
||||||
const int panelLeft = kStatusPadding;
|
|
||||||
const int panelTop = std::max(kStatusPadding, clientHeight - panelHeight - kStatusPadding);
|
|
||||||
MoveWindow(controls.panel, panelLeft, panelTop, panelWidth, panelHeight, TRUE);
|
|
||||||
|
|
||||||
const int rowX = panelLeft + kStatusPadding;
|
|
||||||
const int editX = rowX + kStatusLabelWidth + kStatusGap;
|
|
||||||
const int buttonX = panelLeft + panelWidth - kStatusPadding - kStatusButtonWidth;
|
|
||||||
const int editWidth = std::max(80, buttonX - editX - kStatusGap);
|
|
||||||
const int oscWidth = std::max(80, panelLeft + panelWidth - editX - kStatusPadding);
|
|
||||||
const int row1 = panelTop + kStatusPadding;
|
|
||||||
const int row2 = row1 + kStatusRowHeight + kStatusGap;
|
|
||||||
const int row3 = row2 + kStatusRowHeight + kStatusGap;
|
|
||||||
|
|
||||||
MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
|
||||||
MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE);
|
|
||||||
RaiseStatusControls(controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite)
|
|
||||||
{
|
|
||||||
if (!StatusStripCreated(controls))
|
|
||||||
return;
|
|
||||||
|
|
||||||
SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str());
|
|
||||||
SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str());
|
|
||||||
SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenUrl(const char* url)
|
|
||||||
{
|
|
||||||
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShowUnhandledExceptionMessage(const char* prefix)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (const std::exception& exception)
|
|
||||||
{
|
|
||||||
std::string message = std::string(prefix) + "\n\n" + exception.what();
|
|
||||||
MessageBoxA(NULL, message.c_str(), "Unhandled exception", MB_OK | MB_ICONERROR);
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, prefix, "Unhandled exception", MB_OK | MB_ICONERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the pixel format for a given device context
|
|
||||||
void SetDCPixelFormat(HDC hDC)
|
|
||||||
{
|
|
||||||
int nPixelFormat;
|
|
||||||
|
|
||||||
static PIXELFORMATDESCRIPTOR pfd = {
|
|
||||||
sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
|
|
||||||
1, // Version of this structure
|
|
||||||
PFD_DRAW_TO_WINDOW | // Draw to Window (not to bitmap)
|
|
||||||
PFD_SUPPORT_OPENGL | // Support OpenGL calls in window
|
|
||||||
PFD_DOUBLEBUFFER, // Double buffered mode
|
|
||||||
PFD_TYPE_RGBA, // RGBA Color mode
|
|
||||||
32, // Want 32 bit color
|
|
||||||
0,0,0,0,0,0, // Not used to select mode
|
|
||||||
0,0, // Not used to select mode
|
|
||||||
0,0,0,0,0, // Not used to select mode
|
|
||||||
16, // Size of depth buffer
|
|
||||||
0, // Not used
|
|
||||||
0, // Not used
|
|
||||||
0, // Not used
|
|
||||||
0, // Not used
|
|
||||||
0,0,0 }; // Not used
|
|
||||||
|
|
||||||
// Choose a pixel format that best matches that described in pfd
|
|
||||||
nPixelFormat = ChoosePixelFormat(hDC, &pfd);
|
|
||||||
|
|
||||||
// Set the pixel format for the device context
|
|
||||||
SetPixelFormat(hDC, nPixelFormat, &pfd);
|
|
||||||
}
|
|
||||||
|
|
||||||
HGLRC CreateModernOpenGLContext(HDC hDC)
|
|
||||||
{
|
|
||||||
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB =
|
|
||||||
reinterpret_cast<PFNWGLCREATECONTEXTATTRIBSARBPROC>(wglGetProcAddress("wglCreateContextAttribsARB"));
|
|
||||||
if (!wglCreateContextAttribsARB)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
const int versionCandidates[][2] =
|
|
||||||
{
|
|
||||||
{ 4, 5 },
|
|
||||||
{ 4, 3 },
|
|
||||||
{ 3, 3 }
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& version : versionCandidates)
|
|
||||||
{
|
|
||||||
const int attribs[] =
|
|
||||||
{
|
|
||||||
WGL_CONTEXT_MAJOR_VERSION_ARB, version[0],
|
|
||||||
WGL_CONTEXT_MINOR_VERSION_ARB, version[1],
|
|
||||||
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
HGLRC modernContext = wglCreateContextAttribsARB(hDC, 0, attribs);
|
|
||||||
if (modernContext != NULL)
|
|
||||||
return modernContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
|
||||||
{
|
|
||||||
MSG msg; // Windows message structure
|
|
||||||
WNDCLASS wc; // Windows class structure
|
|
||||||
HWND hWnd; // Storeage for window handle
|
|
||||||
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
|
|
||||||
TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
|
|
||||||
|
|
||||||
// Initialize global strings
|
|
||||||
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
|
|
||||||
LoadString(hInstance, IDC_OPENGLOUTPUT, szWindowClass, MAX_LOADSTRING);
|
|
||||||
|
|
||||||
// Register Window style
|
|
||||||
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
|
|
||||||
wc.lpfnWndProc = (WNDPROC) WndProc;
|
|
||||||
wc.cbClsExtra = 0;
|
|
||||||
wc.cbWndExtra = 0;
|
|
||||||
wc.hInstance = hInstance;
|
|
||||||
wc.hIcon = NULL;
|
|
||||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
||||||
|
|
||||||
// No need for background brush for OpenGL window
|
|
||||||
wc.hbrBackground = NULL;
|
|
||||||
wc.lpszMenuName = NULL;
|
|
||||||
wc.lpszClassName = szWindowClass;
|
|
||||||
|
|
||||||
// Register the window class
|
|
||||||
if (RegisterClass(&wc) == 0)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
// Create the main application window
|
|
||||||
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
|
|
||||||
CW_USEDEFAULT, 0, 250, 250, NULL, NULL, hInstance, NULL);
|
|
||||||
|
|
||||||
// If window was not created, quit
|
|
||||||
if (hWnd == NULL)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
// Display the window
|
|
||||||
ShowWindow(hWnd,SW_SHOW);
|
|
||||||
UpdateWindow(hWnd);
|
|
||||||
|
|
||||||
// Process application messages until the application closes
|
|
||||||
while (GetMessage(&msg, NULL, 0, 0))
|
|
||||||
{
|
|
||||||
TranslateMessage(&msg);
|
|
||||||
DispatchMessage(&msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)msg.wParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window procedure, handles all messages for this program
|
|
||||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
||||||
{
|
|
||||||
static HGLRC hRC = NULL; // Permenant Rendering context
|
|
||||||
static HDC hDC = NULL; // Private GDI Device context
|
|
||||||
static OpenGLComposite* pOpenGLComposite = NULL;
|
|
||||||
static bool sInteractiveResize = false;
|
|
||||||
static StatusStripControls sStatusStrip;
|
|
||||||
|
|
||||||
switch (message)
|
|
||||||
{
|
|
||||||
// Window creation, setup for OpenGL context
|
|
||||||
case WM_CREATE:
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Store the device context
|
|
||||||
hDC = GetDC(hWnd);
|
|
||||||
|
|
||||||
// Select the pixel format
|
|
||||||
SetDCPixelFormat(hDC);
|
|
||||||
|
|
||||||
// Create the rendering context and make it current
|
|
||||||
hRC = wglCreateContext(hDC);
|
|
||||||
wglMakeCurrent(hDC, hRC);
|
|
||||||
|
|
||||||
HGLRC modernRC = CreateModernOpenGLContext(hDC);
|
|
||||||
if (modernRC == NULL)
|
|
||||||
{
|
|
||||||
MessageBox(NULL, _T("This application requires an OpenGL 3.3+ core profile context."), _T("OpenGL initialization Error."), MB_OK);
|
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
|
||||||
wglDeleteContext(hRC);
|
|
||||||
hRC = modernRC;
|
|
||||||
wglMakeCurrent(hDC, hRC);
|
|
||||||
|
|
||||||
// Initialize COM
|
|
||||||
HRESULT result;
|
|
||||||
result = CoInitialize(NULL);
|
|
||||||
if (FAILED(result))
|
|
||||||
{
|
|
||||||
MessageBox(NULL, _T("Initialization of COM failed."), _T("Application initialization Error."),MB_OK);
|
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup OpenGL and DeckLink capture and playout object
|
|
||||||
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
|
|
||||||
|
|
||||||
if (pOpenGLComposite->InitDeckLink())
|
|
||||||
{
|
|
||||||
wglMakeCurrent( NULL, NULL );
|
|
||||||
if (pOpenGLComposite->Start())
|
|
||||||
{
|
|
||||||
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
|
||||||
break; // success
|
|
||||||
}
|
|
||||||
MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "The OpenGL/DeckLink runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed to initialize - cleanup
|
|
||||||
delete pOpenGLComposite;
|
|
||||||
pOpenGLComposite = NULL;
|
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime.");
|
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case kCreateStatusStripMessage:
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
{
|
|
||||||
if (!StatusStripCreated(sStatusStrip))
|
|
||||||
CreateStatusStrip(hWnd, sStatusStrip);
|
|
||||||
|
|
||||||
UpdateStatusStrip(sStatusStrip, *pOpenGLComposite);
|
|
||||||
LayoutStatusStrip(hWnd, sStatusStrip);
|
|
||||||
RECT clientRect = {};
|
|
||||||
if (GetClientRect(hWnd, &clientRect))
|
|
||||||
{
|
|
||||||
pOpenGLComposite->resizeGL(
|
|
||||||
static_cast<WORD>(clientRect.right - clientRect.left),
|
|
||||||
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
|
||||||
}
|
|
||||||
InvalidateRect(hWnd, NULL, FALSE);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_DESTROY:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
{
|
|
||||||
pOpenGLComposite->Stop();
|
|
||||||
delete pOpenGLComposite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deselect the current rendering context and delete it
|
|
||||||
wglMakeCurrent(hDC, NULL);
|
|
||||||
wglDeleteContext(hRC);
|
|
||||||
|
|
||||||
// Tell the application to terminate after the window is gone
|
|
||||||
PostQuitMessage(0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_ENTERSIZEMOVE:
|
|
||||||
sInteractiveResize = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_EXITSIZEMOVE:
|
|
||||||
sInteractiveResize = false;
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
{
|
|
||||||
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_SIZE:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (StatusStripCreated(sStatusStrip))
|
|
||||||
LayoutStatusStrip(hWnd, sStatusStrip);
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_ERASEBKGND:
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
case WM_PAINT:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PAINTSTRUCT paint = {};
|
|
||||||
BeginPaint(hWnd, &paint);
|
|
||||||
EndPaint(hWnd, &paint);
|
|
||||||
|
|
||||||
if (!sInteractiveResize && pOpenGLComposite)
|
|
||||||
{
|
|
||||||
wglMakeCurrent(hDC, hRC);
|
|
||||||
pOpenGLComposite->paintGL(true);
|
|
||||||
wglMakeCurrent( NULL, NULL );
|
|
||||||
RaiseStatusControls(sStatusStrip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
wglMakeCurrent( NULL, NULL );
|
|
||||||
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_KEYDOWN:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
|
|
||||||
{
|
|
||||||
pOpenGLComposite->ReloadShader();
|
|
||||||
InvalidateRect(hWnd, NULL, FALSE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime.");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WM_COMMAND:
|
|
||||||
switch (LOWORD(wParam))
|
|
||||||
{
|
|
||||||
case kOpenControlButtonId:
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
{
|
|
||||||
std::string url = pOpenGLComposite->GetControlUrl();
|
|
||||||
OpenUrl(url.c_str());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case kOpenDocsButtonId:
|
|
||||||
if (pOpenGLComposite)
|
|
||||||
{
|
|
||||||
std::string url = pOpenGLComposite->GetDocsUrl();
|
|
||||||
OpenUrl(url.c_str());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,47 +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-
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
// LoopThroughWithOpenGLCompositing.h
|
|
||||||
// LoopThroughWithOpenGLCompositing
|
|
||||||
//
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "resource.h"
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,95 +0,0 @@
|
|||||||
// Microsoft Visual C++ generated resource script.
|
|
||||||
//
|
|
||||||
#include "resource.h"
|
|
||||||
|
|
||||||
#define APSTUDIO_READONLY_SYMBOLS
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Generated from the TEXTINCLUDE 2 resource.
|
|
||||||
//
|
|
||||||
#ifndef APSTUDIO_INVOKED
|
|
||||||
#include "targetver.h"
|
|
||||||
#endif
|
|
||||||
#define APSTUDIO_HIDDEN_SYMBOLS
|
|
||||||
#include "windows.h"
|
|
||||||
#undef APSTUDIO_HIDDEN_SYMBOLS
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
#undef APSTUDIO_READONLY_SYMBOLS
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
// English (U.S.) resources
|
|
||||||
|
|
||||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
|
||||||
#ifdef _WIN32
|
|
||||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|
||||||
#pragma code_page(1252)
|
|
||||||
#endif //_WIN32
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Icon
|
|
||||||
//
|
|
||||||
|
|
||||||
// Icon with lowest ID value placed first to ensure application icon
|
|
||||||
// remains consistent on all systems.
|
|
||||||
IDI_OPENGLOUTPUT ICON "LoopThroughWithOpenGLCompositing.ico"
|
|
||||||
IDI_SMALL ICON "small.ico"
|
|
||||||
|
|
||||||
#ifdef APSTUDIO_INVOKED
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// TEXTINCLUDE
|
|
||||||
//
|
|
||||||
|
|
||||||
1 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"resource.h\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
2 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"#ifndef APSTUDIO_INVOKED\r\n"
|
|
||||||
"#include ""targetver.h""\r\n"
|
|
||||||
"#endif\r\n"
|
|
||||||
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
|
||||||
"#include ""windows.h""\r\n"
|
|
||||||
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
|
||||||
"\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
3 TEXTINCLUDE
|
|
||||||
BEGIN
|
|
||||||
"\r\n"
|
|
||||||
"\0"
|
|
||||||
END
|
|
||||||
|
|
||||||
#endif // APSTUDIO_INVOKED
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// String Table
|
|
||||||
//
|
|
||||||
|
|
||||||
STRINGTABLE
|
|
||||||
BEGIN
|
|
||||||
IDS_APP_TITLE "Video Shader Toys"
|
|
||||||
IDC_OPENGLOUTPUT "OPENGLOUTPUT"
|
|
||||||
END
|
|
||||||
|
|
||||||
#endif // English (U.S.) resources
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#ifndef APSTUDIO_INVOKED
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Generated from the TEXTINCLUDE 3 resource.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
|
||||||
#endif // not APSTUDIO_INVOKED
|
|
||||||
|
|
||||||
@@ -1,648 +0,0 @@
|
|||||||
#include "stdafx.h"
|
|
||||||
#include "ControlServer.h"
|
|
||||||
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
|
|
||||||
#include <Wincrypt.h>
|
|
||||||
#include <ws2tcpip.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#pragma comment(lib, "Ws2_32.lib")
|
|
||||||
#pragma comment(lib, "Crypt32.lib")
|
|
||||||
#pragma comment(lib, "Advapi32.lib")
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
|
||||||
constexpr DWORD kStateBroadcastThrottleMs = 50;
|
|
||||||
|
|
||||||
bool InitializeWinsock(std::string& error)
|
|
||||||
{
|
|
||||||
WSADATA wsaData = {};
|
|
||||||
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
error = "WSAStartup failed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ToLower(std::string text)
|
|
||||||
{
|
|
||||||
std::transform(text.begin(), text.end(), text.begin(),
|
|
||||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsSafeUiPath(const std::filesystem::path& relativePath)
|
|
||||||
{
|
|
||||||
for (const std::filesystem::path& part : relativePath)
|
|
||||||
{
|
|
||||||
if (part == "..")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !relativePath.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GuessContentType(const std::filesystem::path& assetPath)
|
|
||||||
{
|
|
||||||
const std::string extension = ToLower(assetPath.extension().string());
|
|
||||||
if (extension == ".js" || extension == ".mjs")
|
|
||||||
return "text/javascript";
|
|
||||||
if (extension == ".css")
|
|
||||||
return "text/css";
|
|
||||||
if (extension == ".json")
|
|
||||||
return "application/json";
|
|
||||||
if (extension == ".yaml" || extension == ".yml")
|
|
||||||
return "application/yaml";
|
|
||||||
if (extension == ".svg")
|
|
||||||
return "image/svg+xml";
|
|
||||||
if (extension == ".png")
|
|
||||||
return "image/png";
|
|
||||||
if (extension == ".jpg" || extension == ".jpeg")
|
|
||||||
return "image/jpeg";
|
|
||||||
if (extension == ".ico")
|
|
||||||
return "image/x-icon";
|
|
||||||
if (extension == ".map")
|
|
||||||
return "application/json";
|
|
||||||
if (extension == ".md")
|
|
||||||
return "text/markdown";
|
|
||||||
return "text/html";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::ControlServer()
|
|
||||||
: mPort(0), mRunning(false), mBroadcastPending(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::~ControlServer()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error)
|
|
||||||
{
|
|
||||||
mUiRoot = uiRoot;
|
|
||||||
mDocsRoot = docsRoot;
|
|
||||||
mCallbacks = callbacks;
|
|
||||||
|
|
||||||
if (!InitializeWinsock(error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mListenSocket.reset(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
|
||||||
if (!mListenSocket.valid())
|
|
||||||
{
|
|
||||||
error = "Could not create listening socket.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
u_long nonBlocking = 1;
|
|
||||||
ioctlsocket(mListenSocket.get(), FIONBIO, &nonBlocking);
|
|
||||||
|
|
||||||
sockaddr_in address = {};
|
|
||||||
address.sin_family = AF_INET;
|
|
||||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
||||||
|
|
||||||
bool bound = false;
|
|
||||||
for (unsigned short offset = 0; offset < 20; ++offset)
|
|
||||||
{
|
|
||||||
address.sin_port = htons(static_cast<u_short>(preferredPort + offset));
|
|
||||||
if (bind(mListenSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0)
|
|
||||||
{
|
|
||||||
mPort = preferredPort + offset;
|
|
||||||
bound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bound)
|
|
||||||
{
|
|
||||||
error = "Could not bind the local control server to any port in the preferred range.";
|
|
||||||
mListenSocket.reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listen(mListenSocket.get(), SOMAXCONN) != 0)
|
|
||||||
{
|
|
||||||
error = "Could not start listening on the local control server socket.";
|
|
||||||
mListenSocket.reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRunning = true;
|
|
||||||
mThread = std::thread(&ControlServer::ServerLoop, this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServer::Stop()
|
|
||||||
{
|
|
||||||
const bool wasActive = mRunning || mListenSocket.valid() || mThread.joinable();
|
|
||||||
mRunning = false;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
for (ClientConnection& client : mClients)
|
|
||||||
client.socket.reset();
|
|
||||||
mClients.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
mListenSocket.reset();
|
|
||||||
|
|
||||||
if (mThread.joinable())
|
|
||||||
mThread.join();
|
|
||||||
|
|
||||||
if (wasActive)
|
|
||||||
WSACleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServer::BroadcastState()
|
|
||||||
{
|
|
||||||
mBroadcastPending = false;
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
BroadcastStateLocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServer::RequestBroadcastState()
|
|
||||||
{
|
|
||||||
mBroadcastPending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServer::ServerLoop()
|
|
||||||
{
|
|
||||||
DWORD lastStateBroadcastMs = GetTickCount();
|
|
||||||
while (mRunning)
|
|
||||||
{
|
|
||||||
TryAcceptClient();
|
|
||||||
|
|
||||||
const DWORD nowMs = GetTickCount();
|
|
||||||
if (mBroadcastPending && nowMs - lastStateBroadcastMs >= kStateBroadcastThrottleMs)
|
|
||||||
{
|
|
||||||
BroadcastState();
|
|
||||||
lastStateBroadcastMs = nowMs;
|
|
||||||
}
|
|
||||||
else if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
|
||||||
{
|
|
||||||
BroadcastState();
|
|
||||||
lastStateBroadcastMs = nowMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sleep(25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::HandleHttpClient(UniqueSocket clientSocket)
|
|
||||||
{
|
|
||||||
std::string request;
|
|
||||||
char buffer[8192];
|
|
||||||
int received = recv(clientSocket.get(), buffer, sizeof(buffer), 0);
|
|
||||||
if (received <= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
request.assign(buffer, buffer + received);
|
|
||||||
return HandleHttpRequest(std::move(clientSocket), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::TryAcceptClient()
|
|
||||||
{
|
|
||||||
sockaddr_in clientAddress = {};
|
|
||||||
int addressSize = sizeof(clientAddress);
|
|
||||||
UniqueSocket clientSocket(accept(mListenSocket.get(), reinterpret_cast<sockaddr*>(&clientAddress), &addressSize));
|
|
||||||
if (!clientSocket.valid())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return HandleHttpClient(std::move(clientSocket));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body)
|
|
||||||
{
|
|
||||||
std::ostringstream response;
|
|
||||||
response << "HTTP/1.1 " << status << "\r\n";
|
|
||||||
response << "Content-Type: " << contentType << "\r\n";
|
|
||||||
response << "Content-Length: " << body.size() << "\r\n";
|
|
||||||
response << "Connection: close\r\n\r\n";
|
|
||||||
response << body;
|
|
||||||
|
|
||||||
const std::string payload = response.str();
|
|
||||||
return send(clientSocket, payload.c_str(), static_cast<int>(payload.size()), 0) == static_cast<int>(payload.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const HttpResponse& response)
|
|
||||||
{
|
|
||||||
return SendHttpResponse(clientSocket, response.status, response.contentType, response.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::HandleHttpRequest(UniqueSocket clientSocket, const std::string& request)
|
|
||||||
{
|
|
||||||
HttpRequest httpRequest;
|
|
||||||
if (!ParseHttpRequest(request, httpRequest))
|
|
||||||
{
|
|
||||||
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Bad Request");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ToLower(GetHeaderValue(httpRequest, "Upgrade")) == "websocket")
|
|
||||||
return HandleWebSocketUpgrade(std::move(clientSocket), httpRequest);
|
|
||||||
|
|
||||||
const HttpResponse response = RouteHttpRequest(httpRequest);
|
|
||||||
SendHttpResponse(clientSocket.get(), response);
|
|
||||||
if (response.broadcastState)
|
|
||||||
BroadcastState();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::RouteHttpRequest(const HttpRequest& request)
|
|
||||||
{
|
|
||||||
if (request.method == "GET")
|
|
||||||
return ServeGetRequest(request);
|
|
||||||
|
|
||||||
if (request.method == "POST")
|
|
||||||
return HandleApiPost(request);
|
|
||||||
|
|
||||||
return { "404 Not Found", "text/plain", "Not Found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::ServeGetRequest(const HttpRequest& request) const
|
|
||||||
{
|
|
||||||
if (request.path == "/" || request.path == "/index.html")
|
|
||||||
return ServeUiAsset("index.html");
|
|
||||||
|
|
||||||
if (request.path == "/api/state")
|
|
||||||
return { "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}" };
|
|
||||||
|
|
||||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
|
||||||
return ServeOpenApiSpec();
|
|
||||||
|
|
||||||
if (request.path == "/docs" || request.path == "/docs/")
|
|
||||||
return ServeSwaggerDocs();
|
|
||||||
|
|
||||||
const std::string docsPrefix = "/docs/";
|
|
||||||
if (request.path.rfind(docsPrefix, 0) == 0)
|
|
||||||
return ServeDocsAsset(request.path.substr(docsPrefix.size()));
|
|
||||||
|
|
||||||
if (request.path.size() > 1)
|
|
||||||
{
|
|
||||||
const HttpResponse assetResponse = ServeUiAsset(request.path.substr(1));
|
|
||||||
if (!assetResponse.body.empty())
|
|
||||||
return assetResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { "404 Not Found", "text/plain", "Not Found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::ServeUiAsset(const std::string& relativePath) const
|
|
||||||
{
|
|
||||||
std::string contentType;
|
|
||||||
const std::string body = LoadUiAsset(relativePath, contentType);
|
|
||||||
return body.empty()
|
|
||||||
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
|
|
||||||
: HttpResponse{ "200 OK", contentType, body };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::ServeDocsAsset(const std::string& relativePath) const
|
|
||||||
{
|
|
||||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
|
||||||
if (!IsSafeUiPath(sanitizedPath))
|
|
||||||
return { "404 Not Found", "text/plain", "Not Found" };
|
|
||||||
|
|
||||||
const std::filesystem::path docsPath = mDocsRoot / sanitizedPath;
|
|
||||||
const std::string body = LoadTextFile(docsPath);
|
|
||||||
return body.empty()
|
|
||||||
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
|
|
||||||
: HttpResponse{ "200 OK", GuessContentType(docsPath), body };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::ServeOpenApiSpec() const
|
|
||||||
{
|
|
||||||
const std::filesystem::path specPath = mDocsRoot / "openapi.yaml";
|
|
||||||
const std::string body = LoadTextFile(specPath);
|
|
||||||
return body.empty()
|
|
||||||
? HttpResponse{ "404 Not Found", "text/plain", "OpenAPI spec not found" }
|
|
||||||
: HttpResponse{ "200 OK", GuessContentType(specPath), body };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::ServeSwaggerDocs() const
|
|
||||||
{
|
|
||||||
std::ostringstream html;
|
|
||||||
html << "<!doctype html>\n"
|
|
||||||
<< "<html lang=\"en\">\n"
|
|
||||||
<< "<head>\n"
|
|
||||||
<< " <meta charset=\"utf-8\">\n"
|
|
||||||
<< " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
|
|
||||||
<< " <title>Video Shader Toys API Docs</title>\n"
|
|
||||||
<< " <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n"
|
|
||||||
<< "</head>\n"
|
|
||||||
<< "<body>\n"
|
|
||||||
<< " <div id=\"swagger-ui\"></div>\n"
|
|
||||||
<< " <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
|
||||||
<< " <script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
|
||||||
<< "</body>\n"
|
|
||||||
<< "</html>\n";
|
|
||||||
return { "200 OK", "text/html", html.str() };
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServer::HttpResponse ControlServer::HandleApiPost(const HttpRequest& request)
|
|
||||||
{
|
|
||||||
JsonValue root;
|
|
||||||
std::string parseError;
|
|
||||||
if (!ParseJson(request.body, root, parseError))
|
|
||||||
return { "400 Bad Request", "application/json", BuildJsonResponse(false, parseError) };
|
|
||||||
|
|
||||||
std::string actionError;
|
|
||||||
const bool success = InvokePostRoute(request.path, root, actionError);
|
|
||||||
return {
|
|
||||||
success ? "200 OK" : "400 Bad Request",
|
|
||||||
"application/json",
|
|
||||||
BuildJsonResponse(success, actionError),
|
|
||||||
success
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError)
|
|
||||||
{
|
|
||||||
using PostHandler = std::function<bool(const JsonValue&, std::string&)>;
|
|
||||||
const std::map<std::string, PostHandler> postRoutes =
|
|
||||||
{
|
|
||||||
{ "/api/layers/add", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* shaderId = json.find("shaderId");
|
|
||||||
return shaderId && mCallbacks.addLayer && mCallbacks.addLayer(shaderId->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/remove", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
return layerId && mCallbacks.removeLayer && mCallbacks.removeLayer(layerId->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/move", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
const JsonValue* direction = json.find("direction");
|
|
||||||
return layerId && direction && mCallbacks.moveLayer &&
|
|
||||||
mCallbacks.moveLayer(layerId->asString(), static_cast<int>(direction->asNumber()), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/reorder", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
const JsonValue* targetIndex = json.find("targetIndex");
|
|
||||||
return layerId && targetIndex && mCallbacks.moveLayerToIndex &&
|
|
||||||
mCallbacks.moveLayerToIndex(layerId->asString(), static_cast<std::size_t>(targetIndex->asNumber()), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/set-bypass", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
const JsonValue* bypass = json.find("bypass");
|
|
||||||
return layerId && bypass && mCallbacks.setLayerBypass &&
|
|
||||||
mCallbacks.setLayerBypass(layerId->asString(), bypass->asBoolean(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/set-shader", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
const JsonValue* shaderId = json.find("shaderId");
|
|
||||||
return layerId && shaderId && mCallbacks.setLayerShader &&
|
|
||||||
mCallbacks.setLayerShader(layerId->asString(), shaderId->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/update-parameter", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
const JsonValue* parameterId = json.find("parameterId");
|
|
||||||
const JsonValue* value = json.find("value");
|
|
||||||
return layerId && parameterId && value && mCallbacks.updateLayerParameter &&
|
|
||||||
mCallbacks.updateLayerParameter(layerId->asString(), parameterId->asString(), SerializeJson(*value, false), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/layers/reset-parameters", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* layerId = json.find("layerId");
|
|
||||||
return layerId && mCallbacks.resetLayerParameters &&
|
|
||||||
mCallbacks.resetLayerParameters(layerId->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/stack-presets/save", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* presetName = json.find("presetName");
|
|
||||||
return presetName && mCallbacks.saveStackPreset &&
|
|
||||||
mCallbacks.saveStackPreset(presetName->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/stack-presets/load", [this](const JsonValue& json, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* presetName = json.find("presetName");
|
|
||||||
return presetName && mCallbacks.loadStackPreset &&
|
|
||||||
mCallbacks.loadStackPreset(presetName->asString(), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/reload", [this](const JsonValue&, std::string& error)
|
|
||||||
{
|
|
||||||
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
|
|
||||||
{
|
|
||||||
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto route = postRoutes.find(path);
|
|
||||||
return route != postRoutes.end() && route->second(root, actionError);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request)
|
|
||||||
{
|
|
||||||
const std::string clientKey = GetHeaderValue(request, "Sec-WebSocket-Key");
|
|
||||||
if (clientKey.empty())
|
|
||||||
{
|
|
||||||
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Missing Sec-WebSocket-Key");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream response;
|
|
||||||
response << "HTTP/1.1 101 Switching Protocols\r\n";
|
|
||||||
response << "Upgrade: websocket\r\n";
|
|
||||||
response << "Connection: Upgrade\r\n";
|
|
||||||
response << "Sec-WebSocket-Accept: " << ComputeWebSocketAcceptKey(clientKey) << "\r\n\r\n";
|
|
||||||
|
|
||||||
const std::string payload = response.str();
|
|
||||||
send(clientSocket.get(), payload.c_str(), static_cast<int>(payload.size()), 0);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
ClientConnection client;
|
|
||||||
client.socket.reset(clientSocket.release());
|
|
||||||
client.websocket = true;
|
|
||||||
mClients.push_back(std::move(client));
|
|
||||||
mBroadcastPending = false;
|
|
||||||
BroadcastStateLocked();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& payload)
|
|
||||||
{
|
|
||||||
std::string frame;
|
|
||||||
frame.push_back(static_cast<char>(0x81));
|
|
||||||
if (payload.size() <= 125)
|
|
||||||
{
|
|
||||||
frame.push_back(static_cast<char>(payload.size()));
|
|
||||||
}
|
|
||||||
else if (payload.size() <= 65535)
|
|
||||||
{
|
|
||||||
frame.push_back(126);
|
|
||||||
frame.push_back(static_cast<char>((payload.size() >> 8) & 0xFF));
|
|
||||||
frame.push_back(static_cast<char>(payload.size() & 0xFF));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
frame.push_back(127);
|
|
||||||
for (int shift = 56; shift >= 0; shift -= 8)
|
|
||||||
frame.push_back(static_cast<char>((payload.size() >> shift) & 0xFF));
|
|
||||||
}
|
|
||||||
frame.append(payload);
|
|
||||||
|
|
||||||
return send(clientSocket, frame.data(), static_cast<int>(frame.size()), 0) == static_cast<int>(frame.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServer::BroadcastStateLocked()
|
|
||||||
{
|
|
||||||
if (mClients.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
|
||||||
for (auto it = mClients.begin(); it != mClients.end();)
|
|
||||||
{
|
|
||||||
if (!SendWebSocketText(it->socket.get(), stateMessage))
|
|
||||||
{
|
|
||||||
it = mClients.erase(it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const
|
|
||||||
{
|
|
||||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
|
||||||
if (!IsSafeUiPath(sanitizedPath))
|
|
||||||
return std::string();
|
|
||||||
|
|
||||||
const std::filesystem::path assetPath = mUiRoot / sanitizedPath;
|
|
||||||
contentType = GuessContentType(assetPath);
|
|
||||||
return LoadTextFile(assetPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::LoadTextFile(const std::filesystem::path& path) const
|
|
||||||
{
|
|
||||||
std::ifstream input(path, std::ios::binary);
|
|
||||||
if (!input)
|
|
||||||
return std::string();
|
|
||||||
|
|
||||||
std::ostringstream buffer;
|
|
||||||
buffer << input.rdbuf();
|
|
||||||
return buffer.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::BuildJsonResponse(bool success, const std::string& error) const
|
|
||||||
{
|
|
||||||
JsonValue response = JsonValue::MakeObject();
|
|
||||||
response.set("ok", JsonValue(success));
|
|
||||||
if (!error.empty())
|
|
||||||
response.set("error", JsonValue(error));
|
|
||||||
return SerializeJson(response, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::Base64Encode(const unsigned char* data, DWORD dataLength)
|
|
||||||
{
|
|
||||||
DWORD outputLength = 0;
|
|
||||||
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &outputLength);
|
|
||||||
std::string encoded(outputLength, '\0');
|
|
||||||
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &encoded[0], &outputLength);
|
|
||||||
if (!encoded.empty() && encoded.back() == '\0')
|
|
||||||
encoded.pop_back();
|
|
||||||
return encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::ComputeWebSocketAcceptKey(const std::string& clientKey)
|
|
||||||
{
|
|
||||||
const std::string combined = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
||||||
HCRYPTPROV provider = 0;
|
|
||||||
HCRYPTHASH hash = 0;
|
|
||||||
BYTE digest[20] = {};
|
|
||||||
DWORD digestLength = sizeof(digest);
|
|
||||||
|
|
||||||
CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
|
|
||||||
CryptCreateHash(provider, CALG_SHA1, 0, 0, &hash);
|
|
||||||
CryptHashData(hash, reinterpret_cast<const BYTE*>(combined.data()), static_cast<DWORD>(combined.size()), 0);
|
|
||||||
CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLength, 0);
|
|
||||||
|
|
||||||
if (hash)
|
|
||||||
CryptDestroyHash(hash);
|
|
||||||
if (provider)
|
|
||||||
CryptReleaseContext(provider, 0);
|
|
||||||
|
|
||||||
return Base64Encode(digest, digestLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ControlServer::GetHeaderValue(const HttpRequest& request, const std::string& headerName)
|
|
||||||
{
|
|
||||||
const auto header = request.headers.find(ToLower(headerName));
|
|
||||||
return header == request.headers.end() ? std::string() : header->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
|
||||||
{
|
|
||||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
|
||||||
if (requestLineEnd == std::string::npos)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const std::string requestLine = rawRequest.substr(0, requestLineEnd);
|
|
||||||
const std::size_t methodEnd = requestLine.find(' ');
|
|
||||||
if (methodEnd == std::string::npos)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const std::size_t pathEnd = requestLine.find(' ', methodEnd + 1);
|
|
||||||
if (pathEnd == std::string::npos)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
request.method = requestLine.substr(0, methodEnd);
|
|
||||||
request.path = requestLine.substr(methodEnd + 1, pathEnd - methodEnd - 1);
|
|
||||||
request.headers.clear();
|
|
||||||
|
|
||||||
const std::size_t headersStart = requestLineEnd + 2;
|
|
||||||
const std::size_t bodySeparator = rawRequest.find("\r\n\r\n", headersStart);
|
|
||||||
const std::size_t headersEnd = bodySeparator == std::string::npos ? rawRequest.size() : bodySeparator;
|
|
||||||
|
|
||||||
for (std::size_t lineStart = headersStart; lineStart < headersEnd;)
|
|
||||||
{
|
|
||||||
const std::size_t lineEnd = rawRequest.find("\r\n", lineStart);
|
|
||||||
const std::size_t currentLineEnd = lineEnd == std::string::npos ? headersEnd : std::min(lineEnd, headersEnd);
|
|
||||||
const std::string line = rawRequest.substr(lineStart, currentLineEnd - lineStart);
|
|
||||||
const std::size_t separator = line.find(':');
|
|
||||||
if (separator != std::string::npos)
|
|
||||||
{
|
|
||||||
const std::string key = ToLower(line.substr(0, separator));
|
|
||||||
std::string value = line.substr(separator + 1);
|
|
||||||
const std::size_t first = value.find_first_not_of(" \t");
|
|
||||||
const std::size_t last = value.find_last_not_of(" \t");
|
|
||||||
request.headers[key] = first == std::string::npos ? std::string() : value.substr(first, last - first + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lineEnd == std::string::npos || lineEnd >= headersEnd)
|
|
||||||
break;
|
|
||||||
lineStart = lineEnd + 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
request.body = bodySeparator == std::string::npos ? std::string() : rawRequest.substr(bodySeparator + 4);
|
|
||||||
return !request.method.empty() && !request.path.empty();
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "NativeSockets.h"
|
|
||||||
|
|
||||||
#include <winsock2.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class JsonValue;
|
|
||||||
|
|
||||||
class ControlServer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
struct Callbacks
|
|
||||||
{
|
|
||||||
std::function<std::string()> getStateJson;
|
|
||||||
std::function<bool(const std::string&, std::string&)> addLayer;
|
|
||||||
std::function<bool(const std::string&, std::string&)> removeLayer;
|
|
||||||
std::function<bool(const std::string&, int, std::string&)> moveLayer;
|
|
||||||
std::function<bool(const std::string&, std::size_t, std::string&)> moveLayerToIndex;
|
|
||||||
std::function<bool(const std::string&, bool, std::string&)> setLayerBypass;
|
|
||||||
std::function<bool(const std::string&, const std::string&, std::string&)> setLayerShader;
|
|
||||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateLayerParameter;
|
|
||||||
std::function<bool(const std::string&, std::string&)> resetLayerParameters;
|
|
||||||
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
|
||||||
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
|
||||||
std::function<bool(std::string&)> reloadShader;
|
|
||||||
std::function<bool(std::string&)> requestScreenshot;
|
|
||||||
};
|
|
||||||
|
|
||||||
ControlServer();
|
|
||||||
~ControlServer();
|
|
||||||
|
|
||||||
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
|
|
||||||
void Stop();
|
|
||||||
void BroadcastState();
|
|
||||||
void RequestBroadcastState();
|
|
||||||
|
|
||||||
unsigned short GetPort() const { return mPort; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ClientConnection
|
|
||||||
{
|
|
||||||
UniqueSocket socket;
|
|
||||||
bool websocket = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HttpRequest
|
|
||||||
{
|
|
||||||
std::string method;
|
|
||||||
std::string path;
|
|
||||||
std::map<std::string, std::string> headers;
|
|
||||||
std::string body;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HttpResponse
|
|
||||||
{
|
|
||||||
std::string status;
|
|
||||||
std::string contentType;
|
|
||||||
std::string body;
|
|
||||||
bool broadcastState = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
void ServerLoop();
|
|
||||||
bool HandleHttpClient(UniqueSocket clientSocket);
|
|
||||||
bool TryAcceptClient();
|
|
||||||
bool SendHttpResponse(SOCKET clientSocket, const HttpResponse& response);
|
|
||||||
bool SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body);
|
|
||||||
bool HandleHttpRequest(UniqueSocket clientSocket, const std::string& request);
|
|
||||||
bool HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request);
|
|
||||||
HttpResponse RouteHttpRequest(const HttpRequest& request);
|
|
||||||
HttpResponse ServeGetRequest(const HttpRequest& request) const;
|
|
||||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
|
||||||
HttpResponse ServeDocsAsset(const std::string& relativePath) const;
|
|
||||||
HttpResponse ServeOpenApiSpec() const;
|
|
||||||
HttpResponse ServeSwaggerDocs() const;
|
|
||||||
HttpResponse HandleApiPost(const HttpRequest& request);
|
|
||||||
bool InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError);
|
|
||||||
bool SendWebSocketText(SOCKET clientSocket, const std::string& payload);
|
|
||||||
void BroadcastStateLocked();
|
|
||||||
std::string LoadUiAsset(const std::string& relativePath, std::string& contentType) const;
|
|
||||||
std::string LoadTextFile(const std::filesystem::path& path) const;
|
|
||||||
std::string BuildJsonResponse(bool success, const std::string& error = std::string()) const;
|
|
||||||
static std::string Base64Encode(const unsigned char* data, DWORD dataLength);
|
|
||||||
static std::string ComputeWebSocketAcceptKey(const std::string& clientKey);
|
|
||||||
static std::string GetHeaderValue(const HttpRequest& request, const std::string& headerName);
|
|
||||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::filesystem::path mUiRoot;
|
|
||||||
std::filesystem::path mDocsRoot;
|
|
||||||
Callbacks mCallbacks;
|
|
||||||
UniqueSocket mListenSocket;
|
|
||||||
unsigned short mPort;
|
|
||||||
std::thread mThread;
|
|
||||||
std::atomic<bool> mRunning;
|
|
||||||
std::atomic<bool> mBroadcastPending;
|
|
||||||
mutable std::mutex mMutex;
|
|
||||||
std::vector<ClientConnection> mClients;
|
|
||||||
};
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
#include "ControlServices.h"
|
|
||||||
|
|
||||||
#include "ControlServer.h"
|
|
||||||
#include "OscServer.h"
|
|
||||||
#include "RuntimeControlBridge.h"
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
ControlServices::ControlServices() :
|
|
||||||
mControlServer(std::make_unique<ControlServer>()),
|
|
||||||
mOscServer(std::make_unique<OscServer>()),
|
|
||||||
mPollRunning(false),
|
|
||||||
mRegistryChanged(false),
|
|
||||||
mReloadRequested(false),
|
|
||||||
mPollFailed(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
ControlServices::~ControlServices()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
|
|
||||||
if (!StartControlServicesBoundary(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::BeginPolling(RuntimeHost& runtimeHost)
|
|
||||||
{
|
|
||||||
StartPolling(runtimeHost);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::Stop()
|
|
||||||
{
|
|
||||||
StopPolling();
|
|
||||||
|
|
||||||
if (mOscServer)
|
|
||||||
mOscServer->Stop();
|
|
||||||
|
|
||||||
if (mControlServer)
|
|
||||||
mControlServer->Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::BroadcastState()
|
|
||||||
{
|
|
||||||
if (mControlServer)
|
|
||||||
mControlServer->BroadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::RequestBroadcastState()
|
|
||||||
{
|
|
||||||
if (mControlServer)
|
|
||||||
mControlServer->RequestBroadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
|
||||||
{
|
|
||||||
(void)error;
|
|
||||||
|
|
||||||
PendingOscUpdate update;
|
|
||||||
update.layerKey = layerKey;
|
|
||||||
update.parameterKey = parameterKey;
|
|
||||||
update.valueJson = valueJson;
|
|
||||||
|
|
||||||
const std::string routeKey = layerKey + "\n" + parameterKey;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
|
||||||
mPendingOscUpdates[routeKey] = std::move(update);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
|
|
||||||
{
|
|
||||||
appliedUpdates.clear();
|
|
||||||
|
|
||||||
std::map<std::string, PendingOscUpdate> pending;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
|
||||||
if (mPendingOscUpdates.empty())
|
|
||||||
return true;
|
|
||||||
pending.swap(mPendingOscUpdates);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& entry : pending)
|
|
||||||
{
|
|
||||||
JsonValue targetValue;
|
|
||||||
std::string parseError;
|
|
||||||
if (!ParseJson(entry.second.valueJson, targetValue, parseError))
|
|
||||||
{
|
|
||||||
OutputDebugStringA(("OSC queued value parse failed: " + parseError + "\n").c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppliedOscUpdate appliedUpdate;
|
|
||||||
appliedUpdate.routeKey = entry.first;
|
|
||||||
appliedUpdate.layerKey = entry.second.layerKey;
|
|
||||||
appliedUpdate.parameterKey = entry.second.parameterKey;
|
|
||||||
appliedUpdate.targetValue = targetValue;
|
|
||||||
appliedUpdates.push_back(std::move(appliedUpdate));
|
|
||||||
}
|
|
||||||
|
|
||||||
(void)error;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ControlServices::QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error)
|
|
||||||
{
|
|
||||||
(void)error;
|
|
||||||
|
|
||||||
PendingOscCommit commit;
|
|
||||||
commit.routeKey = routeKey;
|
|
||||||
commit.layerKey = layerKey;
|
|
||||||
commit.parameterKey = parameterKey;
|
|
||||||
commit.value = value;
|
|
||||||
commit.generation = generation;
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
|
||||||
mPendingOscCommits[routeKey] = std::move(commit);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::ClearOscState()
|
|
||||||
{
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
|
||||||
mPendingOscUpdates.clear();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
|
||||||
mPendingOscCommits.clear();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
|
||||||
mCompletedOscCommits.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
|
||||||
{
|
|
||||||
completedCommits.clear();
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
|
||||||
if (mCompletedOscCommits.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
completedCommits.swap(mCompletedOscCommits);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimePollEvents ControlServices::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 ControlServices::StartPolling(RuntimeHost& runtimeHost)
|
|
||||||
{
|
|
||||||
if (mPollRunning.exchange(true))
|
|
||||||
return;
|
|
||||||
|
|
||||||
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::StopPolling()
|
|
||||||
{
|
|
||||||
if (!mPollRunning.exchange(false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (mPollThread.joinable())
|
|
||||||
mPollThread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ControlServices::PollLoop(RuntimeHost& runtimeHost)
|
|
||||||
{
|
|
||||||
while (mPollRunning)
|
|
||||||
{
|
|
||||||
std::map<std::string, PendingOscCommit> pendingCommits;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
|
||||||
pendingCommits.swap(mPendingOscCommits);
|
|
||||||
}
|
|
||||||
for (const auto& entry : pendingCommits)
|
|
||||||
{
|
|
||||||
std::string commitError;
|
|
||||||
if (runtimeHost.UpdateLayerParameterByControlKey(
|
|
||||||
entry.second.layerKey,
|
|
||||||
entry.second.parameterKey,
|
|
||||||
entry.second.value,
|
|
||||||
false,
|
|
||||||
commitError))
|
|
||||||
{
|
|
||||||
CompletedOscCommit completedCommit;
|
|
||||||
completedCommit.routeKey = entry.second.routeKey;
|
|
||||||
completedCommit.generation = entry.second.generation;
|
|
||||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
|
||||||
mCompletedOscCommits.push_back(std::move(completedCommit));
|
|
||||||
}
|
|
||||||
else if (!commitError.empty())
|
|
||||||
{
|
|
||||||
OutputDebugStringA(("OSC commit failed: " + commitError + "\n").c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class ControlServer;
|
|
||||||
class OpenGLComposite;
|
|
||||||
class OscServer;
|
|
||||||
class RuntimeHost;
|
|
||||||
|
|
||||||
struct RuntimePollEvents
|
|
||||||
{
|
|
||||||
bool registryChanged = false;
|
|
||||||
bool reloadRequested = false;
|
|
||||||
bool failed = false;
|
|
||||||
std::string error;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ControlServices
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
struct AppliedOscUpdate
|
|
||||||
{
|
|
||||||
std::string routeKey;
|
|
||||||
std::string layerKey;
|
|
||||||
std::string parameterKey;
|
|
||||||
JsonValue targetValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CompletedOscCommit
|
|
||||||
{
|
|
||||||
std::string routeKey;
|
|
||||||
uint64_t generation = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ControlServices();
|
|
||||||
~ControlServices();
|
|
||||||
|
|
||||||
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
|
||||||
void BeginPolling(RuntimeHost& runtimeHost);
|
|
||||||
void Stop();
|
|
||||||
void BroadcastState();
|
|
||||||
void RequestBroadcastState();
|
|
||||||
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
|
||||||
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
|
|
||||||
bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error);
|
|
||||||
void ClearOscState();
|
|
||||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
|
||||||
RuntimePollEvents ConsumePollEvents();
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct PendingOscUpdate
|
|
||||||
{
|
|
||||||
std::string layerKey;
|
|
||||||
std::string parameterKey;
|
|
||||||
std::string valueJson;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PendingOscCommit
|
|
||||||
{
|
|
||||||
std::string routeKey;
|
|
||||||
std::string layerKey;
|
|
||||||
std::string parameterKey;
|
|
||||||
JsonValue value;
|
|
||||||
uint64_t generation = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
std::mutex mPendingOscMutex;
|
|
||||||
std::map<std::string, PendingOscUpdate> mPendingOscUpdates;
|
|
||||||
std::mutex mPendingOscCommitMutex;
|
|
||||||
std::map<std::string, PendingOscCommit> mPendingOscCommits;
|
|
||||||
std::mutex mCompletedOscCommitMutex;
|
|
||||||
std::vector<CompletedOscCommit> mCompletedOscCommits;
|
|
||||||
};
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
#include "stdafx.h"
|
|
||||||
#include "OscServer.h"
|
|
||||||
|
|
||||||
#include <ws2tcpip.h>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstring>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#pragma comment(lib, "Ws2_32.lib")
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
bool InitializeWinsock(std::string& error)
|
|
||||||
{
|
|
||||||
WSADATA wsaData = {};
|
|
||||||
const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
error = "WSAStartup failed.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> SplitAddress(const std::string& address)
|
|
||||||
{
|
|
||||||
std::vector<std::string> parts;
|
|
||||||
std::size_t start = !address.empty() && address[0] == '/' ? 1 : 0;
|
|
||||||
|
|
||||||
while (start <= address.size())
|
|
||||||
{
|
|
||||||
const std::size_t slash = address.find('/', start);
|
|
||||||
const std::size_t end = slash == std::string::npos ? address.size() : slash;
|
|
||||||
if (end > start)
|
|
||||||
parts.push_back(address.substr(start, end - start));
|
|
||||||
if (slash == std::string::npos)
|
|
||||||
break;
|
|
||||||
start = slash + 1;
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
OscServer::OscServer()
|
|
||||||
: mPort(0), mRunning(false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
OscServer::~OscServer()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error)
|
|
||||||
{
|
|
||||||
if (port == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
mCallbacks = callbacks;
|
|
||||||
mPort = port;
|
|
||||||
|
|
||||||
if (!InitializeWinsock(error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mSocket.reset(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
|
|
||||||
if (!mSocket.valid())
|
|
||||||
{
|
|
||||||
error = "Could not create OSC UDP socket.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DWORD timeoutMilliseconds = 100;
|
|
||||||
setsockopt(mSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMilliseconds), sizeof(timeoutMilliseconds));
|
|
||||||
|
|
||||||
sockaddr_in address = {};
|
|
||||||
address.sin_family = AF_INET;
|
|
||||||
if (!TryParseBindAddress(bindAddress, address.sin_addr, error))
|
|
||||||
{
|
|
||||||
mSocket.reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
address.sin_port = htons(static_cast<u_short>(port));
|
|
||||||
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
|
|
||||||
{
|
|
||||||
error = "Could not bind OSC listener to " + bindAddress + ":" + std::to_string(port) + ".";
|
|
||||||
mSocket.reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRunning = true;
|
|
||||||
mThread = std::thread(&OscServer::ServerLoop, this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
|
|
||||||
{
|
|
||||||
if (bindAddress.empty())
|
|
||||||
{
|
|
||||||
error = "OSC bind address must not be empty.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
address = {};
|
|
||||||
if (InetPtonA(AF_INET, bindAddress.c_str(), &address) != 1)
|
|
||||||
{
|
|
||||||
error = "Invalid OSC bind address '" + bindAddress + "'. Use an IPv4 address such as 127.0.0.1 or 0.0.0.0.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OscServer::Stop()
|
|
||||||
{
|
|
||||||
mRunning = false;
|
|
||||||
mSocket.reset();
|
|
||||||
if (mThread.joinable())
|
|
||||||
mThread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OscServer::ServerLoop()
|
|
||||||
{
|
|
||||||
std::array<char, 4096> buffer = {};
|
|
||||||
while (mRunning)
|
|
||||||
{
|
|
||||||
sockaddr_in sender = {};
|
|
||||||
int senderLength = sizeof(sender);
|
|
||||||
const int byteCount = recvfrom(mSocket.get(), buffer.data(), static_cast<int>(buffer.size()), 0,
|
|
||||||
reinterpret_cast<sockaddr*>(&sender), &senderLength);
|
|
||||||
if (byteCount <= 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
OscMessage message;
|
|
||||||
std::string error;
|
|
||||||
if (DecodeMessage(buffer.data(), byteCount, message, error))
|
|
||||||
{
|
|
||||||
if (!DispatchMessage(message, error) && !error.empty())
|
|
||||||
OutputDebugStringA(("OSC dispatch failed: " + error + "\n").c_str());
|
|
||||||
}
|
|
||||||
else if (!error.empty())
|
|
||||||
{
|
|
||||||
OutputDebugStringA(("OSC decode failed: " + error + "\n").c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const
|
|
||||||
{
|
|
||||||
int offset = 0;
|
|
||||||
if (!ReadPaddedString(data, byteCount, offset, message.address) || message.address.empty() || message.address[0] != '/')
|
|
||||||
{
|
|
||||||
error = "Invalid OSC address.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string typeTags;
|
|
||||||
if (!ReadPaddedString(data, byteCount, offset, typeTags) || typeTags.empty() || typeTags[0] != ',')
|
|
||||||
{
|
|
||||||
error = "Invalid OSC type tag string.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeTags.size() < 2)
|
|
||||||
{
|
|
||||||
error = "OSC message has no parameter value.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> values;
|
|
||||||
for (std::size_t index = 1; index < typeTags.size(); ++index)
|
|
||||||
{
|
|
||||||
std::string valueJson;
|
|
||||||
if (!DecodeArgument(data, byteCount, offset, typeTags[index], valueJson))
|
|
||||||
{
|
|
||||||
error = "Unsupported or malformed OSC value type.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
values.push_back(valueJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.size() == 1)
|
|
||||||
{
|
|
||||||
message.valueJson = values.front();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream arrayJson;
|
|
||||||
arrayJson << "[";
|
|
||||||
for (std::size_t index = 0; index < values.size(); ++index)
|
|
||||||
{
|
|
||||||
if (index > 0)
|
|
||||||
arrayJson << ",";
|
|
||||||
arrayJson << values[index];
|
|
||||||
}
|
|
||||||
arrayJson << "]";
|
|
||||||
message.valueJson = arrayJson.str();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const
|
|
||||||
{
|
|
||||||
const std::vector<std::string> parts = SplitAddress(message.address);
|
|
||||||
if (parts.size() != 3 || parts[0] != "VideoShaderToys")
|
|
||||||
{
|
|
||||||
error = "Unsupported OSC address: " + message.address;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mCallbacks.updateParameter &&
|
|
||||||
mCallbacks.updateParameter(parts[1], parts[2], message.valueJson, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson)
|
|
||||||
{
|
|
||||||
if (valueType == 'f')
|
|
||||||
{
|
|
||||||
double value = 0.0;
|
|
||||||
if (!ReadFloat32(data, byteCount, offset, value))
|
|
||||||
return false;
|
|
||||||
std::ostringstream stream;
|
|
||||||
stream << std::setprecision(9) << value;
|
|
||||||
valueJson = stream.str();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueType == 'd')
|
|
||||||
{
|
|
||||||
double value = 0.0;
|
|
||||||
if (!ReadFloat64(data, byteCount, offset, value))
|
|
||||||
return false;
|
|
||||||
std::ostringstream stream;
|
|
||||||
stream << std::setprecision(17) << value;
|
|
||||||
valueJson = stream.str();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueType == 'i')
|
|
||||||
{
|
|
||||||
int value = 0;
|
|
||||||
if (!ReadInt32(data, byteCount, offset, value))
|
|
||||||
return false;
|
|
||||||
valueJson = std::to_string(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueType == 's')
|
|
||||||
{
|
|
||||||
std::string value;
|
|
||||||
if (!ReadPaddedString(data, byteCount, offset, value))
|
|
||||||
return false;
|
|
||||||
valueJson = BuildJsonString(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueType == 'T' || valueType == 'F')
|
|
||||||
{
|
|
||||||
valueJson = valueType == 'T' ? "true" : "false";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value)
|
|
||||||
{
|
|
||||||
if (offset < 0 || offset >= byteCount)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const int start = offset;
|
|
||||||
while (offset < byteCount && data[offset] != '\0')
|
|
||||||
++offset;
|
|
||||||
if (offset >= byteCount)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
value.assign(data + start, data + offset);
|
|
||||||
++offset;
|
|
||||||
while (offset % 4 != 0)
|
|
||||||
++offset;
|
|
||||||
return offset <= byteCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::ReadInt32(const char* data, int byteCount, int& offset, int& value)
|
|
||||||
{
|
|
||||||
if (offset + 4 > byteCount)
|
|
||||||
return false;
|
|
||||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
|
|
||||||
value = static_cast<int>((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
|
|
||||||
offset += 4;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double& value)
|
|
||||||
{
|
|
||||||
int bits = 0;
|
|
||||||
if (!ReadInt32(data, byteCount, offset, bits))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
float floatValue = 0.0f;
|
|
||||||
const unsigned int unsignedBits = static_cast<unsigned int>(bits);
|
|
||||||
std::memcpy(&floatValue, &unsignedBits, sizeof(floatValue));
|
|
||||||
value = static_cast<double>(floatValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OscServer::ReadFloat64(const char* data, int byteCount, int& offset, double& value)
|
|
||||||
{
|
|
||||||
if (offset + 8 > byteCount)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
|
|
||||||
uint64_t bits = 0;
|
|
||||||
for (int index = 0; index < 8; ++index)
|
|
||||||
bits = (bits << 8) | static_cast<uint64_t>(bytes[index]);
|
|
||||||
|
|
||||||
std::memcpy(&value, &bits, sizeof(value));
|
|
||||||
offset += 8;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string OscServer::BuildJsonString(const std::string& value)
|
|
||||||
{
|
|
||||||
std::ostringstream stream;
|
|
||||||
stream << '"';
|
|
||||||
for (char ch : value)
|
|
||||||
{
|
|
||||||
if (ch == '"' || ch == '\\')
|
|
||||||
stream << '\\';
|
|
||||||
stream << ch;
|
|
||||||
}
|
|
||||||
stream << '"';
|
|
||||||
return stream.str();
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "NativeSockets.h"
|
|
||||||
|
|
||||||
#include <winsock2.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
class OscServer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
struct Callbacks
|
|
||||||
{
|
|
||||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
|
||||||
};
|
|
||||||
|
|
||||||
OscServer();
|
|
||||||
~OscServer();
|
|
||||||
|
|
||||||
bool Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error);
|
|
||||||
void Stop();
|
|
||||||
|
|
||||||
unsigned short GetPort() const { return mPort; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend struct OscServerTestAccess;
|
|
||||||
|
|
||||||
struct OscMessage
|
|
||||||
{
|
|
||||||
std::string address;
|
|
||||||
std::string valueJson;
|
|
||||||
};
|
|
||||||
|
|
||||||
void ServerLoop();
|
|
||||||
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
|
|
||||||
bool DispatchMessage(const OscMessage& message, std::string& error) const;
|
|
||||||
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error);
|
|
||||||
static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson);
|
|
||||||
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
|
|
||||||
static bool ReadInt32(const char* data, int byteCount, int& offset, int& value);
|
|
||||||
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value);
|
|
||||||
static bool ReadFloat64(const char* data, int byteCount, int& offset, double& value);
|
|
||||||
static std::string BuildJsonString(const std::string& value);
|
|
||||||
|
|
||||||
Callbacks mCallbacks;
|
|
||||||
UniqueSocket mSocket;
|
|
||||||
unsigned short mPort;
|
|
||||||
std::thread mThread;
|
|
||||||
std::atomic<bool> mRunning;
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#include "RuntimeControlBridge.h"
|
|
||||||
|
|
||||||
#include "ControlServices.h"
|
|
||||||
#include "ControlServer.h"
|
|
||||||
#include "OpenGLComposite.h"
|
|
||||||
#include "OscServer.h"
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
bool StartControlServicesBoundary(
|
|
||||||
OpenGLComposite& composite,
|
|
||||||
RuntimeHost& runtimeHost,
|
|
||||||
ControlServices& controlServices,
|
|
||||||
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 = [&controlServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
|
||||||
return controlServices.QueueOscUpdate(layerKey, parameterKey, valueJson, actionError);
|
|
||||||
};
|
|
||||||
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class ControlServer;
|
|
||||||
class ControlServices;
|
|
||||||
class OpenGLComposite;
|
|
||||||
class OscServer;
|
|
||||||
class RuntimeHost;
|
|
||||||
|
|
||||||
bool StartControlServicesBoundary(
|
|
||||||
OpenGLComposite& composite,
|
|
||||||
RuntimeHost& runtimeHost,
|
|
||||||
ControlServices& controlServices,
|
|
||||||
ControlServer& controlServer,
|
|
||||||
OscServer& oscServer,
|
|
||||||
std::string& error);
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#include "RuntimeServices.h"
|
|
||||||
|
|
||||||
RuntimeServices::RuntimeServices() :
|
|
||||||
mControlServices(std::make_unique<ControlServices>())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeServices::~RuntimeServices()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
|
||||||
{
|
|
||||||
return mControlServices && mControlServices->Start(composite, runtimeHost, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
|
|
||||||
{
|
|
||||||
if (mControlServices)
|
|
||||||
mControlServices->BeginPolling(runtimeHost);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::Stop()
|
|
||||||
{
|
|
||||||
if (mControlServices)
|
|
||||||
mControlServices->Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::BroadcastState()
|
|
||||||
{
|
|
||||||
if (mControlServices)
|
|
||||||
mControlServices->BroadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::RequestBroadcastState()
|
|
||||||
{
|
|
||||||
if (mControlServices)
|
|
||||||
mControlServices->RequestBroadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
|
||||||
{
|
|
||||||
return mControlServices && mControlServices->QueueOscUpdate(layerKey, parameterKey, valueJson, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
|
|
||||||
{
|
|
||||||
if (!mControlServices)
|
|
||||||
{
|
|
||||||
appliedUpdates.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mControlServices->ApplyPendingOscUpdates(appliedUpdates, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeServices::QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error)
|
|
||||||
{
|
|
||||||
return mControlServices && mControlServices->QueueOscCommit(routeKey, layerKey, parameterKey, value, generation, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::ClearOscState()
|
|
||||||
{
|
|
||||||
if (mControlServices)
|
|
||||||
mControlServices->ClearOscState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
|
||||||
{
|
|
||||||
if (!mControlServices)
|
|
||||||
{
|
|
||||||
completedCommits.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mControlServices->ConsumeCompletedOscCommits(completedCommits);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
|
||||||
{
|
|
||||||
return mControlServices ? mControlServices->ConsumePollEvents() : RuntimePollEvents{};
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ControlServices.h"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
class OpenGLComposite;
|
|
||||||
class RuntimeHost;
|
|
||||||
|
|
||||||
class RuntimeServices
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using AppliedOscUpdate = ControlServices::AppliedOscUpdate;
|
|
||||||
using CompletedOscCommit = ControlServices::CompletedOscCommit;
|
|
||||||
|
|
||||||
RuntimeServices();
|
|
||||||
~RuntimeServices();
|
|
||||||
|
|
||||||
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
|
||||||
void BeginPolling(RuntimeHost& runtimeHost);
|
|
||||||
void Stop();
|
|
||||||
void BroadcastState();
|
|
||||||
void RequestBroadcastState();
|
|
||||||
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
|
||||||
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
|
|
||||||
bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error);
|
|
||||||
void ClearOscState();
|
|
||||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
|
||||||
RuntimePollEvents ConsumePollEvents();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unique_ptr<ControlServices> mControlServices;
|
|
||||||
};
|
|
||||||
@@ -1,758 +0,0 @@
|
|||||||
#include "DeckLinkDisplayMode.h"
|
|
||||||
#include "OpenGLComposite.h"
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include "GlRenderConstants.h"
|
|
||||||
#include "PngScreenshotWriter.h"
|
|
||||||
#include "RenderEngine.h"
|
|
||||||
#include "RuntimeParameterUtils.h"
|
|
||||||
#include "RuntimeServices.h"
|
|
||||||
#include "ShaderBuildQueue.h"
|
|
||||||
#include "VideoBackend.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cmath>
|
|
||||||
#include <ctime>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <memory>
|
|
||||||
#include <set>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
|
||||||
constexpr double kOscSmoothingReferenceFps = 60.0;
|
|
||||||
constexpr double kOscSmoothingMaxStepSeconds = 0.25;
|
|
||||||
|
|
||||||
std::string SimplifyOscControlKey(const std::string& text)
|
|
||||||
{
|
|
||||||
std::string simplified;
|
|
||||||
for (unsigned char ch : text)
|
|
||||||
{
|
|
||||||
if (std::isalnum(ch))
|
|
||||||
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
|
||||||
}
|
|
||||||
return simplified;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
|
|
||||||
{
|
|
||||||
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
double ClampOscAlpha(double value)
|
|
||||||
{
|
|
||||||
return (std::max)(0.0, (std::min)(1.0, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds)
|
|
||||||
{
|
|
||||||
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
|
||||||
if (clampedSmoothing <= 0.0)
|
|
||||||
return 0.0;
|
|
||||||
if (clampedSmoothing >= 1.0)
|
|
||||||
return 1.0;
|
|
||||||
|
|
||||||
const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds));
|
|
||||||
if (clampedDeltaSeconds <= 0.0)
|
|
||||||
return 0.0;
|
|
||||||
|
|
||||||
const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps;
|
|
||||||
return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
|
||||||
{
|
|
||||||
switch (definition.type)
|
|
||||||
{
|
|
||||||
case ShaderParameterType::Boolean:
|
|
||||||
return JsonValue(value.booleanValue);
|
|
||||||
case ShaderParameterType::Enum:
|
|
||||||
return JsonValue(value.enumValue);
|
|
||||||
case ShaderParameterType::Text:
|
|
||||||
return JsonValue(value.textValue);
|
|
||||||
case ShaderParameterType::Trigger:
|
|
||||||
case ShaderParameterType::Float:
|
|
||||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
|
||||||
case ShaderParameterType::Vec2:
|
|
||||||
case ShaderParameterType::Color:
|
|
||||||
{
|
|
||||||
JsonValue array = JsonValue::MakeArray();
|
|
||||||
for (double number : value.numberValues)
|
|
||||||
array.pushBack(JsonValue(number));
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|
||||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
|
||||||
mUseCommittedLayerStates(false),
|
|
||||||
mScreenshotRequested(false)
|
|
||||||
{
|
|
||||||
InitializeCriticalSection(&pMutex);
|
|
||||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
|
||||||
mRuntimeStore = std::make_unique<RuntimeStore>(*mRuntimeHost);
|
|
||||||
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(*mRuntimeHost);
|
|
||||||
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore);
|
|
||||||
mRenderEngine = std::make_unique<RenderEngine>(
|
|
||||||
*mRuntimeSnapshotProvider,
|
|
||||||
mRuntimeHost->GetHealthTelemetry(),
|
|
||||||
pMutex,
|
|
||||||
hGLDC,
|
|
||||||
hGLRC,
|
|
||||||
[this]() { renderEffect(); },
|
|
||||||
[this]() { ProcessScreenshotRequest(); },
|
|
||||||
[this]() { paintGL(false); });
|
|
||||||
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeHost->GetHealthTelemetry());
|
|
||||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider);
|
|
||||||
mRuntimeServices = std::make_unique<RuntimeServices>();
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLComposite::~OpenGLComposite()
|
|
||||||
{
|
|
||||||
if (mRuntimeServices)
|
|
||||||
mRuntimeServices->Stop();
|
|
||||||
if (mShaderBuildQueue)
|
|
||||||
mShaderBuildQueue->Stop();
|
|
||||||
if (mVideoBackend)
|
|
||||||
mVideoBackend->ReleaseResources();
|
|
||||||
|
|
||||||
DeleteCriticalSection(&pMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::InitDeckLink()
|
|
||||||
{
|
|
||||||
return InitVideoIO();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::InitVideoIO()
|
|
||||||
{
|
|
||||||
VideoFormatSelection videoModes;
|
|
||||||
std::string initFailureReason;
|
|
||||||
|
|
||||||
if (mRuntimeStore && mRuntimeStore->GetRuntimeRepositoryRoot().empty())
|
|
||||||
{
|
|
||||||
std::string runtimeError;
|
|
||||||
if (!mRuntimeStore->InitializeStore(runtimeError))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mRuntimeStore)
|
|
||||||
{
|
|
||||||
if (!ResolveConfiguredVideoFormats(
|
|
||||||
mRuntimeStore->GetConfiguredInputVideoFormat(),
|
|
||||||
mRuntimeStore->GetConfiguredInputFrameRate(),
|
|
||||||
mRuntimeStore->GetConfiguredOutputVideoFormat(),
|
|
||||||
mRuntimeStore->GetConfiguredOutputFrameRate(),
|
|
||||||
videoModes,
|
|
||||||
initFailureReason))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mVideoBackend->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;
|
|
||||||
}
|
|
||||||
const bool outputAlphaRequired = mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured();
|
|
||||||
if (!mVideoBackend->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
|
|
||||||
goto error;
|
|
||||||
|
|
||||||
if (! CheckOpenGLExtensions())
|
|
||||||
{
|
|
||||||
initFailureReason = "OpenGL extension checks failed.";
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! InitOpenGLState())
|
|
||||||
{
|
|
||||||
initFailureReason = "OpenGL state initialization failed.";
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
|
|
||||||
PublishVideoIOStatus(mVideoBackend->OutputModelName().empty()
|
|
||||||
? "DeckLink output device selected."
|
|
||||||
: ("Selected output device: " + mVideoBackend->OutputModelName()));
|
|
||||||
|
|
||||||
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
|
||||||
if (mVideoBackend->OutputFrameWidth() < 1920)
|
|
||||||
resizeWindow(mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight());
|
|
||||||
else
|
|
||||||
resizeWindow(mVideoBackend->OutputFrameWidth() / 2, mVideoBackend->OutputFrameHeight() / 2);
|
|
||||||
|
|
||||||
if (!mVideoBackend->ConfigureInput(videoModes.input, initFailureReason))
|
|
||||||
{
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
if (!mVideoBackend->HasInputDevice() && mRuntimeHost)
|
|
||||||
{
|
|
||||||
mRuntimeHost->GetHealthTelemetry().ReportSignalStatus(
|
|
||||||
false,
|
|
||||||
mVideoBackend->InputFrameWidth(),
|
|
||||||
mVideoBackend->InputFrameHeight(),
|
|
||||||
mVideoBackend->InputDisplayModeName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mVideoBackend->ConfigureOutput(videoModes.output, mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), initFailureReason))
|
|
||||||
{
|
|
||||||
goto error;
|
|
||||||
}
|
|
||||||
|
|
||||||
PublishVideoIOStatus(mVideoBackend->StatusMessage());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
error:
|
|
||||||
if (!initFailureReason.empty())
|
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
|
||||||
mVideoBackend->ReleaseResources();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::paintGL(bool force)
|
|
||||||
{
|
|
||||||
if (!force)
|
|
||||||
{
|
|
||||||
if (IsIconic(hGLWnd))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u;
|
|
||||||
if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()))
|
|
||||||
{
|
|
||||||
ValidateRect(hGLWnd, NULL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidateRect(hGLWnd, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
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().
|
|
||||||
mRenderEngine->ResizeView(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::resizeWindow(int width, int height)
|
|
||||||
{
|
|
||||||
RECT r;
|
|
||||||
if (GetWindowRect(hGLWnd, &r))
|
|
||||||
{
|
|
||||||
SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
|
||||||
{
|
|
||||||
if (!mRuntimeHost)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!statusMessage.empty())
|
|
||||||
mVideoBackend->SetStatusMessage(statusMessage);
|
|
||||||
|
|
||||||
mRuntimeHost->SetVideoIOStatus(
|
|
||||||
"decklink",
|
|
||||||
mVideoBackend->OutputModelName(),
|
|
||||||
mVideoBackend->SupportsInternalKeying(),
|
|
||||||
mVideoBackend->SupportsExternalKeying(),
|
|
||||||
mVideoBackend->KeyerInterfaceAvailable(),
|
|
||||||
mRuntimeStore ? mRuntimeStore->IsExternalKeyingConfigured() : false,
|
|
||||||
mVideoBackend->ExternalKeyingActive(),
|
|
||||||
mVideoBackend->StatusMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::InitOpenGLState()
|
|
||||||
{
|
|
||||||
if (! ResolveGLExtensions())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
std::string runtimeError;
|
|
||||||
if (mRuntimeStore->GetRuntimeRepositoryRoot().empty() && !mRuntimeStore->InitializeStore(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 (!mRenderEngine->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!mRenderEngine->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string rendererError;
|
|
||||||
if (!mRenderEngine->InitializeResources(
|
|
||||||
mVideoBackend->InputFrameWidth(),
|
|
||||||
mVideoBackend->InputFrameHeight(),
|
|
||||||
mVideoBackend->CaptureTextureWidth(),
|
|
||||||
mVideoBackend->OutputFrameWidth(),
|
|
||||||
mVideoBackend->OutputFrameHeight(),
|
|
||||||
mVideoBackend->OutputPackTextureWidth(),
|
|
||||||
rendererError))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mRenderEngine->CompileLayerPrograms(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mRuntimeStore->SetCompileStatus(true, "Shader layers compiled successfully.");
|
|
||||||
mUseCommittedLayerStates = false;
|
|
||||||
|
|
||||||
mRenderEngine->ResetTemporalHistoryState();
|
|
||||||
mRenderEngine->ResetShaderFeedbackState();
|
|
||||||
|
|
||||||
broadcastRuntimeState();
|
|
||||||
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::Start()
|
|
||||||
{
|
|
||||||
return mVideoBackend->Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::Stop()
|
|
||||||
{
|
|
||||||
if (mRuntimeServices)
|
|
||||||
mRuntimeServices->Stop();
|
|
||||||
|
|
||||||
const bool wasExternalKeyingActive = mVideoBackend->ExternalKeyingActive();
|
|
||||||
mVideoBackend->Stop();
|
|
||||||
if (wasExternalKeyingActive)
|
|
||||||
PublishVideoIOStatus("External keying has been disabled.");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
|
||||||
{
|
|
||||||
(void)error;
|
|
||||||
mScreenshotRequested.store(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::renderEffect()
|
|
||||||
{
|
|
||||||
ProcessRuntimePollResults();
|
|
||||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
|
||||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
|
||||||
if (mRuntimeHost && mRuntimeServices)
|
|
||||||
{
|
|
||||||
std::string oscError;
|
|
||||||
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
|
||||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
|
||||||
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
|
||||||
{
|
|
||||||
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
|
||||||
if (overlayIt == mOscOverlayStates.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
OscOverlayState& overlay = overlayIt->second;
|
|
||||||
if (overlay.commitQueued &&
|
|
||||||
overlay.pendingCommitGeneration == completedCommit.generation &&
|
|
||||||
overlay.generation == completedCommit.generation)
|
|
||||||
{
|
|
||||||
mOscOverlayStates.erase(overlayIt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> pendingOscRouteKeys;
|
|
||||||
const auto oscNow = std::chrono::steady_clock::now();
|
|
||||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
|
||||||
{
|
|
||||||
const std::string routeKey = update.routeKey;
|
|
||||||
auto overlayIt = mOscOverlayStates.find(routeKey);
|
|
||||||
if (overlayIt == mOscOverlayStates.end())
|
|
||||||
{
|
|
||||||
OscOverlayState overlay;
|
|
||||||
overlay.layerKey = update.layerKey;
|
|
||||||
overlay.parameterKey = update.parameterKey;
|
|
||||||
overlay.targetValue = update.targetValue;
|
|
||||||
overlay.lastUpdatedTime = oscNow;
|
|
||||||
overlay.lastAppliedTime = oscNow;
|
|
||||||
overlay.generation = 1;
|
|
||||||
mOscOverlayStates[routeKey] = std::move(overlay);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
overlayIt->second.targetValue = update.targetValue;
|
|
||||||
overlayIt->second.lastUpdatedTime = oscNow;
|
|
||||||
overlayIt->second.generation += 1;
|
|
||||||
overlayIt->second.commitQueued = false;
|
|
||||||
}
|
|
||||||
pendingOscRouteKeys.insert(routeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
|
|
||||||
{
|
|
||||||
if (states.empty() || mOscOverlayStates.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const double smoothing = ClampOscAlpha(mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0);
|
|
||||||
std::vector<std::string> overlayKeysToRemove;
|
|
||||||
for (auto& item : mOscOverlayStates)
|
|
||||||
{
|
|
||||||
OscOverlayState& overlay = item.second;
|
|
||||||
auto stateIt = std::find_if(states.begin(), states.end(),
|
|
||||||
[&overlay](const RuntimeRenderState& state)
|
|
||||||
{
|
|
||||||
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
|
||||||
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
|
||||||
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
|
||||||
});
|
|
||||||
if (stateIt == states.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
|
||||||
[&overlay](const ShaderParameterDefinition& definition)
|
|
||||||
{
|
|
||||||
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
|
||||||
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
|
||||||
});
|
|
||||||
if (definitionIt == stateIt->parameterDefinitions.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
|
||||||
{
|
|
||||||
if (pendingOscRouteKeys.find(item.first) == pendingOscRouteKeys.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
|
||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
|
||||||
const double triggerTime = stateIt->timeSeconds;
|
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
|
||||||
overlayKeysToRemove.push_back(item.first);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderParameterValue targetValue;
|
|
||||||
std::string normalizeError;
|
|
||||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const bool smoothable =
|
|
||||||
smoothing > 0.0 &&
|
|
||||||
(definitionIt->type == ShaderParameterType::Float ||
|
|
||||||
definitionIt->type == ShaderParameterType::Vec2 ||
|
|
||||||
definitionIt->type == ShaderParameterType::Color);
|
|
||||||
if (!smoothable)
|
|
||||||
{
|
|
||||||
overlay.currentValue = targetValue;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
|
||||||
if (allowCommit &&
|
|
||||||
!overlay.commitQueued &&
|
|
||||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
|
||||||
mRuntimeServices)
|
|
||||||
{
|
|
||||||
std::string commitError;
|
|
||||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation, commitError))
|
|
||||||
{
|
|
||||||
overlay.pendingCommitGeneration = overlay.generation;
|
|
||||||
overlay.commitQueued = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overlay.hasCurrentValue)
|
|
||||||
{
|
|
||||||
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
|
||||||
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
|
||||||
if (currentIt != stateIt->parameterValues.end())
|
|
||||||
overlay.currentValue = currentIt->second;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
|
||||||
overlay.currentValue.numberValues = targetValue.numberValues;
|
|
||||||
|
|
||||||
double smoothingAlpha = smoothing;
|
|
||||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
|
||||||
{
|
|
||||||
const double deltaSeconds =
|
|
||||||
std::chrono::duration_cast<std::chrono::duration<double>>(oscNow - overlay.lastAppliedTime).count();
|
|
||||||
smoothingAlpha = ComputeTimeBasedOscAlpha(smoothing, deltaSeconds);
|
|
||||||
}
|
|
||||||
overlay.lastAppliedTime = oscNow;
|
|
||||||
|
|
||||||
ShaderParameterValue nextValue = targetValue;
|
|
||||||
bool converged = true;
|
|
||||||
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
|
||||||
{
|
|
||||||
const double currentNumber = overlay.currentValue.numberValues[index];
|
|
||||||
const double targetNumber = targetValue.numberValues[index];
|
|
||||||
const double delta = targetNumber - currentNumber;
|
|
||||||
double nextNumber = currentNumber + delta * smoothingAlpha;
|
|
||||||
if (std::fabs(delta) <= 0.0005)
|
|
||||||
nextNumber = targetNumber;
|
|
||||||
else
|
|
||||||
converged = false;
|
|
||||||
nextValue.numberValues[index] = nextNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (converged)
|
|
||||||
nextValue.numberValues = targetValue.numberValues;
|
|
||||||
|
|
||||||
overlay.currentValue = nextValue;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
|
||||||
if (allowCommit &&
|
|
||||||
converged &&
|
|
||||||
!overlay.commitQueued &&
|
|
||||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
|
||||||
mRuntimeServices)
|
|
||||||
{
|
|
||||||
std::string commitError;
|
|
||||||
JsonValue committedValue = BuildOscCommitValue(*definitionIt, overlay.currentValue);
|
|
||||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, committedValue, overlay.generation, commitError))
|
|
||||||
{
|
|
||||||
overlay.pendingCommitGeneration = overlay.generation;
|
|
||||||
overlay.commitQueued = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
|
||||||
mOscOverlayStates.erase(overlayKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bool hasInputSource = mVideoBackend->HasInputSource();
|
|
||||||
std::vector<RuntimeRenderState> layerStates;
|
|
||||||
mRenderEngine->ResolveRenderLayerStates(
|
|
||||||
mUseCommittedLayerStates.load(),
|
|
||||||
mVideoBackend->InputFrameWidth(),
|
|
||||||
mVideoBackend->InputFrameHeight(),
|
|
||||||
applyOscOverlays,
|
|
||||||
layerStates);
|
|
||||||
const unsigned historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
|
|
||||||
mRenderEngine->RenderLayerStack(
|
|
||||||
hasInputSource,
|
|
||||||
layerStates,
|
|
||||||
mVideoBackend->InputFrameWidth(),
|
|
||||||
mVideoBackend->InputFrameHeight(),
|
|
||||||
mVideoBackend->CaptureTextureWidth(),
|
|
||||||
mVideoBackend->InputPixelFormat(),
|
|
||||||
historyCap);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::ProcessScreenshotRequest()
|
|
||||||
{
|
|
||||||
if (!mScreenshotRequested.exchange(false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const unsigned width = mVideoBackend ? mVideoBackend->OutputFrameWidth() : 0;
|
|
||||||
const unsigned height = mVideoBackend ? mVideoBackend->OutputFrameHeight() : 0;
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<unsigned char> topDownPixels;
|
|
||||||
if (!mRenderEngine->CaptureOutputFrameRgbaTopDown(width, height, topDownPixels))
|
|
||||||
return;
|
|
||||||
|
|
||||||
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 = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty()
|
|
||||||
? mRuntimeStore->GetRuntimeDataRoot()
|
|
||||||
: 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 (!mRuntimeServices)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
|
|
||||||
if (events.failed)
|
|
||||||
{
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimePollFailure(events.error));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (events.registryChanged)
|
|
||||||
broadcastRuntimeState();
|
|
||||||
|
|
||||||
if (!events.reloadRequested)
|
|
||||||
{
|
|
||||||
PreparedShaderBuild readyBuild;
|
|
||||||
if (!mShaderBuildQueue || !mShaderBuildQueue->TryConsumeReadyBuild(readyBuild))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
char compilerErrorMessage[1024] = {};
|
|
||||||
if (!mRenderEngine->ApplyPreparedShaderBuild(
|
|
||||||
readyBuild,
|
|
||||||
mVideoBackend->InputFrameWidth(),
|
|
||||||
mVideoBackend->InputFrameHeight(),
|
|
||||||
mRuntimeCoordinator && mRuntimeCoordinator->PreserveFeedbackOnNextShaderBuild(),
|
|
||||||
sizeof(compilerErrorMessage),
|
|
||||||
compilerErrorMessage))
|
|
||||||
{
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildFailure(compilerErrorMessage));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildSuccess());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimeReloadRequest());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::RequestShaderBuild()
|
|
||||||
{
|
|
||||||
if (!mShaderBuildQueue || !mVideoBackend)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mShaderBuildQueue->RequestBuild(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error)
|
|
||||||
{
|
|
||||||
if (!result.accepted)
|
|
||||||
{
|
|
||||||
if (error)
|
|
||||||
*error = result.errorMessage;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.compileStatusChanged && mRuntimeStore)
|
|
||||||
mRuntimeStore->SetCompileStatus(result.compileStatusSucceeded, result.compileStatusMessage);
|
|
||||||
|
|
||||||
if (result.clearReloadRequest && mRuntimeStore)
|
|
||||||
mRuntimeStore->ClearReloadRequest();
|
|
||||||
|
|
||||||
switch (result.committedStateMode)
|
|
||||||
{
|
|
||||||
case RuntimeCoordinatorCommittedStateMode::UseCommittedStates:
|
|
||||||
mUseCommittedLayerStates = true;
|
|
||||||
break;
|
|
||||||
case RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots:
|
|
||||||
mUseCommittedLayerStates = false;
|
|
||||||
break;
|
|
||||||
case RuntimeCoordinatorCommittedStateMode::Unchanged:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.clearTransientOscState)
|
|
||||||
{
|
|
||||||
mOscOverlayStates.clear();
|
|
||||||
if (mRuntimeServices)
|
|
||||||
mRuntimeServices->ClearOscState();
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyRuntimeCoordinatorRenderReset(result.renderResetScope);
|
|
||||||
|
|
||||||
if (result.shaderBuildRequested)
|
|
||||||
RequestShaderBuild();
|
|
||||||
|
|
||||||
if (result.runtimeStateBroadcastRequired)
|
|
||||||
broadcastRuntimeState();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope)
|
|
||||||
{
|
|
||||||
if (!mRenderEngine)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (resetScope)
|
|
||||||
{
|
|
||||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
|
|
||||||
mRenderEngine->ResetTemporalHistoryState();
|
|
||||||
break;
|
|
||||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
|
|
||||||
mRenderEngine->ResetTemporalHistoryState();
|
|
||||||
mRenderEngine->ResetShaderFeedbackState();
|
|
||||||
break;
|
|
||||||
case RuntimeCoordinatorRenderResetScope::None:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLComposite::broadcastRuntimeState()
|
|
||||||
{
|
|
||||||
if (mRuntimeServices)
|
|
||||||
mRuntimeServices->BroadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::CheckOpenGLExtensions()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
#ifndef __OPENGL_COMPOSITE_H__
|
|
||||||
#define __OPENGL_COMPOSITE_H__
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <process.h>
|
|
||||||
#include <tchar.h>
|
|
||||||
#include <gl/gl.h>
|
|
||||||
#include <gl/glu.h>
|
|
||||||
|
|
||||||
#include <objbase.h>
|
|
||||||
#include <atlbase.h>
|
|
||||||
#include <comutil.h>
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include "RuntimeCoordinator.h"
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
#include "RuntimeStore.h"
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <atomic>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <deque>
|
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
class RenderEngine;
|
|
||||||
class RuntimeServices;
|
|
||||||
class ShaderBuildQueue;
|
|
||||||
class VideoBackend;
|
|
||||||
|
|
||||||
|
|
||||||
class OpenGLComposite
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
|
||||||
~OpenGLComposite();
|
|
||||||
|
|
||||||
bool InitDeckLink();
|
|
||||||
bool InitVideoIO();
|
|
||||||
bool Start();
|
|
||||||
bool Stop();
|
|
||||||
bool ReloadShader(bool preserveFeedbackState = false);
|
|
||||||
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 GetOscBindAddress() const;
|
|
||||||
std::string GetControlUrl() const;
|
|
||||||
std::string GetDocsUrl() const;
|
|
||||||
std::string GetOscAddress() const;
|
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
|
||||||
void paintGL(bool force = false);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void resizeWindow(int width, int height);
|
|
||||||
bool CheckOpenGLExtensions();
|
|
||||||
void PublishVideoIOStatus(const std::string& statusMessage);
|
|
||||||
struct OscOverlayState
|
|
||||||
{
|
|
||||||
std::string layerKey;
|
|
||||||
std::string parameterKey;
|
|
||||||
JsonValue targetValue;
|
|
||||||
ShaderParameterValue currentValue;
|
|
||||||
bool hasCurrentValue = false;
|
|
||||||
std::chrono::steady_clock::time_point lastUpdatedTime;
|
|
||||||
std::chrono::steady_clock::time_point lastAppliedTime;
|
|
||||||
uint64_t generation = 0;
|
|
||||||
uint64_t pendingCommitGeneration = 0;
|
|
||||||
bool commitQueued = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
HWND hGLWnd;
|
|
||||||
HDC hGLDC;
|
|
||||||
HGLRC hGLRC;
|
|
||||||
CRITICAL_SECTION pMutex;
|
|
||||||
|
|
||||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
|
||||||
std::unique_ptr<RuntimeStore> mRuntimeStore;
|
|
||||||
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
|
|
||||||
std::unique_ptr<RuntimeSnapshotProvider> mRuntimeSnapshotProvider;
|
|
||||||
std::unique_ptr<RenderEngine> mRenderEngine;
|
|
||||||
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
|
||||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
|
||||||
std::unique_ptr<VideoBackend> mVideoBackend;
|
|
||||||
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
|
||||||
std::atomic<bool> mUseCommittedLayerStates;
|
|
||||||
std::atomic<bool> mScreenshotRequested;
|
|
||||||
|
|
||||||
bool InitOpenGLState();
|
|
||||||
void renderEffect();
|
|
||||||
bool ProcessRuntimePollResults();
|
|
||||||
void RequestShaderBuild();
|
|
||||||
bool ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error = nullptr);
|
|
||||||
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
|
|
||||||
void ProcessScreenshotRequest();
|
|
||||||
std::filesystem::path BuildScreenshotPath() const;
|
|
||||||
void broadcastRuntimeState();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // __OPENGL_COMPOSITE_H__
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#include "OpenGLComposite.h"
|
|
||||||
#include "RuntimeServices.h"
|
|
||||||
|
|
||||||
std::string OpenGLComposite::GetRuntimeStateJson() const
|
|
||||||
{
|
|
||||||
return mRuntimeStore ? mRuntimeStore->BuildPersistentStateJson() : "{}";
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned short OpenGLComposite::GetControlServerPort() const
|
|
||||||
{
|
|
||||||
return mRuntimeStore ? mRuntimeStore->GetConfiguredControlServerPort() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned short OpenGLComposite::GetOscPort() const
|
|
||||||
{
|
|
||||||
return mRuntimeStore ? mRuntimeStore->GetConfiguredOscPort() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string OpenGLComposite::GetOscBindAddress() const
|
|
||||||
{
|
|
||||||
return mRuntimeStore ? mRuntimeStore->GetConfiguredOscBindAddress() : "127.0.0.1";
|
|
||||||
}
|
|
||||||
|
|
||||||
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://" + GetOscBindAddress() + ":" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->AddLayer(shaderId), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RemoveLayer(layerId), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayer(layerId, direction), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayerToIndex(layerId, targetIndex), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerBypass(layerId, bypassed), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerShader(layerId, shaderId), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameter(layerId, parameterId, parsedValue), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->ResetLayerParameters(layerId), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SaveStackPreset(presetName), &error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeCoordinator &&
|
|
||||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->LoadStackPreset(presetName), &error);
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
#include "RenderEngine.h"
|
|
||||||
|
|
||||||
#include <gl/gl.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
RenderEngine::RenderEngine(
|
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
|
||||||
HealthTelemetry& healthTelemetry,
|
|
||||||
CRITICAL_SECTION& mutex,
|
|
||||||
HDC hdc,
|
|
||||||
HGLRC hglrc,
|
|
||||||
RenderEffectCallback renderEffect,
|
|
||||||
ScreenshotCallback screenshotReady,
|
|
||||||
PreviewPaintCallback previewPaint) :
|
|
||||||
mRenderer(),
|
|
||||||
mRenderPass(mRenderer),
|
|
||||||
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
|
|
||||||
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
|
|
||||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
|
||||||
mMutex(mutex),
|
|
||||||
mHdc(hdc),
|
|
||||||
mHglrc(hglrc)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderEngine::~RenderEngine()
|
|
||||||
{
|
|
||||||
mRenderer.DestroyResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
return mShaderPrograms.CompileDecodeShader(errorMessageSize, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
return mShaderPrograms.CompileOutputPackShader(errorMessageSize, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::InitializeResources(
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned captureTextureWidth,
|
|
||||||
unsigned outputFrameWidth,
|
|
||||||
unsigned outputFrameHeight,
|
|
||||||
unsigned outputPackTextureWidth,
|
|
||||||
std::string& error)
|
|
||||||
{
|
|
||||||
return mRenderer.InitializeResources(
|
|
||||||
inputFrameWidth,
|
|
||||||
inputFrameHeight,
|
|
||||||
captureTextureWidth,
|
|
||||||
outputFrameWidth,
|
|
||||||
outputFrameHeight,
|
|
||||||
outputPackTextureWidth,
|
|
||||||
error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
return mShaderPrograms.CompileLayerPrograms(inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
return mShaderPrograms.CommitPreparedLayerPrograms(preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::ApplyPreparedShaderBuild(
|
|
||||||
const PreparedShaderBuild& preparedBuild,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
bool preserveFeedbackState,
|
|
||||||
int errorMessageSize,
|
|
||||||
char* errorMessage)
|
|
||||||
{
|
|
||||||
if (!CommitPreparedLayerPrograms(preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mCachedLayerRenderStates = mShaderPrograms.CommittedLayerStates();
|
|
||||||
mCachedRenderStateVersion = preparedBuild.renderSnapshot.versions.renderStateVersion;
|
|
||||||
mCachedParameterStateVersion = preparedBuild.renderSnapshot.versions.parameterStateVersion;
|
|
||||||
mCachedRenderStateWidth = preparedBuild.renderSnapshot.outputWidth;
|
|
||||||
mCachedRenderStateHeight = preparedBuild.renderSnapshot.outputHeight;
|
|
||||||
ResetTemporalHistoryState();
|
|
||||||
if (!preserveFeedbackState)
|
|
||||||
ResetShaderFeedbackState();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<RuntimeRenderState>& RenderEngine::CommittedLayerStates() const
|
|
||||||
{
|
|
||||||
return mShaderPrograms.CommittedLayerStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderEngine::ResetTemporalHistoryState()
|
|
||||||
{
|
|
||||||
mShaderPrograms.ResetTemporalHistoryState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderEngine::ResetShaderFeedbackState()
|
|
||||||
{
|
|
||||||
mShaderPrograms.ResetShaderFeedbackState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderEngine::ResizeView(int width, int height)
|
|
||||||
{
|
|
||||||
mRenderer.ResizeView(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight)
|
|
||||||
{
|
|
||||||
if (!force)
|
|
||||||
{
|
|
||||||
if (previewFps == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
const auto minimumInterval = std::chrono::microseconds(1000000 / (previewFps == 0 ? 1u : previewFps));
|
|
||||||
if (mLastPreviewPresentTime != std::chrono::steady_clock::time_point() &&
|
|
||||||
now - mLastPreviewPresentTime < minimumInterval)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryEnterCriticalSection(&mMutex))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
|
|
||||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
|
||||||
{
|
|
||||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
|
||||||
if (!TryEnterCriticalSection(&mMutex))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
|
||||||
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
|
|
||||||
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
|
||||||
if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, videoState.captureTextureWidth, videoState.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
|
||||||
else
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, videoState.captureTextureWidth, videoState.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
|
||||||
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
|
||||||
{
|
|
||||||
EnterCriticalSection(&mMutex);
|
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
|
||||||
const bool rendered = mRenderPipeline.RenderFrame(context, outputFrame);
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return rendered;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::ResolveRenderLayerStates(
|
|
||||||
bool useCommittedLayerStates,
|
|
||||||
unsigned renderWidth,
|
|
||||||
unsigned renderHeight,
|
|
||||||
OverlayApplier overlayApplier,
|
|
||||||
std::vector<RuntimeRenderState>& layerStates)
|
|
||||||
{
|
|
||||||
layerStates.clear();
|
|
||||||
if (useCommittedLayerStates)
|
|
||||||
{
|
|
||||||
layerStates = mShaderPrograms.CommittedLayerStates();
|
|
||||||
if (overlayApplier)
|
|
||||||
overlayApplier(layerStates, false);
|
|
||||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RuntimeSnapshotVersions versions = mRuntimeSnapshotProvider.GetVersions();
|
|
||||||
const bool renderStateCacheValid =
|
|
||||||
!mCachedLayerRenderStates.empty() &&
|
|
||||||
mCachedRenderStateVersion == versions.renderStateVersion &&
|
|
||||||
mCachedRenderStateWidth == renderWidth &&
|
|
||||||
mCachedRenderStateHeight == renderHeight;
|
|
||||||
|
|
||||||
if (renderStateCacheValid)
|
|
||||||
{
|
|
||||||
RuntimeRenderStateSnapshot renderSnapshot;
|
|
||||||
renderSnapshot.outputWidth = renderWidth;
|
|
||||||
renderSnapshot.outputHeight = renderHeight;
|
|
||||||
renderSnapshot.versions.renderStateVersion = mCachedRenderStateVersion;
|
|
||||||
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
|
|
||||||
renderSnapshot.states = mCachedLayerRenderStates;
|
|
||||||
|
|
||||||
if (overlayApplier)
|
|
||||||
overlayApplier(renderSnapshot.states, true);
|
|
||||||
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
|
|
||||||
mRuntimeSnapshotProvider.TryRefreshSnapshotParameters(renderSnapshot))
|
|
||||||
{
|
|
||||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
|
||||||
if (overlayApplier)
|
|
||||||
overlayApplier(renderSnapshot.states, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
mCachedLayerRenderStates = renderSnapshot.states;
|
|
||||||
layerStates = renderSnapshot.states;
|
|
||||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeRenderStateSnapshot renderSnapshot;
|
|
||||||
if (mRuntimeSnapshotProvider.TryGetRenderStateSnapshot(renderWidth, renderHeight, renderSnapshot))
|
|
||||||
{
|
|
||||||
mCachedLayerRenderStates = renderSnapshot.states;
|
|
||||||
mCachedRenderStateVersion = renderSnapshot.versions.renderStateVersion;
|
|
||||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
|
||||||
mCachedRenderStateWidth = renderSnapshot.outputWidth;
|
|
||||||
mCachedRenderStateHeight = renderSnapshot.outputHeight;
|
|
||||||
if (overlayApplier)
|
|
||||||
overlayApplier(mCachedLayerRenderStates, true);
|
|
||||||
layerStates = mCachedLayerRenderStates;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlayApplier)
|
|
||||||
overlayApplier(mCachedLayerRenderStates, true);
|
|
||||||
layerStates = mCachedLayerRenderStates;
|
|
||||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
|
||||||
return !layerStates.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderEngine::RenderLayerStack(
|
|
||||||
bool hasInputSource,
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned captureTextureWidth,
|
|
||||||
VideoIOPixelFormat inputPixelFormat,
|
|
||||||
unsigned historyCap)
|
|
||||||
{
|
|
||||||
mRenderPass.Render(
|
|
||||||
hasInputSource,
|
|
||||||
layerStates,
|
|
||||||
inputFrameWidth,
|
|
||||||
inputFrameHeight,
|
|
||||||
captureTextureWidth,
|
|
||||||
inputPixelFormat,
|
|
||||||
historyCap,
|
|
||||||
[this](const RuntimeRenderState& state, OpenGLRenderer::LayerProgram::TextBinding& textBinding, std::string& error) {
|
|
||||||
return mShaderPrograms.UpdateTextBindingTexture(state, textBinding, error);
|
|
||||||
},
|
|
||||||
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable) {
|
|
||||||
return mShaderPrograms.UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::ReadOutputFrameRgba(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
|
|
||||||
{
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
EnterCriticalSection(&mMutex);
|
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
|
||||||
|
|
||||||
bottomUpPixels.resize(static_cast<std::size_t>(width) * height * 4);
|
|
||||||
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);
|
|
||||||
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderEngine::CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
|
|
||||||
{
|
|
||||||
std::vector<unsigned char> bottomUpPixels;
|
|
||||||
if (!ReadOutputFrameRgba(width, height, bottomUpPixels))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
topDownPixels.resize(bottomUpPixels.size());
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderPass.h"
|
|
||||||
#include "OpenGLRenderPipeline.h"
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "OpenGLShaderPrograms.h"
|
|
||||||
#include "HealthTelemetry.h"
|
|
||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <chrono>
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class RenderEngine
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using RenderEffectCallback = std::function<void()>;
|
|
||||||
using ScreenshotCallback = std::function<void()>;
|
|
||||||
using PreviewPaintCallback = std::function<void()>;
|
|
||||||
using OverlayApplier = std::function<void(std::vector<RuntimeRenderState>& states, bool allowCommit)>;
|
|
||||||
|
|
||||||
RenderEngine(
|
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
|
||||||
HealthTelemetry& healthTelemetry,
|
|
||||||
CRITICAL_SECTION& mutex,
|
|
||||||
HDC hdc,
|
|
||||||
HGLRC hglrc,
|
|
||||||
RenderEffectCallback renderEffect,
|
|
||||||
ScreenshotCallback screenshotReady,
|
|
||||||
PreviewPaintCallback previewPaint);
|
|
||||||
~RenderEngine();
|
|
||||||
|
|
||||||
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
|
||||||
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
|
||||||
bool InitializeResources(
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned captureTextureWidth,
|
|
||||||
unsigned outputFrameWidth,
|
|
||||||
unsigned outputFrameHeight,
|
|
||||||
unsigned outputPackTextureWidth,
|
|
||||||
std::string& error);
|
|
||||||
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
|
||||||
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
|
||||||
bool ApplyPreparedShaderBuild(
|
|
||||||
const PreparedShaderBuild& preparedBuild,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
bool preserveFeedbackState,
|
|
||||||
int errorMessageSize,
|
|
||||||
char* errorMessage);
|
|
||||||
|
|
||||||
const std::vector<RuntimeRenderState>& CommittedLayerStates() const;
|
|
||||||
void ResetTemporalHistoryState();
|
|
||||||
void ResetShaderFeedbackState();
|
|
||||||
void ResizeView(int width, int height);
|
|
||||||
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
|
||||||
bool TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
|
||||||
bool RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
|
||||||
bool ResolveRenderLayerStates(
|
|
||||||
bool useCommittedLayerStates,
|
|
||||||
unsigned renderWidth,
|
|
||||||
unsigned renderHeight,
|
|
||||||
OverlayApplier overlayApplier,
|
|
||||||
std::vector<RuntimeRenderState>& layerStates);
|
|
||||||
void RenderLayerStack(
|
|
||||||
bool hasInputSource,
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned captureTextureWidth,
|
|
||||||
VideoIOPixelFormat inputPixelFormat,
|
|
||||||
unsigned historyCap);
|
|
||||||
bool ReadOutputFrameRgba(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels);
|
|
||||||
bool CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
|
|
||||||
|
|
||||||
private:
|
|
||||||
OpenGLRenderer mRenderer;
|
|
||||||
OpenGLRenderPass mRenderPass;
|
|
||||||
OpenGLRenderPipeline mRenderPipeline;
|
|
||||||
OpenGLShaderPrograms mShaderPrograms;
|
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
|
||||||
CRITICAL_SECTION& mMutex;
|
|
||||||
HDC mHdc;
|
|
||||||
HGLRC mHglrc;
|
|
||||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
|
||||||
uint64_t mCachedRenderStateVersion = 0;
|
|
||||||
uint64_t mCachedParameterStateVersion = 0;
|
|
||||||
unsigned mCachedRenderStateWidth = 0;
|
|
||||||
unsigned mCachedRenderStateHeight = 0;
|
|
||||||
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
|
||||||
};
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
#include "OpenGLRenderPass.h"
|
|
||||||
|
|
||||||
#include "GlRenderConstants.h"
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
|
|
||||||
mRenderer(renderer)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPass::Render(
|
|
||||||
bool hasInputSource,
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned captureTextureWidth,
|
|
||||||
VideoIOPixelFormat inputPixelFormat,
|
|
||||||
unsigned historyCap,
|
|
||||||
const TextBindingUpdater& updateTextBinding,
|
|
||||||
const GlobalParamsUpdater& updateGlobalParams)
|
|
||||||
{
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_BLEND);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
if (hasInputSource)
|
|
||||||
{
|
|
||||||
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
|
||||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
|
|
||||||
if (layerStates.empty() || layerPrograms.empty())
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
|
||||||
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const std::vector<RenderPassDescriptor>& passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
|
|
||||||
for (const RenderPassDescriptor& pass : passes)
|
|
||||||
{
|
|
||||||
RenderLayerPass(
|
|
||||||
pass,
|
|
||||||
inputFrameWidth,
|
|
||||||
inputFrameHeight,
|
|
||||||
historyCap,
|
|
||||||
updateTextBinding,
|
|
||||||
updateGlobalParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
|
|
||||||
mRenderer.FeedbackBuffers().FinalizeFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
|
||||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
|
||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
|
||||||
glUseProgram(mRenderer.DecodeProgram());
|
|
||||||
|
|
||||||
const GLint packedResolutionLocation = mRenderer.DecodePackedResolutionLocation();
|
|
||||||
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
|
|
||||||
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
|
|
||||||
if (packedResolutionLocation >= 0)
|
|
||||||
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
|
|
||||||
if (decodedResolutionLocation >= 0)
|
|
||||||
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
|
|
||||||
if (inputPixelFormatLocation >= 0)
|
|
||||||
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
|
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates,
|
|
||||||
std::vector<LayerProgram>& layerPrograms) const
|
|
||||||
{
|
|
||||||
// Flatten the layer stack into concrete GL passes. A layer may now contain
|
|
||||||
// several shader passes, but the outer stack still sees one visible output
|
|
||||||
// per layer.
|
|
||||||
std::vector<RenderPassDescriptor>& passes = mPassScratch;
|
|
||||||
passes.clear();
|
|
||||||
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
|
|
||||||
std::size_t descriptorCount = 0;
|
|
||||||
for (std::size_t index = 0; index < passCount; ++index)
|
|
||||||
descriptorCount += layerPrograms[index].passes.size();
|
|
||||||
passes.reserve(descriptorCount);
|
|
||||||
|
|
||||||
GLuint sourceTexture = mRenderer.DecodedTexture();
|
|
||||||
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
|
|
||||||
for (std::size_t index = 0; index < passCount; ++index)
|
|
||||||
{
|
|
||||||
const RuntimeRenderState& state = layerStates[index];
|
|
||||||
LayerProgram& layerProgram = layerPrograms[index];
|
|
||||||
if (layerProgram.passes.empty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Preserve the original two-target layer ping-pong. Intermediate passes
|
|
||||||
// inside this layer are routed through pooled temporary targets instead.
|
|
||||||
const std::size_t remaining = layerStates.size() - index;
|
|
||||||
const bool writeToMain = (remaining % 2) == 1;
|
|
||||||
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
|
|
||||||
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
|
|
||||||
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
|
|
||||||
|
|
||||||
const GLuint layerInputTexture = sourceTexture;
|
|
||||||
const GLuint layerInputFramebuffer = sourceFramebuffer;
|
|
||||||
GLuint previousPassTexture = layerInputTexture;
|
|
||||||
GLuint previousPassFramebuffer = layerInputFramebuffer;
|
|
||||||
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
|
|
||||||
std::size_t temporaryTargetIndex = 0;
|
|
||||||
|
|
||||||
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
|
|
||||||
{
|
|
||||||
PassProgram& passProgram = layerProgram.passes[passIndex];
|
|
||||||
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
|
|
||||||
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
|
|
||||||
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
|
|
||||||
|
|
||||||
GLuint passSourceTexture = previousPassTexture;
|
|
||||||
GLuint passSourceFramebuffer = previousPassFramebuffer;
|
|
||||||
if (!passProgram.inputNames.empty())
|
|
||||||
{
|
|
||||||
// v1 multipass uses the first declared input as gVideoInput.
|
|
||||||
// Later inputs are parsed for forward compatibility.
|
|
||||||
const std::string& inputName = passProgram.inputNames.front();
|
|
||||||
if (inputName == "layerInput")
|
|
||||||
{
|
|
||||||
passSourceTexture = layerInputTexture;
|
|
||||||
passSourceFramebuffer = layerInputFramebuffer;
|
|
||||||
}
|
|
||||||
else if (inputName == "previousPass")
|
|
||||||
{
|
|
||||||
passSourceTexture = previousPassTexture;
|
|
||||||
passSourceFramebuffer = previousPassFramebuffer;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auto namedOutputIt = namedOutputs.find(inputName);
|
|
||||||
if (namedOutputIt != namedOutputs.end())
|
|
||||||
{
|
|
||||||
passSourceTexture = namedOutputIt->second.first;
|
|
||||||
passSourceFramebuffer = namedOutputIt->second.second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint passDestinationTexture = layerOutputTexture;
|
|
||||||
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
|
|
||||||
RenderPassOutputTarget outputTarget = layerOutputTarget;
|
|
||||||
if (!writesLayerOutput)
|
|
||||||
{
|
|
||||||
// Temporary targets are reserved when the shader stack is
|
|
||||||
// committed, avoiding texture allocation during playback.
|
|
||||||
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
|
|
||||||
{
|
|
||||||
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
|
|
||||||
++temporaryTargetIndex;
|
|
||||||
passDestinationTexture = temporaryTarget.texture;
|
|
||||||
passDestinationFramebuffer = temporaryTarget.framebuffer;
|
|
||||||
outputTarget = RenderPassOutputTarget::Temporary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderPassDescriptor pass;
|
|
||||||
pass.kind = RenderPassKind::LayerEffect;
|
|
||||||
pass.outputTarget = outputTarget;
|
|
||||||
pass.passIndex = passes.size();
|
|
||||||
pass.passId = passProgram.passId;
|
|
||||||
pass.layerId = state.layerId;
|
|
||||||
pass.shaderId = state.shaderId;
|
|
||||||
pass.layerInputTexture = layerInputTexture;
|
|
||||||
pass.sourceTexture = passSourceTexture;
|
|
||||||
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
|
|
||||||
pass.destinationTexture = passDestinationTexture;
|
|
||||||
pass.destinationFramebuffer = passDestinationFramebuffer;
|
|
||||||
pass.layerProgram = &layerProgram;
|
|
||||||
pass.passProgram = &passProgram;
|
|
||||||
pass.layerState = &state;
|
|
||||||
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
|
||||||
pass.captureFeedbackWrite = state.feedback.enabled && passProgram.passId == state.feedback.writePassId;
|
|
||||||
passes.push_back(pass);
|
|
||||||
|
|
||||||
// A later pass can reference either the explicit output name or the
|
|
||||||
// pass id, which keeps small manifests pleasant to write.
|
|
||||||
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
|
||||||
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
|
||||||
previousPassTexture = passDestinationTexture;
|
|
||||||
previousPassFramebuffer = passDestinationFramebuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceTexture = layerOutputTexture;
|
|
||||||
sourceFramebuffer = layerOutputFramebuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return passes;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPass::RenderLayerPass(
|
|
||||||
const RenderPassDescriptor& pass,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned historyCap,
|
|
||||||
const TextBindingUpdater& updateTextBinding,
|
|
||||||
const GlobalParamsUpdater& updateGlobalParams)
|
|
||||||
{
|
|
||||||
if (pass.passProgram == nullptr || pass.layerState == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
RenderShaderProgram(
|
|
||||||
pass.layerInputTexture,
|
|
||||||
pass.sourceTexture,
|
|
||||||
pass.destinationFramebuffer,
|
|
||||||
*pass.passProgram,
|
|
||||||
*pass.layerState,
|
|
||||||
inputFrameWidth,
|
|
||||||
inputFrameHeight,
|
|
||||||
historyCap,
|
|
||||||
updateTextBinding,
|
|
||||||
updateGlobalParams);
|
|
||||||
|
|
||||||
if (pass.capturePreLayerHistory)
|
|
||||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
|
||||||
if (pass.captureFeedbackWrite)
|
|
||||||
mRenderer.FeedbackBuffers().CaptureFeedbackFramebuffer(pass.layerId, pass.destinationFramebuffer, inputFrameWidth, inputFrameHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPass::RenderShaderProgram(
|
|
||||||
GLuint layerInputTexture,
|
|
||||||
GLuint sourceTexture,
|
|
||||||
GLuint destinationFrameBuffer,
|
|
||||||
PassProgram& passProgram,
|
|
||||||
const RuntimeRenderState& state,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned historyCap,
|
|
||||||
const TextBindingUpdater& updateTextBinding,
|
|
||||||
const GlobalParamsUpdater& updateGlobalParams)
|
|
||||||
{
|
|
||||||
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
|
|
||||||
{
|
|
||||||
std::string textError;
|
|
||||||
if (!updateTextBinding(state, textBinding, textError))
|
|
||||||
OutputDebugStringA((textError + "\n").c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
|
||||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
||||||
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
|
|
||||||
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
|
||||||
const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state);
|
|
||||||
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
|
||||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, layerInputTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures);
|
|
||||||
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
|
||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
|
||||||
glUseProgram(passProgram.program);
|
|
||||||
// The UBO is shared by every pass in a layer; texture routing is what
|
|
||||||
// changes from pass to pass.
|
|
||||||
updateGlobalParams(
|
|
||||||
state,
|
|
||||||
mRenderer.TemporalHistory().SourceAvailableCount(),
|
|
||||||
mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId),
|
|
||||||
mRenderer.FeedbackBuffers().FeedbackAvailable(state));
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "RenderPassDescriptor.h"
|
|
||||||
#include "ShaderTextureBindings.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
#include "VideoIOFormat.h"
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class OpenGLRenderPass
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
|
||||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
|
||||||
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
|
||||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned, bool)>;
|
|
||||||
|
|
||||||
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);
|
|
||||||
std::vector<RenderPassDescriptor> BuildLayerPassDescriptors(
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates,
|
|
||||||
std::vector<LayerProgram>& layerPrograms) const;
|
|
||||||
void RenderLayerPass(
|
|
||||||
const RenderPassDescriptor& pass,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned historyCap,
|
|
||||||
const TextBindingUpdater& updateTextBinding,
|
|
||||||
const GlobalParamsUpdater& updateGlobalParams);
|
|
||||||
void RenderShaderProgram(
|
|
||||||
GLuint layerInputTexture,
|
|
||||||
GLuint sourceTexture,
|
|
||||||
GLuint destinationFrameBuffer,
|
|
||||||
PassProgram& passProgram,
|
|
||||||
const RuntimeRenderState& state,
|
|
||||||
unsigned inputFrameWidth,
|
|
||||||
unsigned inputFrameHeight,
|
|
||||||
unsigned historyCap,
|
|
||||||
const TextBindingUpdater& updateTextBinding,
|
|
||||||
const GlobalParamsUpdater& updateGlobalParams);
|
|
||||||
|
|
||||||
OpenGLRenderer& mRenderer;
|
|
||||||
ShaderTextureBindings mTextureBindings;
|
|
||||||
mutable std::vector<RenderPassDescriptor> mPassScratch;
|
|
||||||
};
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
#include "OpenGLRenderPipeline.h"
|
|
||||||
|
|
||||||
#include "HealthTelemetry.h"
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
#include "VideoIOFormat.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <gl/gl.h>
|
|
||||||
|
|
||||||
OpenGLRenderPipeline::OpenGLRenderPipeline(
|
|
||||||
OpenGLRenderer& renderer,
|
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
|
||||||
HealthTelemetry& healthTelemetry,
|
|
||||||
RenderEffectCallback renderEffect,
|
|
||||||
OutputReadyCallback outputReady,
|
|
||||||
PaintCallback paint) :
|
|
||||||
mRenderer(renderer),
|
|
||||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
|
||||||
mHealthTelemetry(healthTelemetry),
|
|
||||||
mRenderEffect(renderEffect),
|
|
||||||
mOutputReady(outputReady),
|
|
||||||
mPaint(paint)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLRenderPipeline::~OpenGLRenderPipeline()
|
|
||||||
{
|
|
||||||
ResetAsyncReadbackState();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
|
||||||
{
|
|
||||||
const VideoIOState& state = context.videoState;
|
|
||||||
|
|
||||||
const auto renderStartTime = std::chrono::steady_clock::now();
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
|
||||||
mRenderEffect();
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
|
||||||
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
|
||||||
if (mOutputReady)
|
|
||||||
mOutputReady();
|
|
||||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
|
||||||
PackOutputFor10Bit(state);
|
|
||||||
glFlush();
|
|
||||||
|
|
||||||
const auto renderEndTime = std::chrono::steady_clock::now();
|
|
||||||
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
|
||||||
mHealthTelemetry.TryRecordPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
|
|
||||||
mRuntimeSnapshotProvider.TryAdvanceFrame();
|
|
||||||
|
|
||||||
ReadOutputFrame(state, outputFrame);
|
|
||||||
if (mPaint)
|
|
||||||
mPaint();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
|
||||||
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_BLEND);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
|
|
||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
|
||||||
glUseProgram(mRenderer.OutputPackProgram());
|
|
||||||
|
|
||||||
const GLint outputResolutionLocation = mRenderer.OutputPackResolutionLocation();
|
|
||||||
const GLint activeWordsLocation = mRenderer.OutputPackActiveWordsLocation();
|
|
||||||
const GLint packFormatLocation = mRenderer.OutputPackFormatLocation();
|
|
||||||
if (outputResolutionLocation >= 0)
|
|
||||||
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
|
||||||
if (activeWordsLocation >= 0)
|
|
||||||
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
|
|
||||||
if (packFormatLocation >= 0)
|
|
||||||
glUniform1i(packFormatLocation, state.outputPixelFormat == VideoIOPixelFormat::Yuva10 ? 2 : 1);
|
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
|
|
||||||
{
|
|
||||||
if (requiredBytes == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
ResetAsyncReadbackState();
|
|
||||||
mAsyncReadbackBytes = requiredBytes;
|
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
|
||||||
{
|
|
||||||
glGenBuffers(1, &slot.pixelPackBuffer);
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
|
||||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
|
||||||
slot.sizeBytes = requiredBytes;
|
|
||||||
slot.inFlight = false;
|
|
||||||
}
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
mAsyncReadbackWriteIndex = 0;
|
|
||||||
mAsyncReadbackReadIndex = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ResetAsyncReadbackState()
|
|
||||||
{
|
|
||||||
FlushAsyncReadbackPipeline();
|
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
|
||||||
slot.sizeBytes = 0;
|
|
||||||
|
|
||||||
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
|
||||||
{
|
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
|
||||||
{
|
|
||||||
if (slot.pixelPackBuffer != 0)
|
|
||||||
{
|
|
||||||
glDeleteBuffers(1, &slot.pixelPackBuffer);
|
|
||||||
slot.pixelPackBuffer = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mAsyncReadbackWriteIndex = 0;
|
|
||||||
mAsyncReadbackReadIndex = 0;
|
|
||||||
mAsyncReadbackBytes = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
|
||||||
{
|
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
|
||||||
{
|
|
||||||
if (slot.fence != nullptr)
|
|
||||||
{
|
|
||||||
glDeleteSync(slot.fence);
|
|
||||||
slot.fence = nullptr;
|
|
||||||
}
|
|
||||||
slot.inFlight = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mAsyncReadbackWriteIndex = 0;
|
|
||||||
mAsyncReadbackReadIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
|
|
||||||
{
|
|
||||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
|
||||||
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
|
|
||||||
const GLenum format = usePackedOutput ? GL_RGBA : GL_BGRA;
|
|
||||||
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
|
|
||||||
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
|
|
||||||
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
|
|
||||||
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
|
|
||||||
|
|
||||||
if (requiredBytes == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (mAsyncReadbackBytes != requiredBytes
|
|
||||||
|| mAsyncReadbackFormat != format
|
|
||||||
|| mAsyncReadbackType != type
|
|
||||||
|| mAsyncReadbackFramebuffer != framebuffer)
|
|
||||||
{
|
|
||||||
mAsyncReadbackFormat = format;
|
|
||||||
mAsyncReadbackType = type;
|
|
||||||
mAsyncReadbackFramebuffer = framebuffer;
|
|
||||||
if (!EnsureAsyncReadbackBuffers(requiredBytes))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
|
|
||||||
if (slot.fence != nullptr)
|
|
||||||
{
|
|
||||||
glDeleteSync(slot.fence);
|
|
||||||
slot.fence = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
|
||||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
|
||||||
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
|
|
||||||
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
slot.inFlight = slot.fence != nullptr;
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
|
|
||||||
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
|
|
||||||
{
|
|
||||||
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackReadIndex];
|
|
||||||
if (!slot.inFlight || slot.fence == nullptr || slot.pixelPackBuffer == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
|
|
||||||
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
|
|
||||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
glDeleteSync(slot.fence);
|
|
||||||
slot.fence = nullptr;
|
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
|
||||||
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
|
||||||
if (mappedBytes == nullptr)
|
|
||||||
{
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
slot.inFlight = false;
|
|
||||||
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
|
|
||||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
|
||||||
|
|
||||||
slot.inFlight = false;
|
|
||||||
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
|
||||||
CacheOutputFrame(outputFrame);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFrame)
|
|
||||||
{
|
|
||||||
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
|
|
||||||
mCachedOutputFrame.resize(byteCount);
|
|
||||||
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
|
|
||||||
{
|
|
||||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
|
||||||
|
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
|
||||||
if (usePackedOutput)
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
|
||||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
|
||||||
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
|
||||||
{
|
|
||||||
if (TryConsumeAsyncReadback(outputFrame, 500000))
|
|
||||||
{
|
|
||||||
QueueAsyncReadback(state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If async readback misses the playout deadline, prefer a fresh synchronous
|
|
||||||
// frame over reusing stale cached output, then restart the async pipeline.
|
|
||||||
if (outputFrame.bytes != nullptr)
|
|
||||||
{
|
|
||||||
ReadOutputFrameSynchronously(state, outputFrame.bytes);
|
|
||||||
CacheOutputFrame(outputFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
FlushAsyncReadbackPipeline();
|
|
||||||
QueueAsyncReadback(state);
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <functional>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class OpenGLRenderer;
|
|
||||||
class HealthTelemetry;
|
|
||||||
class RuntimeSnapshotProvider;
|
|
||||||
|
|
||||||
struct RenderPipelineFrameContext
|
|
||||||
{
|
|
||||||
VideoIOState videoState;
|
|
||||||
VideoIOCompletion completion;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OpenGLRenderPipeline
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using RenderEffectCallback = std::function<void()>;
|
|
||||||
using OutputReadyCallback = std::function<void()>;
|
|
||||||
using PaintCallback = std::function<void()>;
|
|
||||||
|
|
||||||
OpenGLRenderPipeline(
|
|
||||||
OpenGLRenderer& renderer,
|
|
||||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
|
||||||
HealthTelemetry& healthTelemetry,
|
|
||||||
RenderEffectCallback renderEffect,
|
|
||||||
OutputReadyCallback outputReady,
|
|
||||||
PaintCallback paint);
|
|
||||||
~OpenGLRenderPipeline();
|
|
||||||
|
|
||||||
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct AsyncReadbackSlot
|
|
||||||
{
|
|
||||||
GLuint pixelPackBuffer = 0;
|
|
||||||
GLsync fence = nullptr;
|
|
||||||
std::size_t sizeBytes = 0;
|
|
||||||
bool inFlight = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
|
|
||||||
void ResetAsyncReadbackState();
|
|
||||||
void FlushAsyncReadbackPipeline();
|
|
||||||
void QueueAsyncReadback(const VideoIOState& state);
|
|
||||||
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
|
|
||||||
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
|
|
||||||
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
|
|
||||||
void PackOutputFor10Bit(const VideoIOState& state);
|
|
||||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
|
||||||
|
|
||||||
OpenGLRenderer& mRenderer;
|
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
|
||||||
HealthTelemetry& mHealthTelemetry;
|
|
||||||
RenderEffectCallback mRenderEffect;
|
|
||||||
OutputReadyCallback mOutputReady;
|
|
||||||
PaintCallback mPaint;
|
|
||||||
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
|
|
||||||
std::size_t mAsyncReadbackWriteIndex = 0;
|
|
||||||
std::size_t mAsyncReadbackReadIndex = 0;
|
|
||||||
std::size_t mAsyncReadbackBytes = 0;
|
|
||||||
GLenum mAsyncReadbackFormat = GL_BGRA;
|
|
||||||
GLenum mAsyncReadbackType = GL_UNSIGNED_INT_8_8_8_8_REV;
|
|
||||||
GLuint mAsyncReadbackFramebuffer = 0;
|
|
||||||
std::vector<unsigned char> mCachedOutputFrame;
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#include "OpenGLVideoIOBridge.h"
|
|
||||||
|
|
||||||
#include "RenderEngine.h"
|
|
||||||
|
|
||||||
OpenGLVideoIOBridge::OpenGLVideoIOBridge(RenderEngine& renderEngine) :
|
|
||||||
mRenderEngine(renderEngine)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLVideoIOBridge::UploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& state)
|
|
||||||
{
|
|
||||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
|
||||||
return; // don't transfer texture when there's no input
|
|
||||||
|
|
||||||
mRenderEngine.TryUploadInputFrame(inputFrame, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLVideoIOBridge::RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame)
|
|
||||||
{
|
|
||||||
RenderPipelineFrameContext frameContext;
|
|
||||||
frameContext.videoState = state;
|
|
||||||
frameContext.completion = completion;
|
|
||||||
|
|
||||||
mRenderEngine.RenderOutputFrame(frameContext, outputFrame);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderPipeline.h"
|
|
||||||
|
|
||||||
class RenderEngine;
|
|
||||||
|
|
||||||
class OpenGLVideoIOBridge
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit OpenGLVideoIOBridge(RenderEngine& renderEngine);
|
|
||||||
|
|
||||||
void UploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& state);
|
|
||||||
void RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame);
|
|
||||||
|
|
||||||
private:
|
|
||||||
RenderEngine& mRenderEngine;
|
|
||||||
};
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
#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();
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
void WritePngFileAsync(
|
|
||||||
const std::filesystem::path& outputPath,
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
std::vector<unsigned char> rgbaPixels);
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <gl/gl.h>
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class RenderPassKind
|
|
||||||
{
|
|
||||||
LayerEffect
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class RenderPassOutputTarget
|
|
||||||
{
|
|
||||||
Temporary,
|
|
||||||
LayerTemp,
|
|
||||||
Composite
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderPassDescriptor
|
|
||||||
{
|
|
||||||
RenderPassKind kind = RenderPassKind::LayerEffect;
|
|
||||||
RenderPassOutputTarget outputTarget = RenderPassOutputTarget::Composite;
|
|
||||||
std::size_t passIndex = 0;
|
|
||||||
std::string passId;
|
|
||||||
std::string layerId;
|
|
||||||
std::string shaderId;
|
|
||||||
GLuint layerInputTexture = 0;
|
|
||||||
GLuint sourceTexture = 0;
|
|
||||||
GLuint sourceFramebuffer = 0;
|
|
||||||
GLuint destinationTexture = 0;
|
|
||||||
GLuint destinationFramebuffer = 0;
|
|
||||||
OpenGLRenderer::LayerProgram* layerProgram = nullptr;
|
|
||||||
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
|
||||||
const RuntimeRenderState* layerState = nullptr;
|
|
||||||
bool capturePreLayerHistory = false;
|
|
||||||
bool captureFeedbackWrite = false;
|
|
||||||
};
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
#include "ShaderFeedbackBuffers.h"
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void ConfigureFeedbackTexture(unsigned frameWidth, unsigned frameHeight)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderFeedbackBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
|
||||||
{
|
|
||||||
if (!EnsureZeroTexture())
|
|
||||||
{
|
|
||||||
error = "Failed to initialize shader feedback fallback texture.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> requiredLayerIds;
|
|
||||||
for (const RuntimeRenderState& state : layerStates)
|
|
||||||
{
|
|
||||||
if (!state.feedback.enabled)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
requiredLayerIds.insert(state.layerId);
|
|
||||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
|
||||||
if (surfaceIt == mSurfacesByLayerId.end() ||
|
|
||||||
surfaceIt->second.width != frameWidth ||
|
|
||||||
surfaceIt->second.height != frameHeight)
|
|
||||||
{
|
|
||||||
Surface replacement;
|
|
||||||
if (!CreateSurface(replacement, frameWidth, frameHeight, error))
|
|
||||||
return false;
|
|
||||||
mSurfacesByLayerId[state.layerId] = std::move(replacement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = mSurfacesByLayerId.begin(); it != mSurfacesByLayerId.end();)
|
|
||||||
{
|
|
||||||
if (requiredLayerIds.find(it->first) == requiredLayerIds.end())
|
|
||||||
{
|
|
||||||
DestroySurface(it->second);
|
|
||||||
it = mSurfacesByLayerId.erase(it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::DestroyResources()
|
|
||||||
{
|
|
||||||
for (auto& entry : mSurfacesByLayerId)
|
|
||||||
DestroySurface(entry.second);
|
|
||||||
mSurfacesByLayerId.clear();
|
|
||||||
|
|
||||||
if (mZeroTexture != 0)
|
|
||||||
{
|
|
||||||
glDeleteTextures(1, &mZeroTexture);
|
|
||||||
mZeroTexture = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::ResetState()
|
|
||||||
{
|
|
||||||
for (auto& entry : mSurfacesByLayerId)
|
|
||||||
ClearSurfaceState(entry.second);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint ShaderFeedbackBuffers::ResolveReadTexture(const RuntimeRenderState& state) const
|
|
||||||
{
|
|
||||||
if (!state.feedback.enabled)
|
|
||||||
return mZeroTexture;
|
|
||||||
|
|
||||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
|
||||||
if (surfaceIt == mSurfacesByLayerId.end() || !surfaceIt->second.hasData)
|
|
||||||
return mZeroTexture;
|
|
||||||
|
|
||||||
return surfaceIt->second.slots[surfaceIt->second.readIndex].texture != 0
|
|
||||||
? surfaceIt->second.slots[surfaceIt->second.readIndex].texture
|
|
||||||
: mZeroTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderFeedbackBuffers::FeedbackAvailable(const RuntimeRenderState& state) const
|
|
||||||
{
|
|
||||||
if (!state.feedback.enabled)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
|
||||||
return surfaceIt != mSurfacesByLayerId.end() && surfaceIt->second.hasData;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
|
||||||
{
|
|
||||||
auto surfaceIt = mSurfacesByLayerId.find(layerId);
|
|
||||||
if (surfaceIt == mSurfacesByLayerId.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Surface& surface = surfaceIt->second;
|
|
||||||
const unsigned writeIndex = 1u - surface.readIndex;
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, surface.slots[writeIndex].framebuffer);
|
|
||||||
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
|
||||||
surface.pendingWrite = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::FinalizeFrame()
|
|
||||||
{
|
|
||||||
for (auto& entry : mSurfacesByLayerId)
|
|
||||||
{
|
|
||||||
Surface& surface = entry.second;
|
|
||||||
if (!surface.pendingWrite)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
surface.readIndex = 1u - surface.readIndex;
|
|
||||||
surface.hasData = true;
|
|
||||||
surface.pendingWrite = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderFeedbackBuffers::EnsureZeroTexture()
|
|
||||||
{
|
|
||||||
if (mZeroTexture != 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
glGenTextures(1, &mZeroTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, mZeroTexture);
|
|
||||||
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);
|
|
||||||
const float zeroPixel[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA, GL_FLOAT, zeroPixel);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
return mZeroTexture != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderFeedbackBuffers::CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
|
||||||
{
|
|
||||||
DestroySurface(surface);
|
|
||||||
|
|
||||||
surface.width = frameWidth;
|
|
||||||
surface.height = frameHeight;
|
|
||||||
for (Slot& slot : surface.slots)
|
|
||||||
{
|
|
||||||
glGenTextures(1, &slot.texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
|
||||||
ConfigureFeedbackTexture(frameWidth, frameHeight);
|
|
||||||
|
|
||||||
glGenFramebuffers(1, &slot.framebuffer);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
{
|
|
||||||
error = "Failed to initialize a shader feedback framebuffer.";
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
DestroySurface(surface);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
ClearSurfaceState(surface);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::DestroySurface(Surface& surface)
|
|
||||||
{
|
|
||||||
for (Slot& slot : surface.slots)
|
|
||||||
{
|
|
||||||
if (slot.framebuffer != 0)
|
|
||||||
glDeleteFramebuffers(1, &slot.framebuffer);
|
|
||||||
if (slot.texture != 0)
|
|
||||||
glDeleteTextures(1, &slot.texture);
|
|
||||||
slot.framebuffer = 0;
|
|
||||||
slot.texture = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.width = 0;
|
|
||||||
surface.height = 0;
|
|
||||||
surface.readIndex = 0;
|
|
||||||
surface.hasData = false;
|
|
||||||
surface.pendingWrite = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderFeedbackBuffers::ClearSurfaceState(Surface& surface)
|
|
||||||
{
|
|
||||||
surface.readIndex = 0;
|
|
||||||
surface.hasData = false;
|
|
||||||
surface.pendingWrite = false;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class ShaderFeedbackBuffers
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
struct Slot
|
|
||||||
{
|
|
||||||
GLuint texture = 0;
|
|
||||||
GLuint framebuffer = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Surface
|
|
||||||
{
|
|
||||||
Slot slots[2];
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
unsigned readIndex = 0;
|
|
||||||
bool hasData = false;
|
|
||||||
bool pendingWrite = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
|
||||||
void DestroyResources();
|
|
||||||
void ResetState();
|
|
||||||
GLuint ResolveReadTexture(const RuntimeRenderState& state) const;
|
|
||||||
bool FeedbackAvailable(const RuntimeRenderState& state) const;
|
|
||||||
void CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
|
||||||
void FinalizeFrame();
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool EnsureZeroTexture();
|
|
||||||
bool CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
|
||||||
void DestroySurface(Surface& surface);
|
|
||||||
void ClearSurfaceState(Surface& surface);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::map<std::string, Surface> mSurfacesByLayerId;
|
|
||||||
GLuint mZeroTexture = 0;
|
|
||||||
};
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
#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 feedbackTextureCount = state.feedback.enabled ? 1u : 0u;
|
|
||||||
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + feedbackTextureCount + 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const
|
|
||||||
{
|
|
||||||
std::vector<GLuint> textures;
|
|
||||||
textures.reserve(historyCap);
|
|
||||||
for (unsigned index = 0; index < historyCap; ++index)
|
|
||||||
textures.push_back(ResolveTexture(sourceHistoryRing, fallbackTexture, index));
|
|
||||||
return textures;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const
|
|
||||||
{
|
|
||||||
const Ring* temporalRing = nullptr;
|
|
||||||
auto it = preLayerHistoryByLayerId.find(state.layerId);
|
|
||||||
if (it != preLayerHistoryByLayerId.end())
|
|
||||||
temporalRing = &it->second;
|
|
||||||
|
|
||||||
std::vector<GLuint> textures;
|
|
||||||
textures.reserve(historyCap);
|
|
||||||
for (unsigned index = 0; index < historyCap; ++index)
|
|
||||||
textures.push_back(temporalRing ? ResolveTexture(*temporalRing, fallbackTexture, index) : fallbackTexture);
|
|
||||||
return textures;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
|
|
||||||
{
|
|
||||||
if (ring.filledCount == 0 || ring.slots.empty())
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#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);
|
|
||||||
std::vector<GLuint> ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const;
|
|
||||||
std::vector<GLuint> ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const;
|
|
||||||
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
|
||||||
unsigned SourceAvailableCount() const;
|
|
||||||
unsigned AvailableCountForLayer(const std::string& layerId) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Ring sourceHistoryRing;
|
|
||||||
std::map<std::string, Ring> preLayerHistoryByLayerId;
|
|
||||||
bool mNeedsReset = true;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <gl/gl.h>
|
|
||||||
|
|
||||||
constexpr GLuint kLayerInputTextureUnit = 0;
|
|
||||||
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
|
||||||
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
|
||||||
constexpr GLuint kPackedVideoTextureUnit = 2;
|
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
|
||||||
constexpr unsigned kPrerollFrameCount = 12;
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#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;
|
|
||||||
};
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
glGenRenderbuffers(1, &mIdColorBuf);
|
|
||||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
|
||||||
glGenVertexArrays(1, &mFullscreenVAO);
|
|
||||||
glGenBuffers(1, &mGlobalParamsUBO);
|
|
||||||
|
|
||||||
if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
|
|
||||||
return false;
|
|
||||||
if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
|
|
||||||
return false;
|
|
||||||
if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
|
||||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
|
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
{
|
|
||||||
error = "Cannot initialize framebuffer.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
|
|
||||||
return false;
|
|
||||||
if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
|
||||||
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);
|
|
||||||
|
|
||||||
mResourcesInitialized = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
|
||||||
{
|
|
||||||
mDecodeProgram = program;
|
|
||||||
mDecodeVertexShader = vertexShader;
|
|
||||||
mDecodeFragmentShader = fragmentShader;
|
|
||||||
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
|
|
||||||
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
|
|
||||||
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
|
||||||
{
|
|
||||||
mOutputPackProgram = program;
|
|
||||||
mOutputPackVertexShader = vertexShader;
|
|
||||||
mOutputPackFragmentShader = fragmentShader;
|
|
||||||
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
|
|
||||||
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
|
|
||||||
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
|
|
||||||
{
|
|
||||||
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::ResizeView(int width, int height)
|
|
||||||
{
|
|
||||||
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, OutputFramebuffer());
|
|
||||||
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 (mIdColorBuf != 0)
|
|
||||||
glDeleteRenderbuffers(1, &mIdColorBuf);
|
|
||||||
if (mIdDepthBuf != 0)
|
|
||||||
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
|
||||||
if (mCaptureTexture != 0)
|
|
||||||
glDeleteTextures(1, &mCaptureTexture);
|
|
||||||
if (mTextureUploadBuffer != 0)
|
|
||||||
glDeleteBuffers(1, &mTextureUploadBuffer);
|
|
||||||
mRenderTargets.Destroy();
|
|
||||||
|
|
||||||
mFullscreenVAO = 0;
|
|
||||||
mGlobalParamsUBO = 0;
|
|
||||||
mIdColorBuf = 0;
|
|
||||||
mIdDepthBuf = 0;
|
|
||||||
mCaptureTexture = 0;
|
|
||||||
mTextureUploadBuffer = 0;
|
|
||||||
mGlobalParamsUBOSize = 0;
|
|
||||||
mResourcesInitialized = false;
|
|
||||||
|
|
||||||
mTemporalHistory.DestroyResources();
|
|
||||||
mFeedbackBuffers.DestroyResources();
|
|
||||||
DestroyLayerPrograms();
|
|
||||||
DestroyDecodeShaderProgram();
|
|
||||||
DestroyOutputPackShaderProgram();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
|
||||||
{
|
|
||||||
for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
|
|
||||||
{
|
|
||||||
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
|
|
||||||
{
|
|
||||||
if (binding.texture != 0)
|
|
||||||
{
|
|
||||||
glDeleteTextures(1, &binding.texture);
|
|
||||||
binding.texture = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
passProgram.textureBindings.clear();
|
|
||||||
|
|
||||||
for (LayerProgram::TextBinding& binding : passProgram.textBindings)
|
|
||||||
{
|
|
||||||
if (binding.texture != 0)
|
|
||||||
{
|
|
||||||
glDeleteTextures(1, &binding.texture);
|
|
||||||
binding.texture = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
passProgram.textBindings.clear();
|
|
||||||
|
|
||||||
if (passProgram.program != 0)
|
|
||||||
{
|
|
||||||
glDeleteProgram(passProgram.program);
|
|
||||||
passProgram.program = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passProgram.fragmentShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(passProgram.fragmentShader);
|
|
||||||
passProgram.fragmentShader = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passProgram.vertexShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(passProgram.vertexShader);
|
|
||||||
passProgram.vertexShader = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layerProgram.passes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::DestroyLayerPrograms()
|
|
||||||
{
|
|
||||||
for (LayerProgram& layerProgram : mLayerPrograms)
|
|
||||||
DestroySingleLayerProgram(layerProgram);
|
|
||||||
mLayerPrograms.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::DestroyDecodeShaderProgram()
|
|
||||||
{
|
|
||||||
if (mDecodeProgram != 0)
|
|
||||||
{
|
|
||||||
glDeleteProgram(mDecodeProgram);
|
|
||||||
mDecodeProgram = 0;
|
|
||||||
}
|
|
||||||
mDecodePackedResolutionLocation = -1;
|
|
||||||
mDecodeDecodedResolutionLocation = -1;
|
|
||||||
mDecodeInputPixelFormatLocation = -1;
|
|
||||||
|
|
||||||
if (mDecodeFragmentShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(mDecodeFragmentShader);
|
|
||||||
mDecodeFragmentShader = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDecodeVertexShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(mDecodeVertexShader);
|
|
||||||
mDecodeVertexShader = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLRenderer::DestroyOutputPackShaderProgram()
|
|
||||||
{
|
|
||||||
if (mOutputPackProgram != 0)
|
|
||||||
{
|
|
||||||
glDeleteProgram(mOutputPackProgram);
|
|
||||||
mOutputPackProgram = 0;
|
|
||||||
}
|
|
||||||
mOutputPackResolutionLocation = -1;
|
|
||||||
mOutputPackActiveWordsLocation = -1;
|
|
||||||
mOutputPackFormatLocation = -1;
|
|
||||||
|
|
||||||
if (mOutputPackFragmentShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(mOutputPackFragmentShader);
|
|
||||||
mOutputPackFragmentShader = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mOutputPackVertexShader != 0)
|
|
||||||
{
|
|
||||||
glDeleteShader(mOutputPackVertexShader);
|
|
||||||
mOutputPackVertexShader = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
#include "RenderTargetPool.h"
|
|
||||||
#include "ShaderFeedbackBuffers.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;
|
|
||||||
|
|
||||||
struct PassProgram
|
|
||||||
{
|
|
||||||
std::string passId;
|
|
||||||
std::vector<std::string> inputNames;
|
|
||||||
std::string outputName;
|
|
||||||
GLuint shaderTextureBase = 0;
|
|
||||||
GLuint program = 0;
|
|
||||||
GLuint vertexShader = 0;
|
|
||||||
GLuint fragmentShader = 0;
|
|
||||||
std::vector<TextureBinding> textureBindings;
|
|
||||||
std::vector<TextBinding> textBindings;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<PassProgram> passes;
|
|
||||||
};
|
|
||||||
|
|
||||||
GLuint CaptureTexture() const { return mCaptureTexture; }
|
|
||||||
GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
|
|
||||||
GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
|
|
||||||
GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
|
|
||||||
GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
|
|
||||||
GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
|
|
||||||
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
|
|
||||||
GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
|
|
||||||
GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
|
|
||||||
GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
|
|
||||||
GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
|
|
||||||
GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
|
|
||||||
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
|
|
||||||
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
|
||||||
GLuint DecodeProgram() const { return mDecodeProgram; }
|
|
||||||
GLuint OutputPackProgram() const { return mOutputPackProgram; }
|
|
||||||
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
|
|
||||||
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
|
|
||||||
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
|
|
||||||
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
|
|
||||||
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
|
|
||||||
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
|
||||||
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
|
||||||
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
|
||||||
bool ResourcesInitialized() const { return mResourcesInitialized; }
|
|
||||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
|
||||||
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
|
||||||
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
|
||||||
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
|
|
||||||
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
|
|
||||||
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
|
||||||
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
|
||||||
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
|
||||||
ShaderFeedbackBuffers& FeedbackBuffers() { return mFeedbackBuffers; }
|
|
||||||
const ShaderFeedbackBuffers& FeedbackBuffers() const { return mFeedbackBuffers; }
|
|
||||||
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 mTextureUploadBuffer = 0;
|
|
||||||
GLuint mIdColorBuf = 0;
|
|
||||||
GLuint mIdDepthBuf = 0;
|
|
||||||
GLuint mFullscreenVAO = 0;
|
|
||||||
GLuint mGlobalParamsUBO = 0;
|
|
||||||
GLuint mDecodeProgram = 0;
|
|
||||||
GLuint mDecodeVertexShader = 0;
|
|
||||||
GLuint mDecodeFragmentShader = 0;
|
|
||||||
GLint mDecodePackedResolutionLocation = -1;
|
|
||||||
GLint mDecodeDecodedResolutionLocation = -1;
|
|
||||||
GLint mDecodeInputPixelFormatLocation = -1;
|
|
||||||
GLuint mOutputPackProgram = 0;
|
|
||||||
GLuint mOutputPackVertexShader = 0;
|
|
||||||
GLuint mOutputPackFragmentShader = 0;
|
|
||||||
GLint mOutputPackResolutionLocation = -1;
|
|
||||||
GLint mOutputPackActiveWordsLocation = -1;
|
|
||||||
GLint mOutputPackFormatLocation = -1;
|
|
||||||
GLsizeiptr mGlobalParamsUBOSize = 0;
|
|
||||||
bool mResourcesInitialized = false;
|
|
||||||
int mViewWidth = 0;
|
|
||||||
int mViewHeight = 0;
|
|
||||||
std::vector<LayerProgram> mLayerPrograms;
|
|
||||||
RenderTargetPool mRenderTargets;
|
|
||||||
TemporalHistoryBuffers mTemporalHistory;
|
|
||||||
ShaderFeedbackBuffers mFeedbackBuffers;
|
|
||||||
};
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#include "RenderTargetPool.h"
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void ConfigureRenderTargetTexture(
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
GLenum internalFormat,
|
|
||||||
GLenum pixelFormat,
|
|
||||||
GLenum pixelType)
|
|
||||||
{
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, pixelFormat, pixelType, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderTargetPool::Create(
|
|
||||||
RenderTargetId id,
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
GLenum internalFormat,
|
|
||||||
GLenum pixelFormat,
|
|
||||||
GLenum pixelType,
|
|
||||||
const char* errorPrefix,
|
|
||||||
std::string& error)
|
|
||||||
{
|
|
||||||
RenderTarget& target = mTargets[TargetIndex(id)];
|
|
||||||
if (target.texture != 0 || target.framebuffer != 0)
|
|
||||||
{
|
|
||||||
error = std::string(errorPrefix) + " render target was already initialized.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
glGenTextures(1, &target.texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
|
||||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
|
|
||||||
glGenFramebuffers(1, &target.framebuffer);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
{
|
|
||||||
error = std::string("Cannot initialize ") + errorPrefix + " framebuffer.";
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
target.width = width;
|
|
||||||
target.height = height;
|
|
||||||
target.internalFormat = internalFormat;
|
|
||||||
target.pixelFormat = pixelFormat;
|
|
||||||
target.pixelType = pixelType;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RenderTargetPool::ReserveTemporaryTargets(
|
|
||||||
std::size_t count,
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
GLenum internalFormat,
|
|
||||||
GLenum pixelFormat,
|
|
||||||
GLenum pixelType,
|
|
||||||
std::string& error)
|
|
||||||
{
|
|
||||||
if (mTemporaryTargets.size() == count)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
DestroyTemporaryTargets();
|
|
||||||
|
|
||||||
mTemporaryTargets.resize(count);
|
|
||||||
for (std::size_t index = 0; index < mTemporaryTargets.size(); ++index)
|
|
||||||
{
|
|
||||||
RenderTarget& target = mTemporaryTargets[index];
|
|
||||||
glGenTextures(1, &target.texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
|
||||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
|
|
||||||
glGenFramebuffers(1, &target.framebuffer);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
|
||||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
{
|
|
||||||
error = "Cannot initialize temporary render target.";
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
target.width = width;
|
|
||||||
target.height = height;
|
|
||||||
target.internalFormat = internalFormat;
|
|
||||||
target.pixelFormat = pixelFormat;
|
|
||||||
target.pixelType = pixelType;
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderTargetPool::DestroyTemporaryTargets()
|
|
||||||
{
|
|
||||||
for (RenderTarget& target : mTemporaryTargets)
|
|
||||||
{
|
|
||||||
if (target.framebuffer != 0)
|
|
||||||
glDeleteFramebuffers(1, &target.framebuffer);
|
|
||||||
if (target.texture != 0)
|
|
||||||
glDeleteTextures(1, &target.texture);
|
|
||||||
}
|
|
||||||
mTemporaryTargets.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderTargetPool::Destroy()
|
|
||||||
{
|
|
||||||
for (RenderTarget& target : mTargets)
|
|
||||||
{
|
|
||||||
if (target.framebuffer != 0)
|
|
||||||
glDeleteFramebuffers(1, &target.framebuffer);
|
|
||||||
if (target.texture != 0)
|
|
||||||
glDeleteTextures(1, &target.texture);
|
|
||||||
target = RenderTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
DestroyTemporaryTargets();
|
|
||||||
}
|
|
||||||
|
|
||||||
const RenderTarget& RenderTargetPool::Target(RenderTargetId id) const
|
|
||||||
{
|
|
||||||
return mTargets[TargetIndex(id)];
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
enum class RenderTargetId
|
|
||||||
{
|
|
||||||
Decoded,
|
|
||||||
LayerTemp,
|
|
||||||
Composite,
|
|
||||||
Output,
|
|
||||||
OutputPack,
|
|
||||||
Count
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderTarget
|
|
||||||
{
|
|
||||||
GLuint texture = 0;
|
|
||||||
GLuint framebuffer = 0;
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
GLenum internalFormat = GL_RGBA8;
|
|
||||||
GLenum pixelFormat = GL_RGBA;
|
|
||||||
GLenum pixelType = GL_UNSIGNED_BYTE;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RenderTargetPool
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
bool Create(
|
|
||||||
RenderTargetId id,
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
GLenum internalFormat,
|
|
||||||
GLenum pixelFormat,
|
|
||||||
GLenum pixelType,
|
|
||||||
const char* errorPrefix,
|
|
||||||
std::string& error);
|
|
||||||
bool ReserveTemporaryTargets(
|
|
||||||
std::size_t count,
|
|
||||||
unsigned width,
|
|
||||||
unsigned height,
|
|
||||||
GLenum internalFormat,
|
|
||||||
GLenum pixelFormat,
|
|
||||||
GLenum pixelType,
|
|
||||||
std::string& error);
|
|
||||||
void DestroyTemporaryTargets();
|
|
||||||
void Destroy();
|
|
||||||
|
|
||||||
GLuint Texture(RenderTargetId id) const { return Target(id).texture; }
|
|
||||||
GLuint Framebuffer(RenderTargetId id) const { return Target(id).framebuffer; }
|
|
||||||
const RenderTarget& Target(RenderTargetId id) const;
|
|
||||||
const RenderTarget& TemporaryTarget(std::size_t index) const { return mTemporaryTargets[index]; }
|
|
||||||
std::size_t TemporaryTargetCount() const { return mTemporaryTargets.size(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
static std::size_t TargetIndex(RenderTargetId id) { return static_cast<std::size_t>(id); }
|
|
||||||
|
|
||||||
std::array<RenderTarget, static_cast<std::size_t>(RenderTargetId::Count)> mTargets;
|
|
||||||
std::vector<RenderTarget> mTemporaryTargets;
|
|
||||||
};
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#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"
|
|
||||||
"uniform int uOutputPackFormat;\n"
|
|
||||||
"in vec2 vTexCoord;\n"
|
|
||||||
"layout(location = 0) out vec4 fragColor;\n"
|
|
||||||
"vec4 rgbaAt(int x, int y)\n"
|
|
||||||
"{\n"
|
|
||||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
|
||||||
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0), vec4(0.0), vec4(1.0));\n"
|
|
||||||
"}\n"
|
|
||||||
"vec3 rgbAt(int x, int y)\n"
|
|
||||||
"{\n"
|
|
||||||
" return rgbaAt(x, y).rgb;\n"
|
|
||||||
"}\n"
|
|
||||||
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
|
|
||||||
"{\n"
|
|
||||||
" 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"
|
|
||||||
"vec4 bigEndianWordToBytes(uint word)\n"
|
|
||||||
"{\n"
|
|
||||||
" return vec4(float((word >> 24) & 255u), float((word >> 16) & 255u), float((word >> 8) & 255u), float(word & 255u)) / 255.0;\n"
|
|
||||||
"}\n"
|
|
||||||
"vec4 packAy10Word(ivec2 outCoord)\n"
|
|
||||||
"{\n"
|
|
||||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
|
||||||
" if (outCoord.x >= size.x)\n"
|
|
||||||
" return vec4(0.0);\n"
|
|
||||||
" int pixelBase = (outCoord.x / 2) * 2;\n"
|
|
||||||
" int y = outCoord.y;\n"
|
|
||||||
" vec4 rgba0 = rgbaAt(pixelBase + 0, y);\n"
|
|
||||||
" vec4 rgba1 = rgbaAt(pixelBase + 1, y);\n"
|
|
||||||
" vec3 c0 = rgbToLegalYcbcr10(rgba0.rgb);\n"
|
|
||||||
" vec3 c1 = rgbToLegalYcbcr10(rgba1.rgb);\n"
|
|
||||||
" float chroma = (outCoord.x & 1) == 0 ? round((c0.y + c1.y) * 0.5) : round((c0.z + c1.z) * 0.5);\n"
|
|
||||||
" float alpha = round(clamp(((outCoord.x & 1) == 0 ? rgba0.a : rgba1.a), 0.0, 1.0) * 1023.0);\n"
|
|
||||||
" float luma = (outCoord.x & 1) == 0 ? c0.x : c1.x;\n"
|
|
||||||
" uint word = ((uint(luma) & 1023u) << 22) | ((uint(chroma) & 1023u) << 12) | ((uint(alpha) & 1023u) << 2);\n"
|
|
||||||
" return bigEndianWordToBytes(word);\n"
|
|
||||||
"}\n"
|
|
||||||
"void main()\n"
|
|
||||||
"{\n"
|
|
||||||
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
|
|
||||||
" if (uOutputPackFormat == 2)\n"
|
|
||||||
" {\n"
|
|
||||||
" fragColor = packAy10Word(outCoord);\n"
|
|
||||||
" return;\n"
|
|
||||||
" }\n"
|
|
||||||
" if (float(outCoord.x) >= uActiveV210Words)\n"
|
|
||||||
" {\n"
|
|
||||||
" fragColor = vec4(0.0);\n"
|
|
||||||
" 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";
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
extern const char* kFullscreenTriangleVertexShaderSource;
|
|
||||||
extern const char* kDecodeFragmentShaderSource;
|
|
||||||
extern const char* kOutputPackFragmentShaderSource;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
#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, bool feedbackAvailable)
|
|
||||||
{
|
|
||||||
std::vector<unsigned char>& buffer = mScratchBuffer;
|
|
||||||
buffer.clear();
|
|
||||||
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));
|
|
||||||
AppendStd140Int(buffer, feedbackAvailable ? 1 : 0);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class GlobalParamsBuffer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
|
||||||
|
|
||||||
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
|
||||||
|
|
||||||
private:
|
|
||||||
OpenGLRenderer& mRenderer;
|
|
||||||
std::vector<unsigned char> mScratchBuffer;
|
|
||||||
};
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::LayerProgram>& layerPrograms)
|
|
||||||
{
|
|
||||||
// Only one layer renders at a time, so the pool needs to cover the widest
|
|
||||||
// layer, not the sum of every intermediate pass in the stack.
|
|
||||||
std::size_t requiredTargets = 0;
|
|
||||||
for (const OpenGLRenderer::LayerProgram& layerProgram : layerPrograms)
|
|
||||||
{
|
|
||||||
const std::size_t internalPasses = layerProgram.passes.size() > 0 ? layerProgram.passes.size() - 1 : 0;
|
|
||||||
if (internalPasses > requiredTargets)
|
|
||||||
requiredTargets = internalPasses;
|
|
||||||
}
|
|
||||||
return requiredTargets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider) :
|
|
||||||
mRenderer(renderer),
|
|
||||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
|
||||||
mGlobalParamsBuffer(renderer),
|
|
||||||
mCompiler(renderer, runtimeSnapshotProvider, mTextureBindings)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
const RuntimeRenderStateSnapshot renderSnapshot =
|
|
||||||
mRuntimeSnapshotProvider.GetRenderStateSnapshot(inputFrameWidth, inputFrameHeight);
|
|
||||||
const std::vector<RuntimeRenderState>& layerStates = renderSnapshot.states;
|
|
||||||
std::string temporalError;
|
|
||||||
const unsigned historyCap = mRuntimeSnapshotProvider.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;
|
|
||||||
}
|
|
||||||
if (mRenderer.ResourcesInitialized() &&
|
|
||||||
!mRenderer.FeedbackBuffers().EnsureResources(layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
|
||||||
{
|
|
||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial startup still compiles synchronously; auto-reload uses the build
|
|
||||||
// queue so Slang/file work stays off the playback path.
|
|
||||||
std::vector<LayerProgram> newPrograms;
|
|
||||||
newPrograms.reserve(layerStates.size());
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string targetError;
|
|
||||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
|
||||||
{
|
|
||||||
for (LayerProgram& program : newPrograms)
|
|
||||||
DestroySingleLayerProgram(program);
|
|
||||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DestroyLayerPrograms();
|
|
||||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
|
||||||
mCommittedLayerStates = renderSnapshot.states;
|
|
||||||
|
|
||||||
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 = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
|
|
||||||
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.renderSnapshot.states, historyCap, temporalError))
|
|
||||||
{
|
|
||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.renderSnapshot.states, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
|
|
||||||
{
|
|
||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (mRenderer.ResourcesInitialized() &&
|
|
||||||
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.renderSnapshot.states, inputFrameWidth, inputFrameHeight, temporalError))
|
|
||||||
{
|
|
||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The prepared build already contains GLSL text for each pass. This commit
|
|
||||||
// step performs the short GL work on the render thread.
|
|
||||||
std::vector<LayerProgram> newPrograms;
|
|
||||||
newPrograms.reserve(preparedBuild.layers.size());
|
|
||||||
|
|
||||||
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
|
|
||||||
{
|
|
||||||
LayerProgram layerProgram;
|
|
||||||
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.passes, layerProgram, errorMessageSize, errorMessage))
|
|
||||||
{
|
|
||||||
for (LayerProgram& program : newPrograms)
|
|
||||||
DestroySingleLayerProgram(program);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
newPrograms.push_back(layerProgram);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string targetError;
|
|
||||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
|
||||||
{
|
|
||||||
for (LayerProgram& program : newPrograms)
|
|
||||||
DestroySingleLayerProgram(program);
|
|
||||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DestroyLayerPrograms();
|
|
||||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
|
||||||
mCommittedLayerStates = preparedBuild.renderSnapshot.states;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLShaderPrograms::ResetShaderFeedbackState()
|
|
||||||
{
|
|
||||||
mRenderer.FeedbackBuffers().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, bool feedbackAvailable)
|
|
||||||
{
|
|
||||||
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "GlobalParamsBuffer.h"
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "RuntimeSnapshotProvider.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, RuntimeSnapshotProvider& runtimeSnapshotProvider);
|
|
||||||
|
|
||||||
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();
|
|
||||||
void ResetShaderFeedbackState();
|
|
||||||
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, bool feedbackAvailable);
|
|
||||||
|
|
||||||
private:
|
|
||||||
OpenGLRenderer& mRenderer;
|
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
|
||||||
ShaderTextureBindings mTextureBindings;
|
|
||||||
GlobalParamsBuffer mGlobalParamsBuffer;
|
|
||||||
ShaderProgramCompiler mCompiler;
|
|
||||||
std::vector<RuntimeRenderState> mCommittedLayerStates;
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
#include "ShaderBuildQueue.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderBuildQueue::ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider) :
|
|
||||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
|
||||||
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.renderSnapshot = mRuntimeSnapshotProvider.GetRenderStateSnapshot(outputWidth, outputHeight);
|
|
||||||
build.layers.reserve(build.renderSnapshot.states.size());
|
|
||||||
|
|
||||||
for (const RuntimeRenderState& state : build.renderSnapshot.states)
|
|
||||||
{
|
|
||||||
PreparedLayerShader layer;
|
|
||||||
layer.state = state;
|
|
||||||
if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, 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;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct PreparedLayerShader
|
|
||||||
{
|
|
||||||
RuntimeRenderState state;
|
|
||||||
std::vector<ShaderPassBuildSource> passes;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PreparedShaderBuild
|
|
||||||
{
|
|
||||||
uint64_t generation = 0;
|
|
||||||
bool succeeded = false;
|
|
||||||
std::string message;
|
|
||||||
RuntimeRenderStateSnapshot renderSnapshot;
|
|
||||||
std::vector<PreparedLayerShader> layers;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ShaderBuildQueue
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider);
|
|
||||||
~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);
|
|
||||||
|
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#include "ShaderProgramCompiler.h"
|
|
||||||
|
|
||||||
#include "GlRenderConstants.h"
|
|
||||||
#include "GlScopedObjects.h"
|
|
||||||
#include "GlShaderSources.h"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
if (!errorMessage || errorMessageSize <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider, ShaderTextureBindings& textureBindings) :
|
|
||||||
mRenderer(renderer),
|
|
||||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
|
||||||
mTextureBindings(textureBindings)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
std::vector<ShaderPassBuildSource> passSources;
|
|
||||||
std::string loadError;
|
|
||||||
|
|
||||||
if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
|
|
||||||
{
|
|
||||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
GLsizei errorBufferSize = 0;
|
|
||||||
std::string loadError;
|
|
||||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
|
||||||
|
|
||||||
layerProgram.layerId = state.layerId;
|
|
||||||
layerProgram.shaderId = state.shaderId;
|
|
||||||
layerProgram.passes.clear();
|
|
||||||
|
|
||||||
for (const auto& passSource : passSources)
|
|
||||||
{
|
|
||||||
GLint compileResult = GL_FALSE;
|
|
||||||
GLint linkResult = GL_FALSE;
|
|
||||||
const char* fragmentSource = passSource.fragmentShaderSource.c_str();
|
|
||||||
|
|
||||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
|
||||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
|
||||||
glCompileShader(newVertexShader.get());
|
|
||||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
|
||||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
|
||||||
glCompileShader(newFragmentShader.get());
|
|
||||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlProgram newProgram(glCreateProgram());
|
|
||||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
|
||||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
|
||||||
glLinkProgram(newProgram.get());
|
|
||||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
|
||||||
if (linkResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<LayerProgram::TextureBinding> textureBindings;
|
|
||||||
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
|
|
||||||
{
|
|
||||||
LayerProgram::TextureBinding textureBinding;
|
|
||||||
textureBinding.samplerName = textureAsset.id;
|
|
||||||
textureBinding.sourcePath = textureAsset.path;
|
|
||||||
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
|
|
||||||
{
|
|
||||||
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
|
|
||||||
{
|
|
||||||
if (loadedTexture.texture != 0)
|
|
||||||
glDeleteTextures(1, &loadedTexture.texture);
|
|
||||||
}
|
|
||||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
|
||||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
textureBindings.push_back(textureBinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<LayerProgram::TextBinding> textBindings;
|
|
||||||
mTextureBindings.CreateTextBindings(state, textBindings);
|
|
||||||
|
|
||||||
PassProgram passProgram;
|
|
||||||
passProgram.passId = passSource.passId;
|
|
||||||
passProgram.inputNames = passSource.inputNames;
|
|
||||||
passProgram.outputName = passSource.outputName;
|
|
||||||
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames());
|
|
||||||
passProgram.textureBindings.swap(textureBindings);
|
|
||||||
passProgram.textBindings.swap(textBindings);
|
|
||||||
|
|
||||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
|
|
||||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
|
||||||
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
|
|
||||||
|
|
||||||
const unsigned historyCap = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
|
|
||||||
glUseProgram(newProgram.get());
|
|
||||||
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
|
|
||||||
glUseProgram(0);
|
|
||||||
|
|
||||||
passProgram.program = newProgram.release();
|
|
||||||
passProgram.vertexShader = newVertexShader.release();
|
|
||||||
passProgram.fragmentShader = newFragmentShader.release();
|
|
||||||
layerProgram.passes.push_back(std::move(passProgram));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
GLsizei errorBufferSize = 0;
|
|
||||||
GLint compileResult = GL_FALSE;
|
|
||||||
GLint linkResult = GL_FALSE;
|
|
||||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
|
||||||
const char* fragmentSource = kDecodeFragmentShaderSource;
|
|
||||||
|
|
||||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
|
||||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
|
||||||
glCompileShader(newVertexShader.get());
|
|
||||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
|
||||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
|
||||||
glCompileShader(newFragmentShader.get());
|
|
||||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlProgram newProgram(glCreateProgram());
|
|
||||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
|
||||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
|
||||||
glLinkProgram(newProgram.get());
|
|
||||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
|
||||||
if (linkResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mRenderer.DestroyDecodeShaderProgram();
|
|
||||||
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
|
||||||
{
|
|
||||||
GLsizei errorBufferSize = 0;
|
|
||||||
GLint compileResult = GL_FALSE;
|
|
||||||
GLint linkResult = GL_FALSE;
|
|
||||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
|
||||||
const char* fragmentSource = kOutputPackFragmentShaderSource;
|
|
||||||
|
|
||||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
|
||||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
|
||||||
glCompileShader(newVertexShader.get());
|
|
||||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
|
||||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
|
||||||
glCompileShader(newFragmentShader.get());
|
|
||||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
|
||||||
if (compileResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedGlProgram newProgram(glCreateProgram());
|
|
||||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
|
||||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
|
||||||
glLinkProgram(newProgram.get());
|
|
||||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
|
||||||
if (linkResult == GL_FALSE)
|
|
||||||
{
|
|
||||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
glUseProgram(newProgram.get());
|
|
||||||
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
|
|
||||||
if (outputSamplerLocation >= 0)
|
|
||||||
glUniform1i(outputSamplerLocation, 0);
|
|
||||||
glUseProgram(0);
|
|
||||||
|
|
||||||
mRenderer.DestroyOutputPackShaderProgram();
|
|
||||||
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
#include "ShaderTextureBindings.h"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class ShaderProgramCompiler
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
|
||||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
|
||||||
|
|
||||||
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider, ShaderTextureBindings& textureBindings);
|
|
||||||
|
|
||||||
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
|
||||||
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
|
||||||
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
|
||||||
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
|
||||||
|
|
||||||
private:
|
|
||||||
OpenGLRenderer& mRenderer;
|
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
|
||||||
ShaderTextureBindings& mTextureBindings;
|
|
||||||
};
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
#include "ShaderTextureBindings.h"
|
|
||||||
|
|
||||||
#include "GlRenderConstants.h"
|
|
||||||
#include "TextRasterizer.h"
|
|
||||||
#include "TextureAssetLoader.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
|
|
||||||
{
|
|
||||||
auto valueIt = state.parameterValues.find(parameterId);
|
|
||||||
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
|
|
||||||
{
|
|
||||||
if (!definition.fontId.empty())
|
|
||||||
{
|
|
||||||
for (const ShaderFontAsset& fontAsset : state.fontAssets)
|
|
||||||
{
|
|
||||||
if (fontAsset.id == definition.fontId)
|
|
||||||
return &fontAsset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
|
||||||
{
|
|
||||||
return ::LoadTextureAsset(textureAsset, textureId, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
|
|
||||||
{
|
|
||||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
|
||||||
{
|
|
||||||
if (definition.type != ShaderParameterType::Text)
|
|
||||||
continue;
|
|
||||||
LayerProgram::TextBinding textBinding;
|
|
||||||
textBinding.parameterId = definition.id;
|
|
||||||
textBinding.samplerName = definition.id + "Texture";
|
|
||||||
textBinding.fontId = definition.fontId;
|
|
||||||
glGenTextures(1, &textBinding.texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
textBindings.push_back(textBinding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
|
||||||
{
|
|
||||||
const std::string text = TextValueForBinding(state, textBinding.parameterId);
|
|
||||||
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
|
|
||||||
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
|
|
||||||
if (definitionIt == state.parameterDefinitions.end())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
|
|
||||||
std::filesystem::path fontPath;
|
|
||||||
if (fontAsset)
|
|
||||||
fontPath = fontAsset->path;
|
|
||||||
|
|
||||||
std::vector<unsigned char> sdf;
|
|
||||||
if (!RasterizeTextSdf(text, fontPath, sdf, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
GLint previousActiveTexture = 0;
|
|
||||||
GLint previousUnpackBuffer = 0;
|
|
||||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
|
|
||||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
|
|
||||||
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
|
|
||||||
|
|
||||||
textBinding.renderedText = text;
|
|
||||||
textBinding.renderedWidth = kTextTextureWidth;
|
|
||||||
textBinding.renderedHeight = kTextTextureHeight;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
|
|
||||||
{
|
|
||||||
GLint location = glGetUniformLocation(program, samplerName.c_str());
|
|
||||||
if (location >= 0)
|
|
||||||
return location;
|
|
||||||
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint ShaderTextureBindings::ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const
|
|
||||||
{
|
|
||||||
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
|
||||||
{
|
|
||||||
return ResolveFeedbackTextureUnit(state, historyCap) + (state.feedback.enabled ? 1u : 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
|
||||||
{
|
|
||||||
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
|
||||||
|
|
||||||
const GLint layerInputLocation = FindSamplerUniformLocation(program, "gLayerInput");
|
|
||||||
if (layerInputLocation >= 0)
|
|
||||||
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
|
||||||
|
|
||||||
const GLint videoInputLocation = FindSamplerUniformLocation(program, "gVideoInput");
|
|
||||||
if (videoInputLocation >= 0)
|
|
||||||
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
|
||||||
|
|
||||||
for (unsigned index = 0; index < historyCap; ++index)
|
|
||||||
{
|
|
||||||
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
|
|
||||||
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
|
|
||||||
if (sourceSamplerLocation >= 0)
|
|
||||||
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
|
|
||||||
|
|
||||||
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
|
|
||||||
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
|
|
||||||
if (temporalSamplerLocation >= 0)
|
|
||||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.feedback.enabled)
|
|
||||||
{
|
|
||||||
const GLint feedbackSamplerLocation = FindSamplerUniformLocation(program, "gFeedbackState");
|
|
||||||
if (feedbackSamplerLocation >= 0)
|
|
||||||
glUniform1i(feedbackSamplerLocation, static_cast<GLint>(ResolveFeedbackTextureUnit(state, historyCap)));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
|
||||||
{
|
|
||||||
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
|
||||||
if (textureSamplerLocation >= 0)
|
|
||||||
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
|
||||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
|
||||||
{
|
|
||||||
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
|
|
||||||
if (textSamplerLocation >= 0)
|
|
||||||
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
|
||||||
const PassProgram& passProgram,
|
|
||||||
GLuint layerInputTexture,
|
|
||||||
GLuint originalLayerInputTexture,
|
|
||||||
const RuntimeRenderState& state,
|
|
||||||
GLuint feedbackTexture,
|
|
||||||
const std::vector<GLuint>& sourceHistoryTextures,
|
|
||||||
const std::vector<GLuint>& temporalHistoryTextures) const
|
|
||||||
{
|
|
||||||
RuntimeTextureBindingPlan plan;
|
|
||||||
plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit });
|
|
||||||
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
|
|
||||||
|
|
||||||
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
|
|
||||||
{
|
|
||||||
plan.bindings.push_back({
|
|
||||||
"sourceHistory",
|
|
||||||
"gSourceHistory" + std::to_string(index),
|
|
||||||
sourceHistoryTextures[index],
|
|
||||||
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
|
|
||||||
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
|
|
||||||
{
|
|
||||||
plan.bindings.push_back({
|
|
||||||
"temporalHistory",
|
|
||||||
"gTemporalHistory" + std::to_string(index),
|
|
||||||
temporalHistoryTextures[index],
|
|
||||||
temporalBase + static_cast<GLuint>(index)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint feedbackTextureUnit = ResolveFeedbackTextureUnit(state, static_cast<unsigned>(sourceHistoryTextures.size()));
|
|
||||||
if (state.feedback.enabled)
|
|
||||||
{
|
|
||||||
plan.bindings.push_back({
|
|
||||||
"feedbackState",
|
|
||||||
"gFeedbackState",
|
|
||||||
feedbackTexture,
|
|
||||||
feedbackTextureUnit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0
|
|
||||||
? passProgram.shaderTextureBase
|
|
||||||
: feedbackTextureUnit + (state.feedback.enabled ? 1u : 0u);
|
|
||||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
|
||||||
{
|
|
||||||
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
|
||||||
plan.bindings.push_back({
|
|
||||||
"shaderTexture",
|
|
||||||
textureBinding.samplerName,
|
|
||||||
textureBinding.texture,
|
|
||||||
shaderTextureBase + static_cast<GLuint>(index)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
|
||||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
|
||||||
{
|
|
||||||
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
|
|
||||||
plan.bindings.push_back({
|
|
||||||
"textTexture",
|
|
||||||
textBinding.samplerName,
|
|
||||||
textBinding.texture,
|
|
||||||
textTextureBase + static_cast<GLuint>(index)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
|
||||||
{
|
|
||||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
|
||||||
{
|
|
||||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, binding.texture);
|
|
||||||
}
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
|
||||||
{
|
|
||||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
|
||||||
{
|
|
||||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
}
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGLRenderer.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class ShaderTextureBindings
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
|
||||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
|
||||||
|
|
||||||
struct RuntimeTextureBinding
|
|
||||||
{
|
|
||||||
std::string semanticName;
|
|
||||||
std::string samplerName;
|
|
||||||
GLuint texture = 0;
|
|
||||||
GLuint textureUnit = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RuntimeTextureBindingPlan
|
|
||||||
{
|
|
||||||
std::vector<RuntimeTextureBinding> bindings;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
|
||||||
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
|
||||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
|
||||||
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
|
||||||
GLuint ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const;
|
|
||||||
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
|
||||||
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
|
||||||
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
|
||||||
const PassProgram& passProgram,
|
|
||||||
GLuint layerInputTexture,
|
|
||||||
GLuint originalLayerInputTexture,
|
|
||||||
const RuntimeRenderState& state,
|
|
||||||
GLuint feedbackTexture,
|
|
||||||
const std::vector<GLuint>& sourceHistoryTextures,
|
|
||||||
const std::vector<GLuint>& temporalHistoryTextures) const;
|
|
||||||
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
|
||||||
void UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
|
||||||
};
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#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);
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#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);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
//{{NO_DEPENDENCIES}}
|
|
||||||
// Microsoft Visual C++ generated include file.
|
|
||||||
// Used by LoopThroughWithOpenGLCompositing.rc
|
|
||||||
//
|
|
||||||
#define IDC_MYICON 2
|
|
||||||
#define IDD_OPENGLOUTPUT_DIALOG 102
|
|
||||||
#define IDS_APP_TITLE 103
|
|
||||||
#define IDI_OPENGLOUTPUT 107
|
|
||||||
#define IDI_SMALL 108
|
|
||||||
#define IDC_OPENGLOUTPUT 109
|
|
||||||
#define IDR_MAINFRAME 128
|
|
||||||
#define IDC_STATIC -1
|
|
||||||
|
|
||||||
// Next default values for new objects
|
|
||||||
//
|
|
||||||
#ifdef APSTUDIO_INVOKED
|
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
|
||||||
#define _APS_NO_MFC 1
|
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 129
|
|
||||||
#define _APS_NEXT_COMMAND_VALUE 32771
|
|
||||||
#define _APS_NEXT_CONTROL_VALUE 1000
|
|
||||||
#define _APS_NEXT_SYMED_VALUE 110
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#include "stdafx.h"
|
|
||||||
#include "HealthTelemetry.h"
|
|
||||||
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
HealthTelemetry::HealthTelemetry(RuntimeHost& runtimeHost) :
|
|
||||||
mRuntimeHost(runtimeHost)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void HealthTelemetry::ReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
|
||||||
{
|
|
||||||
mRuntimeHost.WriteSignalStatus(hasSignal, width, height, modeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HealthTelemetry::TryReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.TryWriteSignalStatus(hasSignal, width, height, modeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HealthTelemetry::RecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
|
||||||
{
|
|
||||||
mRuntimeHost.WritePerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HealthTelemetry::TryRecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.TryWritePerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HealthTelemetry::RecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
|
||||||
{
|
|
||||||
mRuntimeHost.WriteFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
|
||||||
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HealthTelemetry::TryRecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.TryWriteFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
|
|
||||||
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class RuntimeHost;
|
|
||||||
|
|
||||||
// Phase 1 compatibility seam for status and timing reporting. The current
|
|
||||||
// implementation still writes through RuntimeHost, but callers can now target
|
|
||||||
// HealthTelemetry as the home for operational visibility work.
|
|
||||||
class HealthTelemetry
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit HealthTelemetry(RuntimeHost& runtimeHost);
|
|
||||||
|
|
||||||
void ReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
bool TryReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
|
|
||||||
void RecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
bool TryRecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
|
|
||||||
void RecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
bool TryRecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
|
|
||||||
private:
|
|
||||||
RuntimeHost& mRuntimeHost;
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <ctime>
|
|
||||||
|
|
||||||
struct RuntimeClockSnapshot
|
|
||||||
{
|
|
||||||
double utcTimeSeconds = 0.0;
|
|
||||||
double utcOffsetSeconds = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
RuntimeClockSnapshot GetRuntimeClockSnapshot();
|
|
||||||
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now);
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#include "RuntimeCoordinator.h"
|
|
||||||
|
|
||||||
#include "RuntimeStore.h"
|
|
||||||
|
|
||||||
RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore) :
|
|
||||||
mRuntimeStore(runtimeStore)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::AddLayer(const std::string& shaderId)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& layerId)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerId, int direction)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerBypass(const std::string& layerId, bool bypassed)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerShader(const std::string& layerId, const std::string& shaderId)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(layerId, parameterId, newValue, error), error, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.SetStoredParameterValueByControlKey(layerKey, parameterKey, newValue, error), error, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::string& layerId)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false);
|
|
||||||
if (!result.accepted)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
result.clearTransientOscState = true;
|
|
||||||
result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::SaveStackPreset(const std::string& presetName)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::LoadStackPreset(const std::string& presetName)
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
return ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::RequestShaderReload(bool preserveFeedbackState)
|
|
||||||
{
|
|
||||||
return BuildQueuedReloadResult(preserveFeedbackState);
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimePollFailure(const std::string& error)
|
|
||||||
{
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = true;
|
|
||||||
result.runtimeStateBroadcastRequired = true;
|
|
||||||
result.compileStatusChanged = true;
|
|
||||||
result.compileStatusSucceeded = false;
|
|
||||||
result.compileStatusMessage = error;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildFailure(const std::string& error)
|
|
||||||
{
|
|
||||||
mPreserveFeedbackOnNextShaderBuild = false;
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = true;
|
|
||||||
result.runtimeStateBroadcastRequired = true;
|
|
||||||
result.compileStatusChanged = true;
|
|
||||||
result.compileStatusSucceeded = false;
|
|
||||||
result.compileStatusMessage = error;
|
|
||||||
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildSuccess()
|
|
||||||
{
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = true;
|
|
||||||
result.runtimeStateBroadcastRequired = true;
|
|
||||||
result.compileStatusChanged = true;
|
|
||||||
result.compileStatusSucceeded = true;
|
|
||||||
result.compileStatusMessage = "Shader layers compiled successfully.";
|
|
||||||
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots;
|
|
||||||
mPreserveFeedbackOnNextShaderBuild = false;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimeReloadRequest()
|
|
||||||
{
|
|
||||||
return BuildQueuedReloadResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeCoordinator::PreserveFeedbackOnNextShaderBuild() const
|
|
||||||
{
|
|
||||||
return mPreserveFeedbackOnNextShaderBuild;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState)
|
|
||||||
{
|
|
||||||
if (!succeeded)
|
|
||||||
{
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = false;
|
|
||||||
result.errorMessage = errorMessage;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reloadRequired)
|
|
||||||
return BuildQueuedReloadResult(preserveFeedbackState);
|
|
||||||
|
|
||||||
return BuildAcceptedNoReloadResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::BuildQueuedReloadResult(bool preserveFeedbackState)
|
|
||||||
{
|
|
||||||
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = true;
|
|
||||||
result.runtimeStateBroadcastRequired = true;
|
|
||||||
result.shaderBuildRequested = true;
|
|
||||||
result.compileStatusChanged = true;
|
|
||||||
result.compileStatusSucceeded = true;
|
|
||||||
result.compileStatusMessage = "Shader rebuild queued.";
|
|
||||||
result.clearReloadRequest = true;
|
|
||||||
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RuntimeCoordinator::BuildAcceptedNoReloadResult() const
|
|
||||||
{
|
|
||||||
RuntimeCoordinatorResult result;
|
|
||||||
result.accepted = true;
|
|
||||||
result.runtimeStateBroadcastRequired = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class RuntimeStore;
|
|
||||||
|
|
||||||
enum class RuntimeCoordinatorCommittedStateMode
|
|
||||||
{
|
|
||||||
Unchanged,
|
|
||||||
UseCommittedStates,
|
|
||||||
UseLiveSnapshots
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class RuntimeCoordinatorRenderResetScope
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
TemporalHistoryOnly,
|
|
||||||
TemporalHistoryAndFeedback
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RuntimeCoordinatorResult
|
|
||||||
{
|
|
||||||
bool accepted = false;
|
|
||||||
bool runtimeStateBroadcastRequired = false;
|
|
||||||
bool shaderBuildRequested = false;
|
|
||||||
bool clearTransientOscState = false;
|
|
||||||
bool compileStatusChanged = false;
|
|
||||||
bool compileStatusSucceeded = false;
|
|
||||||
bool clearReloadRequest = false;
|
|
||||||
RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged;
|
|
||||||
RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None;
|
|
||||||
std::string compileStatusMessage;
|
|
||||||
std::string errorMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RuntimeCoordinator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit RuntimeCoordinator(RuntimeStore& runtimeStore);
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult AddLayer(const std::string& shaderId);
|
|
||||||
RuntimeCoordinatorResult RemoveLayer(const std::string& layerId);
|
|
||||||
RuntimeCoordinatorResult MoveLayer(const std::string& layerId, int direction);
|
|
||||||
RuntimeCoordinatorResult MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex);
|
|
||||||
RuntimeCoordinatorResult SetLayerBypass(const std::string& layerId, bool bypassed);
|
|
||||||
RuntimeCoordinatorResult SetLayerShader(const std::string& layerId, const std::string& shaderId);
|
|
||||||
RuntimeCoordinatorResult UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue);
|
|
||||||
RuntimeCoordinatorResult UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue);
|
|
||||||
RuntimeCoordinatorResult ResetLayerParameters(const std::string& layerId);
|
|
||||||
RuntimeCoordinatorResult SaveStackPreset(const std::string& presetName);
|
|
||||||
RuntimeCoordinatorResult LoadStackPreset(const std::string& presetName);
|
|
||||||
|
|
||||||
RuntimeCoordinatorResult RequestShaderReload(bool preserveFeedbackState = false);
|
|
||||||
RuntimeCoordinatorResult HandleRuntimePollFailure(const std::string& error);
|
|
||||||
RuntimeCoordinatorResult HandlePreparedShaderBuildFailure(const std::string& error);
|
|
||||||
RuntimeCoordinatorResult HandlePreparedShaderBuildSuccess();
|
|
||||||
RuntimeCoordinatorResult HandleRuntimeReloadRequest();
|
|
||||||
bool PreserveFeedbackOnNextShaderBuild() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState);
|
|
||||||
RuntimeCoordinatorResult BuildQueuedReloadResult(bool preserveFeedbackState);
|
|
||||||
RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const;
|
|
||||||
|
|
||||||
RuntimeStore& mRuntimeStore;
|
|
||||||
bool mPreserveFeedbackOnNextShaderBuild = false;
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,212 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "HealthTelemetry.h"
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
#include "ShaderTypes.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <map>
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class RuntimeHost
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
RuntimeHost();
|
|
||||||
|
|
||||||
bool Initialize(std::string& error);
|
|
||||||
|
|
||||||
bool PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
|
||||||
bool ManualReloadRequested();
|
|
||||||
void ClearReloadRequest();
|
|
||||||
|
|
||||||
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 UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
|
||||||
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
|
||||||
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error);
|
|
||||||
bool ApplyOscTargetByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& targetValue, double smoothingAmount, bool& keepApplying, std::string& resolvedLayerId, std::string& resolvedParameterId, ShaderParameterValue& appliedValue, std::string& error);
|
|
||||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
|
||||||
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
|
||||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
|
||||||
|
|
||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
|
||||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
|
||||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
|
||||||
void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
|
||||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
|
||||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
void AdvanceFrame();
|
|
||||||
bool TryAdvanceFrame();
|
|
||||||
HealthTelemetry& GetHealthTelemetry() { return mHealthTelemetry; }
|
|
||||||
const HealthTelemetry& GetHealthTelemetry() const { return mHealthTelemetry; }
|
|
||||||
|
|
||||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
|
|
||||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
|
||||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
|
||||||
bool TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const;
|
|
||||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
|
||||||
std::string BuildStateJson() const;
|
|
||||||
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
|
|
||||||
uint64_t GetParameterStateVersion() const { return mParameterStateVersion.load(std::memory_order_relaxed); }
|
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
|
||||||
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
|
||||||
const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; }
|
|
||||||
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
|
||||||
unsigned short GetServerPort() const { return mServerPort; }
|
|
||||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
|
||||||
const std::string& GetOscBindAddress() const { return mConfig.oscBindAddress; }
|
|
||||||
double GetOscSmoothing() const { return mConfig.oscSmoothing; }
|
|
||||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
|
||||||
unsigned GetPreviewFps() const { return mConfig.previewFps; }
|
|
||||||
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
|
||||||
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
|
|
||||||
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
|
|
||||||
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
|
|
||||||
const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; }
|
|
||||||
void SetServerPort(unsigned short port);
|
|
||||||
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct AppConfig
|
|
||||||
{
|
|
||||||
std::string shaderLibrary = "shaders";
|
|
||||||
unsigned short serverPort = 8080;
|
|
||||||
unsigned short oscPort = 9000;
|
|
||||||
std::string oscBindAddress = "127.0.0.1";
|
|
||||||
double oscSmoothing = 0.18;
|
|
||||||
bool autoReload = true;
|
|
||||||
unsigned maxTemporalHistoryFrames = 4;
|
|
||||||
unsigned previewFps = 30;
|
|
||||||
bool enableExternalKeying = false;
|
|
||||||
std::string inputVideoFormat = "1080p";
|
|
||||||
std::string inputFrameRate = "59.94";
|
|
||||||
std::string outputVideoFormat = "1080p";
|
|
||||||
std::string outputFrameRate = "59.94";
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DeckLinkOutputStatus
|
|
||||||
{
|
|
||||||
std::string backendName = "decklink";
|
|
||||||
std::string modelName;
|
|
||||||
bool supportsInternalKeying = false;
|
|
||||||
bool supportsExternalKeying = false;
|
|
||||||
bool keyerInterfaceAvailable = false;
|
|
||||||
bool externalKeyingRequested = false;
|
|
||||||
bool externalKeyingActive = false;
|
|
||||||
std::string statusMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LayerPersistentState
|
|
||||||
{
|
|
||||||
std::string id;
|
|
||||||
std::string shaderId;
|
|
||||||
bool bypass = false;
|
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PersistentState
|
|
||||||
{
|
|
||||||
std::vector<LayerPersistentState> layers;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool LoadConfig(std::string& error);
|
|
||||||
bool LoadPersistentState(std::string& error);
|
|
||||||
bool SavePersistentState(std::string& error) const;
|
|
||||||
bool ScanShaderPackages(std::string& error);
|
|
||||||
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
|
||||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
|
||||||
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
|
|
||||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
|
||||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
|
||||||
bool ResolvePaths(std::string& error);
|
|
||||||
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
|
||||||
JsonValue BuildStateValue() const;
|
|
||||||
JsonValue SerializeLayerStackLocked() const;
|
|
||||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
|
||||||
void NormalizePersistentLayerIdsLocked();
|
|
||||||
std::vector<std::string> GetStackPresetNamesLocked() const;
|
|
||||||
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
|
||||||
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
|
||||||
std::string TemporalHistorySourceToString(TemporalHistorySource source) const;
|
|
||||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
|
||||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
|
||||||
std::string GenerateLayerId();
|
|
||||||
void WriteSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
bool TryWriteSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
|
||||||
void MarkRenderStateDirtyLocked();
|
|
||||||
void MarkParameterStateDirtyLocked();
|
|
||||||
void WritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
bool TryWritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
|
||||||
void WriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
bool TryWriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class HealthTelemetry;
|
|
||||||
HealthTelemetry mHealthTelemetry;
|
|
||||||
mutable std::mutex mMutex;
|
|
||||||
AppConfig mConfig;
|
|
||||||
PersistentState mPersistentState;
|
|
||||||
std::filesystem::path mRepoRoot;
|
|
||||||
std::filesystem::path mUiRoot;
|
|
||||||
std::filesystem::path mDocsRoot;
|
|
||||||
std::filesystem::path mShaderRoot;
|
|
||||||
std::filesystem::path mRuntimeRoot;
|
|
||||||
std::filesystem::path mPresetRoot;
|
|
||||||
std::filesystem::path mRuntimeStatePath;
|
|
||||||
std::filesystem::path mConfigPath;
|
|
||||||
std::filesystem::path mWrapperPath;
|
|
||||||
std::filesystem::path mGeneratedGlslPath;
|
|
||||||
std::filesystem::path mPatchedGlslPath;
|
|
||||||
std::map<std::string, ShaderPackage> mPackagesById;
|
|
||||||
std::vector<std::string> mPackageOrder;
|
|
||||||
std::vector<ShaderPackageStatus> mPackageStatuses;
|
|
||||||
bool mReloadRequested;
|
|
||||||
bool mCompileSucceeded;
|
|
||||||
std::string mCompileMessage;
|
|
||||||
bool mHasSignal;
|
|
||||||
unsigned mSignalWidth;
|
|
||||||
unsigned mSignalHeight;
|
|
||||||
std::string mSignalModeName;
|
|
||||||
double mFrameBudgetMilliseconds;
|
|
||||||
double mRenderMilliseconds;
|
|
||||||
double mSmoothedRenderMilliseconds;
|
|
||||||
double mCompletionIntervalMilliseconds;
|
|
||||||
double mSmoothedCompletionIntervalMilliseconds;
|
|
||||||
double mMaxCompletionIntervalMilliseconds;
|
|
||||||
double mStartupRandom;
|
|
||||||
uint64_t mLateFrameCount;
|
|
||||||
uint64_t mDroppedFrameCount;
|
|
||||||
uint64_t mFlushedFrameCount;
|
|
||||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
|
||||||
unsigned short mServerPort;
|
|
||||||
bool mAutoReloadEnabled;
|
|
||||||
std::chrono::steady_clock::time_point mStartTime;
|
|
||||||
std::chrono::steady_clock::time_point mLastScanTime;
|
|
||||||
std::atomic<uint64_t> mFrameCounter{ 0 };
|
|
||||||
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
|
||||||
std::atomic<uint64_t> mParameterStateVersion{ 0 };
|
|
||||||
uint64_t mNextLayerId;
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#include "RuntimeSnapshotProvider.h"
|
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
RuntimeSnapshotProvider::RuntimeSnapshotProvider(RuntimeHost& runtimeHost) :
|
|
||||||
mRuntimeHost(runtimeHost)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.BuildLayerPassFragmentShaderSources(layerId, passSources, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetMaxTemporalHistoryFrames();
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const
|
|
||||||
{
|
|
||||||
RuntimeSnapshotVersions versions;
|
|
||||||
versions.renderStateVersion = mRuntimeHost.GetRenderStateVersion();
|
|
||||||
versions.parameterStateVersion = mRuntimeHost.GetParameterStateVersion();
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeRenderFrameContext RuntimeSnapshotProvider::GetFrameContext() const
|
|
||||||
{
|
|
||||||
std::vector<RuntimeRenderState> stateScratch(1);
|
|
||||||
mRuntimeHost.RefreshDynamicRenderStateFields(stateScratch);
|
|
||||||
|
|
||||||
RuntimeRenderFrameContext frameContext;
|
|
||||||
const RuntimeRenderState& state = stateScratch.front();
|
|
||||||
frameContext.timeSeconds = state.timeSeconds;
|
|
||||||
frameContext.utcTimeSeconds = state.utcTimeSeconds;
|
|
||||||
frameContext.utcOffsetSeconds = state.utcOffsetSeconds;
|
|
||||||
frameContext.startupRandom = state.startupRandom;
|
|
||||||
frameContext.frameCount = state.frameCount;
|
|
||||||
return frameContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeSnapshotProvider::AdvanceFrame()
|
|
||||||
{
|
|
||||||
mRuntimeHost.AdvanceFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::TryAdvanceFrame()
|
|
||||||
{
|
|
||||||
return mRuntimeHost.TryAdvanceFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeRenderStateSnapshot RuntimeSnapshotProvider::GetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const
|
|
||||||
{
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
|
||||||
|
|
||||||
RuntimeRenderStateSnapshot snapshot;
|
|
||||||
snapshot.outputWidth = outputWidth;
|
|
||||||
snapshot.outputHeight = outputHeight;
|
|
||||||
snapshot.states = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
|
|
||||||
|
|
||||||
const RuntimeSnapshotVersions versionsAfter = GetVersions();
|
|
||||||
if (versionsBefore.renderStateVersion == versionsAfter.renderStateVersion &&
|
|
||||||
versionsBefore.parameterStateVersion == versionsAfter.parameterStateVersion)
|
|
||||||
{
|
|
||||||
snapshot.versions = versionsAfter;
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::TryGetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const
|
|
||||||
{
|
|
||||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
|
||||||
|
|
||||||
std::vector<RuntimeRenderState> states;
|
|
||||||
if (!mRuntimeHost.TryGetLayerRenderStates(outputWidth, outputHeight, states))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const RuntimeSnapshotVersions versionsAfter = GetVersions();
|
|
||||||
if (versionsBefore.renderStateVersion != versionsAfter.renderStateVersion ||
|
|
||||||
versionsBefore.parameterStateVersion != versionsAfter.parameterStateVersion)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot.outputWidth = outputWidth;
|
|
||||||
snapshot.outputHeight = outputHeight;
|
|
||||||
snapshot.versions = versionsAfter;
|
|
||||||
snapshot.states = std::move(states);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::TryRefreshSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const
|
|
||||||
{
|
|
||||||
const uint64_t expectedRenderStateVersion = snapshot.versions.renderStateVersion;
|
|
||||||
if (!mRuntimeHost.TryRefreshCachedLayerStates(snapshot.states))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const RuntimeSnapshotVersions versions = GetVersions();
|
|
||||||
if (versions.renderStateVersion != expectedRenderStateVersion)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
snapshot.versions = versions;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeSnapshotProvider::ApplyFrameContext(std::vector<RuntimeRenderState>& states, const RuntimeRenderFrameContext& frameContext) const
|
|
||||||
{
|
|
||||||
for (RuntimeRenderState& state : states)
|
|
||||||
{
|
|
||||||
state.timeSeconds = frameContext.timeSeconds;
|
|
||||||
state.utcTimeSeconds = frameContext.utcTimeSeconds;
|
|
||||||
state.utcOffsetSeconds = frameContext.utcOffsetSeconds;
|
|
||||||
state.startupRandom = frameContext.startupRandom;
|
|
||||||
state.frameCount = frameContext.frameCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeSnapshotProvider::ApplyFrameContext(RuntimeRenderStateSnapshot& snapshot, const RuntimeRenderFrameContext& frameContext) const
|
|
||||||
{
|
|
||||||
ApplyFrameContext(snapshot.states, frameContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RuntimeRenderState> RuntimeSnapshotProvider::GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const
|
|
||||||
{
|
|
||||||
return GetRenderStateSnapshot(outputWidth, outputHeight).states;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
|
||||||
{
|
|
||||||
RuntimeRenderStateSnapshot snapshot;
|
|
||||||
if (!TryGetRenderStateSnapshot(outputWidth, outputHeight, snapshot))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
states = std::move(snapshot.states);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeSnapshotProvider::TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const
|
|
||||||
{
|
|
||||||
RuntimeRenderStateSnapshot snapshot;
|
|
||||||
snapshot.versions.renderStateVersion = mRuntimeHost.GetRenderStateVersion();
|
|
||||||
snapshot.versions.parameterStateVersion = mRuntimeHost.GetParameterStateVersion();
|
|
||||||
snapshot.states = states;
|
|
||||||
if (!TryRefreshSnapshotParameters(snapshot))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
states = std::move(snapshot.states);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
|
||||||
{
|
|
||||||
ApplyFrameContext(states, GetFrameContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t RuntimeSnapshotProvider::GetRenderStateVersion() const
|
|
||||||
{
|
|
||||||
return GetVersions().renderStateVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t RuntimeSnapshotProvider::GetParameterStateVersion() const
|
|
||||||
{
|
|
||||||
return GetVersions().parameterStateVersion;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
struct RuntimeSnapshotVersions
|
|
||||||
{
|
|
||||||
uint64_t renderStateVersion = 0;
|
|
||||||
uint64_t parameterStateVersion = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RuntimeRenderFrameContext
|
|
||||||
{
|
|
||||||
double timeSeconds = 0.0;
|
|
||||||
double utcTimeSeconds = 0.0;
|
|
||||||
double utcOffsetSeconds = 0.0;
|
|
||||||
double startupRandom = 0.0;
|
|
||||||
double frameCount = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RuntimeRenderStateSnapshot
|
|
||||||
{
|
|
||||||
RuntimeSnapshotVersions versions;
|
|
||||||
unsigned outputWidth = 0;
|
|
||||||
unsigned outputHeight = 0;
|
|
||||||
std::vector<RuntimeRenderState> states;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RuntimeSnapshotProvider
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit RuntimeSnapshotProvider(RuntimeHost& runtimeHost);
|
|
||||||
|
|
||||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
|
|
||||||
unsigned GetMaxTemporalHistoryFrames() const;
|
|
||||||
RuntimeSnapshotVersions GetVersions() const;
|
|
||||||
RuntimeRenderFrameContext GetFrameContext() const;
|
|
||||||
void AdvanceFrame();
|
|
||||||
bool TryAdvanceFrame();
|
|
||||||
RuntimeRenderStateSnapshot GetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const;
|
|
||||||
bool TryGetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const;
|
|
||||||
bool TryRefreshSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const;
|
|
||||||
void ApplyFrameContext(std::vector<RuntimeRenderState>& states, const RuntimeRenderFrameContext& frameContext) const;
|
|
||||||
void ApplyFrameContext(RuntimeRenderStateSnapshot& snapshot, const RuntimeRenderFrameContext& frameContext) const;
|
|
||||||
|
|
||||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
|
||||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
|
||||||
bool TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const;
|
|
||||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
|
||||||
uint64_t GetRenderStateVersion() const;
|
|
||||||
uint64_t GetParameterStateVersion() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
RuntimeHost& mRuntimeHost;
|
|
||||||
};
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#include "RuntimeStore.h"
|
|
||||||
|
|
||||||
RuntimeStore::RuntimeStore(RuntimeHost& runtimeHost) :
|
|
||||||
mRuntimeHost(runtimeHost)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::InitializeStore(std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.Initialize(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string RuntimeStore::BuildPersistentStateJson() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.BuildStateJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.AddLayer(shaderId, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.RemoveLayer(layerId, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.MoveLayer(layerId, direction, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.MoveLayerToIndex(layerId, targetIndex, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.SetLayerBypass(layerId, bypassed, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.SetLayerShader(layerId, shaderId, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.UpdateLayerParameter(layerId, parameterId, newValue, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, persistState, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.ResetLayerParameters(layerId, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.SaveStackPreset(presetName, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
|
|
||||||
{
|
|
||||||
return mRuntimeHost.LoadStackPreset(presetName, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetRepoRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetUiRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetDocsRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetRuntimeRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned short RuntimeStore::GetConfiguredControlServerPort() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetServerPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned short RuntimeStore::GetConfiguredOscPort() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetOscPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetOscBindAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
double RuntimeStore::GetConfiguredOscSmoothing() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetOscSmoothing();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetMaxTemporalHistoryFrames();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned RuntimeStore::GetConfiguredPreviewFps() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetPreviewFps();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeStore::IsExternalKeyingConfigured() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.ExternalKeyingEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetInputVideoFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetInputFrameRate();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetOutputVideoFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
|
|
||||||
{
|
|
||||||
return mRuntimeHost.GetOutputFrameRate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
|
|
||||||
{
|
|
||||||
mRuntimeHost.SetCompileStatus(succeeded, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeStore::ClearReloadRequest()
|
|
||||||
{
|
|
||||||
mRuntimeHost.ClearReloadRequest();
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class RuntimeStore
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit RuntimeStore(RuntimeHost& runtimeHost);
|
|
||||||
|
|
||||||
bool InitializeStore(std::string& error);
|
|
||||||
std::string BuildPersistentStateJson() const;
|
|
||||||
|
|
||||||
bool CreateStoredLayer(const std::string& shaderId, std::string& error);
|
|
||||||
bool DeleteStoredLayer(const std::string& layerId, std::string& error);
|
|
||||||
bool MoveStoredLayer(const std::string& layerId, int direction, std::string& error);
|
|
||||||
bool MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
|
||||||
bool SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error);
|
|
||||||
bool SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error);
|
|
||||||
bool SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
|
||||||
bool SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
|
||||||
bool SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error);
|
|
||||||
bool ResetStoredLayerParameterValues(const std::string& layerId, std::string& error);
|
|
||||||
bool SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const;
|
|
||||||
bool LoadStackPresetSnapshot(const std::string& presetName, std::string& error);
|
|
||||||
|
|
||||||
const std::filesystem::path& GetRuntimeRepositoryRoot() const;
|
|
||||||
const std::filesystem::path& GetRuntimeUiRoot() const;
|
|
||||||
const std::filesystem::path& GetRuntimeDocsRoot() const;
|
|
||||||
const std::filesystem::path& GetRuntimeDataRoot() const;
|
|
||||||
unsigned short GetConfiguredControlServerPort() const;
|
|
||||||
unsigned short GetConfiguredOscPort() const;
|
|
||||||
const std::string& GetConfiguredOscBindAddress() const;
|
|
||||||
double GetConfiguredOscSmoothing() const;
|
|
||||||
unsigned GetConfiguredMaxTemporalHistoryFrames() const;
|
|
||||||
unsigned GetConfiguredPreviewFps() const;
|
|
||||||
bool IsExternalKeyingConfigured() const;
|
|
||||||
const std::string& GetConfiguredInputVideoFormat() const;
|
|
||||||
const std::string& GetConfiguredInputFrameRate() const;
|
|
||||||
const std::string& GetConfiguredOutputVideoFormat() const;
|
|
||||||
const std::string& GetConfiguredOutputFrameRate() const;
|
|
||||||
|
|
||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
|
||||||
void ClearReloadRequest();
|
|
||||||
|
|
||||||
private:
|
|
||||||
RuntimeHost& mRuntimeHost;
|
|
||||||
};
|
|
||||||
@@ -1,818 +0,0 @@
|
|||||||
#include "stdafx.h"
|
|
||||||
#include "ShaderPackageRegistry.h"
|
|
||||||
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cctype>
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::string Trim(const std::string& text)
|
|
||||||
{
|
|
||||||
std::size_t start = 0;
|
|
||||||
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
|
|
||||||
++start;
|
|
||||||
|
|
||||||
std::size_t end = text.size();
|
|
||||||
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
|
|
||||||
--end;
|
|
||||||
|
|
||||||
return text.substr(start, end - start);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsFiniteNumber(double value)
|
|
||||||
{
|
|
||||||
return std::isfinite(value) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
|
||||||
{
|
|
||||||
if (typeName == "float")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Float;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "vec2")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Vec2;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "color")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Color;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "bool")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Boolean;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "enum")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Enum;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "text")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Text;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeName == "trigger")
|
|
||||||
{
|
|
||||||
type = ShaderParameterType::Trigger;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
|
|
||||||
{
|
|
||||||
if (sourceName == "source")
|
|
||||||
{
|
|
||||||
source = TemporalHistorySource::Source;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (sourceName == "preLayerInput")
|
|
||||||
{
|
|
||||||
source = TemporalHistorySource::PreLayerInput;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (sourceName == "none")
|
|
||||||
{
|
|
||||||
source = TemporalHistorySource::None;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
|
|
||||||
{
|
|
||||||
std::ifstream input(path, std::ios::binary);
|
|
||||||
if (!input)
|
|
||||||
{
|
|
||||||
error = "Could not open file: " + path.string();
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream buffer;
|
|
||||||
buffer << input.rdbuf();
|
|
||||||
return buffer.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
|
||||||
{
|
|
||||||
return manifestPath.string();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* fieldValue = object.find(fieldName);
|
|
||||||
if (!fieldValue || !fieldValue->isString())
|
|
||||||
{
|
|
||||||
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = fieldValue->asString();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (!RequireStringField(object, fieldName, value, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (Trim(value).empty())
|
|
||||||
{
|
|
||||||
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* fieldValue = object.find(fieldName);
|
|
||||||
if (!fieldValue)
|
|
||||||
{
|
|
||||||
value = fallback;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!fieldValue->isString())
|
|
||||||
{
|
|
||||||
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value = fieldValue->asString();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
value = object.find(fieldName);
|
|
||||||
if (!value)
|
|
||||||
return true;
|
|
||||||
if (!value->isArray())
|
|
||||||
{
|
|
||||||
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
value = object.find(fieldName);
|
|
||||||
if (!value)
|
|
||||||
return true;
|
|
||||||
if (!value->isObject())
|
|
||||||
{
|
|
||||||
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (value.isNumber())
|
|
||||||
{
|
|
||||||
numbers.push_back(value.asNumber());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (value.isArray())
|
|
||||||
{
|
|
||||||
numbers = JsonArrayToNumbers(value);
|
|
||||||
if (numbers.size() != value.asArray().size())
|
|
||||||
{
|
|
||||||
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
|
|
||||||
{
|
|
||||||
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (char ch : identifier)
|
|
||||||
{
|
|
||||||
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
|
|
||||||
if (!(std::isalnum(unsignedCh) || ch == '_'))
|
|
||||||
{
|
|
||||||
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
|
|
||||||
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
|
|
||||||
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
|
|
||||||
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
|
|
||||||
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
shaderPackage.directoryPath = manifestPath.parent_path();
|
|
||||||
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
|
|
||||||
shaderPackage.manifestPath = manifestPath;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* passesValue = nullptr;
|
|
||||||
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!passesValue)
|
|
||||||
{
|
|
||||||
// Existing shader packages are treated as a single implicit pass, so
|
|
||||||
// multipass support does not require manifest churn.
|
|
||||||
ShaderPassDefinition pass;
|
|
||||||
pass.id = "main";
|
|
||||||
pass.entryPoint = shaderPackage.entryPoint;
|
|
||||||
pass.sourcePath = shaderPackage.shaderPath;
|
|
||||||
pass.outputName = "layerOutput";
|
|
||||||
if (!std::filesystem::exists(pass.sourcePath))
|
|
||||||
{
|
|
||||||
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
|
|
||||||
shaderPackage.passes.push_back(pass);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passesValue->asArray().empty())
|
|
||||||
{
|
|
||||||
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const JsonValue& passJson : passesValue->asArray())
|
|
||||||
{
|
|
||||||
if (!passJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string passId;
|
|
||||||
std::string sourcePath;
|
|
||||||
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
|
|
||||||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
|
|
||||||
{
|
|
||||||
if (existingPass.id == passId)
|
|
||||||
{
|
|
||||||
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderPassDefinition pass;
|
|
||||||
pass.id = passId;
|
|
||||||
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
|
|
||||||
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
|
|
||||||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const JsonValue* inputsValue = nullptr;
|
|
||||||
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (inputsValue)
|
|
||||||
{
|
|
||||||
for (const JsonValue& inputValue : inputsValue->asArray())
|
|
||||||
{
|
|
||||||
if (!inputValue.isString())
|
|
||||||
{
|
|
||||||
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pass.inputNames.push_back(inputValue.asString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep source validation in the registry. Bad pass declarations then
|
|
||||||
// appear as unavailable shaders instead of failing at render time.
|
|
||||||
if (!std::filesystem::exists(pass.sourcePath))
|
|
||||||
{
|
|
||||||
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
|
|
||||||
shaderPackage.passes.push_back(pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* texturesValue = nullptr;
|
|
||||||
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (!texturesValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (const JsonValue& textureJson : texturesValue->asArray())
|
|
||||||
{
|
|
||||||
if (!textureJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string textureId;
|
|
||||||
std::string texturePath;
|
|
||||||
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
|
|
||||||
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ShaderTextureAsset textureAsset;
|
|
||||||
textureAsset.id = textureId;
|
|
||||||
textureAsset.path = shaderPackage.directoryPath / texturePath;
|
|
||||||
if (!std::filesystem::exists(textureAsset.path))
|
|
||||||
{
|
|
||||||
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
|
|
||||||
shaderPackage.textureAssets.push_back(textureAsset);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* fontsValue = nullptr;
|
|
||||||
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (!fontsValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (const JsonValue& fontJson : fontsValue->asArray())
|
|
||||||
{
|
|
||||||
if (!fontJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string fontId;
|
|
||||||
std::string fontPath;
|
|
||||||
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
|
||||||
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ShaderFontAsset fontAsset;
|
|
||||||
fontAsset.id = fontId;
|
|
||||||
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
|
||||||
if (!std::filesystem::exists(fontAsset.path))
|
|
||||||
{
|
|
||||||
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
|
||||||
shaderPackage.fontAssets.push_back(fontAsset);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* temporalValue = nullptr;
|
|
||||||
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (!temporalValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const JsonValue* enabledValue = temporalValue->find("enabled");
|
|
||||||
if (!enabledValue || !enabledValue->asBoolean(false))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
std::string historySourceName;
|
|
||||||
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
|
|
||||||
if (!historyLengthValue || !historyLengthValue->isNumber())
|
|
||||||
{
|
|
||||||
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
|
||||||
if (!ParseTemporalHistorySource(historySourceName, historySource))
|
|
||||||
{
|
|
||||||
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const double requestedHistoryLength = historyLengthValue->asNumber();
|
|
||||||
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
|
|
||||||
{
|
|
||||||
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderPackage.temporal.enabled = true;
|
|
||||||
shaderPackage.temporal.historySource = historySource;
|
|
||||||
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
|
|
||||||
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseFeedbackSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* feedbackValue = nullptr;
|
|
||||||
if (!OptionalObjectField(manifestJson, "feedback", feedbackValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (!feedbackValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
const JsonValue* enabledValue = feedbackValue->find("enabled");
|
|
||||||
if (!enabledValue || !enabledValue->asBoolean(false))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
shaderPackage.feedback.enabled = true;
|
|
||||||
if (!OptionalStringField(*feedbackValue, "writePass", shaderPackage.feedback.writePassId, "", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (shaderPackage.feedback.writePassId.empty())
|
|
||||||
{
|
|
||||||
if (shaderPackage.passes.empty())
|
|
||||||
{
|
|
||||||
error = "Feedback-enabled shader has no passes to target in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
shaderPackage.feedback.writePassId = shaderPackage.passes.back().id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidateShaderIdentifier(shaderPackage.feedback.writePassId, "feedback.writePass", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const auto passIt = std::find_if(shaderPackage.passes.begin(), shaderPackage.passes.end(),
|
|
||||||
[&shaderPackage](const ShaderPassDefinition& pass) { return pass.id == shaderPackage.feedback.writePassId; });
|
|
||||||
if (passIt == shaderPackage.passes.end())
|
|
||||||
{
|
|
||||||
error = "Feedback writePass '" + shaderPackage.feedback.writePassId + "' does not match any declared pass in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
|
|
||||||
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* defaultValue = parameterJson.find("default");
|
|
||||||
if (!defaultValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Boolean)
|
|
||||||
{
|
|
||||||
if (!defaultValue->isBoolean())
|
|
||||||
{
|
|
||||||
error = "Boolean parameter default must be a boolean for: " + definition.id;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
definition.defaultBoolean = defaultValue->asBoolean(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
|
||||||
{
|
|
||||||
if (!defaultValue->isString())
|
|
||||||
{
|
|
||||||
error = "Enum parameter default must be a string for: " + definition.id;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
definition.defaultEnumValue = defaultValue->asString();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* optionsValue = nullptr;
|
|
||||||
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
|
|
||||||
{
|
|
||||||
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const JsonValue& optionJson : optionsValue->asArray())
|
|
||||||
{
|
|
||||||
if (!optionJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderParameterOption option;
|
|
||||||
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
|
|
||||||
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
definition.enumOptions.push_back(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool defaultFound = definition.defaultEnumValue.empty();
|
|
||||||
for (const ShaderParameterOption& option : definition.enumOptions)
|
|
||||||
{
|
|
||||||
if (option.value == definition.defaultEnumValue)
|
|
||||||
{
|
|
||||||
defaultFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!defaultFound)
|
|
||||||
{
|
|
||||||
error = "Enum parameter default is not present in its option list for: " + definition.id;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
if (!parameterJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string typeName;
|
|
||||||
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
|
|
||||||
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
|
|
||||||
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
|
|
||||||
{
|
|
||||||
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParseShaderParameterType(typeName, definition.type))
|
|
||||||
{
|
|
||||||
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
|
|
||||||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
|
|
||||||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
|
|
||||||
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Text)
|
|
||||||
{
|
|
||||||
if (const JsonValue* fontValue = parameterJson.find("font"))
|
|
||||||
{
|
|
||||||
if (!fontValue->isString())
|
|
||||||
{
|
|
||||||
error = "Text parameter 'font' must be a string for: " + definition.id;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
definition.fontId = fontValue->asString();
|
|
||||||
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
|
||||||
{
|
|
||||||
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
|
||||||
{
|
|
||||||
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.type == ShaderParameterType::Enum)
|
|
||||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
||||||
{
|
|
||||||
const JsonValue* parametersValue = nullptr;
|
|
||||||
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
if (!parametersValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (const JsonValue& parameterJson : parametersValue->asArray())
|
|
||||||
{
|
|
||||||
ShaderParameterDefinition definition;
|
|
||||||
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
shaderPackage.parameters.push_back(definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderPackageRegistry::Scan(
|
|
||||||
const std::filesystem::path& shaderRoot,
|
|
||||||
std::map<std::string, ShaderPackage>& packagesById,
|
|
||||||
std::vector<std::string>& packageOrder,
|
|
||||||
std::vector<ShaderPackageStatus>& packageStatuses,
|
|
||||||
std::string& error) const
|
|
||||||
{
|
|
||||||
packagesById.clear();
|
|
||||||
packageOrder.clear();
|
|
||||||
packageStatuses.clear();
|
|
||||||
|
|
||||||
if (!std::filesystem::exists(shaderRoot))
|
|
||||||
{
|
|
||||||
error = "Shader library directory does not exist: " + shaderRoot.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
|
|
||||||
{
|
|
||||||
if (!entry.is_directory())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
std::filesystem::path manifestPath = entry.path() / "shader.json";
|
|
||||||
if (!std::filesystem::exists(manifestPath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ShaderPackage shaderPackage;
|
|
||||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
|
||||||
{
|
|
||||||
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
|
|
||||||
error.clear();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
|
||||||
{
|
|
||||||
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
packageOrder.push_back(shaderPackage.id);
|
|
||||||
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
|
|
||||||
packagesById[shaderPackage.id] = shaderPackage;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::sort(packageOrder.begin(), packageOrder.end());
|
|
||||||
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
|
|
||||||
return left.displayName < right.displayName;
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
|
|
||||||
{
|
|
||||||
const std::string manifestText = ReadTextFile(manifestPath, error);
|
|
||||||
if (manifestText.empty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
JsonValue manifestJson;
|
|
||||||
if (!ParseJson(manifestText, manifestJson, error))
|
|
||||||
return false;
|
|
||||||
if (!manifestJson.isObject())
|
|
||||||
{
|
|
||||||
error = "Shader manifest root must be an object: " + manifestPath.string();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
|
|
||||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
|
||||||
{
|
|
||||||
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
|
|
||||||
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
|
|
||||||
}
|
|
||||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
|
||||||
|
|
||||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
|
||||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
|
||||||
ParseFeedbackSettings(manifestJson, shaderPackage, manifestPath, error) &&
|
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,46 +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-
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
// stdafx.cpp : source file that includes just the standard includes
|
|
||||||
// LoopThroughWithOpenGLCompositing.pch will be the pre-compiled header
|
|
||||||
// stdafx.obj will contain the pre-compiled type information
|
|
||||||
|
|
||||||
#include "stdafx.h"
|
|
||||||
|
|
||||||
@@ -1,65 +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-
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
// stdafx.h : include file for standard system include files,
|
|
||||||
// or project specific include files that are used frequently, but
|
|
||||||
// are changed infrequently
|
|
||||||
//
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "targetver.h"
|
|
||||||
|
|
||||||
#ifndef NOMINMAX
|
|
||||||
#define NOMINMAX
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
|
||||||
// Windows Header Files:
|
|
||||||
#include <winsock2.h>
|
|
||||||
#include <ws2tcpip.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
// C RunTime Header Files
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <malloc.h>
|
|
||||||
#include <memory.h>
|
|
||||||
#include <tchar.h>
|
|
||||||
|
|
||||||
@@ -1,64 +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-
|
|
||||||
*/
|
|
||||||
//
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
// The following macros define the minimum required platform. The minimum required platform
|
|
||||||
// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run
|
|
||||||
// your application. The macros work by enabling all features available on platform versions up to and
|
|
||||||
// including the version specified.
|
|
||||||
|
|
||||||
// Modify the following defines if you have to target a platform prior to the ones specified below.
|
|
||||||
// Refer to MSDN for the latest info on corresponding values for different platforms.
|
|
||||||
#ifndef WINVER // Specifies that the minimum required platform is Windows Vista.
|
|
||||||
#define WINVER 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
|
|
||||||
#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef _WIN32_WINDOWS // Specifies that the minimum required platform is Windows 98.
|
|
||||||
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef _WIN32_IE // Specifies that the minimum required platform is Internet Explorer 7.0.
|
|
||||||
#define _WIN32_IE 0x0700 // Change this to the appropriate value to target other versions of IE.
|
|
||||||
#endif
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
#include "VideoBackend.h"
|
|
||||||
|
|
||||||
#include "DeckLinkSession.h"
|
|
||||||
#include "OpenGLVideoIOBridge.h"
|
|
||||||
#include "HealthTelemetry.h"
|
|
||||||
#include "RenderEngine.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry) :
|
|
||||||
mHealthTelemetry(healthTelemetry),
|
|
||||||
mVideoIODevice(std::make_unique<DeckLinkSession>()),
|
|
||||||
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoBackend::~VideoBackend()
|
|
||||||
{
|
|
||||||
ReleaseResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::ReleaseResources()
|
|
||||||
{
|
|
||||||
if (mVideoIODevice)
|
|
||||||
mVideoIODevice->ReleaseResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->DiscoverDevicesAndModes(videoModes, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->ConfigureInput(
|
|
||||||
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
|
|
||||||
inputVideoMode,
|
|
||||||
error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->ConfigureOutput(
|
|
||||||
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
|
|
||||||
outputVideoMode,
|
|
||||||
externalKeyingEnabled,
|
|
||||||
error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::Start()
|
|
||||||
{
|
|
||||||
return mVideoIODevice->Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::Stop()
|
|
||||||
{
|
|
||||||
return mVideoIODevice->Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const VideoIOState& VideoBackend::State() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->State();
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoIOState& VideoBackend::MutableState()
|
|
||||||
{
|
|
||||||
return mVideoIODevice->MutableState();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::BeginOutputFrame(VideoIOOutputFrame& frame)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->BeginOutputFrame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::EndOutputFrame(VideoIOOutputFrame& frame)
|
|
||||||
{
|
|
||||||
mVideoIODevice->EndOutputFrame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
|
||||||
{
|
|
||||||
return mVideoIODevice->ScheduleOutputFrame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result)
|
|
||||||
{
|
|
||||||
mVideoIODevice->AccountForCompletionResult(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::HasInputDevice() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->HasInputDevice();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::HasInputSource() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->HasInputSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::InputFrameWidth() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->InputFrameWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::InputFrameHeight() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->InputFrameHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::OutputFrameWidth() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->OutputFrameWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::OutputFrameHeight() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->OutputFrameHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::CaptureTextureWidth() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->CaptureTextureWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned VideoBackend::OutputPackTextureWidth() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->OutputPackTextureWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoIOPixelFormat VideoBackend::InputPixelFormat() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->InputPixelFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& VideoBackend::InputDisplayModeName() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->InputDisplayModeName();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& VideoBackend::OutputModelName() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->OutputModelName();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::SupportsInternalKeying() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->SupportsInternalKeying();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::SupportsExternalKeying() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->SupportsExternalKeying();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::KeyerInterfaceAvailable() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->KeyerInterfaceAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VideoBackend::ExternalKeyingActive() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->ExternalKeyingActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& VideoBackend::StatusMessage() const
|
|
||||||
{
|
|
||||||
return mVideoIODevice->StatusMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::SetStatusMessage(const std::string& message)
|
|
||||||
{
|
|
||||||
mVideoIODevice->SetStatusMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
|
|
||||||
{
|
|
||||||
const VideoIOState& state = mVideoIODevice->State();
|
|
||||||
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
|
||||||
|
|
||||||
if (mBridge)
|
|
||||||
mBridge->UploadInputFrame(frame, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
|
|
||||||
{
|
|
||||||
RecordFramePacing(completion.result);
|
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
|
||||||
if (!BeginOutputFrame(outputFrame))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const VideoIOState& state = mVideoIODevice->State();
|
|
||||||
if (mBridge)
|
|
||||||
mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
|
||||||
|
|
||||||
EndOutputFrame(outputFrame);
|
|
||||||
AccountForCompletionResult(completion.result);
|
|
||||||
|
|
||||||
// Schedule the next frame after render work is complete so device-side
|
|
||||||
// bookkeeping stays with the backend seam and the bridge stays render-only.
|
|
||||||
ScheduleOutputFrame(outputFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
|
|
||||||
{
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
|
||||||
{
|
|
||||||
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
|
||||||
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
|
||||||
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
|
||||||
else
|
|
||||||
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
|
||||||
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
|
||||||
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
|
||||||
}
|
|
||||||
mLastPlayoutCompletionTime = now;
|
|
||||||
|
|
||||||
if (completionResult == VideoIOCompletionResult::DisplayedLate)
|
|
||||||
++mLateFrameCount;
|
|
||||||
else if (completionResult == VideoIOCompletionResult::Dropped)
|
|
||||||
++mDroppedFrameCount;
|
|
||||||
else if (completionResult == VideoIOCompletionResult::Flushed)
|
|
||||||
++mFlushedFrameCount;
|
|
||||||
|
|
||||||
mHealthTelemetry.TryRecordFramePacingStats(
|
|
||||||
mCompletionIntervalMilliseconds,
|
|
||||||
mSmoothedCompletionIntervalMilliseconds,
|
|
||||||
mMaxCompletionIntervalMilliseconds,
|
|
||||||
mLateFrameCount,
|
|
||||||
mDroppedFrameCount,
|
|
||||||
mFlushedFrameCount);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class HealthTelemetry;
|
|
||||||
class OpenGLVideoIOBridge;
|
|
||||||
class RenderEngine;
|
|
||||||
class VideoIODevice;
|
|
||||||
|
|
||||||
class VideoBackend
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry);
|
|
||||||
~VideoBackend();
|
|
||||||
|
|
||||||
void ReleaseResources();
|
|
||||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
|
||||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
|
|
||||||
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
|
|
||||||
bool ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
|
|
||||||
bool Start();
|
|
||||||
bool Stop();
|
|
||||||
|
|
||||||
const VideoIOState& State() const;
|
|
||||||
VideoIOState& MutableState();
|
|
||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
|
||||||
void EndOutputFrame(VideoIOOutputFrame& frame);
|
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
|
|
||||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
|
||||||
|
|
||||||
bool HasInputDevice() const;
|
|
||||||
bool HasInputSource() const;
|
|
||||||
unsigned InputFrameWidth() const;
|
|
||||||
unsigned InputFrameHeight() const;
|
|
||||||
unsigned OutputFrameWidth() const;
|
|
||||||
unsigned OutputFrameHeight() const;
|
|
||||||
unsigned CaptureTextureWidth() const;
|
|
||||||
unsigned OutputPackTextureWidth() const;
|
|
||||||
VideoIOPixelFormat InputPixelFormat() const;
|
|
||||||
const std::string& InputDisplayModeName() const;
|
|
||||||
const std::string& OutputModelName() const;
|
|
||||||
bool SupportsInternalKeying() const;
|
|
||||||
bool SupportsExternalKeying() const;
|
|
||||||
bool KeyerInterfaceAvailable() const;
|
|
||||||
bool ExternalKeyingActive() const;
|
|
||||||
const std::string& StatusMessage() const;
|
|
||||||
void SetStatusMessage(const std::string& message);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void HandleInputFrame(const VideoIOFrame& frame);
|
|
||||||
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
|
|
||||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
|
||||||
|
|
||||||
HealthTelemetry& mHealthTelemetry;
|
|
||||||
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
|
||||||
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
|
|
||||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
|
||||||
double mCompletionIntervalMilliseconds = 0.0;
|
|
||||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
|
||||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
|
||||||
uint64_t mLateFrameCount = 0;
|
|
||||||
uint64_t mDroppedFrameCount = 0;
|
|
||||||
uint64_t mFlushedFrameCount = 0;
|
|
||||||
};
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "DeckLinkDisplayMode.h"
|
|
||||||
#include "VideoIOFormat.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class VideoIOBackend
|
|
||||||
{
|
|
||||||
DeckLink
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class VideoIOCompletionResult
|
|
||||||
{
|
|
||||||
Completed,
|
|
||||||
DisplayedLate,
|
|
||||||
Dropped,
|
|
||||||
Flushed,
|
|
||||||
Unknown
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOConfig
|
|
||||||
{
|
|
||||||
VideoFormatSelection videoModes;
|
|
||||||
bool externalKeyingEnabled = false;
|
|
||||||
bool preferTenBit = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOState
|
|
||||||
{
|
|
||||||
FrameSize inputFrameSize;
|
|
||||||
FrameSize outputFrameSize;
|
|
||||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
unsigned inputFrameRowBytes = 0;
|
|
||||||
unsigned outputFrameRowBytes = 0;
|
|
||||||
unsigned captureTextureWidth = 0;
|
|
||||||
unsigned outputPackTextureWidth = 0;
|
|
||||||
std::string inputDisplayModeName = "1080p59.94";
|
|
||||||
std::string outputDisplayModeName = "1080p59.94";
|
|
||||||
std::string outputModelName;
|
|
||||||
std::string statusMessage;
|
|
||||||
std::string formatStatusMessage;
|
|
||||||
bool hasInputDevice = false;
|
|
||||||
bool hasInputSource = false;
|
|
||||||
bool supportsInternalKeying = false;
|
|
||||||
bool supportsExternalKeying = false;
|
|
||||||
bool keyerInterfaceAvailable = false;
|
|
||||||
bool externalKeyingActive = false;
|
|
||||||
double frameBudgetMilliseconds = 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOFrame
|
|
||||||
{
|
|
||||||
void* bytes = nullptr;
|
|
||||||
long rowBytes = 0;
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
||||||
bool hasNoInputSource = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOOutputFrame
|
|
||||||
{
|
|
||||||
void* bytes = nullptr;
|
|
||||||
long rowBytes = 0;
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
|
||||||
void* nativeFrame = nullptr;
|
|
||||||
void* nativeBuffer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOCompletion
|
|
||||||
{
|
|
||||||
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOScheduleTime
|
|
||||||
{
|
|
||||||
int64_t streamTime = 0;
|
|
||||||
int64_t duration = 0;
|
|
||||||
int64_t timeScale = 0;
|
|
||||||
uint64_t frameIndex = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class VideoIODevice
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
|
|
||||||
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
|
||||||
|
|
||||||
virtual ~VideoIODevice() = default;
|
|
||||||
virtual void ReleaseResources() = 0;
|
|
||||||
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
|
||||||
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
|
||||||
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
|
||||||
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
|
||||||
virtual bool Start() = 0;
|
|
||||||
virtual bool Stop() = 0;
|
|
||||||
virtual const VideoIOState& State() const = 0;
|
|
||||||
virtual VideoIOState& MutableState() = 0;
|
|
||||||
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
|
||||||
virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0;
|
|
||||||
|
|
||||||
bool HasInputDevice() const { return State().hasInputDevice; }
|
|
||||||
bool HasInputSource() const { return State().hasInputSource; }
|
|
||||||
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
|
|
||||||
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
|
|
||||||
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
|
|
||||||
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
|
|
||||||
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
|
|
||||||
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
|
|
||||||
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
|
|
||||||
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
|
|
||||||
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
|
|
||||||
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
|
|
||||||
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
|
|
||||||
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
|
|
||||||
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
|
|
||||||
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
|
|
||||||
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
|
||||||
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
|
||||||
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
|
||||||
const std::string& OutputModelName() const { return State().outputModelName; }
|
|
||||||
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
|
|
||||||
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
|
|
||||||
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
|
|
||||||
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
|
||||||
const std::string& StatusMessage() const { return State().statusMessage; }
|
|
||||||
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
|
||||||
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#include "VideoPlayoutScheduler.h"
|
|
||||||
|
|
||||||
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
|
||||||
{
|
|
||||||
mFrameDuration = frameDuration;
|
|
||||||
mTimeScale = timeScale;
|
|
||||||
Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoPlayoutScheduler::Reset()
|
|
||||||
{
|
|
||||||
mScheduledFrameIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
|
||||||
{
|
|
||||||
VideoIOScheduleTime time;
|
|
||||||
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
|
|
||||||
time.duration = mFrameDuration;
|
|
||||||
time.timeScale = mTimeScale;
|
|
||||||
time.frameIndex = mScheduledFrameIndex;
|
|
||||||
++mScheduledFrameIndex;
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
|
|
||||||
{
|
|
||||||
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
|
|
||||||
mScheduledFrameIndex += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
|
||||||
{
|
|
||||||
return mTimeScale != 0
|
|
||||||
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
|
||||||
: 0.0;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
class VideoPlayoutScheduler
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void Configure(int64_t frameDuration, int64_t timeScale);
|
|
||||||
void Reset();
|
|
||||||
VideoIOScheduleTime NextScheduleTime();
|
|
||||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
|
||||||
double FrameBudgetMilliseconds() const;
|
|
||||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
|
||||||
int64_t TimeScale() const { return mTimeScale; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
int64_t mFrameDuration = 0;
|
|
||||||
int64_t mTimeScale = 0;
|
|
||||||
uint64_t mScheduledFrameIndex = 0;
|
|
||||||
};
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
#include "DeckLinkFrameTransfer.h"
|
|
||||||
|
|
||||||
#include "DeckLinkSession.h"
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// DeckLink Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
|
|
||||||
m_pOwner(pOwner),
|
|
||||||
mRefCount(1)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::QueryInterface(REFIID, LPVOID* ppv)
|
|
||||||
{
|
|
||||||
*ppv = NULL;
|
|
||||||
return E_NOINTERFACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG CaptureDelegate::AddRef()
|
|
||||||
{
|
|
||||||
return InterlockedIncrement(&mRefCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG CaptureDelegate::Release()
|
|
||||||
{
|
|
||||||
int newCount = InterlockedDecrement(&mRefCount);
|
|
||||||
if (newCount == 0)
|
|
||||||
delete this;
|
|
||||||
return newCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket*)
|
|
||||||
{
|
|
||||||
if (!inputFrame)
|
|
||||||
{
|
|
||||||
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
|
|
||||||
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
|
||||||
{
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// DeckLink Playout Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
|
|
||||||
m_pOwner(pOwner),
|
|
||||||
mRefCount(1)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT PlayoutDelegate::QueryInterface(REFIID, LPVOID* ppv)
|
|
||||||
{
|
|
||||||
*ppv = NULL;
|
|
||||||
return E_NOINTERFACE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG PlayoutDelegate::AddRef()
|
|
||||||
{
|
|
||||||
return InterlockedIncrement(&mRefCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
ULONG PlayoutDelegate::Release()
|
|
||||||
{
|
|
||||||
int newCount = InterlockedDecrement(&mRefCount);
|
|
||||||
if (newCount == 0)
|
|
||||||
delete this;
|
|
||||||
return newCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
|
|
||||||
{
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case bmdOutputFrameDisplayedLate:
|
|
||||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Displayed Late\n");
|
|
||||||
break;
|
|
||||||
case bmdOutputFrameDropped:
|
|
||||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Dropped\n");
|
|
||||||
break;
|
|
||||||
case bmdOutputFrameCompleted:
|
|
||||||
case bmdOutputFrameFlushed:
|
|
||||||
// Don't log bmdOutputFrameFlushed result since it is expected when Stop() is called
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT PlayoutDelegate::ScheduledPlaybackHasStopped()
|
|
||||||
{
|
|
||||||
return S_OK;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
#include "DeckLinkAPI_h.h"
|
|
||||||
|
|
||||||
class DeckLinkSession;
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Capture Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class CaptureDelegate : public IDeckLinkInputCallback
|
|
||||||
{
|
|
||||||
DeckLinkSession* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
CaptureDelegate(DeckLinkSession* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////
|
|
||||||
// Render Delegate Class
|
|
||||||
////////////////////////////////////////////
|
|
||||||
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
|
|
||||||
{
|
|
||||||
DeckLinkSession* m_pOwner;
|
|
||||||
LONG mRefCount;
|
|
||||||
|
|
||||||
public:
|
|
||||||
PlayoutDelegate(DeckLinkSession* pOwner);
|
|
||||||
|
|
||||||
// IUnknown needs only a dummy implementation
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
|
|
||||||
virtual ULONG STDMETHODCALLTYPE AddRef();
|
|
||||||
virtual ULONG STDMETHODCALLTYPE Release();
|
|
||||||
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
|
||||||
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped();
|
|
||||||
};
|
|
||||||
@@ -1,15 +1,55 @@
|
|||||||
{
|
{
|
||||||
"shaderLibrary": "shaders",
|
"$schema": "./runtime-host.schema.json",
|
||||||
"serverPort": 8080,
|
"autoReload": true,
|
||||||
|
"input": {
|
||||||
|
"backend": "ndi",
|
||||||
|
"device": "AIDENLAPTOP (Test Pattern)",
|
||||||
|
"frameRate": "59.94",
|
||||||
|
"resolution": "1080p"
|
||||||
|
},
|
||||||
|
"maxTemporalHistoryFrames": 12,
|
||||||
"oscBindAddress": "0.0.0.0",
|
"oscBindAddress": "0.0.0.0",
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"oscSmoothing": 0.18,
|
"oscSmoothing": 0.18,
|
||||||
"inputVideoFormat": "1080p",
|
"output": {
|
||||||
"inputFrameRate": "59.94",
|
"backend": "ndi",
|
||||||
"outputVideoFormat": "1080p",
|
"device": "Shader",
|
||||||
"outputFrameRate": "59.94",
|
"frameRate": "59.94",
|
||||||
"autoReload": true,
|
"keying": {
|
||||||
"maxTemporalHistoryFrames": 12,
|
"alphaRequired": false,
|
||||||
"previewFps": 30,
|
"external": false
|
||||||
"enableExternalKeying": true
|
},
|
||||||
|
"pixelFormat": "auto",
|
||||||
|
"resolution": "1080p"
|
||||||
|
},
|
||||||
|
"windowOutput": {
|
||||||
|
"fullscreen": true,
|
||||||
|
"borderless": true,
|
||||||
|
"display": "default",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"width": 0,
|
||||||
|
"height": 0,
|
||||||
|
"vsync": true,
|
||||||
|
"allowTearing": false
|
||||||
|
},
|
||||||
|
"colorPipeline": {
|
||||||
|
"ocioEnabled": false,
|
||||||
|
"ocioConfig": "",
|
||||||
|
"inputColorSpace": "scene_linear",
|
||||||
|
"workingColorSpace": "scene_linear",
|
||||||
|
"outputColorSpace": "rec709",
|
||||||
|
"display": "sRGB",
|
||||||
|
"view": "Rec.709",
|
||||||
|
"look": "",
|
||||||
|
"exposure": 0,
|
||||||
|
"gamma": 1,
|
||||||
|
"workingFormat": "rgba16f",
|
||||||
|
"linearWorkingSpace": true
|
||||||
|
},
|
||||||
|
"previewEnabled": true,
|
||||||
|
"previewFps": 59.94,
|
||||||
|
"runtimeShaderId": "",
|
||||||
|
"serverPort": 8080,
|
||||||
|
"shaderLibrary": "shaders"
|
||||||
}
|
}
|
||||||
|
|||||||
361
config/runtime-host.schema.json
Normal file
361
config/runtime-host.schema.json
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://render-cadence.local/schemas/runtime-host.schema.json",
|
||||||
|
"title": "Render Cadence Runtime Host Configuration",
|
||||||
|
"description": "Startup configuration for the Render Cadence native host. This schema documents the settings currently read by AppConfigProvider.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Editor-only schema reference. The native host ignores this field."
|
||||||
|
},
|
||||||
|
"shaderLibrary": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "shaders",
|
||||||
|
"description": "Path to the shader package library directory. Relative paths are resolved from the process/repo working location."
|
||||||
|
},
|
||||||
|
"serverPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"default": 8080,
|
||||||
|
"description": "Preferred HTTP control server port."
|
||||||
|
},
|
||||||
|
"oscBindAddress": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "0.0.0.0",
|
||||||
|
"description": "OSC bind address reserved for the control surface. The current native host reports this through an OSC status stub but does not start the UDP listener yet."
|
||||||
|
},
|
||||||
|
"oscPort": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 65535,
|
||||||
|
"default": 9000,
|
||||||
|
"description": "OSC UDP port reserved for the control surface. Use 0 to mark the OSC stub disabled."
|
||||||
|
},
|
||||||
|
"oscSmoothing": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0.18,
|
||||||
|
"description": "Reserved OSC smoothing amount reported through the OSC status stub."
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"$ref": "#/$defs/input"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"$ref": "#/$defs/output"
|
||||||
|
},
|
||||||
|
"windowOutput": {
|
||||||
|
"$ref": "#/$defs/windowOutput"
|
||||||
|
},
|
||||||
|
"colorPipeline": {
|
||||||
|
"$ref": "#/$defs/colorPipeline"
|
||||||
|
},
|
||||||
|
"autoReload": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When true, the runtime control layer may automatically rescan/reload shader packages when requested by the app flow."
|
||||||
|
},
|
||||||
|
"maxTemporalHistoryFrames": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 12,
|
||||||
|
"description": "Maximum temporal history frames exposed to shaders that request history."
|
||||||
|
},
|
||||||
|
"previewEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Starts the optional preview window on its own thread."
|
||||||
|
},
|
||||||
|
"previewFps": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"default": 59.94,
|
||||||
|
"description": "Target repaint rate for the optional preview window. It does not change render/output cadence."
|
||||||
|
},
|
||||||
|
"runtimeShaderId": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Optional startup shader package id used only when no saved runtime layer stack is restored. Leave empty to keep the simple fallback renderer until layers are added or restored."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"shaderLibrary",
|
||||||
|
"serverPort",
|
||||||
|
"input",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"inputBackend": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decklink",
|
||||||
|
"ndi",
|
||||||
|
"window",
|
||||||
|
"fullscreen",
|
||||||
|
"borderless",
|
||||||
|
"none",
|
||||||
|
"disabled",
|
||||||
|
"off"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputBackend": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decklink",
|
||||||
|
"ndi",
|
||||||
|
"none",
|
||||||
|
"disabled",
|
||||||
|
"off"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"720p",
|
||||||
|
"1080i",
|
||||||
|
"1080p",
|
||||||
|
"2160p",
|
||||||
|
"4k",
|
||||||
|
"uhd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"23.98",
|
||||||
|
"24",
|
||||||
|
"25",
|
||||||
|
"29.97",
|
||||||
|
"30",
|
||||||
|
"50",
|
||||||
|
"59.94",
|
||||||
|
"60"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Video input backend configuration. DeckLink uses the configured resolution/frameRate as a capture mode. NDI adapts to the received source shape and logs if it differs from these expected values.",
|
||||||
|
"properties": {
|
||||||
|
"backend": {
|
||||||
|
"$ref": "#/$defs/inputBackend",
|
||||||
|
"default": "decklink",
|
||||||
|
"description": "Input backend. Use 'decklink' for Blackmagic capture, 'ndi' for NDI receive, or 'none'/'disabled'/'off' for black fallback input."
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "default",
|
||||||
|
"description": "Input device/source selector. DeckLink currently uses 'default'. NDI accepts 'default'/'auto' for the first discovered source or an exact NDI source name."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"$ref": "#/$defs/resolution",
|
||||||
|
"default": "1080p",
|
||||||
|
"description": "Expected input resolution/mode. For NDI this is advisory only; received source dimensions are used."
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"$ref": "#/$defs/frameRate",
|
||||||
|
"default": "59.94",
|
||||||
|
"description": "Expected input frame rate. DeckLink uses this to resolve the hardware mode; NDI currently treats it as an expectation for logging/state."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backend",
|
||||||
|
"device",
|
||||||
|
"resolution",
|
||||||
|
"frameRate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Video output backend configuration.",
|
||||||
|
"properties": {
|
||||||
|
"backend": {
|
||||||
|
"$ref": "#/$defs/outputBackend",
|
||||||
|
"default": "decklink",
|
||||||
|
"description": "Output backend. Use 'decklink' for Blackmagic playout, 'ndi' for NDI send, or 'none'/'disabled'/'off' to disable scheduled output."
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "default",
|
||||||
|
"description": "Output device selector. DeckLink currently uses the first compatible/default output device. NDI uses this as the advertised sender name; 'default' becomes 'Render Cadence'."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"$ref": "#/$defs/resolution",
|
||||||
|
"default": "1080p",
|
||||||
|
"description": "Output render and video mode resolution."
|
||||||
|
},
|
||||||
|
"frameRate": {
|
||||||
|
"$ref": "#/$defs/frameRate",
|
||||||
|
"default": "59.94",
|
||||||
|
"description": "Output render cadence and video mode frame rate."
|
||||||
|
},
|
||||||
|
"pixelFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"auto",
|
||||||
|
"bgra8",
|
||||||
|
"uyvy8"
|
||||||
|
],
|
||||||
|
"default": "auto",
|
||||||
|
"description": "Requested system-memory output pixel format. Auto uses UYVY8 for DeckLink/NDI unless Output alpha is enabled, then BGRA8."
|
||||||
|
},
|
||||||
|
"keying": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Output alpha options. The control UI exposes these as a single Output alpha control.",
|
||||||
|
"properties": {
|
||||||
|
"external": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "DeckLink external-keyer backing field. The control UI only writes this true for DeckLink output."
|
||||||
|
},
|
||||||
|
"alphaRequired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"external",
|
||||||
|
"alphaRequired"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backend",
|
||||||
|
"device",
|
||||||
|
"resolution",
|
||||||
|
"frameRate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"windowOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Reserved settings for a future full-resolution/full-frame-rate borderless or fullscreen video output backend. These are not used by the preview window.",
|
||||||
|
"properties": {
|
||||||
|
"fullscreen": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When the future window backend is implemented, request exclusive/fullscreen-style presentation."
|
||||||
|
},
|
||||||
|
"borderless": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When true, the future window backend should create a borderless output window."
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "default",
|
||||||
|
"description": "Display/monitor selector for the future window backend."
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Window X position used when fullscreen is false or when a backend needs explicit placement."
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Window Y position used when fullscreen is false or when a backend needs explicit placement."
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0,
|
||||||
|
"description": "Optional window width override. Zero means use output resolution."
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 0,
|
||||||
|
"description": "Optional window height override. Zero means use output resolution."
|
||||||
|
},
|
||||||
|
"vsync": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Presentation sync request for the future window backend."
|
||||||
|
},
|
||||||
|
"allowTearing": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Allow tearing for the future window backend when the graphics API and OS support it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colorPipeline": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Reserved color-management settings for a future OCIO-backed linear RGBA16F render pipeline.",
|
||||||
|
"properties": {
|
||||||
|
"ocioEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Enable future OpenColorIO transforms. Currently parsed and reported only."
|
||||||
|
},
|
||||||
|
"ocioConfig": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Path to an OCIO config file or package-relative config identifier."
|
||||||
|
},
|
||||||
|
"inputColorSpace": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "scene_linear",
|
||||||
|
"description": "Input/video color space to convert from at the input edge."
|
||||||
|
},
|
||||||
|
"workingColorSpace": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "scene_linear",
|
||||||
|
"description": "Linear working space shader packages should be able to assume."
|
||||||
|
},
|
||||||
|
"outputColorSpace": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "rec709",
|
||||||
|
"description": "Output transform target color space for scheduled video output."
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "sRGB",
|
||||||
|
"description": "OCIO display name for preview/window/display transforms."
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Rec.709",
|
||||||
|
"description": "OCIO view name for preview/window/display transforms."
|
||||||
|
},
|
||||||
|
"look": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"description": "Optional OCIO look name."
|
||||||
|
},
|
||||||
|
"exposure": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"description": "Reserved display transform exposure adjustment."
|
||||||
|
},
|
||||||
|
"gamma": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"default": 1,
|
||||||
|
"description": "Reserved display transform gamma adjustment."
|
||||||
|
},
|
||||||
|
"workingFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"rgba16f",
|
||||||
|
"rgba32f"
|
||||||
|
],
|
||||||
|
"default": "rgba16f",
|
||||||
|
"description": "Future render target/intermediate working format."
|
||||||
|
},
|
||||||
|
"linearWorkingSpace": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Documents the intended linear-light working pipeline."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
# Architecture Resilience Review
|
|
||||||
|
|
||||||
This note summarizes the main architectural improvements that would make the app more resilient during live use, especially around timing isolation, failure isolation, and recoverability.
|
|
||||||
|
|
||||||
Phase checklist:
|
|
||||||
|
|
||||||
- [ ] Define subsystem boundaries and target architecture
|
|
||||||
- [ ] Introduce an internal event model
|
|
||||||
- [ ] Split `RuntimeHost`
|
|
||||||
- [ ] Make the render thread the sole GL owner
|
|
||||||
- [ ] Refactor live state layering into an explicit composition model
|
|
||||||
- [ ] Move persistence onto a background snapshot writer
|
|
||||||
- [ ] Make DeckLink/backend lifecycle explicit with a state machine
|
|
||||||
- [ ] Add structured health, telemetry, and operational reporting
|
|
||||||
|
|
||||||
## Timing Review
|
|
||||||
|
|
||||||
The recent OSC work removed several control-path stalls, but the app still has a few deeper timing characteristics that matter for live resilience:
|
|
||||||
|
|
||||||
- output playout is still effectively render-on-demand from the DeckLink completion callback
|
|
||||||
- output buffering and preroll are now larger, but the buffering model is still static and only loosely related to actual render cost
|
|
||||||
- GPU readback is partly asynchronous, but the fallback path still returns to synchronous readback on any miss
|
|
||||||
- preview presentation is still tied to the playout render path
|
|
||||||
- background service timing still relies on coarse polling sleeps
|
|
||||||
|
|
||||||
Those points are important because they affect not just average performance, but how the app behaves under brief spikes, device jitter, or load bursts.
|
|
||||||
|
|
||||||
## Key Findings
|
|
||||||
|
|
||||||
### 1. `RuntimeHost` is carrying too many responsibilities
|
|
||||||
|
|
||||||
`RuntimeHost` currently acts as:
|
|
||||||
|
|
||||||
- config store
|
|
||||||
- persistent state store
|
|
||||||
- live parameter/state authority
|
|
||||||
- shader package registry owner
|
|
||||||
- status/telemetry sink
|
|
||||||
- control mutation entrypoint
|
|
||||||
|
|
||||||
That makes it a single contention and failure domain. It is also why OSC and render timing issues repeatedly surfaced around shared state access.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- split persisted config/state from live render-facing state
|
|
||||||
- separate status/telemetry updates from control mutation paths
|
|
||||||
- make render consume snapshots rather than sharing a large mutable authority object
|
|
||||||
|
|
||||||
### 2. OpenGL ownership is still centralized behind one shared lock
|
|
||||||
|
|
||||||
Even after recent timing improvements, preview, input upload, and playout rendering still rely on one shared GL context protected by one `CRITICAL_SECTION`.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:93)
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:253)
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:70)
|
|
||||||
|
|
||||||
This is still a central choke point and limits timing isolation.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- use one dedicated render thread as the sole GL owner
|
|
||||||
- have input/output/control threads queue work instead of performing GL work directly
|
|
||||||
- remove ad hoc GL use from callback threads
|
|
||||||
|
|
||||||
### 3. Control flow is spread across polling and shared-memory patterns
|
|
||||||
|
|
||||||
`RuntimeServices` currently mixes:
|
|
||||||
|
|
||||||
- file polling
|
|
||||||
- deferred OSC commit handling
|
|
||||||
- control service orchestration
|
|
||||||
|
|
||||||
OSC ingest, overlay application, and host sync are distributed across several components.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:178)
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- introduce a small internal event pipeline or message bus
|
|
||||||
- use typed events for OSC, reloads, persistence requests, and status changes
|
|
||||||
- make timing ownership explicit per subsystem
|
|
||||||
|
|
||||||
Example event types:
|
|
||||||
|
|
||||||
- `OscParameterTargeted`
|
|
||||||
- `RenderOverlaySettled`
|
|
||||||
- `PersistStateRequested`
|
|
||||||
- `ShaderReloadRequested`
|
|
||||||
- `DeckLinkStatusChanged`
|
|
||||||
|
|
||||||
### 4. Error handling is still heavily UI-coupled
|
|
||||||
|
|
||||||
Failures are often surfaced via `MessageBoxA`, while background services mainly log with `OutputDebugStringA`.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:314)
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:478)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:205)
|
|
||||||
|
|
||||||
This is not ideal for a live system where modal dialogs and silent debug logging are both poor operational behavior.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- introduce structured in-app error reporting
|
|
||||||
- define severity levels and counters
|
|
||||||
- prefer degraded runtime states over modal failure handling where possible
|
|
||||||
- add a rolling log file for operational troubleshooting
|
|
||||||
|
|
||||||
### 5. Live OSC overlay and persisted state are still separate concepts without a formal model
|
|
||||||
|
|
||||||
The current design works better now, but it still relies on hand-managed reconciliation between:
|
|
||||||
|
|
||||||
- persisted parameter state in `RuntimeHost`
|
|
||||||
- transient OSC overlay state in `OpenGLComposite`
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:66)
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
Formalize three layers of state:
|
|
||||||
|
|
||||||
- base persisted state
|
|
||||||
- operator/UI committed state
|
|
||||||
- transient live automation overlay
|
|
||||||
|
|
||||||
Then render can always resolve:
|
|
||||||
|
|
||||||
- `final = base + committed + transient`
|
|
||||||
|
|
||||||
That avoids special-case sync behavior becoming scattered across the code.
|
|
||||||
|
|
||||||
### 6. DeckLink lifecycle could be modeled more explicitly
|
|
||||||
|
|
||||||
`DeckLinkSession` has a number of imperative calls, but startup, preroll, running, degraded, and stopped are not represented as an explicit state machine.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:17)
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- introduce explicit session states
|
|
||||||
- define allowed transitions
|
|
||||||
- centralize recovery behavior
|
|
||||||
- make shutdown ordering and degraded-mode behavior more predictable
|
|
||||||
|
|
||||||
Timing-specific additions:
|
|
||||||
|
|
||||||
- separate "device callback received" from "render the next output frame" so output cadence is not driven directly by the completion callback thread
|
|
||||||
- make playout headroom configurable and adaptive instead of using a fixed compile-time preroll count
|
|
||||||
- track an explicit backend health state such as `running-steady`, `catching-up`, `late`, and `dropping`
|
|
||||||
|
|
||||||
Relevant timing code:
|
|
||||||
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:420)
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:487)
|
|
||||||
- [VideoPlayoutScheduler.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp:26)
|
|
||||||
|
|
||||||
Why this matters:
|
|
||||||
|
|
||||||
- `PlayoutFrameCompleted()` currently begins an output frame, takes the shared GL path, renders, reads back, and schedules the next frame in one callback-driven flow.
|
|
||||||
- `VideoPlayoutScheduler::AccountForCompletionResult()` currently reacts to both late and dropped frames by blindly advancing the schedule index by `2`, which is simple but not especially robust.
|
|
||||||
- `kPrerollFrameCount` is now `12`, but `DeckLinkSession::ConfigureOutput()` still creates a fixed pool of `10` mutable output frames. That mismatch suggests the buffering model is not being sized from one coherent source of truth.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- move playout to a producer/consumer model where a render worker fills output buffers ahead of the DeckLink callback
|
|
||||||
- define buffer-pool sizing from one policy object, for example: preroll depth, minimum spare buffers, and allowed catch-up depth
|
|
||||||
- replace fixed "skip two frames" recovery with measured lag accounting based on actual scheduled-versus-completed position
|
|
||||||
- expose playout latency as a runtime setting or policy, rather than burying it in a constant
|
|
||||||
|
|
||||||
### 6a. The current playout timing model is still callback-coupled
|
|
||||||
|
|
||||||
The app now has more headroom, but the next output frame is still produced directly in the scheduled-frame completion callback path.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
|
|
||||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:53)
|
|
||||||
|
|
||||||
That means the completion callback is currently responsible for:
|
|
||||||
|
|
||||||
- frame pacing accounting
|
|
||||||
- acquiring the next output buffer
|
|
||||||
- taking the GL critical section
|
|
||||||
- rendering the composite
|
|
||||||
- performing output readback
|
|
||||||
- scheduling the next frame
|
|
||||||
|
|
||||||
This works when the app is comfortably within budget, but it makes deadline misses much harder to absorb gracefully.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- make the DeckLink callback a lightweight notifier
|
|
||||||
- have a dedicated playout worker or render worker keep an ahead-of-time queue of ready output frames
|
|
||||||
- treat callback time as control-plane time, not render time
|
|
||||||
|
|
||||||
### 6b. A producer/consumer playout model would be a better long-term fit
|
|
||||||
|
|
||||||
The stronger architecture for this app is:
|
|
||||||
|
|
||||||
- a render scheduler or dedicated render thread runs at the configured video cadence
|
|
||||||
- rendering produces completed output frames ahead of need
|
|
||||||
- those frames are placed into a bounded queue or ring buffer
|
|
||||||
- the DeckLink side consumes already-prepared frames when callbacks indicate they are needed
|
|
||||||
|
|
||||||
That is a better fit than callback-driven rendering because it separates:
|
|
||||||
|
|
||||||
- render timing
|
|
||||||
- GL ownership
|
|
||||||
- output-device timing
|
|
||||||
- latency policy
|
|
||||||
|
|
||||||
In that model:
|
|
||||||
|
|
||||||
- render is the producer
|
|
||||||
- DeckLink is the timing consumer
|
|
||||||
- the queue between them becomes the main place to manage latency versus resilience
|
|
||||||
|
|
||||||
Why this is preferable:
|
|
||||||
|
|
||||||
- brief callback jitter is less likely to become a visible dropped frame
|
|
||||||
- render spikes can be absorbed by queue headroom instead of immediately missing output deadlines
|
|
||||||
- latency becomes an explicit policy choice rather than an incidental side effect of callback timing
|
|
||||||
- queue depth, underruns, stale-frame reuse, and catch-up behavior become measurable and tunable
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- move toward a bounded producer/consumer playout queue
|
|
||||||
- make queue depth and target headroom runtime policy, not compile-time constants
|
|
||||||
- define explicit underrun behavior, for example:
|
|
||||||
- reuse newest completed frame
|
|
||||||
- reuse last scheduled frame
|
|
||||||
- output black or degraded frame
|
|
||||||
- keep DeckLink callbacks limited to dequeue/schedule/accounting work wherever possible
|
|
||||||
|
|
||||||
### 7. Persistence should be more asynchronous and debounced
|
|
||||||
|
|
||||||
`SavePersistentState()` is still called directly from many update paths.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1841)
|
|
||||||
|
|
||||||
Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from:
|
|
||||||
|
|
||||||
- a debounced persistence queue
|
|
||||||
- atomic write-behind snapshots
|
|
||||||
- clear separation between state mutation and disk flush
|
|
||||||
|
|
||||||
This improves both resilience and timing safety.
|
|
||||||
|
|
||||||
### 8. Telemetry is useful, but still too coarse
|
|
||||||
|
|
||||||
The app already records render timing and playout pacing, which is a good foundation.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24)
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:24)
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
Add lightweight tracing for:
|
|
||||||
|
|
||||||
- input callback latency
|
|
||||||
- input upload skip count
|
|
||||||
- GL lock wait time
|
|
||||||
- render queue depth
|
|
||||||
- render time
|
|
||||||
- pass build/compile latency
|
|
||||||
- readback time
|
|
||||||
- output scheduling lag
|
|
||||||
- output queue depth
|
|
||||||
- preroll depth versus spare-buffer depth
|
|
||||||
- preview present cost and skipped-preview count
|
|
||||||
- control queue depth
|
|
||||||
- `RuntimeHost` lock contention
|
|
||||||
|
|
||||||
That would make future tuning and failure diagnosis much easier.
|
|
||||||
|
|
||||||
Timing-specific observations from the current code:
|
|
||||||
|
|
||||||
- render time is captured as one total number in [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24), but not split into draw, pack, readback wait, readback copy, or preview present
|
|
||||||
- frame pacing stats are recorded in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:17), but there is no explicit visibility into how much queued playout headroom remains
|
|
||||||
- input uploads are intentionally skipped when the GL bridge is busy in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:60), but the app does not currently surface how often that is happening
|
|
||||||
|
|
||||||
### 8a. Preview and playout are still too close together
|
|
||||||
|
|
||||||
The desktop preview is rate-limited, but still presented from inside the render pipeline path.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54)
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:235)
|
|
||||||
|
|
||||||
This means preview presentation can still consume time on the same path that is trying to meet output deadlines.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- treat preview as best-effort and entirely subordinate to playout
|
|
||||||
- move preview present to a separate presentation schedule fed from the latest completed render
|
|
||||||
- record preview skips and preview present cost independently from playout timing
|
|
||||||
|
|
||||||
### 8b. Readback is improved, but still not fully deadline-safe
|
|
||||||
|
|
||||||
The async readback path is a good step, but the miss path still falls back to synchronous `glReadPixels()` and then flushes the async pipeline.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:150)
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:228)
|
|
||||||
|
|
||||||
That means a single late GPU fence can push the app back onto the most timing-sensitive path exactly when it is already under pressure.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- increase readback instrumentation before changing policy again
|
|
||||||
- consider deeper readback buffering or a true stale-frame reuse policy instead of immediate synchronous fallback
|
|
||||||
- separate "freshest possible frame" policy from "never miss output deadline" policy and make that tradeoff explicit
|
|
||||||
|
|
||||||
### 8c. Background control and file-watch timing are still coarse
|
|
||||||
|
|
||||||
`RuntimeServices::PollLoop()` currently uses a `25 x Sleep(10)` loop, which gives it a coarse `~250 ms` cadence for file-watch polling and deferred OSC commit work.
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:245)
|
|
||||||
|
|
||||||
That is acceptable for non-critical background work, but it is still too blunt to be the long-term timing model for coordination-heavy runtime services.
|
|
||||||
|
|
||||||
Recommended direction:
|
|
||||||
|
|
||||||
- replace coarse sleep polling with waitable events or condition-variable driven wakeups where practical
|
|
||||||
- isolate truly background work from latency-sensitive control reconciliation
|
|
||||||
- add separate metrics for queue age, not just queue depth
|
|
||||||
|
|
||||||
## Phased Roadmap
|
|
||||||
|
|
||||||
This roadmap is ordered by architectural dependency rather than by “quick wins.” The goal is to move the app toward clearer ownership boundaries and safer live behavior without doing later work on top of foundations that are likely to change again.
|
|
||||||
|
|
||||||
### Phase 1. Define subsystem boundaries and target architecture
|
|
||||||
|
|
||||||
Before changing major internals, formalize the target responsibilities for each major part of the app.
|
|
||||||
|
|
||||||
Target split:
|
|
||||||
|
|
||||||
- `RuntimeStore`
|
|
||||||
- persisted config
|
|
||||||
- persisted layer stack
|
|
||||||
- preset persistence
|
|
||||||
- `RuntimeCoordinator`
|
|
||||||
- mutation validation and classification
|
|
||||||
- committed-live versus transient policy
|
|
||||||
- snapshot and persistence requests
|
|
||||||
- `RuntimeSnapshotProvider`
|
|
||||||
- render-facing immutable or near-immutable snapshots
|
|
||||||
- parameter values prepared for the render path
|
|
||||||
- `ControlServices`
|
|
||||||
- OSC ingress
|
|
||||||
- web control ingress
|
|
||||||
- reload/file-watch requests
|
|
||||||
- commit/persist requests
|
|
||||||
- `RenderEngine`
|
|
||||||
- sole owner of live GL rendering
|
|
||||||
- sole consumer of render snapshots plus transient overlays
|
|
||||||
- `VideoBackend`
|
|
||||||
- DeckLink input/output lifecycle
|
|
||||||
- pacing and scheduling
|
|
||||||
- `HealthTelemetry`
|
|
||||||
- logging
|
|
||||||
- counters
|
|
||||||
- timing traces
|
|
||||||
- degraded-state reporting
|
|
||||||
|
|
||||||
Why this phase comes first:
|
|
||||||
|
|
||||||
- it prevents later refactors from reintroducing responsibility overlap
|
|
||||||
- it gives names to the seams the later phases will build around
|
|
||||||
- it reduces the risk of replacing one monolith with several poorly-defined ones
|
|
||||||
|
|
||||||
Suggested deliverables:
|
|
||||||
|
|
||||||
- a short architecture diagram
|
|
||||||
- a responsibility table for each subsystem
|
|
||||||
- a list of allowed dependencies between subsystems
|
|
||||||
- a dedicated Phase 1 design note:
|
|
||||||
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
|
|
||||||
- a subsystem design bundle index:
|
|
||||||
- [docs/subsystems/README.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
|
|
||||||
|
|
||||||
### Phase 2. Introduce an internal event model
|
|
||||||
|
|
||||||
Once subsystem boundaries are defined, introduce a typed event pipeline between them. This should happen before large state splits so the app has a stable coordination model.
|
|
||||||
|
|
||||||
Example event families:
|
|
||||||
|
|
||||||
- control events
|
|
||||||
- `OscParameterTargeted`
|
|
||||||
- `UiParameterCommitted`
|
|
||||||
- `TriggerFired`
|
|
||||||
- runtime events
|
|
||||||
- `ShaderReloadRequested`
|
|
||||||
- `PackagesRescanned`
|
|
||||||
- `PersistStateRequested`
|
|
||||||
- render events
|
|
||||||
- `OverlayApplied`
|
|
||||||
- `OverlaySettled`
|
|
||||||
- `SnapshotPublished`
|
|
||||||
- backend events
|
|
||||||
- `InputSignalChanged`
|
|
||||||
- `OutputLateFrameDetected`
|
|
||||||
- `OutputDroppedFrameDetected`
|
|
||||||
- health events
|
|
||||||
- `SubsystemWarningRaised`
|
|
||||||
- `SubsystemRecovered`
|
|
||||||
|
|
||||||
Why this phase comes second:
|
|
||||||
|
|
||||||
- it provides a migration path away from direct cross-calls
|
|
||||||
- it makes ownership explicit before data structures are split apart
|
|
||||||
- it lets you move one subsystem at a time without losing coordination
|
|
||||||
|
|
||||||
Suggested outcome:
|
|
||||||
|
|
||||||
- the app stops relying on “shared object plus mutex plus polling” as the default coordination pattern
|
|
||||||
|
|
||||||
### Phase 3. Split `RuntimeHost` into persistent state, render snapshot state, and service-facing coordination
|
|
||||||
|
|
||||||
After the event model exists, break apart `RuntimeHost`.
|
|
||||||
|
|
||||||
Recommended split:
|
|
||||||
|
|
||||||
- `RuntimeStore`
|
|
||||||
- owns config and saved layer data
|
|
||||||
- handles serialization/deserialization
|
|
||||||
- does not sit on the live render path
|
|
||||||
- `RuntimeCoordinator`
|
|
||||||
- resolves control actions
|
|
||||||
- validates mutations
|
|
||||||
- publishes new snapshots
|
|
||||||
- bridges events between services and render
|
|
||||||
- `RuntimeSnapshotProvider`
|
|
||||||
- publishes immutable render snapshots
|
|
||||||
- avoids large shared mutable structures on the render path
|
|
||||||
|
|
||||||
Why this phase comes before render-thread isolation:
|
|
||||||
|
|
||||||
- render isolation is easier when the render thread consumes clean snapshots instead of a large mutable host object
|
|
||||||
- otherwise the GL refactor still drags along too much shared state complexity
|
|
||||||
|
|
||||||
Primary design rule:
|
|
||||||
|
|
||||||
- render should read snapshots
|
|
||||||
- persistence should write stored state
|
|
||||||
- services should request mutations through the coordinator
|
|
||||||
|
|
||||||
### Phase 4. Make the render thread the sole GL owner
|
|
||||||
|
|
||||||
With state and coordination cleaner, move to a dedicated render-thread model.
|
|
||||||
|
|
||||||
Target behavior:
|
|
||||||
|
|
||||||
- one thread owns the GL context
|
|
||||||
- input callbacks never perform GL work directly
|
|
||||||
- output callbacks never perform GL work directly
|
|
||||||
- preview presentation, texture upload, render passes, readback, and output pack work are all issued by the render thread
|
|
||||||
|
|
||||||
Other threads should only:
|
|
||||||
|
|
||||||
- enqueue new video frames
|
|
||||||
- enqueue control updates
|
|
||||||
- enqueue backend events
|
|
||||||
- consume produced output buffers
|
|
||||||
|
|
||||||
Why this phase comes here:
|
|
||||||
|
|
||||||
- it is much safer once state access and control coordination are no longer centered on `RuntimeHost`
|
|
||||||
- it avoids coupling the render-thread refactor to storage and service refactors at the same time
|
|
||||||
|
|
||||||
Expected benefits:
|
|
||||||
|
|
||||||
- less cross-thread GL contention
|
|
||||||
- easier timing reasoning
|
|
||||||
- much lower risk of callback-driven stalls
|
|
||||||
- a clearer foundation for future GPU pipeline work
|
|
||||||
|
|
||||||
### Phase 5. Refactor live state layering into an explicit composition model
|
|
||||||
|
|
||||||
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
|
|
||||||
|
|
||||||
Recommended layers:
|
|
||||||
|
|
||||||
- base persisted state
|
|
||||||
- operator-committed live state
|
|
||||||
- transient automation overlay
|
|
||||||
|
|
||||||
Render should derive final values from a clear composition rule such as:
|
|
||||||
|
|
||||||
- `final = base + committed + transient`
|
|
||||||
|
|
||||||
Why this phase follows render isolation:
|
|
||||||
|
|
||||||
- once render owns snapshot consumption, it becomes much easier to cleanly evaluate layered state without touching persistence or control services
|
|
||||||
- it turns the current OSC overlay behavior into a first-class model instead of an implementation detail
|
|
||||||
|
|
||||||
Expected benefits:
|
|
||||||
|
|
||||||
- fewer one-off sync rules
|
|
||||||
- clearer behavior for OSC, UI changes, and automation
|
|
||||||
- easier future expansion to presets, cues, or timed transitions
|
|
||||||
|
|
||||||
### Phase 6. Move persistence onto a background snapshot writer
|
|
||||||
|
|
||||||
After the state model is explicit, persistence should become a background concern rather than a synchronous side effect of mutations.
|
|
||||||
|
|
||||||
Target behavior:
|
|
||||||
|
|
||||||
- mutations update authoritative in-memory stored state
|
|
||||||
- persistence requests are queued
|
|
||||||
- disk writes are debounced and coalesced
|
|
||||||
- writes are atomic and versioned where practical
|
|
||||||
|
|
||||||
Why this phase comes after state splitting:
|
|
||||||
|
|
||||||
- otherwise persistence logic will need to be rewritten twice
|
|
||||||
- it should operate on the new `RuntimeStore` model, not on the current mixed-responsibility object
|
|
||||||
|
|
||||||
Expected benefits:
|
|
||||||
|
|
||||||
- less timing interference
|
|
||||||
- better corruption resistance
|
|
||||||
- cleaner restart/recovery semantics
|
|
||||||
|
|
||||||
### Phase 7. Make DeckLink/backend lifecycle explicit with a state machine
|
|
||||||
|
|
||||||
Once the render and state layers are cleaner, refactor the video backend into an explicit lifecycle model.
|
|
||||||
|
|
||||||
Suggested states:
|
|
||||||
|
|
||||||
- uninitialized
|
|
||||||
- devices-discovered
|
|
||||||
- configured
|
|
||||||
- prerolling
|
|
||||||
- running
|
|
||||||
- degraded
|
|
||||||
- stopping
|
|
||||||
- stopped
|
|
||||||
- failed
|
|
||||||
|
|
||||||
Why this phase belongs here:
|
|
||||||
|
|
||||||
- the backend should integrate with the new event model
|
|
||||||
- degraded/recovery behavior will be easier once rendering and state coordination are already more deterministic
|
|
||||||
|
|
||||||
Expected benefits:
|
|
||||||
|
|
||||||
- safer startup/shutdown ordering
|
|
||||||
- clearer recovery behavior
|
|
||||||
- easier handling of missing input, dropped frames, or reconfiguration
|
|
||||||
- a clearer place to own playout headroom policy, output queue sizing, and late-frame recovery behavior
|
|
||||||
|
|
||||||
### Phase 8. Add structured health, telemetry, and operational reporting
|
|
||||||
|
|
||||||
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
|
|
||||||
|
|
||||||
Recommended coverage:
|
|
||||||
|
|
||||||
- render queue depth
|
|
||||||
- GL lock wait time, if any shared lock remains
|
|
||||||
- input callback latency
|
|
||||||
- input upload skip count
|
|
||||||
- output scheduling lag
|
|
||||||
- output queue depth and spare-buffer depth
|
|
||||||
- readback timing
|
|
||||||
- readback fence wait timing
|
|
||||||
- synchronous readback fallback count
|
|
||||||
- preview present timing and skipped-preview count
|
|
||||||
- snapshot publish frequency
|
|
||||||
- persistence queue depth
|
|
||||||
- event queue depth
|
|
||||||
- backend state transitions
|
|
||||||
- warning/error counters per subsystem
|
|
||||||
|
|
||||||
Also replace modal-only error handling with:
|
|
||||||
|
|
||||||
- structured in-app health state
|
|
||||||
- severity-based logging
|
|
||||||
- rolling log files
|
|
||||||
- operator-visible degraded-state messages
|
|
||||||
|
|
||||||
Why this phase comes last:
|
|
||||||
|
|
||||||
- it should instrument the architecture you intend to keep
|
|
||||||
- otherwise instrumentation work gets invalidated by the refactor
|
|
||||||
|
|
||||||
## Recommended Execution Order
|
|
||||||
|
|
||||||
If this is approached as a serious architecture program rather than opportunistic cleanup, the recommended order is:
|
|
||||||
|
|
||||||
1. Define subsystem boundaries and target architecture.
|
|
||||||
2. Introduce the internal event model.
|
|
||||||
3. Split `RuntimeHost`.
|
|
||||||
4. Make the render thread the sole GL owner.
|
|
||||||
5. Formalize live state layering and composition.
|
|
||||||
6. Move persistence to a background snapshot writer.
|
|
||||||
7. Refactor DeckLink/backend lifecycle into an explicit state machine.
|
|
||||||
8. Add structured telemetry, health reporting, and operational diagnostics.
|
|
||||||
|
|
||||||
## Why This Order Makes Sense
|
|
||||||
|
|
||||||
This order tries to avoid doing foundational work twice.
|
|
||||||
|
|
||||||
- The event model comes before major subsystem extraction so coordination patterns stabilize early.
|
|
||||||
- `RuntimeHost` is split before render isolation so the render thread does not inherit the current monolithic state model.
|
|
||||||
- Live state layering is formalized only after render ownership is clearer.
|
|
||||||
- Persistence is moved later so it can target the final state model rather than the current one.
|
|
||||||
- Telemetry is intentionally late so it instruments the architecture that survives the refactor.
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
The app is in a much better place than it was before the OSC timing work, but the main remaining architectural risk is still shared ownership. Too many responsibilities converge on `RuntimeHost` and the shared GL path. The most sensible path forward is:
|
|
||||||
|
|
||||||
1. define boundaries
|
|
||||||
2. establish an event model
|
|
||||||
3. split state ownership
|
|
||||||
4. isolate rendering
|
|
||||||
5. formalize layered live state
|
|
||||||
6. background persistence
|
|
||||||
7. explicit backend lifecycle
|
|
||||||
8. health and telemetry
|
|
||||||
|
|
||||||
That sequence gives each later phase a cleaner foundation than the current app has today.
|
|
||||||
198
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
198
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Current System Architecture
|
||||||
|
|
||||||
|
This document describes how the current `RenderCadenceCompositor` app works.
|
||||||
|
|
||||||
|
The implementation is cadence-first: the render/output path owns frame timing, while shader compilation, HTTP control, preview, persistence, and video I/O edges stay outside the render cadence loop wherever possible. The guardrails in [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) are the operating contract.
|
||||||
|
|
||||||
|
## Application Shape
|
||||||
|
|
||||||
|
The app is a native C++ OpenGL compositor with:
|
||||||
|
|
||||||
|
- optional DeckLink input
|
||||||
|
- optional DeckLink scheduled output
|
||||||
|
- a render-thread-owned OpenGL context
|
||||||
|
- a pluggable app-side runtime-content controller
|
||||||
|
- the default runtime Slang shader package stack from `shaders/`
|
||||||
|
- a local HTTP/WebSocket control server
|
||||||
|
- optional Win32/GDI preview from system-memory output frames
|
||||||
|
- background runtime-state persistence
|
||||||
|
- cadence telemetry and log output
|
||||||
|
|
||||||
|
Primary source areas:
|
||||||
|
|
||||||
|
- `src/app`: startup/shutdown orchestration, config loading, video backend factory, runtime-content controller boundary, and the default shader runtime-content controller
|
||||||
|
- `src/render`: cadence clock, input texture upload, render-content boundary, readback, and runtime GL support
|
||||||
|
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
|
||||||
|
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
|
||||||
|
- `src/frames`: system-memory frame exchange
|
||||||
|
- `src/video/core`: generic video IO edge contracts, mode descriptions, formats, and output scheduling thread
|
||||||
|
- `src/video/decklink`: current DeckLink input/output backend
|
||||||
|
- `src/video/playout`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers
|
||||||
|
- `src/video/legacy`: older backend pipeline pieces kept separate from the current app path
|
||||||
|
- `src/runtime/catalog`: supported shader catalog and package filtering
|
||||||
|
- `src/runtime/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
|
||||||
|
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
|
||||||
|
- `src/runtime/state`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
|
||||||
|
- `src/runtime/text`: MSDF/MTSDF font atlas build and CPU-side prepared text texture composition
|
||||||
|
- `src/control`: command parsing, HTTP/WebSocket transport helpers, OSC status stub, OpenAPI state JSON
|
||||||
|
- `src/app/RenderCadenceHttpRoutes.*`: this app's current HTTP endpoint map
|
||||||
|
- `src/preview`: optional non-consuming preview window
|
||||||
|
- `src/telemetry` and `src/logging`: runtime observation and logging
|
||||||
|
|
||||||
|
## Startup
|
||||||
|
|
||||||
|
Startup broadly proceeds as:
|
||||||
|
|
||||||
|
1. Load `config/runtime-host.json` through `AppConfigProvider`, then apply CLI overrides.
|
||||||
|
2. Initialize the active `IRuntimeContentController`. The checked-in app uses `ShaderRuntimeContentController`, which loads the supported shader catalog, starts runtime-state persistence, and tries to restore `runtime/runtime_state.json`.
|
||||||
|
3. If restore fails or no usable state exists, the shader controller falls back to the optional configured startup shader. The checked-in config leaves `runtimeShaderId` empty, so a fresh host keeps the simple fallback renderer.
|
||||||
|
4. Start the render thread.
|
||||||
|
5. Start the active runtime-content controller. The shader controller queues background Slang builds for every pending active layer.
|
||||||
|
6. Build a small completed-frame reserve.
|
||||||
|
7. Start optional preview, optional video output, telemetry, and HTTP control.
|
||||||
|
|
||||||
|
The runtime-state restore is intentionally app/control side work. The render thread does not read JSON, inspect the shader library, or decide what to compile.
|
||||||
|
|
||||||
|
## Runtime Content And Shader Layer State
|
||||||
|
|
||||||
|
`RenderCadenceApp` owns startup, video output, preview, telemetry, OSC status, and HTTP server lifetime. It does not own the Slang shader stack directly. Runtime content plugs in through `IRuntimeContentController`.
|
||||||
|
|
||||||
|
The checked-in implementation is `ShaderRuntimeContentController`. It wraps `RuntimeLayerController`, exposes shader catalog/layer JSON for `/api/state`, handles shader layer POST commands, and publishes render-ready shader layer snapshots to the render thread.
|
||||||
|
|
||||||
|
`RuntimeLayerController` owns the app-side layer model and coordinates:
|
||||||
|
|
||||||
|
- supported shader catalog loading
|
||||||
|
- layer add/remove/reorder/bypass/shader assignment
|
||||||
|
- parameter update/reset
|
||||||
|
- startup restore
|
||||||
|
- reload reconciliation
|
||||||
|
- background Slang builds
|
||||||
|
- render-layer publication
|
||||||
|
- runtime-state persistence requests
|
||||||
|
|
||||||
|
`RuntimeLayerModel` owns the in-memory active stack:
|
||||||
|
|
||||||
|
- layer id
|
||||||
|
- shader id and display name
|
||||||
|
- build state and message
|
||||||
|
- bypass state
|
||||||
|
- manifest parameter definitions
|
||||||
|
- optional shader-declared custom UI metadata
|
||||||
|
- current parameter values
|
||||||
|
- render-ready artifacts
|
||||||
|
|
||||||
|
The current durable runtime state is stored in `runtime/runtime_state.json`. It contains the active stack order, shader ids, bypass flags, and parameter values. On startup, valid saved layers are restored in order. Missing shader packages are skipped, invalid saved parameter values fall back to manifest defaults, and a missing or unusable file falls back to the optional configured startup shader.
|
||||||
|
|
||||||
|
Manual stack preset routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet. `runtime_state.json` is the supported latest-working-state mechanism.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
`RuntimeStatePersistenceWriter` performs debounced background writes to `runtime/runtime_state.json`.
|
||||||
|
|
||||||
|
Durable UI/API mutations request persistence after they are accepted:
|
||||||
|
|
||||||
|
- add/remove layer
|
||||||
|
- reorder layer
|
||||||
|
- set bypass
|
||||||
|
- set shader
|
||||||
|
- update parameter
|
||||||
|
- reset parameters
|
||||||
|
- reload compatibility refresh
|
||||||
|
|
||||||
|
The mutation path snapshots the current layer model and hands serialized state to the writer. File IO happens on the persistence worker, not on the render thread or cadence path. Shutdown flushes the latest pending snapshot.
|
||||||
|
|
||||||
|
OSC-driven changes are intentionally not part of this autosave path yet.
|
||||||
|
|
||||||
|
The host configuration editor is separate from runtime layer persistence. The UI reads active and saved startup config through `/api/config`, saves `config/runtime-host.json` through `/api/config/save`, and requests a native host restart through `/api/app/restart`. Render cadence, video input/output selection, resolution, frame rate, output pixel format, HTTP port, and preview settings are still startup-owned; they are not hot-swapped inside the cadence path.
|
||||||
|
|
||||||
|
## Shader Reload
|
||||||
|
|
||||||
|
`POST /api/reload` and the control UI reload button:
|
||||||
|
|
||||||
|
- rescan `shaders/`
|
||||||
|
- re-read manifests
|
||||||
|
- rebuild the supported shader catalog
|
||||||
|
- refresh optional shader custom UI metadata
|
||||||
|
- update active layer metadata and parameter definitions from changed manifests
|
||||||
|
- preserve compatible parameter values
|
||||||
|
- default new or incompatible parameter values
|
||||||
|
- queue recompilation for every catalog-valid layer in the active stack
|
||||||
|
|
||||||
|
Reload does not compile every package in the shader library. A package is compiled when it is part of the active layer stack. If an active layer references a shader that no longer exists, that layer is marked failed and skipped. Existing render output remains active where possible until replacement builds are ready.
|
||||||
|
|
||||||
|
`autoReload` is still exposed in config/state for compatibility, but automatic file watching is not currently wired.
|
||||||
|
|
||||||
|
## Render Ownership
|
||||||
|
|
||||||
|
The render thread owns the app OpenGL context during normal operation.
|
||||||
|
|
||||||
|
The render path consumes published render-layer snapshots. It does not:
|
||||||
|
|
||||||
|
- parse JSON
|
||||||
|
- scan files
|
||||||
|
- launch Slang
|
||||||
|
- run font atlas generation
|
||||||
|
- perform persistence
|
||||||
|
- handle HTTP or OSC
|
||||||
|
- call DeckLink discovery/setup APIs
|
||||||
|
|
||||||
|
When a runtime shader build completes, the default shader runtime-content controller publishes a render-layer artifact. The render thread forwards pending layer snapshots to the active render-content adapter. The default `RuntimeShaderRenderContent` owns the runtime scene, diffs the snapshot, and queues changed pass programs to the shared-context prepare worker. The render thread swaps in an already-prepared render plan at a frame boundary through that adapter.
|
||||||
|
|
||||||
|
## Video And Preview
|
||||||
|
|
||||||
|
Video input and output are optional edges. `input.backend` and `output.backend` select the concrete backend through the app-side backend factory. DeckLink and NDI are the current concrete backends, and `none` disables an edge. `input` and `output` also carry the device selector plus resolution/frame-rate settings. Configured video modes are represented in `src/video/core` and translated to backend-specific modes only inside the concrete edge.
|
||||||
|
|
||||||
|
The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
|
||||||
|
|
||||||
|
The output edge consumes completed system-memory frames from `SystemFrameExchange`. The render thread owns output pixel packing before readback: BGRA8 is read directly, and UYVY8 is packed on the GPU into a half-width RGBA8 target before async PBO readback. DeckLink and NDI output then schedule/send those completed CPU frames without invoking GL or converting pixels. If video output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
|
||||||
|
|
||||||
|
Runtime state exposes backend-neutral output telemetry through `videoOutput`. Portable fields such as `enabled`, `backend`, and `scheduleFailures` stay at that level; backend-specific counters live under `videoOutput.backendMetrics`.
|
||||||
|
|
||||||
|
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints BGRA8 directly, decodes UYVY8 only for preview presentation, and skips preview ticks instead of blocking the frame exchange.
|
||||||
|
|
||||||
|
Screenshot routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet.
|
||||||
|
|
||||||
|
## Control Surface
|
||||||
|
|
||||||
|
The HTTP server runs on its own thread. `HttpControlServer` owns socket lifetime, HTTP parsing, static asset helpers, OpenAPI/Swagger helper serving, and WebSocket state transport. `RenderCadenceHttpRoutes` owns this app's current endpoint map:
|
||||||
|
|
||||||
|
- UI assets
|
||||||
|
- shader package custom UI assets under `/shader-assets/{shaderId}/...`
|
||||||
|
- OpenAPI/Swagger docs
|
||||||
|
- `GET /api/state`
|
||||||
|
- `/ws` state updates
|
||||||
|
- layer mutation POST routes, dispatched to the active runtime-content controller
|
||||||
|
- `/api/reload`
|
||||||
|
|
||||||
|
Known but not implemented in the current native command path:
|
||||||
|
|
||||||
|
- `/api/layers/move`
|
||||||
|
- `/api/stack-presets/save`
|
||||||
|
- `/api/stack-presets/load`
|
||||||
|
- `/api/screenshot`
|
||||||
|
|
||||||
|
Unsupported routes return an action response with `ok: false`.
|
||||||
|
|
||||||
|
Forks can reuse the HTTP/WebSocket shell without keeping these endpoints by installing a different route callback.
|
||||||
|
|
||||||
|
`OscControlServer` is currently a lifecycle/status stub. It consumes startup OSC config, exposes configured/disabled/not-listening state through `/api/state`, and leaves UDP socket receive, OSC decode, and runtime dispatch unimplemented until that ingress boundary is built deliberately.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Native tests cover the main non-GL contracts:
|
||||||
|
|
||||||
|
- JSON parsing/serialization
|
||||||
|
- runtime parameter normalization
|
||||||
|
- shader package registry and Slang validation
|
||||||
|
- supported shader catalog
|
||||||
|
- runtime layer restore/reload behavior
|
||||||
|
- runtime-state persistence writer
|
||||||
|
- HTTP transport and app-route dispatch
|
||||||
|
- frame exchange and input mailbox behavior
|
||||||
|
- video format and scheduling helpers
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||||
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# OSC Control
|
# OSC Control
|
||||||
|
|
||||||
Video Shader Toys can listen for local OSC messages and map them onto shader layer parameters.
|
This is the intended OSC control contract, but OSC ingress is not wired in the current `RenderCadenceCompositor` native host yet. The native host has an OSC service stub that consumes config and reports status in `/api/state`; it does not open a UDP listener or dispatch messages yet. Use the REST layer parameter endpoints or the control UI for live parameter changes today.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -14,9 +14,7 @@ Set the UDP port in `config/runtime-host.json`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `oscPort` to `0` to disable the OSC listener.
|
Today, `oscPort: 0` marks the OSC stub disabled and any nonzero port marks it configured but not listening. When OSC ingress is implemented, `oscBindAddress: "127.0.0.1"` should keep OSC local to the host, and `oscBindAddress: "0.0.0.0"` should listen on all IPv4 interfaces. `oscSmoothing` is reserved for a per-frame easing amount on numeric OSC controls.
|
||||||
Set `oscBindAddress` to `127.0.0.1` to keep OSC local to the host, or `0.0.0.0` to listen on all IPv4 interfaces.
|
|
||||||
Set `oscSmoothing` to a value from `0.0` to `1.0` to add a subtle per-frame easing amount for numeric OSC controls. `0.0` disables smoothing, and larger values respond more quickly.
|
|
||||||
|
|
||||||
## Address Pattern
|
## Address Pattern
|
||||||
|
|
||||||
|
|||||||
@@ -1,664 +0,0 @@
|
|||||||
# Phase 1 Design: Subsystem Boundaries and Target Architecture
|
|
||||||
|
|
||||||
This document expands Phase 1 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete target design. Its purpose is to define the long-term subsystem split before later phases introduce a full event model, split `RuntimeHost`, and move rendering onto a sole-owner render thread.
|
|
||||||
|
|
||||||
The main goal of Phase 1 is not to immediately rewrite the app. It is to establish clear ownership boundaries so later refactors all move toward the same architecture instead of solving local problems in conflicting ways.
|
|
||||||
|
|
||||||
## Why Phase 1 Exists
|
|
||||||
|
|
||||||
Today the app works, but too many responsibilities still converge in a few places:
|
|
||||||
|
|
||||||
- `RuntimeHost` owns persistence, live layer state, shader package access, status reporting, and mutation entrypoints.
|
|
||||||
- `OpenGLComposite` coordinates runtime setup, render state retrieval, shader rebuild handling, transient OSC overlay behavior, and video backend integration.
|
|
||||||
- DeckLink callback-driven playout still reaches directly into render-facing work.
|
|
||||||
- Background services rely on polling and shared mutable state more than explicit subsystem contracts.
|
|
||||||
|
|
||||||
Those are exactly the kinds of overlaps that make timing issues, state regressions, and recovery edge cases harder to solve cleanly.
|
|
||||||
|
|
||||||
Phase 1 creates a map for where each responsibility should eventually live.
|
|
||||||
|
|
||||||
## Design Goals
|
|
||||||
|
|
||||||
The target architecture should optimize for:
|
|
||||||
|
|
||||||
- live timing isolation
|
|
||||||
- explicit state ownership
|
|
||||||
- predictable recovery behavior
|
|
||||||
- clear boundaries between persistent state and transient live state
|
|
||||||
- easier testing of non-GL and non-hardware logic
|
|
||||||
- fewer cross-thread shared mutable objects
|
|
||||||
- a playout model that can evolve toward producer/consumer scheduling
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
Phase 1 does not itself require:
|
|
||||||
|
|
||||||
- replacing every direct call with events immediately
|
|
||||||
- moving all rendering to a new thread yet
|
|
||||||
- redesigning the shader contract again
|
|
||||||
- changing DeckLink behavior in place
|
|
||||||
- removing all existing classes before replacements exist
|
|
||||||
|
|
||||||
This phase is the target design and the dependency rules. Later phases perform the actual extraction.
|
|
||||||
|
|
||||||
## Current Pressure Points
|
|
||||||
|
|
||||||
The following current code paths are the strongest evidence for the split proposed here:
|
|
||||||
|
|
||||||
- `RuntimeHost` is both store and live authority:
|
|
||||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:726)
|
|
||||||
- `OpenGLComposite` is both app orchestrator and render/runtime coordinator:
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:106)
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:283)
|
|
||||||
- `RuntimeServices` mixes service orchestration with polling and deferred state work:
|
|
||||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
|
||||||
- Playout is still callback-coupled to render-facing work:
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:68)
|
|
||||||
|
|
||||||
## Target Subsystems
|
|
||||||
|
|
||||||
The long-term architecture should converge on seven primary subsystems:
|
|
||||||
|
|
||||||
1. `RuntimeStore`
|
|
||||||
2. `RuntimeCoordinator`
|
|
||||||
3. `RuntimeSnapshotProvider`
|
|
||||||
4. `ControlServices`
|
|
||||||
5. `RenderEngine`
|
|
||||||
6. `VideoBackend`
|
|
||||||
7. `HealthTelemetry`
|
|
||||||
|
|
||||||
The split below is intentionally sharper than the current code. The point is to make ownership obvious.
|
|
||||||
|
|
||||||
Subsystem-specific design notes that elaborate these boundaries live under [docs/subsystems](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems).
|
|
||||||
|
|
||||||
## Phase 1 Document Set
|
|
||||||
|
|
||||||
This document is the parent note for the Phase 1 subsystem package. The bundle index and subsystem notes live here:
|
|
||||||
|
|
||||||
- [Subsystem Design Index](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
|
|
||||||
- [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
|
|
||||||
- [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
|
|
||||||
- [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
|
|
||||||
- [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
|
|
||||||
- [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
|
|
||||||
- [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
|
|
||||||
- [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
|
|
||||||
|
|
||||||
## Current Implementation Foothold
|
|
||||||
|
|
||||||
The codebase now has an initial Phase 1 compatibility split in place:
|
|
||||||
|
|
||||||
- `RuntimeStore`
|
|
||||||
- [RuntimeStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h)
|
|
||||||
- [RuntimeStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp)
|
|
||||||
- `RuntimeCoordinator`
|
|
||||||
- [RuntimeCoordinator.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.h)
|
|
||||||
- [RuntimeCoordinator.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.cpp)
|
|
||||||
- `RuntimeSnapshotProvider`
|
|
||||||
- [RuntimeSnapshotProvider.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h)
|
|
||||||
- [RuntimeSnapshotProvider.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp)
|
|
||||||
- `ControlServices`
|
|
||||||
- [ControlServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h)
|
|
||||||
- [ControlServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp)
|
|
||||||
- `HealthTelemetry`
|
|
||||||
- [HealthTelemetry.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/HealthTelemetry.h)
|
|
||||||
- [HealthTelemetry.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/HealthTelemetry.cpp)
|
|
||||||
- `RenderEngine`
|
|
||||||
- [RenderEngine.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h)
|
|
||||||
- [RenderEngine.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp)
|
|
||||||
- `VideoBackend`
|
|
||||||
- [VideoBackend.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h)
|
|
||||||
- [VideoBackend.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp)
|
|
||||||
|
|
||||||
These are still compatibility seams, not a completed subsystem extraction. Most of them continue to delegate heavily to `RuntimeHost`, `OpenGLComposite`, `DeckLinkSession`, and the existing bridge/pipeline classes. Their purpose is to give later Phase 1 work real code boundaries that can be expanded in parallel:
|
|
||||||
|
|
||||||
- store-facing UI/runtime control calls in `OpenGLCompositeRuntimeControls.cpp` now route through `RuntimeStore`
|
|
||||||
- mutation and reload policy now routes through `RuntimeCoordinator`
|
|
||||||
- render-state and shader-build reads in `OpenGLComposite.cpp`, `OpenGLShaderPrograms.cpp`, and `ShaderBuildQueue.cpp` now route through `RuntimeSnapshotProvider`
|
|
||||||
- service ingress and polling coordination now route through `ControlServices`
|
|
||||||
- timing and status writes now route through `HealthTelemetry`
|
|
||||||
- render-side frame advancement and render-performance reporting now flow through `RuntimeSnapshotProvider` and `HealthTelemetry` instead of directly through `RuntimeHost`
|
|
||||||
- `OpenGLComposite` now owns a `RenderEngine` seam for renderer, pipeline, render-pass, and shader-program responsibilities
|
|
||||||
- `OpenGLComposite` now owns a `VideoBackend` seam for device/session ownership and callback wiring
|
|
||||||
- `OpenGLVideoIOBridge` now acts as an explicit compatibility adapter between `VideoBackend` and `RenderEngine`, instead of `OpenGLComposite` directly owning both sides
|
|
||||||
|
|
||||||
That means the next parallel Phase 1 work can focus on moving responsibility behind these seams instead of first inventing them.
|
|
||||||
|
|
||||||
## Subsystem Responsibilities
|
|
||||||
|
|
||||||
### `RuntimeStore`
|
|
||||||
|
|
||||||
`RuntimeStore` owns persisted and operator-authored state.
|
|
||||||
|
|
||||||
It is the source of truth for:
|
|
||||||
|
|
||||||
- runtime config loaded from disk
|
|
||||||
- persisted layer stack structure
|
|
||||||
- persisted parameter values
|
|
||||||
- stack preset serialization/deserialization
|
|
||||||
- shader/package metadata that must survive across renders
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- render-thread timing
|
|
||||||
- GL resource lifetime
|
|
||||||
- live transient overlays
|
|
||||||
- hardware callback coordination
|
|
||||||
- UI/websocket broadcasting policy
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- disk I/O belongs here or in its dedicated writer helper
|
|
||||||
- values here are authoritative for saved state
|
|
||||||
- writes may be debounced later, but the data model itself belongs here
|
|
||||||
|
|
||||||
### `RuntimeCoordinator`
|
|
||||||
|
|
||||||
`RuntimeCoordinator` is the mutation and policy layer.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- receiving valid mutation requests from controls, services, or automation
|
|
||||||
- validating requested changes against shader definitions and config rules
|
|
||||||
- resolving how persisted state, committed live state, and transient overlays should interact
|
|
||||||
- requesting snapshot publication when state changes affect render
|
|
||||||
- requesting persistence when stored state changes
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- direct disk serialization details
|
|
||||||
- direct GL work
|
|
||||||
- hardware device lifecycle
|
|
||||||
- polling loops
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- all non-render mutations should eventually flow through this layer
|
|
||||||
- this layer decides whether a change is persisted, transient, or both
|
|
||||||
- this layer owns state policy, not device policy
|
|
||||||
|
|
||||||
### `RuntimeSnapshotProvider`
|
|
||||||
|
|
||||||
`RuntimeSnapshotProvider` publishes render-facing snapshots.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- building immutable or near-immutable render snapshots
|
|
||||||
- translating runtime state into render-ready structures
|
|
||||||
- publishing versioned snapshots
|
|
||||||
- serving the render side without large mutable shared locks
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- deciding whether a mutation is allowed
|
|
||||||
- directly applying UI/OSC requests
|
|
||||||
- persistence
|
|
||||||
- shader compilation orchestration
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- render consumes snapshots, not live mutable store objects
|
|
||||||
- snapshots should be cheap to read and explicit about version changes
|
|
||||||
- dynamic frame-only values may still be attached later, but the snapshot shape should stay stable
|
|
||||||
|
|
||||||
### `ControlServices`
|
|
||||||
|
|
||||||
`ControlServices` is the ingress boundary for non-render control sources.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- OSC receive and route resolution
|
|
||||||
- REST/websocket/control UI ingress
|
|
||||||
- file-watch or reload request ingress
|
|
||||||
- translating external inputs into typed internal actions/events
|
|
||||||
- low-cost buffering/coalescing where appropriate
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- persistence decisions
|
|
||||||
- render snapshot building
|
|
||||||
- hardware playout policy
|
|
||||||
- direct long-lived state ownership beyond ingress-specific queues
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- external inputs enter here and are normalized before they touch core state
|
|
||||||
- service-specific timing concerns stay here unless they affect whole-app policy
|
|
||||||
- no service should directly mutate render-facing state structures
|
|
||||||
|
|
||||||
### `RenderEngine`
|
|
||||||
|
|
||||||
`RenderEngine` is the owner of live rendering behavior.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- sole ownership of GL work in the target architecture
|
|
||||||
- shader program lifecycle once compilation outputs are available
|
|
||||||
- texture upload scheduling
|
|
||||||
- render-pass execution
|
|
||||||
- temporal history and shader feedback resources
|
|
||||||
- transient render-only overlays
|
|
||||||
- preview production as a subordinate output
|
|
||||||
- output-frame production for the video backend
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- persistence
|
|
||||||
- user-facing control normalization
|
|
||||||
- hardware discovery/configuration
|
|
||||||
- high-level runtime mutation policy
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- render consumes snapshots plus render-local transient state
|
|
||||||
- render-local state is allowed if it stays render-local
|
|
||||||
- preview must be treated as best-effort relative to playout
|
|
||||||
|
|
||||||
### `VideoBackend`
|
|
||||||
|
|
||||||
`VideoBackend` owns input/output device lifecycle and playout policy.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- input device configuration and callbacks
|
|
||||||
- output device configuration and callbacks
|
|
||||||
- frame scheduling policy
|
|
||||||
- buffer-pool ownership
|
|
||||||
- playout headroom policy
|
|
||||||
- input signal status
|
|
||||||
- backend state transitions and recovery logic
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- composing frames
|
|
||||||
- owning GL contexts long-term
|
|
||||||
- validating shader parameter changes
|
|
||||||
- persistence
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- this subsystem is the consumer of rendered output frames, not the owner of frame composition policy
|
|
||||||
- it should evolve toward producer/consumer playout rather than callback-driven rendering
|
|
||||||
- backend state should be explicit and reportable
|
|
||||||
|
|
||||||
### `HealthTelemetry`
|
|
||||||
|
|
||||||
`HealthTelemetry` owns structured operational visibility.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- logging
|
|
||||||
- warning/error counters
|
|
||||||
- timing traces
|
|
||||||
- subsystem health state
|
|
||||||
- degraded-mode reporting
|
|
||||||
- operator-visible health summaries
|
|
||||||
|
|
||||||
It should not be responsible for:
|
|
||||||
|
|
||||||
- deciding core app behavior
|
|
||||||
- owning render or backend state
|
|
||||||
- persistence policy
|
|
||||||
|
|
||||||
Design rules:
|
|
||||||
|
|
||||||
- all major subsystems publish health information here
|
|
||||||
- health visibility should outlive UI connection state
|
|
||||||
- modal dialogs should not be the main operational surface
|
|
||||||
|
|
||||||
## Target Dependency Rules
|
|
||||||
|
|
||||||
The architecture should follow these rules as closely as possible.
|
|
||||||
|
|
||||||
Allowed dependency directions:
|
|
||||||
|
|
||||||
- `ControlServices -> RuntimeCoordinator`
|
|
||||||
- `RuntimeCoordinator -> RuntimeStore`
|
|
||||||
- `RuntimeCoordinator -> RuntimeSnapshotProvider`
|
|
||||||
- `RuntimeCoordinator -> HealthTelemetry`
|
|
||||||
- `RuntimeSnapshotProvider -> RuntimeStore`
|
|
||||||
- `RenderEngine -> RuntimeSnapshotProvider`
|
|
||||||
- `RenderEngine -> HealthTelemetry`
|
|
||||||
- `VideoBackend -> RenderEngine`
|
|
||||||
- `VideoBackend -> HealthTelemetry`
|
|
||||||
|
|
||||||
Conditionally allowed during migration:
|
|
||||||
|
|
||||||
- `ControlServices -> HealthTelemetry`
|
|
||||||
- `ControlServices -> RuntimeStore` only through temporary compatibility shims
|
|
||||||
|
|
||||||
Not allowed in the target design:
|
|
||||||
|
|
||||||
- `RenderEngine -> RuntimeStore`
|
|
||||||
- `RenderEngine -> ControlServices`
|
|
||||||
- `VideoBackend -> RuntimeStore`
|
|
||||||
- `ControlServices -> RenderEngine` for direct mutation
|
|
||||||
- `RuntimeStore -> RenderEngine`
|
|
||||||
- `HealthTelemetry -> any subsystem` for control flow
|
|
||||||
|
|
||||||
The key principle is:
|
|
||||||
|
|
||||||
- store owns durable data
|
|
||||||
- coordinator owns mutation policy
|
|
||||||
- snapshot provider owns render-facing state publication
|
|
||||||
- render owns live GPU execution
|
|
||||||
- backend owns device timing
|
|
||||||
- telemetry observes all of them
|
|
||||||
|
|
||||||
## State Ownership Model
|
|
||||||
|
|
||||||
The app has several different kinds of state, and Phase 1 should name them explicitly.
|
|
||||||
|
|
||||||
### Persisted State
|
|
||||||
|
|
||||||
Owned by `RuntimeStore`.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- layer stack structure
|
|
||||||
- selected shader ids
|
|
||||||
- saved parameter values
|
|
||||||
- runtime host config
|
|
||||||
- stack presets
|
|
||||||
|
|
||||||
### Committed Live State
|
|
||||||
|
|
||||||
Owned logically by `RuntimeCoordinator`, stored in the store or a live-state companion depending on future implementation.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- current operator-selected parameter values
|
|
||||||
- current bypass state
|
|
||||||
- current selected shader for each layer
|
|
||||||
|
|
||||||
This is state that should normally survive until explicitly changed and can be persisted if policy says so.
|
|
||||||
|
|
||||||
### Transient Live Overlay State
|
|
||||||
|
|
||||||
Owned by the subsystem that consumes it, not by the persisted store.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- active OSC overlay targets while automation is flowing
|
|
||||||
- shader feedback buffers
|
|
||||||
- temporal history textures
|
|
||||||
- queued input frames
|
|
||||||
- in-flight preview state
|
|
||||||
- playout queue state
|
|
||||||
|
|
||||||
This is where many current issues come from. The design rule is:
|
|
||||||
|
|
||||||
- transient state may influence output
|
|
||||||
- transient state should not masquerade as persisted truth
|
|
||||||
|
|
||||||
### Health and Timing State
|
|
||||||
|
|
||||||
Owned by `HealthTelemetry`.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- frame pacing stats
|
|
||||||
- render timing
|
|
||||||
- late/dropped frame counters
|
|
||||||
- queue depths
|
|
||||||
- warning states
|
|
||||||
|
|
||||||
## Target Runtime Flow
|
|
||||||
|
|
||||||
This section describes the intended long-term flow once later phases are in place.
|
|
||||||
|
|
||||||
### Control Mutation Flow
|
|
||||||
|
|
||||||
1. OSC/UI/file-watch input enters `ControlServices`.
|
|
||||||
2. `ControlServices` normalizes it into an internal action or event.
|
|
||||||
3. `RuntimeCoordinator` validates and classifies the action.
|
|
||||||
4. If the action changes durable state, `RuntimeStore` is updated.
|
|
||||||
5. If the action changes render-facing state, `RuntimeSnapshotProvider` publishes a new snapshot.
|
|
||||||
6. If the action requires persistence, a persistence request is queued.
|
|
||||||
7. Health/timing observations are emitted separately.
|
|
||||||
|
|
||||||
### Render Flow
|
|
||||||
|
|
||||||
1. `RenderEngine` consumes the latest published snapshot.
|
|
||||||
2. `RenderEngine` combines that snapshot with render-local transient state.
|
|
||||||
3. `RenderEngine` performs uploads, pass execution, feedback/history maintenance, and output production.
|
|
||||||
4. `RenderEngine` produces:
|
|
||||||
- preview-ready output
|
|
||||||
- video-backend-ready output frames
|
|
||||||
- render timing and warning signals
|
|
||||||
|
|
||||||
### Video Output Flow
|
|
||||||
|
|
||||||
Target long-term flow:
|
|
||||||
|
|
||||||
1. `RenderEngine` produces completed output frames ahead of demand.
|
|
||||||
2. `VideoBackend` consumes those frames from a bounded queue or ring buffer.
|
|
||||||
3. Device callbacks only drive dequeue/schedule/accounting behavior.
|
|
||||||
4. `HealthTelemetry` records queue depth, lateness, underruns, and recovery events.
|
|
||||||
|
|
||||||
### Reload / Shader Rebuild Flow
|
|
||||||
|
|
||||||
1. file-watch or manual reload enters through `ControlServices`
|
|
||||||
2. `RuntimeCoordinator` classifies the reload request
|
|
||||||
3. `RuntimeStore` and shader/package metadata are refreshed if needed
|
|
||||||
4. `RuntimeSnapshotProvider` republishes affected snapshot state
|
|
||||||
5. `RenderEngine` rebuilds render-local resources from the new snapshot/build outputs
|
|
||||||
|
|
||||||
The important boundary here is that reload is not "a render concern that also touches persistence." It is a coordinated runtime concern with a render-local execution phase.
|
|
||||||
|
|
||||||
## Suggested Public Interfaces
|
|
||||||
|
|
||||||
These are not final class signatures, but they show the shape the architecture should move toward.
|
|
||||||
|
|
||||||
### `RuntimeStore`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `LoadConfig()`
|
|
||||||
- `LoadPersistentState()`
|
|
||||||
- `SavePersistentStateSnapshot(...)`
|
|
||||||
- `GetStoredLayerStack()`
|
|
||||||
- `SetStoredLayerStack(...)`
|
|
||||||
- `GetStackPresetNames()`
|
|
||||||
- `SaveStackPreset(...)`
|
|
||||||
- `LoadStackPreset(...)`
|
|
||||||
|
|
||||||
### `RuntimeCoordinator`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `ApplyControlMutation(...)`
|
|
||||||
- `ApplyAutomationTarget(...)`
|
|
||||||
- `ResetLayer(...)`
|
|
||||||
- `RequestReload(...)`
|
|
||||||
- `CommitOverlayState(...)`
|
|
||||||
- `PublishSnapshotIfNeeded()`
|
|
||||||
- `RequestPersistenceIfNeeded()`
|
|
||||||
|
|
||||||
### `RuntimeSnapshotProvider`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `BuildSnapshot(...)`
|
|
||||||
- `GetLatestSnapshot()`
|
|
||||||
- `GetSnapshotVersion()`
|
|
||||||
- `PublishSnapshot(...)`
|
|
||||||
|
|
||||||
### `ControlServices`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `StartOscIngress(...)`
|
|
||||||
- `StartWebControlIngress(...)`
|
|
||||||
- `StartFileWatchIngress(...)`
|
|
||||||
- `EnqueueControlAction(...)`
|
|
||||||
- `DrainServiceEvents(...)`
|
|
||||||
|
|
||||||
### `RenderEngine`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `StartRenderLoop(...)`
|
|
||||||
- `ConsumeSnapshot(...)`
|
|
||||||
- `EnqueueInputFrame(...)`
|
|
||||||
- `ProduceOutputFrame(...)`
|
|
||||||
- `ResetRenderLocalState(...)`
|
|
||||||
- `HandleRebuildOutputs(...)`
|
|
||||||
|
|
||||||
### `VideoBackend`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `ConfigureInput(...)`
|
|
||||||
- `ConfigureOutput(...)`
|
|
||||||
- `StartPlayout(...)`
|
|
||||||
- `StopPlayout(...)`
|
|
||||||
- `ConsumeRenderedFrame(...)`
|
|
||||||
- `ReportBackendState(...)`
|
|
||||||
|
|
||||||
### `HealthTelemetry`
|
|
||||||
|
|
||||||
Core responsibilities:
|
|
||||||
|
|
||||||
- `RecordTimingSample(...)`
|
|
||||||
- `RecordCounterDelta(...)`
|
|
||||||
- `RaiseWarning(...)`
|
|
||||||
- `ClearWarning(...)`
|
|
||||||
- `AppendLogEntry(...)`
|
|
||||||
- `BuildHealthSnapshot()`
|
|
||||||
|
|
||||||
## Mapping From Current Code to Target Subsystems
|
|
||||||
|
|
||||||
This is not a one-to-one rename plan. It is a responsibility migration map.
|
|
||||||
|
|
||||||
### Current `RuntimeHost`
|
|
||||||
|
|
||||||
Should eventually split across:
|
|
||||||
|
|
||||||
- `RuntimeStore`
|
|
||||||
- `RuntimeCoordinator`
|
|
||||||
- `RuntimeSnapshotProvider`
|
|
||||||
- parts of `HealthTelemetry`
|
|
||||||
|
|
||||||
Likely examples:
|
|
||||||
|
|
||||||
- config loading/saving -> `RuntimeStore`
|
|
||||||
- layer stack mutation validation -> `RuntimeCoordinator`
|
|
||||||
- render state building/versioning -> `RuntimeSnapshotProvider`
|
|
||||||
- timing/status setters -> `HealthTelemetry`
|
|
||||||
|
|
||||||
### Current `RuntimeServices`
|
|
||||||
|
|
||||||
Should eventually become mostly:
|
|
||||||
|
|
||||||
- `ControlServices`
|
|
||||||
- a small service-hosting shell
|
|
||||||
|
|
||||||
Likely examples:
|
|
||||||
|
|
||||||
- OSC ingress/coalescing -> `ControlServices`
|
|
||||||
- file-watch ingress -> `ControlServices`
|
|
||||||
- deferred service coordination now done by polling -> split between `ControlServices` and event-driven coordinator calls
|
|
||||||
|
|
||||||
### Current `OpenGLComposite`
|
|
||||||
|
|
||||||
Should eventually split across:
|
|
||||||
|
|
||||||
- application bootstrap shell
|
|
||||||
- `RenderEngine`
|
|
||||||
- orchestration glue that wires subsystems together
|
|
||||||
|
|
||||||
Likely examples:
|
|
||||||
|
|
||||||
- render-pass facing code -> `RenderEngine`
|
|
||||||
- app/service/backend bootstrap -> composition root
|
|
||||||
- runtime mutation API surface -> coordinator-facing adapter, not render owner
|
|
||||||
|
|
||||||
### Current `OpenGLVideoIOBridge` and `DeckLinkSession`
|
|
||||||
|
|
||||||
Should eventually align more clearly under:
|
|
||||||
|
|
||||||
- `VideoBackend`
|
|
||||||
- `RenderEngine`
|
|
||||||
|
|
||||||
Likely examples:
|
|
||||||
|
|
||||||
- device callback and scheduling policy -> `VideoBackend`
|
|
||||||
- GL upload/readback/render work -> `RenderEngine`
|
|
||||||
|
|
||||||
## Architectural Guardrails
|
|
||||||
|
|
||||||
As later phases begin, these rules should be treated as guardrails.
|
|
||||||
|
|
||||||
### 1. No new cross-cutting state should be added to `RuntimeHost`
|
|
||||||
|
|
||||||
If a new feature needs durable state, place it conceptually under `RuntimeStore`.
|
|
||||||
If it needs render-local transient state, place it conceptually under `RenderEngine`.
|
|
||||||
If it needs timing/status counters, place it conceptually under `HealthTelemetry`.
|
|
||||||
|
|
||||||
### 2. Render-local state should stay render-local
|
|
||||||
|
|
||||||
Do not push shader feedback, temporal history, preview caches, or playout queues back into the store just to make them easy to reach from other systems.
|
|
||||||
|
|
||||||
### 3. Device callbacks should not become a dumping ground for app work
|
|
||||||
|
|
||||||
Callback threads should converge toward signaling and queue management, not core rendering, persistence, or control mutation.
|
|
||||||
|
|
||||||
### 4. Persistence should not be used as a control synchronization mechanism
|
|
||||||
|
|
||||||
Saving state is not how subsystems discover changes. Published snapshots and explicit events should handle that.
|
|
||||||
|
|
||||||
### 5. Health reporting should observe, not coordinate
|
|
||||||
|
|
||||||
Telemetry systems may record warnings and degraded states, but they should not become the hidden control plane for the app.
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
Phase 1 is a design phase, but it should support incremental migration.
|
|
||||||
|
|
||||||
Recommended order after this document:
|
|
||||||
|
|
||||||
1. Introduce names and interfaces before moving logic.
|
|
||||||
2. Create compatibility adapters around `RuntimeHost` rather than forcing a flag day.
|
|
||||||
3. Move read-only render snapshot publication out before moving all mutation logic.
|
|
||||||
4. Move service ingress boundaries out before removing the old polling shell.
|
|
||||||
5. Isolate timing/health setters from the core store as early as practical.
|
|
||||||
|
|
||||||
This keeps progress measurable while reducing rewrite risk.
|
|
||||||
|
|
||||||
## Suggested Deliverables for Completing Phase 1
|
|
||||||
|
|
||||||
Phase 1 can reasonably be considered complete once the project has:
|
|
||||||
|
|
||||||
- this subsystem-boundary design document
|
|
||||||
- agreed subsystem names and responsibilities
|
|
||||||
- agreed allowed dependency directions
|
|
||||||
- explicit state categories: persisted, committed live, transient overlay, health/timing
|
|
||||||
- a current-to-target responsibility map for `RuntimeHost`, `RuntimeServices`, `OpenGLComposite`, and backend/render bridge code
|
|
||||||
- a decision that later phases will build against this target rather than inventing new boundaries ad hoc
|
|
||||||
|
|
||||||
## Open Questions For Later Phases
|
|
||||||
|
|
||||||
These do not block Phase 1, but they should remain visible.
|
|
||||||
|
|
||||||
- Should shader package registry ownership live entirely in `RuntimeStore`, or should compile-ready derived registry data move into the snapshot provider?
|
|
||||||
- Should committed live state be stored directly in `RuntimeStore`, or split into store plus live-session state owned by the coordinator?
|
|
||||||
- How much of shader build orchestration belongs to `RenderEngine` versus a separate build service?
|
|
||||||
- At what phase should preview become fully decoupled from playout cadence?
|
|
||||||
- Should persistence become its own `PersistenceWriter` subsystem in Phase 6, or remain an implementation detail under `RuntimeStore`?
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
Phase 1 should establish one simple rule for the rest of the refactor:
|
|
||||||
|
|
||||||
- durable state lives in the store
|
|
||||||
- mutation policy lives in the coordinator
|
|
||||||
- render-facing state is published as snapshots
|
|
||||||
- external control sources enter through services
|
|
||||||
- GL work belongs to render
|
|
||||||
- hardware pacing belongs to the backend
|
|
||||||
- health visibility belongs to telemetry
|
|
||||||
|
|
||||||
If later phases keep to that rule, the architecture will become materially more resilient without needing another round of foundational boundary changes.
|
|
||||||
164
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
164
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Render Cadence Golden Rules
|
||||||
|
|
||||||
|
These are the non-negotiable rules for the new render-cadence architecture.
|
||||||
|
|
||||||
|
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
|
||||||
|
|
||||||
|
## 1. The Render Thread Owns Its Render Context
|
||||||
|
|
||||||
|
Only the render thread may bind and use its primary OpenGL context.
|
||||||
|
|
||||||
|
For a fork that replaces the renderer with D3D or another graphics API, read this rule as the same ownership contract: the cadence/render thread owns the primary render device/context path, and non-render services exchange prepared data with it rather than calling rendering APIs themselves.
|
||||||
|
|
||||||
|
Allowed on the render thread:
|
||||||
|
|
||||||
|
- GL resource creation and destruction for resources it owns
|
||||||
|
- GL shader/program swap from an already-prepared GL program
|
||||||
|
- drawing the next frame
|
||||||
|
- async readback queueing and completion polling
|
||||||
|
- publishing completed system-memory frames
|
||||||
|
|
||||||
|
Not allowed on the render thread:
|
||||||
|
|
||||||
|
- Slang compiler invocation
|
||||||
|
- manifest scanning/parsing
|
||||||
|
- filesystem discovery
|
||||||
|
- image/font/LUT decoding
|
||||||
|
- persistence
|
||||||
|
- network/API/OSC handling
|
||||||
|
- DeckLink scheduling
|
||||||
|
- blocking console logging
|
||||||
|
- config file discovery or parsing
|
||||||
|
|
||||||
|
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
||||||
|
|
||||||
|
## 2. Render Cadence Does Not Chase Buffers
|
||||||
|
|
||||||
|
The render thread runs at the selected render cadence.
|
||||||
|
|
||||||
|
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
|
||||||
|
|
||||||
|
Buffers absorb timing differences. They do not control render cadence.
|
||||||
|
|
||||||
|
## 3. Video I/O Never Renders
|
||||||
|
|
||||||
|
DeckLink output consumes already-rendered system-memory frames.
|
||||||
|
|
||||||
|
The output/scheduling side may:
|
||||||
|
|
||||||
|
- schedule completed frames
|
||||||
|
- release frames after DeckLink completion
|
||||||
|
- report late/dropped/schedule telemetry
|
||||||
|
- record app-side poll misses
|
||||||
|
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||||
|
|
||||||
|
It must not:
|
||||||
|
|
||||||
|
- render fallback frames
|
||||||
|
- invoke GL
|
||||||
|
- compile shaders
|
||||||
|
- block the render cadence waiting for DeckLink
|
||||||
|
- continuously rewrite healthy scheduled timestamps
|
||||||
|
|
||||||
|
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||||
|
|
||||||
|
Output pixel-format packing that requires GL belongs in the render-owned readback path before frame publication. Video I/O edges may select supported system-memory formats, but they must not invoke GL or become hidden pixel-conversion renderers.
|
||||||
|
|
||||||
|
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||||
|
|
||||||
|
## 4. Runtime Build Work Produces Artifacts
|
||||||
|
|
||||||
|
Runtime shader work is split into two phases:
|
||||||
|
|
||||||
|
1. CPU/build phase outside the render thread
|
||||||
|
2. shared-context GL preparation outside the render thread where practical
|
||||||
|
3. GL program swap on the render thread
|
||||||
|
|
||||||
|
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||||
|
|
||||||
|
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
|
||||||
|
|
||||||
|
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
||||||
|
|
||||||
|
## 5. No Hidden Blocking In The Cadence Path
|
||||||
|
|
||||||
|
The render loop must not do work with unbounded or OS-dependent latency.
|
||||||
|
|
||||||
|
Examples to avoid:
|
||||||
|
|
||||||
|
- file reads
|
||||||
|
- directory scans
|
||||||
|
- image decoding
|
||||||
|
- process launches
|
||||||
|
- waits on worker threads
|
||||||
|
- blocking locks around slow code
|
||||||
|
- synchronous GPU readback waits
|
||||||
|
- console I/O
|
||||||
|
|
||||||
|
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
|
||||||
|
|
||||||
|
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||||
|
|
||||||
|
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
|
||||||
|
|
||||||
|
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||||
|
|
||||||
|
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||||
|
|
||||||
|
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||||
|
|
||||||
|
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
|
||||||
|
|
||||||
|
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
|
||||||
|
|
||||||
|
## 8. Telemetry Must Name Ownership Clearly
|
||||||
|
|
||||||
|
Counters should say which subsystem had the event.
|
||||||
|
|
||||||
|
Good examples:
|
||||||
|
|
||||||
|
- `renderFps`
|
||||||
|
- `scheduleFps`
|
||||||
|
- `completedPollMisses`
|
||||||
|
- `scheduleFailures`
|
||||||
|
- `decklinkBuffered`
|
||||||
|
- `deckLinkScheduleLeadFrames`
|
||||||
|
- `deckLinkScheduleRealignments`
|
||||||
|
- `inputCaptureFps`
|
||||||
|
- `inputSubmitMs`
|
||||||
|
- `inputUploadMs`
|
||||||
|
- `inputConvertMs`
|
||||||
|
- `shaderCommitted`
|
||||||
|
- `shaderFailures`
|
||||||
|
|
||||||
|
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
|
||||||
|
|
||||||
|
## 9. Keep Files Small And Role-Based
|
||||||
|
|
||||||
|
A file should have one clear reason to change.
|
||||||
|
|
||||||
|
Preferred boundaries:
|
||||||
|
|
||||||
|
- app orchestration
|
||||||
|
- render cadence/thread ownership
|
||||||
|
- GL rendering
|
||||||
|
- runtime artifact build/bridge
|
||||||
|
- app-owned display/render layer model
|
||||||
|
- parameter packing
|
||||||
|
- system-memory frame exchange
|
||||||
|
- DeckLink output scheduling
|
||||||
|
- telemetry
|
||||||
|
- local control/API edge
|
||||||
|
- config loading
|
||||||
|
- JSON presentation/serialization
|
||||||
|
- logging
|
||||||
|
|
||||||
|
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
|
||||||
|
|
||||||
|
## 10. Prefer Explicit Unsupported States
|
||||||
|
|
||||||
|
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
|
||||||
|
|
||||||
|
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
|
||||||
|
|
||||||
|
Unsupported is healthy when it protects the architecture.
|
||||||
@@ -6,24 +6,214 @@ info:
|
|||||||
REST API exposed by the local Video Shader Toys control server.
|
REST API exposed by the local Video Shader Toys control server.
|
||||||
|
|
||||||
The API is intended for local control tools and the bundled React UI. All mutating
|
The API is intended for local control tools and the bundled React UI. All mutating
|
||||||
endpoints return a small action result object. Successful mutating requests also
|
endpoints return a small action result object.
|
||||||
broadcast the latest runtime state over the `/ws` WebSocket.
|
|
||||||
|
|
||||||
WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws`
|
RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
|
||||||
to receive full runtime state JSON messages whenever state changes.
|
WebSocket state updates consumed by the bundled control UI.
|
||||||
servers:
|
servers:
|
||||||
- url: http://127.0.0.1:8080
|
- url: http://127.0.0.1:8080
|
||||||
description: Default local control server
|
description: Default local control server
|
||||||
tags:
|
tags:
|
||||||
- name: State
|
- name: State
|
||||||
description: Runtime state and status.
|
description: Runtime state and status.
|
||||||
|
- name: Config
|
||||||
|
description: Startup host configuration and restart control.
|
||||||
|
- name: Static
|
||||||
|
description: Bundled control UI and static assets served by the local host.
|
||||||
|
- name: Docs
|
||||||
|
description: OpenAPI and Swagger UI documentation served by the local host.
|
||||||
- name: Layers
|
- name: Layers
|
||||||
description: Layer stack control.
|
description: Layer stack control.
|
||||||
- name: Stack Presets
|
- name: Stack Presets
|
||||||
description: Save and recall layer stack presets.
|
description: Planned save/recall layer stack presets. The current native host exposes the routes but returns an unimplemented action result.
|
||||||
- name: Runtime
|
- name: Runtime
|
||||||
description: Runtime actions.
|
description: Runtime actions.
|
||||||
paths:
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve the bundled control UI
|
||||||
|
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||||
|
operationId: getControlUiRoot
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Control UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: UI bundle was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/index.html:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve the bundled control UI index file
|
||||||
|
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||||
|
operationId: getControlUiIndex
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Control UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: UI bundle was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/assets/{assetPath}:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve a bundled control UI asset
|
||||||
|
description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
|
||||||
|
operationId: getControlUiAsset
|
||||||
|
parameters:
|
||||||
|
- name: assetPath
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Relative asset path below `ui/dist/assets`.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Static asset.
|
||||||
|
content:
|
||||||
|
text/javascript:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
text/css:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/svg+xml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/png:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: Asset was not found or the path was unsafe.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/shader-assets/{shaderId}/{assetPath}:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve a shader package UI asset
|
||||||
|
description: Serves custom shader UI files declared by `shader.json`. Assets are resolved through the active runtime-content controller and must stay inside that shader package's `ui/` directory.
|
||||||
|
operationId: getShaderPackageAsset
|
||||||
|
parameters:
|
||||||
|
- name: shaderId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Shader package id.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: assetPath
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Relative asset path below the shader package, usually `ui/controls.js`.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Shader package UI asset.
|
||||||
|
content:
|
||||||
|
text/javascript:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
text/css:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/svg+xml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/png:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: Asset was not found, the shader did not declare custom UI, or the path was unsafe.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve Swagger UI
|
||||||
|
description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
|
||||||
|
operationId: getSwaggerUi
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Swagger UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs/:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve Swagger UI
|
||||||
|
description: Alias for `/docs`.
|
||||||
|
operationId: getSwaggerUiWithTrailingSlash
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Swagger UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs/openapi.yaml:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve the OpenAPI document
|
||||||
|
operationId: getOpenApiDocumentFromDocs
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OpenAPI YAML document.
|
||||||
|
content:
|
||||||
|
application/yaml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: OpenAPI document was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/openapi.yaml:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve the OpenAPI document
|
||||||
|
description: Alias for `/docs/openapi.yaml`.
|
||||||
|
operationId: getOpenApiDocument
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OpenAPI YAML document.
|
||||||
|
content:
|
||||||
|
application/yaml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: OpenAPI document was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/api/state:
|
/api/state:
|
||||||
get:
|
get:
|
||||||
tags: [State]
|
tags: [State]
|
||||||
@@ -36,6 +226,83 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/RuntimeState"
|
$ref: "#/components/schemas/RuntimeState"
|
||||||
|
/api/config:
|
||||||
|
get:
|
||||||
|
tags: [Config]
|
||||||
|
summary: Get active and saved host config
|
||||||
|
operationId: getHostConfig
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Active startup config and the config currently saved on disk.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/HostConfigResponse"
|
||||||
|
/api/ndi/sources:
|
||||||
|
get:
|
||||||
|
tags: [Config]
|
||||||
|
summary: Discover NDI sources
|
||||||
|
description: Returns currently discoverable NDI sources for the host-config input device picker. Manual source names remain valid even when discovery returns no matches.
|
||||||
|
operationId: getNdiSources
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Current NDI source discovery result.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NdiSourcesResponse"
|
||||||
|
/api/config/save:
|
||||||
|
post:
|
||||||
|
tags: [Config]
|
||||||
|
summary: Save host config
|
||||||
|
description: Saves `runtime-host.json`. Startup-owned services use the new values after app restart.
|
||||||
|
operationId: saveHostConfig
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/HostConfig"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ActionOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/ActionError"
|
||||||
|
/api/app/restart:
|
||||||
|
post:
|
||||||
|
tags: [Config]
|
||||||
|
summary: Restart the native host
|
||||||
|
operationId: restartHost
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ActionOk"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/ActionError"
|
||||||
|
/ws:
|
||||||
|
get:
|
||||||
|
tags: [State]
|
||||||
|
summary: Stream runtime state over WebSocket
|
||||||
|
description: |
|
||||||
|
Upgrades to a WebSocket connection. The server sends JSON runtime-state
|
||||||
|
snapshots using the same shape as `GET /api/state` whenever the serialized
|
||||||
|
state changes.
|
||||||
|
operationId: streamRuntimeState
|
||||||
|
responses:
|
||||||
|
"101":
|
||||||
|
description: WebSocket protocol upgrade accepted.
|
||||||
|
"400":
|
||||||
|
description: The request was not a valid WebSocket upgrade.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/api/layers/add:
|
/api/layers/add:
|
||||||
post:
|
post:
|
||||||
tags: [Layers]
|
tags: [Layers]
|
||||||
@@ -72,7 +339,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
tags: [Layers]
|
tags: [Layers]
|
||||||
summary: Move a layer by direction
|
summary: Move a layer by direction
|
||||||
description: Moves a layer up or down by one or more positions, depending on the signed direction value.
|
description: Planned relative move route. The current native command path supports `/api/layers/reorder`; this route currently returns an unimplemented action result.
|
||||||
operationId: moveLayer
|
operationId: moveLayer
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -168,7 +435,8 @@ paths:
|
|||||||
/api/stack-presets/save:
|
/api/stack-presets/save:
|
||||||
post:
|
post:
|
||||||
tags: [Stack Presets]
|
tags: [Stack Presets]
|
||||||
summary: Save the current layer stack as a preset
|
summary: Planned preset save route
|
||||||
|
description: Planned preset route. The current native command path currently returns an unimplemented action result; latest-working-state autosave uses `runtime/runtime_state.json`.
|
||||||
operationId: saveStackPreset
|
operationId: saveStackPreset
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -184,7 +452,8 @@ paths:
|
|||||||
/api/stack-presets/load:
|
/api/stack-presets/load:
|
||||||
post:
|
post:
|
||||||
tags: [Stack Presets]
|
tags: [Stack Presets]
|
||||||
summary: Load a layer stack preset
|
summary: Planned preset load route
|
||||||
|
description: Planned preset route. The current native command path currently returns an unimplemented action result; latest-working-state startup restore uses `runtime/runtime_state.json`.
|
||||||
operationId: loadStackPreset
|
operationId: loadStackPreset
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -201,7 +470,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
tags: [Runtime]
|
tags: [Runtime]
|
||||||
summary: Reload shaders
|
summary: Reload shaders
|
||||||
description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
|
description: Rescans the shader library, re-reads manifests, refreshes shader availability/errors, reconciles active layer parameter definitions, and queues recompilation for every catalog-valid layer in the active stack. It does not compile every shader package in the library; packages are compiled when they are active layers. If a rebuild fails, the previous working render stack remains active where possible.
|
||||||
operationId: reloadShaders
|
operationId: reloadShaders
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
@@ -218,8 +487,8 @@ paths:
|
|||||||
/api/screenshot:
|
/api/screenshot:
|
||||||
post:
|
post:
|
||||||
tags: [Runtime]
|
tags: [Runtime]
|
||||||
summary: Queue a PNG screenshot of the final output render target
|
summary: Planned screenshot route
|
||||||
description: Captures the next completed output render target and writes it under `runtime/screenshots/`.
|
description: Planned screenshot route. The current native command path currently returns an unimplemented action result.
|
||||||
operationId: queueScreenshot
|
operationId: queueScreenshot
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
@@ -348,6 +617,116 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: live-show-look
|
example: live-show-look
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
HostConfigResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
active:
|
||||||
|
$ref: "#/components/schemas/HostConfig"
|
||||||
|
disk:
|
||||||
|
$ref: "#/components/schemas/HostConfig"
|
||||||
|
diskLoaded:
|
||||||
|
type: boolean
|
||||||
|
restartRequired:
|
||||||
|
type: boolean
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
NdiSourcesResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
sources:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/NdiSource"
|
||||||
|
additionalProperties: false
|
||||||
|
NdiSource:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: User-visible NDI source name, usually `MACHINE (SOURCE)`.
|
||||||
|
urlAddress:
|
||||||
|
type: string
|
||||||
|
description: SDK-provided URL/address for diagnostics. Saved host config still uses the source name.
|
||||||
|
additionalProperties: false
|
||||||
|
HostConfig:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
$schema:
|
||||||
|
type: string
|
||||||
|
shaderLibrary:
|
||||||
|
type: string
|
||||||
|
serverPort:
|
||||||
|
type: number
|
||||||
|
oscBindAddress:
|
||||||
|
type: string
|
||||||
|
oscPort:
|
||||||
|
type: number
|
||||||
|
oscSmoothing:
|
||||||
|
type: number
|
||||||
|
input:
|
||||||
|
$ref: "#/components/schemas/HostVideoInputConfig"
|
||||||
|
output:
|
||||||
|
$ref: "#/components/schemas/HostVideoOutputConfig"
|
||||||
|
autoReload:
|
||||||
|
type: boolean
|
||||||
|
maxTemporalHistoryFrames:
|
||||||
|
type: number
|
||||||
|
previewEnabled:
|
||||||
|
type: boolean
|
||||||
|
previewFps:
|
||||||
|
type: number
|
||||||
|
runtimeShaderId:
|
||||||
|
type: string
|
||||||
|
description: Optional startup shader id used only when no saved runtime layer stack is restored.
|
||||||
|
default: ""
|
||||||
|
additionalProperties: false
|
||||||
|
HostVideoInputConfig:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
enum: [decklink, ndi, none]
|
||||||
|
device:
|
||||||
|
type: string
|
||||||
|
resolution:
|
||||||
|
type: string
|
||||||
|
frameRate:
|
||||||
|
type: string
|
||||||
|
additionalProperties: false
|
||||||
|
HostVideoOutputConfig:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
enum: [decklink, ndi, none]
|
||||||
|
device:
|
||||||
|
type: string
|
||||||
|
resolution:
|
||||||
|
type: string
|
||||||
|
frameRate:
|
||||||
|
type: string
|
||||||
|
pixelFormat:
|
||||||
|
type: string
|
||||||
|
enum: [auto, bgra8, uyvy8]
|
||||||
|
keying:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
external:
|
||||||
|
type: boolean
|
||||||
|
description: DeckLink external-keyer backing field. The bundled UI exposes this together with alphaRequired as Output alpha and only writes it true for DeckLink output.
|
||||||
|
alphaRequired:
|
||||||
|
type: boolean
|
||||||
|
description: General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format.
|
||||||
|
additionalProperties: false
|
||||||
|
additionalProperties: false
|
||||||
RuntimeState:
|
RuntimeState:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -357,12 +736,18 @@ components:
|
|||||||
$ref: "#/components/schemas/RuntimeStatus"
|
$ref: "#/components/schemas/RuntimeStatus"
|
||||||
video:
|
video:
|
||||||
$ref: "#/components/schemas/VideoStatus"
|
$ref: "#/components/schemas/VideoStatus"
|
||||||
|
videoOutput:
|
||||||
|
$ref: "#/components/schemas/VideoOutputStatus"
|
||||||
decklink:
|
decklink:
|
||||||
$ref: "#/components/schemas/DeckLinkStatus"
|
$ref: "#/components/schemas/DeckLinkStatus"
|
||||||
videoIO:
|
videoIO:
|
||||||
$ref: "#/components/schemas/VideoIOStatus"
|
$ref: "#/components/schemas/VideoIOStatus"
|
||||||
performance:
|
performance:
|
||||||
$ref: "#/components/schemas/PerformanceStatus"
|
$ref: "#/components/schemas/PerformanceStatus"
|
||||||
|
backendPlayout:
|
||||||
|
$ref: "#/components/schemas/BackendPlayoutStatus"
|
||||||
|
runtimeEvents:
|
||||||
|
$ref: "#/components/schemas/RuntimeEventStatus"
|
||||||
shaders:
|
shaders:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -382,20 +767,77 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
oscPort:
|
oscPort:
|
||||||
type: number
|
type: number
|
||||||
|
oscBindAddress:
|
||||||
|
type: string
|
||||||
|
oscSmoothing:
|
||||||
|
type: number
|
||||||
autoReload:
|
autoReload:
|
||||||
type: boolean
|
type: boolean
|
||||||
maxTemporalHistoryFrames:
|
maxTemporalHistoryFrames:
|
||||||
type: number
|
type: number
|
||||||
enableExternalKeying:
|
previewFps:
|
||||||
|
type: number
|
||||||
|
osc:
|
||||||
|
$ref: "#/components/schemas/AppOscStatus"
|
||||||
|
input:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
enum: [decklink, ndi, none]
|
||||||
|
device:
|
||||||
|
type: string
|
||||||
|
resolution:
|
||||||
|
type: string
|
||||||
|
frameRate:
|
||||||
|
type: string
|
||||||
|
output:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
enum: [decklink, ndi, none]
|
||||||
|
device:
|
||||||
|
type: string
|
||||||
|
resolution:
|
||||||
|
type: string
|
||||||
|
frameRate:
|
||||||
|
type: string
|
||||||
|
pixelFormat:
|
||||||
|
type: string
|
||||||
|
enum: [auto, bgra8, uyvy8]
|
||||||
|
systemFramePixelFormat:
|
||||||
|
type: string
|
||||||
|
keying:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
outputAlpha:
|
||||||
|
type: boolean
|
||||||
|
description: Operator-facing derived alpha state. True when alphaRequired or the DeckLink external-keyer backing field is true.
|
||||||
|
external:
|
||||||
|
type: boolean
|
||||||
|
description: DeckLink external-keyer backing field. The bundled UI exposes this together with alphaRequired as Output alpha and only writes it true for DeckLink output.
|
||||||
|
alphaRequired:
|
||||||
|
type: boolean
|
||||||
|
description: General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format.
|
||||||
|
AppOscStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
configured:
|
||||||
type: boolean
|
type: boolean
|
||||||
inputVideoFormat:
|
description: True when OSC has a nonzero configured port.
|
||||||
|
listening:
|
||||||
|
type: boolean
|
||||||
|
description: False in the current native host because UDP OSC ingress is only stubbed.
|
||||||
|
bindAddress:
|
||||||
type: string
|
type: string
|
||||||
inputFrameRate:
|
port:
|
||||||
type: string
|
type: number
|
||||||
outputVideoFormat:
|
smoothing:
|
||||||
type: string
|
type: number
|
||||||
outputFrameRate:
|
statusMessage:
|
||||||
type: string
|
type: string
|
||||||
|
additionalProperties: false
|
||||||
RuntimeStatus:
|
RuntimeStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -416,6 +858,52 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
modeName:
|
modeName:
|
||||||
type: string
|
type: string
|
||||||
|
VideoOutputStatus:
|
||||||
|
type: object
|
||||||
|
description: Backend-neutral output telemetry. Backend-specific counters live under `backendMetrics`.
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
|
example: decklink
|
||||||
|
statusMessage:
|
||||||
|
type: string
|
||||||
|
scheduleFailures:
|
||||||
|
type: number
|
||||||
|
completions:
|
||||||
|
type: number
|
||||||
|
late:
|
||||||
|
type: number
|
||||||
|
dropped:
|
||||||
|
type: number
|
||||||
|
backendMetrics:
|
||||||
|
$ref: "#/components/schemas/VideoOutputBackendMetrics"
|
||||||
|
VideoOutputBackendMetrics:
|
||||||
|
type: object
|
||||||
|
description: Backend-specific output metrics. For `decklink`, this contains DeckLink schedule and buffer telemetry.
|
||||||
|
additionalProperties: true
|
||||||
|
properties:
|
||||||
|
bufferedAvailable:
|
||||||
|
type: boolean
|
||||||
|
buffered:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
scheduleCallMs:
|
||||||
|
type: number
|
||||||
|
scheduleLeadAvailable:
|
||||||
|
type: boolean
|
||||||
|
scheduleLeadFrames:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
playbackFrameIndex:
|
||||||
|
type: number
|
||||||
|
nextScheduleFrameIndex:
|
||||||
|
type: number
|
||||||
|
playbackStreamTime:
|
||||||
|
type: number
|
||||||
|
scheduleRealignments:
|
||||||
|
type: number
|
||||||
DeckLinkStatus:
|
DeckLinkStatus:
|
||||||
type: object
|
type: object
|
||||||
deprecated: true
|
deprecated: true
|
||||||
@@ -462,10 +950,12 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
renderMs:
|
renderMs:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||||
smoothedRenderMs:
|
smoothedRenderMs:
|
||||||
type: number
|
type: number
|
||||||
budgetUsedPercent:
|
budgetUsedPercent:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||||
completionIntervalMs:
|
completionIntervalMs:
|
||||||
type: number
|
type: number
|
||||||
smoothedCompletionIntervalMs:
|
smoothedCompletionIntervalMs:
|
||||||
@@ -478,6 +968,262 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
flushedFrameCount:
|
flushedFrameCount:
|
||||||
type: number
|
type: number
|
||||||
|
cadence:
|
||||||
|
$ref: "#/components/schemas/CadenceTelemetry"
|
||||||
|
CadenceTelemetry:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
clockOverruns:
|
||||||
|
type: number
|
||||||
|
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
|
||||||
|
clockSkippedFrames:
|
||||||
|
type: number
|
||||||
|
description: Total render cadence frame intervals skipped instead of catch-up rendering.
|
||||||
|
clockOveruns:
|
||||||
|
type: number
|
||||||
|
deprecated: true
|
||||||
|
description: Deprecated misspelled alias for clockOverruns.
|
||||||
|
clockSkipped:
|
||||||
|
type: number
|
||||||
|
deprecated: true
|
||||||
|
description: Deprecated alias for clockSkippedFrames.
|
||||||
|
renderFrameMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
|
||||||
|
renderFrameBudgetUsedPercent:
|
||||||
|
type: number
|
||||||
|
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
|
||||||
|
renderFrameMaxMs:
|
||||||
|
type: number
|
||||||
|
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
|
||||||
|
readbackQueueMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent duration spent queueing async PBO readback for the selected system-frame pixel format after rendering.
|
||||||
|
completedReadbackCopyMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent duration spent mapping and copying a completed readback into system-memory frame storage.
|
||||||
|
completedDrops:
|
||||||
|
type: number
|
||||||
|
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
|
||||||
|
acquireMisses:
|
||||||
|
type: number
|
||||||
|
description: Number of times render/readback could not acquire a writable system-memory frame slot.
|
||||||
|
inputFramesReceived:
|
||||||
|
type: number
|
||||||
|
inputFramesDropped:
|
||||||
|
type: number
|
||||||
|
inputConsumeMisses:
|
||||||
|
type: number
|
||||||
|
description: Render ticks where no ready input frame was available to upload.
|
||||||
|
inputUploadMisses:
|
||||||
|
type: number
|
||||||
|
description: Input texture upload attempts that reused the previous GL input texture.
|
||||||
|
inputReadyFrames:
|
||||||
|
type: number
|
||||||
|
description: Ready input frames currently queued in the input mailbox.
|
||||||
|
inputReadingFrames:
|
||||||
|
type: number
|
||||||
|
description: Input frames currently protected while render uploads them.
|
||||||
|
inputLatestAgeMs:
|
||||||
|
type: number
|
||||||
|
inputUploadMs:
|
||||||
|
type: number
|
||||||
|
inputCaptureFps:
|
||||||
|
type: number
|
||||||
|
inputConvertMs:
|
||||||
|
type: number
|
||||||
|
inputSubmitMs:
|
||||||
|
type: number
|
||||||
|
inputCaptureFormat:
|
||||||
|
type: string
|
||||||
|
deckLinkScheduleLeadAvailable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether DeckLink playback stream-time lead telemetry is currently available.
|
||||||
|
deckLinkScheduleLeadFrames:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
|
||||||
|
deckLinkPlaybackFrameIndex:
|
||||||
|
type: number
|
||||||
|
description: DeckLink playback stream time converted to frame index at the configured output cadence.
|
||||||
|
deckLinkNextScheduleFrameIndex:
|
||||||
|
type: number
|
||||||
|
description: Next frame index the app scheduler will assign to a DeckLink output frame.
|
||||||
|
deckLinkPlaybackStreamTime:
|
||||||
|
type: number
|
||||||
|
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
|
||||||
|
deckLinkScheduleRealignments:
|
||||||
|
type: number
|
||||||
|
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
|
||||||
|
BackendPlayoutStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lifecycleState:
|
||||||
|
type: string
|
||||||
|
example: running
|
||||||
|
degraded:
|
||||||
|
type: boolean
|
||||||
|
statusMessage:
|
||||||
|
type: string
|
||||||
|
lateFrameCount:
|
||||||
|
type: number
|
||||||
|
droppedFrameCount:
|
||||||
|
type: number
|
||||||
|
flushedFrameCount:
|
||||||
|
type: number
|
||||||
|
readyQueue:
|
||||||
|
$ref: "#/components/schemas/BackendReadyQueueStatus"
|
||||||
|
outputRender:
|
||||||
|
$ref: "#/components/schemas/BackendOutputRenderStatus"
|
||||||
|
recovery:
|
||||||
|
$ref: "#/components/schemas/BackendPlayoutRecoveryStatus"
|
||||||
|
BackendReadyQueueStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
depth:
|
||||||
|
type: number
|
||||||
|
description: Current number of ready output frames.
|
||||||
|
capacity:
|
||||||
|
type: number
|
||||||
|
description: Maximum ready output frames currently allowed.
|
||||||
|
minDepth:
|
||||||
|
type: number
|
||||||
|
description: Minimum observed ready queue depth since backend worker start.
|
||||||
|
maxDepth:
|
||||||
|
type: number
|
||||||
|
description: Maximum observed ready queue depth since backend worker start.
|
||||||
|
zeroDepthCount:
|
||||||
|
type: number
|
||||||
|
description: Number of observed samples where the ready queue was empty.
|
||||||
|
pushedCount:
|
||||||
|
type: number
|
||||||
|
poppedCount:
|
||||||
|
type: number
|
||||||
|
droppedCount:
|
||||||
|
type: number
|
||||||
|
underrunCount:
|
||||||
|
type: number
|
||||||
|
BackendOutputRenderStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
renderMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent output render duration in milliseconds.
|
||||||
|
smoothedRenderMs:
|
||||||
|
type: number
|
||||||
|
description: Smoothed output render duration in milliseconds.
|
||||||
|
maxRenderMs:
|
||||||
|
type: number
|
||||||
|
description: Maximum observed output render duration in milliseconds.
|
||||||
|
acquireFrameMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent acquiring a writable backend output frame in milliseconds.
|
||||||
|
renderRequestMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent executing the render-thread output frame request in milliseconds.
|
||||||
|
endAccessMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent ending write access to the backend output frame in milliseconds.
|
||||||
|
queueWaitMs:
|
||||||
|
type: number
|
||||||
|
description: Time the output render request spent waiting for the render thread in milliseconds.
|
||||||
|
drawMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent drawing, blitting, packing, and flushing the output frame in milliseconds.
|
||||||
|
fenceWaitMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent waiting for the async readback fence in milliseconds.
|
||||||
|
mapMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent mapping the async readback pixel buffer in milliseconds.
|
||||||
|
readbackCopyMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent copying async readback bytes into the backend output frame in milliseconds.
|
||||||
|
cachedCopyMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent copying the cached output frame when async readback is not ready in milliseconds.
|
||||||
|
asyncQueueMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent queueing the next async readback in milliseconds.
|
||||||
|
asyncQueueBufferMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent orphaning or allocating the async readback pixel buffer in milliseconds.
|
||||||
|
asyncQueueSetupMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent applying readback pixel-store, framebuffer, and pixel-pack-buffer state in milliseconds.
|
||||||
|
asyncQueueReadPixelsMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent issuing glReadPixels for the async readback in milliseconds.
|
||||||
|
asyncQueueFenceMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent creating the async readback fence in milliseconds.
|
||||||
|
syncReadMs:
|
||||||
|
type: number
|
||||||
|
description: Time spent in bootstrap synchronous readback in milliseconds.
|
||||||
|
asyncReadbackMissCount:
|
||||||
|
type: number
|
||||||
|
description: Count of output render requests where async readback was not ready.
|
||||||
|
cachedFallbackCount:
|
||||||
|
type: number
|
||||||
|
description: Count of output render requests served from the cached output frame.
|
||||||
|
syncFallbackCount:
|
||||||
|
type: number
|
||||||
|
description: Count of output render requests that used bootstrap synchronous readback.
|
||||||
|
BackendPlayoutRecoveryStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
completionResult:
|
||||||
|
type: string
|
||||||
|
enum: [Completed, DisplayedLate, Dropped, Flushed, Unknown]
|
||||||
|
completedFrameIndex:
|
||||||
|
type: number
|
||||||
|
scheduledFrameIndex:
|
||||||
|
type: number
|
||||||
|
scheduledLeadFrames:
|
||||||
|
type: number
|
||||||
|
measuredLagFrames:
|
||||||
|
type: number
|
||||||
|
catchUpFrames:
|
||||||
|
type: number
|
||||||
|
lateStreak:
|
||||||
|
type: number
|
||||||
|
dropStreak:
|
||||||
|
type: number
|
||||||
|
RuntimeEventStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
queue:
|
||||||
|
$ref: "#/components/schemas/RuntimeEventQueueStatus"
|
||||||
|
dispatch:
|
||||||
|
$ref: "#/components/schemas/RuntimeEventDispatchStatus"
|
||||||
|
RuntimeEventQueueStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
depth:
|
||||||
|
type: number
|
||||||
|
capacity:
|
||||||
|
type: number
|
||||||
|
droppedCount:
|
||||||
|
type: number
|
||||||
|
oldestEventAgeMs:
|
||||||
|
type: number
|
||||||
|
RuntimeEventDispatchStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
dispatchCallCount:
|
||||||
|
type: number
|
||||||
|
dispatchedEventCount:
|
||||||
|
type: number
|
||||||
|
handlerInvocationCount:
|
||||||
|
type: number
|
||||||
|
handlerFailureCount:
|
||||||
|
type: number
|
||||||
|
lastDispatchDurationMs:
|
||||||
|
type: number
|
||||||
|
maxDispatchDurationMs:
|
||||||
|
type: number
|
||||||
ShaderSummary:
|
ShaderSummary:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -497,6 +1243,31 @@ components:
|
|||||||
description: Error text for unavailable shader packages.
|
description: Error text for unavailable shader packages.
|
||||||
temporal:
|
temporal:
|
||||||
$ref: "#/components/schemas/TemporalState"
|
$ref: "#/components/schemas/TemporalState"
|
||||||
|
feedback:
|
||||||
|
$ref: "#/components/schemas/FeedbackState"
|
||||||
|
ui:
|
||||||
|
$ref: "#/components/schemas/ShaderUiDefinition"
|
||||||
|
nullable: true
|
||||||
|
ShaderUiDefinition:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [webComponent]
|
||||||
|
entry:
|
||||||
|
type: string
|
||||||
|
description: Package-relative JavaScript module path from the shader manifest.
|
||||||
|
example: ui/controls.js
|
||||||
|
tag:
|
||||||
|
type: string
|
||||||
|
description: Custom element tag registered by the module.
|
||||||
|
example: my-shader-controls
|
||||||
|
assetUrl:
|
||||||
|
type: string
|
||||||
|
description: HTTP URL for loading the JavaScript module from the local control server.
|
||||||
|
example: /shader-assets/my-shader/ui/controls.js
|
||||||
|
additionalProperties: false
|
||||||
TemporalState:
|
TemporalState:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -509,6 +1280,13 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
effectiveHistoryLength:
|
effectiveHistoryLength:
|
||||||
type: number
|
type: number
|
||||||
|
FeedbackState:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
writePass:
|
||||||
|
type: string
|
||||||
LayerState:
|
LayerState:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -522,6 +1300,9 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
temporal:
|
temporal:
|
||||||
$ref: "#/components/schemas/TemporalState"
|
$ref: "#/components/schemas/TemporalState"
|
||||||
|
ui:
|
||||||
|
$ref: "#/components/schemas/ShaderUiDefinition"
|
||||||
|
nullable: true
|
||||||
parameters:
|
parameters:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -570,6 +1351,9 @@ components:
|
|||||||
font:
|
font:
|
||||||
type: string
|
type: string
|
||||||
description: Font asset id used by text parameters, when declared.
|
description: Font asset id used by text parameters, when declared.
|
||||||
|
fontParameter:
|
||||||
|
type: string
|
||||||
|
description: Enum parameter id used to select a text parameter font at runtime.
|
||||||
value:
|
value:
|
||||||
description: Current parameter value.
|
description: Current parameter value.
|
||||||
oneOf:
|
oneOf:
|
||||||
|
|||||||
@@ -1,589 +0,0 @@
|
|||||||
# ControlServices Subsystem Design
|
|
||||||
|
|
||||||
This document expands the `ControlServices` subsystem described in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target role of `ControlServices` as the ingress boundary for non-render control sources and the normalization layer that turns external input into typed internal actions.
|
|
||||||
|
|
||||||
The intent here is to make `ControlServices` explicit enough that later phases can extract it from the current `RuntimeServices` / `ControlServer` / `OscServer` mix without inventing new boundaries ad hoc.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
`ControlServices` is the subsystem that accepts external control traffic and turns it into safe, typed, low-cost input for the rest of the app.
|
|
||||||
|
|
||||||
In the target architecture, `ControlServices` should:
|
|
||||||
|
|
||||||
- own ingress for OSC, HTTP/REST-style control routes, WebSocket session management, and file-watch/reload signals
|
|
||||||
- normalize transport-specific payloads into typed internal actions or events
|
|
||||||
- apply ingress-local buffering, coalescing, deduplication, and rate limiting where useful
|
|
||||||
- expose service timing and health observations to `HealthTelemetry`
|
|
||||||
- forward normalized actions into `RuntimeCoordinator`
|
|
||||||
|
|
||||||
It should not:
|
|
||||||
|
|
||||||
- decide persistence policy
|
|
||||||
- mutate persisted state directly
|
|
||||||
- build render snapshots
|
|
||||||
- own render-local overlay state
|
|
||||||
- own device timing or playout policy
|
|
||||||
|
|
||||||
This subsystem is intentionally narrow in authority and broad in transport coverage.
|
|
||||||
|
|
||||||
## Why This Subsystem Exists
|
|
||||||
|
|
||||||
Today the app already has a recognizable control-services slice, but it is spread across several classes:
|
|
||||||
|
|
||||||
- `RuntimeServices` hosts control server startup, OSC queues, deferred OSC commits, and file-watch polling:
|
|
||||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:24)
|
|
||||||
- `ControlServer` owns HTTP, WebSocket upgrade, static asset serving, and direct callback-based route dispatch:
|
|
||||||
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:15)
|
|
||||||
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:88)
|
|
||||||
- `OscServer` owns UDP socket receive, OSC decode, and parameter callback dispatch:
|
|
||||||
- [OscServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h:11)
|
|
||||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:58)
|
|
||||||
|
|
||||||
The current shape works, but it mixes:
|
|
||||||
|
|
||||||
- transport handling
|
|
||||||
- action normalization
|
|
||||||
- direct callback dispatch
|
|
||||||
- coarse background polling
|
|
||||||
- transient queue ownership
|
|
||||||
- UI broadcast behavior
|
|
||||||
- partial runtime mutation coordination
|
|
||||||
|
|
||||||
That overlap is exactly what Phase 1 is trying to remove.
|
|
||||||
|
|
||||||
## Design Goals
|
|
||||||
|
|
||||||
`ControlServices` should optimize for:
|
|
||||||
|
|
||||||
- low-latency ingress without forcing immediate whole-app work
|
|
||||||
- clear transport boundaries
|
|
||||||
- deterministic normalization of external input
|
|
||||||
- isolation of service-specific timing concerns
|
|
||||||
- easy replacement of polling flows with typed events
|
|
||||||
- no direct knowledge of render-local implementation details
|
|
||||||
- safe behavior under bursty traffic such as high-rate OSC
|
|
||||||
|
|
||||||
## Subsystem Responsibilities
|
|
||||||
|
|
||||||
`ControlServices` owns the following concerns.
|
|
||||||
|
|
||||||
### 1. Transport Ingress
|
|
||||||
|
|
||||||
It accepts input from external control-facing sources such as:
|
|
||||||
|
|
||||||
- OSC/UDP parameter control
|
|
||||||
- HTTP API requests from the native control UI or external clients
|
|
||||||
- WebSocket connection lifecycle for state consumers
|
|
||||||
- file-watch triggers and manual reload requests
|
|
||||||
- future automation ingress such as MIDI, serial, or remote control bridges
|
|
||||||
|
|
||||||
The key rule is that transport-specific details stop here.
|
|
||||||
|
|
||||||
### 2. Action Normalization
|
|
||||||
|
|
||||||
Every ingress path should be converted into a typed internal action or event before it touches runtime policy.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- OSC `/layer/param` traffic becomes `AutomationTargetReceived`
|
|
||||||
- `POST /api/layers/add` becomes `LayerAddRequested`
|
|
||||||
- `POST /api/reload` becomes `ShaderReloadRequested`
|
|
||||||
- file-watch changes become `RegistryChangedDetected` or `ReloadRequested`
|
|
||||||
|
|
||||||
The rest of the app should not need to know whether an action came from UDP, HTTP, the embedded UI, or a background watcher.
|
|
||||||
|
|
||||||
### 3. Ingress-Local Buffering and Coalescing
|
|
||||||
|
|
||||||
`ControlServices` may maintain short-lived queues or coalesced maps when that is the correct place to absorb bursty input.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- latest-value coalescing per OSC route
|
|
||||||
- pending reload edge detection
|
|
||||||
- bounded outbound state-broadcast requests
|
|
||||||
- short-lived delivery queues for already-classified follow-up work, as long as commit and persistence policy still belong to `RuntimeCoordinator`
|
|
||||||
|
|
||||||
This state is ingress-local and must not become a substitute for committed runtime state.
|
|
||||||
|
|
||||||
### 4. WebSocket Session Management
|
|
||||||
|
|
||||||
The subsystem owns connection lifecycle for clients that observe runtime state, but it does not own the authoritative runtime model.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- accepting WebSocket upgrades
|
|
||||||
- tracking connected clients
|
|
||||||
- forwarding serialized state snapshots or health payloads produced elsewhere
|
|
||||||
- applying broadcast throttling or collapse policies when necessary
|
|
||||||
|
|
||||||
It is not responsible for deciding what the authoritative state is.
|
|
||||||
|
|
||||||
### 5. File-Watch and Reload Ingress
|
|
||||||
|
|
||||||
The subsystem should own the detection side of registry/file changes and reload requests.
|
|
||||||
|
|
||||||
It may:
|
|
||||||
|
|
||||||
- observe filesystem changes
|
|
||||||
- debounce bursts of related file events
|
|
||||||
- translate those changes into typed reload actions
|
|
||||||
|
|
||||||
It should not directly trigger render rebuilds or mutate shader/package state itself.
|
|
||||||
|
|
||||||
### 6. Service Health and Timing Reporting
|
|
||||||
|
|
||||||
`ControlServices` should emit operational signals into `HealthTelemetry`, including:
|
|
||||||
|
|
||||||
- OSC packet rate
|
|
||||||
- OSC decode failures
|
|
||||||
- queue depth / coalesced route count
|
|
||||||
- dropped or collapsed ingress events
|
|
||||||
- HTTP error counts
|
|
||||||
- WebSocket connection count
|
|
||||||
- reload request frequency
|
|
||||||
- file-watch failures
|
|
||||||
- service-thread startup/shutdown errors
|
|
||||||
|
|
||||||
## Explicit Non-Responsibilities
|
|
||||||
|
|
||||||
The following must stay outside `ControlServices` in the target design.
|
|
||||||
|
|
||||||
### Persistence Decisions
|
|
||||||
|
|
||||||
The subsystem may report that an input requested a state change, but it should not decide whether that change is persisted.
|
|
||||||
|
|
||||||
That belongs to `RuntimeCoordinator` and `RuntimeStore`.
|
|
||||||
|
|
||||||
### Render Snapshot Publication
|
|
||||||
|
|
||||||
`ControlServices` must not publish render-facing snapshots or poke render-local structures directly.
|
|
||||||
|
|
||||||
### Render-Local Overlay Ownership
|
|
||||||
|
|
||||||
Live OSC overlays, temporal state, shader feedback, and render-only transient state belong to `RenderEngine`.
|
|
||||||
|
|
||||||
`ControlServices` may ingest automation targets, but it should not own how those targets are applied inside the render domain.
|
|
||||||
|
|
||||||
### Hardware Timing or Playout Recovery
|
|
||||||
|
|
||||||
Device scheduling, queue headroom, and callback recovery belong to `VideoBackend`, not the control ingress path.
|
|
||||||
|
|
||||||
## Ingress Boundary Model
|
|
||||||
|
|
||||||
The clean boundary for `ControlServices` is:
|
|
||||||
|
|
||||||
- external transport in
|
|
||||||
- typed action/event out
|
|
||||||
|
|
||||||
That implies three layers inside the subsystem.
|
|
||||||
|
|
||||||
### Transport Adapters
|
|
||||||
|
|
||||||
These are protocol-facing components.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- `OscIngress`
|
|
||||||
- `HttpControlIngress`
|
|
||||||
- `WebSocketSessionHost`
|
|
||||||
- `FileWatchIngress`
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- socket/file watcher lifecycle
|
|
||||||
- protocol decoding
|
|
||||||
- request framing
|
|
||||||
- transport-level validation
|
|
||||||
- low-level authentication or origin checks later if added
|
|
||||||
|
|
||||||
### Normalization Layer
|
|
||||||
|
|
||||||
This layer translates decoded transport input into typed actions.
|
|
||||||
|
|
||||||
Responsibilities:
|
|
||||||
|
|
||||||
- route parsing
|
|
||||||
- payload type normalization
|
|
||||||
- parameter name/key resolution where that is purely syntactic
|
|
||||||
- conversion from transport-specific errors into typed ingress errors
|
|
||||||
|
|
||||||
This layer should not perform deep runtime mutation policy.
|
|
||||||
|
|
||||||
### Service Coordination Shell
|
|
||||||
|
|
||||||
This shell owns:
|
|
||||||
|
|
||||||
- startup/shutdown ordering for ingress services
|
|
||||||
- shared ingress-local queues
|
|
||||||
- service-thread lifecycle
|
|
||||||
- handing normalized actions to `RuntimeCoordinator`
|
|
||||||
- handing outbound snapshot payloads to WebSocket clients
|
|
||||||
|
|
||||||
This shell is the spiritual successor to the hosting part of current `RuntimeServices`, but with a much narrower responsibility set.
|
|
||||||
|
|
||||||
## Service Timing Concerns
|
|
||||||
|
|
||||||
`ControlServices` is the correct place to isolate transport-level timing concerns that should not leak into whole-app state policy.
|
|
||||||
|
|
||||||
### OSC Timing
|
|
||||||
|
|
||||||
Current behavior already points in the right direction:
|
|
||||||
|
|
||||||
- OSC receive is on its own thread in `OscServer`
|
|
||||||
- latest values are coalesced by route in `RuntimeServices`
|
|
||||||
- updates are applied once per render tick rather than per packet
|
|
||||||
|
|
||||||
Relevant code:
|
|
||||||
|
|
||||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:95)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:65)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:82)
|
|
||||||
|
|
||||||
Target rule:
|
|
||||||
|
|
||||||
- network receive and decode stay inside `ControlServices`
|
|
||||||
- coalescing policy stays inside `ControlServices`
|
|
||||||
- classification of the resulting action belongs to `RuntimeCoordinator`
|
|
||||||
- render-local application belongs to `RenderEngine`
|
|
||||||
|
|
||||||
This keeps high-rate ingress cheap without giving the service layer authority over render behavior or committed-state policy.
|
|
||||||
|
|
||||||
### HTTP / UI Timing
|
|
||||||
|
|
||||||
HTTP control requests are operator-facing and usually low-rate, but the UI can still generate bursts through slider drags or repeated parameter edits.
|
|
||||||
|
|
||||||
`ControlServices` should:
|
|
||||||
|
|
||||||
- normalize each request into a typed action
|
|
||||||
- allow collapse/throttle policies for purely observational outbound state pushes
|
|
||||||
- avoid synchronous full-state serialization on every ingress event where possible
|
|
||||||
|
|
||||||
It should not decide whether a request results in immediate, deferred, transient, or persisted mutation. That is a coordinator concern.
|
|
||||||
|
|
||||||
### WebSocket Broadcast Timing
|
|
||||||
|
|
||||||
Outbound state streaming is control-plane behavior, not core runtime ownership.
|
|
||||||
|
|
||||||
Current code already distinguishes immediate and requested broadcasts:
|
|
||||||
|
|
||||||
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:163)
|
|
||||||
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:170)
|
|
||||||
|
|
||||||
Target rule:
|
|
||||||
|
|
||||||
- `ControlServices` may own broadcast scheduling and collapse policy
|
|
||||||
- the source state payload should come from snapshot/telemetry producers, not from service-owned mutable state
|
|
||||||
|
|
||||||
### File-Watch Timing
|
|
||||||
|
|
||||||
Current file-watch and deferred OSC commit work run on a coarse poll loop:
|
|
||||||
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
|
||||||
|
|
||||||
This is one of the cleaner migration opportunities in the whole app.
|
|
||||||
|
|
||||||
Target rule:
|
|
||||||
|
|
||||||
- file-watch detection belongs in `ControlServices`
|
|
||||||
- coarse polling should eventually be replaced with either event-driven watching or a narrower, typed background loop
|
|
||||||
- detected changes should be debounced and surfaced as typed reload-related actions
|
|
||||||
|
|
||||||
### Service Backpressure
|
|
||||||
|
|
||||||
`ControlServices` needs explicit backpressure rules for high-rate sources.
|
|
||||||
|
|
||||||
Recommended policies:
|
|
||||||
|
|
||||||
- coalesce latest-value automation by route
|
|
||||||
- bound per-service queues
|
|
||||||
- count and report dropped/coalesced events
|
|
||||||
- prefer collapsing observation work before collapsing operator mutations
|
|
||||||
- never let service queues become hidden durable state
|
|
||||||
|
|
||||||
## Interfaces
|
|
||||||
|
|
||||||
These are suggested target-facing interfaces, not final class signatures.
|
|
||||||
|
|
||||||
### Subsystem Shell
|
|
||||||
|
|
||||||
Possible top-level responsibilities:
|
|
||||||
|
|
||||||
- `Start(...)`
|
|
||||||
- `Stop()`
|
|
||||||
- `PublishStateSnapshot(...)`
|
|
||||||
- `PublishHealthSnapshot(...)`
|
|
||||||
- `DrainNormalizedActions(...)`
|
|
||||||
|
|
||||||
The shell should feel like a host for ingress adapters plus a normalization/buffering boundary.
|
|
||||||
|
|
||||||
### OSC Ingress
|
|
||||||
|
|
||||||
Possible responsibilities:
|
|
||||||
|
|
||||||
- `StartOscIngress(...)`
|
|
||||||
- `StopOscIngress()`
|
|
||||||
- `ConfigureOscBinding(...)`
|
|
||||||
- `EnqueueDecodedOscMessage(...)`
|
|
||||||
- `DrainCoalescedAutomationTargets(...)`
|
|
||||||
|
|
||||||
### HTTP / Web Control Ingress
|
|
||||||
|
|
||||||
Possible responsibilities:
|
|
||||||
|
|
||||||
- `StartHttpIngress(...)`
|
|
||||||
- `StopHttpIngress()`
|
|
||||||
- `HandleHttpRequest(...)`
|
|
||||||
- `HandleWebSocketUpgrade(...)`
|
|
||||||
- `QueueStateBroadcastRequest()`
|
|
||||||
|
|
||||||
### File-Watch Ingress
|
|
||||||
|
|
||||||
Possible responsibilities:
|
|
||||||
|
|
||||||
- `StartFileWatchIngress(...)`
|
|
||||||
- `StopFileWatchIngress()`
|
|
||||||
- `PollOrConsumeFileEvents(...)`
|
|
||||||
- `DrainReloadSignals(...)`
|
|
||||||
|
|
||||||
### Normalized Action Types
|
|
||||||
|
|
||||||
These should likely become shared event/action definitions in Phase 2, but `ControlServices` should be designed around them now.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- `LayerAddRequested`
|
|
||||||
- `LayerRemovedRequested`
|
|
||||||
- `LayerReorderedRequested`
|
|
||||||
- `LayerBypassSetRequested`
|
|
||||||
- `LayerShaderSetRequested`
|
|
||||||
- `ParameterSetRequested`
|
|
||||||
- `LayerResetRequested`
|
|
||||||
- `StackPresetSaveRequested`
|
|
||||||
- `StackPresetLoadRequested`
|
|
||||||
- `ShaderReloadRequested`
|
|
||||||
- `ScreenshotRequested`
|
|
||||||
- `AutomationTargetReceived`
|
|
||||||
- `RegistryChangeDetected`
|
|
||||||
|
|
||||||
## Data Ownership Inside The Subsystem
|
|
||||||
|
|
||||||
`ControlServices` is allowed to own ingress-local ephemeral state.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- connected WebSocket client list
|
|
||||||
- pending broadcast flag
|
|
||||||
- coalesced OSC route map
|
|
||||||
- outstanding decoded-but-undrained action queue
|
|
||||||
- file-watch debounce state
|
|
||||||
- transport error counters before publication to telemetry
|
|
||||||
|
|
||||||
It should not own:
|
|
||||||
|
|
||||||
- authoritative layer stack state
|
|
||||||
- committed parameter values
|
|
||||||
- render snapshots
|
|
||||||
- playout queue state
|
|
||||||
- shader feedback or render overlays
|
|
||||||
|
|
||||||
The rule is simple:
|
|
||||||
|
|
||||||
- if the state exists only to absorb or forward external input, it can live here
|
|
||||||
- if the state defines how the app should behave over time, it belongs elsewhere
|
|
||||||
|
|
||||||
## Outbound Boundaries
|
|
||||||
|
|
||||||
`ControlServices` talks outward in only a few approved directions.
|
|
||||||
|
|
||||||
### To `RuntimeCoordinator`
|
|
||||||
|
|
||||||
Primary outbound path.
|
|
||||||
|
|
||||||
It sends:
|
|
||||||
|
|
||||||
- normalized mutation requests
|
|
||||||
- automation targets
|
|
||||||
- reload requests
|
|
||||||
- stack preset requests
|
|
||||||
|
|
||||||
It does not send:
|
|
||||||
|
|
||||||
- transport-specific objects such as raw sockets or OSC packet structures
|
|
||||||
- render-facing state objects
|
|
||||||
|
|
||||||
### To `HealthTelemetry`
|
|
||||||
|
|
||||||
Observation-only relationship.
|
|
||||||
|
|
||||||
It sends:
|
|
||||||
|
|
||||||
- counters
|
|
||||||
- warnings
|
|
||||||
- timing samples
|
|
||||||
- service health transitions
|
|
||||||
|
|
||||||
It should not use `HealthTelemetry` as a hidden control path.
|
|
||||||
|
|
||||||
### From Snapshot / Telemetry Producers To Web Clients
|
|
||||||
|
|
||||||
`ControlServices` may deliver serialized outbound payloads to WebSocket clients, but the authoritative payload contents should be produced by the owning subsystems.
|
|
||||||
|
|
||||||
That means a later design may look like:
|
|
||||||
|
|
||||||
- `RuntimeSnapshotProvider` provides render-facing snapshot payloads or a runtime-state projection derived from those published snapshots
|
|
||||||
- `RuntimeCoordinator` or a later runtime-read-model helper provides control-plane runtime summaries when the UI needs more than raw render state
|
|
||||||
- `HealthTelemetry` provides health payloads
|
|
||||||
- `ControlServices` delivers them to connected observers
|
|
||||||
|
|
||||||
## Current Code Mapping
|
|
||||||
|
|
||||||
This section maps the current implementation onto the target subsystem.
|
|
||||||
|
|
||||||
### Current `RuntimeServices`
|
|
||||||
|
|
||||||
Should split into:
|
|
||||||
|
|
||||||
- `ControlServices` shell
|
|
||||||
- temporary compatibility adapter into `RuntimeCoordinator`
|
|
||||||
- removal of any direct runtime-state mutation responsibilities over time
|
|
||||||
|
|
||||||
Likely keep under `ControlServices`:
|
|
||||||
|
|
||||||
- service startup/shutdown
|
|
||||||
- OSC update coalescing
|
|
||||||
- Web control hosting shell
|
|
||||||
- file-watch ingress hosting
|
|
||||||
|
|
||||||
Should move out later:
|
|
||||||
|
|
||||||
- direct `RuntimeHost` polling dependency
|
|
||||||
- deferred OSC commit behavior as currently implemented through direct host mutation
|
|
||||||
- any remaining direct state-broadcast decisions tied to runtime internals
|
|
||||||
|
|
||||||
### Current `ControlServer`
|
|
||||||
|
|
||||||
Should become primarily:
|
|
||||||
|
|
||||||
- HTTP ingress adapter
|
|
||||||
- WebSocket session host
|
|
||||||
- static asset/doc host if that remains embedded
|
|
||||||
|
|
||||||
The callback table in current code:
|
|
||||||
|
|
||||||
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:18)
|
|
||||||
|
|
||||||
is a useful migration aid, but long-term it should evolve from callback-per-action toward typed action emission.
|
|
||||||
|
|
||||||
### Current `OscServer`
|
|
||||||
|
|
||||||
Should remain transport-focused.
|
|
||||||
|
|
||||||
Its clean long-term responsibilities are:
|
|
||||||
|
|
||||||
- UDP socket lifecycle
|
|
||||||
- OSC frame decode
|
|
||||||
- syntactic route extraction
|
|
||||||
- emitting decoded automation payloads into the `ControlServices` shell
|
|
||||||
|
|
||||||
It should not own any runtime state semantics beyond ingress decoding.
|
|
||||||
|
|
||||||
## Migration Plan
|
|
||||||
|
|
||||||
The safest migration is incremental.
|
|
||||||
|
|
||||||
### Step 1. Name The Boundary Explicitly
|
|
||||||
|
|
||||||
Create and use the `ControlServices` name in docs and future interfaces before moving all logic.
|
|
||||||
|
|
||||||
This document is part of that step.
|
|
||||||
|
|
||||||
### Step 2. Convert Callback Thinking Into Action Thinking
|
|
||||||
|
|
||||||
Without changing all runtime code at once, introduce typed action/event shapes for the major ingress paths.
|
|
||||||
|
|
||||||
The goal is for transports to emit actions, even if temporary adapters still call into existing code.
|
|
||||||
|
|
||||||
### Step 3. Extract Service Hosting From `OpenGLComposite`
|
|
||||||
|
|
||||||
`OpenGLComposite` currently owns `RuntimeServices` startup and consumption:
|
|
||||||
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:312)
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:723)
|
|
||||||
|
|
||||||
That should move toward a composition root or subsystem host arrangement where render is no longer the owner of control ingress.
|
|
||||||
|
|
||||||
### Step 4. Remove Direct `RuntimeHost` Dependency
|
|
||||||
|
|
||||||
Current polling and deferred OSC commit work directly against `RuntimeHost`:
|
|
||||||
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
|
||||||
|
|
||||||
That should be replaced with coordinator-facing actions and later event-driven flows.
|
|
||||||
|
|
||||||
### Step 5. Split Out Observation Delivery
|
|
||||||
|
|
||||||
WebSocket outbound delivery can stay in `ControlServices`, but serialization ownership should move toward the owning subsystems so the service layer stops assembling authoritative state itself.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
### Risk 1. Recreating `RuntimeHost` Coupling Under A New Name
|
|
||||||
|
|
||||||
If `ControlServices` is allowed to keep direct knowledge of runtime mutation internals, it will become a renamed version of the same coupling problem.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- keep the boundary strict
|
|
||||||
- route mutations through coordinator interfaces
|
|
||||||
- treat direct host calls as temporary compatibility only
|
|
||||||
|
|
||||||
### Risk 2. Service Queues Becoming Hidden State Authority
|
|
||||||
|
|
||||||
Latest-value OSC maps and reload debounce flags are appropriate here. Full committed runtime state is not.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- define ingress-local versus authoritative state explicitly
|
|
||||||
- bound queues
|
|
||||||
- publish queue metrics into telemetry
|
|
||||||
|
|
||||||
### Risk 3. WebSocket Broadcast Path Reintroducing Heavy Synchronous Work
|
|
||||||
|
|
||||||
If `ControlServices` becomes the place where whole runtime state is rebuilt or serialized on every input, it will recreate timing stalls.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- broadcast snapshots produced elsewhere
|
|
||||||
- collapse redundant outbound requests
|
|
||||||
- track serialization/broadcast timing in telemetry
|
|
||||||
|
|
||||||
### Risk 4. Polling Surviving Too Long As Architecture
|
|
||||||
|
|
||||||
Some polling may remain during migration, but it should not become the permanent contract.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- isolate polling behind ingress interfaces
|
|
||||||
- make replacement with event-driven flows a planned Phase 2/3 outcome
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
- Should the embedded static UI/docs hosting stay inside `ControlServices`, or move to a thinner app-shell concern while control APIs remain in `ControlServices`?
|
|
||||||
- Should outbound state for WebSocket clients be one combined payload or separate runtime and health channels?
|
|
||||||
- How much route/key resolution should happen in `ControlServices` versus `RuntimeCoordinator`?
|
|
||||||
- Should any deferred automation-settle delivery remain in `ControlServices`, or should all commit/settle policy move entirely into coordinator/render ownership once the live-state model is formalized?
|
|
||||||
- When file watching is modernized, should reload classification live entirely in `ControlServices`, or should it emit a lower-level `FilesChanged` event and let `RuntimeCoordinator` decide reload semantics?
|
|
||||||
- Will future non-OSC automation sources reuse the same `AutomationTargetReceived` path, or need source-specific typed actions for policy reasons?
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
`ControlServices` should become the app's clean ingress boundary:
|
|
||||||
|
|
||||||
- transport handling stays here
|
|
||||||
- input normalization stays here
|
|
||||||
- ingress-local buffering stays here
|
|
||||||
- mutation policy does not
|
|
||||||
- authoritative runtime state does not
|
|
||||||
- render-local transient state does not
|
|
||||||
|
|
||||||
If later phases keep that line sharp, the app gains a control layer that is fast, testable, and timing-aware without becoming another shared state authority.
|
|
||||||
@@ -1,644 +0,0 @@
|
|||||||
# HealthTelemetry Subsystem Design
|
|
||||||
|
|
||||||
This document expands the `HealthTelemetry` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
|
||||||
|
|
||||||
`HealthTelemetry` is the subsystem that owns operational visibility for the app. Its purpose is to gather health state, warnings, counters, logs, and timing observations from the other subsystems and publish them in a structured way without becoming a second control plane.
|
|
||||||
|
|
||||||
Today, those responsibilities are fragmented across `RuntimeHost` status setters, ad hoc `OutputDebugStringA` calls, callback-local warnings, and UI-facing runtime-state payloads. The result is that the app can often detect problems, but it does not yet have one clear place that answers:
|
|
||||||
|
|
||||||
- what is healthy right now
|
|
||||||
- what is degraded right now
|
|
||||||
- what has recently gone wrong
|
|
||||||
- which subsystem is under pressure
|
|
||||||
- how timing behavior is trending over time
|
|
||||||
|
|
||||||
`HealthTelemetry` is the target boundary that should answer those questions.
|
|
||||||
|
|
||||||
## Why This Subsystem Exists
|
|
||||||
|
|
||||||
The current code already contains meaningful health and timing signals, but they are spread through unrelated ownership domains:
|
|
||||||
|
|
||||||
- `RuntimeHost` stores signal and timing status:
|
|
||||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:41)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1353)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1415)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1441)
|
|
||||||
- render and bridge code report timing by writing back into `RuntimeHost`:
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
|
||||||
- backend warning paths still log directly:
|
|
||||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
|
||||||
- control ingress failures still log directly:
|
|
||||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
|
||||||
|
|
||||||
This creates several recurring problems:
|
|
||||||
|
|
||||||
- health information shares storage and lock scope with runtime state
|
|
||||||
- warnings are not consistently classified by subsystem or severity
|
|
||||||
- timing data is hard to compare across render, control, and backend paths
|
|
||||||
- UI connection state and operational state are too closely coupled
|
|
||||||
- logging is mostly text-first instead of structured-first
|
|
||||||
- recovery behavior is hard to audit because the app does not retain a coherent health snapshot
|
|
||||||
|
|
||||||
`HealthTelemetry` exists so later phases can move timing and health concerns out of `RuntimeHost`, out of callback-local logging, and into one subsystem whose only job is observation and reporting.
|
|
||||||
|
|
||||||
## Design Goals
|
|
||||||
|
|
||||||
`HealthTelemetry` should optimize for:
|
|
||||||
|
|
||||||
- one authoritative home for operational visibility
|
|
||||||
- structured health state per subsystem
|
|
||||||
- timing and counter recording that does not require a UI to be connected
|
|
||||||
- low-friction reporting from render, backend, coordinator, and services
|
|
||||||
- explicit degraded-mode reporting instead of only raw text logs
|
|
||||||
- support for live operator summaries and deeper engineering diagnostics
|
|
||||||
- minimal risk of telemetry writes becoming a render or callback bottleneck
|
|
||||||
|
|
||||||
## Responsibilities
|
|
||||||
|
|
||||||
`HealthTelemetry` owns structured operational visibility.
|
|
||||||
|
|
||||||
Primary responsibilities:
|
|
||||||
|
|
||||||
- accept timing samples from major subsystems
|
|
||||||
- accept counter deltas and point-in-time gauges
|
|
||||||
- accept warning, error, and degraded-state transitions
|
|
||||||
- collect subsystem-scoped health state
|
|
||||||
- collect operator-visible summary state
|
|
||||||
- collect structured log entries
|
|
||||||
- build stable health snapshots for UI, diagnostics, and later persistence/export if desired
|
|
||||||
- retain recent history needed for short-term troubleshooting
|
|
||||||
- classify observations by subsystem, severity, and category
|
|
||||||
|
|
||||||
Secondary responsibilities that still fit here:
|
|
||||||
|
|
||||||
- smoothing or rolling-window summaries for timing metrics
|
|
||||||
- mapping raw subsystem observations into operator-facing health summaries
|
|
||||||
- deduplicating repeated warnings
|
|
||||||
- tracking warning open/clear lifecycles
|
|
||||||
- providing bounded in-memory history for recent logs and warning transitions
|
|
||||||
|
|
||||||
## Explicit Non-Responsibilities
|
|
||||||
|
|
||||||
`HealthTelemetry` should not become a behavior owner.
|
|
||||||
|
|
||||||
It does not own:
|
|
||||||
|
|
||||||
- layer stack truth
|
|
||||||
- persistence policy
|
|
||||||
- render scheduling
|
|
||||||
- DeckLink scheduling
|
|
||||||
- OSC buffering or routing
|
|
||||||
- reload coordination
|
|
||||||
- shader compilation
|
|
||||||
- recovery actions themselves
|
|
||||||
|
|
||||||
It also should not decide:
|
|
||||||
|
|
||||||
- whether render should skip a frame
|
|
||||||
- whether VideoBackend should increase queue depth
|
|
||||||
- whether RuntimeCoordinator should reject a mutation
|
|
||||||
- whether ControlServices should drop or coalesce ingress traffic
|
|
||||||
|
|
||||||
Those decisions belong to the subsystem being observed. `HealthTelemetry` may describe that a subsystem is degraded, but it must not quietly become the mechanism that tells the app how to react.
|
|
||||||
|
|
||||||
## Ownership Boundaries
|
|
||||||
|
|
||||||
`HealthTelemetry` owns the following state categories.
|
|
||||||
|
|
||||||
### Structured Log State
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- subsystem name
|
|
||||||
- severity
|
|
||||||
- category
|
|
||||||
- timestamp
|
|
||||||
- message
|
|
||||||
- optional structured fields such as layer id, preset name, queue depth, or shader id
|
|
||||||
|
|
||||||
This replaces the idea that `OutputDebugStringA` text is itself the main diagnostic product.
|
|
||||||
|
|
||||||
### Warning And Error State
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- active warning set
|
|
||||||
- warning occurrence counts
|
|
||||||
- first-seen and last-seen timestamps
|
|
||||||
- clear timestamps
|
|
||||||
- subsystem-scoped degraded flags
|
|
||||||
|
|
||||||
This is the durable in-memory operational state that should answer "what is currently wrong?" even if no UI was connected when the warning was raised.
|
|
||||||
|
|
||||||
### Timing State
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- render duration
|
|
||||||
- frame budget
|
|
||||||
- playout completion interval
|
|
||||||
- smoothed completion interval
|
|
||||||
- queue depth
|
|
||||||
- input upload skip count
|
|
||||||
- async readback fallback count
|
|
||||||
- control ingress lag or queue depth
|
|
||||||
- snapshot publication cost
|
|
||||||
|
|
||||||
This state should be organized as time-series-like rolling telemetry, not as a grab bag of unrelated `double` fields mixed into the runtime store.
|
|
||||||
|
|
||||||
### Health Snapshot State
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- current subsystem health summaries
|
|
||||||
- current operator-facing overall health summary
|
|
||||||
- most recent warning list
|
|
||||||
- recent counters and timing summaries
|
|
||||||
- "degraded but still running" status
|
|
||||||
|
|
||||||
This is the material that `ControlServices` or a diagnostics endpoint may later publish.
|
|
||||||
|
|
||||||
## State Model
|
|
||||||
|
|
||||||
The subsystem should model health and telemetry in a way that supports both machine-friendly and operator-friendly views.
|
|
||||||
|
|
||||||
Suggested conceptual model:
|
|
||||||
|
|
||||||
- `TelemetryLogEntry`
|
|
||||||
- `TelemetryWarningRecord`
|
|
||||||
- `TelemetryCounterState`
|
|
||||||
- `TelemetryGaugeState`
|
|
||||||
- `TelemetryTimingSeries`
|
|
||||||
- `SubsystemHealthState`
|
|
||||||
- `HealthSnapshot`
|
|
||||||
|
|
||||||
Important distinction:
|
|
||||||
|
|
||||||
- raw observations are append/update operations
|
|
||||||
- health snapshots are derived read models
|
|
||||||
|
|
||||||
That distinction matters because the system should be able to retain richer recent telemetry internally than what is necessarily sent to the UI on every refresh.
|
|
||||||
|
|
||||||
## Subsystem Health Domains
|
|
||||||
|
|
||||||
`HealthTelemetry` should track health by subsystem rather than as one flat status blob.
|
|
||||||
|
|
||||||
At minimum, Phase 1 should assume domains for:
|
|
||||||
|
|
||||||
- `RuntimeStore`
|
|
||||||
- `RuntimeCoordinator`
|
|
||||||
- `RuntimeSnapshotProvider`
|
|
||||||
- `ControlServices`
|
|
||||||
- `RenderEngine`
|
|
||||||
- `VideoBackend`
|
|
||||||
|
|
||||||
Optional cross-cutting domain:
|
|
||||||
|
|
||||||
- `ApplicationShell`
|
|
||||||
|
|
||||||
Each domain should be able to express states such as:
|
|
||||||
|
|
||||||
- `Healthy`
|
|
||||||
- `Warning`
|
|
||||||
- `Degraded`
|
|
||||||
- `Error`
|
|
||||||
- `Unavailable`
|
|
||||||
|
|
||||||
The exact enum can change, but the design should preserve the idea that each subsystem reports into its own health lane first, and only then is an overall status derived.
|
|
||||||
|
|
||||||
## Logging Boundaries
|
|
||||||
|
|
||||||
Logging belongs here, but logging should be structured-first.
|
|
||||||
|
|
||||||
Expected inputs:
|
|
||||||
|
|
||||||
- subsystem-scoped debug information
|
|
||||||
- warning and error messages
|
|
||||||
- recovery events
|
|
||||||
- notable state transitions
|
|
||||||
- significant operator actions that matter for diagnostics
|
|
||||||
|
|
||||||
Expected design rules:
|
|
||||||
|
|
||||||
- textual messages are still useful, but they should be wrapped in a structured log entry
|
|
||||||
- repeated transient failures should be rate-limited or deduplicated at the telemetry layer where possible
|
|
||||||
- log storage should be bounded in memory
|
|
||||||
- UI publication should read from health/log snapshots, not scrape stdout/debug output
|
|
||||||
|
|
||||||
Examples of current direct log paths that should eventually move behind `HealthTelemetry`:
|
|
||||||
|
|
||||||
- OSC decode/dispatch failures
|
|
||||||
- screenshot write failures
|
|
||||||
- DeckLink fallback warnings
|
|
||||||
- late/dropped frame warnings
|
|
||||||
|
|
||||||
## Metrics And Timing Boundaries
|
|
||||||
|
|
||||||
Timing and metrics should also move here, but their ownership line matters.
|
|
||||||
|
|
||||||
`HealthTelemetry` should own:
|
|
||||||
|
|
||||||
- metric collection interfaces
|
|
||||||
- rolling summaries
|
|
||||||
- recent history buffers
|
|
||||||
- warning thresholds if the app later chooses to define them declaratively
|
|
||||||
- operator-facing derived summaries
|
|
||||||
|
|
||||||
The producing subsystem should still own:
|
|
||||||
|
|
||||||
- the meaning of the measurement
|
|
||||||
- when it is sampled
|
|
||||||
- whether it triggers local mitigation
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- `RenderEngine` owns when render duration is sampled
|
|
||||||
- `VideoBackend` owns when queue depth or playout lateness is sampled
|
|
||||||
- `ControlServices` owns when ingress backlog is sampled
|
|
||||||
- `RuntimeSnapshotProvider` owns when snapshot publish/build timing is sampled
|
|
||||||
|
|
||||||
`HealthTelemetry` should not invent those timings by inference. It records them when producers report them.
|
|
||||||
|
|
||||||
## Proposed Interfaces
|
|
||||||
|
|
||||||
These are target-shape interfaces, not final signatures.
|
|
||||||
|
|
||||||
### Write/Record Interface
|
|
||||||
|
|
||||||
Core write-side operations could look like:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
enum class TelemetrySeverity;
|
|
||||||
enum class TelemetrySubsystem;
|
|
||||||
|
|
||||||
struct TelemetryLogEntry;
|
|
||||||
struct TelemetryWarning;
|
|
||||||
struct TelemetryTimingSample;
|
|
||||||
struct TelemetryCounterDelta;
|
|
||||||
struct TelemetryGaugeUpdate;
|
|
||||||
|
|
||||||
class IHealthTelemetry
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
virtual void AppendLogEntry(const TelemetryLogEntry& entry) = 0;
|
|
||||||
virtual void RaiseWarning(const TelemetryWarning& warning) = 0;
|
|
||||||
virtual void ClearWarning(std::string_view warningKey) = 0;
|
|
||||||
virtual void RecordTimingSample(const TelemetryTimingSample& sample) = 0;
|
|
||||||
virtual void RecordCounterDelta(const TelemetryCounterDelta& delta) = 0;
|
|
||||||
virtual void RecordGauge(const TelemetryGaugeUpdate& gauge) = 0;
|
|
||||||
virtual void ReportSubsystemState(TelemetrySubsystem subsystem,
|
|
||||||
SubsystemHealthState state) = 0;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The key is that every subsystem should be able to publish observations without also needing to know how UI payloads, rolling summaries, or log retention are implemented.
|
|
||||||
|
|
||||||
### Read Interface
|
|
||||||
|
|
||||||
Expected read-side operations:
|
|
||||||
|
|
||||||
- `BuildHealthSnapshot()`
|
|
||||||
- `GetSubsystemHealth(...)`
|
|
||||||
- `GetRecentLogs(...)`
|
|
||||||
- `GetActiveWarnings()`
|
|
||||||
- `GetRecentTimingSummary(...)`
|
|
||||||
|
|
||||||
Design notes:
|
|
||||||
|
|
||||||
- the read interface should return stable snapshots or read models
|
|
||||||
- UI/websocket publication should consume those snapshots through `ControlServices`
|
|
||||||
- read-side access should not require direct knowledge of internal ring buffers or lock layout
|
|
||||||
|
|
||||||
## Producer Expectations By Subsystem
|
|
||||||
|
|
||||||
The parent Phase 1 design already allows multiple subsystems to publish into telemetry. This section makes that concrete.
|
|
||||||
|
|
||||||
### From `RuntimeCoordinator`
|
|
||||||
|
|
||||||
Expected observations:
|
|
||||||
|
|
||||||
- mutation rejected
|
|
||||||
- reload requested
|
|
||||||
- preset apply failed
|
|
||||||
- transient state cleared due to compatibility rules
|
|
||||||
- policy-driven degraded notices such as repeated invalid external control input
|
|
||||||
|
|
||||||
### From `RuntimeSnapshotProvider`
|
|
||||||
|
|
||||||
Expected observations:
|
|
||||||
|
|
||||||
- snapshot publication duration
|
|
||||||
- snapshot build failure
|
|
||||||
- snapshot version churn metrics
|
|
||||||
- repeated publish retries or stale-snapshot conditions
|
|
||||||
|
|
||||||
### From `ControlServices`
|
|
||||||
|
|
||||||
Expected observations:
|
|
||||||
|
|
||||||
- OSC decode failures
|
|
||||||
- websocket broadcast failures
|
|
||||||
- REST/control transport errors
|
|
||||||
- ingress queue depth
|
|
||||||
- coalescing/drop counts
|
|
||||||
- file-watch reload request activity
|
|
||||||
|
|
||||||
### From `RenderEngine`
|
|
||||||
|
|
||||||
Expected observations:
|
|
||||||
|
|
||||||
- frame render duration
|
|
||||||
- upload duration
|
|
||||||
- readback duration
|
|
||||||
- fallback to synchronous readback
|
|
||||||
- preview present timing
|
|
||||||
- render-local state resets caused by reload or incompatibility
|
|
||||||
|
|
||||||
### From `VideoBackend`
|
|
||||||
|
|
||||||
Expected observations:
|
|
||||||
|
|
||||||
- current playout queue depth
|
|
||||||
- input signal state
|
|
||||||
- late frames
|
|
||||||
- dropped frames
|
|
||||||
- backend mode changes
|
|
||||||
- fallback from 10-bit to 8-bit input
|
|
||||||
- output-only black-frame mode
|
|
||||||
|
|
||||||
## Current Code Mapping
|
|
||||||
|
|
||||||
The current codebase already contains several telemetry responsibilities that should migrate here.
|
|
||||||
|
|
||||||
### `RuntimeHost` Status Setters
|
|
||||||
|
|
||||||
These are the clearest existing candidates:
|
|
||||||
|
|
||||||
- `SetSignalStatus(...)`
|
|
||||||
- `TrySetSignalStatus(...)`
|
|
||||||
- `SetPerformanceStats(...)`
|
|
||||||
- `TrySetPerformanceStats(...)`
|
|
||||||
- `SetFramePacingStats(...)`
|
|
||||||
- `TrySetFramePacingStats(...)`
|
|
||||||
|
|
||||||
See:
|
|
||||||
|
|
||||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:41)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1353)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1415)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1441)
|
|
||||||
|
|
||||||
In the target architecture, this kind of state should no longer sit on the same object that owns persistent layer truth.
|
|
||||||
|
|
||||||
### Render Timing Production
|
|
||||||
|
|
||||||
Current render timing is produced in:
|
|
||||||
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
|
||||||
|
|
||||||
That timing sample should conceptually become:
|
|
||||||
|
|
||||||
- `RenderEngine -> HealthTelemetry::RecordTimingSample(...)`
|
|
||||||
|
|
||||||
not:
|
|
||||||
|
|
||||||
- `RenderEngine -> RuntimeHost::TrySetPerformanceStats(...)`
|
|
||||||
|
|
||||||
### Playout And Signal Status Production
|
|
||||||
|
|
||||||
Current signal and frame pacing updates are produced in:
|
|
||||||
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:61)
|
|
||||||
|
|
||||||
These should eventually become structured `VideoBackend` observations instead of bridge-to-host status writes.
|
|
||||||
|
|
||||||
### Direct Warning And Log Paths
|
|
||||||
|
|
||||||
Current examples:
|
|
||||||
|
|
||||||
- late/dropped frame warnings:
|
|
||||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
|
||||||
- backend fallback warnings:
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
|
||||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:320)
|
|
||||||
- OSC errors:
|
|
||||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
|
||||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
|
||||||
|
|
||||||
All of these are clear migration candidates for `AppendLogEntry(...)`, `RaiseWarning(...)`, or counter/timing updates.
|
|
||||||
|
|
||||||
## Health Snapshot Contract
|
|
||||||
|
|
||||||
`HealthTelemetry` should expose one coherent health snapshot that other publication layers can consume.
|
|
||||||
|
|
||||||
That snapshot should be able to answer, at minimum:
|
|
||||||
|
|
||||||
- what the overall app health is
|
|
||||||
- whether input signal is present
|
|
||||||
- whether playout is healthy, degraded, or underrunning
|
|
||||||
- whether render timing is within budget
|
|
||||||
- what active warnings exist
|
|
||||||
- what recent notable events occurred
|
|
||||||
- what the current subsystem-specific states are
|
|
||||||
|
|
||||||
The important boundary is:
|
|
||||||
|
|
||||||
- `HealthTelemetry` builds the health snapshot
|
|
||||||
- `ControlServices` may publish it
|
|
||||||
- UI consumes it
|
|
||||||
|
|
||||||
That avoids rebuilding health summaries ad hoc in UI-facing runtime state serializers.
|
|
||||||
|
|
||||||
## Concurrency Expectations
|
|
||||||
|
|
||||||
This subsystem will likely receive updates from multiple threads:
|
|
||||||
|
|
||||||
- control ingress threads
|
|
||||||
- render thread
|
|
||||||
- backend callback threads
|
|
||||||
- coordinator/service threads
|
|
||||||
|
|
||||||
So the design should assume:
|
|
||||||
|
|
||||||
- low-contention write paths
|
|
||||||
- bounded memory
|
|
||||||
- no long-held global mutex that callbacks and render both depend on
|
|
||||||
|
|
||||||
Phase 1 does not require lock-free implementation, but it does require the architecture to avoid recreating the `RuntimeHost` problem where health writes share the same lock as durable state and render-facing concerns.
|
|
||||||
|
|
||||||
Practical expectations:
|
|
||||||
|
|
||||||
- per-domain aggregation or lightweight internal locking is acceptable
|
|
||||||
- read snapshots should be cheap and stable
|
|
||||||
- callback paths should record telemetry cheaply and return
|
|
||||||
|
|
||||||
## Migration Plan From Current Code
|
|
||||||
|
|
||||||
The safest migration path is to peel telemetry responsibilities away from the existing classes incrementally.
|
|
||||||
|
|
||||||
### Step 1: Introduce The `HealthTelemetry` Interface
|
|
||||||
|
|
||||||
Create a small interface and health model types first.
|
|
||||||
|
|
||||||
Initial responsibilities:
|
|
||||||
|
|
||||||
- append structured logs
|
|
||||||
- record timing samples
|
|
||||||
- record counter deltas
|
|
||||||
- raise and clear warnings
|
|
||||||
- build a read-only health snapshot
|
|
||||||
|
|
||||||
The first implementation can still be backed by simple in-memory structures.
|
|
||||||
|
|
||||||
### Step 2: Move New Observations Off `RuntimeHost`
|
|
||||||
|
|
||||||
Before removing old setters, route new health-style work into `HealthTelemetry` instead of adding more `RuntimeHost` status fields.
|
|
||||||
|
|
||||||
This prevents the old status surface from growing during migration.
|
|
||||||
|
|
||||||
### Step 3: Replace `RuntimeHost` Status Setters With Telemetry Producers
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
|
|
||||||
- render timing writes
|
|
||||||
- signal status writes
|
|
||||||
- playout pacing writes
|
|
||||||
|
|
||||||
so they publish structured observations instead of mutating store-adjacent fields.
|
|
||||||
|
|
||||||
### Step 4: Replace Direct `OutputDebugStringA` Warning Paths
|
|
||||||
|
|
||||||
Wrap common warning/error cases in telemetry producers.
|
|
||||||
|
|
||||||
This includes:
|
|
||||||
|
|
||||||
- OSC decode/dispatch failures
|
|
||||||
- DeckLink late/dropped frame notifications
|
|
||||||
- backend fallback notices
|
|
||||||
- screenshot write failures
|
|
||||||
|
|
||||||
Direct debug output can remain as a sink of telemetry if desired, but not as the primary source of truth.
|
|
||||||
|
|
||||||
### Step 5: Publish Health Snapshot Through UI/Diagnostics Paths
|
|
||||||
|
|
||||||
Once the snapshot format exists, let `ControlServices` publish health summaries and recent warnings explicitly rather than depending on the runtime-state payload alone.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
### 1. Telemetry becomes a hidden behavior controller
|
|
||||||
|
|
||||||
If warning states start being used as the indirect way subsystems tell each other what to do, the subsystem boundary will fail.
|
|
||||||
|
|
||||||
Guardrail:
|
|
||||||
|
|
||||||
- telemetry observes and reports
|
|
||||||
- it does not coordinate or command
|
|
||||||
|
|
||||||
### 2. Logging stays string-only
|
|
||||||
|
|
||||||
If the subsystem only centralizes text logging without structure, later diagnostics will still be difficult.
|
|
||||||
|
|
||||||
Guardrail:
|
|
||||||
|
|
||||||
- severity, subsystem, category, and optional fields should be first-class
|
|
||||||
|
|
||||||
### 3. Timing writes become too expensive
|
|
||||||
|
|
||||||
If every sample requires heavy locking or snapshot rebuilds, render and callback timing could regress.
|
|
||||||
|
|
||||||
Guardrail:
|
|
||||||
|
|
||||||
- cheap recording path
|
|
||||||
- derived summaries built separately from hot-path writes
|
|
||||||
|
|
||||||
### 4. Health snapshot duplicates runtime truth
|
|
||||||
|
|
||||||
If health snapshots start storing copies of durable runtime state, the subsystem boundary will blur again.
|
|
||||||
|
|
||||||
Guardrail:
|
|
||||||
|
|
||||||
- health snapshots summarize operational state
|
|
||||||
- they do not become a second runtime store
|
|
||||||
|
|
||||||
### 5. Warning severity semantics drift by subsystem
|
|
||||||
|
|
||||||
If each subsystem invents its own meaning for warning/degraded/error, operator visibility becomes noisy and inconsistent.
|
|
||||||
|
|
||||||
Guardrail:
|
|
||||||
|
|
||||||
- define shared severity and health-state vocabulary early
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
### 1. Should debug-output sinks remain enabled by default?
|
|
||||||
|
|
||||||
Current recommendation:
|
|
||||||
|
|
||||||
- yes, as a sink fed by structured telemetry entries, not as the source of truth
|
|
||||||
|
|
||||||
### 2. How much timing history should be retained in memory?
|
|
||||||
|
|
||||||
Current recommendation:
|
|
||||||
|
|
||||||
- enough for short-term live troubleshooting and UI summaries
|
|
||||||
- not an unbounded time-series archive
|
|
||||||
|
|
||||||
### 3. Should operator-facing health and engineering diagnostics use the same snapshot?
|
|
||||||
|
|
||||||
Current recommendation:
|
|
||||||
|
|
||||||
- share one core telemetry model
|
|
||||||
- allow separate derived views for concise operator summaries versus deeper engineering detail
|
|
||||||
|
|
||||||
### 4. Where should threshold policy live if the app later formalizes warnings like "render over budget"?
|
|
||||||
|
|
||||||
Current recommendation:
|
|
||||||
|
|
||||||
- telemetry may evaluate declared thresholds
|
|
||||||
- subsystem owners still own mitigation behavior
|
|
||||||
|
|
||||||
### 5. Should input signal presence remain part of runtime state or move fully into telemetry?
|
|
||||||
|
|
||||||
Current recommendation:
|
|
||||||
|
|
||||||
- treat it as operational health state under `VideoBackend` reporting into telemetry
|
|
||||||
- avoid keeping it as a core durable runtime-store concern
|
|
||||||
|
|
||||||
## Success Criteria For This Subsystem
|
|
||||||
|
|
||||||
`HealthTelemetry` can be considered well-defined once the codebase can say, without ambiguity:
|
|
||||||
|
|
||||||
- all major subsystems have one place to publish timing, warnings, and counters
|
|
||||||
- health and timing state no longer share ownership with durable runtime state
|
|
||||||
- the UI can consume a stable health snapshot without scraping unrelated runtime fields
|
|
||||||
- direct debug-string warning paths are being retired or wrapped behind structured telemetry
|
|
||||||
- degraded-but-running conditions are visible as first-class state
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
`HealthTelemetry` is the subsystem that should answer:
|
|
||||||
|
|
||||||
- what is healthy right now
|
|
||||||
- what is degraded right now
|
|
||||||
- what recent warnings and errors occurred
|
|
||||||
- how render, control, and playout timing are behaving
|
|
||||||
|
|
||||||
It should:
|
|
||||||
|
|
||||||
- collect structured logs
|
|
||||||
- collect warnings and counters
|
|
||||||
- collect timing samples and gauges
|
|
||||||
- build stable health snapshots for publication
|
|
||||||
|
|
||||||
It should not:
|
|
||||||
|
|
||||||
- own core runtime truth
|
|
||||||
- decide app behavior
|
|
||||||
- coordinate recovery actions
|
|
||||||
- become a replacement for the render or backend policy layers
|
|
||||||
|
|
||||||
If this boundary holds, later phases can remove timing and warning state from `RuntimeHost` and move toward a much more diagnosable live system.
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Phase 1 Subsystem Design Index
|
|
||||||
|
|
||||||
This directory contains the subsystem-specific design notes for Phase 1 of the architecture roadmap.
|
|
||||||
|
|
||||||
Start here if you want the Phase 1 package to read as one coherent deliverable rather than as separate subsystem writeups.
|
|
||||||
|
|
||||||
Parent documents:
|
|
||||||
|
|
||||||
- [Architecture Resilience Review](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md)
|
|
||||||
- [Phase 1: Subsystem Boundaries and Target Architecture](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
|
|
||||||
|
|
||||||
## How This Set Fits Together
|
|
||||||
|
|
||||||
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) defines the top-level subsystem split, dependency rules, state categories, and migration guardrails.
|
|
||||||
- The notes in this directory expand each subsystem boundary without changing the parent Phase 1 design.
|
|
||||||
- The subsystem notes are meant to be read as design companions, not as independent alternate architectures.
|
|
||||||
|
|
||||||
## Recommended Reading Order
|
|
||||||
|
|
||||||
1. [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
|
|
||||||
2. [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
|
|
||||||
3. [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
|
|
||||||
4. [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
|
|
||||||
5. [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
|
|
||||||
6. [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
|
|
||||||
7. [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
|
|
||||||
8. [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
|
|
||||||
|
|
||||||
That order mirrors the intended dependency story:
|
|
||||||
|
|
||||||
- durable state first
|
|
||||||
- mutation and publication next
|
|
||||||
- ingress and render boundaries after that
|
|
||||||
- device timing and operational visibility last
|
|
||||||
|
|
||||||
## Subsystem Notes
|
|
||||||
|
|
||||||
- [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
|
|
||||||
Durable runtime config, persisted layer state, presets, and package metadata ownership.
|
|
||||||
- [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
|
|
||||||
Mutation validation, state classification, reset/reload policy, and publication/persistence requests.
|
|
||||||
- [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
|
|
||||||
Render-facing snapshot build, publication, and versioning boundaries.
|
|
||||||
- [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
|
|
||||||
OSC, HTTP/WebSocket, and file-watch ingress plus normalization and service-local buffering.
|
|
||||||
- [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
|
|
||||||
Sole-owner render/GL boundary, render-local transient state, preview, and playout-ready frame production.
|
|
||||||
- [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
|
|
||||||
Device lifecycle, input/output pacing, buffer policy, and producer/consumer playout direction.
|
|
||||||
- [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
|
|
||||||
Logs, warnings, counters, timing traces, and subsystem health snapshots.
|
|
||||||
|
|
||||||
## What Phase 1 Should Settle
|
|
||||||
|
|
||||||
Phase 1 should leave the project with:
|
|
||||||
|
|
||||||
- one agreed subsystem vocabulary
|
|
||||||
- one agreed dependency direction map
|
|
||||||
- one agreed state-category model
|
|
||||||
- one agreed current-to-target migration story
|
|
||||||
|
|
||||||
Phase 1 does not need to settle every later implementation detail. The subsystem notes intentionally leave some questions open where later phases need room to choose concrete mechanics.
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
# RenderEngine Subsystem Design
|
|
||||||
|
|
||||||
This document expands the `RenderEngine` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target ownership, boundaries, and migration shape for the rendering subsystem so later phases can move GL work out of today's mixed orchestration paths without inventing new boundaries on the fly.
|
|
||||||
|
|
||||||
The intent here is not to force a one-step rewrite. It is to make the target render boundary explicit enough that later work on events, `RuntimeHost` splitting, and backend decoupling all land in the same place.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
`RenderEngine` is the live frame-production subsystem.
|
|
||||||
|
|
||||||
It owns:
|
|
||||||
|
|
||||||
- GL context ownership in the target architecture
|
|
||||||
- render loop cadence and render task execution
|
|
||||||
- shader program and render-pass execution once build outputs are available
|
|
||||||
- capture texture upload scheduling once frames are accepted for render
|
|
||||||
- temporal history resources
|
|
||||||
- shader feedback resources
|
|
||||||
- render-local transient overlays
|
|
||||||
- preview-ready frame production
|
|
||||||
- playout-ready frame production
|
|
||||||
- render-local reset and rebuild behavior
|
|
||||||
|
|
||||||
It does not own:
|
|
||||||
|
|
||||||
- persisted runtime state
|
|
||||||
- high-level mutation policy
|
|
||||||
- OSC/UI ingress
|
|
||||||
- device discovery or callback policy
|
|
||||||
- playout queue policy
|
|
||||||
- operator-visible health policy beyond publishing observations
|
|
||||||
|
|
||||||
In the Phase 1 terminology, `RenderEngine` consumes snapshots plus render-local transient state and produces completed visual frames plus timing signals.
|
|
||||||
|
|
||||||
## Why This Subsystem Needs A Sharp Boundary
|
|
||||||
|
|
||||||
The current rendering path is split across several classes:
|
|
||||||
|
|
||||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:86) constructs the renderer, render pipeline, shader programs, runtime services, and video bridge in one owner.
|
|
||||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31) performs pass execution, pack/readback, preview paint, and performance stat publication.
|
|
||||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:58) accepts capture frames and still performs render work from the playout completion callback path.
|
|
||||||
- `OpenGLComposite` still holds render-local overlay behavior and shader rebuild handling alongside runtime orchestration responsibilities.
|
|
||||||
|
|
||||||
That split is workable today, but it creates architectural pressure:
|
|
||||||
|
|
||||||
- GL ownership is thread-shared instead of sole-owned.
|
|
||||||
- render and playout timing are still callback-coupled.
|
|
||||||
- preview and playout are produced in the same immediate path.
|
|
||||||
- render-local transient state is too easy to leak back into runtime-facing code.
|
|
||||||
- it is difficult to test render behavior separately from app bootstrap and hardware integration.
|
|
||||||
|
|
||||||
`RenderEngine` exists to absorb that responsibility into one subsystem with one direction of ownership.
|
|
||||||
|
|
||||||
## Responsibilities
|
|
||||||
|
|
||||||
### 1. Sole GL Ownership
|
|
||||||
|
|
||||||
In the target design, `RenderEngine` should be the only subsystem that performs long-lived GL work.
|
|
||||||
|
|
||||||
That includes:
|
|
||||||
|
|
||||||
- context binding and release policy
|
|
||||||
- framebuffer and texture lifetime
|
|
||||||
- shader program binding and draw execution
|
|
||||||
- upload/readback buffer lifetime
|
|
||||||
- preview blit or present paths
|
|
||||||
- render-local resource reset on rebuild or video-format changes
|
|
||||||
|
|
||||||
This is the most important boundary. Other subsystems may request work or provide data, but they should not directly perform GL commands.
|
|
||||||
|
|
||||||
### 2. Snapshot Consumption
|
|
||||||
|
|
||||||
`RenderEngine` should consume immutable or near-immutable render snapshots from `RuntimeSnapshotProvider`.
|
|
||||||
|
|
||||||
It is responsible for:
|
|
||||||
|
|
||||||
- detecting snapshot version changes
|
|
||||||
- rebuilding or re-binding render-local resources when the snapshot changes
|
|
||||||
- resolving render-pass execution from snapshot contents
|
|
||||||
- separating structural snapshot changes from transient overlay changes
|
|
||||||
|
|
||||||
It should not inspect mutable runtime store objects directly.
|
|
||||||
|
|
||||||
### 3. Frame Production
|
|
||||||
|
|
||||||
`RenderEngine` should produce completed frames for two consumers:
|
|
||||||
|
|
||||||
- preview presentation
|
|
||||||
- `VideoBackend` playout consumption
|
|
||||||
|
|
||||||
Those outputs may share most of their render work, but they are not equal-priority outputs. The subsystem rule from Phase 1 should be preserved:
|
|
||||||
|
|
||||||
- playout is the primary timing-sensitive output
|
|
||||||
- preview is subordinate and best-effort
|
|
||||||
|
|
||||||
### 4. Render-Local Transient State
|
|
||||||
|
|
||||||
`RenderEngine` owns transient visual state that affects output but is not persisted truth.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- temporal history textures
|
|
||||||
- feedback ping-pong buffers
|
|
||||||
- render-local OSC/live overlay state
|
|
||||||
- queued input frames accepted for upload
|
|
||||||
- cached readback frames
|
|
||||||
- preview-only presentation state
|
|
||||||
- in-flight rebuild generations
|
|
||||||
|
|
||||||
This state should remain render-local even when it influences visible output.
|
|
||||||
|
|
||||||
### 5. Shader Build Application
|
|
||||||
|
|
||||||
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:
|
|
||||||
|
|
||||||
- program creation/link usage
|
|
||||||
- pass graph application
|
|
||||||
- sampler/texture binding layout application
|
|
||||||
- resource reallocation required by shader shape changes
|
|
||||||
- safe invalidation of old render-local feedback/history resources
|
|
||||||
|
|
||||||
### 6. Render Timing Publication
|
|
||||||
|
|
||||||
`RenderEngine` should publish observations to `HealthTelemetry` such as:
|
|
||||||
|
|
||||||
- frame render duration
|
|
||||||
- upload duration
|
|
||||||
- pass execution duration
|
|
||||||
- pack/readback duration
|
|
||||||
- preview present timing
|
|
||||||
- rebuild stalls
|
|
||||||
- dropped/skipped input uploads
|
|
||||||
- output frame production latency
|
|
||||||
|
|
||||||
It should publish them, not own the health policy built from them.
|
|
||||||
|
|
||||||
## Non-Responsibilities
|
|
||||||
|
|
||||||
The target boundary should remain explicit about what does not belong here.
|
|
||||||
|
|
||||||
`RenderEngine` should not:
|
|
||||||
|
|
||||||
- decide whether a parameter mutation is persisted
|
|
||||||
- normalize OSC/UI actions
|
|
||||||
- choose device modes
|
|
||||||
- own DeckLink callback behavior
|
|
||||||
- own playout headroom policy
|
|
||||||
- perform stack preset serialization
|
|
||||||
- broadcast UI state
|
|
||||||
- treat telemetry as a control plane
|
|
||||||
|
|
||||||
Those rules matter because the current codebase often solves timing issues by letting the render path reach sideways into nearby systems.
|
|
||||||
|
|
||||||
## GL Ownership Model
|
|
||||||
|
|
||||||
## Target Rule
|
|
||||||
|
|
||||||
One subsystem owns GL. In practice that should mean one render thread becomes the long-lived GL owner in a later phase.
|
|
||||||
|
|
||||||
The render thread should:
|
|
||||||
|
|
||||||
- create or adopt the GL context
|
|
||||||
- execute all frame production work
|
|
||||||
- perform accepted texture uploads
|
|
||||||
- execute all pass graphs
|
|
||||||
- manage async readback and output packing
|
|
||||||
- manage feedback/history resets and reallocations
|
|
||||||
|
|
||||||
Other threads should interact with the subsystem through queues, snapshots, and completion signals, not by borrowing the GL context.
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
Today GL work is still shared across callback-driven entrypoints:
|
|
||||||
|
|
||||||
- input upload occurs in [OpenGLVideoIOBridge::VideoFrameArrived()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:58)
|
|
||||||
- playout-triggered render occurs in [OpenGLVideoIOBridge::PlayoutFrameCompleted()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:95)
|
|
||||||
- render-pass execution occurs in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31)
|
|
||||||
|
|
||||||
The `CRITICAL_SECTION` protects correctness, but it is not the target architectural model.
|
|
||||||
|
|
||||||
## Migration Direction
|
|
||||||
|
|
||||||
Phase 1 should treat the current bridge lock as a temporary compatibility mechanism. The target path should be:
|
|
||||||
|
|
||||||
1. input callback enqueues frame payloads or references
|
|
||||||
2. render thread accepts the latest usable input frame
|
|
||||||
3. render thread performs uploads on its own cadence
|
|
||||||
4. render thread produces completed output frames ahead of backend demand
|
|
||||||
5. backend callbacks only dequeue and schedule pre-rendered frames
|
|
||||||
|
|
||||||
That removes the need for callback threads to ever own GL.
|
|
||||||
|
|
||||||
## Render Loop Boundaries
|
|
||||||
|
|
||||||
`RenderEngine` should own a render loop with explicit phases. A good target shape is:
|
|
||||||
|
|
||||||
1. drain render-side commands and accepted service events
|
|
||||||
2. swap to the latest published snapshot if needed
|
|
||||||
3. apply render-local transient overlays
|
|
||||||
4. accept or coalesce latest input frame for upload
|
|
||||||
5. perform required uploads
|
|
||||||
6. execute pass graph
|
|
||||||
7. update temporal and feedback resources
|
|
||||||
8. pack and stage output frame(s)
|
|
||||||
9. publish preview-ready image if due
|
|
||||||
10. publish playout-ready frame(s) to `VideoBackend`
|
|
||||||
11. emit timing and health samples
|
|
||||||
|
|
||||||
The important property is that preview, playout preparation, feedback maintenance, and upload execution all happen under one render-owned cadence rather than as ad hoc side effects of unrelated callbacks.
|
|
||||||
|
|
||||||
## Snapshot And Overlay Interaction
|
|
||||||
|
|
||||||
`RenderEngine` should treat snapshots and overlays as different layers of state.
|
|
||||||
|
|
||||||
### Snapshot Inputs
|
|
||||||
|
|
||||||
Snapshots should provide:
|
|
||||||
|
|
||||||
- layer stack structure
|
|
||||||
- shader/package selections
|
|
||||||
- validated committed parameter values
|
|
||||||
- pass graph definitions
|
|
||||||
- resource requirements derived from runtime state
|
|
||||||
|
|
||||||
### Render-Local Overlay Inputs
|
|
||||||
|
|
||||||
Overlays should provide:
|
|
||||||
|
|
||||||
- active automation targets
|
|
||||||
- smoothed transient parameter overrides
|
|
||||||
- temporary visual state that should not persist back into the store
|
|
||||||
- queued reset/rebuild invalidations for render-local resources
|
|
||||||
|
|
||||||
### Resolution Rule
|
|
||||||
|
|
||||||
The render-side resolution order should be:
|
|
||||||
|
|
||||||
1. snapshot committed state forms the baseline
|
|
||||||
2. render-local transient overlays are applied on top
|
|
||||||
3. feedback/history resources influence shading as render-local inputs
|
|
||||||
4. completed frame is produced without mutating the underlying snapshot
|
|
||||||
|
|
||||||
This is especially important after the OSC work already moved toward render-local overlays. Phase 1 should keep that direction: render consumes committed truth plus transient live overlays, but render does not become the owner of persisted truth.
|
|
||||||
|
|
||||||
## Preview And Playout Relationship
|
|
||||||
|
|
||||||
Preview should be a subordinate consumer of render results, not a peer that can disturb playout timing.
|
|
||||||
|
|
||||||
### Target Rule
|
|
||||||
|
|
||||||
- playout deadlines come first
|
|
||||||
- preview is best-effort
|
|
||||||
- preview cadence may be reduced independently
|
|
||||||
- preview failure must not stall output frame production
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
|
|
||||||
Today preview still hangs off the render pipeline path through `mPaint()` in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54). That keeps preview close enough to the playout path that it is still part of the same timing surface.
|
|
||||||
|
|
||||||
### Target Shape
|
|
||||||
|
|
||||||
`RenderEngine` should internally distinguish:
|
|
||||||
|
|
||||||
- playout-ready frame production
|
|
||||||
- preview presentation or preview-copy publication
|
|
||||||
|
|
||||||
Possible later implementations:
|
|
||||||
|
|
||||||
- playout frame and preview frame share one composite render, but preview present is decoupled and rate-limited
|
|
||||||
- render publishes a preview texture handle or CPU-side preview image to a preview presenter
|
|
||||||
- preview updates are skipped under load without affecting playout queue fill
|
|
||||||
|
|
||||||
The exact implementation can change later, but the subsystem contract should already assume preview is subordinate.
|
|
||||||
|
|
||||||
## Interaction With `RuntimeSnapshotProvider`
|
|
||||||
|
|
||||||
`RenderEngine` should depend on `RuntimeSnapshotProvider`, not on `RuntimeStore`.
|
|
||||||
|
|
||||||
Expected interactions:
|
|
||||||
|
|
||||||
- query latest snapshot version
|
|
||||||
- consume latest stable snapshot
|
|
||||||
- detect structural versus parameter-only changes
|
|
||||||
- request no mutation back into the snapshot provider during render
|
|
||||||
|
|
||||||
Expected non-interactions:
|
|
||||||
|
|
||||||
- no direct persistence reads/writes
|
|
||||||
- no raw store mutation
|
|
||||||
- no direct service ingress handling
|
|
||||||
|
|
||||||
This is one of the main Phase 1 guardrails, because the current code often achieves convenience by letting render reach back into runtime-owned mutable objects.
|
|
||||||
|
|
||||||
## Interaction With `VideoBackend`
|
|
||||||
|
|
||||||
The target dependency direction stays:
|
|
||||||
|
|
||||||
- `VideoBackend -> RenderEngine`
|
|
||||||
|
|
||||||
That means:
|
|
||||||
|
|
||||||
- backend requests or consumes ready frames
|
|
||||||
- backend reports output timing/completion events
|
|
||||||
- render does not own output device policy
|
|
||||||
|
|
||||||
`RenderEngine` should expose frame-production and queue-facing interfaces, while `VideoBackend` owns:
|
|
||||||
|
|
||||||
- device callback handling
|
|
||||||
- output scheduling policy
|
|
||||||
- buffer pool policy
|
|
||||||
- backend state transitions
|
|
||||||
|
|
||||||
In later phases, this should evolve toward a producer/consumer queue where:
|
|
||||||
|
|
||||||
- render produces completed frames ahead of demand
|
|
||||||
- backend consumes already-produced frames
|
|
||||||
- callbacks drive dequeue/schedule/accounting only
|
|
||||||
|
|
||||||
## Current Code Mapping
|
|
||||||
|
|
||||||
The following current responsibilities should converge into `RenderEngine`.
|
|
||||||
|
|
||||||
### From `OpenGLComposite`
|
|
||||||
|
|
||||||
- render-local overlay management
|
|
||||||
- render-facing rebuild application
|
|
||||||
- screenshot-related render execution hooks
|
|
||||||
- render bootstrap ownership currently mixed with app bootstrap
|
|
||||||
|
|
||||||
### From `OpenGLRenderPipeline`
|
|
||||||
|
|
||||||
- frame render orchestration
|
|
||||||
- output pack conversion
|
|
||||||
- async readback state
|
|
||||||
- output frame caching
|
|
||||||
- preview-ready signal publication
|
|
||||||
|
|
||||||
### From `OpenGLVideoIOBridge`
|
|
||||||
|
|
||||||
- GL texture upload execution should move under render ownership
|
|
||||||
- playout callback render work should move out of the callback path
|
|
||||||
|
|
||||||
### Remains Outside `RenderEngine`
|
|
||||||
|
|
||||||
- device callback registration
|
|
||||||
- playout scheduling policy
|
|
||||||
- signal/device status lifecycle
|
|
||||||
- runtime mutation policy
|
|
||||||
|
|
||||||
## Suggested Internal Components
|
|
||||||
|
|
||||||
This document does not require final class names, but `RenderEngine` will likely be easier to evolve if it is not one monolithic replacement for `OpenGLComposite`.
|
|
||||||
|
|
||||||
Reasonable internal pieces could include:
|
|
||||||
|
|
||||||
- `RenderLoopController`
|
|
||||||
- `RenderSnapshotConsumer`
|
|
||||||
- `RenderOverlayState`
|
|
||||||
- `RenderInputQueue`
|
|
||||||
- `RenderPassExecutor`
|
|
||||||
- `RenderHistoryManager`
|
|
||||||
- `RenderOutputStager`
|
|
||||||
- `PreviewPresenter`
|
|
||||||
|
|
||||||
Those are internal implementation helpers. They should not become new cross-cutting subsystem boundaries by themselves.
|
|
||||||
|
|
||||||
## Public Interface Shape
|
|
||||||
|
|
||||||
Aligned with the Phase 1 design, `RenderEngine` should eventually expose operations in this family:
|
|
||||||
|
|
||||||
- `StartRenderLoop(...)`
|
|
||||||
- `StopRenderLoop()`
|
|
||||||
- `ConsumeSnapshot(...)`
|
|
||||||
- `EnqueueInputFrame(...)`
|
|
||||||
- `ApplyOverlayUpdate(...)`
|
|
||||||
- `RequestRenderLocalReset(...)`
|
|
||||||
- `HandleRebuildOutputs(...)`
|
|
||||||
- `TryProduceOutputFrame(...)`
|
|
||||||
- `GetPreviewFrame(...)`
|
|
||||||
- `ReportRenderState()`
|
|
||||||
|
|
||||||
Interface goals:
|
|
||||||
|
|
||||||
- calls are explicit about whether they mutate render-local state or request frame production
|
|
||||||
- no caller needs direct GL access
|
|
||||||
- preview and playout are exposed as outputs, not as reasons for callers to enter the render path
|
|
||||||
|
|
||||||
## Migration Plan From Current Code
|
|
||||||
|
|
||||||
### Step 1. Name The Boundary
|
|
||||||
|
|
||||||
Treat `OpenGLRenderPipeline` plus the render portions of `OpenGLComposite` and `OpenGLVideoIOBridge` as conceptually belonging to `RenderEngine`, even before physical extraction is complete.
|
|
||||||
|
|
||||||
### Step 2. Stop New Render Work From Escaping
|
|
||||||
|
|
||||||
As new features are added, keep:
|
|
||||||
|
|
||||||
- feedback buffers
|
|
||||||
- temporal history
|
|
||||||
- render-local overlays
|
|
||||||
- preview state
|
|
||||||
|
|
||||||
inside render-owned code paths instead of putting them back into `RuntimeHost` or service layers.
|
|
||||||
|
|
||||||
### Step 3. Isolate Snapshot Consumption
|
|
||||||
|
|
||||||
Introduce snapshot-facing APIs so render no longer depends on broad `RuntimeHost` state access for frame production.
|
|
||||||
|
|
||||||
### Step 4. Move Uploads Onto Render Ownership
|
|
||||||
|
|
||||||
Input callbacks should enqueue or hand off frame data; render executes the upload.
|
|
||||||
|
|
||||||
### Step 5. Break Callback-Driven Rendering
|
|
||||||
|
|
||||||
Move from "render in playout completion callback" to "render ahead and let backend consume ready frames."
|
|
||||||
|
|
||||||
### Step 6. Decouple Preview Cadence
|
|
||||||
|
|
||||||
Make preview a best-effort presentation path with its own skip/rate-limit policy.
|
|
||||||
|
|
||||||
### Step 7. Narrow `OpenGLComposite`
|
|
||||||
|
|
||||||
After the above, `OpenGLComposite` should collapse toward a composition root and legacy adapter rather than remaining the owner of render behavior.
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
### Latency Risk
|
|
||||||
|
|
||||||
Moving to queue-based frame production can accidentally increase latency if headroom is allowed to grow without policy. `RenderEngine` should therefore expose queue-friendly production, but `VideoBackend` must still own explicit latency/headroom policy.
|
|
||||||
|
|
||||||
### Resource Churn Risk
|
|
||||||
|
|
||||||
Snapshot changes, shader rebuilds, and video-format changes can cause expensive reallocation of:
|
|
||||||
|
|
||||||
- feedback surfaces
|
|
||||||
- history buffers
|
|
||||||
- output pack resources
|
|
||||||
- readback buffers
|
|
||||||
|
|
||||||
The subsystem needs clear structural-change boundaries so parameter-only changes do not trigger broad resource churn.
|
|
||||||
|
|
||||||
### Preview Coupling Risk
|
|
||||||
|
|
||||||
If preview remains too close to the render/playout path, it can continue to steal budget from output production even after the rest of the subsystem is cleaned up.
|
|
||||||
|
|
||||||
### Readback Deadline Risk
|
|
||||||
|
|
||||||
The current async readback path still falls back to synchronous reads when the deadline is missed. That behavior may remain necessary, but `RenderEngine` should treat it as a degraded-path metric, not as an invisible normal case.
|
|
||||||
|
|
||||||
### Overlay Complexity Risk
|
|
||||||
|
|
||||||
Render-local overlays are powerful, but they can become a hidden second state model if not kept clearly subordinate to committed snapshot state.
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
- Should preview become a separate presenter helper inside `RenderEngine`, or remain a subordinate callback/output sink?
|
|
||||||
- Where should screenshot capture live long-term: inside `RenderEngine`, or in a small render consumer layered on top of it?
|
|
||||||
- Should shader compilation outputs be delivered to render as whole-framegraph rebuild packages, or incrementally by layer/pass?
|
|
||||||
- How should input frame ownership work under load: newest-only, bounded queue, or policy selected by backend mode?
|
|
||||||
- Should render expose one playout-ready frame at a time, or a bounded ring the backend drains directly?
|
|
||||||
- What exact distinction should the snapshot provider publish between structural changes and parameter-only changes so render rebuilds stay cheap?
|
|
||||||
|
|
||||||
## Phase 1 Exit Criteria For `RenderEngine`
|
|
||||||
|
|
||||||
For Phase 1, this subsystem design is sufficiently defined once the project agrees that:
|
|
||||||
|
|
||||||
- render is the sole long-term owner of GL work
|
|
||||||
- render consumes snapshots, not mutable runtime store objects
|
|
||||||
- preview is subordinate to playout
|
|
||||||
- feedback/history/overlays are render-local transient state
|
|
||||||
- backend callbacks should converge toward dequeue/schedule behavior rather than direct rendering
|
|
||||||
- current render responsibilities in `OpenGLComposite`, `OpenGLRenderPipeline`, and `OpenGLVideoIOBridge` are expected to migrate under this subsystem
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
`RenderEngine` should become the subsystem that owns live GPU execution and nothing else.
|
|
||||||
|
|
||||||
It consumes committed snapshots plus render-local overlays, owns the full GL lifecycle, produces preview and playout-ready frames, and publishes timing observations. It should not own persistence, control ingress, or hardware scheduling policy. If later phases hold to that line, timing work and render-state work can get cleaner without reintroducing the same cross-thread coupling in a different form.
|
|
||||||
@@ -1,555 +0,0 @@
|
|||||||
# RuntimeCoordinator Design Note
|
|
||||||
|
|
||||||
This document defines the target design for the `RuntimeCoordinator` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
|
||||||
|
|
||||||
`RuntimeCoordinator` is the mutation and policy layer for the app. Its job is to accept already-normalized actions from ingress systems, decide whether those actions are valid, classify how they should affect durable and live state, and trigger downstream publication or persistence work without taking ownership of rendering, device callbacks, or disk serialization details.
|
|
||||||
|
|
||||||
## Why This Subsystem Exists
|
|
||||||
|
|
||||||
Today the app's mutation path is split across several places:
|
|
||||||
|
|
||||||
- `RuntimeHost` performs validation, mutation, persistence, render-state invalidation, and some status updates:
|
|
||||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
|
||||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:891)
|
|
||||||
- `OpenGLComposite` currently acts like an orchestration shell and a mutation coordinator at the same time:
|
|
||||||
- [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp:1)
|
|
||||||
- `RuntimeServices` still owns some deferred control flow around OSC commit and polling:
|
|
||||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
|
|
||||||
|
|
||||||
That overlap makes several kinds of regressions more likely:
|
|
||||||
|
|
||||||
- persistence policy leaks into control handlers
|
|
||||||
- render invalidation rules are spread across UI and non-UI paths
|
|
||||||
- transient automation behavior is hard to reason about
|
|
||||||
- reload behavior is partly a render concern and partly a runtime concern
|
|
||||||
- future event-model work has no single policy owner to target
|
|
||||||
|
|
||||||
`RuntimeCoordinator` exists to centralize those decisions without becoming a new monolith.
|
|
||||||
|
|
||||||
## Core Responsibilities
|
|
||||||
|
|
||||||
`RuntimeCoordinator` should own the following responsibilities.
|
|
||||||
|
|
||||||
### 1. Mutation intake after normalization
|
|
||||||
|
|
||||||
`RuntimeCoordinator` accepts typed, already-parsed actions from `ControlServices` or composition-root adapters. Examples:
|
|
||||||
|
|
||||||
- add/remove/move layer
|
|
||||||
- change shader on a layer
|
|
||||||
- change a parameter value
|
|
||||||
- reset a layer
|
|
||||||
- save or load a stack preset
|
|
||||||
- request a shader/package reload
|
|
||||||
- apply a transient automation target
|
|
||||||
- commit or clear transient overlay state
|
|
||||||
|
|
||||||
The coordinator should not parse JSON, decode OSC payloads, or inspect HTTP payload syntax. That belongs to ingress systems.
|
|
||||||
|
|
||||||
### 2. Validation and policy decisions
|
|
||||||
|
|
||||||
The coordinator validates whether a requested mutation is allowed and decides how it should behave.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- whether a layer id exists
|
|
||||||
- whether a shader id is valid
|
|
||||||
- whether a parameter exists on the targeted shader
|
|
||||||
- whether a value is within the definition's allowed range or enum set
|
|
||||||
- whether a trigger should update committed state, transient state, or both
|
|
||||||
- whether a structural change should preserve compatible transient state such as feedback buffers
|
|
||||||
|
|
||||||
This is the main policy surface that is currently spread between `RuntimeHost` methods such as:
|
|
||||||
|
|
||||||
- `AddLayer(...)`
|
|
||||||
- `SetLayerShader(...)`
|
|
||||||
- `UpdateLayerParameter(...)`
|
|
||||||
- `UpdateLayerParameterByControlKey(...)`
|
|
||||||
- `ApplyOscTargetByControlKey(...)`
|
|
||||||
- `ResetLayerParameters(...)`
|
|
||||||
|
|
||||||
See [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15).
|
|
||||||
|
|
||||||
### 3. State classification
|
|
||||||
|
|
||||||
The coordinator decides which state category a mutation affects:
|
|
||||||
|
|
||||||
- persisted state
|
|
||||||
- committed live state
|
|
||||||
- transient live overlay state
|
|
||||||
- health/timing state only
|
|
||||||
|
|
||||||
The design rule is that classification belongs here, not in the ingress layer and not in render code.
|
|
||||||
|
|
||||||
### 4. Snapshot publication requests
|
|
||||||
|
|
||||||
When a mutation changes render-facing state, the coordinator asks `RuntimeSnapshotProvider` to publish a new snapshot or mark one dirty for publication.
|
|
||||||
|
|
||||||
The coordinator does not build render snapshots itself.
|
|
||||||
|
|
||||||
### 5. Persistence requests
|
|
||||||
|
|
||||||
When a mutation changes durable state, the coordinator asks `RuntimeStore` to record the new authoritative state and, when applicable, request persistence through the store's write path.
|
|
||||||
|
|
||||||
The coordinator does not serialize files directly.
|
|
||||||
|
|
||||||
### 6. Cross-subsystem consistency policy
|
|
||||||
|
|
||||||
The coordinator is where "what else must happen if this changes?" lives.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- a layer add/remove/move may require:
|
|
||||||
- store mutation
|
|
||||||
- snapshot republish
|
|
||||||
- compatibility-preserving render-state reset policy
|
|
||||||
- optional UI-state notification via later event-model work
|
|
||||||
- a stack preset load may require:
|
|
||||||
- replacement of committed layer stack state
|
|
||||||
- invalidation of transient overlay state that no longer maps cleanly
|
|
||||||
- snapshot republish
|
|
||||||
- deferred persistence request
|
|
||||||
- an automation target may require:
|
|
||||||
- transient overlay update only
|
|
||||||
- no persistence write
|
|
||||||
- optional later commit into committed live state if policy says so
|
|
||||||
|
|
||||||
## Explicit Non-Responsibilities
|
|
||||||
|
|
||||||
`RuntimeCoordinator` should explicitly not own the following.
|
|
||||||
|
|
||||||
### Not a persistence engine
|
|
||||||
|
|
||||||
It does not:
|
|
||||||
|
|
||||||
- read or write files
|
|
||||||
- decide file formats
|
|
||||||
- own preset storage layout
|
|
||||||
- perform debounced disk flushing logic
|
|
||||||
|
|
||||||
Those belong in `RuntimeStore` and later persistence helpers.
|
|
||||||
|
|
||||||
### Not a render engine
|
|
||||||
|
|
||||||
It does not:
|
|
||||||
|
|
||||||
- own GL objects
|
|
||||||
- perform shader compilation
|
|
||||||
- reset temporal history textures directly
|
|
||||||
- build render passes
|
|
||||||
- hold frame queues
|
|
||||||
|
|
||||||
It may request policy outcomes that cause render-local resets, but render performs the work.
|
|
||||||
|
|
||||||
### Not a hardware/backend owner
|
|
||||||
|
|
||||||
It does not:
|
|
||||||
|
|
||||||
- configure DeckLink
|
|
||||||
- react directly to device callbacks
|
|
||||||
- schedule playout
|
|
||||||
- own input signal callbacks
|
|
||||||
|
|
||||||
### Not an ingress transport layer
|
|
||||||
|
|
||||||
It does not:
|
|
||||||
|
|
||||||
- parse OSC wire messages
|
|
||||||
- host websockets
|
|
||||||
- own HTTP handlers
|
|
||||||
- own polling loops
|
|
||||||
|
|
||||||
### Not a health reporting sink
|
|
||||||
|
|
||||||
It can emit mutation outcomes and warnings to `HealthTelemetry`, but it should not own counters, logs, or dashboards.
|
|
||||||
|
|
||||||
## Mutation Policy
|
|
||||||
|
|
||||||
The coordinator should use a small number of policy classes of mutation behavior rather than ad hoc per-call decisions.
|
|
||||||
|
|
||||||
### Durable mutation
|
|
||||||
|
|
||||||
Updates authoritative state that should survive beyond the current session flow.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- add/remove/move layer
|
|
||||||
- change selected shader on a layer
|
|
||||||
- update a parameter via UI or API
|
|
||||||
- load a stack preset
|
|
||||||
- reset a layer to defaults
|
|
||||||
|
|
||||||
Expected coordinator behavior:
|
|
||||||
|
|
||||||
1. validate the request
|
|
||||||
2. normalize the target and value if needed
|
|
||||||
3. update committed/durable state via `RuntimeStore`
|
|
||||||
4. request snapshot publication
|
|
||||||
5. request persistence according to policy
|
|
||||||
|
|
||||||
### Live committed mutation
|
|
||||||
|
|
||||||
Updates committed current-session state that should be treated as true until changed again, but may not need synchronous persistence.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- a UI action that changes a parameter repeatedly while dragging
|
|
||||||
- a manual operator bypass toggle during live use
|
|
||||||
|
|
||||||
Expected coordinator behavior:
|
|
||||||
|
|
||||||
1. update committed live state
|
|
||||||
2. request snapshot publication
|
|
||||||
3. decide whether persistence should happen immediately, be debounced, or be deferred
|
|
||||||
|
|
||||||
### Transient overlay mutation
|
|
||||||
|
|
||||||
Affects output but should not masquerade as stored truth.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- active OSC automation target
|
|
||||||
- short-lived trigger-driven visual automation state
|
|
||||||
|
|
||||||
Expected coordinator behavior:
|
|
||||||
|
|
||||||
1. validate the route and target parameter
|
|
||||||
2. classify the action as transient
|
|
||||||
3. update overlay state through the appropriate owner boundary
|
|
||||||
4. avoid persistence unless a separate commit policy is invoked
|
|
||||||
|
|
||||||
### Coordination-only mutation
|
|
||||||
|
|
||||||
A request that mainly exists to trigger a flow rather than edit value state.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- request reload
|
|
||||||
- request publish-now
|
|
||||||
- request clear transient state on reset/rebuild
|
|
||||||
|
|
||||||
## Interaction With State Categories
|
|
||||||
|
|
||||||
This section restates the Phase 1 state model specifically from the coordinator's perspective.
|
|
||||||
|
|
||||||
### Persisted state
|
|
||||||
|
|
||||||
`RuntimeCoordinator` does not own persisted state, but it decides when persisted state should change.
|
|
||||||
|
|
||||||
Typical interaction:
|
|
||||||
|
|
||||||
- validate request
|
|
||||||
- call into `RuntimeStore`
|
|
||||||
- receive success/failure
|
|
||||||
- request persistence if policy says this mutation should be durable
|
|
||||||
|
|
||||||
### Committed live state
|
|
||||||
|
|
||||||
This is the coordinator's primary logical domain.
|
|
||||||
|
|
||||||
Even if the implementation initially stores committed live state inside `RuntimeHost` or later inside `RuntimeStore`, the coordinator should be considered the policy owner of:
|
|
||||||
|
|
||||||
- current layer stack composition
|
|
||||||
- current selected shaders
|
|
||||||
- current bypass flags
|
|
||||||
- current operator-authored parameter values
|
|
||||||
|
|
||||||
### Transient live overlay state
|
|
||||||
|
|
||||||
The coordinator defines the rules for transient state, but should not become the long-term storage owner for render-local transient data.
|
|
||||||
|
|
||||||
The expected split is:
|
|
||||||
|
|
||||||
- coordinator owns policy
|
|
||||||
- `ControlServices` may own short ingress-side queues and coalescing buffers
|
|
||||||
- `RenderEngine` owns render-local transient application state
|
|
||||||
- `VideoBackend` owns playout and device transient state
|
|
||||||
|
|
||||||
For OSC specifically, the coordinator should eventually decide:
|
|
||||||
|
|
||||||
- whether an automation change is transient-only
|
|
||||||
- whether it should later commit into committed live state
|
|
||||||
- what reset/reload actions invalidate it
|
|
||||||
|
|
||||||
### Health and timing state
|
|
||||||
|
|
||||||
The coordinator may emit events like:
|
|
||||||
|
|
||||||
- mutation rejected
|
|
||||||
- reload requested
|
|
||||||
- preset load succeeded/failed
|
|
||||||
- transient state cleared because structure changed
|
|
||||||
|
|
||||||
But those are observations into `HealthTelemetry`, not coordinator-owned data.
|
|
||||||
|
|
||||||
## Proposed Interfaces
|
|
||||||
|
|
||||||
These are target-shape interfaces, not final signatures.
|
|
||||||
|
|
||||||
### Input-facing API
|
|
||||||
|
|
||||||
Core mutation entrypoints could look like:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
struct RuntimeMutationRequest;
|
|
||||||
struct RuntimeMutationResult;
|
|
||||||
struct ReloadRequest;
|
|
||||||
struct OverlayCommitRequest;
|
|
||||||
|
|
||||||
class RuntimeCoordinator
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
RuntimeMutationResult ApplyMutation(const RuntimeMutationRequest& request);
|
|
||||||
RuntimeMutationResult ApplyAutomationTarget(const RuntimeMutationRequest& request);
|
|
||||||
RuntimeMutationResult ResetLayer(const std::string& layerId);
|
|
||||||
RuntimeMutationResult RequestReload(const ReloadRequest& request);
|
|
||||||
RuntimeMutationResult CommitOverlayState(const OverlayCommitRequest& request);
|
|
||||||
RuntimeMutationResult ClearTransientStateForScope(const RuntimeResetScope& scope);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The important point is not the exact names. It is that ingress systems send typed requests into one policy owner.
|
|
||||||
|
|
||||||
### Downstream collaborators
|
|
||||||
|
|
||||||
The coordinator likely needs collaborators conceptually equivalent to:
|
|
||||||
|
|
||||||
- `IRuntimeStore`
|
|
||||||
- `IRuntimeSnapshotProvider`
|
|
||||||
- `IHealthTelemetry`
|
|
||||||
- later, a compatibility shim for still-existing `RuntimeHost` behavior during migration
|
|
||||||
|
|
||||||
### Mutation result shape
|
|
||||||
|
|
||||||
A useful result structure should carry more than success/failure. It should support policy-driven downstream behavior without re-deriving the decision elsewhere.
|
|
||||||
|
|
||||||
Suggested fields:
|
|
||||||
|
|
||||||
- `accepted`
|
|
||||||
- `errorMessage`
|
|
||||||
- `stateChanged`
|
|
||||||
- `persistedStateChanged`
|
|
||||||
- `committedLiveStateChanged`
|
|
||||||
- `transientStateChanged`
|
|
||||||
- `snapshotPublicationRequired`
|
|
||||||
- `persistenceRequested`
|
|
||||||
- `renderResetScope`
|
|
||||||
- `telemetryNotes`
|
|
||||||
|
|
||||||
This prevents callers from guessing whether they need to reload, publish, or persist.
|
|
||||||
|
|
||||||
## Current Code Mapping
|
|
||||||
|
|
||||||
The current app does not have a separate coordinator class, but several existing code paths are clearly doing coordinator work.
|
|
||||||
|
|
||||||
### `OpenGLCompositeRuntimeControls.cpp`
|
|
||||||
|
|
||||||
Methods like:
|
|
||||||
|
|
||||||
- `AddLayer(...)`
|
|
||||||
- `RemoveLayer(...)`
|
|
||||||
- `MoveLayer(...)`
|
|
||||||
- `SetLayerBypass(...)`
|
|
||||||
- `SetLayerShader(...)`
|
|
||||||
- `UpdateLayerParameterJson(...)`
|
|
||||||
- `ResetLayerParameters(...)`
|
|
||||||
- `SaveStackPreset(...)`
|
|
||||||
- `LoadStackPreset(...)`
|
|
||||||
|
|
||||||
currently do this pattern:
|
|
||||||
|
|
||||||
1. call `RuntimeHost` mutation
|
|
||||||
2. decide whether to call `ReloadShader(...)`
|
|
||||||
3. call `broadcastRuntimeState()`
|
|
||||||
|
|
||||||
See [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp:1).
|
|
||||||
|
|
||||||
That "call host, then decide reload/broadcast policy" logic is a direct candidate for migration into `RuntimeCoordinator`.
|
|
||||||
|
|
||||||
### `RuntimeHost`
|
|
||||||
|
|
||||||
`RuntimeHost` currently combines:
|
|
||||||
|
|
||||||
- mutation validation
|
|
||||||
- state mutation
|
|
||||||
- value normalization
|
|
||||||
- persistence writes
|
|
||||||
- render-state dirty marking
|
|
||||||
|
|
||||||
Examples in [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:891):
|
|
||||||
|
|
||||||
- `AddLayer(...)`
|
|
||||||
- `SetLayerShader(...)`
|
|
||||||
- `UpdateLayerParameter(...)`
|
|
||||||
- `UpdateLayerParameterByControlKey(...)`
|
|
||||||
- `ApplyOscTargetByControlKey(...)`
|
|
||||||
- `ResetLayerParameters(...)`
|
|
||||||
- `LoadStackPreset(...)`
|
|
||||||
|
|
||||||
The target design is not to move all implementation in one step. It is to peel policy and orchestration decisions away first.
|
|
||||||
|
|
||||||
### `RuntimeServices`
|
|
||||||
|
|
||||||
Current OSC-specific flow in `RuntimeServices` includes:
|
|
||||||
|
|
||||||
- queueing updates
|
|
||||||
- applying pending updates
|
|
||||||
- queueing commits
|
|
||||||
- consuming completed commits
|
|
||||||
- clearing OSC state
|
|
||||||
|
|
||||||
See [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46).
|
|
||||||
|
|
||||||
The coordinator should eventually own the rules for when these updates are transient, when they commit, and what reset/reload does to them, while `ControlServices` keeps only the ingress mechanics.
|
|
||||||
|
|
||||||
## Recommended Internal Model
|
|
||||||
|
|
||||||
The coordinator should remain small enough to reason about. A good target is to split its internal logic into policy-focused helpers rather than letting one class become another `RuntimeHost`.
|
|
||||||
|
|
||||||
Possible internal helper concepts:
|
|
||||||
|
|
||||||
- `LayerMutationPolicy`
|
|
||||||
- `ParameterMutationPolicy`
|
|
||||||
- `PresetMutationPolicy`
|
|
||||||
- `ReloadPolicy`
|
|
||||||
- `OverlayPolicy`
|
|
||||||
|
|
||||||
That can still be presented as one subsystem to the rest of the app, while keeping the implementation testable.
|
|
||||||
|
|
||||||
## Snapshot Publication Contract
|
|
||||||
|
|
||||||
The coordinator should never force callers to know whether a snapshot must be rebuilt. That policy should be owned here.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
- parameter changes require snapshot publication
|
|
||||||
- layer reorder requires snapshot publication
|
|
||||||
- shader swap requires snapshot publication and render-local rebuild work
|
|
||||||
- stack preset load requires snapshot publication and likely broader transient-state invalidation
|
|
||||||
- pure health/status changes do not require snapshot publication
|
|
||||||
|
|
||||||
This contract matters because current call sites often use coarse actions like `ReloadShader()` after structural edits. The coordinator should return a more precise outcome than "reload or not."
|
|
||||||
|
|
||||||
## Reload and Reset Policy
|
|
||||||
|
|
||||||
Reload and reset behavior has been a recurring source of edge cases in the current app, especially with shader feedback, temporal history, and OSC overlay state.
|
|
||||||
|
|
||||||
The coordinator should define explicit reset scopes such as:
|
|
||||||
|
|
||||||
- parameter-values-only reset
|
|
||||||
- committed-live-state reset for a layer
|
|
||||||
- transient-overlay reset for a layer
|
|
||||||
- render-local-history reset for a layer
|
|
||||||
- whole-stack structural reset
|
|
||||||
- reload-induced compatibility reset
|
|
||||||
|
|
||||||
That allows later phases to stop encoding reset behavior implicitly in UI handlers or render rebuild code.
|
|
||||||
|
|
||||||
## Migration Plan From Current Code
|
|
||||||
|
|
||||||
The coordinator should be introduced incrementally.
|
|
||||||
|
|
||||||
### Step 1. Define request and result types
|
|
||||||
|
|
||||||
Introduce typed mutation request/result objects without changing most internals yet.
|
|
||||||
|
|
||||||
### Step 2. Wrap current `RuntimeHost` mutations behind coordinator entrypoints
|
|
||||||
|
|
||||||
The first implementation can still delegate heavily into `RuntimeHost`, but the call sites should stop deciding policy on their own.
|
|
||||||
|
|
||||||
For example, instead of:
|
|
||||||
|
|
||||||
1. `OpenGLComposite::AddLayer()`
|
|
||||||
2. `RuntimeHost::AddLayer()`
|
|
||||||
3. `ReloadShader(true)`
|
|
||||||
4. `broadcastRuntimeState()`
|
|
||||||
|
|
||||||
the flow becomes:
|
|
||||||
|
|
||||||
1. `OpenGLComposite` or `ControlServices` creates a typed request
|
|
||||||
2. `RuntimeCoordinator::ApplyMutation(...)`
|
|
||||||
3. coordinator returns a result describing snapshot, reset, and persistence needs
|
|
||||||
4. composition root dispatches those downstream effects
|
|
||||||
|
|
||||||
### Step 3. Move validation and classification out of `RuntimeHost`
|
|
||||||
|
|
||||||
Once coordinator entrypoints are stable, pull up:
|
|
||||||
|
|
||||||
- mutation classification
|
|
||||||
- reset/reload policy
|
|
||||||
- transient-versus-durable decisions
|
|
||||||
|
|
||||||
while leaving raw store operations in place.
|
|
||||||
|
|
||||||
### Step 4. Narrow `RuntimeHost` into store and snapshot collaborators
|
|
||||||
|
|
||||||
Only after the coordinator is clearly owning policy should `RuntimeHost` be split into real target subsystems.
|
|
||||||
|
|
||||||
## Key Risks
|
|
||||||
|
|
||||||
### Risk 1. Coordinator becomes a new god object
|
|
||||||
|
|
||||||
If the coordinator starts owning persistence details, status counters, or render reset mechanics directly, it will just recreate the current problem under a new name.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- keep collaborators explicit
|
|
||||||
- keep request/result types narrow
|
|
||||||
- avoid direct dependencies on render or backend internals
|
|
||||||
|
|
||||||
### Risk 2. Call sites bypass coordinator during migration
|
|
||||||
|
|
||||||
If new code continues calling `RuntimeHost` directly for convenience, the architecture will fork into two policy systems.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- treat the coordinator as the required entrypoint for new non-render mutations
|
|
||||||
- add compatibility adapters rather than parallel mutation paths
|
|
||||||
|
|
||||||
### Risk 3. Too much policy stays implicit in return conventions
|
|
||||||
|
|
||||||
If callers still infer policy from "which method was called," the coordinator will not actually clarify the system.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- return explicit mutation outcomes
|
|
||||||
- define reset and publication scopes as named concepts
|
|
||||||
|
|
||||||
### Risk 4. Transient-state ownership remains fuzzy
|
|
||||||
|
|
||||||
OSC overlay behavior, feedback invalidation, and reload compatibility can easily blur subsystem boundaries again.
|
|
||||||
|
|
||||||
Mitigation:
|
|
||||||
|
|
||||||
- coordinator owns classification rules
|
|
||||||
- subsystem owners retain storage ownership
|
|
||||||
- reset scopes are explicit
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
- Should committed live state remain physically stored in `RuntimeStore`, or should the coordinator gain a live-session companion object before Phase 3?
|
|
||||||
- Should preset load/save stay synchronous through early migration, or should the coordinator always treat them as policy requests whose persistence effects may complete later?
|
|
||||||
- Should reload requests be modeled as a dedicated mutation class distinct from ordinary control mutations from the start?
|
|
||||||
- How much normalization of parameter values should remain in store-side helpers versus moving into coordinator policy helpers?
|
|
||||||
- Should transient overlay commit policy be global, or parameter-definition-driven for specific shader controls?
|
|
||||||
- What is the minimal reset-scope vocabulary needed to avoid hard-coding reload behavior in `RenderEngine` later?
|
|
||||||
|
|
||||||
## Short Version
|
|
||||||
|
|
||||||
`RuntimeCoordinator` is where the app decides what a valid change means.
|
|
||||||
|
|
||||||
It should:
|
|
||||||
|
|
||||||
- accept typed mutations from ingress systems
|
|
||||||
- validate and classify them
|
|
||||||
- update durable and committed state through `RuntimeStore`
|
|
||||||
- request render-facing publication through `RuntimeSnapshotProvider`
|
|
||||||
- request persistence when policy requires it
|
|
||||||
- define reset, reload, and transient-overlay rules
|
|
||||||
|
|
||||||
It should not:
|
|
||||||
|
|
||||||
- parse transport payloads
|
|
||||||
- own GL work
|
|
||||||
- own device callbacks
|
|
||||||
- write files directly
|
|
||||||
- become a replacement monolith for every kind of state
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user