Compare commits
135 Commits
1d08dec5fe
...
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 |
@@ -7,6 +7,9 @@ on:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
schedule:
|
||||
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
|
||||
- cron: "0 14 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -15,8 +18,53 @@ jobs:
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Manual Checkout With LFS Submodules
|
||||
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
|
||||
shell: powershell
|
||||
@@ -31,29 +79,73 @@ jobs:
|
||||
- name: Configure Debug
|
||||
shell: powershell
|
||||
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 }}"
|
||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||
$slangRoot = $env:SLANG_ROOT
|
||||
}
|
||||
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 = @(
|
||||
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||
(Join-Path $slangRoot "bin\slang-compiler.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 $_) })
|
||||
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
|
||||
}
|
||||
|
||||
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
|
||||
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
|
||||
shell: powershell
|
||||
@@ -82,13 +174,59 @@ jobs:
|
||||
package-windows:
|
||||
name: Windows Release Package
|
||||
runs-on: windows-2022
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
needs:
|
||||
- native-windows
|
||||
- ui-ubuntu
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Manual Checkout With LFS Submodules
|
||||
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
|
||||
shell: powershell
|
||||
@@ -110,29 +248,73 @@ jobs:
|
||||
- name: Configure Release
|
||||
shell: powershell
|
||||
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 }}"
|
||||
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
|
||||
$slangRoot = $env:SLANG_ROOT
|
||||
}
|
||||
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 = @(
|
||||
(Join-Path $slangRoot "bin\slangc.exe"),
|
||||
(Join-Path $slangRoot "bin\slang-compiler.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 $_) })
|
||||
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
|
||||
}
|
||||
|
||||
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
|
||||
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
|
||||
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
|
||||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -2,21 +2,21 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug LoopThroughWithOpenGLCompositing",
|
||||
"name": "Debug RenderCadenceCompositor",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
|
||||
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [],
|
||||
"console": "internalConsole",
|
||||
"console": "externalTerminal",
|
||||
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"requireExactSource": true,
|
||||
"logging": {
|
||||
"moduleLoad": true
|
||||
},
|
||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
@@ -2,44 +2,72 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build LoopThroughWithOpenGLCompositing Debug x64",
|
||||
"label": "Configure Debug x64",
|
||||
"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": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||
"--config",
|
||||
"Debug",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing",
|
||||
"RenderCadenceCompositor",
|
||||
"--parallel"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": "Configure Debug x64",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Build LoopThroughWithOpenGLCompositing Release x64",
|
||||
"label": "Build RenderCadenceCompositor Release x64",
|
||||
"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-release",
|
||||
"--config",
|
||||
"Release",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing",
|
||||
"RenderCadenceCompositor",
|
||||
"--parallel"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
|
||||
"label": "Run Native Tests Debug x64",
|
||||
"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": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||
|
||||
832
CMakeLists.txt
832
CMakeLists.txt
@@ -1,572 +1,314 @@
|
||||
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_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
||||
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(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
|
||||
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
||||
set(LEGACY_THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty")
|
||||
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()
|
||||
|
||||
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
|
||||
"${SLANG_ROOT}/bin/slangc.exe"
|
||||
"${SLANG_ROOT}/bin/slang-compiler.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)
|
||||
if(NOT EXISTS "${SLANG_RUNTIME_FILE}")
|
||||
message(FATAL_ERROR "Required Slang runtime file not found: ${SLANG_RUNTIME_FILE}")
|
||||
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
||||
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
||||
"${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()
|
||||
endforeach()
|
||||
|
||||
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||
if(NOT EXISTS "${SLANG_LICENSE_FILE}")
|
||||
message(FATAL_ERROR "Slang license file not found: ${SLANG_LICENSE_FILE}")
|
||||
foreach(msdf_runtime_file IN LISTS MSDF_ATLAS_GEN_RUNTIME_FILES)
|
||||
if(EXISTS "${msdf_runtime_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()
|
||||
|
||||
set(APP_SOURCES
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
||||
"${APP_DIR}/control/ControlServer.cpp"
|
||||
"${APP_DIR}/control/ControlServer.h"
|
||||
"${APP_DIR}/control/ControlServices.cpp"
|
||||
"${APP_DIR}/control/ControlServices.h"
|
||||
"${APP_DIR}/control/OscServer.cpp"
|
||||
"${APP_DIR}/control/OscServer.h"
|
||||
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
||||
"${APP_DIR}/control/RuntimeControlBridge.h"
|
||||
"${APP_DIR}/control/RuntimeServiceLiveBridge.cpp"
|
||||
"${APP_DIR}/control/RuntimeServiceLiveBridge.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/composite/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/gl/composite/OpenGLComposite.h"
|
||||
"${APP_DIR}/gl/composite/OpenGLCompositeRuntimeControls.cpp"
|
||||
"${APP_DIR}/gl/threading/RenderCommandQueue.cpp"
|
||||
"${APP_DIR}/gl/threading/RenderCommandQueue.h"
|
||||
"${APP_DIR}/gl/RenderEngine.cpp"
|
||||
"${APP_DIR}/gl/RenderEngine.h"
|
||||
"${APP_DIR}/gl/frame/RenderFrameState.h"
|
||||
"${APP_DIR}/gl/frame/RenderFrameStateResolver.cpp"
|
||||
"${APP_DIR}/gl/frame/RenderFrameStateResolver.h"
|
||||
"${APP_DIR}/gl/frame/RuntimeUpdateController.cpp"
|
||||
"${APP_DIR}/gl/frame/RuntimeUpdateController.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/coordination/RuntimeCoordinator.cpp"
|
||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventCoalescingQueue.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventDispatcher.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEvent.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventPayloads.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventQueue.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventType.h"
|
||||
"${APP_DIR}/runtime/live/CommittedLiveState.cpp"
|
||||
"${APP_DIR}/runtime/live/CommittedLiveState.h"
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.h"
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.h"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.h"
|
||||
"${APP_DIR}/runtime/persistence/PersistenceRequest.h"
|
||||
"${APP_DIR}/runtime/persistence/PersistenceWriter.cpp"
|
||||
"${APP_DIR}/runtime/persistence/PersistenceWriter.h"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.h"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.h"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.h"
|
||||
"${APP_DIR}/runtime/snapshot/RuntimeSnapshotProvider.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RuntimeSnapshotProvider.h"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeStoreReadModels.h"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.h"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.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/composite"
|
||||
"${APP_DIR}/gl/frame"
|
||||
"${APP_DIR}/gl/pipeline"
|
||||
"${APP_DIR}/gl/renderer"
|
||||
"${APP_DIR}/gl/shader"
|
||||
"${APP_DIR}/gl/threading"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/coordination"
|
||||
"${APP_DIR}/runtime/events"
|
||||
"${APP_DIR}/runtime/live"
|
||||
"${APP_DIR}/runtime/persistence"
|
||||
"${APP_DIR}/runtime/presentation"
|
||||
"${APP_DIR}/runtime/snapshot"
|
||||
"${APP_DIR}/runtime/store"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
"${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)
|
||||
if(EXISTS "${MSDF_ATLAS_GEN_README_FILE}")
|
||||
install(FILES "${MSDF_ATLAS_GEN_README_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "MSDF_ATLAS_GEN_README.md"
|
||||
)
|
||||
else()
|
||||
message(STATUS "msdf-atlas-gen readme file not found: ${MSDF_ATLAS_GEN_README_FILE}")
|
||||
endif()
|
||||
|
||||
add_executable(RuntimeJsonTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeJsonTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeJsonTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||
|
||||
add_executable(RuntimeClockTests
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeClockTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||
|
||||
add_executable(HealthTelemetryTests
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/HealthTelemetryTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(HealthTelemetryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(HealthTelemetryTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME HealthTelemetryTests COMMAND HealthTelemetryTests)
|
||||
|
||||
add_executable(RuntimeParameterUtilsTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||
|
||||
add_executable(RuntimeEventTypeTests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeEventTypeTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeEventTypeTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/events"
|
||||
"${APP_DIR}/runtime/persistence"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeEventTypeTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeEventTypeTests COMMAND RuntimeEventTypeTests)
|
||||
|
||||
add_executable(RuntimeLiveStateTests
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeLiveStateTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeLiveStateTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/live"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeLiveStateTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeLiveStateTests COMMAND RuntimeLiveStateTests)
|
||||
|
||||
add_executable(RuntimeStateLayerModelTests
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeStateLayerModelTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeStateLayerModelTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/live"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeStateLayerModelTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeStateLayerModelTests COMMAND RuntimeStateLayerModelTests)
|
||||
|
||||
add_executable(PersistenceWriterTests
|
||||
"${APP_DIR}/runtime/persistence/PersistenceWriter.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/PersistenceWriterTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(PersistenceWriterTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/persistence"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(PersistenceWriterTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME PersistenceWriterTests COMMAND PersistenceWriterTests)
|
||||
|
||||
add_executable(RuntimeSubsystemTests
|
||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
||||
"${APP_DIR}/runtime/live/CommittedLiveState.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp"
|
||||
"${APP_DIR}/runtime/persistence/PersistenceWriter.cpp"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.cpp"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeSubsystemTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeSubsystemTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/coordination"
|
||||
"${APP_DIR}/runtime/events"
|
||||
"${APP_DIR}/runtime/live"
|
||||
"${APP_DIR}/runtime/persistence"
|
||||
"${APP_DIR}/runtime/presentation"
|
||||
"${APP_DIR}/runtime/snapshot"
|
||||
"${APP_DIR}/runtime/store"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeSubsystemTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeSubsystemTests COMMAND RuntimeSubsystemTests)
|
||||
|
||||
add_executable(Std140BufferTests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(Std140BufferTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/gl/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(Std140BufferTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||
|
||||
add_executable(RenderCommandQueueTests
|
||||
"${APP_DIR}/gl/threading/RenderCommandQueue.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCommandQueueTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RenderCommandQueueTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/gl/threading"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RenderCommandQueueTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RenderCommandQueueTests COMMAND RenderCommandQueueTests)
|
||||
|
||||
add_executable(ShaderPackageRegistryTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||
|
||||
add_executable(ShaderSlangValidationTests
|
||||
"${APP_DIR}/runtime/support/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}/runtime/support"
|
||||
"${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}"
|
||||
if(EXISTS "${SLANG_LICENSE_FILE}")
|
||||
install(FILES "${SLANG_LICENSE_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "SLANG_LICENSE.txt"
|
||||
)
|
||||
)
|
||||
else()
|
||||
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NDI_RUNTIME_DLL}")
|
||||
install(FILES "${NDI_RUNTIME_DLL}"
|
||||
DESTINATION "."
|
||||
)
|
||||
else()
|
||||
message(STATUS "NDI runtime DLL not found and will not be installed: ${NDI_RUNTIME_DLL}")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NDI_LICENSE_FILE}")
|
||||
install(FILES "${NDI_LICENSE_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "NDI_SDK_LICENSE_AGREEMENT.pdf"
|
||||
)
|
||||
else()
|
||||
message(STATUS "NDI license file not found: ${NDI_LICENSE_FILE}")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${NDI_NOTICES_FILE}")
|
||||
install(FILES "${NDI_NOTICES_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "NDI_RUNTIME_LICENSES.txt"
|
||||
)
|
||||
else()
|
||||
message(STATUS "NDI runtime notices file not found: ${NDI_NOTICES_FILE}")
|
||||
endif()
|
||||
|
||||
if(EXISTS "${DECKLINK_SDK_LICENSE_FILE}")
|
||||
install(FILES "${DECKLINK_SDK_LICENSE_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "BLACKMAGIC_DECKLINK_SDK_EULA.pdf"
|
||||
)
|
||||
else()
|
||||
message(STATUS "Blackmagic DeckLink SDK license file not found: ${DECKLINK_SDK_LICENSE_FILE}")
|
||||
endif()
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||
DESTINATION "."
|
||||
@@ -597,5 +339,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
|
||||
DESTINATION "docs"
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})
|
||||
|
||||
218
README.md
218
README.md
@@ -1,27 +1,37 @@
|
||||
# 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.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `apps/LoopThroughWithOpenGLCompositing/`: native C++ host app.
|
||||
- `src/`: native C++ host app.
|
||||
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
|
||||
- `ui/`: Vite/React control UI.
|
||||
- `config/runtime-host.json`: runtime configuration.
|
||||
- `runtime/templates/`: tracked shader wrapper templates.
|
||||
- `runtime/`: ignored generated runtime cache/state output. See `runtime/README.md`.
|
||||
- `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.
|
||||
|
||||
Native app internals are grouped by boundary:
|
||||
|
||||
- `videoio/`: backend-neutral video I/O contracts, formats, and playout timing.
|
||||
- `videoio/decklink/`: DeckLink-specific device adapter, callbacks, and SDK bindings.
|
||||
- `gl/renderer/`: low-level OpenGL resources and extension helpers.
|
||||
- `gl/pipeline/`: frame pipeline, render passes, video I/O bridge, preview/readback, and screenshots.
|
||||
- `gl/shader/`: shader compilation, texture/text assets, UBO packing, and shader program ownership.
|
||||
- `app/`: startup/shutdown orchestration, runtime-content controller boundary, config, preview, telemetry, and HTTP hookup.
|
||||
- `control/`: HTTP/WebSocket server, command parsing, and runtime-state JSON presentation.
|
||||
- `frames/`: system-memory frame exchange and input mailbox handoff.
|
||||
- `render/`: render thread, readback, runtime render scene, and shared-context shader program preparation.
|
||||
- `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
|
||||
|
||||
@@ -29,20 +39,73 @@ Native app internals are grouped by boundary:
|
||||
- CMake 3.24 or newer.
|
||||
- Node.js and npm for the control UI.
|
||||
- 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
|
||||
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
|
||||
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
|
||||
|
||||
Configure and build the native app:
|
||||
@@ -65,10 +128,11 @@ The native app serves `ui/dist` when it exists, otherwise it falls back to the s
|
||||
The control UI provides:
|
||||
|
||||
- A searchable shader library for adding layers.
|
||||
- Compact parameter rows with inline descriptions and OSC copy controls.
|
||||
- Stack save/recall presets.
|
||||
- Compact parameter rows with inline descriptions and intended OSC route copy controls.
|
||||
- Shader-declared custom Web Component control panels with default-control fallback.
|
||||
- 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
|
||||
|
||||
@@ -88,10 +152,12 @@ The package folder will contain:
|
||||
|
||||
```text
|
||||
dist/VideoShader/
|
||||
LoopThroughWithOpenGLCompositing.exe
|
||||
RenderCadenceCompositor.exe
|
||||
Processing.NDI.Lib.x64.dll
|
||||
config/
|
||||
shaders/
|
||||
3rdParty/slang/bin/
|
||||
3rdParty/msdf-atlas-gen/
|
||||
ui/dist/
|
||||
docs/
|
||||
SHADER_CONTRACT.md
|
||||
@@ -99,9 +165,9 @@ dist/VideoShader/
|
||||
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:
|
||||
|
||||
@@ -130,7 +196,6 @@ Current native test coverage includes:
|
||||
- Parameter normalization and preset filename safety.
|
||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||
- OSC packet parsing.
|
||||
- Slang validation for every available shader package.
|
||||
|
||||
## Runtime Configuration
|
||||
@@ -144,19 +209,36 @@ Current native test coverage includes:
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"runtimeShaderId": "",
|
||||
"input": {
|
||||
"backend": "decklink",
|
||||
"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,
|
||||
"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:
|
||||
|
||||
@@ -164,11 +246,43 @@ The control UI is available at:
|
||||
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
|
||||
|
||||
@@ -191,13 +305,15 @@ A Swagger UI page is available at:
|
||||
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
|
||||
runtime/screenshots/
|
||||
@@ -205,13 +321,15 @@ runtime/screenshots/
|
||||
|
||||
## 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
|
||||
/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
|
||||
|
||||
@@ -225,7 +343,19 @@ shaders/<id>/
|
||||
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
|
||||
|
||||
@@ -235,8 +365,8 @@ Runtime-generated files are intentionally ignored:
|
||||
- `runtime/shader_cache/active_shader.raw.frag`
|
||||
- `runtime/shader_cache/active_shader.frag`
|
||||
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
||||
- `runtime/stack_presets/*.json`
|
||||
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
|
||||
- `runtime/stack_presets/*.json` reserved manual preset output; preset routes are not implemented in the native host yet.
|
||||
- `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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
- `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`.
|
||||
- `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`.
|
||||
|
||||
@@ -257,19 +391,20 @@ Example runner paths:
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
- Audio.
|
||||
- Genlock.
|
||||
- Logs.
|
||||
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2 for an embedded native control surface.
|
||||
- MSDF typography rasterisation
|
||||
- More shader-library organisation and filtering as the built-in library grows.
|
||||
- Optional linear-light compositing mode.
|
||||
- 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
|
||||
- allow 3 vector exposed controls
|
||||
- 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,542 +0,0 @@
|
||||
#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(NULL, 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)
|
||||
{
|
||||
pOpenGLComposite->paintGL(true);
|
||||
RaiseStatusControls(sStatusStrip);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
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,3 +0,0 @@
|
||||
#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,343 +0,0 @@
|
||||
#include "ControlServices.h"
|
||||
|
||||
#include "ControlServer.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeControlBridge.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kCompatibilityPollFallbackInterval = std::chrono::milliseconds(250);
|
||||
}
|
||||
|
||||
ControlServices::ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mControlServer(std::make_unique<ControlServer>()),
|
||||
mOscServer(std::make_unique<OscServer>()),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
mPollRunning(false)
|
||||
{
|
||||
}
|
||||
|
||||
ControlServices::~ControlServices()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool ControlServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (!StartControlServicesBoundary(composite, runtimeStore, *this, *mControlServer, *mOscServer, error))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlServices::BeginPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
StartPolling(runtimeCoordinator);
|
||||
}
|
||||
|
||||
void ControlServices::Stop()
|
||||
{
|
||||
StopPolling();
|
||||
|
||||
if (mOscServer)
|
||||
mOscServer->Stop();
|
||||
|
||||
if (mControlServer)
|
||||
mControlServer->Stop();
|
||||
}
|
||||
|
||||
void ControlServices::BroadcastState()
|
||||
{
|
||||
if (mControlServer)
|
||||
mControlServer->BroadcastState();
|
||||
}
|
||||
|
||||
void ControlServices::RequestBroadcastState()
|
||||
{
|
||||
PublishRuntimeStateBroadcastRequested("control-service-request");
|
||||
|
||||
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));
|
||||
PublishOscValueReceived(entry.second, entry.first);
|
||||
}
|
||||
|
||||
(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);
|
||||
}
|
||||
WakePolling();
|
||||
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::ClearOscStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
for (auto it = mPendingOscUpdates.begin(); it != mPendingOscUpdates.end();)
|
||||
{
|
||||
if (it->second.layerKey == layerKey)
|
||||
it = mPendingOscUpdates.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
for (auto it = mPendingOscCommits.begin(); it != mPendingOscCommits.end();)
|
||||
{
|
||||
if (it->second.layerKey == layerKey)
|
||||
it = mPendingOscCommits.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
for (auto it = mCompletedOscCommits.begin(); it != mCompletedOscCommits.end();)
|
||||
{
|
||||
if (it->routeKey.rfind(layerKey + "\n", 0) == 0)
|
||||
it = mCompletedOscCommits.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||
{
|
||||
completedCommits.clear();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
if (mCompletedOscCommits.empty())
|
||||
return;
|
||||
|
||||
completedCommits.swap(mCompletedOscCommits);
|
||||
}
|
||||
|
||||
void ControlServices::StartPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
if (mPollRunning.exchange(true))
|
||||
return;
|
||||
|
||||
mPollThread = std::thread([this, &runtimeCoordinator]() { PollLoop(runtimeCoordinator); });
|
||||
}
|
||||
|
||||
void ControlServices::StopPolling()
|
||||
{
|
||||
if (!mPollRunning.exchange(false))
|
||||
return;
|
||||
|
||||
WakePolling();
|
||||
if (mPollThread.joinable())
|
||||
mPollThread.join();
|
||||
}
|
||||
|
||||
void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
while (mPollRunning)
|
||||
{
|
||||
std::map<std::string, PendingOscCommit> pendingCommits;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
pendingCommits.swap(mPendingOscCommits);
|
||||
}
|
||||
for (const auto& entry : pendingCommits)
|
||||
{
|
||||
PublishOscCommitRequested(entry.second);
|
||||
const RuntimeCoordinatorResult result = runtimeCoordinator.CommitOscParameterByControlKey(
|
||||
entry.second.layerKey,
|
||||
entry.second.parameterKey,
|
||||
entry.second.value);
|
||||
if (result.accepted)
|
||||
{
|
||||
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));
|
||||
PublishOscOverlaySettled(entry.second);
|
||||
}
|
||||
else if (!result.errorMessage.empty())
|
||||
{
|
||||
OutputDebugStringA(("OSC commit failed: " + result.errorMessage + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool registryChanged = false;
|
||||
const RuntimeCoordinatorResult pollResult = runtimeCoordinator.PollRuntimeStoreChanges(registryChanged);
|
||||
if (pollResult.compileStatusChanged && !pollResult.compileStatusSucceeded && !pollResult.compileStatusMessage.empty())
|
||||
OutputDebugStringA(("Runtime poll failed: " + pollResult.compileStatusMessage + "\n").c_str());
|
||||
|
||||
std::unique_lock<std::mutex> wakeLock(mPollWakeMutex);
|
||||
mPollWakeCondition.wait_for(wakeLock, kCompatibilityPollFallbackInterval, [this]() {
|
||||
return !mPollRunning.load() || mPollWakeRequested;
|
||||
});
|
||||
mPollWakeRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::WakePolling()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPollWakeMutex);
|
||||
mPollWakeRequested = true;
|
||||
}
|
||||
mPollWakeCondition.notify_one();
|
||||
}
|
||||
|
||||
void ControlServices::PublishRuntimeStateBroadcastRequested(const std::string& reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeStateBroadcastRequestedEvent event;
|
||||
event.reason = reason;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("RuntimeStateBroadcastRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RuntimeStateBroadcastRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscValueReceivedEvent event;
|
||||
event.routeKey = routeKey;
|
||||
event.layerKey = update.layerKey;
|
||||
event.parameterKey = update.parameterKey;
|
||||
event.valueJson = update.valueJson;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscValueReceived event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscValueReceived event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscCommitRequested(const PendingOscCommit& commit)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscCommitRequestedEvent event;
|
||||
event.routeKey = commit.routeKey;
|
||||
event.layerKey = commit.layerKey;
|
||||
event.parameterKey = commit.parameterKey;
|
||||
event.valueJson = SerializeJson(commit.value, false);
|
||||
event.generation = commit.generation;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscCommitRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscCommitRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscOverlaySettled(const PendingOscCommit& commit)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscOverlayEvent event;
|
||||
event.routeKey = commit.routeKey;
|
||||
event.layerKey = commit.layerKey;
|
||||
event.parameterKey = commit.parameterKey;
|
||||
event.generation = commit.generation;
|
||||
event.settled = true;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscOverlaySettled event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscOverlaySettled event publish threw.\n");
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class ControlServer;
|
||||
class OpenGLComposite;
|
||||
class OscServer;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeStore;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
explicit ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~ControlServices();
|
||||
|
||||
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);
|
||||
void BeginPolling(RuntimeCoordinator& runtimeCoordinator);
|
||||
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 ClearOscStateForLayerKey(const std::string& layerKey);
|
||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
||||
|
||||
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(RuntimeCoordinator& runtimeCoordinator);
|
||||
void StopPolling();
|
||||
void PollLoop(RuntimeCoordinator& runtimeCoordinator);
|
||||
void WakePolling();
|
||||
void PublishRuntimeStateBroadcastRequested(const std::string& reason);
|
||||
void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey);
|
||||
void PublishOscCommitRequested(const PendingOscCommit& commit);
|
||||
void PublishOscOverlaySettled(const PendingOscCommit& commit);
|
||||
|
||||
std::unique_ptr<ControlServer> mControlServer;
|
||||
std::unique_ptr<OscServer> mOscServer;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
std::thread mPollThread;
|
||||
std::atomic<bool> mPollRunning;
|
||||
std::mutex mPollWakeMutex;
|
||||
std::condition_variable mPollWakeCondition;
|
||||
bool mPollWakeRequested = false;
|
||||
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,54 +0,0 @@
|
||||
#include "RuntimeControlBridge.h"
|
||||
|
||||
#include "ControlServices.h"
|
||||
#include "ControlServer.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
bool StartControlServicesBoundary(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeStore& runtimeStore,
|
||||
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(runtimeStore.GetRuntimeUiRoot(), runtimeStore.GetRuntimeDocsRoot(), runtimeStore.GetConfiguredControlServerPort(), callbacks, error))
|
||||
return false;
|
||||
runtimeStore.SetBoundControlServerPort(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 (runtimeStore.GetConfiguredOscPort() > 0 &&
|
||||
!oscServer.Start(runtimeStore.GetConfiguredOscBindAddress(), runtimeStore.GetConfiguredOscPort(), oscCallbacks, error))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class ControlServer;
|
||||
class ControlServices;
|
||||
class OpenGLComposite;
|
||||
class OscServer;
|
||||
class RuntimeStore;
|
||||
|
||||
bool StartControlServicesBoundary(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeStore& runtimeStore,
|
||||
ControlServices& controlServices,
|
||||
ControlServer& controlServer,
|
||||
OscServer& oscServer,
|
||||
std::string& error);
|
||||
@@ -1,72 +0,0 @@
|
||||
#include "RuntimeServiceLiveBridge.h"
|
||||
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void DrainServiceEvents(RuntimeServices& runtimeServices, RenderEngine& renderEngine)
|
||||
{
|
||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||
|
||||
std::string oscError;
|
||||
if (!runtimeServices.ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||
runtimeServices.ConsumeCompletedOscCommits(completedOscCommits);
|
||||
|
||||
std::vector<RenderEngine::OscOverlayUpdate> overlayUpdates;
|
||||
overlayUpdates.reserve(appliedOscUpdates.size());
|
||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||
{
|
||||
overlayUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||
}
|
||||
|
||||
std::vector<RenderEngine::OscOverlayCommitCompletion> overlayCommitCompletions;
|
||||
overlayCommitCompletions.reserve(completedOscCommits.size());
|
||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||
{
|
||||
overlayCommitCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||
}
|
||||
|
||||
renderEngine.UpdateOscOverlayState(overlayUpdates, overlayCommitCompletions);
|
||||
}
|
||||
|
||||
void QueueServiceCommitRequests(
|
||||
RuntimeServices& runtimeServices,
|
||||
const std::vector<RenderEngine::OscOverlayCommitRequest>& commitRequests)
|
||||
{
|
||||
for (const RenderEngine::OscOverlayCommitRequest& commitRequest : commitRequests)
|
||||
{
|
||||
std::string commitError;
|
||||
if (!runtimeServices.QueueOscCommit(
|
||||
commitRequest.routeKey,
|
||||
commitRequest.layerKey,
|
||||
commitRequest.parameterKey,
|
||||
commitRequest.value,
|
||||
commitRequest.generation,
|
||||
commitError) &&
|
||||
!commitError.empty())
|
||||
{
|
||||
OutputDebugStringA(("OSC commit queue failed: " + commitError + "\n").c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
const RenderFrameInput& input,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
DrainServiceEvents(runtimeServices, renderEngine);
|
||||
|
||||
std::vector<RenderEngine::OscOverlayCommitRequest> commitRequests;
|
||||
const bool resolved = renderEngine.ResolveRenderFrameState(input, &commitRequests, frameState);
|
||||
|
||||
QueueServiceCommitRequests(runtimeServices, commitRequests);
|
||||
return resolved;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderFrameState.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
class RenderEngine;
|
||||
class RuntimeServices;
|
||||
|
||||
class RuntimeServiceLiveBridge
|
||||
{
|
||||
public:
|
||||
static bool PrepareLiveRenderFrameState(
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
const RenderFrameInput& input,
|
||||
RenderFrameState& frameState);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
RuntimeServices::RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mControlServices(std::make_unique<ControlServices>(runtimeEventDispatcher))
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeServices::~RuntimeServices()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error)
|
||||
{
|
||||
return mControlServices && mControlServices->Start(composite, runtimeStore, error);
|
||||
}
|
||||
|
||||
void RuntimeServices::BeginPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
if (mControlServices)
|
||||
mControlServices->BeginPolling(runtimeCoordinator);
|
||||
}
|
||||
|
||||
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::ClearOscStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
if (mControlServices)
|
||||
mControlServices->ClearOscStateForLayerKey(layerKey);
|
||||
}
|
||||
|
||||
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||
{
|
||||
if (!mControlServices)
|
||||
{
|
||||
completedCommits.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
mControlServices->ConsumeCompletedOscCommits(completedCommits);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ControlServices.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
class OpenGLComposite;
|
||||
class RuntimeCoordinator;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeStore;
|
||||
|
||||
class RuntimeServices
|
||||
{
|
||||
public:
|
||||
using AppliedOscUpdate = ControlServices::AppliedOscUpdate;
|
||||
using CompletedOscCommit = ControlServices::CompletedOscCommit;
|
||||
|
||||
explicit RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~RuntimeServices();
|
||||
|
||||
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);
|
||||
void BeginPolling(RuntimeCoordinator& runtimeCoordinator);
|
||||
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 ClearOscStateForLayerKey(const std::string& layerKey);
|
||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
||||
|
||||
private:
|
||||
std::unique_ptr<ControlServices> mControlServices;
|
||||
};
|
||||
@@ -1,663 +0,0 @@
|
||||
#include "RenderEngine.h"
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
RenderEngine::RenderEngine(
|
||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||
HealthTelemetry& healthTelemetry,
|
||||
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),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc),
|
||||
mFrameStateResolver(runtimeSnapshotProvider)
|
||||
{
|
||||
}
|
||||
|
||||
RenderEngine::~RenderEngine()
|
||||
{
|
||||
StopRenderThread();
|
||||
if (!mResourcesDestroyed)
|
||||
{
|
||||
mRenderer.DestroyResources();
|
||||
mResourcesDestroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderEngine::StartRenderThread()
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
return true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadStopping = false;
|
||||
}
|
||||
|
||||
std::promise<bool> ready;
|
||||
std::future<bool> readyResult = ready.get_future();
|
||||
mRenderThread = std::thread(&RenderEngine::RenderThreadMain, this, std::move(ready));
|
||||
if (!readyResult.get())
|
||||
{
|
||||
if (mRenderThread.joinable())
|
||||
mRenderThread.join();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderEngine::StopRenderThread()
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
if (!mResourcesDestroyed)
|
||||
{
|
||||
mRenderer.DestroyResources();
|
||||
mResourcesDestroyed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadStopping = true;
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
|
||||
if (mRenderThread.joinable())
|
||||
mRenderThread.join();
|
||||
}
|
||||
|
||||
void RenderEngine::RenderThreadMain(std::promise<bool> ready)
|
||||
{
|
||||
mRenderThreadId = GetCurrentThreadId();
|
||||
if (!wglMakeCurrent(mHdc, mHglrc))
|
||||
{
|
||||
mRenderThreadId = 0;
|
||||
ready.set_value(false);
|
||||
return;
|
||||
}
|
||||
|
||||
mRenderThreadRunning = true;
|
||||
ready.set_value(true);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadCondition.wait(lock, [this]() {
|
||||
return mRenderThreadStopping || !mRenderThreadTasks.empty();
|
||||
});
|
||||
|
||||
if (mRenderThreadStopping && mRenderThreadTasks.empty())
|
||||
break;
|
||||
|
||||
task = std::move(mRenderThreadTasks.front());
|
||||
mRenderThreadTasks.pop();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("Render thread task failed with an unhandled exception.\n");
|
||||
}
|
||||
}
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
mRenderThreadRunning = false;
|
||||
mRenderThreadId = 0;
|
||||
}
|
||||
|
||||
void RenderEngine::ReportRenderThreadRequestFailure(const char* operationName, const char* reason)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "Render thread request failed";
|
||||
if (operationName && operationName[0] != '\0')
|
||||
message << " [" << operationName << "]";
|
||||
if (reason && reason[0] != '\0')
|
||||
message << ": " << reason;
|
||||
message << ".\n";
|
||||
OutputDebugStringA(message.str().c_str());
|
||||
}
|
||||
|
||||
bool RenderEngine::IsRenderThreadAccessExpected() const
|
||||
{
|
||||
return !mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId;
|
||||
}
|
||||
|
||||
void RenderEngine::ReportWrongThreadRenderAccess(const char* operationName) const
|
||||
{
|
||||
if (IsRenderThreadAccessExpected())
|
||||
return;
|
||||
|
||||
std::ostringstream message;
|
||||
message << "Wrong-thread render access detected";
|
||||
if (operationName && operationName[0] != '\0')
|
||||
message << " [" << operationName << "]";
|
||||
message << ".\n";
|
||||
OutputDebugStringA(message.str().c_str());
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, errorMessageSize, errorMessage]() {
|
||||
return mShaderPrograms.CompileDecodeShader(errorMessageSize, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, errorMessageSize, 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 InvokeOnRenderThread([this, inputFrameWidth, inputFrameHeight, captureTextureWidth, outputFrameWidth, outputFrameHeight, outputPackTextureWidth, &error]() {
|
||||
return mRenderer.InitializeResources(
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
captureTextureWidth,
|
||||
outputFrameWidth,
|
||||
outputFrameHeight,
|
||||
outputPackTextureWidth,
|
||||
error);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage]() {
|
||||
return mShaderPrograms.CompileLayerPrograms(inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, &preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, 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;
|
||||
|
||||
mFrameStateResolver.StoreCommittedSnapshot(preparedBuild.renderSnapshot, mShaderPrograms.CommittedLayerStates());
|
||||
ResetTemporalHistoryState();
|
||||
if (!preserveFeedbackState)
|
||||
ResetShaderFeedbackState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderEngine::ResetTemporalHistoryState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ResetShaderFeedbackState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::ShaderFeedbackOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope)
|
||||
{
|
||||
InvokeOnRenderThread([this, resetScope]() {
|
||||
switch (resetScope)
|
||||
{
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
break;
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryAndFeedback);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
break;
|
||||
case RuntimeCoordinatorRenderResetScope::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ResetTemporalHistoryStateOnRenderThread()
|
||||
{
|
||||
ReportWrongThreadRenderAccess("reset-temporal-history");
|
||||
mShaderPrograms.ResetTemporalHistoryState();
|
||||
}
|
||||
|
||||
void RenderEngine::ResetShaderFeedbackStateOnRenderThread()
|
||||
{
|
||||
ReportWrongThreadRenderAccess("reset-shader-feedback");
|
||||
mShaderPrograms.ResetShaderFeedbackState();
|
||||
}
|
||||
|
||||
void RenderEngine::ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope)
|
||||
{
|
||||
switch (resetScope)
|
||||
{
|
||||
case RenderCommandResetScope::ShaderFeedbackOnly:
|
||||
ResetShaderFeedbackStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::TemporalHistoryOnly:
|
||||
ResetTemporalHistoryStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::TemporalHistoryAndFeedback:
|
||||
ResetTemporalHistoryStateOnRenderThread();
|
||||
ResetShaderFeedbackStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessRenderResetCommandsOnRenderThread()
|
||||
{
|
||||
RenderCommandResetScope resetScope = RenderCommandResetScope::None;
|
||||
while (mRenderCommandQueue.TryTakeRenderReset(resetScope))
|
||||
ApplyRenderResetOnRenderThread(resetScope);
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueuePreviewPresentWake()
|
||||
{
|
||||
if (!mRenderThreadRunning)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mPreviewPresentWakePending)
|
||||
{
|
||||
mPreviewPresentWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mPreviewPresentWakePending = false;
|
||||
}
|
||||
ProcessPreviewPresentCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessPreviewPresentCommandsOnRenderThread()
|
||||
{
|
||||
RenderPreviewPresentRequest request;
|
||||
if (mRenderCommandQueue.TryTakePreviewPresent(request))
|
||||
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueueInputUploadWake()
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mInputUploadWakePending)
|
||||
{
|
||||
mInputUploadWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mInputUploadWakePending = false;
|
||||
}
|
||||
ProcessInputUploadCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessInputUploadCommandsOnRenderThread()
|
||||
{
|
||||
RenderInputUploadRequest request;
|
||||
while (mRenderCommandQueue.TryTakeInputUpload(request))
|
||||
{
|
||||
if (request.ownedBytes.empty())
|
||||
continue;
|
||||
|
||||
request.inputFrame.bytes = request.ownedBytes.data();
|
||||
UploadInputFrameOnRenderThread(request.inputFrame, request.videoState);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueueScreenshotCaptureWake()
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mScreenshotCaptureWakePending)
|
||||
{
|
||||
mScreenshotCaptureWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mScreenshotCaptureWakePending = false;
|
||||
}
|
||||
ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread()
|
||||
{
|
||||
RenderScreenshotCaptureRequest request;
|
||||
ScreenshotCaptureCallback completion;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
completion = mScreenshotCaptureCompletion;
|
||||
}
|
||||
|
||||
while (mRenderCommandQueue.TryTakeScreenshotCapture(request))
|
||||
{
|
||||
if (!completion)
|
||||
continue;
|
||||
|
||||
std::vector<unsigned char> topDownPixels;
|
||||
if (CaptureOutputFrameRgbaTopDownOnRenderThread(request.width, request.height, topDownPixels))
|
||||
completion(request.width, request.height, std::move(topDownPixels));
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::ClearOscOverlayState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRuntimeLiveState.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ClearOscOverlayStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
InvokeOnRenderThread([this, layerKey]() {
|
||||
mRuntimeLiveState.ClearForLayerKey(layerKey);
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::UpdateOscOverlayState(
|
||||
const std::vector<OscOverlayUpdate>& updates,
|
||||
const std::vector<OscOverlayCommitCompletion>& completedCommits)
|
||||
{
|
||||
std::vector<RuntimeLiveOscCommitCompletion> liveCompletions;
|
||||
liveCompletions.reserve(completedCommits.size());
|
||||
for (const OscOverlayCommitCompletion& completedCommit : completedCommits)
|
||||
liveCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||
mRuntimeLiveState.ApplyOscCommitCompletions(liveCompletions);
|
||||
|
||||
std::vector<RuntimeLiveOscUpdate> liveUpdates;
|
||||
liveUpdates.reserve(updates.size());
|
||||
for (const OscOverlayUpdate& update : updates)
|
||||
liveUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||
mRuntimeLiveState.ApplyOscUpdates(liveUpdates);
|
||||
}
|
||||
|
||||
void RenderEngine::ResizeView(int width, int height)
|
||||
{
|
||||
InvokeOnRenderThread([this, width, 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 (mRenderThreadRunning)
|
||||
{
|
||||
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
|
||||
EnqueuePreviewPresentWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure("preview-present", "render thread is not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderEngine::PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("preview-present");
|
||||
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
|
||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion)
|
||||
{
|
||||
if (width == 0 || height == 0 || !completion)
|
||||
return false;
|
||||
if (!mRenderThreadRunning)
|
||||
return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mScreenshotCaptureCompletion = std::move(completion);
|
||||
}
|
||||
mRenderCommandQueue.RequestScreenshotCapture({ width, height });
|
||||
EnqueueScreenshotCaptureWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||
{
|
||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||
return true;
|
||||
if (inputFrame.rowBytes <= 0 || inputFrame.height == 0)
|
||||
return false;
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(inputFrame.rowBytes) * inputFrame.height;
|
||||
RenderInputUploadRequest request;
|
||||
request.inputFrame = inputFrame;
|
||||
request.videoState = videoState;
|
||||
request.ownedBytes.resize(byteCount);
|
||||
std::memcpy(request.ownedBytes.data(), inputFrame.bytes, byteCount);
|
||||
request.inputFrame.bytes = nullptr;
|
||||
|
||||
mRenderCommandQueue.RequestInputUpload(request);
|
||||
EnqueueInputUploadWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("input-upload");
|
||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||
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);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
{
|
||||
return TryInvokeOnRenderThread("output-render", [this, &context, &outputFrame]() {
|
||||
mRenderCommandQueue.RequestOutputFrame({ context.videoState, context.completion });
|
||||
RenderOutputFrameRequest request;
|
||||
return mRenderCommandQueue.TryTakeOutputFrame(request) &&
|
||||
RenderOutputFrameOnRenderThread({ request.videoState, request.completion }, outputFrame);
|
||||
});
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure("output-render", "render thread is not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("output-render");
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
ProcessInputUploadCommandsOnRenderThread();
|
||||
return mRenderPipeline.RenderFrame(context, outputFrame);
|
||||
}
|
||||
|
||||
bool RenderEngine::ResolveRenderFrameState(
|
||||
const RenderFrameInput& input,
|
||||
std::vector<OscOverlayCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
std::vector<RuntimeLiveOscCommitRequest> liveCommitRequests;
|
||||
const bool resolved = mFrameStateResolver.Resolve(
|
||||
input,
|
||||
mShaderPrograms.CommittedLayerStates(),
|
||||
mRuntimeLiveState,
|
||||
commitRequests ? &liveCommitRequests : nullptr,
|
||||
frameState);
|
||||
|
||||
if (commitRequests)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitRequest& request : liveCommitRequests)
|
||||
commitRequests->push_back({ request.routeKey, request.layerKey, request.parameterKey, request.value, request.generation });
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
void RenderEngine::RenderPreparedFrame(const RenderFrameState& frameState)
|
||||
{
|
||||
RenderLayerStack(
|
||||
frameState.hasInputSource,
|
||||
frameState.layerStates,
|
||||
frameState.inputFrameWidth,
|
||||
frameState.inputFrameHeight,
|
||||
frameState.captureTextureWidth,
|
||||
frameState.inputPixelFormat,
|
||||
frameState.historyCap);
|
||||
}
|
||||
|
||||
void RenderEngine::RenderLayerStack(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("render-layer-stack");
|
||||
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::ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("read-output-frame-rgba");
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
|
||||
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);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
|
||||
{
|
||||
std::vector<unsigned char> bottomUpPixels;
|
||||
if (!ReadOutputFrameRgbaOnRenderThread(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,231 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderPass.h"
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
#include "RenderCommandQueue.h"
|
||||
#include "RenderFrameState.h"
|
||||
#include "RenderFrameStateResolver.h"
|
||||
#include "HealthTelemetry.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class RenderEngine
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using ScreenshotCallback = std::function<void()>;
|
||||
using ScreenshotCaptureCallback = std::function<void(unsigned, unsigned, std::vector<unsigned char>)>;
|
||||
using PreviewPaintCallback = std::function<void()>;
|
||||
|
||||
struct OscOverlayUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
|
||||
struct OscOverlayCommitCompletion
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct OscOverlayCommitRequest
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue value;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
RenderEngine(
|
||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||
HealthTelemetry& healthTelemetry,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
ScreenshotCallback screenshotReady,
|
||||
PreviewPaintCallback previewPaint);
|
||||
~RenderEngine();
|
||||
|
||||
bool StartRenderThread();
|
||||
void StopRenderThread();
|
||||
|
||||
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 ApplyPreparedShaderBuild(
|
||||
const PreparedShaderBuild& preparedBuild,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
bool preserveFeedbackState,
|
||||
int errorMessageSize,
|
||||
char* errorMessage);
|
||||
|
||||
void ResetTemporalHistoryState();
|
||||
void ResetShaderFeedbackState();
|
||||
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
|
||||
void ClearOscOverlayState();
|
||||
void ClearOscOverlayStateForLayerKey(const std::string& layerKey);
|
||||
void UpdateOscOverlayState(
|
||||
const std::vector<OscOverlayUpdate>& updates,
|
||||
const std::vector<OscOverlayCommitCompletion>& completedCommits);
|
||||
void ResizeView(int width, int height);
|
||||
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||
bool RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion);
|
||||
bool QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||
bool RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
bool ResolveRenderFrameState(
|
||||
const RenderFrameInput& input,
|
||||
std::vector<OscOverlayCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState);
|
||||
void RenderPreparedFrame(const RenderFrameState& frameState);
|
||||
|
||||
private:
|
||||
static constexpr std::chrono::milliseconds kRenderThreadRequestTimeout{ 250 };
|
||||
|
||||
struct RenderThreadTaskState
|
||||
{
|
||||
std::atomic<bool> started = false;
|
||||
std::atomic<bool> cancelled = false;
|
||||
};
|
||||
|
||||
template<typename Func>
|
||||
auto InvokeOnRenderThread(Func&& func) -> decltype(func())
|
||||
{
|
||||
using Result = decltype(func());
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return func();
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<Result()>>(std::forward<Func>(func));
|
||||
std::future<Result> result = task->get_future();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadTasks.push([task]() { (*task)(); });
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
return result.get();
|
||||
}
|
||||
|
||||
template<typename Func>
|
||||
bool TryInvokeOnRenderThread(const char* operationName, Func&& func)
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return func();
|
||||
|
||||
auto state = std::make_shared<RenderThreadTaskState>();
|
||||
auto task = std::make_shared<std::packaged_task<bool()>>(
|
||||
[state, func = std::forward<Func>(func)]() mutable {
|
||||
state->started = true;
|
||||
if (state->cancelled)
|
||||
return false;
|
||||
|
||||
return func();
|
||||
});
|
||||
std::future<bool> result = task->get_future();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (mRenderThreadStopping)
|
||||
{
|
||||
ReportRenderThreadRequestFailure(operationName, "render thread is stopping");
|
||||
return false;
|
||||
}
|
||||
mRenderThreadTasks.push([task]() { (*task)(); });
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
|
||||
if (result.wait_for(kRenderThreadRequestTimeout) == std::future_status::ready)
|
||||
return result.get();
|
||||
|
||||
if (!state->started)
|
||||
{
|
||||
state->cancelled = true;
|
||||
ReportRenderThreadRequestFailure(operationName, "timed out before execution");
|
||||
return false;
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure(operationName, "exceeded timeout while executing; waiting for safe completion");
|
||||
return result.get();
|
||||
}
|
||||
|
||||
void RenderThreadMain(std::promise<bool> ready);
|
||||
void ReportRenderThreadRequestFailure(const char* operationName, const char* reason);
|
||||
bool IsRenderThreadAccessExpected() const;
|
||||
void ReportWrongThreadRenderAccess(const char* operationName) const;
|
||||
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||
void RenderLayerStack(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap);
|
||||
void ResetTemporalHistoryStateOnRenderThread();
|
||||
void ResetShaderFeedbackStateOnRenderThread();
|
||||
void ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope);
|
||||
void ProcessRenderResetCommandsOnRenderThread();
|
||||
void EnqueuePreviewPresentWake();
|
||||
void ProcessPreviewPresentCommandsOnRenderThread();
|
||||
void EnqueueInputUploadWake();
|
||||
void ProcessInputUploadCommandsOnRenderThread();
|
||||
void EnqueueScreenshotCaptureWake();
|
||||
void ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||
bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||
bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||
bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
bool ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels);
|
||||
bool CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
|
||||
|
||||
OpenGLRenderer mRenderer;
|
||||
OpenGLRenderPass mRenderPass;
|
||||
OpenGLRenderPipeline mRenderPipeline;
|
||||
OpenGLShaderPrograms mShaderPrograms;
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
|
||||
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||
RenderCommandQueue mRenderCommandQueue;
|
||||
RenderFrameStateResolver mFrameStateResolver;
|
||||
RuntimeLiveState mRuntimeLiveState;
|
||||
std::thread mRenderThread;
|
||||
std::atomic<DWORD> mRenderThreadId = 0;
|
||||
std::mutex mRenderThreadMutex;
|
||||
std::condition_variable mRenderThreadCondition;
|
||||
std::queue<std::function<void()>> mRenderThreadTasks;
|
||||
std::atomic<bool> mRenderThreadRunning = false;
|
||||
bool mRenderThreadStopping = false;
|
||||
bool mPreviewPresentWakePending = false;
|
||||
bool mInputUploadWakePending = false;
|
||||
bool mScreenshotCaptureWakePending = false;
|
||||
ScreenshotCaptureCallback mScreenshotCaptureCompletion;
|
||||
bool mResourcesDestroyed = false;
|
||||
};
|
||||
@@ -1,419 +0,0 @@
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "PngScreenshotWriter.h"
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeServiceLiveBridge.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "RuntimeUpdateController.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
#include "VideoBackend.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC)
|
||||
{
|
||||
mRuntimeStore = std::make_unique<RuntimeStore>();
|
||||
mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
|
||||
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher);
|
||||
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore, *mRuntimeEventDispatcher);
|
||||
mRenderEngine = std::make_unique<RenderEngine>(
|
||||
*mRuntimeSnapshotProvider,
|
||||
mRuntimeStore->GetHealthTelemetry(),
|
||||
hGLDC,
|
||||
hGLRC,
|
||||
[this]() { renderEffect(); },
|
||||
[]() {},
|
||||
[this]() { paintGL(false); });
|
||||
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher);
|
||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher);
|
||||
mRuntimeServices = std::make_unique<RuntimeServices>(*mRuntimeEventDispatcher);
|
||||
mRuntimeUpdateController = std::make_unique<RuntimeUpdateController>(
|
||||
*mRuntimeStore,
|
||||
*mRuntimeCoordinator,
|
||||
*mRuntimeEventDispatcher,
|
||||
*mRuntimeServices,
|
||||
*mRenderEngine,
|
||||
*mShaderBuildQueue,
|
||||
*mVideoBackend);
|
||||
}
|
||||
|
||||
OpenGLComposite::~OpenGLComposite()
|
||||
{
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->Stop();
|
||||
if (mShaderBuildQueue)
|
||||
mShaderBuildQueue->Stop();
|
||||
if (mVideoBackend)
|
||||
mVideoBackend->ReleaseResources();
|
||||
if (mRuntimeStore)
|
||||
{
|
||||
std::string persistenceError;
|
||||
if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
|
||||
OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
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())
|
||||
mVideoBackend->ReportNoInputDeviceSignalStatus();
|
||||
|
||||
if (!mVideoBackend->ConfigureOutput(videoModes.output, mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), initFailureReason))
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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, *mRuntimeStore, 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.");
|
||||
|
||||
mRenderEngine->ResetTemporalHistoryState();
|
||||
mRenderEngine->ResetShaderFeedbackState();
|
||||
|
||||
mRuntimeUpdateController->BroadcastRuntimeState();
|
||||
mRuntimeServices->BeginPolling(*mRuntimeCoordinator);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Start()
|
||||
{
|
||||
if (!mRenderEngine->StartRenderThread())
|
||||
return false;
|
||||
|
||||
if (mVideoBackend->Start())
|
||||
return true;
|
||||
|
||||
mRenderEngine->StopRenderThread();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Stop()
|
||||
{
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->Stop();
|
||||
|
||||
const bool wasExternalKeyingActive = mVideoBackend->ExternalKeyingActive();
|
||||
mVideoBackend->Stop();
|
||||
if (wasExternalKeyingActive)
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
"External keying has been disabled.");
|
||||
|
||||
if (mRenderEngine)
|
||||
mRenderEngine->StopRenderThread();
|
||||
|
||||
if (mRuntimeStore)
|
||||
{
|
||||
std::string persistenceError;
|
||||
if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
|
||||
OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState));
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||
{
|
||||
if (!mRenderEngine || !mVideoBackend)
|
||||
{
|
||||
error = "The render engine is not ready.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsigned width = mVideoBackend->OutputFrameWidth();
|
||||
const unsigned height = mVideoBackend->OutputFrameHeight();
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
error = "The output frame size is not available.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path outputPath;
|
||||
try
|
||||
{
|
||||
outputPath = BuildScreenshotPath();
|
||||
std::filesystem::create_directories(outputPath.parent_path());
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRenderEngine->RequestScreenshotCapture(
|
||||
width,
|
||||
height,
|
||||
[outputPath](unsigned captureWidth, unsigned captureHeight, std::vector<unsigned char> topDownPixels) {
|
||||
try
|
||||
{
|
||||
WritePngFileAsync(outputPath, captureWidth, captureHeight, std::move(topDownPixels));
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||
}
|
||||
}))
|
||||
{
|
||||
error = "Screenshot capture request failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLComposite::renderEffect()
|
||||
{
|
||||
if (mRuntimeUpdateController)
|
||||
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||
|
||||
const RenderFrameInput frameInput = BuildRenderFrameInput();
|
||||
RenderFrame(frameInput);
|
||||
}
|
||||
|
||||
RenderFrameInput OpenGLComposite::BuildRenderFrameInput() const
|
||||
{
|
||||
RenderFrameInput frameInput;
|
||||
frameInput.useCommittedLayerStates = mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates();
|
||||
frameInput.hasInputSource = mVideoBackend->HasInputSource();
|
||||
frameInput.renderWidth = mVideoBackend->InputFrameWidth();
|
||||
frameInput.renderHeight = mVideoBackend->InputFrameHeight();
|
||||
frameInput.inputFrameWidth = mVideoBackend->InputFrameWidth();
|
||||
frameInput.inputFrameHeight = mVideoBackend->InputFrameHeight();
|
||||
frameInput.captureTextureWidth = mVideoBackend->CaptureTextureWidth();
|
||||
frameInput.inputPixelFormat = mVideoBackend->InputPixelFormat();
|
||||
frameInput.historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
|
||||
frameInput.oscSmoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0;
|
||||
return frameInput;
|
||||
}
|
||||
|
||||
void OpenGLComposite::RenderFrame(const RenderFrameInput& frameInput)
|
||||
{
|
||||
RenderFrameState frameState;
|
||||
if (mRuntimeServices)
|
||||
{
|
||||
RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(
|
||||
*mRuntimeServices,
|
||||
*mRenderEngine,
|
||||
frameInput,
|
||||
frameState);
|
||||
}
|
||||
else
|
||||
{
|
||||
mRenderEngine->ResolveRenderFrameState(frameInput, nullptr, frameState);
|
||||
}
|
||||
mRenderEngine->RenderPreparedFrame(frameState);
|
||||
}
|
||||
|
||||
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::CheckOpenGLExtensions()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#ifndef __OPENGL_COMPOSITE_H__
|
||||
#define __OPENGL_COMPOSITE_H__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
|
||||
#include "RenderFrameState.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class RenderEngine;
|
||||
class RuntimeCoordinator;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeSnapshotProvider;
|
||||
class RuntimeServices;
|
||||
class RuntimeStore;
|
||||
class RuntimeUpdateController;
|
||||
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();
|
||||
|
||||
HWND hGLWnd;
|
||||
HDC hGLDC;
|
||||
HGLRC hGLRC;
|
||||
|
||||
std::unique_ptr<RuntimeStore> mRuntimeStore;
|
||||
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
|
||||
std::unique_ptr<RuntimeSnapshotProvider> mRuntimeSnapshotProvider;
|
||||
std::unique_ptr<RuntimeEventDispatcher> mRuntimeEventDispatcher;
|
||||
std::unique_ptr<RenderEngine> mRenderEngine;
|
||||
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||
std::unique_ptr<RuntimeUpdateController> mRuntimeUpdateController;
|
||||
std::unique_ptr<VideoBackend> mVideoBackend;
|
||||
|
||||
bool InitOpenGLState();
|
||||
void renderEffect();
|
||||
RenderFrameInput BuildRenderFrameInput() const;
|
||||
void RenderFrame(const RenderFrameInput& frameInput);
|
||||
std::filesystem::path BuildScreenshotPath() const;
|
||||
};
|
||||
|
||||
#endif // __OPENGL_COMPOSITE_H__
|
||||
@@ -1,126 +0,0 @@
|
||||
#include "OpenGLComposite.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "RuntimeUpdateController.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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->AddLayer(shaderId), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RemoveLayer(layerId), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayer(layerId, direction), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayerToIndex(layerId, targetIndex), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerBypass(layerId, bypassed), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->ResetLayerParameters(layerId), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SaveStackPreset(presetName), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
|
||||
{
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->LoadStackPreset(presetName), &error);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct RenderFrameInput
|
||||
{
|
||||
bool useCommittedLayerStates = false;
|
||||
bool hasInputSource = false;
|
||||
unsigned renderWidth = 0;
|
||||
unsigned renderHeight = 0;
|
||||
unsigned inputFrameWidth = 0;
|
||||
unsigned inputFrameHeight = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
unsigned historyCap = 0;
|
||||
double oscSmoothing = 0.0;
|
||||
};
|
||||
|
||||
struct RenderFrameState
|
||||
{
|
||||
bool hasInputSource = false;
|
||||
unsigned inputFrameWidth = 0;
|
||||
unsigned inputFrameHeight = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
unsigned historyCap = 0;
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
#include "RenderFrameStateResolver.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
||||
}
|
||||
|
||||
RenderFrameStateResolver::RenderFrameStateResolver(RuntimeSnapshotProvider& runtimeSnapshotProvider) :
|
||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderFrameStateResolver::StoreCommittedSnapshot(
|
||||
const RuntimeRenderStateSnapshot& snapshot,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates)
|
||||
{
|
||||
mCachedLayerRenderStates = committedLayerStates;
|
||||
mCachedRenderStateVersion = snapshot.versions.renderStateVersion;
|
||||
mCachedParameterStateVersion = snapshot.versions.parameterStateVersion;
|
||||
mCachedRenderStateWidth = snapshot.outputWidth;
|
||||
mCachedRenderStateHeight = snapshot.outputHeight;
|
||||
}
|
||||
|
||||
bool RenderFrameStateResolver::Resolve(
|
||||
const RenderFrameInput& input,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates,
|
||||
RuntimeLiveState& liveState,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
frameState.hasInputSource = input.hasInputSource;
|
||||
frameState.inputFrameWidth = input.inputFrameWidth;
|
||||
frameState.inputFrameHeight = input.inputFrameHeight;
|
||||
frameState.captureTextureWidth = input.captureTextureWidth;
|
||||
frameState.inputPixelFormat = input.inputPixelFormat;
|
||||
frameState.historyCap = input.historyCap;
|
||||
frameState.layerStates.clear();
|
||||
|
||||
if (input.useCommittedLayerStates)
|
||||
{
|
||||
frameState.layerStates = ComposeLayerStates(committedLayerStates, liveState, false, input.oscSmoothing, commitRequests);
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return true;
|
||||
}
|
||||
|
||||
const RuntimeSnapshotVersions versions = mRuntimeSnapshotProvider.GetVersions();
|
||||
const bool renderStateCacheValid =
|
||||
!mCachedLayerRenderStates.empty() &&
|
||||
mCachedRenderStateVersion == versions.renderStateVersion &&
|
||||
mCachedRenderStateWidth == input.renderWidth &&
|
||||
mCachedRenderStateHeight == input.renderHeight;
|
||||
|
||||
if (renderStateCacheValid)
|
||||
{
|
||||
RuntimeRenderStateSnapshot renderSnapshot;
|
||||
renderSnapshot.outputWidth = input.renderWidth;
|
||||
renderSnapshot.outputHeight = input.renderHeight;
|
||||
renderSnapshot.versions.renderStateVersion = mCachedRenderStateVersion;
|
||||
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
|
||||
renderSnapshot.states = mCachedLayerRenderStates;
|
||||
|
||||
renderSnapshot.states = ComposeLayerStates(renderSnapshot.states, liveState, true, input.oscSmoothing, commitRequests);
|
||||
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
|
||||
mRuntimeSnapshotProvider.TryRefreshPublishedSnapshotParameters(renderSnapshot))
|
||||
{
|
||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||
renderSnapshot.states = ComposeLayerStates(renderSnapshot.states, liveState, true, input.oscSmoothing, commitRequests);
|
||||
}
|
||||
|
||||
mCachedLayerRenderStates = renderSnapshot.states;
|
||||
frameState.layerStates = renderSnapshot.states;
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot renderSnapshot;
|
||||
if (mRuntimeSnapshotProvider.TryPublishRenderStateSnapshot(input.renderWidth, input.renderHeight, renderSnapshot))
|
||||
{
|
||||
mCachedLayerRenderStates = renderSnapshot.states;
|
||||
mCachedRenderStateVersion = renderSnapshot.versions.renderStateVersion;
|
||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||
mCachedRenderStateWidth = renderSnapshot.outputWidth;
|
||||
mCachedRenderStateHeight = renderSnapshot.outputHeight;
|
||||
mCachedLayerRenderStates = ComposeLayerStates(mCachedLayerRenderStates, liveState, true, input.oscSmoothing, commitRequests);
|
||||
frameState.layerStates = mCachedLayerRenderStates;
|
||||
return true;
|
||||
}
|
||||
|
||||
frameState.layerStates = ComposeLayerStates(mCachedLayerRenderStates, liveState, true, input.oscSmoothing, commitRequests);
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return !frameState.layerStates.empty();
|
||||
}
|
||||
|
||||
std::vector<RuntimeRenderState> RenderFrameStateResolver::ComposeLayerStates(
|
||||
const std::vector<RuntimeRenderState>& baseStates,
|
||||
RuntimeLiveState& liveState,
|
||||
bool allowCommit,
|
||||
double smoothing,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests) const
|
||||
{
|
||||
LayeredRenderStateInput input;
|
||||
input.committedLiveLayerStates = &baseStates;
|
||||
input.transientAutomationOverlay = &liveState;
|
||||
input.allowTransientAutomationCommits = allowCommit;
|
||||
input.collectTransientAutomationCommitRequests = commitRequests != nullptr;
|
||||
input.transientAutomationSmoothing = smoothing;
|
||||
input.transientAutomationCommitDelay = kOscOverlayCommitDelay;
|
||||
input.now = std::chrono::steady_clock::now();
|
||||
const RenderStateCompositionResult result = mRenderStateComposer.BuildFrameState(input);
|
||||
|
||||
if (commitRequests)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitRequest& request : result.commitRequests)
|
||||
commitRequests->push_back(request);
|
||||
}
|
||||
return result.layerStates;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderFrameState.h"
|
||||
#include "RenderStateComposer.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class RenderFrameStateResolver
|
||||
{
|
||||
public:
|
||||
explicit RenderFrameStateResolver(RuntimeSnapshotProvider& runtimeSnapshotProvider);
|
||||
|
||||
void StoreCommittedSnapshot(
|
||||
const RuntimeRenderStateSnapshot& snapshot,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates);
|
||||
bool Resolve(
|
||||
const RenderFrameInput& input,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates,
|
||||
RuntimeLiveState& liveState,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState);
|
||||
|
||||
private:
|
||||
std::vector<RuntimeRenderState> ComposeLayerStates(
|
||||
const std::vector<RuntimeRenderState>& baseStates,
|
||||
RuntimeLiveState& liveState,
|
||||
bool allowCommit,
|
||||
double smoothing,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests) const;
|
||||
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
RenderStateComposer mRenderStateComposer;
|
||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||
uint64_t mCachedRenderStateVersion = 0;
|
||||
uint64_t mCachedParameterStateVersion = 0;
|
||||
unsigned mCachedRenderStateWidth = 0;
|
||||
unsigned mCachedRenderStateHeight = 0;
|
||||
};
|
||||
@@ -1,378 +0,0 @@
|
||||
#include "RuntimeUpdateController.h"
|
||||
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
#include "VideoBackend.h"
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace
|
||||
{
|
||||
RuntimeCoordinatorRenderResetScope ToRuntimeCoordinatorRenderResetScope(RuntimeEventRenderResetScope scope)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case RuntimeEventRenderResetScope::TemporalHistoryOnly:
|
||||
return RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly;
|
||||
case RuntimeEventRenderResetScope::TemporalHistoryAndFeedback:
|
||||
return RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
|
||||
case RuntimeEventRenderResetScope::None:
|
||||
default:
|
||||
return RuntimeCoordinatorRenderResetScope::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeUpdateController::RuntimeUpdateController(
|
||||
RuntimeStore& runtimeStore,
|
||||
RuntimeCoordinator& runtimeCoordinator,
|
||||
RuntimeEventDispatcher& runtimeEventDispatcher,
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
ShaderBuildQueue& shaderBuildQueue,
|
||||
VideoBackend& videoBackend) :
|
||||
mRuntimeStore(runtimeStore),
|
||||
mRuntimeCoordinator(runtimeCoordinator),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
mRuntimeServices(runtimeServices),
|
||||
mRenderEngine(renderEngine),
|
||||
mShaderBuildQueue(shaderBuildQueue),
|
||||
mVideoBackend(videoBackend)
|
||||
{
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RuntimeStateBroadcastRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRuntimeStateBroadcastRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RuntimeReloadRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRuntimeReloadRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RuntimePersistenceRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRuntimePersistenceRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildRequested,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildPrepared,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildPrepared(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildFailed,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildFailed(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::CompileStatusChanged,
|
||||
[this](const RuntimeEvent& event) { HandleCompileStatusChanged(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RenderResetRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRenderResetRequested(event); });
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error)
|
||||
{
|
||||
if (!result.accepted)
|
||||
{
|
||||
if (error)
|
||||
*error = result.errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.compileStatusChanged)
|
||||
{
|
||||
mRuntimeStore.SetCompileStatus(result.compileStatusSucceeded, result.compileStatusMessage);
|
||||
++mPendingCoordinatorCompileStatusEvents;
|
||||
}
|
||||
|
||||
if (result.clearReloadRequest)
|
||||
mRuntimeStore.ClearReloadRequest();
|
||||
|
||||
mRuntimeCoordinator.ApplyCommittedStateMode(result.committedStateMode);
|
||||
|
||||
switch (result.transientOscInvalidation)
|
||||
{
|
||||
case RuntimeCoordinatorTransientOscInvalidation::All:
|
||||
mRenderEngine.ClearOscOverlayState();
|
||||
mRuntimeServices.ClearOscState();
|
||||
break;
|
||||
case RuntimeCoordinatorTransientOscInvalidation::Layer:
|
||||
mRenderEngine.ClearOscOverlayStateForLayerKey(result.transientOscLayerKey);
|
||||
mRuntimeServices.ClearOscStateForLayerKey(result.transientOscLayerKey);
|
||||
break;
|
||||
case RuntimeCoordinatorTransientOscInvalidation::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mRenderEngine.ApplyRuntimeCoordinatorRenderReset(result.renderResetScope);
|
||||
if (result.renderResetScope != RuntimeCoordinatorRenderResetScope::None)
|
||||
++mPendingCoordinatorRenderResetEvents;
|
||||
|
||||
if (result.shaderBuildRequested)
|
||||
{
|
||||
RequestShaderBuild();
|
||||
++mPendingCoordinatorShaderBuildEvents;
|
||||
}
|
||||
|
||||
if (result.runtimeStateBroadcastRequired)
|
||||
BroadcastRuntimeState();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ProcessRuntimeWork()
|
||||
{
|
||||
DispatchRuntimeEvents();
|
||||
|
||||
return ConsumeReadyShaderBuild(0, true, true);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::RequestShaderBuild()
|
||||
{
|
||||
mShaderBuildQueue.RequestBuild(mVideoBackend.InputFrameWidth(), mVideoBackend.InputFrameHeight());
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::BroadcastRuntimeState()
|
||||
{
|
||||
RuntimeStateBroadcastRequestedEvent event;
|
||||
event.reason = "runtime-state-changed";
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeUpdateController"))
|
||||
{
|
||||
mRuntimeServices.BroadcastState();
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event)
|
||||
{
|
||||
if (event.source == "ControlServices")
|
||||
return;
|
||||
|
||||
mRuntimeServices.BroadcastState();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRuntimeReloadRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const RuntimeReloadRequestedEvent* payload = std::get_if<RuntimeReloadRequestedEvent>(&event.payload);
|
||||
if (!payload)
|
||||
return;
|
||||
|
||||
mRuntimeStore.ClearReloadRequest();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRuntimePersistenceRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const RuntimePersistenceRequestedEvent* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
|
||||
if (!payload)
|
||||
return;
|
||||
|
||||
std::string error;
|
||||
mRuntimeStore.RequestPersistence(payload->request, error);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Requested)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorShaderBuildEvents))
|
||||
return;
|
||||
|
||||
RequestShaderBuild();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildPrepared(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Prepared)
|
||||
return;
|
||||
|
||||
ConsumeReadyShaderBuild(payload->generation, false, true);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildFailed(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Failed)
|
||||
return;
|
||||
|
||||
ConsumeReadyShaderBuild(payload->generation, false, false);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleCompileStatusChanged(const RuntimeEvent& event)
|
||||
{
|
||||
const CompileStatusChangedEvent* payload = std::get_if<CompileStatusChangedEvent>(&event.payload);
|
||||
if (!payload)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorCompileStatusEvents))
|
||||
return;
|
||||
|
||||
mRuntimeStore.SetCompileStatus(payload->succeeded, payload->message);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRenderResetRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const RenderResetEvent* payload = std::get_if<RenderResetEvent>(&event.payload);
|
||||
if (!payload || payload->applied)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorRenderResetEvents))
|
||||
return;
|
||||
|
||||
mRenderEngine.ApplyRuntimeCoordinatorRenderReset(ToRuntimeCoordinatorRenderResetScope(payload->scope));
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ConsumeReadyShaderBuild(uint64_t expectedGeneration, bool publishPreparedEvent, bool publishFailureEvent)
|
||||
{
|
||||
PreparedShaderBuild readyBuild;
|
||||
const bool consumed = expectedGeneration == 0
|
||||
? mShaderBuildQueue.TryConsumeReadyBuild(readyBuild)
|
||||
: mShaderBuildQueue.TryConsumeReadyBuild(expectedGeneration, readyBuild);
|
||||
if (!consumed)
|
||||
return true;
|
||||
|
||||
const unsigned inputWidth = mVideoBackend.InputFrameWidth();
|
||||
const unsigned inputHeight = mVideoBackend.InputFrameHeight();
|
||||
if (!readyBuild.succeeded)
|
||||
{
|
||||
if (publishFailureEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Failed,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
false,
|
||||
readyBuild.message);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildFailure(readyBuild.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (publishPreparedEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Prepared,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
true,
|
||||
readyBuild.message);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
|
||||
char compilerErrorMessage[1024] = {};
|
||||
if (!mRenderEngine.ApplyPreparedShaderBuild(
|
||||
readyBuild,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
mRuntimeCoordinator.PreserveFeedbackOnNextShaderBuild(),
|
||||
sizeof(compilerErrorMessage),
|
||||
compilerErrorMessage))
|
||||
{
|
||||
const std::string errorMessage = compilerErrorMessage;
|
||||
if (publishFailureEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Failed,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
false,
|
||||
errorMessage);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildFailure(errorMessage));
|
||||
return false;
|
||||
}
|
||||
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Applied,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
true,
|
||||
"Shader layers applied successfully.");
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildSuccess());
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase phase,
|
||||
uint64_t generation,
|
||||
unsigned inputWidth,
|
||||
unsigned inputHeight,
|
||||
bool succeeded,
|
||||
const std::string& message)
|
||||
{
|
||||
ShaderBuildEvent event;
|
||||
event.phase = phase;
|
||||
event.generation = generation;
|
||||
event.inputWidth = inputWidth;
|
||||
event.inputHeight = inputHeight;
|
||||
event.preserveFeedbackState = mRuntimeCoordinator.PreserveFeedbackOnNextShaderBuild();
|
||||
event.succeeded = succeeded;
|
||||
event.message = message;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeUpdateController");
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ShouldSuppressCoordinatorFollowUp(const RuntimeEvent& event, std::size_t& pendingSuppressions)
|
||||
{
|
||||
if (event.source != "RuntimeCoordinator")
|
||||
return false;
|
||||
|
||||
if (pendingSuppressions > 0)
|
||||
--pendingSuppressions;
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeEventDispatchResult RuntimeUpdateController::DispatchRuntimeEvents(std::size_t maxEvents)
|
||||
{
|
||||
RuntimeEventDispatchResult result = mRuntimeEventDispatcher.DispatchPending(maxEvents);
|
||||
const RuntimeEventQueueMetrics queueMetrics = mRuntimeEventDispatcher.GetQueueMetrics();
|
||||
HealthTelemetry& telemetry = mRuntimeStore.GetHealthTelemetry();
|
||||
telemetry.TryRecordRuntimeEventDispatchStats(
|
||||
result.dispatchedEvents,
|
||||
result.handlerInvocations,
|
||||
result.handlerFailures,
|
||||
result.dispatchDurationMilliseconds);
|
||||
telemetry.TryRecordRuntimeEventQueueMetrics(
|
||||
"runtime-events",
|
||||
queueMetrics.depth,
|
||||
queueMetrics.capacity,
|
||||
static_cast<uint64_t>(queueMetrics.droppedCount),
|
||||
queueMetrics.oldestEventAgeMilliseconds);
|
||||
PublishRuntimeEventHealthObservations(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result)
|
||||
{
|
||||
const RuntimeEventQueueMetrics queueMetrics = mRuntimeEventDispatcher.GetQueueMetrics();
|
||||
if (queueMetrics.depth != mLastReportedRuntimeEventQueueDepth ||
|
||||
queueMetrics.droppedCount != mLastReportedRuntimeEventDroppedCount ||
|
||||
queueMetrics.coalescedCount != mLastReportedRuntimeEventCoalescedCount)
|
||||
{
|
||||
QueueDepthChangedEvent queueDepth;
|
||||
queueDepth.queueName = "runtime-events";
|
||||
queueDepth.depth = queueMetrics.depth;
|
||||
queueDepth.capacity = queueMetrics.capacity;
|
||||
queueDepth.droppedCount = queueMetrics.droppedCount;
|
||||
queueDepth.coalescedCount = queueMetrics.coalescedCount;
|
||||
mRuntimeEventDispatcher.PublishPayload(queueDepth, "HealthTelemetry");
|
||||
mLastReportedRuntimeEventQueueDepth = queueMetrics.depth;
|
||||
mLastReportedRuntimeEventDroppedCount = queueMetrics.droppedCount;
|
||||
mLastReportedRuntimeEventCoalescedCount = queueMetrics.coalescedCount;
|
||||
}
|
||||
|
||||
if (result.handlerInvocations == 0 && result.handlerFailures == 0)
|
||||
return;
|
||||
|
||||
TimingSampleRecordedEvent timing;
|
||||
timing.subsystem = "RuntimeEventDispatcher";
|
||||
timing.metric = "dispatchDuration";
|
||||
timing.value = result.dispatchDurationMilliseconds;
|
||||
timing.unit = "ms";
|
||||
mRuntimeEventDispatcher.PublishPayload(timing, "HealthTelemetry");
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeEventPayloads.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class RenderEngine;
|
||||
struct RuntimeEvent;
|
||||
struct RuntimeEventDispatchResult;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeServices;
|
||||
class RuntimeStore;
|
||||
class ShaderBuildQueue;
|
||||
class VideoBackend;
|
||||
|
||||
class RuntimeUpdateController
|
||||
{
|
||||
public:
|
||||
RuntimeUpdateController(
|
||||
RuntimeStore& runtimeStore,
|
||||
RuntimeCoordinator& runtimeCoordinator,
|
||||
RuntimeEventDispatcher& runtimeEventDispatcher,
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
ShaderBuildQueue& shaderBuildQueue,
|
||||
VideoBackend& videoBackend);
|
||||
|
||||
bool ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error = nullptr);
|
||||
bool ProcessRuntimeWork();
|
||||
void RequestShaderBuild();
|
||||
void BroadcastRuntimeState();
|
||||
|
||||
private:
|
||||
void HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event);
|
||||
void HandleRuntimeReloadRequested(const RuntimeEvent& event);
|
||||
void HandleRuntimePersistenceRequested(const RuntimeEvent& event);
|
||||
void HandleShaderBuildRequested(const RuntimeEvent& event);
|
||||
void HandleShaderBuildPrepared(const RuntimeEvent& event);
|
||||
void HandleShaderBuildFailed(const RuntimeEvent& event);
|
||||
void HandleCompileStatusChanged(const RuntimeEvent& event);
|
||||
void HandleRenderResetRequested(const RuntimeEvent& event);
|
||||
bool ConsumeReadyShaderBuild(uint64_t expectedGeneration, bool publishPreparedEvent, bool publishFailureEvent);
|
||||
void PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase phase,
|
||||
uint64_t generation,
|
||||
unsigned inputWidth,
|
||||
unsigned inputHeight,
|
||||
bool succeeded,
|
||||
const std::string& message);
|
||||
bool ShouldSuppressCoordinatorFollowUp(const RuntimeEvent& event, std::size_t& pendingSuppressions);
|
||||
RuntimeEventDispatchResult DispatchRuntimeEvents(std::size_t maxEvents = 0);
|
||||
void PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result);
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
RuntimeCoordinator& mRuntimeCoordinator;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
RuntimeServices& mRuntimeServices;
|
||||
RenderEngine& mRenderEngine;
|
||||
ShaderBuildQueue& mShaderBuildQueue;
|
||||
VideoBackend& mVideoBackend;
|
||||
std::size_t mPendingCoordinatorShaderBuildEvents = 0;
|
||||
std::size_t mPendingCoordinatorCompileStatusEvents = 0;
|
||||
std::size_t mPendingCoordinatorRenderResetEvents = 0;
|
||||
std::size_t mLastReportedRuntimeEventQueueDepth = static_cast<std::size_t>(-1);
|
||||
std::size_t mLastReportedRuntimeEventDroppedCount = static_cast<std::size_t>(-1);
|
||||
std::size_t mLastReportedRuntimeEventCoalescedCount = static_cast<std::size_t>(-1);
|
||||
};
|
||||
@@ -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.AdvanceFrame();
|
||||
|
||||
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.QueueInputFrame(inputFrame, state);
|
||||
}
|
||||
|
||||
bool OpenGLVideoIOBridge::RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
RenderPipelineFrameContext frameContext;
|
||||
frameContext.videoState = state;
|
||||
frameContext.completion = completion;
|
||||
|
||||
return mRenderEngine.RequestOutputFrame(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);
|
||||
bool 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.PublishRenderStateSnapshot(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,166 +0,0 @@
|
||||
#include "ShaderBuildQueue.h"
|
||||
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
|
||||
}
|
||||
|
||||
ShaderBuildQueue::ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
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;
|
||||
}
|
||||
|
||||
bool ShaderBuildQueue::TryConsumeReadyBuild(uint64_t expectedGeneration, PreparedShaderBuild& build)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasReadyBuild || mReadyBuild.generation != expectedGeneration)
|
||||
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);
|
||||
|
||||
bool shouldPublish = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mStopping)
|
||||
return;
|
||||
if (generation != mRequestedGeneration)
|
||||
continue;
|
||||
mReadyBuild = build;
|
||||
mHasReadyBuild = true;
|
||||
shouldPublish = true;
|
||||
}
|
||||
|
||||
if (shouldPublish)
|
||||
PublishBuildLifecycleEvent(build, outputWidth, outputHeight);
|
||||
}
|
||||
}
|
||||
|
||||
PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight)
|
||||
{
|
||||
PreparedShaderBuild build;
|
||||
build.generation = generation;
|
||||
build.renderSnapshot = mRuntimeSnapshotProvider.PublishRenderStateSnapshot(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;
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::PublishBuildLifecycleEvent(const PreparedShaderBuild& build, unsigned outputWidth, unsigned outputHeight) const
|
||||
{
|
||||
ShaderBuildEvent event;
|
||||
event.phase = build.succeeded ? RuntimeEventShaderBuildPhase::Prepared : RuntimeEventShaderBuildPhase::Failed;
|
||||
event.generation = build.generation;
|
||||
event.inputWidth = outputWidth;
|
||||
event.inputHeight = outputHeight;
|
||||
event.succeeded = build.succeeded;
|
||||
event.message = build.message;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "ShaderBuildQueue");
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
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:
|
||||
ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~ShaderBuildQueue();
|
||||
|
||||
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
|
||||
ShaderBuildQueue& operator=(const ShaderBuildQueue&) = delete;
|
||||
|
||||
void RequestBuild(unsigned outputWidth, unsigned outputHeight);
|
||||
bool TryConsumeReadyBuild(PreparedShaderBuild& build);
|
||||
bool TryConsumeReadyBuild(uint64_t expectedGeneration, PreparedShaderBuild& build);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void WorkerLoop();
|
||||
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
|
||||
void PublishBuildLifecycleEvent(const PreparedShaderBuild& build, unsigned outputWidth, unsigned outputHeight) const;
|
||||
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
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,160 +0,0 @@
|
||||
#include "RenderCommandQueue.h"
|
||||
|
||||
void RenderCommandQueue::RequestPreviewPresent(const RenderPreviewPresentRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasPreviewPresentRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mPreviewPresentRequest = request;
|
||||
mHasPreviewPresentRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakePreviewPresent(RenderPreviewPresentRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasPreviewPresentRequest)
|
||||
return false;
|
||||
|
||||
request = mPreviewPresentRequest;
|
||||
mPreviewPresentRequest = {};
|
||||
mHasPreviewPresentRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasScreenshotCaptureRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mScreenshotCaptureRequest = request;
|
||||
mHasScreenshotCaptureRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasScreenshotCaptureRequest)
|
||||
return false;
|
||||
|
||||
request = mScreenshotCaptureRequest;
|
||||
mScreenshotCaptureRequest = {};
|
||||
mHasScreenshotCaptureRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestInputUpload(const RenderInputUploadRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasInputUploadRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mInputUploadRequest = request;
|
||||
mHasInputUploadRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeInputUpload(RenderInputUploadRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasInputUploadRequest)
|
||||
return false;
|
||||
|
||||
request = mInputUploadRequest;
|
||||
mInputUploadRequest = {};
|
||||
mHasInputUploadRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestOutputFrame(const RenderOutputFrameRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mOutputFrameRequests.push_back(request);
|
||||
++mEnqueuedCount;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeOutputFrame(RenderOutputFrameRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mOutputFrameRequests.empty())
|
||||
return false;
|
||||
|
||||
request = mOutputFrameRequests.front();
|
||||
mOutputFrameRequests.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestRenderReset(RenderCommandResetScope scope)
|
||||
{
|
||||
if (scope == RenderCommandResetScope::None)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mRenderResetScope != RenderCommandResetScope::None)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mRenderResetScope = MergeResetScopes(mRenderResetScope, scope);
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeRenderReset(RenderCommandResetScope& scope)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mRenderResetScope == RenderCommandResetScope::None)
|
||||
return false;
|
||||
|
||||
scope = mRenderResetScope;
|
||||
mRenderResetScope = RenderCommandResetScope::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
RenderCommandQueueMetrics RenderCommandQueue::GetMetrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
RenderCommandQueueMetrics metrics;
|
||||
metrics.depth =
|
||||
(mHasPreviewPresentRequest ? 1u : 0u) +
|
||||
(mHasScreenshotCaptureRequest ? 1u : 0u) +
|
||||
(mHasInputUploadRequest ? 1u : 0u) +
|
||||
mOutputFrameRequests.size() +
|
||||
(mRenderResetScope != RenderCommandResetScope::None ? 1u : 0u);
|
||||
metrics.enqueuedCount = mEnqueuedCount;
|
||||
metrics.coalescedCount = mCoalescedCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
RenderCommandResetScope RenderCommandQueue::MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested)
|
||||
{
|
||||
if (current == RenderCommandResetScope::TemporalHistoryAndFeedback ||
|
||||
requested == RenderCommandResetScope::TemporalHistoryAndFeedback)
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryAndFeedback;
|
||||
}
|
||||
|
||||
if ((current == RenderCommandResetScope::TemporalHistoryOnly && requested == RenderCommandResetScope::ShaderFeedbackOnly) ||
|
||||
(current == RenderCommandResetScope::ShaderFeedbackOnly && requested == RenderCommandResetScope::TemporalHistoryOnly))
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryAndFeedback;
|
||||
}
|
||||
|
||||
if (current == RenderCommandResetScope::TemporalHistoryOnly ||
|
||||
requested == RenderCommandResetScope::TemporalHistoryOnly)
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryOnly;
|
||||
}
|
||||
|
||||
if (current == RenderCommandResetScope::ShaderFeedbackOnly ||
|
||||
requested == RenderCommandResetScope::ShaderFeedbackOnly)
|
||||
{
|
||||
return RenderCommandResetScope::ShaderFeedbackOnly;
|
||||
}
|
||||
|
||||
return RenderCommandResetScope::None;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
enum class RenderCommandResetScope
|
||||
{
|
||||
None,
|
||||
ShaderFeedbackOnly,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
struct RenderPreviewPresentRequest
|
||||
{
|
||||
unsigned outputFrameWidth = 0;
|
||||
unsigned outputFrameHeight = 0;
|
||||
};
|
||||
|
||||
struct RenderScreenshotCaptureRequest
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
};
|
||||
|
||||
struct RenderInputUploadRequest
|
||||
{
|
||||
VideoIOFrame inputFrame;
|
||||
VideoIOState videoState;
|
||||
std::vector<unsigned char> ownedBytes;
|
||||
};
|
||||
|
||||
struct RenderOutputFrameRequest
|
||||
{
|
||||
VideoIOState videoState;
|
||||
VideoIOCompletion completion;
|
||||
};
|
||||
|
||||
struct RenderCommandQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
uint64_t enqueuedCount = 0;
|
||||
uint64_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
class RenderCommandQueue
|
||||
{
|
||||
public:
|
||||
void RequestPreviewPresent(const RenderPreviewPresentRequest& request);
|
||||
bool TryTakePreviewPresent(RenderPreviewPresentRequest& request);
|
||||
|
||||
void RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request);
|
||||
bool TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request);
|
||||
|
||||
void RequestInputUpload(const RenderInputUploadRequest& request);
|
||||
bool TryTakeInputUpload(RenderInputUploadRequest& request);
|
||||
|
||||
void RequestOutputFrame(const RenderOutputFrameRequest& request);
|
||||
bool TryTakeOutputFrame(RenderOutputFrameRequest& request);
|
||||
|
||||
void RequestRenderReset(RenderCommandResetScope scope);
|
||||
bool TryTakeRenderReset(RenderCommandResetScope& scope);
|
||||
|
||||
RenderCommandQueueMetrics GetMetrics() const;
|
||||
|
||||
private:
|
||||
static RenderCommandResetScope MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested);
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
bool mHasPreviewPresentRequest = false;
|
||||
RenderPreviewPresentRequest mPreviewPresentRequest;
|
||||
bool mHasScreenshotCaptureRequest = false;
|
||||
RenderScreenshotCaptureRequest mScreenshotCaptureRequest;
|
||||
bool mHasInputUploadRequest = false;
|
||||
RenderInputUploadRequest mInputUploadRequest;
|
||||
std::deque<RenderOutputFrameRequest> mOutputFrameRequests;
|
||||
RenderCommandResetScope mRenderResetScope = RenderCommandResetScope::None;
|
||||
uint64_t mEnqueuedCount = 0;
|
||||
uint64_t mCoalescedCount = 0;
|
||||
};
|
||||
@@ -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,613 +0,0 @@
|
||||
#include "RuntimeCoordinator.h"
|
||||
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeEventPayloads.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
RuntimeEventRenderResetScope ToRuntimeEventRenderResetScope(RuntimeCoordinatorRenderResetScope scope)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
|
||||
return RuntimeEventRenderResetScope::TemporalHistoryOnly;
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
|
||||
return RuntimeEventRenderResetScope::TemporalHistoryAndFeedback;
|
||||
case RuntimeCoordinatorRenderResetScope::None:
|
||||
default:
|
||||
return RuntimeEventRenderResetScope::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRuntimeStore(runtimeStore),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::AddLayer(const std::string& shaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateShaderExists(shaderId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("AddLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true, true);
|
||||
PublishCoordinatorResult("AddLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& layerId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("RemoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true);
|
||||
if (result.accepted)
|
||||
{
|
||||
result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
|
||||
result.transientOscLayerKey = layerId;
|
||||
}
|
||||
PublishCoordinatorResult("RemoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerId, int direction)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
bool shouldMove = false;
|
||||
if (!ResolveLayerMove(layerId, direction, shouldMove, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("MoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
if (!shouldMove)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true, true);
|
||||
PublishCoordinatorResult("MoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
bool shouldMove = false;
|
||||
if (!ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("MoveLayerToIndex", result);
|
||||
return result;
|
||||
}
|
||||
if (!shouldMove)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true, true);
|
||||
PublishCoordinatorResult("MoveLayerToIndex", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerBypass(const std::string& layerId, bool bypassed)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SetLayerBypass", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false, true);
|
||||
PublishCoordinatorResult("SetLayerBypass", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerShader(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error) || !ValidateShaderExists(shaderId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SetLayerShader", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false, true);
|
||||
PublishCoordinatorResult("SetLayerShader", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationById(layerId, parameterId, newValue, true, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("UpdateLayerParameter", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("UpdateLayerParameter", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("UpdateLayerParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("UpdateLayerParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
constexpr RuntimeCoordinatorOscCommitPersistence kDefaultOscCommitPersistence =
|
||||
RuntimeCoordinatorOscCommitPersistence::SessionOnly;
|
||||
constexpr bool kPersistSettledOscCommits =
|
||||
kDefaultOscCommitPersistence == RuntimeCoordinatorOscCommitPersistence::Persistent;
|
||||
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, kPersistSettledOscCommits, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::string& layerId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false, true);
|
||||
if (!result.accepted)
|
||||
{
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
|
||||
result.transientOscLayerKey = layerId;
|
||||
result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SaveStackPreset(const std::string& presetName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidatePresetName(presetName, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SaveStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false, true);
|
||||
PublishCoordinatorResult("SaveStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::LoadStackPreset(const std::string& presetName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidatePresetName(presetName, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("LoadStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false, true);
|
||||
PublishCoordinatorResult("LoadStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::RequestShaderReload(bool preserveFeedbackState)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
PublishManualReloadRequested(preserveFeedbackState, "RequestShaderReload");
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(preserveFeedbackState);
|
||||
PublishCoordinatorFollowUpEvents("RequestShaderReload", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::PollRuntimeStoreChanges(bool& registryChanged)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
registryChanged = false;
|
||||
bool reloadRequested = false;
|
||||
std::string error;
|
||||
if (!mRuntimeStore.PollStoredFileChanges(registryChanged, reloadRequested, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = HandleRuntimePollFailure(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (reloadRequested)
|
||||
{
|
||||
PublishFileChangeDetected("PollRuntimeStoreChanges", registryChanged, reloadRequested);
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(false);
|
||||
PublishCoordinatorFollowUpEvents("PollRuntimeStoreChanges", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (registryChanged)
|
||||
{
|
||||
PublishFileChangeDetected("PollRuntimeStoreChanges", registryChanged, reloadRequested);
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
PublishCoordinatorFollowUpEvents("PollRuntimeStoreChanges", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimePollFailure(const std::string& error)
|
||||
{
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
result.runtimeStateBroadcastRequired = true;
|
||||
result.compileStatusChanged = true;
|
||||
result.compileStatusSucceeded = false;
|
||||
result.compileStatusMessage = error;
|
||||
PublishCoordinatorFollowUpEvents("HandleRuntimePollFailure", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildFailure(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mPreserveFeedbackOnNextShaderBuild = false;
|
||||
mUseCommittedLayerStates = true;
|
||||
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
result.runtimeStateBroadcastRequired = true;
|
||||
result.compileStatusChanged = true;
|
||||
result.compileStatusSucceeded = false;
|
||||
result.compileStatusMessage = error;
|
||||
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
|
||||
PublishCoordinatorFollowUpEvents("HandlePreparedShaderBuildFailure", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildSuccess()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mUseCommittedLayerStates = false;
|
||||
|
||||
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;
|
||||
PublishCoordinatorFollowUpEvents("HandlePreparedShaderBuildSuccess", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimeReloadRequest()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
PublishManualReloadRequested(false, "HandleRuntimeReloadRequest");
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(false);
|
||||
PublishCoordinatorFollowUpEvents("HandleRuntimeReloadRequest", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::ApplyCommittedStateMode(RuntimeCoordinatorCommittedStateMode mode)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
switch (mode)
|
||||
{
|
||||
case RuntimeCoordinatorCommittedStateMode::UseCommittedStates:
|
||||
mUseCommittedLayerStates = true;
|
||||
break;
|
||||
case RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots:
|
||||
mUseCommittedLayerStates = false;
|
||||
break;
|
||||
case RuntimeCoordinatorCommittedStateMode::Unchanged:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::UseCommittedLayerStates() const
|
||||
{
|
||||
return mUseCommittedLayerStates.load();
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::PreserveFeedbackOnNextShaderBuild() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mPreserveFeedbackOnNextShaderBuild;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationById(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
RuntimeStore::StoredParameterSnapshot snapshot;
|
||||
if (!mRuntimeStore.TryGetStoredParameterById(layerId, parameterId, snapshot, error))
|
||||
return false;
|
||||
|
||||
return BuildParameterMutationFromSnapshot(snapshot.layerId, snapshot.definition, snapshot.currentValue, snapshot.hasCurrentValue,
|
||||
newValue, persistState, mutation, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
RuntimeStore::StoredParameterSnapshot snapshot;
|
||||
if (!mRuntimeStore.TryGetStoredParameterByControlKey(layerKey, parameterKey, snapshot, error))
|
||||
return false;
|
||||
|
||||
return BuildParameterMutationFromSnapshot(snapshot.layerId, snapshot.definition, snapshot.currentValue, snapshot.hasCurrentValue,
|
||||
newValue, persistState, mutation, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationFromSnapshot(const std::string& layerId, const ShaderParameterDefinition& definition,
|
||||
const ShaderParameterValue& currentValue, bool hasCurrentValue, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
mutation.layerId = layerId;
|
||||
mutation.parameterId = definition.id;
|
||||
mutation.persistState = persistState;
|
||||
|
||||
if (definition.type == ShaderParameterType::Trigger)
|
||||
{
|
||||
const double previousCount = !hasCurrentValue || currentValue.numberValues.empty()
|
||||
? 0.0
|
||||
: currentValue.numberValues[0];
|
||||
const double triggerTime = mRuntimeStore.GetRuntimeElapsedSeconds();
|
||||
mutation.value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
mutation.persistState = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return NormalizeAndValidateParameterValue(definition, newValue, mutation.value, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidateLayerExists(const std::string& layerId, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.HasStoredLayer(layerId))
|
||||
return true;
|
||||
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidateShaderExists(const std::string& shaderId, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.HasStoredShader(shaderId))
|
||||
return true;
|
||||
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mRuntimeStore.ResolveStoredLayerMove(layerId, direction, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mRuntimeStore.ResolveStoredLayerMoveToIndex(layerId, targetIndex, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidatePresetName(const std::string& presetName, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.IsValidStackPresetName(presetName))
|
||||
return true;
|
||||
|
||||
error = "Preset name must include at least one letter or number.";
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested)
|
||||
{
|
||||
if (!succeeded)
|
||||
{
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = false;
|
||||
result.errorMessage = errorMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (reloadRequired)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(preserveFeedbackState);
|
||||
result.persistenceRequested = persistenceRequested;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
result.persistenceRequested = persistenceRequested;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::BuildQueuedReloadResult(bool preserveFeedbackState)
|
||||
{
|
||||
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
||||
mUseCommittedLayerStates = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishFileChangeDetected(const std::string& reason, bool registryChanged, bool reloadRequested) const
|
||||
{
|
||||
try
|
||||
{
|
||||
FileChangeDetectedEvent event;
|
||||
event.path = reason;
|
||||
event.shaderPackageCandidate = registryChanged || reloadRequested;
|
||||
event.runtimeConfigCandidate = false;
|
||||
event.presetCandidate = false;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeCoordinator");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishManualReloadRequested(bool preserveFeedbackState, const std::string& reason) const
|
||||
{
|
||||
try
|
||||
{
|
||||
ManualReloadRequestedEvent event;
|
||||
event.preserveFeedbackState = preserveFeedbackState;
|
||||
event.reason = reason;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeCoordinator");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishCoordinatorResult(const std::string& action, const RuntimeCoordinatorResult& result) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeMutationEvent mutation;
|
||||
mutation.action = action;
|
||||
mutation.accepted = result.accepted;
|
||||
mutation.runtimeStateChanged = result.accepted && result.runtimeStateBroadcastRequired;
|
||||
mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired;
|
||||
mutation.shaderBuildRequested = result.shaderBuildRequested;
|
||||
mutation.persistenceRequested = result.persistenceRequested;
|
||||
mutation.clearTransientOscState = result.transientOscInvalidation != RuntimeCoordinatorTransientOscInvalidation::None;
|
||||
mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope);
|
||||
mutation.errorMessage = result.errorMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(mutation, "RuntimeCoordinator");
|
||||
|
||||
PublishCoordinatorFollowUpEvents(action, result);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishCoordinatorFollowUpEvents(const std::string& action, const RuntimeCoordinatorResult& result) const
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!result.accepted)
|
||||
return;
|
||||
|
||||
if (result.runtimeStateBroadcastRequired)
|
||||
{
|
||||
RuntimeStateChangedEvent stateChanged;
|
||||
stateChanged.reason = action;
|
||||
stateChanged.renderVisible = result.renderResetScope != RuntimeCoordinatorRenderResetScope::None;
|
||||
stateChanged.persistenceRequested = result.persistenceRequested;
|
||||
mRuntimeEventDispatcher.PublishPayload(stateChanged, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.persistenceRequested)
|
||||
{
|
||||
RuntimePersistenceRequestedEvent persistenceRequested;
|
||||
persistenceRequested.request = PersistenceRequest::RuntimeStateRequest(action);
|
||||
mRuntimeEventDispatcher.PublishPayload(persistenceRequested, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.shaderBuildRequested)
|
||||
{
|
||||
RuntimeReloadRequestedEvent reloadRequested;
|
||||
reloadRequested.preserveFeedbackState = mPreserveFeedbackOnNextShaderBuild;
|
||||
reloadRequested.reason = action;
|
||||
mRuntimeEventDispatcher.PublishPayload(reloadRequested, "RuntimeCoordinator");
|
||||
|
||||
ShaderBuildEvent shaderBuild;
|
||||
shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
|
||||
shaderBuild.preserveFeedbackState = mPreserveFeedbackOnNextShaderBuild;
|
||||
shaderBuild.succeeded = true;
|
||||
shaderBuild.message = result.compileStatusMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(shaderBuild, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.compileStatusChanged)
|
||||
{
|
||||
CompileStatusChangedEvent compileStatus;
|
||||
compileStatus.succeeded = result.compileStatusSucceeded;
|
||||
compileStatus.message = result.compileStatusMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(compileStatus, "RuntimeCoordinator");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
class RuntimeStore;
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
enum class RuntimeCoordinatorCommittedStateMode
|
||||
{
|
||||
Unchanged,
|
||||
UseCommittedStates,
|
||||
UseLiveSnapshots
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorRenderResetScope
|
||||
{
|
||||
None,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorTransientOscInvalidation
|
||||
{
|
||||
None,
|
||||
Layer,
|
||||
All
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorOscCommitPersistence
|
||||
{
|
||||
SessionOnly,
|
||||
Persistent
|
||||
};
|
||||
|
||||
struct RuntimeCoordinatorResult
|
||||
{
|
||||
bool accepted = false;
|
||||
bool runtimeStateBroadcastRequired = false;
|
||||
bool shaderBuildRequested = false;
|
||||
bool persistenceRequested = false;
|
||||
bool compileStatusChanged = false;
|
||||
bool compileStatusSucceeded = false;
|
||||
bool clearReloadRequest = false;
|
||||
RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged;
|
||||
RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None;
|
||||
RuntimeCoordinatorTransientOscInvalidation transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::None;
|
||||
std::string transientOscLayerKey;
|
||||
std::string compileStatusMessage;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
class RuntimeCoordinator
|
||||
{
|
||||
public:
|
||||
RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
|
||||
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 CommitOscParameterByControlKey(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 PollRuntimeStoreChanges(bool& registryChanged);
|
||||
RuntimeCoordinatorResult HandleRuntimePollFailure(const std::string& error);
|
||||
RuntimeCoordinatorResult HandlePreparedShaderBuildFailure(const std::string& error);
|
||||
RuntimeCoordinatorResult HandlePreparedShaderBuildSuccess();
|
||||
RuntimeCoordinatorResult HandleRuntimeReloadRequest();
|
||||
void ApplyCommittedStateMode(RuntimeCoordinatorCommittedStateMode mode);
|
||||
bool UseCommittedLayerStates() const;
|
||||
bool PreserveFeedbackOnNextShaderBuild() const;
|
||||
|
||||
private:
|
||||
struct ResolvedParameterMutation
|
||||
{
|
||||
std::string layerId;
|
||||
std::string parameterId;
|
||||
ShaderParameterValue value;
|
||||
bool persistState = true;
|
||||
};
|
||||
|
||||
bool BuildParameterMutationById(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool BuildParameterMutationByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool BuildParameterMutationFromSnapshot(const std::string& layerId, const ShaderParameterDefinition& definition,
|
||||
const ShaderParameterValue& currentValue, bool hasCurrentValue, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool ValidateLayerExists(const std::string& layerId, std::string& error) const;
|
||||
bool ValidateShaderExists(const std::string& shaderId, std::string& error) const;
|
||||
bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
bool ValidatePresetName(const std::string& presetName, std::string& error) const;
|
||||
RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested);
|
||||
RuntimeCoordinatorResult BuildQueuedReloadResult(bool preserveFeedbackState);
|
||||
RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const;
|
||||
void PublishFileChangeDetected(const std::string& reason, bool registryChanged, bool reloadRequested) const;
|
||||
void PublishManualReloadRequested(bool preserveFeedbackState, const std::string& reason) const;
|
||||
void PublishCoordinatorResult(const std::string& action, const RuntimeCoordinatorResult& result) const;
|
||||
void PublishCoordinatorFollowUpEvents(const std::string& action, const RuntimeCoordinatorResult& result) const;
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
mutable std::mutex mMutex;
|
||||
bool mPreserveFeedbackOnNextShaderBuild = false;
|
||||
std::atomic<bool> mUseCommittedLayerStates{ false };
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventPayloads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
using RuntimeEventPayload = std::variant<
|
||||
std::monostate,
|
||||
OscValueReceivedEvent,
|
||||
OscValueCoalescedEvent,
|
||||
OscCommitRequestedEvent,
|
||||
HttpControlMutationRequestedEvent,
|
||||
WebSocketClientConnectedEvent,
|
||||
RuntimeStateBroadcastRequestedEvent,
|
||||
FileChangeDetectedEvent,
|
||||
ManualReloadRequestedEvent,
|
||||
RuntimeMutationEvent,
|
||||
RuntimeStateChangedEvent,
|
||||
RuntimePersistenceRequestedEvent,
|
||||
RuntimeReloadRequestedEvent,
|
||||
ShaderPackagesChangedEvent,
|
||||
RenderSnapshotPublishRequestedEvent,
|
||||
RuntimeStatePresentationChangedEvent,
|
||||
ShaderBuildEvent,
|
||||
CompileStatusChangedEvent,
|
||||
RenderSnapshotPublishedEvent,
|
||||
RenderResetEvent,
|
||||
OscOverlayEvent,
|
||||
FrameRenderedEvent,
|
||||
PreviewFrameAvailableEvent,
|
||||
InputSignalChangedEvent,
|
||||
InputFrameArrivedEvent,
|
||||
OutputFrameScheduledEvent,
|
||||
OutputFrameCompletedEvent,
|
||||
BackendStateChangedEvent,
|
||||
SubsystemWarningEvent,
|
||||
SubsystemRecoveredEvent,
|
||||
TimingSampleRecordedEvent,
|
||||
QueueDepthChangedEvent>;
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeEventPayload& payload)
|
||||
{
|
||||
return std::visit([](const auto& value) -> RuntimeEventType {
|
||||
using PayloadType = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<PayloadType, std::monostate>)
|
||||
return RuntimeEventType::Unknown;
|
||||
else
|
||||
return RuntimeEventPayloadType(value);
|
||||
}, payload);
|
||||
}
|
||||
|
||||
struct RuntimeEvent
|
||||
{
|
||||
RuntimeEventType type = RuntimeEventType::Unknown;
|
||||
uint64_t sequence = 0;
|
||||
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now();
|
||||
std::string source;
|
||||
RuntimeEventPayload payload;
|
||||
|
||||
bool HasPayload() const
|
||||
{
|
||||
return !std::holds_alternative<std::monostate>(payload);
|
||||
}
|
||||
|
||||
bool PayloadMatchesType() const
|
||||
{
|
||||
return RuntimeEventPayloadType(payload) == type;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Payload>
|
||||
RuntimeEvent MakeRuntimeEvent(Payload payload, std::string source = {}, uint64_t sequence = 0,
|
||||
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now())
|
||||
{
|
||||
RuntimeEvent event;
|
||||
event.type = RuntimeEventPayloadType(payload);
|
||||
event.sequence = sequence;
|
||||
event.createdAt = createdAt;
|
||||
event.source = std::move(source);
|
||||
event.payload = std::move(payload);
|
||||
return event;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEvent.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventCoalescingQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
double oldestEventAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
inline std::string RuntimeEventDefaultCoalescingKey(const RuntimeEvent& event)
|
||||
{
|
||||
if (const auto* payload = std::get_if<OscValueReceivedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
|
||||
if (const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
|
||||
if (const auto* payload = std::get_if<FileChangeDetectedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->path;
|
||||
if (const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" +
|
||||
std::to_string(payload->inputWidth) + "x" +
|
||||
std::to_string(payload->inputHeight) + ":" +
|
||||
(payload->preserveFeedbackState ? "preserve" : "reset");
|
||||
if (const auto* payload = std::get_if<RenderSnapshotPublishRequestedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" +
|
||||
std::to_string(payload->outputWidth) + "x" +
|
||||
std::to_string(payload->outputHeight);
|
||||
if (const auto* payload = std::get_if<TimingSampleRecordedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->subsystem + ":" + payload->metric;
|
||||
if (const auto* payload = std::get_if<QueueDepthChangedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->queueName;
|
||||
|
||||
return std::string(RuntimeEventTypeName(event.type));
|
||||
}
|
||||
|
||||
class RuntimeEventCoalescingQueue
|
||||
{
|
||||
public:
|
||||
using KeySelector = std::function<std::string(const RuntimeEvent&)>;
|
||||
|
||||
explicit RuntimeEventCoalescingQueue(std::size_t capacity = 256, KeySelector keySelector = RuntimeEventDefaultCoalescingKey) :
|
||||
mCapacity(capacity),
|
||||
mKeySelector(std::move(keySelector))
|
||||
{
|
||||
}
|
||||
|
||||
bool Push(RuntimeEvent event)
|
||||
{
|
||||
const std::string key = mKeySelector(event);
|
||||
if (key.empty())
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
auto found = mEntries.find(key);
|
||||
if (found != mEntries.end())
|
||||
{
|
||||
const auto firstCreatedAt = found->second.event.createdAt;
|
||||
found->second.event = std::move(event);
|
||||
found->second.event.createdAt = firstCreatedAt;
|
||||
++found->second.coalescedCount;
|
||||
++mCoalescedCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mEntries.size() >= mCapacity)
|
||||
{
|
||||
++mDroppedCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
mOrder.push_back(key);
|
||||
Entry entry;
|
||||
entry.event = std::move(event);
|
||||
mEntries.emplace(key, std::move(entry));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
|
||||
{
|
||||
std::vector<RuntimeEvent> events;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t count = maxEvents == 0 || maxEvents > mOrder.size() ? mOrder.size() : maxEvents;
|
||||
events.reserve(count);
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::string key = std::move(mOrder.front());
|
||||
mOrder.pop_front();
|
||||
|
||||
auto found = mEntries.find(key);
|
||||
if (found == mEntries.end())
|
||||
continue;
|
||||
|
||||
events.push_back(std::move(found->second.event));
|
||||
mEntries.erase(found);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
RuntimeEventCoalescingQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
RuntimeEventCoalescingQueueMetrics metrics;
|
||||
metrics.depth = mEntries.size();
|
||||
metrics.capacity = mCapacity;
|
||||
metrics.droppedCount = mDroppedCount;
|
||||
metrics.coalescedCount = mCoalescedCount;
|
||||
|
||||
if (!mOrder.empty())
|
||||
{
|
||||
const auto found = mEntries.find(mOrder.front());
|
||||
if (found != mEntries.end())
|
||||
{
|
||||
const auto age = now - found->second.event.createdAt;
|
||||
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t Depth() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mEntries.size();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
RuntimeEvent event;
|
||||
std::size_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
std::size_t mCapacity = 0;
|
||||
KeySelector mKeySelector;
|
||||
std::deque<std::string> mOrder;
|
||||
std::map<std::string, Entry> mEntries;
|
||||
std::size_t mDroppedCount = 0;
|
||||
std::size_t mCoalescedCount = 0;
|
||||
};
|
||||
@@ -1,170 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventCoalescingQueue.h"
|
||||
#include "RuntimeEventQueue.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventDispatchResult
|
||||
{
|
||||
std::size_t dispatchedEvents = 0;
|
||||
std::size_t handlerInvocations = 0;
|
||||
std::size_t handlerFailures = 0;
|
||||
double dispatchDurationMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class RuntimeEventDispatcher
|
||||
{
|
||||
public:
|
||||
using Handler = std::function<void(const RuntimeEvent&)>;
|
||||
|
||||
explicit RuntimeEventDispatcher(std::size_t queueCapacity = 1024) :
|
||||
mQueue(queueCapacity),
|
||||
mCoalescingQueue(queueCapacity)
|
||||
{
|
||||
}
|
||||
|
||||
bool Publish(RuntimeEvent event)
|
||||
{
|
||||
if (!event.PayloadMatchesType())
|
||||
return false;
|
||||
|
||||
if (event.sequence == 0)
|
||||
event.sequence = mNextSequence.fetch_add(1);
|
||||
|
||||
if (ShouldCoalesce(event))
|
||||
return mCoalescingQueue.Push(std::move(event));
|
||||
|
||||
return mQueue.Push(std::move(event));
|
||||
}
|
||||
|
||||
template <typename Payload>
|
||||
bool PublishPayload(Payload payload, std::string source = {})
|
||||
{
|
||||
return Publish(MakeRuntimeEvent(std::move(payload), std::move(source)));
|
||||
}
|
||||
|
||||
void Subscribe(RuntimeEventType type, Handler handler)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
mHandlers[type].push_back(std::move(handler));
|
||||
}
|
||||
|
||||
void SubscribeAll(Handler handler)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
mAllHandlers.push_back(std::move(handler));
|
||||
}
|
||||
|
||||
RuntimeEventDispatchResult DispatchPending(std::size_t maxEvents = 0)
|
||||
{
|
||||
const auto startedAt = std::chrono::steady_clock::now();
|
||||
RuntimeEventDispatchResult result;
|
||||
FlushCoalescedToFifo(maxEvents);
|
||||
std::vector<RuntimeEvent> events = mQueue.Drain(maxEvents);
|
||||
result.dispatchedEvents = events.size();
|
||||
|
||||
for (const RuntimeEvent& event : events)
|
||||
{
|
||||
std::vector<Handler> handlers = HandlersFor(event.type);
|
||||
result.handlerInvocations += handlers.size();
|
||||
|
||||
for (const Handler& handler : handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler(event);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
++result.handlerFailures;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.dispatchDurationMilliseconds =
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - startedAt).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool TryPop(RuntimeEvent& event)
|
||||
{
|
||||
return mQueue.TryPop(event);
|
||||
}
|
||||
|
||||
RuntimeEventQueueMetrics GetQueueMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
RuntimeEventQueueMetrics metrics = mQueue.GetMetrics(now);
|
||||
const RuntimeEventCoalescingQueueMetrics coalescingMetrics = mCoalescingQueue.GetMetrics(now);
|
||||
if (metrics.depth == 0)
|
||||
metrics.oldestEventAgeMilliseconds = coalescingMetrics.oldestEventAgeMilliseconds;
|
||||
else if (coalescingMetrics.depth > 0)
|
||||
metrics.oldestEventAgeMilliseconds = (std::max)(metrics.oldestEventAgeMilliseconds, coalescingMetrics.oldestEventAgeMilliseconds);
|
||||
metrics.depth += coalescingMetrics.depth;
|
||||
metrics.capacity += coalescingMetrics.capacity;
|
||||
metrics.droppedCount += coalescingMetrics.droppedCount;
|
||||
metrics.coalescedCount = coalescingMetrics.coalescedCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t QueueDepth() const
|
||||
{
|
||||
return mQueue.Depth() + mCoalescingQueue.Depth();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool ShouldCoalesce(const RuntimeEvent& event)
|
||||
{
|
||||
switch (event.type)
|
||||
{
|
||||
case RuntimeEventType::OscValueReceived:
|
||||
case RuntimeEventType::OscCommitRequested:
|
||||
case RuntimeEventType::RuntimeStateBroadcastRequested:
|
||||
case RuntimeEventType::FileChangeDetected:
|
||||
case RuntimeEventType::RuntimeReloadRequested:
|
||||
case RuntimeEventType::ShaderBuildRequested:
|
||||
case RuntimeEventType::RenderSnapshotPublishRequested:
|
||||
case RuntimeEventType::TimingSampleRecorded:
|
||||
case RuntimeEventType::QueueDepthChanged:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FlushCoalescedToFifo(std::size_t maxEvents)
|
||||
{
|
||||
const std::size_t fifoDepth = mQueue.Depth();
|
||||
if (maxEvents != 0 && fifoDepth >= maxEvents)
|
||||
return;
|
||||
|
||||
const std::size_t flushLimit = maxEvents == 0 ? 0 : maxEvents - fifoDepth;
|
||||
std::vector<RuntimeEvent> events = mCoalescingQueue.Drain(flushLimit);
|
||||
for (RuntimeEvent& event : events)
|
||||
mQueue.Push(std::move(event));
|
||||
}
|
||||
|
||||
std::vector<Handler> HandlersFor(RuntimeEventType type) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
std::vector<Handler> handlers = mAllHandlers;
|
||||
|
||||
const auto found = mHandlers.find(type);
|
||||
if (found != mHandlers.end())
|
||||
handlers.insert(handlers.end(), found->second.begin(), found->second.end());
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
RuntimeEventQueue mQueue;
|
||||
RuntimeEventCoalescingQueue mCoalescingQueue;
|
||||
std::atomic<uint64_t> mNextSequence{ 1 };
|
||||
mutable std::mutex mHandlerMutex;
|
||||
std::map<RuntimeEventType, std::vector<Handler>> mHandlers;
|
||||
std::vector<Handler> mAllHandlers;
|
||||
};
|
||||
@@ -1,442 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventType.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "PersistenceRequest.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
enum class RuntimeEventSeverity
|
||||
{
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
enum class RuntimeEventRenderResetScope
|
||||
{
|
||||
None,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
enum class RuntimeEventShaderBuildPhase
|
||||
{
|
||||
Requested,
|
||||
Prepared,
|
||||
Applied,
|
||||
Failed
|
||||
};
|
||||
|
||||
struct OscValueReceivedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
std::string valueJson;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct OscValueCoalescedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::size_t coalescedCount = 0;
|
||||
uint64_t latestGeneration = 0;
|
||||
};
|
||||
|
||||
struct OscCommitRequestedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
std::string valueJson;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct HttpControlMutationRequestedEvent
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string bodyJson;
|
||||
};
|
||||
|
||||
struct WebSocketClientConnectedEvent
|
||||
{
|
||||
std::string clientId;
|
||||
std::size_t connectedClientCount = 0;
|
||||
};
|
||||
|
||||
struct RuntimeStateBroadcastRequestedEvent
|
||||
{
|
||||
std::string reason;
|
||||
bool coalescable = true;
|
||||
};
|
||||
|
||||
struct FileChangeDetectedEvent
|
||||
{
|
||||
std::string path;
|
||||
bool shaderPackageCandidate = false;
|
||||
bool runtimeConfigCandidate = false;
|
||||
bool presetCandidate = false;
|
||||
};
|
||||
|
||||
struct ManualReloadRequestedEvent
|
||||
{
|
||||
bool preserveFeedbackState = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RuntimeMutationEvent
|
||||
{
|
||||
std::string action;
|
||||
bool accepted = false;
|
||||
bool runtimeStateChanged = false;
|
||||
bool runtimeStateBroadcastRequired = false;
|
||||
bool shaderBuildRequested = false;
|
||||
bool persistenceRequested = false;
|
||||
bool clearTransientOscState = false;
|
||||
RuntimeEventRenderResetScope renderResetScope = RuntimeEventRenderResetScope::None;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
struct RuntimeStateChangedEvent
|
||||
{
|
||||
std::string reason;
|
||||
bool renderVisible = false;
|
||||
bool persistenceRequested = false;
|
||||
};
|
||||
|
||||
struct RuntimePersistenceRequestedEvent
|
||||
{
|
||||
PersistenceRequest request;
|
||||
};
|
||||
|
||||
struct RuntimeReloadRequestedEvent
|
||||
{
|
||||
bool preserveFeedbackState = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct ShaderPackagesChangedEvent
|
||||
{
|
||||
bool registryChanged = false;
|
||||
std::size_t packageCount = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RenderSnapshotPublishRequestedEvent
|
||||
{
|
||||
unsigned inputWidth = 0;
|
||||
unsigned inputHeight = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RuntimeStatePresentationChangedEvent
|
||||
{
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct ShaderBuildEvent
|
||||
{
|
||||
RuntimeEventShaderBuildPhase phase = RuntimeEventShaderBuildPhase::Requested;
|
||||
uint64_t generation = 0;
|
||||
unsigned inputWidth = 0;
|
||||
unsigned inputHeight = 0;
|
||||
bool preserveFeedbackState = false;
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct CompileStatusChangedEvent
|
||||
{
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct RenderSnapshotPublishedEvent
|
||||
{
|
||||
uint64_t snapshotVersion = 0;
|
||||
uint64_t structureVersion = 0;
|
||||
uint64_t parameterVersion = 0;
|
||||
uint64_t packageVersion = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::size_t layerCount = 0;
|
||||
};
|
||||
|
||||
struct RenderResetEvent
|
||||
{
|
||||
RuntimeEventRenderResetScope scope = RuntimeEventRenderResetScope::None;
|
||||
bool applied = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct OscOverlayEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
uint64_t generation = 0;
|
||||
bool settled = false;
|
||||
};
|
||||
|
||||
struct FrameRenderedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
double renderMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct PreviewFrameAvailableEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
};
|
||||
|
||||
struct InputSignalChangedEvent
|
||||
{
|
||||
bool hasSignal = false;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
std::string modeName;
|
||||
};
|
||||
|
||||
struct InputFrameArrivedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
long rowBytes = 0;
|
||||
std::string pixelFormat;
|
||||
bool hasNoInputSource = false;
|
||||
};
|
||||
|
||||
struct OutputFrameScheduledEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
int64_t streamTime = 0;
|
||||
int64_t duration = 0;
|
||||
int64_t timeScale = 0;
|
||||
};
|
||||
|
||||
struct OutputFrameCompletedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
std::string result;
|
||||
};
|
||||
|
||||
struct BackendStateChangedEvent
|
||||
{
|
||||
std::string backendName;
|
||||
std::string state;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct SubsystemWarningEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string warningKey;
|
||||
RuntimeEventSeverity severity = RuntimeEventSeverity::Warning;
|
||||
std::string message;
|
||||
bool cleared = false;
|
||||
};
|
||||
|
||||
struct SubsystemRecoveredEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string recoveryKey;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct TimingSampleRecordedEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string metric;
|
||||
double value = 0.0;
|
||||
std::string unit;
|
||||
};
|
||||
|
||||
struct QueueDepthChangedEvent
|
||||
{
|
||||
std::string queueName;
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueReceivedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscValueReceived;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueCoalescedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscValueCoalesced;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscCommitRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscCommitRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const HttpControlMutationRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::HttpControlMutationRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const WebSocketClientConnectedEvent&)
|
||||
{
|
||||
return RuntimeEventType::WebSocketClientConnected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateBroadcastRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStateBroadcastRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const FileChangeDetectedEvent&)
|
||||
{
|
||||
return RuntimeEventType::FileChangeDetected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const ManualReloadRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::ManualReloadRequested;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeMutationEvent& event)
|
||||
{
|
||||
return event.accepted ? RuntimeEventType::RuntimeMutationAccepted : RuntimeEventType::RuntimeMutationRejected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStateChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimePersistenceRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimePersistenceRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeReloadRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeReloadRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const ShaderPackagesChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::ShaderPackagesChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RenderSnapshotPublishRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStatePresentationChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStatePresentationChanged;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const ShaderBuildEvent& event)
|
||||
{
|
||||
switch (event.phase)
|
||||
{
|
||||
case RuntimeEventShaderBuildPhase::Requested:
|
||||
return RuntimeEventType::ShaderBuildRequested;
|
||||
case RuntimeEventShaderBuildPhase::Prepared:
|
||||
return RuntimeEventType::ShaderBuildPrepared;
|
||||
case RuntimeEventShaderBuildPhase::Applied:
|
||||
return RuntimeEventType::ShaderBuildApplied;
|
||||
case RuntimeEventShaderBuildPhase::Failed:
|
||||
return RuntimeEventType::ShaderBuildFailed;
|
||||
}
|
||||
|
||||
return RuntimeEventType::ShaderBuildRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const CompileStatusChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::CompileStatusChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RenderSnapshotPublished;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RenderResetEvent& event)
|
||||
{
|
||||
return event.applied ? RuntimeEventType::RenderResetApplied : RuntimeEventType::RenderResetRequested;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const OscOverlayEvent& event)
|
||||
{
|
||||
return event.settled ? RuntimeEventType::OscOverlaySettled : RuntimeEventType::OscOverlayApplied;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const FrameRenderedEvent&)
|
||||
{
|
||||
return RuntimeEventType::FrameRendered;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const PreviewFrameAvailableEvent&)
|
||||
{
|
||||
return RuntimeEventType::PreviewFrameAvailable;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const InputSignalChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::InputSignalChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const InputFrameArrivedEvent&)
|
||||
{
|
||||
return RuntimeEventType::InputFrameArrived;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameScheduledEvent&)
|
||||
{
|
||||
return RuntimeEventType::OutputFrameScheduled;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const OutputFrameCompletedEvent& event)
|
||||
{
|
||||
if (event.result == "DisplayedLate")
|
||||
return RuntimeEventType::OutputLateFrameDetected;
|
||||
if (event.result == "Dropped")
|
||||
return RuntimeEventType::OutputDroppedFrameDetected;
|
||||
return RuntimeEventType::OutputFrameCompleted;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const BackendStateChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::BackendStateChanged;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const SubsystemWarningEvent& event)
|
||||
{
|
||||
return event.cleared ? RuntimeEventType::SubsystemWarningCleared : RuntimeEventType::SubsystemWarningRaised;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const SubsystemRecoveredEvent&)
|
||||
{
|
||||
return RuntimeEventType::SubsystemRecovered;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const TimingSampleRecordedEvent&)
|
||||
{
|
||||
return RuntimeEventType::TimingSampleRecorded;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const QueueDepthChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::QueueDepthChanged;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEvent.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
double oldestEventAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class RuntimeEventQueue
|
||||
{
|
||||
public:
|
||||
explicit RuntimeEventQueue(std::size_t capacity = 1024) :
|
||||
mCapacity(capacity)
|
||||
{
|
||||
}
|
||||
|
||||
bool Push(RuntimeEvent event)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mEvents.size() >= mCapacity)
|
||||
{
|
||||
++mDroppedCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
mEvents.push_back(std::move(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryPop(RuntimeEvent& event)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mEvents.empty())
|
||||
return false;
|
||||
|
||||
event = std::move(mEvents.front());
|
||||
mEvents.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
|
||||
{
|
||||
std::vector<RuntimeEvent> events;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t count = maxEvents == 0 || maxEvents > mEvents.size() ? mEvents.size() : maxEvents;
|
||||
events.reserve(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
events.push_back(std::move(mEvents.front()));
|
||||
mEvents.pop_front();
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
RuntimeEventQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
RuntimeEventQueueMetrics metrics;
|
||||
metrics.depth = mEvents.size();
|
||||
metrics.capacity = mCapacity;
|
||||
metrics.droppedCount = mDroppedCount;
|
||||
if (!mEvents.empty())
|
||||
{
|
||||
const auto age = now - mEvents.front().createdAt;
|
||||
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t Depth() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mEvents.size();
|
||||
}
|
||||
|
||||
std::size_t Capacity() const
|
||||
{
|
||||
return mCapacity;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
std::deque<RuntimeEvent> mEvents;
|
||||
std::size_t mCapacity = 0;
|
||||
std::size_t mDroppedCount = 0;
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
enum class RuntimeEventType
|
||||
{
|
||||
Unknown = 0,
|
||||
|
||||
// Control ingress.
|
||||
OscValueReceived,
|
||||
OscValueCoalesced,
|
||||
OscCommitRequested,
|
||||
HttpControlMutationRequested,
|
||||
WebSocketClientConnected,
|
||||
RuntimeStateBroadcastRequested,
|
||||
FileChangeDetected,
|
||||
ManualReloadRequested,
|
||||
|
||||
// Runtime policy and state.
|
||||
RuntimeMutationAccepted,
|
||||
RuntimeMutationRejected,
|
||||
RuntimeStateChanged,
|
||||
RuntimePersistenceRequested,
|
||||
RuntimeReloadRequested,
|
||||
ShaderPackagesChanged,
|
||||
RenderSnapshotPublishRequested,
|
||||
RuntimeStatePresentationChanged,
|
||||
|
||||
// Shader build lifecycle.
|
||||
ShaderBuildRequested,
|
||||
ShaderBuildPrepared,
|
||||
ShaderBuildApplied,
|
||||
ShaderBuildFailed,
|
||||
CompileStatusChanged,
|
||||
|
||||
// Render lifecycle.
|
||||
RenderSnapshotPublished,
|
||||
RenderResetRequested,
|
||||
RenderResetApplied,
|
||||
OscOverlayApplied,
|
||||
OscOverlaySettled,
|
||||
FrameRendered,
|
||||
PreviewFrameAvailable,
|
||||
|
||||
// Video backend lifecycle.
|
||||
InputSignalChanged,
|
||||
InputFrameArrived,
|
||||
OutputFrameScheduled,
|
||||
OutputFrameCompleted,
|
||||
OutputLateFrameDetected,
|
||||
OutputDroppedFrameDetected,
|
||||
BackendStateChanged,
|
||||
|
||||
// Health and telemetry.
|
||||
SubsystemWarningRaised,
|
||||
SubsystemWarningCleared,
|
||||
SubsystemRecovered,
|
||||
TimingSampleRecorded,
|
||||
QueueDepthChanged
|
||||
};
|
||||
|
||||
constexpr std::string_view RuntimeEventTypeName(RuntimeEventType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RuntimeEventType::Unknown:
|
||||
return "Unknown";
|
||||
case RuntimeEventType::OscValueReceived:
|
||||
return "OscValueReceived";
|
||||
case RuntimeEventType::OscValueCoalesced:
|
||||
return "OscValueCoalesced";
|
||||
case RuntimeEventType::OscCommitRequested:
|
||||
return "OscCommitRequested";
|
||||
case RuntimeEventType::HttpControlMutationRequested:
|
||||
return "HttpControlMutationRequested";
|
||||
case RuntimeEventType::WebSocketClientConnected:
|
||||
return "WebSocketClientConnected";
|
||||
case RuntimeEventType::RuntimeStateBroadcastRequested:
|
||||
return "RuntimeStateBroadcastRequested";
|
||||
case RuntimeEventType::FileChangeDetected:
|
||||
return "FileChangeDetected";
|
||||
case RuntimeEventType::ManualReloadRequested:
|
||||
return "ManualReloadRequested";
|
||||
case RuntimeEventType::RuntimeMutationAccepted:
|
||||
return "RuntimeMutationAccepted";
|
||||
case RuntimeEventType::RuntimeMutationRejected:
|
||||
return "RuntimeMutationRejected";
|
||||
case RuntimeEventType::RuntimeStateChanged:
|
||||
return "RuntimeStateChanged";
|
||||
case RuntimeEventType::RuntimePersistenceRequested:
|
||||
return "RuntimePersistenceRequested";
|
||||
case RuntimeEventType::RuntimeReloadRequested:
|
||||
return "RuntimeReloadRequested";
|
||||
case RuntimeEventType::ShaderPackagesChanged:
|
||||
return "ShaderPackagesChanged";
|
||||
case RuntimeEventType::RenderSnapshotPublishRequested:
|
||||
return "RenderSnapshotPublishRequested";
|
||||
case RuntimeEventType::RuntimeStatePresentationChanged:
|
||||
return "RuntimeStatePresentationChanged";
|
||||
case RuntimeEventType::ShaderBuildRequested:
|
||||
return "ShaderBuildRequested";
|
||||
case RuntimeEventType::ShaderBuildPrepared:
|
||||
return "ShaderBuildPrepared";
|
||||
case RuntimeEventType::ShaderBuildApplied:
|
||||
return "ShaderBuildApplied";
|
||||
case RuntimeEventType::ShaderBuildFailed:
|
||||
return "ShaderBuildFailed";
|
||||
case RuntimeEventType::CompileStatusChanged:
|
||||
return "CompileStatusChanged";
|
||||
case RuntimeEventType::RenderSnapshotPublished:
|
||||
return "RenderSnapshotPublished";
|
||||
case RuntimeEventType::RenderResetRequested:
|
||||
return "RenderResetRequested";
|
||||
case RuntimeEventType::RenderResetApplied:
|
||||
return "RenderResetApplied";
|
||||
case RuntimeEventType::OscOverlayApplied:
|
||||
return "OscOverlayApplied";
|
||||
case RuntimeEventType::OscOverlaySettled:
|
||||
return "OscOverlaySettled";
|
||||
case RuntimeEventType::FrameRendered:
|
||||
return "FrameRendered";
|
||||
case RuntimeEventType::PreviewFrameAvailable:
|
||||
return "PreviewFrameAvailable";
|
||||
case RuntimeEventType::InputSignalChanged:
|
||||
return "InputSignalChanged";
|
||||
case RuntimeEventType::InputFrameArrived:
|
||||
return "InputFrameArrived";
|
||||
case RuntimeEventType::OutputFrameScheduled:
|
||||
return "OutputFrameScheduled";
|
||||
case RuntimeEventType::OutputFrameCompleted:
|
||||
return "OutputFrameCompleted";
|
||||
case RuntimeEventType::OutputLateFrameDetected:
|
||||
return "OutputLateFrameDetected";
|
||||
case RuntimeEventType::OutputDroppedFrameDetected:
|
||||
return "OutputDroppedFrameDetected";
|
||||
case RuntimeEventType::BackendStateChanged:
|
||||
return "BackendStateChanged";
|
||||
case RuntimeEventType::SubsystemWarningRaised:
|
||||
return "SubsystemWarningRaised";
|
||||
case RuntimeEventType::SubsystemWarningCleared:
|
||||
return "SubsystemWarningCleared";
|
||||
case RuntimeEventType::SubsystemRecovered:
|
||||
return "SubsystemRecovered";
|
||||
case RuntimeEventType::TimingSampleRecorded:
|
||||
return "TimingSampleRecorded";
|
||||
case RuntimeEventType::QueueDepthChanged:
|
||||
return "QueueDepthChanged";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
#include "CommittedLiveState.h"
|
||||
|
||||
bool CommittedLiveState::LoadPersistentStateValue(const JsonValue& root)
|
||||
{
|
||||
return mLayerStack.LoadPersistentStateValue(root);
|
||||
}
|
||||
|
||||
JsonValue CommittedLiveState::BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const
|
||||
{
|
||||
return mLayerStack.BuildPersistentStateValue(shaderCatalog);
|
||||
}
|
||||
|
||||
void CommittedLiveState::NormalizeLayerIds()
|
||||
{
|
||||
mLayerStack.NormalizeLayerIds();
|
||||
}
|
||||
|
||||
void CommittedLiveState::EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
mLayerStack.EnsureDefaultsForAllLayers(shaderCatalog);
|
||||
}
|
||||
|
||||
void CommittedLiveState::EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
mLayerStack.EnsureDefaultLayer(shaderCatalog);
|
||||
}
|
||||
|
||||
void CommittedLiveState::RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
mLayerStack.RemoveLayersWithMissingPackages(shaderCatalog);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
return mLayerStack.CreateLayer(shaderCatalog, shaderId, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::DeleteLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
return mLayerStack.DeleteLayer(layerId, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||
{
|
||||
return mLayerStack.MoveLayer(layerId, direction, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||
{
|
||||
return mLayerStack.MoveLayerToIndex(layerId, targetIndex, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
return mLayerStack.SetLayerBypassState(layerId, bypassed, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
return mLayerStack.SetLayerShaderSelection(shaderCatalog, layerId, shaderId, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error)
|
||||
{
|
||||
return mLayerStack.SetParameterValue(layerId, parameterId, value, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error)
|
||||
{
|
||||
return mLayerStack.ResetLayerParameterValues(shaderCatalog, layerId, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::HasLayer(const std::string& layerId) const
|
||||
{
|
||||
return mLayerStack.HasLayer(layerId);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
return mLayerStack.TryGetParameterById(shaderCatalog, layerId, parameterId, snapshot, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
return mLayerStack.TryGetParameterByControlKey(shaderCatalog, layerKey, parameterKey, snapshot, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mLayerStack.ResolveLayerMove(layerId, direction, shouldMove, error);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mLayerStack.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error);
|
||||
}
|
||||
|
||||
JsonValue CommittedLiveState::BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const
|
||||
{
|
||||
return mLayerStack.BuildStackPresetValue(shaderCatalog, presetName);
|
||||
}
|
||||
|
||||
bool CommittedLiveState::LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error)
|
||||
{
|
||||
return mLayerStack.LoadStackPresetValue(shaderCatalog, root, error);
|
||||
}
|
||||
|
||||
CommittedLiveStateReadModel CommittedLiveState::BuildReadModel(const ShaderPackageCatalog& shaderCatalog) const
|
||||
{
|
||||
CommittedLiveStateReadModel model;
|
||||
model.layers = mLayerStack.Layers();
|
||||
model.packagesById = shaderCatalog.CaptureSnapshot().packagesById;
|
||||
return model;
|
||||
}
|
||||
|
||||
std::vector<CommittedLiveState::LayerPersistentState> CommittedLiveState::CopyLayerStates() const
|
||||
{
|
||||
return mLayerStack.Layers();
|
||||
}
|
||||
|
||||
const std::vector<CommittedLiveState::LayerPersistentState>& CommittedLiveState::Layers() const
|
||||
{
|
||||
return mLayerStack.Layers();
|
||||
}
|
||||
|
||||
std::vector<CommittedLiveState::LayerPersistentState>& CommittedLiveState::Layers()
|
||||
{
|
||||
return mLayerStack.Layers();
|
||||
}
|
||||
|
||||
const CommittedLiveState::LayerPersistentState* CommittedLiveState::FindLayerById(const std::string& layerId) const
|
||||
{
|
||||
return mLayerStack.FindLayerById(layerId);
|
||||
}
|
||||
|
||||
const LayerStackStore& CommittedLiveState::LayerStack() const
|
||||
{
|
||||
return mLayerStack;
|
||||
}
|
||||
|
||||
LayerStackStore& CommittedLiveState::LayerStack()
|
||||
{
|
||||
return mLayerStack;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "LayerStackStore.h"
|
||||
#include "RuntimeStoreReadModels.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class CommittedLiveState
|
||||
{
|
||||
public:
|
||||
using LayerPersistentState = LayerStackStore::LayerPersistentState;
|
||||
using StoredParameterSnapshot = LayerStackStore::StoredParameterSnapshot;
|
||||
|
||||
bool LoadPersistentStateValue(const JsonValue& root);
|
||||
JsonValue BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const;
|
||||
void NormalizeLayerIds();
|
||||
void EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog);
|
||||
void EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog);
|
||||
void RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog);
|
||||
|
||||
bool CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||
bool DeleteLayer(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 SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error);
|
||||
bool ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error);
|
||||
|
||||
bool HasLayer(const std::string& layerId) const;
|
||||
bool TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
|
||||
JsonValue BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const;
|
||||
bool LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error);
|
||||
|
||||
CommittedLiveStateReadModel BuildReadModel(const ShaderPackageCatalog& shaderCatalog) const;
|
||||
std::vector<LayerPersistentState> CopyLayerStates() const;
|
||||
const std::vector<LayerPersistentState>& Layers() const;
|
||||
std::vector<LayerPersistentState>& Layers();
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
const LayerStackStore& LayerStack() const;
|
||||
LayerStackStore& LayerStack();
|
||||
|
||||
private:
|
||||
LayerStackStore mLayerStack;
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
#include "RenderStateComposer.h"
|
||||
|
||||
RenderStateCompositionResult RenderStateComposer::BuildFrameState(const LayeredRenderStateInput& input) const
|
||||
{
|
||||
RenderStateCompositionResult result;
|
||||
const std::vector<RuntimeRenderState>* layerStates =
|
||||
input.committedLiveLayerStates ? input.committedLiveLayerStates : input.basePersistedLayerStates;
|
||||
if (!layerStates)
|
||||
return result;
|
||||
|
||||
result.layerStates = *layerStates;
|
||||
result.hasLayerStates = !result.layerStates.empty();
|
||||
if (input.transientAutomationOverlay)
|
||||
{
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.allowCommit = input.allowTransientAutomationCommits;
|
||||
options.smoothing = input.transientAutomationSmoothing;
|
||||
options.commitDelay = input.transientAutomationCommitDelay;
|
||||
options.now = input.now;
|
||||
input.transientAutomationOverlay->ApplyToLayerStates(
|
||||
result.layerStates,
|
||||
options,
|
||||
input.collectTransientAutomationCommitRequests ? &result.commitRequests : nullptr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeLiveState.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
struct LayeredRenderStateInput
|
||||
{
|
||||
const std::vector<RuntimeRenderState>* basePersistedLayerStates = nullptr;
|
||||
const std::vector<RuntimeRenderState>* committedLiveLayerStates = nullptr;
|
||||
RuntimeLiveState* transientAutomationOverlay = nullptr;
|
||||
bool allowTransientAutomationCommits = false;
|
||||
bool collectTransientAutomationCommitRequests = true;
|
||||
double transientAutomationSmoothing = 0.0;
|
||||
std::chrono::milliseconds transientAutomationCommitDelay = std::chrono::milliseconds(150);
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
struct RenderStateCompositionResult
|
||||
{
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||
bool hasLayerStates = false;
|
||||
};
|
||||
|
||||
class RenderStateComposer
|
||||
{
|
||||
public:
|
||||
RenderStateCompositionResult BuildFrameState(const LayeredRenderStateInput& input) const;
|
||||
};
|
||||
@@ -1,329 +0,0 @@
|
||||
#include "RuntimeLiveState.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RuntimeLiveState::Clear()
|
||||
{
|
||||
mOscOverlayStates.clear();
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ClearForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();)
|
||||
{
|
||||
if (OverlayMatchesLayerKey(it->second, layerKey))
|
||||
it = mOscOverlayStates.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeLiveState::OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey)
|
||||
{
|
||||
return MatchesOscControlKey(overlay.layerKey, layerKey);
|
||||
}
|
||||
|
||||
bool RuntimeLiveState::TryResolveOverlayTarget(
|
||||
const OscOverlayState& overlay,
|
||||
const std::vector<RuntimeRenderState>& states,
|
||||
std::vector<RuntimeRenderState>::const_iterator& stateIt,
|
||||
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt)
|
||||
{
|
||||
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())
|
||||
return false;
|
||||
|
||||
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);
|
||||
});
|
||||
return definitionIt != stateIt->parameterDefinitions.end();
|
||||
}
|
||||
|
||||
std::size_t RuntimeLiveState::OverlayCount() const
|
||||
{
|
||||
return mOscOverlayStates.size();
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
for (const RuntimeLiveOscUpdate& update : updates)
|
||||
{
|
||||
auto overlayIt = mOscOverlayStates.find(update.routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
{
|
||||
OscOverlayState overlay;
|
||||
overlay.layerKey = update.layerKey;
|
||||
overlay.parameterKey = update.parameterKey;
|
||||
overlay.targetValue = update.targetValue;
|
||||
overlay.lastUpdatedTime = now;
|
||||
overlay.lastAppliedTime = now;
|
||||
overlay.generation = 1;
|
||||
mOscOverlayStates[update.routeKey] = std::move(overlay);
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayIt->second.targetValue = update.targetValue;
|
||||
overlayIt->second.lastUpdatedTime = now;
|
||||
overlayIt->second.generation += 1;
|
||||
overlayIt->second.commitQueued = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitCompletion& completedCommit : completedCommits)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states)
|
||||
{
|
||||
for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();)
|
||||
{
|
||||
std::vector<RuntimeRenderState>::const_iterator stateIt;
|
||||
std::vector<ShaderParameterDefinition>::const_iterator definitionIt;
|
||||
if (TryResolveOverlayTarget(it->second, states, stateIt, definitionIt))
|
||||
{
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (NormalizeAndValidateParameterValue(*definitionIt, it->second.targetValue, targetValue, normalizeError))
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
it = mOscOverlayStates.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyToLayerStates(
|
||||
std::vector<RuntimeRenderState>& states,
|
||||
const RuntimeLiveStateApplyOptions& options,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests)
|
||||
{
|
||||
if (states.empty() || mOscOverlayStates.empty())
|
||||
return;
|
||||
|
||||
PruneIncompatibleOverlays(states);
|
||||
if (mOscOverlayStates.empty())
|
||||
return;
|
||||
|
||||
const auto now = options.now;
|
||||
const double clampedSmoothing = ClampOscAlpha(options.smoothing);
|
||||
std::vector<std::string> overlayKeysToRemove;
|
||||
|
||||
for (auto& item : mOscOverlayStates)
|
||||
{
|
||||
const std::string& routeKey = item.first;
|
||||
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;
|
||||
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||
continue;
|
||||
|
||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
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(routeKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool smoothable =
|
||||
clampedSmoothing > 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 (options.allowCommit &&
|
||||
!overlay.commitQueued &&
|
||||
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||
commitRequests)
|
||||
{
|
||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation });
|
||||
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 = clampedSmoothing;
|
||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
const double deltaSeconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - overlay.lastAppliedTime).count();
|
||||
smoothingAlpha = ComputeTimeBasedOscAlpha(clampedSmoothing, deltaSeconds);
|
||||
}
|
||||
overlay.lastAppliedTime = now;
|
||||
|
||||
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 (options.allowCommit &&
|
||||
converged &&
|
||||
!overlay.commitQueued &&
|
||||
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||
commitRequests)
|
||||
{
|
||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, BuildOscCommitValue(*definitionIt, overlay.currentValue), overlay.generation });
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||
mOscOverlayStates.erase(overlayKey);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeLiveOscUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
|
||||
struct RuntimeLiveOscCommitCompletion
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct RuntimeLiveOscCommitRequest
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue value;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct RuntimeLiveStateApplyOptions
|
||||
{
|
||||
bool allowCommit = false;
|
||||
double smoothing = 0.0;
|
||||
std::chrono::milliseconds commitDelay = std::chrono::milliseconds(150);
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
class RuntimeLiveState
|
||||
{
|
||||
public:
|
||||
void Clear();
|
||||
void ClearForLayerKey(const std::string& layerKey);
|
||||
std::size_t OverlayCount() const;
|
||||
void ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates);
|
||||
void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits);
|
||||
void PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states);
|
||||
void ApplyToLayerStates(
|
||||
std::vector<RuntimeRenderState>& states,
|
||||
const RuntimeLiveStateApplyOptions& options,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests);
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
|
||||
static bool OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey);
|
||||
static bool TryResolveOverlayTarget(
|
||||
const OscOverlayState& overlay,
|
||||
const std::vector<RuntimeRenderState>& states,
|
||||
std::vector<RuntimeRenderState>::const_iterator& stateIt,
|
||||
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt);
|
||||
|
||||
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
#include "RuntimeStateLayerModel.h"
|
||||
|
||||
const char* RuntimeStateLayerKindName(RuntimeStateLayerKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case RuntimeStateLayerKind::BasePersisted:
|
||||
return "base persisted";
|
||||
case RuntimeStateLayerKind::CommittedLive:
|
||||
return "committed live";
|
||||
case RuntimeStateLayerKind::TransientAutomation:
|
||||
return "transient automation";
|
||||
case RuntimeStateLayerKind::RenderLocal:
|
||||
return "render local";
|
||||
case RuntimeStateLayerKind::HealthConfig:
|
||||
return "health/config";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
int RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case RuntimeStateLayerKind::BasePersisted:
|
||||
return 0;
|
||||
case RuntimeStateLayerKind::CommittedLive:
|
||||
return 1;
|
||||
case RuntimeStateLayerKind::TransientAutomation:
|
||||
return 2;
|
||||
case RuntimeStateLayerKind::RenderLocal:
|
||||
case RuntimeStateLayerKind::HealthConfig:
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerIsDurable(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return kind == RuntimeStateLayerKind::BasePersisted;
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return RuntimeStateLayerCompositionPrecedence(kind) >= 0;
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerIsRenderLocal(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return kind == RuntimeStateLayerKind::RenderLocal;
|
||||
}
|
||||
|
||||
RuntimeStateLayerKind ClassifyRuntimeStateField(RuntimeStateField field)
|
||||
{
|
||||
switch (field)
|
||||
{
|
||||
case RuntimeStateField::PersistedLayerStack:
|
||||
case RuntimeStateField::PersistedParameterValues:
|
||||
case RuntimeStateField::StackPresets:
|
||||
return RuntimeStateLayerKind::BasePersisted;
|
||||
case RuntimeStateField::CommittedSessionParameterValues:
|
||||
case RuntimeStateField::CommittedLayerBypass:
|
||||
case RuntimeStateField::RuntimeCompileReloadFlags:
|
||||
return RuntimeStateLayerKind::CommittedLive;
|
||||
case RuntimeStateField::TransientOscOverlay:
|
||||
case RuntimeStateField::TransientAutomationCommitState:
|
||||
return RuntimeStateLayerKind::TransientAutomation;
|
||||
case RuntimeStateField::RenderLocalTemporalHistory:
|
||||
case RuntimeStateField::RenderLocalFeedbackState:
|
||||
case RuntimeStateField::RenderLocalInputFrames:
|
||||
case RuntimeStateField::RenderLocalOutputFrames:
|
||||
return RuntimeStateLayerKind::RenderLocal;
|
||||
case RuntimeStateField::RuntimeConfiguration:
|
||||
case RuntimeStateField::HealthTelemetry:
|
||||
default:
|
||||
return RuntimeStateLayerKind::HealthConfig;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RuntimeStateLayerDescriptor> GetRuntimeStateLayerInventory()
|
||||
{
|
||||
return {
|
||||
{
|
||||
RuntimeStateLayerKind::BasePersisted,
|
||||
"Base persisted state",
|
||||
"RuntimeStore / LayerStackStore",
|
||||
"Survives restart",
|
||||
"Written to disk",
|
||||
"Default layer stack, shader selections, saved parameter values"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::CommittedLive,
|
||||
"Committed live state",
|
||||
"RuntimeCoordinator / CommittedLiveState",
|
||||
"Current running session",
|
||||
"May request persistence depending on mutation policy",
|
||||
"Operator/session truth until changed again"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::TransientAutomation,
|
||||
"Transient automation overlay",
|
||||
"RuntimeLiveState / RuntimeServiceLiveBridge",
|
||||
"High-rate and short-lived",
|
||||
"Not persisted directly",
|
||||
"Temporary OSC/automation target applied over committed truth"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::RenderLocal,
|
||||
"Render-local state",
|
||||
"RenderEngine",
|
||||
"Render-thread/resource lifetime",
|
||||
"Not persisted",
|
||||
"Temporal history, feedback, input/output queues, and GL-local caches"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::HealthConfig,
|
||||
"Health/config state",
|
||||
"RuntimeConfigStore / HealthTelemetry",
|
||||
"Config survives restart; health is observational",
|
||||
"Config is file-backed; health is reported, not composed",
|
||||
"Does not participate in parameter composition"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<RuntimeStateFieldDescriptor> GetRuntimeStateFieldInventory()
|
||||
{
|
||||
return {
|
||||
{
|
||||
RuntimeStateField::PersistedLayerStack,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::PersistedLayerStack),
|
||||
"persisted layer stack",
|
||||
"LayerStackStore",
|
||||
"Durable layer order, ids, shader selections, and bypass flags"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::PersistedParameterValues,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::PersistedParameterValues),
|
||||
"persisted parameter values",
|
||||
"LayerStackStore",
|
||||
"Saved parameter values used as the baseline for snapshots and presets"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::StackPresets,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::StackPresets),
|
||||
"stack presets",
|
||||
"RuntimeStore / LayerStackStore",
|
||||
"Durable preset files and preset serialization shape"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::CommittedSessionParameterValues,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::CommittedSessionParameterValues),
|
||||
"committed session parameter values",
|
||||
"RuntimeCoordinator policy, CommittedLiveState backing",
|
||||
"Operator/API truth after accepted mutations"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::CommittedLayerBypass,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::CommittedLayerBypass),
|
||||
"committed layer bypass",
|
||||
"RuntimeCoordinator policy, CommittedLiveState backing",
|
||||
"Current operator/API bypass state"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RuntimeCompileReloadFlags,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RuntimeCompileReloadFlags),
|
||||
"runtime compile/reload flags",
|
||||
"RuntimeCoordinator / RuntimeUpdateController",
|
||||
"Session coordination state used to request snapshot or render rebuild work"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::TransientOscOverlay,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::TransientOscOverlay),
|
||||
"transient OSC overlays",
|
||||
"RuntimeLiveState",
|
||||
"High-rate automation values applied above committed state"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::TransientAutomationCommitState,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::TransientAutomationCommitState),
|
||||
"transient automation commit state",
|
||||
"RuntimeLiveState / RuntimeServiceLiveBridge",
|
||||
"Generation and completion bookkeeping for settled overlay commits"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalTemporalHistory,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalTemporalHistory),
|
||||
"render-local temporal history",
|
||||
"RenderEngine",
|
||||
"GL/resource history that must stay out of parameter layering"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalFeedbackState,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalFeedbackState),
|
||||
"render-local feedback state",
|
||||
"RenderEngine",
|
||||
"Feedback buffers and ping-pong resources"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalInputFrames,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalInputFrames),
|
||||
"render-local input frames",
|
||||
"RenderEngine",
|
||||
"Latest accepted input frame payloads and upload staging"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalOutputFrames,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalOutputFrames),
|
||||
"render-local output frames",
|
||||
"RenderEngine",
|
||||
"Readback, packed output, screenshot, and preview staging"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RuntimeConfiguration,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RuntimeConfiguration),
|
||||
"runtime configuration",
|
||||
"RuntimeConfigStore",
|
||||
"File-backed config, not a live parameter layer"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::HealthTelemetry,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::HealthTelemetry),
|
||||
"health telemetry",
|
||||
"HealthTelemetry",
|
||||
"Operational observations, not source state for render values"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class RuntimeStateLayerKind
|
||||
{
|
||||
BasePersisted,
|
||||
CommittedLive,
|
||||
TransientAutomation,
|
||||
RenderLocal,
|
||||
HealthConfig
|
||||
};
|
||||
|
||||
enum class RuntimeStateField
|
||||
{
|
||||
PersistedLayerStack,
|
||||
PersistedParameterValues,
|
||||
StackPresets,
|
||||
CommittedSessionParameterValues,
|
||||
CommittedLayerBypass,
|
||||
RuntimeCompileReloadFlags,
|
||||
TransientOscOverlay,
|
||||
TransientAutomationCommitState,
|
||||
RenderLocalTemporalHistory,
|
||||
RenderLocalFeedbackState,
|
||||
RenderLocalInputFrames,
|
||||
RenderLocalOutputFrames,
|
||||
RuntimeConfiguration,
|
||||
HealthTelemetry
|
||||
};
|
||||
|
||||
struct RuntimeStateLayerDescriptor
|
||||
{
|
||||
RuntimeStateLayerKind kind = RuntimeStateLayerKind::BasePersisted;
|
||||
const char* name = "";
|
||||
const char* owner = "";
|
||||
const char* lifetime = "";
|
||||
const char* persistence = "";
|
||||
const char* renderRole = "";
|
||||
};
|
||||
|
||||
struct RuntimeStateFieldDescriptor
|
||||
{
|
||||
RuntimeStateField field = RuntimeStateField::PersistedLayerStack;
|
||||
RuntimeStateLayerKind layerKind = RuntimeStateLayerKind::BasePersisted;
|
||||
const char* name = "";
|
||||
const char* currentOwner = "";
|
||||
const char* notes = "";
|
||||
};
|
||||
|
||||
const char* RuntimeStateLayerKindName(RuntimeStateLayerKind kind);
|
||||
int RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerIsDurable(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerIsRenderLocal(RuntimeStateLayerKind kind);
|
||||
|
||||
RuntimeStateLayerKind ClassifyRuntimeStateField(RuntimeStateField field);
|
||||
std::vector<RuntimeStateLayerDescriptor> GetRuntimeStateLayerInventory();
|
||||
std::vector<RuntimeStateFieldDescriptor> GetRuntimeStateFieldInventory();
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
enum class PersistenceTargetKind
|
||||
{
|
||||
RuntimeState,
|
||||
StackPreset,
|
||||
RuntimeConfig
|
||||
};
|
||||
|
||||
struct PersistenceRequest
|
||||
{
|
||||
PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState;
|
||||
std::string reason;
|
||||
std::string debounceKey = "runtime-state";
|
||||
bool debounceAllowed = true;
|
||||
bool flushRequested = false;
|
||||
uint64_t sequence = 0;
|
||||
|
||||
static PersistenceRequest RuntimeStateRequest(const std::string& reason)
|
||||
{
|
||||
PersistenceRequest request;
|
||||
request.targetKind = PersistenceTargetKind::RuntimeState;
|
||||
request.reason = reason;
|
||||
request.debounceKey = "runtime-state";
|
||||
request.debounceAllowed = true;
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
struct PersistenceSnapshot
|
||||
{
|
||||
PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState;
|
||||
std::filesystem::path targetPath;
|
||||
std::string contents;
|
||||
std::string reason;
|
||||
std::string debounceKey;
|
||||
bool debounceAllowed = false;
|
||||
bool flushRequested = false;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
@@ -1,276 +0,0 @@
|
||||
#include "PersistenceWriter.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
|
||||
PersistenceWriter::PersistenceWriter(std::chrono::milliseconds debounceDelay, SnapshotSink sink) :
|
||||
mDebounceDelay(debounceDelay),
|
||||
mSink(std::move(sink))
|
||||
{
|
||||
}
|
||||
|
||||
PersistenceWriter::~PersistenceWriter()
|
||||
{
|
||||
StopAndFlush();
|
||||
}
|
||||
|
||||
void PersistenceWriter::SetResultCallback(ResultCallback callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mResultCallback = std::move(callback);
|
||||
}
|
||||
|
||||
bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error)
|
||||
{
|
||||
if (!ValidateSnapshot(snapshot, error))
|
||||
return false;
|
||||
|
||||
const bool succeeded = WriteSnapshotThroughSink(snapshot, error);
|
||||
PublishWriteResult(snapshot, succeeded, error, false);
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool PersistenceWriter::EnqueueSnapshot(const PersistenceSnapshot& snapshot, std::string& error)
|
||||
{
|
||||
if (!ValidateSnapshot(snapshot, error))
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mAcceptingRequests)
|
||||
{
|
||||
error = "Persistence writer is stopping.";
|
||||
return false;
|
||||
}
|
||||
|
||||
StartWorkerLocked();
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (snapshot.debounceAllowed)
|
||||
{
|
||||
const std::string debounceKey = snapshot.debounceKey.empty() ? snapshot.targetPath.string() : snapshot.debounceKey;
|
||||
PendingSnapshot& pending = mDebouncedSnapshots[debounceKey];
|
||||
if (!pending.snapshot.targetPath.empty())
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
pending.snapshot = snapshot;
|
||||
pending.readyAt = snapshot.flushRequested ? now : now + mDebounceDelay;
|
||||
}
|
||||
else
|
||||
{
|
||||
mImmediateSnapshots.push_back(snapshot);
|
||||
++mEnqueuedCount;
|
||||
}
|
||||
|
||||
mCondition.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PersistenceWriter::StopAndFlush()
|
||||
{
|
||||
std::string error;
|
||||
StopAndFlush((std::chrono::milliseconds::max)(), error);
|
||||
}
|
||||
|
||||
bool PersistenceWriter::StopAndFlush(std::chrono::milliseconds timeout, std::string& error)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mAcceptingRequests = false;
|
||||
mStopping = true;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
for (auto& entry : mDebouncedSnapshots)
|
||||
entry.second.readyAt = now;
|
||||
}
|
||||
mCondition.notify_all();
|
||||
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
if (mWorkerRunning)
|
||||
{
|
||||
if (timeout == (std::chrono::milliseconds::max)())
|
||||
{
|
||||
mCondition.wait(lock, [this]() { return !mWorkerRunning; });
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
if (!mCondition.wait_until(lock, deadline, [this]() { return !mWorkerRunning; }))
|
||||
{
|
||||
error = "Timed out while flushing persistence writer.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
if (mWorker.joinable())
|
||||
mWorker.join();
|
||||
return true;
|
||||
}
|
||||
|
||||
PersistenceWriterMetrics PersistenceWriter::GetMetrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
PersistenceWriterMetrics metrics;
|
||||
metrics.pendingCount = PendingCountLocked();
|
||||
metrics.enqueuedCount = mEnqueuedCount;
|
||||
metrics.coalescedCount = mCoalescedCount;
|
||||
metrics.writtenCount = mWrittenCount;
|
||||
metrics.failedCount = mFailedCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool PersistenceWriter::ValidateSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
if (snapshot.targetPath.empty())
|
||||
{
|
||||
error = "Persistence snapshot target path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PersistenceWriter::WriteSnapshotThroughSink(const PersistenceSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
if (mSink)
|
||||
return mSink(snapshot, error);
|
||||
|
||||
std::error_code fsError;
|
||||
std::filesystem::create_directories(snapshot.targetPath.parent_path(), fsError);
|
||||
|
||||
const std::filesystem::path temporaryPath = snapshot.targetPath.string() + ".tmp";
|
||||
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output)
|
||||
{
|
||||
error = "Could not write file: " + temporaryPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
output << snapshot.contents;
|
||||
output.close();
|
||||
if (!output.good())
|
||||
{
|
||||
error = "Could not finish writing file: " + temporaryPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MoveFileExA(temporaryPath.string().c_str(), snapshot.targetPath.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
{
|
||||
const DWORD lastError = GetLastError();
|
||||
std::filesystem::remove(temporaryPath, fsError);
|
||||
error = "Could not replace file: " + snapshot.targetPath.string() + " (Win32 error " + std::to_string(lastError) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PersistenceWriter::PublishWriteResult(const PersistenceSnapshot& snapshot, bool succeeded, const std::string& errorMessage, bool newerRequestPending)
|
||||
{
|
||||
ResultCallback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
callback = mResultCallback;
|
||||
}
|
||||
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
PersistenceWriteResult result;
|
||||
result.targetKind = snapshot.targetKind;
|
||||
result.targetPath = snapshot.targetPath.string();
|
||||
result.reason = snapshot.reason;
|
||||
result.succeeded = succeeded;
|
||||
result.errorMessage = errorMessage;
|
||||
result.newerRequestPending = newerRequestPending;
|
||||
callback(result);
|
||||
}
|
||||
|
||||
void PersistenceWriter::StartWorkerLocked()
|
||||
{
|
||||
if (mWorkerRunning)
|
||||
return;
|
||||
|
||||
mWorkerRunning = true;
|
||||
mWorker = std::thread([this]() { WorkerMain(); });
|
||||
}
|
||||
|
||||
void PersistenceWriter::WorkerMain()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
PersistenceSnapshot snapshot;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
for (;;)
|
||||
{
|
||||
if (!mImmediateSnapshots.empty())
|
||||
{
|
||||
snapshot = std::move(mImmediateSnapshots.front());
|
||||
mImmediateSnapshots.pop_front();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!mDebouncedSnapshots.empty())
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
auto readyIt = mDebouncedSnapshots.end();
|
||||
auto nextReadyAt = (std::chrono::steady_clock::time_point::max)();
|
||||
for (auto it = mDebouncedSnapshots.begin(); it != mDebouncedSnapshots.end(); ++it)
|
||||
{
|
||||
if (it->second.readyAt <= now)
|
||||
{
|
||||
readyIt = it;
|
||||
break;
|
||||
}
|
||||
if (it->second.readyAt < nextReadyAt)
|
||||
nextReadyAt = it->second.readyAt;
|
||||
}
|
||||
|
||||
if (readyIt != mDebouncedSnapshots.end())
|
||||
{
|
||||
snapshot = std::move(readyIt->second.snapshot);
|
||||
mDebouncedSnapshots.erase(readyIt);
|
||||
break;
|
||||
}
|
||||
|
||||
mCondition.wait_until(lock, nextReadyAt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mStopping)
|
||||
{
|
||||
mWorkerRunning = false;
|
||||
mCondition.notify_all();
|
||||
return;
|
||||
}
|
||||
|
||||
mCondition.wait(lock);
|
||||
}
|
||||
}
|
||||
|
||||
std::string error;
|
||||
const bool succeeded = WriteSnapshotThroughSink(snapshot, error);
|
||||
bool newerRequestPending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (succeeded)
|
||||
++mWrittenCount;
|
||||
else
|
||||
++mFailedCount;
|
||||
newerRequestPending = PendingCountLocked() > 0;
|
||||
}
|
||||
PublishWriteResult(snapshot, succeeded, error, newerRequestPending);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t PersistenceWriter::PendingCountLocked() const
|
||||
{
|
||||
return mImmediateSnapshots.size() + mDebouncedSnapshots.size();
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "PersistenceRequest.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
struct PersistenceWriterMetrics
|
||||
{
|
||||
std::size_t pendingCount = 0;
|
||||
uint64_t enqueuedCount = 0;
|
||||
uint64_t coalescedCount = 0;
|
||||
uint64_t writtenCount = 0;
|
||||
uint64_t failedCount = 0;
|
||||
};
|
||||
|
||||
struct PersistenceWriteResult
|
||||
{
|
||||
PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState;
|
||||
std::string targetPath;
|
||||
std::string reason;
|
||||
bool succeeded = false;
|
||||
std::string errorMessage;
|
||||
bool newerRequestPending = false;
|
||||
};
|
||||
|
||||
class PersistenceWriter
|
||||
{
|
||||
public:
|
||||
using SnapshotSink = std::function<bool(const PersistenceSnapshot&, std::string&)>;
|
||||
using ResultCallback = std::function<void(const PersistenceWriteResult&)>;
|
||||
|
||||
explicit PersistenceWriter(
|
||||
std::chrono::milliseconds debounceDelay = std::chrono::milliseconds(50),
|
||||
SnapshotSink sink = SnapshotSink());
|
||||
~PersistenceWriter();
|
||||
|
||||
void SetResultCallback(ResultCallback callback);
|
||||
bool WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error);
|
||||
bool EnqueueSnapshot(const PersistenceSnapshot& snapshot, std::string& error);
|
||||
bool StopAndFlush(std::chrono::milliseconds timeout, std::string& error);
|
||||
void StopAndFlush();
|
||||
PersistenceWriterMetrics GetMetrics() const;
|
||||
|
||||
private:
|
||||
struct PendingSnapshot
|
||||
{
|
||||
PersistenceSnapshot snapshot;
|
||||
std::chrono::steady_clock::time_point readyAt;
|
||||
};
|
||||
|
||||
bool ValidateSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const;
|
||||
bool WriteSnapshotThroughSink(const PersistenceSnapshot& snapshot, std::string& error) const;
|
||||
void PublishWriteResult(const PersistenceSnapshot& snapshot, bool succeeded, const std::string& errorMessage, bool newerRequestPending);
|
||||
void StartWorkerLocked();
|
||||
void WorkerMain();
|
||||
std::size_t PendingCountLocked() const;
|
||||
|
||||
std::chrono::milliseconds mDebounceDelay;
|
||||
SnapshotSink mSink;
|
||||
ResultCallback mResultCallback;
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
std::thread mWorker;
|
||||
bool mWorkerRunning = false;
|
||||
bool mStopping = false;
|
||||
bool mAcceptingRequests = true;
|
||||
std::unordered_map<std::string, PendingSnapshot> mDebouncedSnapshots;
|
||||
std::deque<PersistenceSnapshot> mImmediateSnapshots;
|
||||
uint64_t mEnqueuedCount = 0;
|
||||
uint64_t mCoalescedCount = 0;
|
||||
uint64_t mWrittenCount = 0;
|
||||
uint64_t mFailedCount = 0;
|
||||
};
|
||||
@@ -1,170 +0,0 @@
|
||||
#include "RuntimeStateJson.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string ShaderParameterTypeToString(ShaderParameterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ShaderParameterType::Float: return "float";
|
||||
case ShaderParameterType::Vec2: return "vec2";
|
||||
case ShaderParameterType::Color: return "color";
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "enum";
|
||||
case ShaderParameterType::Text: return "text";
|
||||
case ShaderParameterType::Trigger: return "trigger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
for (const std::string& packageId : shaderCatalog.PackageOrder())
|
||||
{
|
||||
ShaderPackage shaderPackage;
|
||||
if (shaderCatalog.CopyPackage(packageId, shaderPackage))
|
||||
packagesById[packageId] = shaderPackage;
|
||||
}
|
||||
return SerializeLayerStack(layerStack.Layers(), packagesById);
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeLayerStack(const std::vector<LayerStackStore::LayerPersistentState>& layerStates, const std::map<std::string, ShaderPackage>& packagesById)
|
||||
{
|
||||
JsonValue layersValue = JsonValue::MakeArray();
|
||||
for (const LayerStackStore::LayerPersistentState& layer : layerStates)
|
||||
{
|
||||
auto shaderIt = packagesById.find(layer.shaderId);
|
||||
if (shaderIt == packagesById.end())
|
||||
continue;
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
|
||||
JsonValue layerValue = JsonValue::MakeObject();
|
||||
layerValue.set("id", JsonValue(layer.id));
|
||||
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
||||
layerValue.set("shaderName", JsonValue(shaderPackage.displayName));
|
||||
layerValue.set("bypass", JsonValue(layer.bypass));
|
||||
if (shaderPackage.temporal.enabled)
|
||||
{
|
||||
JsonValue temporal = JsonValue::MakeObject();
|
||||
temporal.set("enabled", JsonValue(true));
|
||||
temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderPackage.temporal.historySource)));
|
||||
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.requestedHistoryLength)));
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.effectiveHistoryLength)));
|
||||
layerValue.set("temporal", temporal);
|
||||
}
|
||||
if (shaderPackage.feedback.enabled)
|
||||
{
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId));
|
||||
layerValue.set("feedback", feedback);
|
||||
}
|
||||
|
||||
JsonValue parameters = JsonValue::MakeArray();
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
JsonValue parameter = JsonValue::MakeObject();
|
||||
parameter.set("id", JsonValue(definition.id));
|
||||
parameter.set("label", JsonValue(definition.label));
|
||||
if (!definition.description.empty())
|
||||
parameter.set("description", JsonValue(definition.description));
|
||||
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
||||
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
|
||||
|
||||
if (!definition.minNumbers.empty())
|
||||
{
|
||||
JsonValue minValue = JsonValue::MakeArray();
|
||||
for (double number : definition.minNumbers)
|
||||
minValue.pushBack(JsonValue(number));
|
||||
parameter.set("min", minValue);
|
||||
}
|
||||
if (!definition.maxNumbers.empty())
|
||||
{
|
||||
JsonValue maxValue = JsonValue::MakeArray();
|
||||
for (double number : definition.maxNumbers)
|
||||
maxValue.pushBack(JsonValue(number));
|
||||
parameter.set("max", maxValue);
|
||||
}
|
||||
if (!definition.stepNumbers.empty())
|
||||
{
|
||||
JsonValue stepValue = JsonValue::MakeArray();
|
||||
for (double number : definition.stepNumbers)
|
||||
stepValue.pushBack(JsonValue(number));
|
||||
parameter.set("step", stepValue);
|
||||
}
|
||||
if (definition.type == ShaderParameterType::Enum)
|
||||
{
|
||||
JsonValue options = JsonValue::MakeArray();
|
||||
for (const ShaderParameterOption& option : definition.enumOptions)
|
||||
{
|
||||
JsonValue optionValue = JsonValue::MakeObject();
|
||||
optionValue.set("value", JsonValue(option.value));
|
||||
optionValue.set("label", JsonValue(option.label));
|
||||
options.pushBack(optionValue);
|
||||
}
|
||||
parameter.set("options", options);
|
||||
}
|
||||
if (definition.type == ShaderParameterType::Text)
|
||||
{
|
||||
parameter.set("maxLength", JsonValue(static_cast<double>(definition.maxLength)));
|
||||
if (!definition.fontId.empty())
|
||||
parameter.set("font", JsonValue(definition.fontId));
|
||||
}
|
||||
|
||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
auto valueIt = layer.parameterValues.find(definition.id);
|
||||
if (valueIt != layer.parameterValues.end())
|
||||
value = valueIt->second;
|
||||
parameter.set("value", SerializeParameterValue(definition, value));
|
||||
parameters.pushBack(parameter);
|
||||
}
|
||||
|
||||
layerValue.set("parameters", parameters);
|
||||
layersValue.pushBack(layerValue);
|
||||
}
|
||||
return layersValue;
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeParameterValue(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:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Float:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
{
|
||||
JsonValue array = JsonValue::MakeArray();
|
||||
for (double number : value.numberValues)
|
||||
array.pushBack(JsonValue(number));
|
||||
return array;
|
||||
}
|
||||
}
|
||||
return JsonValue();
|
||||
}
|
||||
|
||||
std::string RuntimeStateJson::TemporalHistorySourceToString(TemporalHistorySource source)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case TemporalHistorySource::Source:
|
||||
return "source";
|
||||
case TemporalHistorySource::PreLayerInput:
|
||||
return "preLayerInput";
|
||||
case TemporalHistorySource::None:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "LayerStackStore.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeStateJson
|
||||
{
|
||||
public:
|
||||
static JsonValue SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog);
|
||||
static JsonValue SerializeLayerStack(const std::vector<LayerStackStore::LayerPersistentState>& layers, const std::map<std::string, ShaderPackage>& packagesById);
|
||||
static JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value);
|
||||
static std::string TemporalHistorySourceToString(TemporalHistorySource source);
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
#include "RuntimeStatePresenter.h"
|
||||
|
||||
#include "RuntimeStateJson.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
std::string RuntimeStatePresenter::BuildRuntimeStateJson(const RuntimeStore& runtimeStore)
|
||||
{
|
||||
return SerializeJson(BuildRuntimeStateValue(runtimeStore), true);
|
||||
}
|
||||
|
||||
JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runtimeStore)
|
||||
{
|
||||
const RuntimeStatePresentationReadModel model = runtimeStore.BuildRuntimeStatePresentationReadModel();
|
||||
const HealthTelemetry::Snapshot& telemetrySnapshot = model.telemetry;
|
||||
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
|
||||
JsonValue app = JsonValue::MakeObject();
|
||||
app.set("serverPort", JsonValue(static_cast<double>(model.serverPort)));
|
||||
app.set("oscPort", JsonValue(static_cast<double>(model.config.oscPort)));
|
||||
app.set("oscBindAddress", JsonValue(model.config.oscBindAddress));
|
||||
app.set("oscSmoothing", JsonValue(model.config.oscSmoothing));
|
||||
app.set("autoReload", JsonValue(model.autoReloadEnabled));
|
||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(model.config.maxTemporalHistoryFrames)));
|
||||
app.set("previewFps", JsonValue(static_cast<double>(model.config.previewFps)));
|
||||
app.set("enableExternalKeying", JsonValue(model.config.enableExternalKeying));
|
||||
app.set("inputVideoFormat", JsonValue(model.config.inputVideoFormat));
|
||||
app.set("inputFrameRate", JsonValue(model.config.inputFrameRate));
|
||||
app.set("outputVideoFormat", JsonValue(model.config.outputVideoFormat));
|
||||
app.set("outputFrameRate", JsonValue(model.config.outputFrameRate));
|
||||
root.set("app", app);
|
||||
|
||||
JsonValue runtime = JsonValue::MakeObject();
|
||||
runtime.set("layerCount", JsonValue(static_cast<double>(model.layerStack.LayerCount())));
|
||||
runtime.set("compileSucceeded", JsonValue(model.compileSucceeded));
|
||||
runtime.set("compileMessage", JsonValue(model.compileMessage));
|
||||
root.set("runtime", runtime);
|
||||
|
||||
JsonValue video = JsonValue::MakeObject();
|
||||
video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal));
|
||||
video.set("width", JsonValue(static_cast<double>(telemetrySnapshot.signal.width)));
|
||||
video.set("height", JsonValue(static_cast<double>(telemetrySnapshot.signal.height)));
|
||||
video.set("modeName", JsonValue(telemetrySnapshot.signal.modeName));
|
||||
root.set("video", video);
|
||||
|
||||
JsonValue deckLink = JsonValue::MakeObject();
|
||||
deckLink.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName));
|
||||
deckLink.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying));
|
||||
deckLink.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying));
|
||||
deckLink.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable));
|
||||
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||
root.set("decklink", deckLink);
|
||||
|
||||
JsonValue videoIO = JsonValue::MakeObject();
|
||||
videoIO.set("backend", JsonValue(telemetrySnapshot.videoIO.backendName));
|
||||
videoIO.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName));
|
||||
videoIO.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying));
|
||||
videoIO.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying));
|
||||
videoIO.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable));
|
||||
videoIO.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||
videoIO.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||
videoIO.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||
root.set("videoIO", videoIO);
|
||||
|
||||
JsonValue performance = JsonValue::MakeObject();
|
||||
performance.set("frameBudgetMs", JsonValue(telemetrySnapshot.performance.frameBudgetMilliseconds));
|
||||
performance.set("renderMs", JsonValue(telemetrySnapshot.performance.renderMilliseconds));
|
||||
performance.set("smoothedRenderMs", JsonValue(telemetrySnapshot.performance.smoothedRenderMilliseconds));
|
||||
performance.set("budgetUsedPercent", JsonValue(
|
||||
telemetrySnapshot.performance.frameBudgetMilliseconds > 0.0
|
||||
? (telemetrySnapshot.performance.smoothedRenderMilliseconds / telemetrySnapshot.performance.frameBudgetMilliseconds) * 100.0
|
||||
: 0.0));
|
||||
performance.set("completionIntervalMs", JsonValue(telemetrySnapshot.performance.completionIntervalMilliseconds));
|
||||
performance.set("smoothedCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.smoothedCompletionIntervalMilliseconds));
|
||||
performance.set("maxCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.maxCompletionIntervalMilliseconds));
|
||||
performance.set("lateFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.lateFrameCount)));
|
||||
performance.set("droppedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.droppedFrameCount)));
|
||||
performance.set("flushedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.flushedFrameCount)));
|
||||
root.set("performance", performance);
|
||||
|
||||
JsonValue eventQueue = JsonValue::MakeObject();
|
||||
eventQueue.set("name", JsonValue(telemetrySnapshot.runtimeEvents.queue.queueName));
|
||||
eventQueue.set("depth", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.depth)));
|
||||
eventQueue.set("capacity", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.capacity)));
|
||||
eventQueue.set("droppedCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.droppedCount)));
|
||||
eventQueue.set("oldestEventAgeMs", JsonValue(telemetrySnapshot.runtimeEvents.queue.oldestEventAgeMilliseconds));
|
||||
|
||||
JsonValue eventDispatch = JsonValue::MakeObject();
|
||||
eventDispatch.set("dispatchCallCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.dispatchCallCount)));
|
||||
eventDispatch.set("dispatchedEventCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.dispatchedEventCount)));
|
||||
eventDispatch.set("handlerInvocationCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.handlerInvocationCount)));
|
||||
eventDispatch.set("handlerFailureCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.handlerFailureCount)));
|
||||
eventDispatch.set("lastDispatchDurationMs", JsonValue(telemetrySnapshot.runtimeEvents.dispatch.lastDispatchDurationMilliseconds));
|
||||
eventDispatch.set("maxDispatchDurationMs", JsonValue(telemetrySnapshot.runtimeEvents.dispatch.maxDispatchDurationMilliseconds));
|
||||
|
||||
JsonValue runtimeEvents = JsonValue::MakeObject();
|
||||
runtimeEvents.set("queue", eventQueue);
|
||||
runtimeEvents.set("dispatch", eventDispatch);
|
||||
root.set("runtimeEvents", runtimeEvents);
|
||||
|
||||
JsonValue shaderLibrary = JsonValue::MakeArray();
|
||||
for (const ShaderPackageStatus& status : model.packageStatuses)
|
||||
{
|
||||
JsonValue shader = JsonValue::MakeObject();
|
||||
shader.set("id", JsonValue(status.id));
|
||||
shader.set("name", JsonValue(status.displayName));
|
||||
shader.set("description", JsonValue(status.description));
|
||||
shader.set("category", JsonValue(status.category));
|
||||
shader.set("available", JsonValue(status.available));
|
||||
if (!status.available)
|
||||
shader.set("error", JsonValue(status.error));
|
||||
|
||||
auto shaderIt = model.shaderCatalog.packagesById.find(status.id);
|
||||
if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.temporal.enabled)
|
||||
{
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
JsonValue temporal = JsonValue::MakeObject();
|
||||
temporal.set("enabled", JsonValue(true));
|
||||
temporal.set("historySource", JsonValue(RuntimeStateJson::TemporalHistorySourceToString(shaderPackage.temporal.historySource)));
|
||||
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.requestedHistoryLength)));
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.effectiveHistoryLength)));
|
||||
shader.set("temporal", temporal);
|
||||
}
|
||||
if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.feedback.enabled)
|
||||
{
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId));
|
||||
shader.set("feedback", feedback);
|
||||
}
|
||||
shaderLibrary.pushBack(shader);
|
||||
}
|
||||
root.set("shaders", shaderLibrary);
|
||||
|
||||
JsonValue stackPresets = JsonValue::MakeArray();
|
||||
for (const std::string& presetName : model.stackPresetNames)
|
||||
stackPresets.pushBack(JsonValue(presetName));
|
||||
root.set("stackPresets", stackPresets);
|
||||
|
||||
root.set("layers", RuntimeStateJson::SerializeLayerStack(model.layerStack.Layers(), model.shaderCatalog.packagesById));
|
||||
return root;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class RuntimeStore;
|
||||
|
||||
class RuntimeStatePresenter
|
||||
{
|
||||
public:
|
||||
static std::string BuildRuntimeStateJson(const RuntimeStore& runtimeStore);
|
||||
static JsonValue BuildRuntimeStateValue(const RuntimeStore& runtimeStore);
|
||||
};
|
||||
@@ -1,193 +0,0 @@
|
||||
#include "RenderSnapshotBuilder.h"
|
||||
|
||||
#include "RuntimeClock.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "ShaderCompiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
RenderSnapshotBuilder::RenderSnapshotBuilder(RuntimeStore& runtimeStore) :
|
||||
mRuntimeStore(runtimeStore)
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
|
||||
{
|
||||
try
|
||||
{
|
||||
ShaderPackage shaderPackage;
|
||||
if (!mRuntimeStore.CopyShaderPackageForStoredLayer(layerId, shaderPackage, error))
|
||||
return false;
|
||||
|
||||
const ShaderCompilerInputs inputs = mRuntimeStore.GetShaderCompilerInputs();
|
||||
|
||||
ShaderCompiler compiler(
|
||||
inputs.repoRoot,
|
||||
inputs.wrapperPath,
|
||||
inputs.generatedGlslPath,
|
||||
inputs.patchedGlslPath,
|
||||
inputs.maxTemporalHistoryFrames);
|
||||
passSources.clear();
|
||||
passSources.reserve(shaderPackage.passes.size());
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
ShaderPassBuildSource passSource;
|
||||
passSource.passId = pass.id;
|
||||
passSource.inputNames = pass.inputNames;
|
||||
passSource.outputName = pass.outputName;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error))
|
||||
return false;
|
||||
passSources.push_back(std::move(passSource));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned RenderSnapshotBuilder::GetMaxTemporalHistoryFrames() const
|
||||
{
|
||||
return mRuntimeStore.GetConfiguredMaxTemporalHistoryFrames();
|
||||
}
|
||||
|
||||
RuntimeSnapshotVersions RenderSnapshotBuilder::GetVersions() const
|
||||
{
|
||||
RuntimeSnapshotVersions versions;
|
||||
versions.renderStateVersion = mRenderStateVersion.load(std::memory_order_relaxed);
|
||||
versions.parameterStateVersion = mParameterStateVersion.load(std::memory_order_relaxed);
|
||||
return versions;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::AdvanceFrame()
|
||||
{
|
||||
++mFrameCounter;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states);
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
RefreshLayerParameters(mRuntimeStore.CopyCommittedLiveLayerStates(), states);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
RefreshDynamicRenderStateFields(mRuntimeStore.GetRenderTimingSnapshot(), states);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::MarkRenderStateDirty()
|
||||
{
|
||||
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::MarkParameterStateDirty()
|
||||
{
|
||||
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
states.clear();
|
||||
|
||||
for (const LayerStackStore::LayerPersistentState& layer : readModel.committedLiveState.layers)
|
||||
{
|
||||
auto shaderIt = readModel.committedLiveState.packagesById.find(layer.shaderId);
|
||||
if (shaderIt == readModel.committedLiveState.packagesById.end())
|
||||
continue;
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
|
||||
RuntimeRenderState state;
|
||||
state.layerId = layer.id;
|
||||
state.shaderId = layer.shaderId;
|
||||
state.shaderName = shaderPackage.displayName;
|
||||
state.mixAmount = 1.0;
|
||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||
state.inputWidth = readModel.signalStatus.width;
|
||||
state.inputHeight = readModel.signalStatus.height;
|
||||
state.outputWidth = outputWidth;
|
||||
state.outputHeight = outputHeight;
|
||||
state.parameterDefinitions = shaderPackage.parameters;
|
||||
state.textureAssets = shaderPackage.textureAssets;
|
||||
state.fontAssets = shaderPackage.fontAssets;
|
||||
state.isTemporal = shaderPackage.temporal.enabled;
|
||||
state.temporalHistorySource = shaderPackage.temporal.historySource;
|
||||
state.requestedTemporalHistoryLength = shaderPackage.temporal.requestedHistoryLength;
|
||||
state.effectiveTemporalHistoryLength = shaderPackage.temporal.effectiveHistoryLength;
|
||||
state.feedback = shaderPackage.feedback;
|
||||
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
auto valueIt = layer.parameterValues.find(definition.id);
|
||||
if (valueIt != layer.parameterValues.end())
|
||||
value = valueIt->second;
|
||||
state.parameterValues[definition.id] = value;
|
||||
}
|
||||
|
||||
states.push_back(state);
|
||||
}
|
||||
|
||||
RefreshDynamicRenderStateFields(readModel.timing, states);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshLayerParameters(const std::vector<LayerStackStore::LayerPersistentState>& layers, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
for (RuntimeRenderState& state : states)
|
||||
{
|
||||
const auto layerIt = std::find_if(layers.begin(), layers.end(),
|
||||
[&state](const LayerStackStore::LayerPersistentState& layer) { return layer.id == state.layerId; });
|
||||
if (layerIt == layers.end())
|
||||
continue;
|
||||
|
||||
state.bypass = layerIt->bypass ? 1.0 : 0.0;
|
||||
state.parameterValues.clear();
|
||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||
{
|
||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
auto valueIt = layerIt->parameterValues.find(definition.id);
|
||||
if (valueIt != layerIt->parameterValues.end())
|
||||
value = valueIt->second;
|
||||
state.parameterValues[definition.id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - timing.startTime).count();
|
||||
const double frameCount = static_cast<double>(mFrameCounter.load(std::memory_order_relaxed));
|
||||
|
||||
for (RuntimeRenderState& state : states)
|
||||
{
|
||||
state.timeSeconds = timeSeconds;
|
||||
state.utcTimeSeconds = clock.utcTimeSeconds;
|
||||
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
||||
state.startupRandom = timing.startupRandom;
|
||||
state.frameCount = frameCount;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeStoreReadModels.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeStore;
|
||||
|
||||
struct RuntimeSnapshotVersions
|
||||
{
|
||||
uint64_t renderStateVersion = 0;
|
||||
uint64_t parameterStateVersion = 0;
|
||||
};
|
||||
|
||||
class RenderSnapshotBuilder
|
||||
{
|
||||
public:
|
||||
explicit RenderSnapshotBuilder(RuntimeStore& runtimeStore);
|
||||
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
|
||||
unsigned GetMaxTemporalHistoryFrames() const;
|
||||
RuntimeSnapshotVersions GetVersions() const;
|
||||
void AdvanceFrame();
|
||||
void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
bool TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
void MarkRenderStateDirty();
|
||||
void MarkParameterStateDirty();
|
||||
|
||||
private:
|
||||
void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshLayerParameters(const std::vector<LayerStackStore::LayerPersistentState>& layers, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector<RuntimeRenderState>& states) const;
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
std::atomic<uint64_t> mFrameCounter{ 0 };
|
||||
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
||||
std::atomic<uint64_t> mParameterStateVersion{ 0 };
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
RuntimeSnapshotProvider::RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRenderSnapshotBuilder(renderSnapshotBuilder),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
|
||||
{
|
||||
try
|
||||
{
|
||||
return mRenderSnapshotBuilder.BuildLayerPassFragmentShaderSources(layerId, passSources, error);
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const
|
||||
{
|
||||
return mRenderSnapshotBuilder.GetMaxTemporalHistoryFrames();
|
||||
}
|
||||
|
||||
RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const
|
||||
{
|
||||
return mRenderSnapshotBuilder.GetVersions();
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::AdvanceFrame()
|
||||
{
|
||||
mRenderSnapshotBuilder.AdvanceFrame();
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const
|
||||
{
|
||||
PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "publish-render-state-snapshot");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
||||
RuntimeRenderStateSnapshot publishedSnapshot;
|
||||
if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, publishedSnapshot))
|
||||
{
|
||||
PublishRenderSnapshotPublished(publishedSnapshot);
|
||||
return publishedSnapshot;
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot snapshot;
|
||||
snapshot.outputWidth = outputWidth;
|
||||
snapshot.outputHeight = outputHeight;
|
||||
mRenderSnapshotBuilder.BuildLayerRenderStates(outputWidth, outputHeight, snapshot.states);
|
||||
|
||||
const RuntimeSnapshotVersions versionsAfter = GetVersions();
|
||||
if (versionsBefore.renderStateVersion == versionsAfter.renderStateVersion &&
|
||||
versionsBefore.parameterStateVersion == versionsAfter.parameterStateVersion)
|
||||
{
|
||||
snapshot.versions = versionsAfter;
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "try-publish-render-state-snapshot");
|
||||
|
||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
||||
if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, snapshot))
|
||||
{
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeRenderState> states;
|
||||
if (!mRenderSnapshotBuilder.TryBuildLayerRenderStates(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);
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryRefreshPublishedSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
const uint64_t expectedRenderStateVersion = snapshot.versions.renderStateVersion;
|
||||
if (!mRenderSnapshotBuilder.TryRefreshLayerParameters(snapshot.states))
|
||||
return false;
|
||||
|
||||
const RuntimeSnapshotVersions versions = GetVersions();
|
||||
if (versions.renderStateVersion != expectedRenderStateVersion)
|
||||
return false;
|
||||
|
||||
snapshot.versions = versions;
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
mRenderSnapshotBuilder.RefreshDynamicRenderStateFields(states);
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions, RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPublishedSnapshotMutex);
|
||||
if (!mHasPublishedRenderStateSnapshot ||
|
||||
!SnapshotMatches(mPublishedRenderStateSnapshot, outputWidth, outputHeight, versions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = mPublishedRenderStateSnapshot;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::StorePublishedRenderStateSnapshot(const RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPublishedSnapshotMutex);
|
||||
mPublishedRenderStateSnapshot = snapshot;
|
||||
mHasPublishedRenderStateSnapshot = true;
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions)
|
||||
{
|
||||
return snapshot.outputWidth == outputWidth &&
|
||||
snapshot.outputHeight == outputHeight &&
|
||||
snapshot.versions.renderStateVersion == versions.renderStateVersion &&
|
||||
snapshot.versions.parameterStateVersion == versions.parameterStateVersion;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderSnapshotPublishRequestedEvent event;
|
||||
event.outputWidth = outputWidth;
|
||||
event.outputHeight = outputHeight;
|
||||
event.reason = reason;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider"))
|
||||
OutputDebugStringA("RenderSnapshotPublishRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RenderSnapshotPublishRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderSnapshotPublishedEvent event;
|
||||
event.snapshotVersion = snapshot.versions.renderStateVersion;
|
||||
event.structureVersion = snapshot.versions.renderStateVersion;
|
||||
event.parameterVersion = snapshot.versions.parameterStateVersion;
|
||||
event.outputWidth = snapshot.outputWidth;
|
||||
event.outputHeight = snapshot.outputHeight;
|
||||
event.layerCount = snapshot.states.size();
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider"))
|
||||
OutputDebugStringA("RenderSnapshotPublished event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RenderSnapshotPublished event publish threw.\n");
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderSnapshotBuilder.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
struct RuntimeRenderStateSnapshot
|
||||
{
|
||||
RuntimeSnapshotVersions versions;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::vector<RuntimeRenderState> states;
|
||||
};
|
||||
|
||||
class RuntimeSnapshotProvider
|
||||
{
|
||||
public:
|
||||
RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
|
||||
unsigned GetMaxTemporalHistoryFrames() const;
|
||||
RuntimeSnapshotVersions GetVersions() const;
|
||||
void AdvanceFrame();
|
||||
RuntimeRenderStateSnapshot PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const;
|
||||
bool TryPublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const;
|
||||
bool TryRefreshPublishedSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
|
||||
private:
|
||||
bool TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions, RuntimeRenderStateSnapshot& snapshot) const;
|
||||
void StorePublishedRenderStateSnapshot(const RuntimeRenderStateSnapshot& snapshot) const;
|
||||
static bool SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions);
|
||||
void PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const;
|
||||
void PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const;
|
||||
|
||||
RenderSnapshotBuilder& mRenderSnapshotBuilder;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
mutable std::mutex mPublishedSnapshotMutex;
|
||||
mutable bool mHasPublishedRenderStateSnapshot = false;
|
||||
mutable RuntimeRenderStateSnapshot mPublishedRenderStateSnapshot;
|
||||
};
|
||||
@@ -1,738 +0,0 @@
|
||||
#include "LayerStackStore.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStateJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string TrimCopy(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);
|
||||
}
|
||||
|
||||
std::string SimplifyControlKey(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 MatchesControlKey(const std::string& candidate, const std::string& key)
|
||||
{
|
||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||
}
|
||||
|
||||
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||
{
|
||||
const std::string prefix = "layer-";
|
||||
if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size())
|
||||
return false;
|
||||
|
||||
uint64_t parsed = 0;
|
||||
for (std::size_t index = prefix.size(); index < layerId.size(); ++index)
|
||||
{
|
||||
const unsigned char ch = static_cast<unsigned char>(layerId[index]);
|
||||
if (!std::isdigit(ch))
|
||||
return false;
|
||||
parsed = parsed * 10 + static_cast<uint64_t>(ch - '0');
|
||||
}
|
||||
|
||||
number = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LayerStackStore::LoadPersistentStateValue(const JsonValue& root)
|
||||
{
|
||||
if (const JsonValue* layersValue = root.find("layers"))
|
||||
{
|
||||
for (const JsonValue& layerValue : layersValue->asArray())
|
||||
{
|
||||
if (!layerValue.isObject())
|
||||
continue;
|
||||
LayerPersistentState layer;
|
||||
if (const JsonValue* idValue = layerValue.find("id"))
|
||||
layer.id = idValue->asString();
|
||||
if (const JsonValue* shaderIdValue = layerValue.find("shaderId"))
|
||||
layer.shaderId = shaderIdValue->asString();
|
||||
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
||||
layer.bypass = bypassValue->asBoolean(false);
|
||||
else if (const JsonValue* enabledValue = layerValue.find("enabled"))
|
||||
layer.bypass = !enabledValue->asBoolean(true);
|
||||
|
||||
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
|
||||
{
|
||||
for (const auto& parameterItem : parameterValues->asObject())
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
const JsonValue& jsonValue = parameterItem.second;
|
||||
if (jsonValue.isBoolean())
|
||||
value.booleanValue = jsonValue.asBoolean();
|
||||
else if (jsonValue.isString())
|
||||
value.enumValue = jsonValue.asString();
|
||||
else if (jsonValue.isNumber())
|
||||
value.numberValues.push_back(jsonValue.asNumber());
|
||||
else if (jsonValue.isArray())
|
||||
value.numberValues = JsonArrayToNumbers(jsonValue);
|
||||
layer.parameterValues[parameterItem.first] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!layer.shaderId.empty())
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string activeShaderId;
|
||||
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
|
||||
activeShaderId = activeShaderValue->asString();
|
||||
|
||||
if (!activeShaderId.empty())
|
||||
{
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = activeShaderId;
|
||||
layer.bypass = false;
|
||||
|
||||
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
|
||||
{
|
||||
const JsonValue* shaderValues = valuesByShader->find(activeShaderId);
|
||||
if (shaderValues)
|
||||
{
|
||||
for (const auto& parameterItem : shaderValues->asObject())
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
const JsonValue& jsonValue = parameterItem.second;
|
||||
if (jsonValue.isBoolean())
|
||||
value.booleanValue = jsonValue.asBoolean();
|
||||
else if (jsonValue.isString())
|
||||
value.enumValue = jsonValue.asString();
|
||||
else if (jsonValue.isNumber())
|
||||
value.numberValues.push_back(jsonValue.asNumber());
|
||||
else if (jsonValue.isArray())
|
||||
value.numberValues = JsonArrayToNumbers(jsonValue);
|
||||
layer.parameterValues[parameterItem.first] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonValue LayerStackStore::BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const
|
||||
{
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
JsonValue layers = JsonValue::MakeArray();
|
||||
for (const LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
JsonValue layerValue = JsonValue::MakeObject();
|
||||
layerValue.set("id", JsonValue(layer.id));
|
||||
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
||||
layerValue.set("bypass", JsonValue(layer.bypass));
|
||||
|
||||
JsonValue parameterValues = JsonValue::MakeObject();
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
for (const auto& parameterItem : layer.parameterValues)
|
||||
{
|
||||
const ShaderParameterDefinition* definition = nullptr;
|
||||
if (shaderPackage)
|
||||
{
|
||||
for (const ShaderParameterDefinition& candidate : shaderPackage->parameters)
|
||||
{
|
||||
if (candidate.id == parameterItem.first)
|
||||
{
|
||||
definition = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (definition)
|
||||
parameterValues.set(parameterItem.first, RuntimeStateJson::SerializeParameterValue(*definition, parameterItem.second));
|
||||
}
|
||||
|
||||
layerValue.set("parameterValues", parameterValues);
|
||||
layers.pushBack(layerValue);
|
||||
}
|
||||
root.set("layers", layers);
|
||||
return root;
|
||||
}
|
||||
|
||||
void LayerStackStore::NormalizeLayerIds()
|
||||
{
|
||||
std::set<std::string> usedIds;
|
||||
uint64_t maxLayerNumber = mNextLayerId;
|
||||
|
||||
for (LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
uint64_t layerNumber = 0;
|
||||
const bool hasReusableId = !layer.id.empty() &&
|
||||
usedIds.find(layer.id) == usedIds.end() &&
|
||||
TryParseLayerIdNumber(layer.id, layerNumber);
|
||||
|
||||
if (hasReusableId)
|
||||
{
|
||||
usedIds.insert(layer.id);
|
||||
maxLayerNumber = (std::max)(maxLayerNumber, layerNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
++maxLayerNumber;
|
||||
layer.id = "layer-" + std::to_string(maxLayerNumber);
|
||||
}
|
||||
while (usedIds.find(layer.id) != usedIds.end());
|
||||
|
||||
usedIds.insert(layer.id);
|
||||
}
|
||||
|
||||
mNextLayerId = maxLayerNumber;
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
for (LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
if (shaderPackage)
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
}
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
if (!mLayers.empty() || shaderCatalog.PackageOrder().empty())
|
||||
return;
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = shaderCatalog.PackageOrder().front();
|
||||
layer.bypass = false;
|
||||
if (const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId))
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
|
||||
void LayerStackStore::RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
for (auto it = mLayers.begin(); it != mLayers.end();)
|
||||
{
|
||||
if (!shaderCatalog.HasPackage(it->shaderId))
|
||||
it = mLayers.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerStackStore::CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = shaderId;
|
||||
layer.bypass = false;
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
mLayers.push_back(layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::DeleteLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
mLayers.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::ptrdiff_t index = std::distance(mLayers.begin(), it);
|
||||
const std::ptrdiff_t newIndex = index + direction;
|
||||
if (newIndex < 0 || newIndex >= static_cast<std::ptrdiff_t>(mLayers.size()))
|
||||
return true;
|
||||
|
||||
std::swap(mLayers[index], mLayers[newIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mLayers.empty())
|
||||
return true;
|
||||
|
||||
if (targetIndex >= mLayers.size())
|
||||
targetIndex = mLayers.size() - 1;
|
||||
|
||||
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mLayers.begin(), it));
|
||||
if (sourceIndex == targetIndex)
|
||||
return true;
|
||||
|
||||
LayerPersistentState movedLayer = *it;
|
||||
mLayers.erase(mLayers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
||||
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->bypass = bypassed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->shaderId = shaderId;
|
||||
layer->parameterValues.clear();
|
||||
EnsureLayerDefaults(*layer, *shaderPackage);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues[parameterId] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + layer->shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues.clear();
|
||||
EnsureLayerDefaults(*layer, *shaderPackage);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::HasLayer(const std::string& layerId) const
|
||||
{
|
||||
return FindLayerById(layerId) != nullptr;
|
||||
}
|
||||
|
||||
bool LayerStackStore::TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
const LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + layer->shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto parameterIt = std::find_if(shaderPackage->parameters.begin(), shaderPackage->parameters.end(),
|
||||
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
||||
if (parameterIt == shaderPackage->parameters.end())
|
||||
{
|
||||
error = "Unknown parameter id: " + parameterId;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = StoredParameterSnapshot();
|
||||
snapshot.layerId = layer->id;
|
||||
snapshot.definition = *parameterIt;
|
||||
auto valueIt = layer->parameterValues.find(parameterIt->id);
|
||||
if (valueIt != layer->parameterValues.end())
|
||||
{
|
||||
snapshot.currentValue = valueIt->second;
|
||||
snapshot.hasCurrentValue = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
const LayerPersistentState* matchedLayer = nullptr;
|
||||
const ShaderPackage* matchedPackage = nullptr;
|
||||
|
||||
for (const LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
if (!shaderPackage)
|
||||
continue;
|
||||
|
||||
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderPackage->id, layerKey) ||
|
||||
MatchesControlKey(shaderPackage->displayName, layerKey))
|
||||
{
|
||||
matchedLayer = &layer;
|
||||
matchedPackage = shaderPackage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedLayer || !matchedPackage)
|
||||
{
|
||||
error = "Unknown OSC layer key: " + layerKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
|
||||
[¶meterKey](const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
|
||||
});
|
||||
if (parameterIt == matchedPackage->parameters.end())
|
||||
{
|
||||
error = "Unknown OSC parameter key: " + parameterKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = StoredParameterSnapshot();
|
||||
snapshot.layerId = matchedLayer->id;
|
||||
snapshot.definition = *parameterIt;
|
||||
auto valueIt = matchedLayer->parameterValues.find(parameterIt->id);
|
||||
if (valueIt != matchedLayer->parameterValues.end())
|
||||
{
|
||||
snapshot.currentValue = valueIt->second;
|
||||
snapshot.hasCurrentValue = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::ptrdiff_t index = std::distance(mLayers.begin(), it);
|
||||
const std::ptrdiff_t newIndex = index + direction;
|
||||
shouldMove = newIndex >= 0 && newIndex < static_cast<std::ptrdiff_t>(mLayers.size()) && newIndex != index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mLayers.empty())
|
||||
{
|
||||
shouldMove = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t clampedTargetIndex = (std::min)(targetIndex, mLayers.size() - 1);
|
||||
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mLayers.begin(), it));
|
||||
shouldMove = sourceIndex != clampedTargetIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonValue LayerStackStore::BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const
|
||||
{
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
root.set("version", JsonValue(1.0));
|
||||
root.set("name", JsonValue(TrimCopy(presetName)));
|
||||
root.set("layers", RuntimeStateJson::SerializeLayerStack(*this, shaderCatalog));
|
||||
return root;
|
||||
}
|
||||
|
||||
bool LayerStackStore::LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error)
|
||||
{
|
||||
const JsonValue* layersValue = root.find("layers");
|
||||
if (!layersValue || !layersValue->isArray())
|
||||
{
|
||||
error = "Preset file is missing a valid 'layers' array.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<LayerPersistentState> nextLayers;
|
||||
uint64_t nextLayerId = mNextLayerId;
|
||||
if (!DeserializeLayerStack(shaderCatalog, *layersValue, nextLayers, nextLayerId, error))
|
||||
return false;
|
||||
|
||||
if (nextLayers.empty())
|
||||
{
|
||||
error = "Preset does not contain any valid layers.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mLayers = std::move(nextLayers);
|
||||
mNextLayerId = nextLayerId;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string LayerStackStore::MakeSafePresetFileStem(const std::string& presetName)
|
||||
{
|
||||
return ::MakeSafePresetFileStem(presetName);
|
||||
}
|
||||
|
||||
const std::vector<LayerStackStore::LayerPersistentState>& LayerStackStore::Layers() const
|
||||
{
|
||||
return mLayers;
|
||||
}
|
||||
|
||||
std::vector<LayerStackStore::LayerPersistentState>& LayerStackStore::Layers()
|
||||
{
|
||||
return mLayers;
|
||||
}
|
||||
|
||||
std::size_t LayerStackStore::LayerCount() const
|
||||
{
|
||||
return mLayers.size();
|
||||
}
|
||||
|
||||
const LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
return it == mLayers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
return it == mLayers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
ShaderParameterValue LayerStackStore::DefaultValueForDefinition(const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return ::DefaultValueForDefinition(definition);
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
auto valueIt = layerState.parameterValues.find(definition.id);
|
||||
if (valueIt == layerState.parameterValues.end())
|
||||
{
|
||||
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonValue valueJson;
|
||||
bool shouldNormalize = true;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
if (valueIt->second.numberValues.empty())
|
||||
shouldNormalize = false;
|
||||
else
|
||||
valueJson = JsonValue(valueIt->second.numberValues.front());
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
valueJson = JsonValue::MakeArray();
|
||||
for (double number : valueIt->second.numberValues)
|
||||
valueJson.pushBack(JsonValue(number));
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
valueJson = JsonValue(valueIt->second.booleanValue);
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
valueJson = JsonValue(valueIt->second.enumValue);
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
{
|
||||
const std::string textValue = !valueIt->second.textValue.empty()
|
||||
? valueIt->second.textValue
|
||||
: valueIt->second.enumValue;
|
||||
if (textValue.empty())
|
||||
{
|
||||
valueIt->second = DefaultValueForDefinition(definition);
|
||||
shouldNormalize = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
valueJson = JsonValue(textValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ShaderParameterType::Trigger:
|
||||
if (valueIt->second.numberValues.empty())
|
||||
valueJson = JsonValue(0.0);
|
||||
else
|
||||
valueJson = JsonValue((std::max)(0.0, std::floor(valueIt->second.numberValues.front())));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!shouldNormalize)
|
||||
continue;
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
std::string normalizeError;
|
||||
if (NormalizeAndValidateParameterValue(definition, valueJson, normalizedValue, normalizeError))
|
||||
valueIt->second = normalizedValue;
|
||||
else
|
||||
valueIt->second = DefaultValueForDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerStackStore::DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId, std::string& error)
|
||||
{
|
||||
for (const JsonValue& layerValue : layersValue.asArray())
|
||||
{
|
||||
if (!layerValue.isObject())
|
||||
continue;
|
||||
|
||||
const JsonValue* shaderIdValue = layerValue.find("shaderId");
|
||||
if (!shaderIdValue)
|
||||
continue;
|
||||
|
||||
const std::string shaderId = shaderIdValue->asString();
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Preset references unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(layers, nextLayerId);
|
||||
layer.shaderId = shaderId;
|
||||
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
||||
layer.bypass = bypassValue->asBoolean(false);
|
||||
|
||||
if (const JsonValue* parametersValue = layerValue.find("parameters"))
|
||||
{
|
||||
for (const JsonValue& parameterValue : parametersValue->asArray())
|
||||
{
|
||||
if (!parameterValue.isObject())
|
||||
continue;
|
||||
|
||||
const JsonValue* parameterIdValue = parameterValue.find("id");
|
||||
const JsonValue* valueValue = parameterValue.find("value");
|
||||
if (!parameterIdValue || !valueValue)
|
||||
continue;
|
||||
|
||||
const std::string parameterId = parameterIdValue->asString();
|
||||
auto definitionIt = std::find_if(shaderPackage->parameters.begin(), shaderPackage->parameters.end(),
|
||||
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
||||
if (definitionIt == shaderPackage->parameters.end())
|
||||
continue;
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, *valueValue, normalizedValue, error))
|
||||
return false;
|
||||
|
||||
layer.parameterValues[parameterId] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
layers.push_back(layer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string LayerStackStore::GenerateLayerId(std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
++nextLayerId;
|
||||
const std::string candidate = "layer-" + std::to_string(nextLayerId);
|
||||
auto it = std::find_if(layers.begin(), layers.end(),
|
||||
[&candidate](const LayerPersistentState& layer) { return layer.id == candidate; });
|
||||
if (it == layers.end())
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class LayerStackStore
|
||||
{
|
||||
public:
|
||||
struct LayerPersistentState
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct StoredParameterSnapshot
|
||||
{
|
||||
std::string layerId;
|
||||
ShaderParameterDefinition definition;
|
||||
ShaderParameterValue currentValue;
|
||||
bool hasCurrentValue = false;
|
||||
};
|
||||
|
||||
bool LoadPersistentStateValue(const JsonValue& root);
|
||||
JsonValue BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const;
|
||||
void NormalizeLayerIds();
|
||||
void EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog);
|
||||
void EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog);
|
||||
void RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog);
|
||||
|
||||
bool CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||
bool DeleteLayer(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 SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error);
|
||||
bool ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error);
|
||||
|
||||
bool HasLayer(const std::string& layerId) const;
|
||||
bool TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
|
||||
JsonValue BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const;
|
||||
bool LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error);
|
||||
static std::string MakeSafePresetFileStem(const std::string& presetName);
|
||||
|
||||
const std::vector<LayerPersistentState>& Layers() const;
|
||||
std::vector<LayerPersistentState>& Layers();
|
||||
std::size_t LayerCount() const;
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||
|
||||
private:
|
||||
static ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition);
|
||||
static void EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage);
|
||||
static bool DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId, std::string& error);
|
||||
static std::string GenerateLayerId(std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId);
|
||||
|
||||
std::vector<LayerPersistentState> mLayers;
|
||||
uint64_t mNextLayerId = 0;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user