Compare commits
290 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfba5dbc40 | |||
| 659fbef1a5 | |||
| 8a6bb81a37 | |||
| b7ce079a26 | |||
| 171b790fa3 | |||
|
|
27690c3afa | ||
|
|
a6d2ee385e | ||
|
|
fce3c3c5ae | ||
|
|
56883439a6 | ||
|
|
fbc2851ccb | ||
|
|
067c606092 | ||
|
|
0f3db3ba1b | ||
|
|
04e0802ef2 | ||
|
|
aa33d72b6e | ||
|
|
216a561ede | ||
|
|
2b995ac058 | ||
|
|
8ffc011ca0 | ||
|
|
f0f8b080ca | ||
|
|
d0b1f63524 | ||
| cfb796756d | |||
| e88a4a1f3b | |||
| 4bfabaca88 | |||
| 16548d54d3 | |||
| b96b32b441 | |||
| 3c3b1d68ff | |||
| c35ca8d61c | |||
| be9f3b4e8b | |||
| af448c338c | |||
| 283f38dddb | |||
| c5f0a9df0e | |||
| 80c6fd2434 | |||
| f8c3c60611 | |||
| 93aa8fa981 | |||
| 4e6b37304f | |||
| cec8b76f61 | |||
| 521b3cb09e | |||
| 3bd6aeb52f | |||
| e006fcc6ee | |||
| 2058f94193 | |||
| 6e8f18e24c | |||
| 2fdb1741f9 | |||
| d831b418d7 | |||
| e6faaee1ca | |||
| 64a6125c3f | |||
| 315cbda9d1 | |||
| 9787ca5f27 | |||
| 7bf5464fd2 | |||
| b7e7452567 | |||
| 35801601a5 | |||
| 0b6a2300ea | |||
| 084e60cbe0 | |||
| f2ff69fe90 | |||
| 4ddb5b6428 | |||
| 108edc096e | |||
| 269dbd0079 | |||
| 4096e9c26a | |||
| f9aac85e5f | |||
| 5cf1a09e75 | |||
| 3fc78d5bb8 | |||
| 5c46eaf18a | |||
| d68cf9b1a0 | |||
| bda9a9dc22 | |||
| c2de2c3738 | |||
| dc247ab58d | |||
| df0a77ef01 | |||
| 09efe2d6a0 | |||
| a9eeed30cf | |||
| e43ac21b2f | |||
| 081364e764 | |||
| f589b1e1fe | |||
| 7e17315e74 | |||
| bfaa3f5e0e | |||
| 1d4eb7a34c | |||
| f461a05c65 | |||
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 | ||
|
|
3e45bba54b | ||
|
|
fd4b70ec9c | ||
|
|
ce28904891 | ||
|
|
2c5e925b97 | ||
|
|
957c0be05a | ||
|
|
0a8b335048 | ||
|
|
6e32941675 | ||
|
|
5fb4607d8c | ||
|
|
f43b6f6519 | ||
|
|
dfd49fd0e3 | ||
|
|
1429b2e660 | ||
|
|
02b221f481 | ||
|
|
6a33bd02ab | ||
|
|
da7e1a93f6 | ||
|
|
334693f28c | ||
|
|
c5fd8e72b4 | ||
|
|
95b4a54326 | ||
|
|
d07ea1f63a | ||
|
|
1ddcf5d621 | ||
|
|
38d729b346 | ||
|
|
4b62627479 | ||
|
|
430cf0733d | ||
|
|
b44504500a | ||
|
|
bc690e2a87 | ||
|
|
9938a6cc26 | ||
|
|
79f7ac6c86 | ||
|
|
44b198b14d | ||
|
|
511b67c9bc | ||
|
|
c0d7e84495 | ||
|
|
4ea829af85 | ||
|
|
e0ca548ef5 | ||
|
|
2531d871e8 | ||
|
|
709d3d3fa4 | ||
|
|
ea31d0ca13 | ||
|
|
f1f4e3421b | ||
|
|
ac729dc2b9 | ||
|
|
bf23cd880a | ||
|
|
9e3412712c | ||
|
|
a434a88108 | ||
|
|
c5cead6003 | ||
|
|
f8adbbe0fe | ||
|
|
0a7954e879 | ||
|
|
f288455709 | ||
|
|
50d5880835 | ||
|
|
52eaf16a8c | ||
|
|
6b0638336a | ||
|
|
0da6ad6802 | ||
|
|
dd3cd6b66c | ||
|
|
1d08dec5fe | ||
|
|
0d57920bc1 | ||
|
|
1629dbc77a | ||
|
|
205c90e52e | ||
|
|
ab38bfad24 | ||
|
|
68503256dc | ||
|
|
a91cc91a21 | ||
|
|
a530325fa1 | ||
|
|
d332dceb5b | ||
|
|
79855d788c | ||
|
|
ff10b66d1d | ||
|
|
fdcc38c6ae | ||
|
|
718e4dcadd | ||
|
|
7740fe209c | ||
|
|
77590f4a62 | ||
|
|
e8a3805fff | ||
|
|
99fd903144 | ||
|
|
761df3b2d0 | ||
|
|
f141d20026 | ||
|
|
bfc32c4a1e | ||
|
|
20476bdf63 | ||
|
|
0ec5a4cfed | ||
|
|
539fcd3351 | ||
|
|
ebc10a9925 | ||
|
|
e5c5920ccd | ||
|
|
3b641dd07a | ||
|
|
e00e2574ed | ||
|
|
e459155d51 | ||
|
|
06f3dd4942 | ||
|
|
0808171677 | ||
|
|
00b6ad4c36 | ||
|
|
d4f6a4a268 | ||
|
|
6e600be112 | ||
|
|
a9b08f7f27 | ||
|
|
ccfc0237fd | ||
|
|
b3705d96cc | ||
|
|
5503ce85a9 | ||
|
|
41677b71ec | ||
|
|
9cbb5d8004 | ||
|
|
cbf1b541dc | ||
|
|
5cbdbd6813 | ||
|
|
b2369c418b | ||
|
|
c4883d3413 | ||
|
|
53e78890a8 | ||
|
|
36b398ea95 | ||
|
|
ba4643dfa3 | ||
|
|
27dbb55f7b | ||
|
|
f6b26bf28b | ||
|
|
861593123d | ||
|
|
34c145e80b | ||
|
|
a24cdc0630 | ||
|
|
120f899b0d | ||
|
|
41075bbc61 | ||
|
|
7f0f60c0e3 | ||
|
|
739231d5a1 | ||
|
|
3629227aa9 | ||
|
|
618831d578 | ||
|
|
c38c22834d | ||
|
|
c8a4bd4c7b | ||
|
|
46129a6044 | ||
|
|
8fcb51d140 | ||
|
|
944773c248 | ||
|
|
7777cfc194 | ||
|
|
198639ae3f | ||
|
|
d7ca42b51b | ||
|
|
f11d531e0c | ||
|
|
a3635b5d31 | ||
|
|
bc9aa6fbad | ||
|
|
0c16665610 | ||
|
|
46f2f1ece5 | ||
|
|
4ffbb97abf | ||
|
|
98f5cbe309 | ||
|
|
93d856b3b6 | ||
| 6ea6971dd6 | |||
| 163d70e9bd | |||
| 8afef5065a | |||
| 27bf2ae45c | |||
| 1ea44ba3ae | |||
| 0af9a72937 | |||
| d650cac857 | |||
| a0cc86f189 | |||
| f322abf79a | |||
| eede6938cb | |||
| ad24a20fdb | |||
| 5ae43513a7 | |||
| cc23e73d51 | |||
| f85abef237 | |||
| 596d370f43 | |||
| 87cb55b80b | |||
| f458eb0130 | |||
| 7d8f9a39d1 | |||
| 5b6e30ad13 | |||
| 07a5c91427 | |||
| 53b980913b | |||
| 4e2ac4a091 | |||
| 3eb5bb5de3 | |||
| ebbc11bb34 | |||
| 6d5a606107 | |||
| 0831e18c2d | |||
| 05d0bcbedd | |||
| 6ea70d9497 | |||
| bc536bd751 | |||
| 7035cde8c8 | |||
| 5eff189bbf | |||
| c9fed70a60 | |||
| fb9122ecdc | |||
| bff27c42a7 | |||
| cea435b609 | |||
| f9ea2d6900 | |||
| 96e7e66b0d | |||
| e5221b329f | |||
| 70be7312b8 | |||
| b2f4d6677c | |||
| 08e039aebe | |||
| 6502344d0a | |||
| e59677c212 | |||
| 3dc7af6fc0 | |||
| ef829bf3ef | |||
| ff1b7519a0 | |||
| 414ef62479 | |||
| d2cf852eb2 | |||
| 73e0af5d2e | |||
| 99e8fb4681 | |||
| a58f8aaf43 | |||
| 515f58b848 | |||
| 02a8a64360 | |||
| a526887ff6 | |||
| d2ac369fdc | |||
| 2317a80ce5 | |||
| 3cb8d3cfad | |||
| 8b9e2916df | |||
| bbbc678c83 | |||
| 1b67777c4a | |||
| 5fd24b3f06 | |||
| 35f5a024fd | |||
| 6918306336 | |||
| 8ec87685b8 | |||
| 8c8028dd1f | |||
| 9e480db31c | |||
| 0bfffa6552 | |||
| 437199f3f0 | |||
| cf31c91831 | |||
| 7e4ab5cbd8 | |||
| 6ce09c0e9c | |||
| 62c3ded1f8 | |||
| 3e8b472f74 | |||
| fd0ebb8d40 | |||
| fcdc5bac6e | |||
| fecc936a14 | |||
| 536f65bf88 | |||
| ce5905373a | |||
| 119e49aec1 | |||
| 1cde845a77 | |||
| 74789b43f6 | |||
| be315111ea |
@@ -7,32 +7,157 @@ on:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
schedule:
|
||||
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
|
||||
- cron: "0 14 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
native-windows:
|
||||
name: Native Windows Build And Tests
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: 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
|
||||
run: |
|
||||
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||
if ($atlHeaders.Count -eq 0) {
|
||||
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||
|
||||
- name: Configure Debug
|
||||
shell: powershell
|
||||
run: cmake --preset vs2022-x64-debug
|
||||
run: |
|
||||
$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 $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 $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. 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"
|
||||
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
|
||||
run: cmake --build --preset build-debug
|
||||
run: cmake --build --preset build-debug --parallel
|
||||
|
||||
- name: Run Native Tests
|
||||
- name: Run Native Tests And Shader Validation
|
||||
shell: powershell
|
||||
run: cmake --build --preset build-debug --target RUN_TESTS
|
||||
run: cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||
|
||||
ui-ubuntu:
|
||||
name: React UI Build
|
||||
runs-on: nubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -48,14 +173,70 @@ jobs:
|
||||
|
||||
package-windows:
|
||||
name: Windows Release Package
|
||||
runs-on: windows-latest
|
||||
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
|
||||
run: |
|
||||
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
|
||||
if ($atlHeaders.Count -eq 0) {
|
||||
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
|
||||
|
||||
- name: Build UI
|
||||
shell: powershell
|
||||
@@ -66,11 +247,78 @@ jobs:
|
||||
|
||||
- name: Configure Release
|
||||
shell: powershell
|
||||
run: cmake --preset vs2022-x64-release
|
||||
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 $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 $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. 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"
|
||||
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
|
||||
run: cmake --build --preset build-release
|
||||
run: cmake --build --preset build-release --parallel
|
||||
|
||||
- name: Install Runtime Package
|
||||
shell: powershell
|
||||
@@ -81,7 +329,8 @@ jobs:
|
||||
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
|
||||
|
||||
- name: Upload Runtime Package
|
||||
uses: actions/upload-artifact@v4
|
||||
# Gitea/GHES-compatible runners do not support the v4 artifact backend yet.
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: VideoShader-windows-release
|
||||
path: dist/VideoShader.zip
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "video-io-3rdParty"]
|
||||
path = video-io-3rdParty
|
||||
url = https://git.f-40.com/aiden/video-io-3rdParty.git
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -2,16 +2,21 @@
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||
"console": "externalTerminal",
|
||||
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"requireExactSource": true,
|
||||
"logging": {
|
||||
"moduleLoad": true
|
||||
},
|
||||
"preLaunchTask": "Build RenderCadenceCompositor Debug x64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/config/runtime-host.json",
|
||||
"/config/runtime-host.*.json"
|
||||
],
|
||||
"url": "./config/runtime-host.schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@@ -2,42 +2,72 @@
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
|
||||
412
CMakeLists.txt
412
CMakeLists.txt
@@ -1,180 +1,316 @@
|
||||
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(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect" CACHE PATH "Path to the NVIDIA_GPUDirect sample directory from the Blackmagic DeckLink SDK")
|
||||
set(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()
|
||||
|
||||
if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
|
||||
message(FATAL_ERROR "NVIDIA GPUDirect library not found under ${GPUDIRECT_DIR}")
|
||||
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(APP_SOURCES
|
||||
"${APP_DIR}/AudioSupport.cpp"
|
||||
"${APP_DIR}/AudioSupport.h"
|
||||
"${APP_DIR}/ControlServer.cpp"
|
||||
"${APP_DIR}/ControlServer.h"
|
||||
"${APP_DIR}/DeckLinkAPI_i.c"
|
||||
"${APP_DIR}/GLExtensions.cpp"
|
||||
"${APP_DIR}/GLExtensions.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
||||
"${APP_DIR}/NativeHandles.h"
|
||||
"${APP_DIR}/NativeSockets.h"
|
||||
"${APP_DIR}/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/OpenGLComposite.h"
|
||||
"${APP_DIR}/OscServer.cpp"
|
||||
"${APP_DIR}/OscServer.h"
|
||||
"${APP_DIR}/resource.h"
|
||||
"${APP_DIR}/RuntimeHost.cpp"
|
||||
"${APP_DIR}/RuntimeHost.h"
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/RuntimeJson.h"
|
||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/ShaderCompiler.h"
|
||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/ShaderPackageRegistry.h"
|
||||
"${APP_DIR}/ShaderTypes.h"
|
||||
"${APP_DIR}/stdafx.cpp"
|
||||
"${APP_DIR}/stdafx.h"
|
||||
"${APP_DIR}/targetver.h"
|
||||
"${APP_DIR}/VideoFrameTransfer.cpp"
|
||||
"${APP_DIR}/VideoFrameTransfer.h"
|
||||
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"
|
||||
)
|
||||
|
||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
||||
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()
|
||||
|
||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${GPUDIRECT_DIR}/include"
|
||||
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"
|
||||
)
|
||||
|
||||
target_link_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${GPUDIRECT_DIR}/lib/x64"
|
||||
set(RUNTIME_PARAMETER_SOURCES
|
||||
${RUNTIME_JSON_SOURCES}
|
||||
"${SRC_DIR}/runtime/state/RuntimeParameterUtils.cpp"
|
||||
)
|
||||
|
||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
dvp.lib
|
||||
opengl32
|
||||
glu32
|
||||
Ws2_32
|
||||
Crypt32
|
||||
Advapi32
|
||||
set(RUNTIME_STATE_SOURCES
|
||||
${RUNTIME_JSON_SOURCES}
|
||||
"${SRC_DIR}/runtime/state/RuntimeStatePersistence.cpp"
|
||||
)
|
||||
|
||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
_UNICODE
|
||||
UNICODE
|
||||
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"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
||||
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")
|
||||
|
||||
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()
|
||||
|
||||
add_executable(RuntimeJsonTests
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeJsonTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeJsonTests PRIVATE /W3)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||
|
||||
add_executable(RuntimeParameterUtilsTests
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
|
||||
if(TARGET RenderCadenceCompositor)
|
||||
install(TARGETS RenderCadenceCompositor
|
||||
RUNTIME DESTINATION "."
|
||||
)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||
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()
|
||||
|
||||
add_executable(ShaderPackageRegistryTests
|
||||
"${APP_DIR}/RuntimeJson.cpp"
|
||||
"${APP_DIR}/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||
)
|
||||
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()
|
||||
|
||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
|
||||
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()
|
||||
|
||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||
|
||||
add_executable(OscServerTests
|
||||
"${APP_DIR}/OscServer.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(OscServerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
)
|
||||
|
||||
target_link_libraries(OscServerTests PRIVATE
|
||||
Ws2_32
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(OscServerTests 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_test(NAME OscServerTests COMMAND OscServerTests)
|
||||
|
||||
add_executable(AudioSupportTests
|
||||
"${APP_DIR}/AudioSupport.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioSupportTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(AudioSupportTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(AudioSupportTests PRIVATE /W3)
|
||||
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()
|
||||
|
||||
add_test(NAME AudioSupportTests COMMAND AudioSupportTests)
|
||||
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()
|
||||
|
||||
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
||||
"$<TARGET_FILE_DIR:LoopThroughWithOpenGLCompositing>/dvp.dll"
|
||||
)
|
||||
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()
|
||||
|
||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||
RUNTIME DESTINATION "."
|
||||
)
|
||||
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()
|
||||
|
||||
install(FILES "${GPUDIRECT_DIR}/bin/x64/dvp.dll"
|
||||
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 "."
|
||||
)
|
||||
|
||||
@@ -203,5 +339,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
|
||||
DESTINATION "docs"
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})
|
||||
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
109
OSC/Test.json
109
OSC/Test.json
@@ -36,7 +36,7 @@
|
||||
"preArgs": "",
|
||||
"typeTags": "",
|
||||
"decimals": 2,
|
||||
"target": "127.0.0.1:9000",
|
||||
"target": "192.168.1.46:9000",
|
||||
"ignoreDefaults": false,
|
||||
"bypass": false,
|
||||
"onCreate": "",
|
||||
@@ -53,8 +53,8 @@
|
||||
"visible": true,
|
||||
"interaction": true,
|
||||
"comments": "XY control for Fisheye Reproject pan and tilt.",
|
||||
"width": 420,
|
||||
"height": 420,
|
||||
"width": 460,
|
||||
"height": 250,
|
||||
"expand": false,
|
||||
"colorText": "auto",
|
||||
"colorWidget": "auto",
|
||||
@@ -70,14 +70,14 @@
|
||||
"css": "",
|
||||
"pips": true,
|
||||
"snap": false,
|
||||
"spring": false,
|
||||
"spring": true,
|
||||
"rangeX": {
|
||||
"min": -60,
|
||||
"max": 60
|
||||
"min": -1,
|
||||
"max": 1
|
||||
},
|
||||
"rangeY": {
|
||||
"min": 45,
|
||||
"max": -45
|
||||
"min": 1,
|
||||
"max": -1
|
||||
},
|
||||
"logScaleX": false,
|
||||
"logScaleY": false,
|
||||
@@ -94,13 +94,13 @@
|
||||
"address": "/VideoShaderToys/fisheye-reproject/xy",
|
||||
"preArgs": "",
|
||||
"typeTags": "",
|
||||
"decimals": "2f",
|
||||
"target": "127.0.0.1:9000",
|
||||
"decimals": "3f",
|
||||
"target": "192.168.1.46:9000",
|
||||
"ignoreDefaults": false,
|
||||
"bypass": true,
|
||||
"onCreate": "",
|
||||
"onValue": "var pan = Array.isArray(value) ? Number(value[0]) : 0;\nvar tilt = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/panDegrees', {type: 'f', value: pan});\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: 'f', value: tilt});",
|
||||
"onTouch": "",
|
||||
"onCreate": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nstate.target = '192.168.1.46:9000';\nstate.panAddress = '/VideoShaderToys/fisheye-reproject/panDegrees';\nstate.tiltAddress = '/VideoShaderToys/fisheye-reproject/tiltDegrees';\nstate.minPan = -60;\nstate.maxPan = 60;\nstate.minTilt = -45;\nstate.maxTilt = 45;\nstate.pan = 0;\nstate.tilt = 0;\nstate.stickX = 0;\nstate.stickY = 0;\nstate.tickMs = 16;\nstate.stepPan = 0.75;\nstate.stepTilt = 0.75;\nstate.deadzone = 0.14;\nstate.applyCurve = function(input) {\n var amount = Math.abs(input);\n if (amount <= state.deadzone) {\n return 0;\n }\n var normalized = (amount - state.deadzone) / (1 - state.deadzone);\n var softened = normalized * normalized * (3 - (2 * normalized));\n return (input < 0 ? -1 : 1) * softened;\n};\nif (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n}",
|
||||
"onValue": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nvar stickX = Array.isArray(value) ? Number(value[0]) : 0;\nvar stickY = Array.isArray(value) ? Number(value[1]) : 0;\nstate.stickX = isFinite(stickX) ? state.applyCurve(stickX) : 0;\nstate.stickY = isFinite(stickY) ? state.applyCurve(stickY) : 0;",
|
||||
"onTouch": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nif (value) {\n if (!state.timer) {\n state.timer = setInterval(function() {\n if (!state.stickX && !state.stickY) {\n return;\n }\n state.pan = Math.max(state.minPan, Math.min(state.maxPan, state.pan + (state.stickX * state.stepPan)));\n state.tilt = Math.max(state.minTilt, Math.min(state.maxTilt, state.tilt + (state.stickY * state.stepTilt)));\n send(state.target, state.panAddress, {type: 'f', value: state.pan});\n send(state.target, state.tiltAddress, {type: 'f', value: state.tilt});\n }, state.tickMs);\n }\n} else {\n state.stickX = 0;\n state.stickY = 0;\n if (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n }\n}",
|
||||
"pointSize": 20,
|
||||
"ephemeral": false,
|
||||
"label": "",
|
||||
@@ -121,7 +121,7 @@
|
||||
"interaction": true,
|
||||
"comments": "",
|
||||
"width": 90,
|
||||
"height": 420,
|
||||
"height": 250,
|
||||
"expand": false,
|
||||
"colorText": "auto",
|
||||
"colorWidget": "auto",
|
||||
@@ -144,90 +144,29 @@
|
||||
"gradient": [],
|
||||
"snap": false,
|
||||
"touchZone": "all",
|
||||
"spring": false,
|
||||
"spring": true,
|
||||
"doubleTap": false,
|
||||
"range": {
|
||||
"min": 100,
|
||||
"max": 10
|
||||
"min": -1,
|
||||
"max": 1
|
||||
},
|
||||
"logScale": false,
|
||||
"sensitivity": 1,
|
||||
"steps": "",
|
||||
"origin": "auto",
|
||||
"value": "",
|
||||
"default": 90,
|
||||
"value": 0,
|
||||
"default": 0,
|
||||
"linkId": "",
|
||||
"address": "/VideoShaderToys/fisheye-reproject/virtualFovDegrees",
|
||||
"preArgs": "",
|
||||
"typeTags": "",
|
||||
"decimals": 2,
|
||||
"target": "127.0.0.1:9000",
|
||||
"ignoreDefaults": false,
|
||||
"bypass": false,
|
||||
"onCreate": "",
|
||||
"onValue": "",
|
||||
"onTouch": ""
|
||||
},
|
||||
{
|
||||
"type": "xy",
|
||||
"top": 700,
|
||||
"left": 190,
|
||||
"lock": false,
|
||||
"id": "Pan Pad",
|
||||
"visible": true,
|
||||
"interaction": true,
|
||||
"comments": "",
|
||||
"width": "auto",
|
||||
"height": "auto",
|
||||
"expand": false,
|
||||
"colorText": "auto",
|
||||
"colorWidget": "auto",
|
||||
"colorStroke": "auto",
|
||||
"colorFill": "auto",
|
||||
"alphaStroke": "auto",
|
||||
"alphaFillOff": "auto",
|
||||
"alphaFillOn": "auto",
|
||||
"lineWidth": "auto",
|
||||
"borderRadius": "auto",
|
||||
"padding": "auto",
|
||||
"html": "",
|
||||
"css": "",
|
||||
"pointSize": 20,
|
||||
"ephemeral": false,
|
||||
"pips": true,
|
||||
"label": "",
|
||||
"snap": false,
|
||||
"spring": false,
|
||||
"rangeX": {
|
||||
"min": -1,
|
||||
"max": 1
|
||||
},
|
||||
"rangeY": {
|
||||
"min": -1,
|
||||
"max": 1
|
||||
},
|
||||
"logScaleX": false,
|
||||
"logScaleY": false,
|
||||
"stepsX": false,
|
||||
"stepsY": false,
|
||||
"clipX": "",
|
||||
"clipY": "",
|
||||
"axisLock": "",
|
||||
"doubleTap": false,
|
||||
"sensitivity": 1,
|
||||
"value": "",
|
||||
"default": "",
|
||||
"linkId": "",
|
||||
"address": "/VideoShaderToys/video-transform/pan",
|
||||
"preArgs": "",
|
||||
"typeTags": "",
|
||||
"decimals": 2,
|
||||
"target": "",
|
||||
"decimals": "3f",
|
||||
"target": "192.168.1.46:9000",
|
||||
"ignoreDefaults": false,
|
||||
"bypass": true,
|
||||
"onCreate": "",
|
||||
"onValue": "var x = Array.isArray(value) ? Number(value[0]) : 0;\nvar y = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/video-transform/pan', {type: 'f', value: x}, {type: 'f', value: y});",
|
||||
"onTouch": ""
|
||||
"onCreate": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nstate.target = '192.168.1.46:9000';\nstate.address = '/VideoShaderToys/fisheye-reproject/virtualFovDegrees';\nstate.minFov = 10;\nstate.maxFov = 100;\nstate.fov = 90;\nstate.stick = 0;\nstate.tickMs = 16;\nstate.stepFov = 0.6;\nstate.deadzone = 0.14;\nstate.applyCurve = function(input) {\n var amount = Math.abs(input);\n if (amount <= state.deadzone) {\n return 0;\n }\n var normalized = (amount - state.deadzone) / (1 - state.deadzone);\n var softened = normalized * normalized * (3 - (2 * normalized));\n return (input < 0 ? -1 : 1) * softened;\n};\nif (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n}",
|
||||
"onValue": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nvar stick = Number(value);\nstate.stick = isFinite(stick) ? state.applyCurve(stick) : 0;",
|
||||
"onTouch": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nif (value) {\n if (!state.timer) {\n state.timer = setInterval(function() {\n if (!state.stick) {\n return;\n }\n state.fov = Math.max(state.minFov, Math.min(state.maxFov, state.fov - (state.stick * state.stepFov)));\n send(state.target, state.address, {type: 'f', value: state.fov});\n }, state.tickMs);\n }\n} else {\n state.stick = 0;\n if (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n }\n}"
|
||||
}
|
||||
],
|
||||
"tabs": []
|
||||
|
||||
287
README.md
287
README.md
@@ -1,49 +1,118 @@
|
||||
# Video Shader
|
||||
|
||||
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI.
|
||||
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink 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.
|
||||
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:
|
||||
|
||||
- `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
|
||||
|
||||
- Windows with Visual Studio 2022 C++ tooling.
|
||||
- CMake 3.24 or newer.
|
||||
- Node.js and npm for the control UI.
|
||||
- Blackmagic DeckLink SDK 16.0 with the NVIDIA GPUDirect sample files available locally.
|
||||
- Slang compiler available under the repo/tooling paths expected by the runtime, or otherwise discoverable by the existing app setup.
|
||||
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
|
||||
- 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.
|
||||
|
||||
The Blackmagic/GPUDirect SDK should not be committed to this repository. `CMakeLists.txt` exposes `GPUDIRECT_DIR` as a cache path so local machines and CI runners can point at their installed SDK location.
|
||||
### Third-party SDK bundle
|
||||
|
||||
Default expected SDK path:
|
||||
|
||||
```text
|
||||
3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect
|
||||
```
|
||||
|
||||
Override example:
|
||||
Org members can initialize the private SDK bundle submodule:
|
||||
|
||||
```powershell
|
||||
cmake --preset vs2022-x64-debug -DGPUDIRECT_DIR="D:/SDKs/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect"
|
||||
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
|
||||
```
|
||||
|
||||
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 -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:
|
||||
|
||||
```powershell
|
||||
cmake --preset vs2022-x64-debug
|
||||
cmake --build --preset build-debug
|
||||
cmake --build --preset build-debug --parallel
|
||||
```
|
||||
|
||||
Build the React control UI:
|
||||
@@ -56,6 +125,15 @@ npm run build
|
||||
|
||||
The native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development.
|
||||
|
||||
The control UI provides:
|
||||
|
||||
- A searchable shader library for adding layers.
|
||||
- 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.
|
||||
- Host config editing, save, restart request, and NDI input source discovery.
|
||||
- Compact video I/O and render cadence status.
|
||||
|
||||
## Package
|
||||
|
||||
Build the UI, build the native Release target, then install into a portable runtime folder:
|
||||
@@ -66,7 +144,7 @@ npm ci
|
||||
npm run build
|
||||
cd ..
|
||||
cmake --preset vs2022-x64-release
|
||||
cmake --build --preset build-release
|
||||
cmake --build --preset build-release --parallel
|
||||
cmake --install build/vs2022-x64-release --config Release --prefix dist/VideoShader
|
||||
```
|
||||
|
||||
@@ -74,15 +152,22 @@ The package folder will contain:
|
||||
|
||||
```text
|
||||
dist/VideoShader/
|
||||
LoopThroughWithOpenGLCompositing.exe
|
||||
dvp.dll
|
||||
RenderCadenceCompositor.exe
|
||||
Processing.NDI.Lib.x64.dll
|
||||
config/
|
||||
shaders/
|
||||
3rdParty/slang/bin/
|
||||
3rdParty/msdf-atlas-gen/
|
||||
ui/dist/
|
||||
docs/
|
||||
SHADER_CONTRACT.md
|
||||
runtime/templates/
|
||||
third_party_notices/
|
||||
```
|
||||
|
||||
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
|
||||
You can run `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 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:
|
||||
|
||||
@@ -95,7 +180,7 @@ Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip
|
||||
Run native tests:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RUN_TESTS
|
||||
cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||
```
|
||||
|
||||
Run the UI production build check:
|
||||
@@ -109,7 +194,9 @@ Current native test coverage includes:
|
||||
|
||||
- JSON parsing and serialization.
|
||||
- Parameter normalization and preset filename safety.
|
||||
- Shader manifest parsing and package registry scanning.
|
||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||
- Slang validation for every available shader package.
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
@@ -119,26 +206,39 @@ Current native test coverage includes:
|
||||
{
|
||||
"shaderLibrary": "shaders",
|
||||
"serverPort": 8080,
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"oscSmoothing": 0.18,
|
||||
"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,
|
||||
"audioEnabled": true,
|
||||
"audioChannelCount": 2,
|
||||
"audioSampleRate": 48000,
|
||||
"audioDelayMode": "matchVideoPreroll",
|
||||
"enableExternalKeying": true
|
||||
"previewEnabled": false,
|
||||
"previewFps": 59.94
|
||||
}
|
||||
```
|
||||
|
||||
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
|
||||
`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.
|
||||
|
||||
`audioEnabled` enables embedded stereo 48 kHz PCM pass-through. Audio is delayed to match the scheduled video preroll and the synchronized level/spectrum data is exposed to shaders.
|
||||
`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`.
|
||||
|
||||
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
|
||||
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:
|
||||
|
||||
@@ -146,6 +246,44 @@ The control UI is available at:
|
||||
http://127.0.0.1:<serverPort>
|
||||
```
|
||||
|
||||
`/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`.
|
||||
|
||||
### Borderless Window / Fullscreen Output Plan
|
||||
|
||||
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
|
||||
|
||||
The local REST control API is documented as an OpenAPI/Swagger spec:
|
||||
@@ -167,17 +305,31 @@ 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, 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 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 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/
|
||||
```
|
||||
|
||||
## OSC Control
|
||||
|
||||
The native host also listens for local OSC parameter control on the configured `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. 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
|
||||
|
||||
@@ -187,9 +339,23 @@ Each shader package lives under:
|
||||
shaders/<id>/
|
||||
shader.json
|
||||
shader.slang
|
||||
optional-extra-pass.slang
|
||||
optional-font-or-texture-assets
|
||||
```
|
||||
|
||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, temporal history support, and the Slang entry point contract.
|
||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, 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
|
||||
|
||||
@@ -198,8 +364,9 @@ Runtime-generated files are intentionally ignored:
|
||||
- `runtime/shader_cache/active_shader_wrapper.slang`
|
||||
- `runtime/shader_cache/active_shader.raw.frag`
|
||||
- `runtime/shader_cache/active_shader.frag`
|
||||
- `runtime/runtime_state.json`
|
||||
- `runtime/stack_presets/*.json`
|
||||
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
|
||||
- `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.
|
||||
|
||||
@@ -207,7 +374,45 @@ Only `runtime/templates/` and `runtime/README.md` are tracked.
|
||||
|
||||
The Gitea workflow expects two act runners:
|
||||
|
||||
- `windows-latest`: builds the native app and runs native tests.
|
||||
- `windows-2022`: builds the native app and runs native tests.
|
||||
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
|
||||
|
||||
If your Windows runner stores the Blackmagic SDK outside the repo, configure `GPUDIRECT_DIR` in the runner environment or adjust the workflow configure command to pass `-DGPUDIRECT_DIR=...`.
|
||||
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
|
||||
|
||||
- `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`.
|
||||
|
||||
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 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.
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2 for an embedded native control surface.
|
||||
- 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
|
||||
- allow shaders to read other shaders data store based on name? or output over OSC
|
||||
- Mipmapping for shader-declared textures
|
||||
- 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,435 +0,0 @@
|
||||
# Shader Package Contract
|
||||
|
||||
This document explains how to create shaders for the Video Shader runtime.
|
||||
|
||||
Each shader is a small package under `shaders/<id>/`:
|
||||
|
||||
```text
|
||||
shaders/my-effect/
|
||||
shader.json
|
||||
shader.slang
|
||||
optional-texture.png
|
||||
```
|
||||
|
||||
The runtime reads `shader.json`, generates a Slang wrapper from `runtime/templates/shader_wrapper.slang.in`, includes your `shader.slang`, compiles the result to GLSL, and exposes the shader in the local control UI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create a folder:
|
||||
|
||||
```text
|
||||
shaders/my-effect/
|
||||
```
|
||||
|
||||
Add `shader.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-effect",
|
||||
"name": "My Effect",
|
||||
"description": "A simple starter shader.",
|
||||
"category": "Custom",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Add `shader.slang`:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 color = context.sourceColor;
|
||||
color.rgb = lerp(color.rgb, 1.0 - color.rgb, strength);
|
||||
return saturate(color);
|
||||
}
|
||||
```
|
||||
|
||||
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
`shader.json` is the runtime-facing description of the shader.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `id`: package ID used by state/presets. Hyphenated names are OK here, for example `my-effect`.
|
||||
- `name`: display name in the UI.
|
||||
- `parameters`: array of exposed controls. Use `[]` if there are no user parameters.
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `description`: display/help text for the shader library.
|
||||
- `category`: UI grouping label.
|
||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||
- `textures`: texture assets to load and expose as samplers.
|
||||
- `temporal`: history-buffer requirements.
|
||||
|
||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
|
||||
- `entryPoint`
|
||||
- parameter `id`
|
||||
- texture `id`
|
||||
|
||||
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
|
||||
|
||||
## Slang Entry Point
|
||||
|
||||
Your shader file must implement the manifest `entryPoint`.
|
||||
|
||||
Default:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return context.sourceColor;
|
||||
}
|
||||
```
|
||||
|
||||
The runtime owns the real fragment shader entry point. Your function is called from the wrapper, and the runtime handles final bypass/mix behavior:
|
||||
|
||||
```slang
|
||||
return lerp(context.sourceColor, effectedColor, mixValue);
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
- Return the fully effected color from your function.
|
||||
- Respect alpha if your shader produces an overlay or sprite.
|
||||
- The runtime will blend your result with the source according to `mixAmount` and bypass state.
|
||||
|
||||
## ShaderContext
|
||||
|
||||
Your entry point receives:
|
||||
|
||||
```slang
|
||||
struct ShaderContext
|
||||
{
|
||||
float2 uv;
|
||||
float4 sourceColor;
|
||||
float2 inputResolution;
|
||||
float2 outputResolution;
|
||||
float time;
|
||||
float frameCount;
|
||||
float mixAmount;
|
||||
float bypass;
|
||||
int sourceHistoryLength;
|
||||
int temporalHistoryLength;
|
||||
float2 audioRms;
|
||||
float2 audioPeak;
|
||||
float audioMonoRms;
|
||||
float audioMonoPeak;
|
||||
float4 audioBands;
|
||||
};
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `uv`: normalized texture coordinates, usually `0..1`.
|
||||
- `sourceColor`: decoded RGBA source video at `uv`.
|
||||
- `inputResolution`: decoded input video resolution in pixels.
|
||||
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
|
||||
- `time`: elapsed runtime time in seconds.
|
||||
- `frameCount`: incrementing frame counter.
|
||||
- `mixAmount`: runtime mix amount.
|
||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
||||
- `audioRms`: left/right RMS level for the audio block synchronized with the rendered output frame.
|
||||
- `audioPeak`: left/right peak level for the same synchronized audio block.
|
||||
- `audioMonoRms`: mono RMS level derived from left/right.
|
||||
- `audioMonoPeak`: mono peak level derived from left/right.
|
||||
- `audioBands`: four smoothed, normalized low-to-high frequency bands.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
The wrapper provides:
|
||||
|
||||
```slang
|
||||
float4 sampleVideo(float2 uv);
|
||||
float4 sampleSourceHistory(int framesAgo, float2 uv);
|
||||
float4 sampleTemporalHistory(int framesAgo, float2 uv);
|
||||
float4 sampleAudioWaveform(float x);
|
||||
float4 sampleAudioSpectrum(float x);
|
||||
```
|
||||
|
||||
`sampleVideo` samples the live decoded source video.
|
||||
|
||||
`sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`.
|
||||
|
||||
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
|
||||
|
||||
`sampleAudioWaveform` samples the current synchronized audio waveform texture. `x` is normalized `0..1`; returned waveform channels are encoded from `-1..1` into `0..1`.
|
||||
|
||||
`sampleAudioSpectrum` samples the current synchronized audio spectrum texture. Values are normalized `0..1`.
|
||||
|
||||
Example:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleSourceHistory(1, context.uv);
|
||||
return lerp(context.sourceColor, previous, 0.35);
|
||||
}
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
Manifest parameters are exposed to Slang as global values with the same `id`.
|
||||
|
||||
Supported types:
|
||||
|
||||
| Manifest type | Slang type | JSON value |
|
||||
| --- | --- | --- |
|
||||
| `float` | `float` | number |
|
||||
| `vec2` | `float2` | `[x, y]` |
|
||||
| `color` | `float4` | `[r, g, b, a]` |
|
||||
| `bool` | `bool` | `true` or `false` |
|
||||
| `enum` | `int` | selected option index |
|
||||
|
||||
Float example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
color.rgb *= brightness;
|
||||
```
|
||||
|
||||
Vector example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-0.2, -0.2],
|
||||
"max": [0.2, 0.2],
|
||||
"step": [0.001, 0.001]
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
float2 uv = clamp(context.uv + offset, float2(0.0), float2(1.0));
|
||||
```
|
||||
|
||||
Color example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "tint",
|
||||
"label": "Tint",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
color *= tint;
|
||||
```
|
||||
|
||||
Boolean example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "invert",
|
||||
"label": "Invert",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
if (invert)
|
||||
color.rgb = 1.0 - color.rgb;
|
||||
```
|
||||
|
||||
Enum example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mode",
|
||||
"label": "Mode",
|
||||
"type": "enum",
|
||||
"default": "normal",
|
||||
"options": [
|
||||
{ "value": "normal", "label": "Normal" },
|
||||
{ "value": "luma", "label": "Luma" },
|
||||
{ "value": "posterize", "label": "Posterize" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Enums are stored in presets/state by their string `value`, but exposed to Slang as a zero-based integer index in option order:
|
||||
|
||||
```slang
|
||||
if (mode == 1)
|
||||
{
|
||||
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
color.rgb = float3(luma);
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
color.rgb = floor(color.rgb * 4.0) / 4.0;
|
||||
}
|
||||
```
|
||||
|
||||
Parameter validation:
|
||||
|
||||
- Float values are clamped to `min`/`max` if provided.
|
||||
- `vec2` must have exactly 2 numbers.
|
||||
- `color` must have exactly 4 numbers.
|
||||
- Enum defaults must match one of the declared option values.
|
||||
- Non-finite numeric values are rejected.
|
||||
|
||||
## Texture Assets
|
||||
|
||||
Declare texture assets in the manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"textures": [
|
||||
{
|
||||
"id": "logoTexture",
|
||||
"path": "logo.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `id` must be a valid shader identifier.
|
||||
- `path` is relative to the shader package directory.
|
||||
- The file must exist when the manifest is loaded.
|
||||
- Texture asset changes trigger shader reload.
|
||||
|
||||
Texture IDs become `Sampler2D<float4>` globals:
|
||||
|
||||
```slang
|
||||
float4 logo = logoTexture.Sample(logoUv);
|
||||
```
|
||||
|
||||
For sprite or overlay shaders, return premultiplied-looking output if you want clean composition:
|
||||
|
||||
```slang
|
||||
float alpha = logo.a;
|
||||
return float4(logo.rgb * alpha, alpha);
|
||||
```
|
||||
|
||||
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
||||
|
||||
## Temporal Shaders
|
||||
|
||||
Temporal shaders can request access to previous frames.
|
||||
|
||||
Manifest example:
|
||||
|
||||
```json
|
||||
{
|
||||
"temporal": {
|
||||
"enabled": true,
|
||||
"historySource": "preLayerInput",
|
||||
"historyLength": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `historySource` values:
|
||||
|
||||
- `source`: decoded source-video history from previous frames.
|
||||
- `preLayerInput`: history of the input arriving at this layer before the shader runs.
|
||||
|
||||
`historyLength` is the requested frame count. The runtime clamps it by `maxTemporalHistoryFrames` in `config/runtime-host.json`.
|
||||
|
||||
Temporal history resets when:
|
||||
|
||||
- layers are added, removed, or reordered
|
||||
- a layer bypass state changes
|
||||
- a layer changes shader
|
||||
- a shader is reloaded or recompiled
|
||||
- render dimensions change
|
||||
|
||||
Use the available history lengths to avoid assuming history is ready on the first frame:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
if (context.temporalHistoryLength <= 0)
|
||||
return context.sourceColor;
|
||||
|
||||
float4 oldFrame = sampleTemporalHistory(3, context.uv);
|
||||
return lerp(context.sourceColor, oldFrame, 0.4);
|
||||
}
|
||||
```
|
||||
|
||||
See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples.
|
||||
|
||||
## Coordinate And Color Notes
|
||||
|
||||
- `uv` is normalized.
|
||||
- Use `context.outputResolution` for pixel-sized effects.
|
||||
- Use `context.inputResolution` when sampling source video by input pixel size.
|
||||
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
||||
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
||||
|
||||
Pixel-size example:
|
||||
|
||||
```slang
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
|
||||
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
||||
```
|
||||
|
||||
## Reload And Generated Files
|
||||
|
||||
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
||||
|
||||
- `active_shader_wrapper.slang`
|
||||
- `active_shader.raw.frag`
|
||||
- `active_shader.frag`
|
||||
|
||||
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.
|
||||
- Do not declare your own `ShaderContext`, `GlobalParams`, `sampleVideo`, `sampleSourceHistory`, or `sampleTemporalHistory`.
|
||||
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
||||
- Remember enum globals are integer indexes, not strings.
|
||||
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
||||
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
||||
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
||||
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
||||
|
||||
## Minimal Package Checklist
|
||||
|
||||
Before committing a new shader package:
|
||||
|
||||
- `shader.json` is valid JSON.
|
||||
- `id` is unique across `shaders/`.
|
||||
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
||||
- `shader.slang` implements the configured entry point.
|
||||
- Texture files referenced by `textures` exist.
|
||||
- Enum defaults are present in their `options`.
|
||||
- Temporal shaders handle short or empty history gracefully.
|
||||
- The app can reload and compile the shader without errors.
|
||||
@@ -1,206 +0,0 @@
|
||||
#include "AudioSupport.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float kInt32ToFloat = 1.0f / 2147483648.0f;
|
||||
constexpr std::size_t kAnalysisWindowSamples = 1024;
|
||||
constexpr std::size_t kMaxBufferedAudioFrames = kAudioSampleRate * 10;
|
||||
|
||||
float Clamp01(float value)
|
||||
{
|
||||
return std::max(0.0f, std::min(1.0f, value));
|
||||
}
|
||||
|
||||
float SampleToFloat(int32_t sample)
|
||||
{
|
||||
return std::max(-1.0f, std::min(1.0f, static_cast<float>(sample) * kInt32ToFloat));
|
||||
}
|
||||
|
||||
float GoertzelMagnitude(const std::vector<float>& samples, float frequency)
|
||||
{
|
||||
if (samples.empty())
|
||||
return 0.0f;
|
||||
|
||||
const double omega = 2.0 * 3.14159265358979323846 * static_cast<double>(frequency) / static_cast<double>(kAudioSampleRate);
|
||||
const double coefficient = 2.0 * std::cos(omega);
|
||||
double q0 = 0.0;
|
||||
double q1 = 0.0;
|
||||
double q2 = 0.0;
|
||||
|
||||
for (float sample : samples)
|
||||
{
|
||||
q0 = coefficient * q1 - q2 + static_cast<double>(sample);
|
||||
q2 = q1;
|
||||
q1 = q0;
|
||||
}
|
||||
|
||||
const double power = q1 * q1 + q2 * q2 - coefficient * q1 * q2;
|
||||
return static_cast<float>(std::sqrt(std::max(0.0, power)) / static_cast<double>(samples.size()));
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
|
||||
{
|
||||
if (frameTimescale == 0)
|
||||
return 0;
|
||||
|
||||
const uint64_t numerator = videoFrameIndex * frameDuration * audioSampleRate;
|
||||
return (numerator + frameTimescale / 2) / frameTimescale;
|
||||
}
|
||||
|
||||
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
|
||||
{
|
||||
const uint64_t start = AudioSampleTimeForVideoFrame(videoFrameIndex, frameDuration, frameTimescale, audioSampleRate);
|
||||
const uint64_t end = AudioSampleTimeForVideoFrame(videoFrameIndex + 1, frameDuration, frameTimescale, audioSampleRate);
|
||||
return static_cast<unsigned>(end > start ? end - start : 0);
|
||||
}
|
||||
|
||||
void AudioDelayBuffer::Reset(unsigned delaySampleFrames)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mSamples.clear();
|
||||
mSamples.resize(static_cast<std::size_t>(delaySampleFrames) * kAudioChannelCount, 0);
|
||||
mUnderrunCount = 0;
|
||||
}
|
||||
|
||||
void AudioDelayBuffer::PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount)
|
||||
{
|
||||
if (!samples || sampleFrameCount == 0)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t sampleCount = sampleFrameCount * kAudioChannelCount;
|
||||
for (std::size_t index = 0; index < sampleCount; ++index)
|
||||
mSamples.push_back(samples[index]);
|
||||
|
||||
const std::size_t maxSamples = kMaxBufferedAudioFrames * kAudioChannelCount;
|
||||
while (mSamples.size() > maxSamples)
|
||||
mSamples.pop_front();
|
||||
}
|
||||
|
||||
AudioFrameBlock AudioDelayBuffer::Pop(std::size_t sampleFrameCount, bool& underrun)
|
||||
{
|
||||
AudioFrameBlock block;
|
||||
block.interleavedSamples.resize(sampleFrameCount * kAudioChannelCount, 0);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t requestedSamples = sampleFrameCount * kAudioChannelCount;
|
||||
underrun = mSamples.size() < requestedSamples;
|
||||
if (underrun)
|
||||
++mUnderrunCount;
|
||||
|
||||
const std::size_t availableSamples = std::min(requestedSamples, mSamples.size());
|
||||
for (std::size_t index = 0; index < availableSamples; ++index)
|
||||
{
|
||||
block.interleavedSamples[index] = mSamples.front();
|
||||
mSamples.pop_front();
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
unsigned AudioDelayBuffer::BufferedSampleFrames() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return static_cast<unsigned>(mSamples.size() / kAudioChannelCount);
|
||||
}
|
||||
|
||||
uint64_t AudioDelayBuffer::UnderrunCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mUnderrunCount;
|
||||
}
|
||||
|
||||
void AudioAnalyzer::Reset()
|
||||
{
|
||||
mMonoHistory.clear();
|
||||
mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
mCurrent = AudioAnalysisSnapshot();
|
||||
}
|
||||
|
||||
AudioAnalysisSnapshot AudioAnalyzer::Analyze(const AudioFrameBlock& block)
|
||||
{
|
||||
AudioAnalysisSnapshot next;
|
||||
double sumSquares[2] = { 0.0, 0.0 };
|
||||
float peak[2] = { 0.0f, 0.0f };
|
||||
double monoSumSquares = 0.0;
|
||||
float monoPeak = 0.0f;
|
||||
const std::size_t frames = block.frameCount();
|
||||
|
||||
for (std::size_t frame = 0; frame < frames; ++frame)
|
||||
{
|
||||
const float left = SampleToFloat(block.interleavedSamples[frame * 2]);
|
||||
const float right = SampleToFloat(block.interleavedSamples[frame * 2 + 1]);
|
||||
const float mono = (left + right) * 0.5f;
|
||||
|
||||
sumSquares[0] += static_cast<double>(left) * left;
|
||||
sumSquares[1] += static_cast<double>(right) * right;
|
||||
peak[0] = std::max(peak[0], std::abs(left));
|
||||
peak[1] = std::max(peak[1], std::abs(right));
|
||||
monoSumSquares += static_cast<double>(mono) * mono;
|
||||
monoPeak = std::max(monoPeak, std::abs(mono));
|
||||
|
||||
mMonoHistory.push_back(mono);
|
||||
while (mMonoHistory.size() > kAnalysisWindowSamples)
|
||||
mMonoHistory.pop_front();
|
||||
}
|
||||
|
||||
if (frames > 0)
|
||||
{
|
||||
next.rms[0] = static_cast<float>(std::sqrt(sumSquares[0] / static_cast<double>(frames)));
|
||||
next.rms[1] = static_cast<float>(std::sqrt(sumSquares[1] / static_cast<double>(frames)));
|
||||
next.peak[0] = peak[0];
|
||||
next.peak[1] = peak[1];
|
||||
next.monoRms = static_cast<float>(std::sqrt(monoSumSquares / static_cast<double>(frames)));
|
||||
next.monoPeak = monoPeak;
|
||||
}
|
||||
|
||||
std::vector<float> window(mMonoHistory.begin(), mMonoHistory.end());
|
||||
const float bandFrequencies[4] = { 90.0f, 300.0f, 1200.0f, 5000.0f };
|
||||
for (std::size_t band = 0; band < next.bands.size(); ++band)
|
||||
{
|
||||
const float raw = Clamp01(GoertzelMagnitude(window, bandFrequencies[band]) * 8.0f);
|
||||
const float smoothing = raw > mSmoothedBands[band] ? 0.45f : 0.12f;
|
||||
mSmoothedBands[band] = mSmoothedBands[band] + (raw - mSmoothedBands[band]) * smoothing;
|
||||
next.bands[band] = Clamp01(mSmoothedBands[band]);
|
||||
}
|
||||
|
||||
for (unsigned x = 0; x < kAudioTextureWidth; ++x)
|
||||
{
|
||||
float mono = 0.0f;
|
||||
if (!mMonoHistory.empty())
|
||||
{
|
||||
const std::size_t historyIndex = static_cast<std::size_t>(
|
||||
(static_cast<uint64_t>(x) * static_cast<uint64_t>(mMonoHistory.size())) / kAudioTextureWidth);
|
||||
auto it = mMonoHistory.begin();
|
||||
std::advance(it, std::min(historyIndex, mMonoHistory.size() - 1));
|
||||
mono = *it;
|
||||
}
|
||||
|
||||
const std::size_t waveformOffset = x * 4;
|
||||
next.texture[waveformOffset + 0] = mono * 0.5f + 0.5f;
|
||||
next.texture[waveformOffset + 1] = next.texture[waveformOffset + 0];
|
||||
next.texture[waveformOffset + 2] = next.monoRms;
|
||||
next.texture[waveformOffset + 3] = 1.0f;
|
||||
|
||||
const float bandPosition = static_cast<float>(x) / static_cast<float>(kAudioTextureWidth - 1);
|
||||
const float scaled = bandPosition * static_cast<float>(next.bands.size() - 1);
|
||||
const unsigned bandA = static_cast<unsigned>(std::floor(scaled));
|
||||
const unsigned bandB = std::min<unsigned>(bandA + 1, static_cast<unsigned>(next.bands.size() - 1));
|
||||
const float t = scaled - static_cast<float>(bandA);
|
||||
const float spectrum = next.bands[bandA] * (1.0f - t) + next.bands[bandB] * t;
|
||||
const std::size_t spectrumOffset = (kAudioTextureWidth + x) * 4;
|
||||
next.texture[spectrumOffset + 0] = spectrum;
|
||||
next.texture[spectrumOffset + 1] = next.bands[0];
|
||||
next.texture[spectrumOffset + 2] = next.bands[1];
|
||||
next.texture[spectrumOffset + 3] = next.bands[2];
|
||||
}
|
||||
|
||||
mCurrent = next;
|
||||
return mCurrent;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
constexpr unsigned kAudioSampleRate = 48000;
|
||||
constexpr unsigned kAudioChannelCount = 2;
|
||||
constexpr unsigned kAudioTextureWidth = 64;
|
||||
constexpr unsigned kAudioTextureHeight = 2;
|
||||
|
||||
struct AudioFrameBlock
|
||||
{
|
||||
std::vector<int32_t> interleavedSamples;
|
||||
|
||||
std::size_t frameCount() const
|
||||
{
|
||||
return interleavedSamples.size() / kAudioChannelCount;
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioAnalysisSnapshot
|
||||
{
|
||||
std::array<float, 2> rms = { 0.0f, 0.0f };
|
||||
std::array<float, 2> peak = { 0.0f, 0.0f };
|
||||
float monoRms = 0.0f;
|
||||
float monoPeak = 0.0f;
|
||||
std::array<float, 4> bands = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, kAudioTextureWidth * kAudioTextureHeight * 4> texture = {};
|
||||
};
|
||||
|
||||
struct AudioStatusSnapshot
|
||||
{
|
||||
bool enabled = false;
|
||||
unsigned bufferedSampleFrames = 0;
|
||||
uint64_t underrunCount = 0;
|
||||
AudioAnalysisSnapshot analysis;
|
||||
};
|
||||
|
||||
class AudioDelayBuffer
|
||||
{
|
||||
public:
|
||||
void Reset(unsigned delaySampleFrames);
|
||||
void PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount);
|
||||
AudioFrameBlock Pop(std::size_t sampleFrameCount, bool& underrun);
|
||||
unsigned BufferedSampleFrames() const;
|
||||
uint64_t UnderrunCount() const;
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
std::deque<int32_t> mSamples;
|
||||
uint64_t mUnderrunCount = 0;
|
||||
};
|
||||
|
||||
class AudioAnalyzer
|
||||
{
|
||||
public:
|
||||
void Reset();
|
||||
AudioAnalysisSnapshot Analyze(const AudioFrameBlock& block);
|
||||
const AudioAnalysisSnapshot& Current() const { return mCurrent; }
|
||||
|
||||
private:
|
||||
std::deque<float> mMonoHistory;
|
||||
std::array<float, 4> mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
AudioAnalysisSnapshot mCurrent;
|
||||
};
|
||||
|
||||
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);
|
||||
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);
|
||||
@@ -1,616 +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
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
BroadcastStateLocked();
|
||||
}
|
||||
|
||||
void ControlServer::ServerLoop()
|
||||
{
|
||||
while (mRunning)
|
||||
{
|
||||
TryAcceptClient();
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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));
|
||||
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()
|
||||
{
|
||||
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,104 +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;
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
mutable std::mutex mMutex;
|
||||
std::vector<ClientConnection> mClients;
|
||||
};
|
||||
@@ -1,365 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.cpp
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include "OpenGLComposite.h"
|
||||
|
||||
#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);
|
||||
|
||||
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;
|
||||
|
||||
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())
|
||||
break; // success
|
||||
}
|
||||
|
||||
// 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 WM_DESTROY:
|
||||
try
|
||||
{
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
pOpenGLComposite->Stop();
|
||||
delete pOpenGLComposite;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
|
||||
}
|
||||
|
||||
// Deselect the current rendering context and delete it
|
||||
wglMakeCurrent(hDC, NULL);
|
||||
wglDeleteContext(hRC);
|
||||
|
||||
// Tell the application to terminate after the window is gone
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
|
||||
case WM_ENTERSIZEMOVE:
|
||||
sInteractiveResize = true;
|
||||
break;
|
||||
|
||||
case WM_EXITSIZEMOVE:
|
||||
sInteractiveResize = false;
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
RECT clientRect = {};
|
||||
if (GetClientRect(hWnd, &clientRect))
|
||||
pOpenGLComposite->resizeGL(static_cast<WORD>(clientRect.right - clientRect.left), static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||
}
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
break;
|
||||
|
||||
case WM_SIZE:
|
||||
try
|
||||
{
|
||||
if (pOpenGLComposite)
|
||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_PAINT:
|
||||
try
|
||||
{
|
||||
PAINTSTRUCT paint = {};
|
||||
BeginPaint(hWnd, &paint);
|
||||
EndPaint(hWnd, &paint);
|
||||
|
||||
if (!sInteractiveResize && pOpenGLComposite)
|
||||
{
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
pOpenGLComposite->paintGL();
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_KEYDOWN:
|
||||
try
|
||||
{
|
||||
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
|
||||
{
|
||||
pOpenGLComposite->ReloadShader();
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
||||
}
|
||||
|
||||
return (0L);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.h
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,95 +0,0 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
#include "targetver.h"
|
||||
#endif
|
||||
#define APSTUDIO_HIDDEN_SYMBOLS
|
||||
#include "windows.h"
|
||||
#undef APSTUDIO_HIDDEN_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (U.S.) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
#ifdef _WIN32
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
#pragma code_page(1252)
|
||||
#endif //_WIN32
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_OPENGLOUTPUT ICON "LoopThroughWithOpenGLCompositing.ico"
|
||||
IDI_SMALL ICON "small.ico"
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#ifndef APSTUDIO_INVOKED\r\n"
|
||||
"#include ""targetver.h""\r\n"
|
||||
"#endif\r\n"
|
||||
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||
"#include ""windows.h""\r\n"
|
||||
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// String Table
|
||||
//
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_APP_TITLE "Video Shader Toys"
|
||||
IDC_OPENGLOUTPUT "OPENGLOUTPUT"
|
||||
END
|
||||
|
||||
#endif // English (U.S.) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2013
|
||||
VisualStudioVersion = 12.0.21005.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LoopThroughWithOpenGLCompositing", "LoopThroughWithOpenGLCompositing.vcxproj", "{92C79085-CA51-4008-95DB-5403D2E19885}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Win32 = Debug|Win32
|
||||
Debug|x64 = Debug|x64
|
||||
Release|Win32 = Release|Win32
|
||||
Release|x64 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|Win32.ActiveCfg = Debug|Win32
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|Win32.Build.0 = Debug|Win32
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|x64.Build.0 = Debug|x64
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|Win32.ActiveCfg = Release|Win32
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|Win32.Build.0 = Release|Win32
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|x64.ActiveCfg = Release|x64
|
||||
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,232 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{92C79085-CA51-4008-95DB-5403D2E19885}</ProjectGuid>
|
||||
<RootNamespace>LoopThroughWithOpenGLCompositing</RootNamespace>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<_ProjectFileVersion>12.0.21005.1</_ProjectFileVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
|
||||
<IntDir>$(Configuration)\</IntDir>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
|
||||
<IntDir>$(Configuration)\</IntDir>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PrecompiledHeader />
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Midl>
|
||||
<TargetEnvironment>X64</TargetEnvironment>
|
||||
</Midl>
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<PrecompiledHeader />
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<PrecompiledHeader />
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Message>Copy nececssary DLLs to target directory</Message>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Midl>
|
||||
<TargetEnvironment>X64</TargetEnvironment>
|
||||
</Midl>
|
||||
<ClCompile>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<PrecompiledHeader />
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="GLExtensions.cpp" />
|
||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
|
||||
<ClCompile Include="OpenGLComposite.cpp" />
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="VideoFrameTransfer.cpp" />
|
||||
<ClCompile Include="DeckLinkAPI_i.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="GLExtensions.h" />
|
||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
|
||||
<ClInclude Include="OpenGLComposite.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="VideoFrameTransfer.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
|
||||
<Image Include="small.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="video_effect.slang" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LoopThroughWithOpenGLCompositing.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\include\DeckLinkAPI.idl" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="DeckLink API">
|
||||
<UniqueIdentifier>{1eab21d6-58f8-49e0-929b-8a4482e04756}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="GLExtensions.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="OpenGLComposite.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="VideoFrameTransfer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="DeckLinkAPI_i.c">
|
||||
<Filter>DeckLink API</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="GLExtensions.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="OpenGLComposite.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="stdafx.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="targetver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="VideoFrameTransfer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="LoopThroughWithOpenGLCompositing.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
<Image Include="small.ico">
|
||||
<Filter>Resource Files</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="LoopThroughWithOpenGLCompositing.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="..\..\include\DeckLinkAPI.idl">
|
||||
<Filter>DeckLink API</Filter>
|
||||
</Midl>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="video_effect.slang">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,407 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
|
||||
#ifndef __OPENGL_COMPOSITE_H__
|
||||
#define __OPENGL_COMPOSITE_H__
|
||||
|
||||
#include <windows.h>
|
||||
#include <process.h>
|
||||
#include <tchar.h>
|
||||
#include <gl/gl.h>
|
||||
#include <gl/glu.h>
|
||||
|
||||
#include <objbase.h>
|
||||
#include <atlbase.h>
|
||||
#include <comutil.h>
|
||||
#include "DeckLinkAPI_h.h"
|
||||
|
||||
#include "AudioSupport.h"
|
||||
#include "VideoFrameTransfer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
class PlayoutDelegate;
|
||||
class CaptureDelegate;
|
||||
class PinnedMemoryAllocator;
|
||||
class ControlServer;
|
||||
class OscServer;
|
||||
|
||||
|
||||
class OpenGLComposite
|
||||
{
|
||||
public:
|
||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
||||
~OpenGLComposite();
|
||||
|
||||
bool InitDeckLink();
|
||||
bool Start();
|
||||
bool Stop();
|
||||
bool ReloadShader();
|
||||
std::string GetRuntimeStateJson() const;
|
||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
|
||||
void resizeGL(WORD width, WORD height);
|
||||
void paintGL();
|
||||
|
||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||
void AudioPacketArrived(IDeckLinkAudioInputPacket* audioPacket);
|
||||
HRESULT RenderAudioSamples(BOOL preroll);
|
||||
HRESULT ScheduleAudioToWaterLevel();
|
||||
void AudioSchedulingLoop();
|
||||
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
||||
|
||||
private:
|
||||
void resizeWindow(int width, int height);
|
||||
bool CheckOpenGLExtensions();
|
||||
|
||||
CaptureDelegate* mCaptureDelegate;
|
||||
PlayoutDelegate* mPlayoutDelegate;
|
||||
HWND hGLWnd;
|
||||
HDC hGLDC;
|
||||
HGLRC hGLRC;
|
||||
CRITICAL_SECTION pMutex;
|
||||
|
||||
// DeckLink
|
||||
IDeckLinkInput* mDLInput;
|
||||
IDeckLinkOutput* mDLOutput;
|
||||
IDeckLinkConfiguration* mDLInputConfiguration;
|
||||
IDeckLinkKeyer* mDLKeyer;
|
||||
std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue;
|
||||
PinnedMemoryAllocator* mPlayoutAllocator;
|
||||
BMDTimeValue mFrameDuration;
|
||||
BMDTimeScale mFrameTimescale;
|
||||
unsigned mTotalPlayoutFrames;
|
||||
uint64_t mAudioOutputSampleTime;
|
||||
unsigned mInputFrameWidth;
|
||||
unsigned mInputFrameHeight;
|
||||
unsigned mOutputFrameWidth;
|
||||
unsigned mOutputFrameHeight;
|
||||
std::string mInputDisplayModeName;
|
||||
std::string mOutputDisplayModeName;
|
||||
bool mHasNoInputSource;
|
||||
std::string mDeckLinkOutputModelName;
|
||||
bool mDeckLinkSupportsInternalKeying;
|
||||
bool mDeckLinkSupportsExternalKeying;
|
||||
bool mDeckLinkKeyerInterfaceAvailable;
|
||||
bool mDeckLinkExternalKeyingActive;
|
||||
std::string mDeckLinkStatusMessage;
|
||||
|
||||
// OpenGL data
|
||||
bool mFastTransferExtensionAvailable;
|
||||
GLuint mCaptureTexture;
|
||||
GLuint mDecodedTexture;
|
||||
GLuint mLayerTempTexture;
|
||||
GLuint mFBOTexture;
|
||||
GLuint mOutputTexture;
|
||||
GLuint mAudioDataTexture;
|
||||
GLuint mUnpinnedTextureBuffer;
|
||||
GLuint mDecodeFrameBuf;
|
||||
GLuint mLayerTempFrameBuf;
|
||||
GLuint mIdFrameBuf;
|
||||
GLuint mOutputFrameBuf;
|
||||
GLuint mIdColorBuf;
|
||||
GLuint mIdDepthBuf;
|
||||
GLuint mFullscreenVAO;
|
||||
GLuint mGlobalParamsUBO;
|
||||
GLuint mDecodeProgram;
|
||||
GLuint mDecodeVertexShader;
|
||||
GLuint mDecodeFragmentShader;
|
||||
GLsizeiptr mGlobalParamsUBOSize;
|
||||
int mViewWidth;
|
||||
int mViewHeight;
|
||||
std::unique_ptr<RuntimeHost> mRuntimeHost;
|
||||
std::unique_ptr<ControlServer> mControlServer;
|
||||
std::unique_ptr<OscServer> mOscServer;
|
||||
bool mAudioEnabled;
|
||||
bool mAudioOutputEnabled;
|
||||
bool mAudioScheduleEnabled;
|
||||
bool mAudioPrerollEnabled;
|
||||
bool mAudioScheduleSilence;
|
||||
bool mAudioScheduleTone;
|
||||
bool mAudioPrerolling;
|
||||
std::atomic<bool> mAudioSchedulerRunning;
|
||||
std::atomic<bool> mPlayoutCallbackActive;
|
||||
std::thread mAudioSchedulerThread;
|
||||
std::mutex mAudioStateMutex;
|
||||
std::mutex mAudioAnalyzerMutex;
|
||||
AudioAnalyzer mAudioAnalyzer;
|
||||
AudioAnalysisSnapshot mAudioAnalysis;
|
||||
struct TimestampedAudioPacket
|
||||
{
|
||||
AudioFrameBlock block;
|
||||
std::vector<int32_t> scheduledOutputSamples;
|
||||
BMDTimeValue streamTime = 0;
|
||||
};
|
||||
std::deque<TimestampedAudioPacket> mAudioPacketQueue;
|
||||
std::deque<TimestampedAudioPacket> mScheduledAudioPacketRetainQueue;
|
||||
std::deque<int32_t> mAudioSampleQueue;
|
||||
std::condition_variable mAudioPacketQueued;
|
||||
unsigned mQueuedAudioSampleFrames = 0;
|
||||
uint64_t mAudioUnderrunCount = 0;
|
||||
uint64_t mAudioToneSampleIndex = 0;
|
||||
bool mHasFirstAudioPacketTime = false;
|
||||
BMDTimeValue mFirstAudioPacketTime = 0;
|
||||
|
||||
struct LayerProgram
|
||||
{
|
||||
struct TextureBinding
|
||||
{
|
||||
std::string samplerName;
|
||||
std::filesystem::path sourcePath;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
std::vector<TextureBinding> textureBindings;
|
||||
};
|
||||
std::vector<LayerProgram> mLayerPrograms;
|
||||
|
||||
struct HistorySlot
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
};
|
||||
|
||||
struct HistoryRing
|
||||
{
|
||||
std::vector<HistorySlot> slots;
|
||||
std::size_t nextWriteIndex = 0;
|
||||
std::size_t filledCount = 0;
|
||||
unsigned effectiveLength = 0;
|
||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
||||
};
|
||||
|
||||
HistoryRing mSourceHistoryRing;
|
||||
std::map<std::string, HistoryRing> mPreLayerHistoryByLayerId;
|
||||
bool mTemporalHistoryNeedsReset;
|
||||
|
||||
bool InitOpenGLState();
|
||||
bool compileLayerPrograms(int errorMessageSize, char* errorMessage);
|
||||
bool compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||
void destroyLayerPrograms();
|
||||
void destroySingleLayerProgram(LayerProgram& layerProgram);
|
||||
void destroyDecodeShaderProgram();
|
||||
void renderDecodePass();
|
||||
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
|
||||
bool loadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||
void bindLayerTextureAssets(const LayerProgram& layerProgram);
|
||||
void renderEffect();
|
||||
bool PollRuntimeChanges();
|
||||
void broadcastRuntimeState();
|
||||
void initializeAudioDelay();
|
||||
BMDTimeValue delayedAudioStreamTime() const;
|
||||
void updateAudioDataTexture(const AudioAnalysisSnapshot& analysis);
|
||||
void updateAudioStatus();
|
||||
bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
||||
bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const;
|
||||
bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error);
|
||||
bool createHistoryRing(HistoryRing& ring, unsigned effectiveLength, TemporalHistorySource historySource, std::string& error);
|
||||
void destroyHistoryRing(HistoryRing& ring);
|
||||
void destroyTemporalHistoryResources();
|
||||
void resetTemporalHistoryState();
|
||||
void pushFramebufferToHistoryRing(GLuint sourceFramebuffer, HistoryRing& ring);
|
||||
void bindHistorySamplers(const RuntimeRenderState& state, GLuint currentSourceTexture);
|
||||
GLuint resolveHistoryTexture(const HistoryRing& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
||||
unsigned sourceHistoryAvailableCount() const;
|
||||
unsigned temporalHistoryAvailableCountForLayer(const std::string& layerId) const;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// PinnedMemoryAllocator
|
||||
////////////////////////////////////////////
|
||||
class PinnedMemoryAllocator : public IDeckLinkVideoBufferAllocator
|
||||
{
|
||||
public:
|
||||
PinnedMemoryAllocator(HDC hdc, HGLRC hglrc, VideoFrameTransfer::Direction direction, unsigned cacheSize, unsigned bufferSize);
|
||||
virtual ~PinnedMemoryAllocator();
|
||||
|
||||
bool transferFrame(void* address, GLuint gpuTexture);
|
||||
void waitForTransferComplete(void* address);
|
||||
unsigned bufferSize() { return mBufferSize; }
|
||||
|
||||
// IUnknown methods
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
||||
|
||||
// IDeckLinkVideoBufferAllocator methods
|
||||
virtual HRESULT STDMETHODCALLTYPE AllocateVideoBuffer (IDeckLinkVideoBuffer** allocatedBuffer) override;
|
||||
|
||||
private:
|
||||
|
||||
void unPinAddress(void* address);
|
||||
|
||||
private:
|
||||
HDC mHGLDC;
|
||||
HGLRC mHGLRC;
|
||||
std::atomic<ULONG> mRefCount;
|
||||
VideoFrameTransfer::Direction mDirection;
|
||||
std::map<void*, VideoFrameTransfer*> mFrameTransfer;
|
||||
unsigned mBufferSize;
|
||||
std::vector<void*> mFrameCache;
|
||||
unsigned mFrameCacheSize;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// InputAllocatorPool
|
||||
////////////////////////////////////////////
|
||||
|
||||
class InputAllocatorPool : public IDeckLinkVideoBufferAllocatorProvider
|
||||
{
|
||||
public:
|
||||
InputAllocatorPool(HDC hdc, HGLRC hglrc);
|
||||
|
||||
// IUnknown interface
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) override;
|
||||
|
||||
// IDeckLinkVideoBufferAllocatorProvider interface
|
||||
HRESULT STDMETHODCALLTYPE GetVideoBufferAllocator(
|
||||
/* [in] */ unsigned int bufferSize,
|
||||
/* [in] */ unsigned int width,
|
||||
/* [in] */ unsigned int height,
|
||||
/* [in] */ unsigned int rowBytes,
|
||||
/* [in] */ BMDPixelFormat pixelFormat,
|
||||
/* [out] */ IDeckLinkVideoBufferAllocator **allocator) override;
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> mRefCount;
|
||||
std::map<unsigned int, CComPtr<PinnedMemoryAllocator> > mAllocatorBySize;
|
||||
HDC mHDC;
|
||||
HGLRC mHGLRC;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// DeckLinkVideoBuffer
|
||||
////////////////////////////////////////////
|
||||
class DeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||
{
|
||||
public:
|
||||
explicit DeckLinkVideoBuffer(std::shared_ptr<void>& buffer, PinnedMemoryAllocator* parent);
|
||||
virtual ~DeckLinkVideoBuffer() = default;
|
||||
|
||||
// IUnknown interface
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
|
||||
virtual ULONG STDMETHODCALLTYPE Release(void) override;
|
||||
|
||||
// IDeckLinkVideoBuffer interface
|
||||
virtual HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetSize(uint64_t* size) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags flags) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags flags) override;
|
||||
|
||||
private:
|
||||
CComPtr<PinnedMemoryAllocator> mParentAllocator; // Dual-purpose: allocator owns mem this points to, and to access transferFrame() via a QueryInterface
|
||||
std::atomic<ULONG> mRefCount;
|
||||
std::shared_ptr<void> mBuffer;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////
|
||||
// Capture Delegate Class
|
||||
////////////////////////////////////////////
|
||||
|
||||
class CaptureDelegate : public IDeckLinkInputCallback
|
||||
{
|
||||
OpenGLComposite* m_pOwner;
|
||||
LONG mRefCount;
|
||||
|
||||
public:
|
||||
CaptureDelegate (OpenGLComposite* pOwner);
|
||||
|
||||
// IUnknown needs only a dummy implementation
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived (IDeckLinkVideoInputFrame *videoFrame, IDeckLinkAudioInputPacket *audioPacket);
|
||||
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged (BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// Render Delegate Class
|
||||
////////////////////////////////////////////
|
||||
|
||||
class PlayoutDelegate : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback
|
||||
{
|
||||
OpenGLComposite* m_pOwner;
|
||||
LONG mRefCount;
|
||||
|
||||
public:
|
||||
PlayoutDelegate (OpenGLComposite* pOwner);
|
||||
|
||||
// IUnknown needs only a dummy implementation
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef ();
|
||||
virtual ULONG STDMETHODCALLTYPE Release ();
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
|
||||
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped ();
|
||||
virtual HRESULT STDMETHODCALLTYPE RenderAudioSamples (BOOL preroll);
|
||||
};
|
||||
|
||||
#endif // __OPENGL_COMPOSITE_H__
|
||||
@@ -1,314 +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(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;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
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 UDP port " + std::to_string(port) + ".";
|
||||
mSocket.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
mRunning = true;
|
||||
mThread = std::thread(&OscServer::ServerLoop, this);
|
||||
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,52 +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(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 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;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,176 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost
|
||||
{
|
||||
public:
|
||||
RuntimeHost();
|
||||
|
||||
bool Initialize(std::string& error);
|
||||
|
||||
bool PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
||||
bool ManualReloadRequested();
|
||||
void ClearReloadRequest();
|
||||
|
||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
||||
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
|
||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
void SetAudioStatus(const AudioStatusSnapshot& status);
|
||||
void AdvanceFrame();
|
||||
|
||||
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||
std::string BuildStateJson() const;
|
||||
|
||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
||||
const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; }
|
||||
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
||||
unsigned short GetServerPort() const { return mServerPort; }
|
||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
||||
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
||||
bool AudioEnabled() const { return mConfig.audioEnabled; }
|
||||
bool AudioOutputEnabled() const { return mConfig.audioOutputEnabled; }
|
||||
bool AudioScheduleEnabled() const { return mConfig.audioScheduleEnabled; }
|
||||
bool AudioPrerollEnabled() const { return mConfig.audioPrerollEnabled; }
|
||||
bool AudioScheduleSilence() const { return mConfig.audioScheduleSilence; }
|
||||
bool AudioScheduleTone() const { return mConfig.audioScheduleTone; }
|
||||
unsigned AudioChannelCount() const { return mConfig.audioChannelCount; }
|
||||
unsigned AudioSampleRate() const { return mConfig.audioSampleRate; }
|
||||
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
|
||||
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
|
||||
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
|
||||
const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; }
|
||||
void SetServerPort(unsigned short port);
|
||||
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
||||
|
||||
private:
|
||||
struct AppConfig
|
||||
{
|
||||
std::string shaderLibrary = "shaders";
|
||||
unsigned short serverPort = 8080;
|
||||
unsigned short oscPort = 9000;
|
||||
bool autoReload = true;
|
||||
unsigned maxTemporalHistoryFrames = 4;
|
||||
bool enableExternalKeying = false;
|
||||
bool audioEnabled = true;
|
||||
bool audioOutputEnabled = true;
|
||||
bool audioScheduleEnabled = true;
|
||||
bool audioPrerollEnabled = true;
|
||||
bool audioScheduleSilence = false;
|
||||
bool audioScheduleTone = false;
|
||||
unsigned audioChannelCount = kAudioChannelCount;
|
||||
unsigned audioSampleRate = kAudioSampleRate;
|
||||
std::string audioDelayMode = "matchVideoPreroll";
|
||||
std::string inputVideoFormat = "1080p";
|
||||
std::string inputFrameRate = "59.94";
|
||||
std::string outputVideoFormat = "1080p";
|
||||
std::string outputFrameRate = "59.94";
|
||||
};
|
||||
|
||||
struct DeckLinkOutputStatus
|
||||
{
|
||||
std::string modelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingRequested = false;
|
||||
bool externalKeyingActive = false;
|
||||
std::string statusMessage;
|
||||
};
|
||||
|
||||
struct LayerPersistentState
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct PersistentState
|
||||
{
|
||||
std::vector<LayerPersistentState> layers;
|
||||
};
|
||||
|
||||
bool LoadConfig(std::string& error);
|
||||
bool LoadPersistentState(std::string& error);
|
||||
bool SavePersistentState(std::string& error) const;
|
||||
bool ScanShaderPackages(std::string& error);
|
||||
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
||||
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||
bool ResolvePaths(std::string& error);
|
||||
JsonValue BuildStateValue() const;
|
||||
JsonValue SerializeLayerStackLocked() const;
|
||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||
std::vector<std::string> GetStackPresetNamesLocked() const;
|
||||
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
||||
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
||||
std::string TemporalHistorySourceToString(TemporalHistorySource source) const;
|
||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
std::string GenerateLayerId();
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
AppConfig mConfig;
|
||||
PersistentState mPersistentState;
|
||||
std::filesystem::path mRepoRoot;
|
||||
std::filesystem::path mUiRoot;
|
||||
std::filesystem::path mDocsRoot;
|
||||
std::filesystem::path mShaderRoot;
|
||||
std::filesystem::path mRuntimeRoot;
|
||||
std::filesystem::path mPresetRoot;
|
||||
std::filesystem::path mRuntimeStatePath;
|
||||
std::filesystem::path mConfigPath;
|
||||
std::filesystem::path mWrapperPath;
|
||||
std::filesystem::path mGeneratedGlslPath;
|
||||
std::filesystem::path mPatchedGlslPath;
|
||||
std::map<std::string, ShaderPackage> mPackagesById;
|
||||
std::vector<std::string> mPackageOrder;
|
||||
bool mReloadRequested;
|
||||
bool mCompileSucceeded;
|
||||
std::string mCompileMessage;
|
||||
bool mHasSignal;
|
||||
unsigned mSignalWidth;
|
||||
unsigned mSignalHeight;
|
||||
std::string mSignalModeName;
|
||||
double mFrameBudgetMilliseconds;
|
||||
double mRenderMilliseconds;
|
||||
double mSmoothedRenderMilliseconds;
|
||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||
AudioStatusSnapshot mAudioStatus;
|
||||
unsigned short mServerPort;
|
||||
bool mAutoReloadEnabled;
|
||||
std::chrono::steady_clock::time_point mStartTime;
|
||||
std::chrono::steady_clock::time_point mLastScanTime;
|
||||
uint64_t mFrameCounter;
|
||||
uint64_t mNextLayerId;
|
||||
};
|
||||
@@ -1,549 +0,0 @@
|
||||
#include "stdafx.h"
|
||||
#include "ShaderPackageRegistry.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string Trim(const std::string& text)
|
||||
{
|
||||
std::size_t start = 0;
|
||||
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
|
||||
++start;
|
||||
|
||||
std::size_t end = text.size();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
|
||||
--end;
|
||||
|
||||
return text.substr(start, end - start);
|
||||
}
|
||||
|
||||
bool IsFiniteNumber(double value)
|
||||
{
|
||||
return std::isfinite(value) != 0;
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
for (const JsonValue& item : value.asArray())
|
||||
{
|
||||
if (item.isNumber())
|
||||
numbers.push_back(item.asNumber());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
||||
{
|
||||
if (typeName == "float")
|
||||
{
|
||||
type = ShaderParameterType::Float;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "vec2")
|
||||
{
|
||||
type = ShaderParameterType::Vec2;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "color")
|
||||
{
|
||||
type = ShaderParameterType::Color;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "bool")
|
||||
{
|
||||
type = ShaderParameterType::Boolean;
|
||||
return true;
|
||||
}
|
||||
if (typeName == "enum")
|
||||
{
|
||||
type = ShaderParameterType::Enum;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
|
||||
{
|
||||
if (sourceName == "source")
|
||||
{
|
||||
source = TemporalHistorySource::Source;
|
||||
return true;
|
||||
}
|
||||
if (sourceName == "preLayerInput")
|
||||
{
|
||||
source = TemporalHistorySource::PreLayerInput;
|
||||
return true;
|
||||
}
|
||||
if (sourceName == "none")
|
||||
{
|
||||
source = TemporalHistorySource::None;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
{
|
||||
error = "Could not open file: " + path.string();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
||||
{
|
||||
return manifestPath.string();
|
||||
}
|
||||
|
||||
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* fieldValue = object.find(fieldName);
|
||||
if (!fieldValue || !fieldValue->isString())
|
||||
{
|
||||
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
value = fieldValue->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (!RequireStringField(object, fieldName, value, manifestPath, error))
|
||||
return false;
|
||||
if (Trim(value).empty())
|
||||
{
|
||||
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* fieldValue = object.find(fieldName);
|
||||
if (!fieldValue)
|
||||
{
|
||||
value = fallback;
|
||||
return true;
|
||||
}
|
||||
if (!fieldValue->isString())
|
||||
{
|
||||
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
value = fieldValue->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
value = object.find(fieldName);
|
||||
if (!value)
|
||||
return true;
|
||||
if (!value->isArray())
|
||||
{
|
||||
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
value = object.find(fieldName);
|
||||
if (!value)
|
||||
return true;
|
||||
if (!value->isObject())
|
||||
{
|
||||
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (value.isNumber())
|
||||
{
|
||||
numbers.push_back(value.asNumber());
|
||||
return true;
|
||||
}
|
||||
if (value.isArray())
|
||||
{
|
||||
numbers = JsonArrayToNumbers(value);
|
||||
if (numbers.size() != value.asArray().size())
|
||||
{
|
||||
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
|
||||
{
|
||||
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char ch : identifier)
|
||||
{
|
||||
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
|
||||
if (!(std::isalnum(unsignedCh) || ch == '_'))
|
||||
{
|
||||
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
|
||||
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
|
||||
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
|
||||
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
|
||||
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
|
||||
return false;
|
||||
|
||||
shaderPackage.directoryPath = manifestPath.parent_path();
|
||||
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
|
||||
shaderPackage.manifestPath = manifestPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* texturesValue = nullptr;
|
||||
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
|
||||
return false;
|
||||
if (!texturesValue)
|
||||
return true;
|
||||
|
||||
for (const JsonValue& textureJson : texturesValue->asArray())
|
||||
{
|
||||
if (!textureJson.isObject())
|
||||
{
|
||||
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string textureId;
|
||||
std::string texturePath;
|
||||
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
|
||||
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
|
||||
{
|
||||
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
|
||||
return false;
|
||||
|
||||
ShaderTextureAsset textureAsset;
|
||||
textureAsset.id = textureId;
|
||||
textureAsset.path = shaderPackage.directoryPath / texturePath;
|
||||
if (!std::filesystem::exists(textureAsset.path))
|
||||
{
|
||||
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
|
||||
shaderPackage.textureAssets.push_back(textureAsset);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* temporalValue = nullptr;
|
||||
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
|
||||
return false;
|
||||
if (!temporalValue)
|
||||
return true;
|
||||
|
||||
const JsonValue* enabledValue = temporalValue->find("enabled");
|
||||
if (!enabledValue || !enabledValue->asBoolean(false))
|
||||
return true;
|
||||
|
||||
std::string historySourceName;
|
||||
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
|
||||
{
|
||||
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
|
||||
if (!historyLengthValue || !historyLengthValue->isNumber())
|
||||
{
|
||||
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
||||
if (!ParseTemporalHistorySource(historySourceName, historySource))
|
||||
{
|
||||
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const double requestedHistoryLength = historyLengthValue->asNumber();
|
||||
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
|
||||
{
|
||||
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
shaderPackage.temporal.enabled = true;
|
||||
shaderPackage.temporal.historySource = historySource;
|
||||
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
|
||||
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
|
||||
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* defaultValue = parameterJson.find("default");
|
||||
if (!defaultValue)
|
||||
return true;
|
||||
|
||||
if (definition.type == ShaderParameterType::Boolean)
|
||||
{
|
||||
if (!defaultValue->isBoolean())
|
||||
{
|
||||
error = "Boolean parameter default must be a boolean for: " + definition.id;
|
||||
return false;
|
||||
}
|
||||
definition.defaultBoolean = defaultValue->asBoolean(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (definition.type == ShaderParameterType::Enum)
|
||||
{
|
||||
if (!defaultValue->isString())
|
||||
{
|
||||
error = "Enum parameter default must be a string for: " + definition.id;
|
||||
return false;
|
||||
}
|
||||
definition.defaultEnumValue = defaultValue->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
||||
}
|
||||
|
||||
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* optionsValue = nullptr;
|
||||
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
|
||||
{
|
||||
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const JsonValue& optionJson : optionsValue->asArray())
|
||||
{
|
||||
if (!optionJson.isObject())
|
||||
{
|
||||
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
ShaderParameterOption option;
|
||||
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
|
||||
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
|
||||
{
|
||||
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
definition.enumOptions.push_back(option);
|
||||
}
|
||||
|
||||
bool defaultFound = definition.defaultEnumValue.empty();
|
||||
for (const ShaderParameterOption& option : definition.enumOptions)
|
||||
{
|
||||
if (option.value == definition.defaultEnumValue)
|
||||
{
|
||||
defaultFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defaultFound)
|
||||
{
|
||||
error = "Enum parameter default is not present in its option list for: " + definition.id;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (!parameterJson.isObject())
|
||||
{
|
||||
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string typeName;
|
||||
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
|
||||
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
|
||||
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
|
||||
{
|
||||
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ParseShaderParameterType(typeName, definition.type))
|
||||
{
|
||||
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.type == ShaderParameterType::Enum)
|
||||
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* parametersValue = nullptr;
|
||||
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
|
||||
return false;
|
||||
if (!parametersValue)
|
||||
return true;
|
||||
|
||||
for (const JsonValue& parameterJson : parametersValue->asArray())
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
|
||||
return false;
|
||||
shaderPackage.parameters.push_back(definition);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
|
||||
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
|
||||
{
|
||||
}
|
||||
|
||||
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
|
||||
{
|
||||
packagesById.clear();
|
||||
packageOrder.clear();
|
||||
|
||||
if (!std::filesystem::exists(shaderRoot))
|
||||
{
|
||||
error = "Shader library directory does not exist: " + shaderRoot.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
continue;
|
||||
|
||||
std::filesystem::path manifestPath = entry.path() / "shader.json";
|
||||
if (!std::filesystem::exists(manifestPath))
|
||||
continue;
|
||||
|
||||
ShaderPackage shaderPackage;
|
||||
if (!ParseManifest(manifestPath, shaderPackage, error))
|
||||
return false;
|
||||
|
||||
if (packagesById.find(shaderPackage.id) != packagesById.end())
|
||||
{
|
||||
error = "Duplicate shader id found: " + shaderPackage.id;
|
||||
return false;
|
||||
}
|
||||
|
||||
packageOrder.push_back(shaderPackage.id);
|
||||
packagesById[shaderPackage.id] = shaderPackage;
|
||||
}
|
||||
|
||||
std::sort(packageOrder.begin(), packageOrder.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
|
||||
{
|
||||
const std::string manifestText = ReadTextFile(manifestPath, error);
|
||||
if (manifestText.empty())
|
||||
return false;
|
||||
|
||||
JsonValue manifestJson;
|
||||
if (!ParseJson(manifestText, manifestJson, error))
|
||||
return false;
|
||||
if (!manifestJson.isObject())
|
||||
{
|
||||
error = "Shader manifest root must be an object: " + manifestPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(shaderPackage.shaderPath))
|
||||
{
|
||||
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
|
||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
|
||||
#include "VideoFrameTransfer.h"
|
||||
#include "NativeHandles.h"
|
||||
|
||||
|
||||
#define DVP_CHECK(cmd) { \
|
||||
DVPStatus hr = (cmd); \
|
||||
if (DVP_STATUS_OK != hr) { \
|
||||
OutputDebugStringA( #cmd " failed\n" ); \
|
||||
ExitProcess(hr); \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
// Initialise static members
|
||||
bool VideoFrameTransfer::mInitialized = false;
|
||||
bool VideoFrameTransfer::mUseDvp = false;
|
||||
unsigned VideoFrameTransfer::mWidth = 0;
|
||||
unsigned VideoFrameTransfer::mHeight = 0;
|
||||
GLuint VideoFrameTransfer::mCaptureTexture = 0;
|
||||
|
||||
// NVIDIA specific static members
|
||||
DVPBufferHandle VideoFrameTransfer::mDvpCaptureTextureHandle = 0;
|
||||
DVPBufferHandle VideoFrameTransfer::mDvpPlaybackTextureHandle = 0;
|
||||
uint32_t VideoFrameTransfer::mBufferAddrAlignment = 0;
|
||||
uint32_t VideoFrameTransfer::mBufferGpuStrideAlignment = 0;
|
||||
uint32_t VideoFrameTransfer::mSemaphoreAddrAlignment = 0;
|
||||
uint32_t VideoFrameTransfer::mSemaphoreAllocSize = 0;
|
||||
uint32_t VideoFrameTransfer::mSemaphorePayloadOffset = 0;
|
||||
uint32_t VideoFrameTransfer::mSemaphorePayloadSize = 0;
|
||||
|
||||
|
||||
bool VideoFrameTransfer::isNvidiaDvpAvailable()
|
||||
{
|
||||
// Look for supported graphics boards
|
||||
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||
if (renderer == NULL)
|
||||
return false;
|
||||
|
||||
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
|
||||
return hasDvp;
|
||||
}
|
||||
|
||||
bool VideoFrameTransfer::isAMDPinnedMemoryAvailable()
|
||||
{
|
||||
// GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported
|
||||
const GLubyte* strExt = glGetString(GL_EXTENSIONS);
|
||||
if (strExt == NULL)
|
||||
{
|
||||
// In a core profile context GL_EXTENSIONS is no longer queryable via glGetString().
|
||||
// Treat this as "extension unavailable" for now; the fast-transfer path is optional.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL);
|
||||
return hasAMDPinned;
|
||||
}
|
||||
|
||||
bool VideoFrameTransfer::checkFastMemoryTransferAvailable()
|
||||
{
|
||||
return (isNvidiaDvpAvailable() || isAMDPinnedMemoryAvailable());
|
||||
}
|
||||
|
||||
bool VideoFrameTransfer::initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture)
|
||||
{
|
||||
if (mInitialized)
|
||||
return false;
|
||||
|
||||
bool hasDvp = isNvidiaDvpAvailable();
|
||||
bool hasAMDPinned = isAMDPinnedMemoryAvailable();
|
||||
|
||||
if (!hasDvp && !hasAMDPinned)
|
||||
return false;
|
||||
|
||||
mUseDvp = hasDvp;
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mCaptureTexture = captureTexture;
|
||||
|
||||
if (! initializeMemoryLocking(mWidth * mHeight * 4)) // BGRA uses 4 bytes per pixel
|
||||
return false;
|
||||
|
||||
if (mUseDvp)
|
||||
{
|
||||
// DVP initialisation
|
||||
DVP_CHECK(dvpInitGLContext(DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT));
|
||||
DVP_CHECK(dvpGetRequiredConstantsGLCtx( &mBufferAddrAlignment, &mBufferGpuStrideAlignment,
|
||||
&mSemaphoreAddrAlignment, &mSemaphoreAllocSize,
|
||||
&mSemaphorePayloadOffset, &mSemaphorePayloadSize));
|
||||
|
||||
// Register textures with DVP
|
||||
DVP_CHECK(dvpCreateGPUTextureGL(captureTexture, &mDvpCaptureTextureHandle));
|
||||
DVP_CHECK(dvpCreateGPUTextureGL(playbackTexture, &mDvpPlaybackTextureHandle));
|
||||
}
|
||||
|
||||
mInitialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VideoFrameTransfer::initializeMemoryLocking(unsigned memSize)
|
||||
{
|
||||
// Increase the process working set size to allow pinning of memory.
|
||||
static SIZE_T dwMin = 0, dwMax = 0;
|
||||
UniqueHandle processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, FALSE, GetCurrentProcessId()));
|
||||
if (!processHandle.valid())
|
||||
return false;
|
||||
|
||||
// Retrieve the working set size of the process.
|
||||
if (!dwMin && !GetProcessWorkingSetSize(processHandle.get(), &dwMin, &dwMax))
|
||||
return false;
|
||||
|
||||
// Allow for 80 frames to be locked
|
||||
BOOL res = SetProcessWorkingSetSize(processHandle.get(), memSize * 80 + dwMin, memSize * 80 + (dwMax-dwMin));
|
||||
if (!res)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// SyncInfo sets up a semaphore which is shared between the GPU and CPU and used to
|
||||
// synchronise access to DVP buffers.
|
||||
struct SyncInfo
|
||||
{
|
||||
SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment);
|
||||
~SyncInfo();
|
||||
|
||||
volatile uint32_t* mSem;
|
||||
volatile uint32_t mReleaseValue;
|
||||
volatile uint32_t mAcquireValue;
|
||||
DVPSyncObjectHandle mDvpSync;
|
||||
};
|
||||
|
||||
SyncInfo::SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment)
|
||||
{
|
||||
mSem = (uint32_t*)_aligned_malloc(semaphoreAllocSize, semaphoreAddrAlignment);
|
||||
|
||||
// Initialise
|
||||
mSem[0] = 0;
|
||||
mReleaseValue = 0;
|
||||
mAcquireValue = 0;
|
||||
|
||||
// Setup DVP sync object and import it
|
||||
DVPSyncObjectDesc syncObjectDesc;
|
||||
syncObjectDesc.externalClientWaitFunc = NULL;
|
||||
syncObjectDesc.sem = (uint32_t*)mSem;
|
||||
|
||||
DVP_CHECK(dvpImportSyncObject(&syncObjectDesc, &mDvpSync));
|
||||
}
|
||||
|
||||
SyncInfo::~SyncInfo()
|
||||
{
|
||||
DVP_CHECK(dvpFreeSyncObject(mDvpSync));
|
||||
_aligned_free((void*)mSem);
|
||||
}
|
||||
|
||||
VideoFrameTransfer::VideoFrameTransfer(unsigned long memSize, void* address, Direction direction) :
|
||||
mBuffer(address),
|
||||
mMemSize(memSize),
|
||||
mDirection(direction),
|
||||
mExtSync(NULL),
|
||||
mGpuSync(NULL),
|
||||
mDvpSysMemHandle(0),
|
||||
mBufferHandle(0)
|
||||
{
|
||||
if (mUseDvp)
|
||||
{
|
||||
// Pin the memory
|
||||
if (! VirtualLock(mBuffer, mMemSize))
|
||||
throw std::runtime_error("Error pinning memory with VirtualLock");
|
||||
|
||||
// Create necessary sysmem and gpu sync objects
|
||||
mExtSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
||||
mGpuSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
|
||||
|
||||
// Register system memory buffers with DVP
|
||||
DVPSysmemBufferDesc sysMemBuffersDesc;
|
||||
sysMemBuffersDesc.width = mWidth;
|
||||
sysMemBuffersDesc.height = mHeight;
|
||||
sysMemBuffersDesc.stride = mWidth * 4;
|
||||
sysMemBuffersDesc.format = DVP_BGRA;
|
||||
sysMemBuffersDesc.type = DVP_UNSIGNED_BYTE;
|
||||
sysMemBuffersDesc.size = mMemSize;
|
||||
sysMemBuffersDesc.bufAddr = mBuffer;
|
||||
|
||||
if (mDirection == CPUtoGPU)
|
||||
{
|
||||
// A UYVY 4:2:2 frame is transferred to the GPU, rather than RGB 4:4:4, so width is halved
|
||||
sysMemBuffersDesc.width /= 2;
|
||||
sysMemBuffersDesc.stride /= 2;
|
||||
}
|
||||
|
||||
DVP_CHECK(dvpCreateBuffer(&sysMemBuffersDesc, &mDvpSysMemHandle));
|
||||
DVP_CHECK(dvpBindToGLCtx(mDvpSysMemHandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create an OpenGL buffer handle to use for pinned memory
|
||||
GLuint bufferHandle;
|
||||
glGenBuffers(1, &bufferHandle);
|
||||
|
||||
// Pin memory by binding buffer to special AMD target.
|
||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, bufferHandle);
|
||||
|
||||
// glBufferData() sets up the address so any OpenGL operation on this buffer will use system memory directly
|
||||
// (assumes address is aligned to 4k boundary).
|
||||
glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, mMemSize, address, GL_STREAM_DRAW);
|
||||
GLenum result = glGetError();
|
||||
if (result != GL_NO_ERROR)
|
||||
{
|
||||
throw std::runtime_error("Error pinning memory with glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, ...)");
|
||||
}
|
||||
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind buffer to target
|
||||
|
||||
mBufferHandle = bufferHandle;
|
||||
}
|
||||
}
|
||||
|
||||
VideoFrameTransfer::~VideoFrameTransfer()
|
||||
{
|
||||
if (mUseDvp)
|
||||
{
|
||||
DVP_CHECK(dvpUnbindFromGLCtx(mDvpSysMemHandle));
|
||||
DVP_CHECK(dvpDestroyBuffer(mDvpSysMemHandle));
|
||||
|
||||
delete mExtSync;
|
||||
delete mGpuSync;
|
||||
|
||||
VirtualUnlock(mBuffer, mMemSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The buffer is un-pinned by the GPU when the buffer is deleted
|
||||
glDeleteBuffers(1, &mBufferHandle);
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoFrameTransfer::performFrameTransfer()
|
||||
{
|
||||
if (mUseDvp)
|
||||
{
|
||||
// NVIDIA DVP transfers
|
||||
DVPStatus status;
|
||||
|
||||
mGpuSync->mReleaseValue++;
|
||||
|
||||
dvpBegin();
|
||||
if (mDirection == CPUtoGPU)
|
||||
{
|
||||
// Copy from system memory to GPU texture
|
||||
dvpMapBufferWaitDVP(mDvpCaptureTextureHandle);
|
||||
status = dvpMemcpyLined( mDvpSysMemHandle, mExtSync->mDvpSync, mExtSync->mAcquireValue, DVP_TIMEOUT_IGNORED,
|
||||
mDvpCaptureTextureHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
||||
dvpMapBufferEndDVP(mDvpCaptureTextureHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Copy from GPU texture to system memory
|
||||
dvpMapBufferWaitDVP(mDvpPlaybackTextureHandle);
|
||||
status = dvpMemcpyLined( mDvpPlaybackTextureHandle, mExtSync->mDvpSync, mExtSync->mReleaseValue, DVP_TIMEOUT_IGNORED,
|
||||
mDvpSysMemHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
|
||||
dvpMapBufferEndDVP(mDvpPlaybackTextureHandle);
|
||||
}
|
||||
dvpEnd();
|
||||
|
||||
return (status == DVP_STATUS_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
// AMD pinned memory transfers
|
||||
if (mDirection == CPUtoGPU)
|
||||
{
|
||||
glEnable(GL_TEXTURE_2D);
|
||||
|
||||
// Use a pinned buffer for the GL_PIXEL_UNPACK_BUFFER target
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mBufferHandle);
|
||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||
|
||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth/2, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||
|
||||
// Ensure pinned texture has been transferred to GPU before we draw with it
|
||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
||||
glDeleteSync(fence);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use a PIXEL PACK BUFFER to read back pixels
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, mBufferHandle);
|
||||
glReadPixels(0, 0, mWidth, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||
|
||||
// Ensure GPU has processed all commands in the pipeline up to this point, before memory is read by the CPU
|
||||
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
|
||||
glDeleteSync(fence);
|
||||
}
|
||||
|
||||
return (glGetError() == GL_NO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoFrameTransfer::waitForTransferComplete()
|
||||
{
|
||||
if (!mUseDvp)
|
||||
return;
|
||||
|
||||
// Block until buffer has completely transferred between GPU and CPU buffer
|
||||
dvpBegin();
|
||||
dvpSyncObjClientWaitComplete(mGpuSync->mDvpSync, DVP_TIMEOUT_IGNORED);
|
||||
dvpEnd();
|
||||
}
|
||||
|
||||
void VideoFrameTransfer::beginTextureInUse(Direction direction)
|
||||
{
|
||||
if (!mUseDvp)
|
||||
return;
|
||||
|
||||
if (direction == CPUtoGPU)
|
||||
dvpMapBufferWaitAPI(mDvpCaptureTextureHandle);
|
||||
else
|
||||
dvpMapBufferWaitAPI(mDvpPlaybackTextureHandle);
|
||||
}
|
||||
|
||||
void VideoFrameTransfer::endTextureInUse(Direction direction)
|
||||
{
|
||||
if (!mUseDvp)
|
||||
return;
|
||||
|
||||
if (direction == CPUtoGPU)
|
||||
dvpMapBufferEndAPI(mDvpCaptureTextureHandle);
|
||||
else
|
||||
dvpMapBufferEndAPI(mDvpPlaybackTextureHandle);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
#ifndef __VIDEO_FRAME_TRANSFER_H__
|
||||
#define __VIDEO_FRAME_TRANSFER_H__
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include <stdexcept>
|
||||
#include <map>
|
||||
|
||||
// NVIDIA GPU Direct For Video with OpenGL requires the following two headers.
|
||||
// See the NVIDIA website to check if your graphics card is supported.
|
||||
#include <DVPAPI.h>
|
||||
#include <dvpapi_gl.h>
|
||||
|
||||
struct SyncInfo;
|
||||
|
||||
|
||||
// Class for performing efficient frame memory transfers between the CPU and GPU,
|
||||
// using NVIDIA and AMD extensions.
|
||||
class VideoFrameTransfer
|
||||
{
|
||||
public:
|
||||
enum Direction
|
||||
{
|
||||
CPUtoGPU,
|
||||
GPUtoCPU
|
||||
};
|
||||
|
||||
VideoFrameTransfer(unsigned long memSize, void* address, Direction direction);
|
||||
~VideoFrameTransfer();
|
||||
|
||||
static bool checkFastMemoryTransferAvailable();
|
||||
static bool initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture);
|
||||
static void beginTextureInUse(Direction direction);
|
||||
static void endTextureInUse(Direction direction);
|
||||
|
||||
bool performFrameTransfer();
|
||||
void waitForTransferComplete();
|
||||
|
||||
private:
|
||||
static bool isNvidiaDvpAvailable();
|
||||
static bool isAMDPinnedMemoryAvailable();
|
||||
static bool initializeMemoryLocking(unsigned memSize);
|
||||
|
||||
void* mBuffer;
|
||||
unsigned long mMemSize;
|
||||
Direction mDirection;
|
||||
static bool mInitialized;
|
||||
static bool mUseDvp;
|
||||
static unsigned mWidth;
|
||||
static unsigned mHeight;
|
||||
static GLuint mCaptureTexture;
|
||||
|
||||
// NVIDIA GPU Direct for Video support
|
||||
SyncInfo* mExtSync;
|
||||
SyncInfo* mGpuSync;
|
||||
DVPBufferHandle mDvpSysMemHandle;
|
||||
|
||||
static DVPBufferHandle mDvpCaptureTextureHandle;
|
||||
static DVPBufferHandle mDvpPlaybackTextureHandle;
|
||||
static uint32_t mBufferAddrAlignment;
|
||||
static uint32_t mBufferGpuStrideAlignment;
|
||||
static uint32_t mSemaphoreAddrAlignment;
|
||||
static uint32_t mSemaphoreAllocSize;
|
||||
static uint32_t mSemaphorePayloadOffset;
|
||||
static uint32_t mSemaphorePayloadSize;
|
||||
|
||||
// GPU buffer bound to the target GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD for pinned memory
|
||||
GLuint mBufferHandle;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,46 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// stdafx.cpp : source file that includes just the standard includes
|
||||
// LoopThroughWithOpenGLCompositing.pch will be the pre-compiled header
|
||||
// stdafx.obj will contain the pre-compiled type information
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// stdafx.h : include file for standard system include files,
|
||||
// or project specific include files that are used frequently, but
|
||||
// are changed infrequently
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "targetver.h"
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
// Windows Header Files:
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
|
||||
// C RunTime Header Files
|
||||
#include <stdlib.h>
|
||||
#include <malloc.h>
|
||||
#include <memory.h>
|
||||
#include <tchar.h>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
#pragma once
|
||||
|
||||
// The following macros define the minimum required platform. The minimum required platform
|
||||
// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run
|
||||
// your application. The macros work by enabling all features available on platform versions up to and
|
||||
// including the version specified.
|
||||
|
||||
// Modify the following defines if you have to target a platform prior to the ones specified below.
|
||||
// Refer to MSDN for the latest info on corresponding values for different platforms.
|
||||
#ifndef WINVER // Specifies that the minimum required platform is Windows Vista.
|
||||
#define WINVER 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
|
||||
#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_WINDOWS // Specifies that the minimum required platform is Windows 98.
|
||||
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_IE // Specifies that the minimum required platform is Internet Explorer 7.0.
|
||||
#define _WIN32_IE 0x0700 // Change this to the appropriate value to target other versions of IE.
|
||||
#endif
|
||||
@@ -1,21 +1,55 @@
|
||||
{
|
||||
"shaderLibrary": "shaders",
|
||||
"serverPort": 8080,
|
||||
"oscPort": 9000,
|
||||
"inputVideoFormat": "1080p",
|
||||
"inputFrameRate": "59.94",
|
||||
"outputVideoFormat": "1080p",
|
||||
"outputFrameRate": "59.94",
|
||||
"$schema": "./runtime-host.schema.json",
|
||||
"autoReload": true,
|
||||
"input": {
|
||||
"backend": "ndi",
|
||||
"device": "AIDENLAPTOP (Test Pattern)",
|
||||
"frameRate": "59.94",
|
||||
"resolution": "1080p"
|
||||
},
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"audioEnabled": true,
|
||||
"audioOutputEnabled": true,
|
||||
"audioScheduleEnabled": true,
|
||||
"audioPrerollEnabled": true,
|
||||
"audioScheduleSilence": false,
|
||||
"audioScheduleTone": false,
|
||||
"audioChannelCount": 2,
|
||||
"audioSampleRate": 48000,
|
||||
"audioDelayMode": "matchVideoPreroll",
|
||||
"enableExternalKeying": true
|
||||
"oscBindAddress": "0.0.0.0",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18,
|
||||
"output": {
|
||||
"backend": "ndi",
|
||||
"device": "Shader",
|
||||
"frameRate": "59.94",
|
||||
"keying": {
|
||||
"alphaRequired": false,
|
||||
"external": false
|
||||
},
|
||||
"pixelFormat": "auto",
|
||||
"resolution": "1080p"
|
||||
},
|
||||
"windowOutput": {
|
||||
"fullscreen": true,
|
||||
"borderless": true,
|
||||
"display": "default",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"vsync": true,
|
||||
"allowTearing": false
|
||||
},
|
||||
"colorPipeline": {
|
||||
"ocioEnabled": false,
|
||||
"ocioConfig": "",
|
||||
"inputColorSpace": "scene_linear",
|
||||
"workingColorSpace": "scene_linear",
|
||||
"outputColorSpace": "rec709",
|
||||
"display": "sRGB",
|
||||
"view": "Rec.709",
|
||||
"look": "",
|
||||
"exposure": 0,
|
||||
"gamma": 1,
|
||||
"workingFormat": "rgba16f",
|
||||
"linearWorkingSpace": true
|
||||
},
|
||||
"previewEnabled": true,
|
||||
"previewFps": 59.94,
|
||||
"runtimeShaderId": "",
|
||||
"serverPort": 8080,
|
||||
"shaderLibrary": "shaders"
|
||||
}
|
||||
|
||||
361
config/runtime-host.schema.json
Normal file
361
config/runtime-host.schema.json
Normal file
@@ -0,0 +1,361 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://render-cadence.local/schemas/runtime-host.schema.json",
|
||||
"title": "Render Cadence Runtime Host Configuration",
|
||||
"description": "Startup configuration for the Render Cadence native host. This schema documents the settings currently read by AppConfigProvider.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "Editor-only schema reference. The native host ignores this field."
|
||||
},
|
||||
"shaderLibrary": {
|
||||
"type": "string",
|
||||
"default": "shaders",
|
||||
"description": "Path to the shader package library directory. Relative paths are resolved from the process/repo working location."
|
||||
},
|
||||
"serverPort": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"default": 8080,
|
||||
"description": "Preferred HTTP control server port."
|
||||
},
|
||||
"oscBindAddress": {
|
||||
"type": "string",
|
||||
"default": "0.0.0.0",
|
||||
"description": "OSC bind address reserved for the control surface. The current native host reports this through an OSC status stub but does not start the UDP listener yet."
|
||||
},
|
||||
"oscPort": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535,
|
||||
"default": 9000,
|
||||
"description": "OSC UDP port reserved for the control surface. Use 0 to mark the OSC stub disabled."
|
||||
},
|
||||
"oscSmoothing": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"default": 0.18,
|
||||
"description": "Reserved OSC smoothing amount reported through the OSC status stub."
|
||||
},
|
||||
"input": {
|
||||
"$ref": "#/$defs/input"
|
||||
},
|
||||
"output": {
|
||||
"$ref": "#/$defs/output"
|
||||
},
|
||||
"windowOutput": {
|
||||
"$ref": "#/$defs/windowOutput"
|
||||
},
|
||||
"colorPipeline": {
|
||||
"$ref": "#/$defs/colorPipeline"
|
||||
},
|
||||
"autoReload": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "When true, the runtime control layer may automatically rescan/reload shader packages when requested by the app flow."
|
||||
},
|
||||
"maxTemporalHistoryFrames": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 12,
|
||||
"description": "Maximum temporal history frames exposed to shaders that request history."
|
||||
},
|
||||
"previewEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Starts the optional preview window on its own thread."
|
||||
},
|
||||
"previewFps": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"default": 59.94,
|
||||
"description": "Target repaint rate for the optional preview window. It does not change render/output cadence."
|
||||
},
|
||||
"runtimeShaderId": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Optional startup shader package id used only when no saved runtime layer stack is restored. Leave empty to keep the simple fallback renderer until layers are added or restored."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"shaderLibrary",
|
||||
"serverPort",
|
||||
"input",
|
||||
"output"
|
||||
],
|
||||
"$defs": {
|
||||
"inputBackend": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"decklink",
|
||||
"ndi",
|
||||
"window",
|
||||
"fullscreen",
|
||||
"borderless",
|
||||
"none",
|
||||
"disabled",
|
||||
"off"
|
||||
]
|
||||
},
|
||||
"outputBackend": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"decklink",
|
||||
"ndi",
|
||||
"none",
|
||||
"disabled",
|
||||
"off"
|
||||
]
|
||||
},
|
||||
"resolution": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"720p",
|
||||
"1080i",
|
||||
"1080p",
|
||||
"2160p",
|
||||
"4k",
|
||||
"uhd"
|
||||
]
|
||||
},
|
||||
"frameRate": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"23.98",
|
||||
"24",
|
||||
"25",
|
||||
"29.97",
|
||||
"30",
|
||||
"50",
|
||||
"59.94",
|
||||
"60"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Video input backend configuration. DeckLink uses the configured resolution/frameRate as a capture mode. NDI adapts to the received source shape and logs if it differs from these expected values.",
|
||||
"properties": {
|
||||
"backend": {
|
||||
"$ref": "#/$defs/inputBackend",
|
||||
"default": "decklink",
|
||||
"description": "Input backend. Use 'decklink' for Blackmagic capture, 'ndi' for NDI receive, or 'none'/'disabled'/'off' for black fallback input."
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"description": "Input device/source selector. DeckLink currently uses 'default'. NDI accepts 'default'/'auto' for the first discovered source or an exact NDI source name."
|
||||
},
|
||||
"resolution": {
|
||||
"$ref": "#/$defs/resolution",
|
||||
"default": "1080p",
|
||||
"description": "Expected input resolution/mode. For NDI this is advisory only; received source dimensions are used."
|
||||
},
|
||||
"frameRate": {
|
||||
"$ref": "#/$defs/frameRate",
|
||||
"default": "59.94",
|
||||
"description": "Expected input frame rate. DeckLink uses this to resolve the hardware mode; NDI currently treats it as an expectation for logging/state."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"backend",
|
||||
"device",
|
||||
"resolution",
|
||||
"frameRate"
|
||||
]
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Video output backend configuration.",
|
||||
"properties": {
|
||||
"backend": {
|
||||
"$ref": "#/$defs/outputBackend",
|
||||
"default": "decklink",
|
||||
"description": "Output backend. Use 'decklink' for Blackmagic playout, 'ndi' for NDI send, or 'none'/'disabled'/'off' to disable scheduled output."
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"description": "Output device selector. DeckLink currently uses the first compatible/default output device. NDI uses this as the advertised sender name; 'default' becomes 'Render Cadence'."
|
||||
},
|
||||
"resolution": {
|
||||
"$ref": "#/$defs/resolution",
|
||||
"default": "1080p",
|
||||
"description": "Output render and video mode resolution."
|
||||
},
|
||||
"frameRate": {
|
||||
"$ref": "#/$defs/frameRate",
|
||||
"default": "59.94",
|
||||
"description": "Output render cadence and video mode frame rate."
|
||||
},
|
||||
"pixelFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"auto",
|
||||
"bgra8",
|
||||
"uyvy8"
|
||||
],
|
||||
"default": "auto",
|
||||
"description": "Requested system-memory output pixel format. Auto uses UYVY8 for DeckLink/NDI unless Output alpha is enabled, then BGRA8."
|
||||
},
|
||||
"keying": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Output alpha options. The control UI exposes these as a single Output alpha control.",
|
||||
"properties": {
|
||||
"external": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "DeckLink external-keyer backing field. The control UI only writes this true for DeckLink output."
|
||||
},
|
||||
"alphaRequired": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"external",
|
||||
"alphaRequired"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"backend",
|
||||
"device",
|
||||
"resolution",
|
||||
"frameRate"
|
||||
]
|
||||
},
|
||||
"windowOutput": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Reserved settings for a future full-resolution/full-frame-rate borderless or fullscreen video output backend. These are not used by the preview window.",
|
||||
"properties": {
|
||||
"fullscreen": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "When the future window backend is implemented, request exclusive/fullscreen-style presentation."
|
||||
},
|
||||
"borderless": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "When true, the future window backend should create a borderless output window."
|
||||
},
|
||||
"display": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"description": "Display/monitor selector for the future window backend."
|
||||
},
|
||||
"x": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Window X position used when fullscreen is false or when a backend needs explicit placement."
|
||||
},
|
||||
"y": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Window Y position used when fullscreen is false or when a backend needs explicit placement."
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0,
|
||||
"description": "Optional window width override. Zero means use output resolution."
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"default": 0,
|
||||
"description": "Optional window height override. Zero means use output resolution."
|
||||
},
|
||||
"vsync": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Presentation sync request for the future window backend."
|
||||
},
|
||||
"allowTearing": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Allow tearing for the future window backend when the graphics API and OS support it."
|
||||
}
|
||||
}
|
||||
},
|
||||
"colorPipeline": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Reserved color-management settings for a future OCIO-backed linear RGBA16F render pipeline.",
|
||||
"properties": {
|
||||
"ocioEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable future OpenColorIO transforms. Currently parsed and reported only."
|
||||
},
|
||||
"ocioConfig": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Path to an OCIO config file or package-relative config identifier."
|
||||
},
|
||||
"inputColorSpace": {
|
||||
"type": "string",
|
||||
"default": "scene_linear",
|
||||
"description": "Input/video color space to convert from at the input edge."
|
||||
},
|
||||
"workingColorSpace": {
|
||||
"type": "string",
|
||||
"default": "scene_linear",
|
||||
"description": "Linear working space shader packages should be able to assume."
|
||||
},
|
||||
"outputColorSpace": {
|
||||
"type": "string",
|
||||
"default": "rec709",
|
||||
"description": "Output transform target color space for scheduled video output."
|
||||
},
|
||||
"display": {
|
||||
"type": "string",
|
||||
"default": "sRGB",
|
||||
"description": "OCIO display name for preview/window/display transforms."
|
||||
},
|
||||
"view": {
|
||||
"type": "string",
|
||||
"default": "Rec.709",
|
||||
"description": "OCIO view name for preview/window/display transforms."
|
||||
},
|
||||
"look": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Optional OCIO look name."
|
||||
},
|
||||
"exposure": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Reserved display transform exposure adjustment."
|
||||
},
|
||||
"gamma": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"default": 1,
|
||||
"description": "Reserved display transform gamma adjustment."
|
||||
},
|
||||
"workingFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"rgba16f",
|
||||
"rgba32f"
|
||||
],
|
||||
"default": "rgba16f",
|
||||
"description": "Future render target/intermediate working format."
|
||||
},
|
||||
"linearWorkingSpace": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Documents the intended linear-light working pipeline."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
# Audio / SDI Tearing Investigation
|
||||
|
||||
Date: 2026-05-05
|
||||
|
||||
## Problem
|
||||
|
||||
After adding DeckLink audio pass-through, the SDI output intermittently shows a torn/corrupted frame. The preview window does not show the artifact.
|
||||
|
||||
Observed artifact:
|
||||
|
||||
- Bottom portion of the SDI image can show an offset mix of current/previous frame.
|
||||
- Looks like a frame-buffer or output-transfer issue rather than shader rendering.
|
||||
- Occurs even with all shaders bypassed.
|
||||
- Main branch is known good with no tearing.
|
||||
|
||||
Later tests also showed audio tearing/stutter when non-silent audio was scheduled.
|
||||
|
||||
## Known Good Baseline
|
||||
|
||||
- `main` branch has no SDI tearing.
|
||||
- Current branch with `audioEnabled: false` ran for several minutes with no visible tearing.
|
||||
|
||||
This strongly suggests the issue is tied to DeckLink audio output/scheduling rather than the shader stack.
|
||||
|
||||
## SDK References Checked
|
||||
|
||||
### `InputLoopThrough`
|
||||
|
||||
Location:
|
||||
|
||||
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/InputLoopThrough`
|
||||
|
||||
Findings:
|
||||
|
||||
- This is the SDK loop-through sample that keeps audio.
|
||||
- It preserves DeckLink audio packet timestamps using `GetPacketTime(..., m_frameTimescale)`.
|
||||
- It schedules audio packets with `ScheduleAudioSamples(..., packetTime, m_frameTimescale, ...)`.
|
||||
- It uses 16-channel 32-bit embedded audio by default.
|
||||
- It has separate scheduling threads for video/audio.
|
||||
- It waits for both video and audio preroll before `StartScheduledPlayback`.
|
||||
|
||||
### `LoopThroughWithOpenGLCompositing`
|
||||
|
||||
Location:
|
||||
|
||||
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/LoopThroughWithOpenGLCompositing`
|
||||
|
||||
Findings:
|
||||
|
||||
- This sample is the base for this app.
|
||||
- It ignores `IDeckLinkAudioInputPacket`.
|
||||
- It does not demonstrate audio pass-through.
|
||||
|
||||
### `SignalGenerator`
|
||||
|
||||
Location:
|
||||
|
||||
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/SignalGenerator`
|
||||
|
||||
Findings:
|
||||
|
||||
- Uses `RenderAudioSamples()` callback to top up audio when DeckLink requests samples.
|
||||
- Uses `GetBufferedAudioSampleFrameCount()` and a water level before scheduling more audio.
|
||||
|
||||
## Tests Tried And Results
|
||||
|
||||
### 1. Initial audio pass-through with FIFO and sample-time accumulator
|
||||
|
||||
Implementation:
|
||||
|
||||
- Copied incoming audio into a stereo FIFO.
|
||||
- Scheduled audio with a generated `mNextAudioSampleFrame` clock in 48 kHz timescale.
|
||||
- Matched delay to video preroll.
|
||||
|
||||
Result:
|
||||
|
||||
- Audio eventually worked.
|
||||
- SDI video tearing appeared.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Basic audio output path triggered SDI instability.
|
||||
|
||||
### 2. Reworked audio toward SDK `InputLoopThrough` packet-timestamp model
|
||||
|
||||
Implementation:
|
||||
|
||||
- Preserved incoming packet time via `GetPacketTime(..., mFrameTimescale)`.
|
||||
- Queued timestamped audio packets.
|
||||
- Scheduled packets with `ScheduleAudioSamples(..., packet.streamTime, mFrameTimescale, ...)`.
|
||||
|
||||
Result:
|
||||
|
||||
- Tearing persisted.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Simply matching SDK timestamp domain did not fix the issue.
|
||||
|
||||
### 3. Restored video callback closer to `main`
|
||||
|
||||
Implementation:
|
||||
|
||||
- Removed extra `glFinish()` calls.
|
||||
- Restored preview/readback ordering closer to `main`.
|
||||
- Re-enabled fast transfer path after earlier tests disabled it.
|
||||
- Removed audio texture upload from video playout callback.
|
||||
- Removed audio analysis and audio locks from video playout callback.
|
||||
- Removed DeckLink scheduling mutex around `ScheduleVideoFrame`.
|
||||
|
||||
Result:
|
||||
|
||||
- Tearing frequency seemed reduced at one point, but tearing persisted.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Extra work in the playout callback may have made timing worse, but was not the root cause.
|
||||
|
||||
### 4. Disabled audio completely
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioEnabled": false
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Ran for several minutes with no visible tearing.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The tearing is tied to audio being enabled.
|
||||
|
||||
### 5. Enabled audio input/analysis but disabled DeckLink audio output
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioEnabled": true,
|
||||
"audioOutputEnabled": false
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No tearing appeared.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- DeckLink audio input and CPU analysis are not the trigger.
|
||||
- The problem is on the DeckLink audio output side.
|
||||
|
||||
### 6. Enabled DeckLink audio output but disabled scheduling
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioEnabled": true,
|
||||
"audioOutputEnabled": true,
|
||||
"audioScheduleEnabled": false
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- No video tearing.
|
||||
- Slight stutter appeared.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- `EnableAudioOutput()` alone did not produce the tearing.
|
||||
- Stutter was likely from enabling an audio output stream without feeding it samples.
|
||||
|
||||
### 7. Enabled audio scheduling but skipped audio preroll
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioEnabled": true,
|
||||
"audioOutputEnabled": true,
|
||||
"audioScheduleEnabled": true,
|
||||
"audioPrerollEnabled": false
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing returned.
|
||||
- Stutter also present.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- `BeginAudioPreroll()` / `EndAudioPreroll()` are not required to trigger the tear.
|
||||
- `ScheduleAudioSamples()` is strongly implicated.
|
||||
|
||||
### 8. Retained scheduled audio packet memory after `ScheduleAudioSamples`
|
||||
|
||||
Implementation:
|
||||
|
||||
- Kept scheduled packet buffers alive in a retain queue after scheduling.
|
||||
- Avoided passing DeckLink pointers to vectors that immediately went out of scope.
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing and stutter persisted.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Buffer lifetime after `ScheduleAudioSamples()` was not the root cause.
|
||||
|
||||
### 9. Added audio water-level cap
|
||||
|
||||
Implementation:
|
||||
|
||||
- Restored SDK-style `GetBufferedAudioSampleFrameCount()` check.
|
||||
- Only scheduled more audio if DeckLink buffer was below the target water level.
|
||||
|
||||
Result:
|
||||
|
||||
- Stutter was reduced.
|
||||
- Video tearing persisted.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Overscheduling contributed to stutter/timing pressure.
|
||||
- It did not explain the tearing.
|
||||
|
||||
### 10. Removed standalone audio scheduler thread
|
||||
|
||||
Implementation:
|
||||
|
||||
- Stopped starting the dedicated audio scheduler thread.
|
||||
- Audio top-up occurred from input packet arrival and `RenderAudioSamples()` callback.
|
||||
|
||||
Result:
|
||||
|
||||
- No meaningful change.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The polling thread itself was not the cause.
|
||||
|
||||
### 11. Switched from timestamped audio output to continuous audio output
|
||||
|
||||
Implementation:
|
||||
|
||||
- Changed audio output to `bmdAudioOutputStreamContinuous`.
|
||||
- Scheduled audio using a monotonic 48 kHz sample clock.
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing and stutter persisted.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The issue was not specific to timestamped output mode.
|
||||
|
||||
### 12. Rendered into the actual `completedFrame`
|
||||
|
||||
Implementation:
|
||||
|
||||
- Changed `PlayoutFrameCompleted()` to reuse the exact `completedFrame` passed by DeckLink rather than rotating an independent output-frame queue.
|
||||
|
||||
Result:
|
||||
|
||||
- No change.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The app was probably not overwriting a still-in-use frame from its output queue.
|
||||
|
||||
### 13. Scheduled generated silence instead of captured audio
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioScheduleSilence": true
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Occasional stutter.
|
||||
- No video tearing.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Scheduling audio buffers itself can be stable if the audio data is zero.
|
||||
- Non-zero audio data appears to be important.
|
||||
|
||||
### 14. Flattened captured audio into PCM FIFO and scheduled fixed chunks
|
||||
|
||||
Implementation:
|
||||
|
||||
- Captured packets were flattened into a PCM FIFO.
|
||||
- DeckLink received fixed 10 ms chunks rather than original packet boundaries.
|
||||
- Missing audio was padded with silence.
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing returned.
|
||||
- Audio stutter/tearing returned.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- Packet boundaries/timestamps were not the whole cause.
|
||||
- Non-zero captured audio data still triggered instability.
|
||||
|
||||
### 15. Scheduled generated 440 Hz tone
|
||||
|
||||
Config:
|
||||
|
||||
```json
|
||||
"audioScheduleTone": true
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing occurred.
|
||||
- Tone/audio also tore.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The issue is not specific to captured input data.
|
||||
- Non-zero scheduled audio, even generated tone, triggers the problem.
|
||||
|
||||
### 16. Changed DeckLink output to 16 embedded audio channels
|
||||
|
||||
Implementation:
|
||||
|
||||
- Enabled DeckLink audio output with 16 channels instead of 2.
|
||||
- Mapped stereo to channels 1/2.
|
||||
- Filled channels 3-16 with silence.
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing and audio tearing still occurred.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The issue is not simply caused by 2-channel embedded audio output.
|
||||
|
||||
### 17. Used DeckLink-owned output video frames with audio enabled
|
||||
|
||||
Implementation:
|
||||
|
||||
- When audio output is enabled:
|
||||
- disabled fast transfer path
|
||||
- created output frames with `CreateVideoFrame()`
|
||||
- avoided `CreateVideoFrameWithBuffer()` and the custom pinned playout allocator
|
||||
|
||||
Result:
|
||||
|
||||
- Video tearing and audio tearing still occurred.
|
||||
|
||||
Conclusion:
|
||||
|
||||
- The custom pinned output video buffers are likely not the root cause.
|
||||
|
||||
## Current Strong Conclusions
|
||||
|
||||
- Shader stack is not the cause.
|
||||
- Preview/render output is not showing the issue, so the artifact is SDI/output-side.
|
||||
- DeckLink audio input is not the cause.
|
||||
- DeckLink audio output enabled but unscheduled does not cause tearing.
|
||||
- `ScheduleAudioSamples()` with zero/silent buffers does not cause tearing.
|
||||
- `ScheduleAudioSamples()` with non-zero audio causes both video tearing and audio tearing.
|
||||
- The problem persists across:
|
||||
- timestamped audio output
|
||||
- continuous audio output
|
||||
- captured audio
|
||||
- generated tone
|
||||
- 2-channel output
|
||||
- 16-channel embedded output
|
||||
- app-owned/pinned output video buffers
|
||||
- DeckLink-owned output video frames
|
||||
|
||||
## Current Hypothesis
|
||||
|
||||
The issue appears to be a DeckLink output interaction where non-zero embedded audio samples disturb SDI video/audio output in this app’s scheduling model.
|
||||
|
||||
Since silence is stable but tone is not, the next likely areas to investigate are:
|
||||
|
||||
- Audio sample format/range/endian expectations.
|
||||
- Whether DeckLink expects 32-bit audio samples to be in a different effective range than we are providing.
|
||||
- Whether the scheduled audio buffer layout for the selected hardware/output mode differs from our assumptions.
|
||||
- Whether the selected output mode/keyer/SDI configuration has constraints when non-zero embedded audio is present.
|
||||
- Whether the SDK sample behaves correctly on the same hardware with a generated tone and same video mode.
|
||||
|
||||
## Suggested Next Tests
|
||||
|
||||
1. Schedule very low amplitude non-zero audio, e.g. constant `1`, then `256`, then a very quiet sine.
|
||||
2. Try 16-bit audio output instead of 32-bit if supported.
|
||||
3. Try `bmdAudioOutputStreamContinuousDontResample`.
|
||||
4. Disable external keying and test with non-zero audio.
|
||||
5. Build/run the SDK `SignalGenerator` or `InputLoopThrough` sample on the same DeckLink device, video mode, and SDI output path with non-zero embedded audio.
|
||||
6. Add instrumentation for DeckLink status/errors around scheduled video/audio completion.
|
||||
7. Confirm Desktop Video setup panel audio/SDI settings for the selected output.
|
||||
|
||||
## Current Config At Time Of Note
|
||||
|
||||
```json
|
||||
"audioEnabled": true,
|
||||
"audioOutputEnabled": true,
|
||||
"audioScheduleEnabled": true,
|
||||
"audioPrerollEnabled": true,
|
||||
"audioScheduleSilence": false,
|
||||
"audioScheduleTone": false
|
||||
```
|
||||
198
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
198
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Current System Architecture
|
||||
|
||||
This document describes how the current `RenderCadenceCompositor` app works.
|
||||
|
||||
The implementation is cadence-first: the render/output path owns frame timing, while shader compilation, HTTP control, preview, persistence, and video I/O edges stay outside the render cadence loop wherever possible. The guardrails in [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) are the operating contract.
|
||||
|
||||
## Application Shape
|
||||
|
||||
The app is a native C++ OpenGL compositor with:
|
||||
|
||||
- optional DeckLink input
|
||||
- optional DeckLink scheduled output
|
||||
- a render-thread-owned OpenGL context
|
||||
- a pluggable app-side runtime-content controller
|
||||
- the default runtime Slang shader package stack from `shaders/`
|
||||
- a local HTTP/WebSocket control server
|
||||
- optional Win32/GDI preview from system-memory output frames
|
||||
- background runtime-state persistence
|
||||
- cadence telemetry and log output
|
||||
|
||||
Primary source areas:
|
||||
|
||||
- `src/app`: startup/shutdown orchestration, config loading, video backend factory, runtime-content controller boundary, and the default shader runtime-content controller
|
||||
- `src/render`: cadence clock, input texture upload, render-content boundary, readback, and runtime GL support
|
||||
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
|
||||
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
|
||||
- `src/frames`: system-memory frame exchange
|
||||
- `src/video/core`: generic video IO edge contracts, mode descriptions, formats, and output scheduling thread
|
||||
- `src/video/decklink`: current DeckLink input/output backend
|
||||
- `src/video/playout`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers
|
||||
- `src/video/legacy`: older backend pipeline pieces kept separate from the current app path
|
||||
- `src/runtime/catalog`: supported shader catalog and package filtering
|
||||
- `src/runtime/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
|
||||
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
|
||||
- `src/runtime/state`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
|
||||
- `src/runtime/text`: MSDF/MTSDF font atlas build and CPU-side prepared text texture composition
|
||||
- `src/control`: command parsing, HTTP/WebSocket transport helpers, OSC status stub, OpenAPI state JSON
|
||||
- `src/app/RenderCadenceHttpRoutes.*`: this app's current HTTP endpoint map
|
||||
- `src/preview`: optional non-consuming preview window
|
||||
- `src/telemetry` and `src/logging`: runtime observation and logging
|
||||
|
||||
## Startup
|
||||
|
||||
Startup broadly proceeds as:
|
||||
|
||||
1. Load `config/runtime-host.json` through `AppConfigProvider`, then apply CLI overrides.
|
||||
2. Initialize the active `IRuntimeContentController`. The checked-in app uses `ShaderRuntimeContentController`, which loads the supported shader catalog, starts runtime-state persistence, and tries to restore `runtime/runtime_state.json`.
|
||||
3. If restore fails or no usable state exists, the shader controller falls back to the optional configured startup shader. The checked-in config leaves `runtimeShaderId` empty, so a fresh host keeps the simple fallback renderer.
|
||||
4. Start the render thread.
|
||||
5. Start the active runtime-content controller. The shader controller queues background Slang builds for every pending active layer.
|
||||
6. Build a small completed-frame reserve.
|
||||
7. Start optional preview, optional video output, telemetry, and HTTP control.
|
||||
|
||||
The runtime-state restore is intentionally app/control side work. The render thread does not read JSON, inspect the shader library, or decide what to compile.
|
||||
|
||||
## Runtime Content And Shader Layer State
|
||||
|
||||
`RenderCadenceApp` owns startup, video output, preview, telemetry, OSC status, and HTTP server lifetime. It does not own the Slang shader stack directly. Runtime content plugs in through `IRuntimeContentController`.
|
||||
|
||||
The checked-in implementation is `ShaderRuntimeContentController`. It wraps `RuntimeLayerController`, exposes shader catalog/layer JSON for `/api/state`, handles shader layer POST commands, and publishes render-ready shader layer snapshots to the render thread.
|
||||
|
||||
`RuntimeLayerController` owns the app-side layer model and coordinates:
|
||||
|
||||
- supported shader catalog loading
|
||||
- layer add/remove/reorder/bypass/shader assignment
|
||||
- parameter update/reset
|
||||
- startup restore
|
||||
- reload reconciliation
|
||||
- background Slang builds
|
||||
- render-layer publication
|
||||
- runtime-state persistence requests
|
||||
|
||||
`RuntimeLayerModel` owns the in-memory active stack:
|
||||
|
||||
- layer id
|
||||
- shader id and display name
|
||||
- build state and message
|
||||
- bypass state
|
||||
- manifest parameter definitions
|
||||
- optional shader-declared custom UI metadata
|
||||
- current parameter values
|
||||
- render-ready artifacts
|
||||
|
||||
The current durable runtime state is stored in `runtime/runtime_state.json`. It contains the active stack order, shader ids, bypass flags, and parameter values. On startup, valid saved layers are restored in order. Missing shader packages are skipped, invalid saved parameter values fall back to manifest defaults, and a missing or unusable file falls back to the optional configured startup shader.
|
||||
|
||||
Manual stack preset routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet. `runtime_state.json` is the supported latest-working-state mechanism.
|
||||
|
||||
## Persistence
|
||||
|
||||
`RuntimeStatePersistenceWriter` performs debounced background writes to `runtime/runtime_state.json`.
|
||||
|
||||
Durable UI/API mutations request persistence after they are accepted:
|
||||
|
||||
- add/remove layer
|
||||
- reorder layer
|
||||
- set bypass
|
||||
- set shader
|
||||
- update parameter
|
||||
- reset parameters
|
||||
- reload compatibility refresh
|
||||
|
||||
The mutation path snapshots the current layer model and hands serialized state to the writer. File IO happens on the persistence worker, not on the render thread or cadence path. Shutdown flushes the latest pending snapshot.
|
||||
|
||||
OSC-driven changes are intentionally not part of this autosave path yet.
|
||||
|
||||
The host configuration editor is separate from runtime layer persistence. The UI reads active and saved startup config through `/api/config`, saves `config/runtime-host.json` through `/api/config/save`, and requests a native host restart through `/api/app/restart`. Render cadence, video input/output selection, resolution, frame rate, output pixel format, HTTP port, and preview settings are still startup-owned; they are not hot-swapped inside the cadence path.
|
||||
|
||||
## Shader Reload
|
||||
|
||||
`POST /api/reload` and the control UI reload button:
|
||||
|
||||
- rescan `shaders/`
|
||||
- re-read manifests
|
||||
- rebuild the supported shader catalog
|
||||
- refresh optional shader custom UI metadata
|
||||
- update active layer metadata and parameter definitions from changed manifests
|
||||
- preserve compatible parameter values
|
||||
- default new or incompatible parameter values
|
||||
- queue recompilation for every catalog-valid layer in the active stack
|
||||
|
||||
Reload does not compile every package in the shader library. A package is compiled when it is part of the active layer stack. If an active layer references a shader that no longer exists, that layer is marked failed and skipped. Existing render output remains active where possible until replacement builds are ready.
|
||||
|
||||
`autoReload` is still exposed in config/state for compatibility, but automatic file watching is not currently wired.
|
||||
|
||||
## Render Ownership
|
||||
|
||||
The render thread owns the app OpenGL context during normal operation.
|
||||
|
||||
The render path consumes published render-layer snapshots. It does not:
|
||||
|
||||
- parse JSON
|
||||
- scan files
|
||||
- launch Slang
|
||||
- run font atlas generation
|
||||
- perform persistence
|
||||
- handle HTTP or OSC
|
||||
- call DeckLink discovery/setup APIs
|
||||
|
||||
When a runtime shader build completes, the default shader runtime-content controller publishes a render-layer artifact. The render thread forwards pending layer snapshots to the active render-content adapter. The default `RuntimeShaderRenderContent` owns the runtime scene, diffs the snapshot, and queues changed pass programs to the shared-context prepare worker. The render thread swaps in an already-prepared render plan at a frame boundary through that adapter.
|
||||
|
||||
## Video And Preview
|
||||
|
||||
Video input and output are optional edges. `input.backend` and `output.backend` select the concrete backend through the app-side backend factory. DeckLink and NDI are the current concrete backends, and `none` disables an edge. `input` and `output` also carry the device selector plus resolution/frame-rate settings. Configured video modes are represented in `src/video/core` and translated to backend-specific modes only inside the concrete edge.
|
||||
|
||||
The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
|
||||
|
||||
The output edge consumes completed system-memory frames from `SystemFrameExchange`. The render thread owns output pixel packing before readback: BGRA8 is read directly, and UYVY8 is packed on the GPU into a half-width RGBA8 target before async PBO readback. DeckLink and NDI output then schedule/send those completed CPU frames without invoking GL or converting pixels. If video output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
|
||||
|
||||
Runtime state exposes backend-neutral output telemetry through `videoOutput`. Portable fields such as `enabled`, `backend`, and `scheduleFailures` stay at that level; backend-specific counters live under `videoOutput.backendMetrics`.
|
||||
|
||||
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints BGRA8 directly, decodes UYVY8 only for preview presentation, and skips preview ticks instead of blocking the frame exchange.
|
||||
|
||||
Screenshot routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet.
|
||||
|
||||
## Control Surface
|
||||
|
||||
The HTTP server runs on its own thread. `HttpControlServer` owns socket lifetime, HTTP parsing, static asset helpers, OpenAPI/Swagger helper serving, and WebSocket state transport. `RenderCadenceHttpRoutes` owns this app's current endpoint map:
|
||||
|
||||
- UI assets
|
||||
- shader package custom UI assets under `/shader-assets/{shaderId}/...`
|
||||
- OpenAPI/Swagger docs
|
||||
- `GET /api/state`
|
||||
- `/ws` state updates
|
||||
- layer mutation POST routes, dispatched to the active runtime-content controller
|
||||
- `/api/reload`
|
||||
|
||||
Known but not implemented in the current native command path:
|
||||
|
||||
- `/api/layers/move`
|
||||
- `/api/stack-presets/save`
|
||||
- `/api/stack-presets/load`
|
||||
- `/api/screenshot`
|
||||
|
||||
Unsupported routes return an action response with `ok: false`.
|
||||
|
||||
Forks can reuse the HTTP/WebSocket shell without keeping these endpoints by installing a different route callback.
|
||||
|
||||
`OscControlServer` is currently a lifecycle/status stub. It consumes startup OSC config, exposes configured/disabled/not-listening state through `/api/state`, and leaves UDP socket receive, OSC decode, and runtime dispatch unimplemented until that ingress boundary is built deliberately.
|
||||
|
||||
## Tests
|
||||
|
||||
Native tests cover the main non-GL contracts:
|
||||
|
||||
- JSON parsing/serialization
|
||||
- runtime parameter normalization
|
||||
- shader package registry and Slang validation
|
||||
- supported shader catalog
|
||||
- runtime layer restore/reload behavior
|
||||
- runtime-state persistence writer
|
||||
- HTTP transport and app-route dispatch
|
||||
- frame exchange and input mailbox behavior
|
||||
- video format and scheduling helpers
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# OSC Control
|
||||
|
||||
Video Shader Toys can listen for local OSC messages and map them onto shader layer parameters.
|
||||
This is the intended OSC control contract, but OSC ingress is not wired in the current `RenderCadenceCompositor` native host yet. The native host has an OSC service stub that consumes config and reports status in `/api/state`; it does not open a UDP listener or dispatch messages yet. Use the REST layer parameter endpoints or the control UI for live parameter changes today.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -8,11 +8,13 @@ Set the UDP port in `config/runtime-host.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"oscPort": 9000
|
||||
"oscBindAddress": "127.0.0.1",
|
||||
"oscPort": 9000,
|
||||
"oscSmoothing": 0.18
|
||||
}
|
||||
```
|
||||
|
||||
Set `oscPort` to `0` to disable the OSC listener.
|
||||
Today, `oscPort: 0` marks the OSC stub disabled and any nonzero port marks it configured but not listening. When OSC ingress is implemented, `oscBindAddress: "127.0.0.1"` should keep OSC local to the host, and `oscBindAddress: "0.0.0.0"` should listen on all IPv4 interfaces. `oscSmoothing` is reserved for a per-frame easing amount on numeric OSC controls.
|
||||
|
||||
## Address Pattern
|
||||
|
||||
@@ -47,6 +49,8 @@ Matching is exact first. If that fails, names are compared in a simplified form
|
||||
|
||||
If multiple layers use the same shader package ID or display name, the first matching layer in the stack is controlled. Use the internal layer ID shown in the UI when you need to target one duplicate layer precisely.
|
||||
|
||||
In the control UI, each parameter row has a small **OSC** button. Clicking it copies that parameter's exact OSC address to the clipboard, which is the safest way to target controls with long names or duplicate shader layers.
|
||||
|
||||
## Values
|
||||
|
||||
The listener accepts these OSC argument types:
|
||||
@@ -59,17 +63,27 @@ The listener accepts these OSC argument types:
|
||||
|
||||
Single-argument messages become scalar JSON values. Multi-argument messages become JSON arrays, which lets OSC drive `vec2` and `color` parameters.
|
||||
|
||||
OSC updates are coalesced by target route and applied once per render tick, so rapid controller motion does not force one runtime mutation, disk write, and UI push per incoming UDP packet. Numeric OSC controls can also be slightly smoothed with `oscSmoothing`.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/fisheye-reproject/panDegrees 45.0
|
||||
/VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid"
|
||||
/VideoShaderToys/video-transform/pan 0.25 -0.5
|
||||
/VideoShaderToys/composition-guides/lineColor 1.0 0.8 0.1 1.0
|
||||
/VideoShaderToys/safe-area-guides/lineColor 1.0 0.8 0.1 1.0
|
||||
```
|
||||
|
||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
||||
|
||||
OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. Stack edits made through the UI and preset operations still persist as before. Smoothing only applies to numeric controls such as floats, `vec2`, and `color`; booleans, enums, text, and triggers stay immediate.
|
||||
|
||||
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||
|
||||
```text
|
||||
/VideoShaderToys/trigger-flash/flash 1
|
||||
```
|
||||
|
||||
## Open Stage Control
|
||||
|
||||
For simple scalar controls, set the widget address and target directly:
|
||||
@@ -106,10 +120,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type:
|
||||
|
||||
## Network
|
||||
|
||||
The listener binds to localhost only:
|
||||
By default the listener binds to localhost only:
|
||||
|
||||
```text
|
||||
127.0.0.1:<oscPort>
|
||||
```
|
||||
|
||||
This keeps the control surface local to the machine running Video Shader Toys.
|
||||
|
||||
To accept OSC from other machines on the network, set:
|
||||
|
||||
```json
|
||||
{
|
||||
"oscBindAddress": "0.0.0.0",
|
||||
"oscPort": 9000
|
||||
}
|
||||
```
|
||||
|
||||
That listens on all IPv4 interfaces, so make sure your firewall and network are configured appropriately.
|
||||
|
||||
164
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
164
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Render Cadence Golden Rules
|
||||
|
||||
These are the non-negotiable rules for the new render-cadence architecture.
|
||||
|
||||
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
|
||||
|
||||
## 1. The Render Thread Owns Its Render Context
|
||||
|
||||
Only the render thread may bind and use its primary OpenGL context.
|
||||
|
||||
For a fork that replaces the renderer with D3D or another graphics API, read this rule as the same ownership contract: the cadence/render thread owns the primary render device/context path, and non-render services exchange prepared data with it rather than calling rendering APIs themselves.
|
||||
|
||||
Allowed on the render thread:
|
||||
|
||||
- GL resource creation and destruction for resources it owns
|
||||
- GL shader/program swap from an already-prepared GL program
|
||||
- drawing the next frame
|
||||
- async readback queueing and completion polling
|
||||
- publishing completed system-memory frames
|
||||
|
||||
Not allowed on the render thread:
|
||||
|
||||
- Slang compiler invocation
|
||||
- manifest scanning/parsing
|
||||
- filesystem discovery
|
||||
- image/font/LUT decoding
|
||||
- persistence
|
||||
- network/API/OSC handling
|
||||
- DeckLink scheduling
|
||||
- blocking console logging
|
||||
- config file discovery or parsing
|
||||
|
||||
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
||||
|
||||
## 2. Render Cadence Does Not Chase Buffers
|
||||
|
||||
The render thread runs at the selected render cadence.
|
||||
|
||||
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
|
||||
|
||||
Buffers absorb timing differences. They do not control render cadence.
|
||||
|
||||
## 3. Video I/O Never Renders
|
||||
|
||||
DeckLink output consumes already-rendered system-memory frames.
|
||||
|
||||
The output/scheduling side may:
|
||||
|
||||
- schedule completed frames
|
||||
- release frames after DeckLink completion
|
||||
- report late/dropped/schedule telemetry
|
||||
- record app-side poll misses
|
||||
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||
|
||||
It must not:
|
||||
|
||||
- render fallback frames
|
||||
- invoke GL
|
||||
- compile shaders
|
||||
- block the render cadence waiting for DeckLink
|
||||
- continuously rewrite healthy scheduled timestamps
|
||||
|
||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||
|
||||
Output pixel-format packing that requires GL belongs in the render-owned readback path before frame publication. Video I/O edges may select supported system-memory formats, but they must not invoke GL or become hidden pixel-conversion renderers.
|
||||
|
||||
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||
|
||||
## 4. Runtime Build Work Produces Artifacts
|
||||
|
||||
Runtime shader work is split into two phases:
|
||||
|
||||
1. CPU/build phase outside the render thread
|
||||
2. shared-context GL preparation outside the render thread where practical
|
||||
3. GL program swap on the render thread
|
||||
|
||||
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||
|
||||
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
|
||||
|
||||
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
||||
|
||||
## 5. No Hidden Blocking In The Cadence Path
|
||||
|
||||
The render loop must not do work with unbounded or OS-dependent latency.
|
||||
|
||||
Examples to avoid:
|
||||
|
||||
- file reads
|
||||
- directory scans
|
||||
- image decoding
|
||||
- process launches
|
||||
- waits on worker threads
|
||||
- blocking locks around slow code
|
||||
- synchronous GPU readback waits
|
||||
- console I/O
|
||||
|
||||
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
|
||||
|
||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||
|
||||
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
|
||||
|
||||
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||
|
||||
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||
|
||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||
|
||||
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
|
||||
|
||||
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
|
||||
|
||||
## 8. Telemetry Must Name Ownership Clearly
|
||||
|
||||
Counters should say which subsystem had the event.
|
||||
|
||||
Good examples:
|
||||
|
||||
- `renderFps`
|
||||
- `scheduleFps`
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `deckLinkScheduleLeadFrames`
|
||||
- `deckLinkScheduleRealignments`
|
||||
- `inputCaptureFps`
|
||||
- `inputSubmitMs`
|
||||
- `inputUploadMs`
|
||||
- `inputConvertMs`
|
||||
- `shaderCommitted`
|
||||
- `shaderFailures`
|
||||
|
||||
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
|
||||
|
||||
## 9. Keep Files Small And Role-Based
|
||||
|
||||
A file should have one clear reason to change.
|
||||
|
||||
Preferred boundaries:
|
||||
|
||||
- app orchestration
|
||||
- render cadence/thread ownership
|
||||
- GL rendering
|
||||
- runtime artifact build/bridge
|
||||
- app-owned display/render layer model
|
||||
- parameter packing
|
||||
- system-memory frame exchange
|
||||
- DeckLink output scheduling
|
||||
- telemetry
|
||||
- local control/API edge
|
||||
- config loading
|
||||
- JSON presentation/serialization
|
||||
- logging
|
||||
|
||||
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
|
||||
|
||||
## 10. Prefer Explicit Unsupported States
|
||||
|
||||
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
|
||||
|
||||
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
|
||||
|
||||
Unsupported is healthy when it protects the architecture.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,12 @@ Packaged documentation:
|
||||
|
||||
Generated files:
|
||||
|
||||
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer.
|
||||
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`.
|
||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
||||
- `runtime_state.json`: persisted layer stack and parameter values.
|
||||
- `stack_presets/*.json`: user-saved layer stack presets.
|
||||
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the most recently compiled shader pass.
|
||||
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc` for the most recently compiled pass.
|
||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path for the most recently compiled pass.
|
||||
- `runtime_state.json`: debounced autosave of the latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup and falls back to the configured default shader if the file is missing or unusable.
|
||||
- `stack_presets/*.json`: planned user-saved layer stack presets. Preset routes are present in the API surface but not implemented in the current native host.
|
||||
- `screenshots/*.png`: planned screenshot output. Screenshot capture is present in the API surface but not implemented in the current native host.
|
||||
|
||||
Git policy:
|
||||
|
||||
|
||||
@@ -11,16 +11,15 @@ struct ShaderContext
|
||||
float2 inputResolution;
|
||||
float2 outputResolution;
|
||||
float time;
|
||||
float utcTimeSeconds;
|
||||
float utcOffsetSeconds;
|
||||
float startupRandom;
|
||||
float frameCount;
|
||||
float mixAmount;
|
||||
float bypass;
|
||||
int sourceHistoryLength;
|
||||
int temporalHistoryLength;
|
||||
float2 audioRms;
|
||||
float2 audioPeak;
|
||||
float audioMonoRms;
|
||||
float audioMonoPeak;
|
||||
float4 audioBands;
|
||||
int feedbackAvailable;
|
||||
};
|
||||
|
||||
cbuffer GlobalParams
|
||||
@@ -28,34 +27,29 @@ cbuffer GlobalParams
|
||||
float gTime;
|
||||
float2 gInputResolution;
|
||||
float2 gOutputResolution;
|
||||
float gUtcTimeSeconds;
|
||||
float gUtcOffsetSeconds;
|
||||
float gStartupRandom;
|
||||
float gFrameCount;
|
||||
float gMixAmount;
|
||||
float gBypass;
|
||||
int gSourceHistoryLength;
|
||||
int gTemporalHistoryLength;
|
||||
float2 gAudioRms;
|
||||
float2 gAudioPeak;
|
||||
float gAudioMonoRms;
|
||||
float gAudioMonoPeak;
|
||||
float4 gAudioBands;
|
||||
int gFeedbackAvailable;
|
||||
{{PARAMETER_UNIFORMS}}};
|
||||
|
||||
Sampler2D<float4> gVideoInput;
|
||||
Sampler2D<float4> gAudioData;
|
||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
||||
Sampler2D<float4> gLayerInput;
|
||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{FEEDBACK_SAMPLER}}{{TEXTURE_SAMPLERS}}
|
||||
{{TEXT_SAMPLERS}}
|
||||
float4 sampleVideo(float2 tc)
|
||||
{
|
||||
return gVideoInput.Sample(tc);
|
||||
}
|
||||
|
||||
float4 sampleAudioWaveform(float x)
|
||||
float4 sampleLayerInput(float2 tc)
|
||||
{
|
||||
return gAudioData.Sample(float2(saturate(x), 0.25));
|
||||
}
|
||||
|
||||
float4 sampleAudioSpectrum(float x)
|
||||
{
|
||||
return gAudioData.Sample(float2(saturate(x), 0.75));
|
||||
return gLayerInput.Sample(tc);
|
||||
}
|
||||
|
||||
float4 sampleSourceHistory(int framesAgo, float2 tc)
|
||||
@@ -88,6 +82,9 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
||||
}
|
||||
}
|
||||
|
||||
{{FEEDBACK_HELPER}}
|
||||
|
||||
{{TEXT_HELPERS}}
|
||||
#include "{{USER_SHADER_INCLUDE}}"
|
||||
|
||||
[shader("fragment")]
|
||||
@@ -99,16 +96,15 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
||||
context.inputResolution = gInputResolution;
|
||||
context.outputResolution = gOutputResolution;
|
||||
context.time = gTime;
|
||||
context.utcTimeSeconds = gUtcTimeSeconds;
|
||||
context.utcOffsetSeconds = gUtcOffsetSeconds;
|
||||
context.startupRandom = gStartupRandom;
|
||||
context.frameCount = gFrameCount;
|
||||
context.mixAmount = gMixAmount;
|
||||
context.bypass = gBypass;
|
||||
context.sourceHistoryLength = gSourceHistoryLength;
|
||||
context.temporalHistoryLength = gTemporalHistoryLength;
|
||||
context.audioRms = gAudioRms;
|
||||
context.audioPeak = gAudioPeak;
|
||||
context.audioMonoRms = gAudioMonoRms;
|
||||
context.audioMonoPeak = gAudioMonoPeak;
|
||||
context.audioBands = gAudioBands;
|
||||
context.feedbackAvailable = gFeedbackAvailable;
|
||||
float4 effectedColor = {{ENTRY_POINT_CALL}};
|
||||
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
|
||||
return lerp(context.sourceColor, effectedColor, mixValue);
|
||||
|
||||
930
shaders/SHADER_CONTRACT.md
Normal file
930
shaders/SHADER_CONTRACT.md
Normal file
@@ -0,0 +1,930 @@
|
||||
# Shader Package Contract
|
||||
|
||||
This document explains how to create shaders for the Video Shader runtime.
|
||||
|
||||
Each shader is a small package under `shaders/<id>/`:
|
||||
|
||||
```text
|
||||
shaders/my-effect/
|
||||
shader.json
|
||||
shader.slang
|
||||
optional-texture.png
|
||||
```
|
||||
|
||||
The runtime reads `shader.json`, generates a Slang wrapper from `runtime/templates/shader_wrapper.slang.in`, includes your `shader.slang`, compiles the result to GLSL, and exposes the shader in the local control UI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create a folder:
|
||||
|
||||
```text
|
||||
shaders/my-effect/
|
||||
```
|
||||
|
||||
Add `shader.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-effect",
|
||||
"name": "My Effect",
|
||||
"description": "A simple starter shader.",
|
||||
"category": "Custom",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Add `shader.slang`:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 color = context.sourceColor;
|
||||
color.rgb = lerp(color.rgb, 1.0 - color.rgb, strength);
|
||||
return saturate(color);
|
||||
}
|
||||
```
|
||||
|
||||
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. You can also use **Reload shaders** in the control UI to manually rescan the shader library.
|
||||
|
||||
## Guidance For Shaders
|
||||
|
||||
When generating a new shader package, prefer matching the existing runtime contract over copying code verbatim from Shadertoy, GLSL sandbox sites, or WebGL demos.
|
||||
|
||||
Important rules:
|
||||
|
||||
- Generate a complete package: `shaders/<id>/shader.json` and `shaders/<id>/shader.slang`.
|
||||
- Use `float4 shadeVideo(ShaderContext context)` unless the manifest explicitly sets a different `entryPoint`.
|
||||
- Do not create `mainImage`, `main`, `fragColor`, `iResolution`, `iTime`, `iChannel0`, or a fragment shader attribute layout. The runtime wrapper provides the real fragment entry point.
|
||||
- Replace Shadertoy `fragCoord` with `context.uv * context.outputResolution`.
|
||||
- Replace `iResolution.xy` with `context.outputResolution`.
|
||||
- Replace `iTime` with `context.time`.
|
||||
- Replace `iFrame` with `context.frameCount`.
|
||||
- Replace source-video `iChannel0` sampling with `sampleVideo(uv)` or `context.sourceColor`.
|
||||
- Use Slang/HLSL names and syntax: `float2`, `float3`, `float4`, `float2x2`, `lerp`, `frac`, `saturate`, and `mul(matrix, vector)`.
|
||||
- Do not use GLSL-only types/functions such as `vec2`, `vec3`, `vec4`, `mat2`, `mix`, `fract`, `mod`, `texture`, or `mainImage`.
|
||||
- Keep parameter IDs, texture IDs, font IDs, and function entry points as valid shader identifiers: letters, numbers, and underscores only, starting with a letter or underscore.
|
||||
- Add only controls that are actually used by the shader.
|
||||
- Prefer a small number of clear controls with conservative defaults.
|
||||
- Keep shaders deterministic unless randomness is an explicit feature. For stable process-level variation, use `context.startupRandom`; for per-pixel pseudo-randomness, hash from `uv`, pixel coordinates, `frameCount`, or trigger values.
|
||||
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
|
||||
- If the source license is unclear or incompatible, do not add the shader package.
|
||||
|
||||
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. CI also runs shader validation, so every available package in `shaders/` should compile successfully. Intentionally broken examples should stay visibly marked as broken rather than pretending to be production shaders.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
`shader.json` is the runtime-facing description of the shader.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `id`: package ID used by state/presets. Hyphenated names are OK here, for example `my-effect`.
|
||||
- `name`: display name in the UI.
|
||||
- `parameters`: array of exposed controls. Use `[]` if there are no user parameters.
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `description`: display/help text for the shader library.
|
||||
- `category`: UI grouping label.
|
||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
|
||||
- `textures`: texture assets to load and expose as samplers.
|
||||
- `fonts`: packaged font assets for live text parameters.
|
||||
- `temporal`: history-buffer requirements.
|
||||
- `feedback`: optional previous-frame shader-local feedback surface.
|
||||
- `ui`: optional custom control UI module for this shader.
|
||||
|
||||
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
|
||||
|
||||
Metadata conventions:
|
||||
|
||||
- Keep `name` short, human-facing, and in title case.
|
||||
- Keep `category` consistent with existing library groups such as `Color`, `Transform`, `Projection`, `Temporal`, `Scopes & Guides`, `Utility`, `Feedback`, and `Calibration`.
|
||||
- Keep `description` to one clear sentence in present tense that explains what the shader does for an operator.
|
||||
- Avoid placeholder, joke, or overly implementation-heavy descriptions unless the shader is intentionally a diagnostic or broken example.
|
||||
|
||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
|
||||
- `entryPoint`
|
||||
- parameter `id`
|
||||
- texture `id`
|
||||
- font `id`
|
||||
|
||||
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
|
||||
|
||||
## Custom Control UI
|
||||
|
||||
Shaders may optionally declare a custom browser control panel implemented as a standard Web Component. This only changes the bundled React control surface; it does not change shader validation, parameter storage, render cadence, or video I/O behavior.
|
||||
|
||||
Manifest example:
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": {
|
||||
"type": "webComponent",
|
||||
"entry": "ui/controls.js",
|
||||
"tag": "my-shader-controls"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `entry` file is loaded as a JavaScript module from:
|
||||
|
||||
```text
|
||||
/shader-assets/{shaderId}/ui/controls.js
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `type` must be `webComponent`.
|
||||
- `entry` must be a safe relative `.js` or `.mjs` path inside the shader package. Use the package `ui/` directory for UI assets.
|
||||
- `tag` must be a valid custom element name with a hyphen, for example `my-shader-controls`.
|
||||
- The `entry` file must exist when the manifest is loaded.
|
||||
- Custom UI controls must update declared manifest parameters. They cannot create hidden runtime parameters or bypass host validation.
|
||||
- The React UI still provides a **Default controls** fallback for each layer.
|
||||
|
||||
Custom element API:
|
||||
|
||||
```js
|
||||
class MyShaderControls extends HTMLElement {
|
||||
set layer(value) {}
|
||||
set parameters(value) {}
|
||||
set values(value) {}
|
||||
|
||||
connectedCallback() {}
|
||||
}
|
||||
|
||||
customElements.define("my-shader-controls", MyShaderControls);
|
||||
```
|
||||
|
||||
The host sets these properties whenever layer state changes:
|
||||
|
||||
- `layer`: full layer object from `/api/state`.
|
||||
- `parameters`: array of manifest parameter definitions with current `value`.
|
||||
- `values`: object keyed by parameter id.
|
||||
- `setParameter(id, value)`: updates a declared layer parameter through the same route used by the default controls.
|
||||
- `requestReset()`: resets the layer parameters through the normal reset route.
|
||||
|
||||
A custom element may either call the functions directly:
|
||||
|
||||
```js
|
||||
this.setParameter("strength", 0.75);
|
||||
this.requestReset();
|
||||
```
|
||||
|
||||
or dispatch events:
|
||||
|
||||
```js
|
||||
this.dispatchEvent(new CustomEvent("shader-parameter-change", {
|
||||
detail: { parameterId: "strength", value: 0.75 }
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new CustomEvent("shader-reset-parameters"));
|
||||
```
|
||||
|
||||
When the host updates the element's properties, it also dispatches `shader-layer-update` with `{ layer, parameters, values }` in `event.detail`.
|
||||
|
||||
## Render Passes
|
||||
|
||||
Most shaders should omit `passes`. The runtime then creates one implicit pass:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "main",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "shadeVideo",
|
||||
"output": "layerOutput"
|
||||
}
|
||||
```
|
||||
|
||||
Advanced shaders may declare explicit passes. All passes may live in one `.slang` file by using different `entryPoint` values, or they may be split across multiple source files:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "blurX",
|
||||
"source": "blur-x.slang",
|
||||
"entryPoint": "blurHorizontal",
|
||||
"inputs": ["layerInput"],
|
||||
"output": "blurredX"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "final.slang",
|
||||
"entryPoint": "finish",
|
||||
"inputs": ["blurredX"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Pass fields:
|
||||
|
||||
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
|
||||
- `source`: required Slang source path relative to the package directory.
|
||||
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
|
||||
- `inputs`: optional list of named inputs. The first input is used as the pass input texture.
|
||||
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
|
||||
|
||||
Pass input names:
|
||||
|
||||
- `layerInput`: the input to this layer, before any of its passes run.
|
||||
- `previousPass`: the previous pass output in this layer. If there is no previous pass, this falls back to `layerInput`.
|
||||
- Any earlier pass `id` or `output` name from the same layer.
|
||||
|
||||
If `inputs` is omitted, the first pass samples `layerInput` and later passes sample `previousPass`.
|
||||
|
||||
Single-file multipass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "mask",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "makeMask",
|
||||
"output": "maskBuffer"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "finish",
|
||||
"inputs": ["maskBuffer"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Pass output names:
|
||||
|
||||
- `layerOutput`: the final visible output of this layer.
|
||||
- Any other name creates an intermediate 16-bit float render target that later passes may sample.
|
||||
|
||||
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
|
||||
|
||||
## Feedback Surface
|
||||
|
||||
Shaders may opt in to a persistent previous-frame feedback surface:
|
||||
|
||||
```json
|
||||
{
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "final"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `enabled`: when `true`, the runtime allocates one persistent `RGBA16F` feedback surface for this shader at the current render resolution.
|
||||
- `writePass`: optional pass `id` whose output should become next frame's feedback surface. If omitted, the runtime uses the final declared pass, or the implicit `main` pass for single-pass shaders.
|
||||
|
||||
Behavior:
|
||||
|
||||
- all passes may sample the same previous-frame feedback surface
|
||||
- one designated pass writes the next feedback surface
|
||||
- feedback is previous-frame state, not same-frame pass chaining
|
||||
|
||||
Guardrails:
|
||||
|
||||
- Feedback is best suited to image-like state such as trails, masks, luminance fields, decay maps, and shader-local analysis buffers.
|
||||
- Feedback is not a precise long-term data store. The surface uses `RGBA16F`, so repeated accumulation, exact counters, and tightly packed metadata can drift or clamp over time.
|
||||
- The feedback surface is currently filtered like an image, not configured as strict texel-addressed storage. If you reserve texels as data slots, sample them carefully and do not assume exact CPU-style array semantics.
|
||||
- Each feedback-enabled layer allocates two full-resolution feedback textures for ping-pong state. This increases VRAM use and adds one extra full-frame feedback copy per rendered frame.
|
||||
- In multipass shaders, feedback remains previous-frame state even when a pass also consumes same-frame pass outputs. Do not treat feedback as another same-frame intermediate buffer.
|
||||
|
||||
Single-pass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "feedback-glow",
|
||||
"name": "Feedback Glow",
|
||||
"feedback": {
|
||||
"enabled": true
|
||||
},
|
||||
"parameters": []
|
||||
}
|
||||
```
|
||||
|
||||
Multipass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "analysis",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "analyzeFrame",
|
||||
"output": "analysisBuffer"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "finishFrame",
|
||||
"inputs": ["analysisBuffer"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "final"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The wrapper exposes:
|
||||
|
||||
```slang
|
||||
float4 sampleFeedback(float2 uv);
|
||||
```
|
||||
|
||||
On the first frame, or after a reset, `sampleFeedback` returns transparent black.
|
||||
|
||||
Feedback resets when:
|
||||
|
||||
- a layer bypass state changes
|
||||
- a layer changes shader
|
||||
- the layer itself is removed
|
||||
- a shader is reloaded or recompiled
|
||||
- render dimensions change
|
||||
- the app restarts
|
||||
|
||||
Ordinary stack add/remove/reorder operations on other layers are intended to preserve feedback state for unchanged feedback-enabled layers.
|
||||
|
||||
So feedback should be treated as live runtime state, not durable saved state.
|
||||
|
||||
## Slang Entry Point
|
||||
|
||||
Your shader file must implement the manifest `entryPoint`.
|
||||
|
||||
Default:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return context.sourceColor;
|
||||
}
|
||||
```
|
||||
|
||||
The runtime owns the real fragment shader entry point. Your function is called from the wrapper, and the runtime handles final bypass/mix behavior:
|
||||
|
||||
```slang
|
||||
return lerp(context.sourceColor, effectedColor, mixValue);
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
- Return the fully effected color from your function.
|
||||
- Respect alpha if your shader produces an overlay or sprite.
|
||||
- The runtime will blend your result with the source according to `mixAmount` and bypass state.
|
||||
|
||||
## ShaderContext
|
||||
|
||||
Your entry point receives:
|
||||
|
||||
```slang
|
||||
struct ShaderContext
|
||||
{
|
||||
float2 uv;
|
||||
float4 sourceColor;
|
||||
float2 inputResolution;
|
||||
float2 outputResolution;
|
||||
float time;
|
||||
float utcTimeSeconds;
|
||||
float utcOffsetSeconds;
|
||||
float startupRandom;
|
||||
float frameCount;
|
||||
float mixAmount;
|
||||
float bypass;
|
||||
int sourceHistoryLength;
|
||||
int temporalHistoryLength;
|
||||
int feedbackAvailable;
|
||||
};
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `uv`: normalized texture coordinates, usually `0..1`.
|
||||
- `sourceColor`: decoded RGBA source video at `uv`.
|
||||
- `inputResolution`: decoded input video resolution in pixels.
|
||||
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured video I/O output mode.
|
||||
- `time`: elapsed runtime time in seconds.
|
||||
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
|
||||
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
|
||||
- `startupRandom`: random `0..1` value generated once when the host process starts. It stays constant for the lifetime of the app and changes on the next launch.
|
||||
- `frameCount`: incrementing frame counter.
|
||||
- `mixAmount`: runtime mix amount.
|
||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
||||
- `feedbackAvailable`: `1` when previous-frame feedback exists for this layer, otherwise `0`.
|
||||
|
||||
Color/precision notes:
|
||||
|
||||
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
||||
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback. If external keying is enabled, output prefers 10-bit YUVA (`Ay10`) when supported so shader alpha can drive the key signal, then falls back to 8-bit BGRA.
|
||||
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
|
||||
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
The wrapper provides:
|
||||
|
||||
```slang
|
||||
float4 sampleLayerInput(float2 uv);
|
||||
float4 sampleVideo(float2 uv);
|
||||
float4 sampleSourceHistory(int framesAgo, float2 uv);
|
||||
float4 sampleTemporalHistory(int framesAgo, float2 uv);
|
||||
float4 sampleFeedback(float2 uv);
|
||||
```
|
||||
|
||||
`sampleLayerInput` samples the input arriving at this shader layer before any of the layer's own passes run. If this layer follows another shader, it sees that previous shader's output. If this is the first shader layer, it sees the decoded source image.
|
||||
|
||||
`sampleVideo` samples the current pass input texture. In single-pass shaders this is usually the layer input. In multipass shaders it may instead be a named pass output or `previousPass`, depending on the manifest routing for that pass.
|
||||
|
||||
`sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`.
|
||||
|
||||
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
|
||||
|
||||
`sampleFeedback` samples the shader-local previous-frame feedback surface. If feedback has not been written yet, it returns transparent black.
|
||||
|
||||
Example:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleSourceHistory(1, context.uv);
|
||||
return lerp(context.sourceColor, previous, 0.35);
|
||||
}
|
||||
```
|
||||
|
||||
Layer-input example:
|
||||
|
||||
```slang
|
||||
float4 finishPass(ShaderContext context)
|
||||
{
|
||||
float3 baseColor = sampleLayerInput(context.uv).rgb;
|
||||
float3 passResult = context.sourceColor.rgb;
|
||||
return float4(baseColor + passResult * 0.25, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
Feedback example:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleFeedback(context.uv);
|
||||
float4 current = context.sourceColor;
|
||||
return lerp(current, previous, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
Multipass feedback example:
|
||||
|
||||
```slang
|
||||
float4 analyzeFrame(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleFeedback(context.uv);
|
||||
float luma = dot(context.sourceColor.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
return float4(lerp(previous.rgb, float3(luma), 0.1), 1.0);
|
||||
}
|
||||
|
||||
float4 finishFrame(ShaderContext context)
|
||||
{
|
||||
float4 analysis = context.sourceColor;
|
||||
return float4(analysis.rgb, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
In that multipass case:
|
||||
|
||||
- `analyzeFrame` reads last frame's feedback
|
||||
- `finishFrame` receives the same-frame pass output through normal multipass routing
|
||||
- the `writePass` decides which pass output becomes next frame's feedback
|
||||
|
||||
That means:
|
||||
|
||||
- use `context.sourceColor` or `sampleVideo()` when you want this pass's routed input
|
||||
- use `sampleLayerInput()` when you want the pre-pass layer input
|
||||
- use `sampleFeedback()` when you want previous-frame persistent shader-local state
|
||||
|
||||
## Parameters
|
||||
|
||||
Manifest parameters are exposed to Slang as global values with the same `id`.
|
||||
|
||||
Supported types:
|
||||
|
||||
| Manifest type | Slang type | JSON value |
|
||||
| --- | --- | --- |
|
||||
| `float` | `float` | number |
|
||||
| `vec2` | `float2` | `[x, y]` |
|
||||
| `color` | `float4` | `[r, g, b, a]` |
|
||||
| `bool` | `bool` | `true` or `false` |
|
||||
| `enum` | `int` | selected option index |
|
||||
| `text` | generated texture/helper | string |
|
||||
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
|
||||
|
||||
Float example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
color.rgb *= brightness;
|
||||
```
|
||||
|
||||
Vector example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-0.2, -0.2],
|
||||
"max": [0.2, 0.2],
|
||||
"step": [0.001, 0.001]
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
float2 uv = clamp(context.uv + offset, float2(0.0), float2(1.0));
|
||||
```
|
||||
|
||||
Color example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "tint",
|
||||
"label": "Tint",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
color *= tint;
|
||||
```
|
||||
|
||||
Boolean example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "invert",
|
||||
"label": "Invert",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
}
|
||||
```
|
||||
|
||||
```slang
|
||||
if (invert)
|
||||
color.rgb = 1.0 - color.rgb;
|
||||
```
|
||||
|
||||
Enum example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mode",
|
||||
"label": "Mode",
|
||||
"type": "enum",
|
||||
"default": "normal",
|
||||
"options": [
|
||||
{ "value": "normal", "label": "Normal" },
|
||||
{ "value": "luma", "label": "Luma" },
|
||||
{ "value": "posterize", "label": "Posterize" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Enums are stored in presets/state by their string `value`, but exposed to Slang as a zero-based integer index in option order:
|
||||
|
||||
```slang
|
||||
if (mode == 1)
|
||||
{
|
||||
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
color.rgb = float3(luma);
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
color.rgb = floor(color.rgb * 4.0) / 4.0;
|
||||
}
|
||||
```
|
||||
|
||||
Text example:
|
||||
|
||||
```json
|
||||
{
|
||||
"fonts": [
|
||||
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" }
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "titleText",
|
||||
"label": "Title",
|
||||
"type": "text",
|
||||
"default": "LIVE",
|
||||
"font": "inter",
|
||||
"maxLength": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Text parameters are runtime-owned strings. They are not emitted as uniform values. Instead, the runtime renders the current string into a single-line SDF mask texture and the shader wrapper exposes helpers based on the parameter id:
|
||||
|
||||
```slang
|
||||
float mask = sampleTitleText(textUv);
|
||||
float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
|
||||
```
|
||||
|
||||
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
|
||||
|
||||
Text parameters can also choose their font from an enum parameter by setting `fontParameter` to that enum parameter's `id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"fonts": [
|
||||
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" },
|
||||
{ "id": "mono", "path": "fonts/Mono-Regular.ttf" }
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "font",
|
||||
"label": "Font",
|
||||
"type": "enum",
|
||||
"default": "inter",
|
||||
"options": [
|
||||
{ "value": "inter", "label": "Inter" },
|
||||
{ "value": "mono", "label": "Mono" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "titleText",
|
||||
"label": "Title",
|
||||
"type": "text",
|
||||
"default": "LIVE",
|
||||
"font": "inter",
|
||||
"fontParameter": "font",
|
||||
"maxLength": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Every option `value` in the font selector enum must match a declared font asset `id`. The `font` field remains useful as the default/fallback font for the text parameter, while `fontParameter` lets operators switch atlases at runtime without adding shader-specific code.
|
||||
|
||||
Trigger example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "flash",
|
||||
"label": "Flash",
|
||||
"type": "trigger"
|
||||
}
|
||||
```
|
||||
|
||||
A trigger appears as a button in the control UI. Pressing it increments the shader-visible integer `flash` and records the runtime time in `flashTime`:
|
||||
|
||||
```slang
|
||||
float age = context.time - flashTime;
|
||||
float intensity = flash > 0 ? exp(-age * 5.0) : 0.0;
|
||||
color.rgb += intensity;
|
||||
```
|
||||
|
||||
Triggers are useful for one-shot shader reactions such as flashes, ripples, cuts, or randomized looks. They do not execute arbitrary CPU code; they only update uniforms consumed by the shader.
|
||||
|
||||
Parameter validation:
|
||||
|
||||
- Float values are clamped to `min`/`max` if provided.
|
||||
- `vec2` must have exactly 2 numbers.
|
||||
- `color` must have exactly 4 numbers.
|
||||
- Enum defaults must match one of the declared option values.
|
||||
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
|
||||
- Text `fontParameter` values must reference an enum parameter whose option values are declared font asset IDs.
|
||||
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
|
||||
- Non-finite numeric values are rejected.
|
||||
|
||||
## Texture Assets
|
||||
|
||||
Declare texture assets in the manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"textures": [
|
||||
{
|
||||
"id": "logoTexture",
|
||||
"path": "logo.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `id` must be a valid shader identifier.
|
||||
- `path` is relative to the shader package directory.
|
||||
- The file must exist when the manifest is loaded.
|
||||
- Texture asset changes trigger shader reload.
|
||||
|
||||
Texture IDs become `Sampler2D<float4>` globals:
|
||||
|
||||
```slang
|
||||
float4 logo = logoTexture.Sample(logoUv);
|
||||
```
|
||||
|
||||
For sprite or overlay shaders, return premultiplied-looking output if you want clean composition:
|
||||
|
||||
```slang
|
||||
float alpha = logo.a;
|
||||
return float4(logo.rgb * alpha, alpha);
|
||||
```
|
||||
|
||||
See `shaders/dvd-bounce/` for a complete texture-driven example.
|
||||
|
||||
## Font Assets
|
||||
|
||||
Declare packaged font assets in the manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"fonts": [
|
||||
{
|
||||
"id": "inter",
|
||||
"path": "fonts/Inter-Regular.ttf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `id` must be a valid shader identifier.
|
||||
- `path` is relative to the shader package directory.
|
||||
- The file must exist when the manifest is loaded.
|
||||
- Font asset changes trigger shader reload.
|
||||
- V1 text layout is single-line; shaders position and scale the generated text texture themselves.
|
||||
|
||||
See `shaders/text-overlay/` for a complete live text example. The sample bundles Roboto Regular and includes its OFL license beside the font file.
|
||||
|
||||
## Temporal Shaders
|
||||
|
||||
Temporal shaders can request access to previous frames.
|
||||
|
||||
Manifest example:
|
||||
|
||||
```json
|
||||
{
|
||||
"temporal": {
|
||||
"enabled": true,
|
||||
"historySource": "preLayerInput",
|
||||
"historyLength": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `historySource` values:
|
||||
|
||||
- `source`: decoded source-video history from previous frames.
|
||||
- `preLayerInput`: history of the input arriving at this layer before the shader runs.
|
||||
|
||||
`historyLength` is the requested frame count. The runtime clamps it by `maxTemporalHistoryFrames` in `config/runtime-host.json`.
|
||||
|
||||
Temporal history resets when:
|
||||
|
||||
- layers are added, removed, or reordered
|
||||
- a layer bypass state changes
|
||||
- a layer changes shader
|
||||
- a shader is reloaded or recompiled
|
||||
- render dimensions change
|
||||
|
||||
Use the available history lengths to avoid assuming history is ready on the first frame:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
if (context.temporalHistoryLength <= 0)
|
||||
return context.sourceColor;
|
||||
|
||||
float4 oldFrame = sampleTemporalHistory(3, context.uv);
|
||||
return lerp(context.sourceColor, oldFrame, 0.4);
|
||||
}
|
||||
```
|
||||
|
||||
See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples.
|
||||
|
||||
## Coordinate And Color Notes
|
||||
|
||||
- `uv` is normalized.
|
||||
- Use `context.outputResolution` for pixel-sized effects.
|
||||
- Use `context.inputResolution` when sampling source video by input pixel size.
|
||||
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
|
||||
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
|
||||
- For generated calibration charts, test patterns, gradients, and exposure ramps, state whether patch values are linear-light, display-referred gamma encoded, Rec.709 encoded, or intentionally artistic.
|
||||
- For one-stop exposure patches, each patch should normally be `baseLevel * 2^patchIndex` before any display/tone encoding.
|
||||
- For Rec.709 OETF encoding, use:
|
||||
|
||||
```slang
|
||||
float rec709Oetf(float linearLevel)
|
||||
{
|
||||
float value = saturate(linearLevel);
|
||||
if (value < 0.018)
|
||||
return 4.5 * value;
|
||||
return 1.099 * pow(value, 0.45) - 0.099;
|
||||
}
|
||||
```
|
||||
|
||||
Pixel-size example:
|
||||
|
||||
```slang
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
|
||||
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
|
||||
```
|
||||
|
||||
## Animation And Timing Notes
|
||||
|
||||
- `context.time` is elapsed runtime time in seconds and is the default animation source for generative shaders.
|
||||
- `context.frameCount` increments once per rendered output frame and is useful when an effect must be frame-locked.
|
||||
- Avoid expensive CPU-like timing logic in the shader; animation should usually be a simple function of `context.time`, `context.frameCount`, trigger uniforms, or parameters.
|
||||
- If a shader appears to judder only while animated, first test whether freezing its time removes the issue. That usually separates animation cadence issues from rendering or transfer issues.
|
||||
- Do not add custom timer uniforms to the wrapper. Use the fields already in `ShaderContext`.
|
||||
|
||||
## Performance Notes
|
||||
|
||||
The app has to meet a fixed video frame cadence, so avoid shader code that only looks good in unconstrained browser demos.
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Keep loops bounded with compile-time constants where possible.
|
||||
- Avoid very high per-pixel raymarch counts by default. If a heavy loop is needed, expose a quality/steps control with a safe default.
|
||||
- Prefer early exits only when they are simple; highly divergent branches can be expensive across a full frame.
|
||||
- Avoid repeated texture sampling in large loops unless the effect really needs it.
|
||||
- Use `context.outputResolution` carefully. A 1080p frame is over 2 million fragments; a tiny extra loop can become expensive.
|
||||
- The UI render time may measure CPU command submission rather than true GPU execution time, so visual frame issues can still be GPU-related even when reported render time is small.
|
||||
- Do not write debug files, allocate resources, or assume CPU-side work can happen from `shader.slang`. Shader code is GPU-only.
|
||||
|
||||
## Reload And Generated Files
|
||||
|
||||
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
|
||||
|
||||
- `active_shader_wrapper.slang`
|
||||
- `active_shader.raw.frag`
|
||||
- `active_shader.frag`
|
||||
|
||||
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
|
||||
|
||||
For multipass shaders, these files reflect the most recently compiled pass. If a package has several passes, the reported compile error and pass name are usually more useful than assuming the cache contains the first pass.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.
|
||||
- Do not declare your own `ShaderContext`, `GlobalParams`, `sampleVideo`, `sampleSourceHistory`, or `sampleTemporalHistory`.
|
||||
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
|
||||
- Remember enum globals are integer indexes, not strings.
|
||||
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
|
||||
- Declare packaged fonts in `shader.json` when text parameters should use a specific font.
|
||||
- For selectable fonts, use a text parameter `fontParameter` that points at an enum whose option values are font IDs.
|
||||
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
|
||||
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
|
||||
- If a Slang name collides with a generated global, rename your parameter or local symbol.
|
||||
|
||||
## Minimal Package Checklist
|
||||
|
||||
Before committing a new shader package:
|
||||
|
||||
- `shader.json` is valid JSON.
|
||||
- `id` is unique across `shaders/`.
|
||||
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
|
||||
- `shader.slang` implements the configured entry point.
|
||||
- Texture files referenced by `textures` exist.
|
||||
- Font files referenced by `fonts` exist.
|
||||
- Enum defaults are present in their `options`.
|
||||
- Text `fontParameter` selectors reference valid font assets through their enum options.
|
||||
- Custom UI entries, when present, load from a safe package-relative JavaScript module and register the declared web component tag.
|
||||
- Temporal shaders handle short or empty history gracefully.
|
||||
- The app can reload and compile the shader without errors.
|
||||
85
shaders/anamorphic-desqueeze/shader.json
Normal file
85
shaders/anamorphic-desqueeze/shader.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"id": "anamorphic-desqueeze",
|
||||
"name": "Anamorphic Desqueeze",
|
||||
"description": "Desqueezes anamorphic footage by 1.3x, 1.33x, 1.5x, or 2x with fit or fill framing.",
|
||||
"category": "Transform",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "desqueezeFactor",
|
||||
"label": "Desqueeze",
|
||||
"type": "enum",
|
||||
"default": "x1_33",
|
||||
"options": [
|
||||
{
|
||||
"value": "x1_3",
|
||||
"label": "1.3x"
|
||||
},
|
||||
{
|
||||
"value": "x1_33",
|
||||
"label": "1.33x"
|
||||
},
|
||||
{
|
||||
"value": "x1_5",
|
||||
"label": "1.5x"
|
||||
},
|
||||
{
|
||||
"value": "x2_0",
|
||||
"label": "2x"
|
||||
}
|
||||
],
|
||||
"description": "Horizontal stretch factor matching the anamorphic lens or adapter."
|
||||
},
|
||||
{
|
||||
"id": "framing",
|
||||
"label": "Framing",
|
||||
"type": "enum",
|
||||
"default": "fit",
|
||||
"options": [
|
||||
{
|
||||
"value": "fit",
|
||||
"label": "Fit"
|
||||
},
|
||||
{
|
||||
"value": "fill",
|
||||
"label": "Fill"
|
||||
}
|
||||
],
|
||||
"description": "Fit preserves the whole image; Fill crops to remove borders."
|
||||
},
|
||||
{
|
||||
"id": "pan",
|
||||
"label": "Pan",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Reframes the desqueezed image after fit/fill scaling."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
32
shaders/anamorphic-desqueeze/shader.slang
Normal file
@@ -0,0 +1,32 @@
|
||||
float selectedDesqueezeFactor()
|
||||
{
|
||||
if (desqueezeFactor == 0)
|
||||
return 1.3;
|
||||
if (desqueezeFactor == 1)
|
||||
return 1.3333333;
|
||||
if (desqueezeFactor == 2)
|
||||
return 1.5;
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float factor = selectedDesqueezeFactor();
|
||||
float2 centered = context.uv - 0.5;
|
||||
|
||||
if (framing == 0)
|
||||
{
|
||||
centered.y *= factor;
|
||||
}
|
||||
else
|
||||
{
|
||||
centered.x /= factor;
|
||||
}
|
||||
|
||||
float2 sourceUv = centered + 0.5 - pan;
|
||||
bool inside = sourceUv.x >= 0.0 && sourceUv.x <= 1.0 && sourceUv.y >= 0.0 && sourceUv.y <= 1.0;
|
||||
if (!inside)
|
||||
return outsideColor;
|
||||
|
||||
return sampleVideo(sourceUv);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"id": "audio-vu-meter",
|
||||
"name": "Audio VU Meter",
|
||||
"description": "Draws stereo audio level meters from the runtime audio analysis data.",
|
||||
"category": "Utility",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "meterPosition",
|
||||
"label": "Position",
|
||||
"type": "vec2",
|
||||
"default": [0.08, 0.82],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.01, 0.01]
|
||||
},
|
||||
{
|
||||
"id": "meterScale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 0.35,
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "meterOpacity",
|
||||
"label": "Opacity",
|
||||
"type": "float",
|
||||
"default": 0.9,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "noiseGate",
|
||||
"label": "Noise Gate",
|
||||
"type": "float",
|
||||
"default": 0.03,
|
||||
"min": 0.0,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "meterColor",
|
||||
"label": "Meter Color",
|
||||
"type": "color",
|
||||
"default": [0.2, 1.0, 0.55, 1.0]
|
||||
},
|
||||
{
|
||||
"id": "peakColor",
|
||||
"label": "Peak Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 0.85, 0.2, 1.0]
|
||||
},
|
||||
{
|
||||
"id": "backgroundOpacity",
|
||||
"label": "Background",
|
||||
"type": "float",
|
||||
"default": 0.45,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "orientation",
|
||||
"label": "Orientation",
|
||||
"type": "enum",
|
||||
"default": "horizontal",
|
||||
"options": [
|
||||
{ "value": "horizontal", "label": "Horizontal" },
|
||||
{ "value": "vertical", "label": "Vertical" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
float rectMask(float2 uv, float2 minUv, float2 maxUv)
|
||||
{
|
||||
float2 insideMin = step(minUv, uv);
|
||||
float2 insideMax = step(uv, maxUv);
|
||||
return insideMin.x * insideMin.y * insideMax.x * insideMax.y;
|
||||
}
|
||||
|
||||
float denoiseLevel(float value)
|
||||
{
|
||||
float gate = saturate(noiseGate);
|
||||
float clean = saturate((value - gate) / max(1.0 - gate, 0.001));
|
||||
return smoothstep(0.0, 1.0, clean);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 color = context.sourceColor;
|
||||
float2 size = orientation == 0 ? float2(meterScale, meterScale * 0.18) : float2(meterScale * 0.18, meterScale);
|
||||
float2 minUv = clamp(meterPosition, 0.0, 1.0 - size);
|
||||
float2 local = (context.uv - minUv) / max(size, float2(0.001));
|
||||
float inside = rectMask(local, float2(0.0), float2(1.0));
|
||||
if (inside <= 0.0)
|
||||
return color;
|
||||
|
||||
float3 bg = lerp(color.rgb, float3(0.0), saturate(backgroundOpacity));
|
||||
float leftLevel = denoiseLevel(context.audioRms.x * 2.4);
|
||||
float rightLevel = denoiseLevel(context.audioRms.y * 2.4);
|
||||
float leftPeak = denoiseLevel(context.audioPeak.x);
|
||||
float rightPeak = denoiseLevel(context.audioPeak.y);
|
||||
|
||||
float bar = 0.0;
|
||||
float peak = 0.0;
|
||||
if (orientation == 0)
|
||||
{
|
||||
float leftRow = rectMask(local, float2(0.04, 0.58), float2(0.96, 0.86));
|
||||
float rightRow = rectMask(local, float2(0.04, 0.14), float2(0.96, 0.42));
|
||||
float leftFill = rectMask(local, float2(0.04, 0.58), float2(0.04 + 0.92 * leftLevel, 0.86));
|
||||
float rightFill = rectMask(local, float2(0.04, 0.14), float2(0.04 + 0.92 * rightLevel, 0.42));
|
||||
float leftPeakLine = rectMask(local, float2(0.04 + 0.92 * leftPeak - 0.006, 0.55), float2(0.04 + 0.92 * leftPeak + 0.006, 0.89));
|
||||
float rightPeakLine = rectMask(local, float2(0.04 + 0.92 * rightPeak - 0.006, 0.11), float2(0.04 + 0.92 * rightPeak + 0.006, 0.45));
|
||||
bar = max(leftFill, rightFill);
|
||||
peak = max(leftPeakLine * leftRow, rightPeakLine * rightRow);
|
||||
}
|
||||
else
|
||||
{
|
||||
float leftColumn = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.96));
|
||||
float rightColumn = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.96));
|
||||
float leftFill = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.04 + 0.92 * leftLevel));
|
||||
float rightFill = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.04 + 0.92 * rightLevel));
|
||||
float leftPeakLine = rectMask(local, float2(0.11, 0.04 + 0.92 * leftPeak - 0.006), float2(0.45, 0.04 + 0.92 * leftPeak + 0.006));
|
||||
float rightPeakLine = rectMask(local, float2(0.55, 0.04 + 0.92 * rightPeak - 0.006), float2(0.89, 0.04 + 0.92 * rightPeak + 0.006));
|
||||
bar = max(leftFill * leftColumn, rightFill * rightColumn);
|
||||
peak = max(leftPeakLine, rightPeakLine);
|
||||
}
|
||||
|
||||
float3 metered = lerp(bg, meterColor.rgb, bar * saturate(meterOpacity) * meterColor.a);
|
||||
metered = lerp(metered, peakColor.rgb, peak * saturate(meterOpacity) * peakColor.a);
|
||||
return float4(metered, color.a);
|
||||
}
|
||||
144
shaders/balatro-swirl/shader.json
Normal file
144
shaders/balatro-swirl/shader.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "balatro-swirl",
|
||||
"name": "Balatro Swirl",
|
||||
"description": "Animated painterly swirl background. Original by localthunk (https://www.playbalatro.com), adapted from https://www.shadertoy.com/view/XXtBRr.",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "spinRotation",
|
||||
"label": "Spin Rotation",
|
||||
"type": "float",
|
||||
"default": -2,
|
||||
"min": -8,
|
||||
"max": 8,
|
||||
"step": 0.05,
|
||||
"description": "Base rotation applied to the swirl field."
|
||||
},
|
||||
{
|
||||
"id": "spinSpeed",
|
||||
"label": "Spin Speed",
|
||||
"type": "float",
|
||||
"default": 7,
|
||||
"min": 0,
|
||||
"max": 20,
|
||||
"step": 0.1,
|
||||
"description": "How quickly the swirl pattern rotates."
|
||||
},
|
||||
{
|
||||
"id": "spinAmount",
|
||||
"label": "Spin Amount",
|
||||
"type": "float",
|
||||
"default": 0.25,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Amount of radial twisting in the swirl."
|
||||
},
|
||||
{
|
||||
"id": "spinEase",
|
||||
"label": "Spin Ease",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Changes how strongly the twist falls off from the center."
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"label": "Contrast",
|
||||
"type": "float",
|
||||
"default": 3.5,
|
||||
"min": 0.5,
|
||||
"max": 8,
|
||||
"step": 0.05,
|
||||
"description": "Adjusts separation between dark and bright areas."
|
||||
},
|
||||
{
|
||||
"id": "lighting",
|
||||
"label": "Lighting",
|
||||
"type": "float",
|
||||
"default": 0.4,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the highlight/shadow modulation."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the generated field in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "colour1",
|
||||
"label": "Colour 1",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.871,
|
||||
0.267,
|
||||
0.231,
|
||||
1
|
||||
],
|
||||
"description": "Primary warm swirl color."
|
||||
},
|
||||
{
|
||||
"id": "colour2",
|
||||
"label": "Colour 2",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0.42,
|
||||
0.706,
|
||||
1
|
||||
],
|
||||
"description": "Secondary cool swirl color."
|
||||
},
|
||||
{
|
||||
"id": "colour3",
|
||||
"label": "Colour 3",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.086,
|
||||
0.137,
|
||||
0.145,
|
||||
1
|
||||
],
|
||||
"description": "Dark base color in the swirl."
|
||||
},
|
||||
{
|
||||
"id": "isRotate",
|
||||
"label": "Rotate Field",
|
||||
"type": "bool",
|
||||
"default": false,
|
||||
"description": "Rotates the whole generated field over time."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
54
shaders/balatro-swirl/shader.slang
Normal file
54
shaders/balatro-swirl/shader.slang
Normal file
@@ -0,0 +1,54 @@
|
||||
float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float seed)
|
||||
{
|
||||
const float pi = 3.14159265359;
|
||||
float safeScreenLength = max(length(screenSize), 1.0);
|
||||
float2 seedOffset = float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
|
||||
float uvLength = length(uv);
|
||||
|
||||
// First warp: convert to polar space and twist the angle more near the
|
||||
// center, creating the large spiral motion.
|
||||
float speed = spinRotation * spinEase * 0.2;
|
||||
if (isRotate)
|
||||
speed = time * speed;
|
||||
speed += 302.2 + seed * 6.2831853;
|
||||
|
||||
float newPixelAngle = atan2(uv.y, uv.x) + speed - spinEase * 20.0 * (spinAmount * uvLength + (1.0 - spinAmount));
|
||||
float2 mid = (screenSize / safeScreenLength) * 0.5;
|
||||
uv = float2(uvLength * cos(newPixelAngle) + mid.x, uvLength * sin(newPixelAngle) + mid.y) - mid;
|
||||
|
||||
uv *= 30.0;
|
||||
speed = (time + seed * 17.0) * spinSpeed;
|
||||
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
||||
|
||||
// Second warp: a short iterative feedback loop turns the spiral into
|
||||
// painterly bands while preserving a fixed compile-time loop bound.
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
|
||||
uv += 0.5 * float2(cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121), sin(uv2.x - 0.113 * speed));
|
||||
float warp = cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);
|
||||
uv -= float2(warp, warp);
|
||||
}
|
||||
|
||||
float contrastMod = 0.25 * contrast + 0.5 * spinAmount + 1.2;
|
||||
float paintRes = min(2.0, max(0.0, length(uv) * 0.035 * contrastMod));
|
||||
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
|
||||
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
|
||||
float c3p = 1.0 - min(1.0, c1p + c2p);
|
||||
// Three soft band weights drive the palette; lighting rides on the brightest
|
||||
// bands so the swirl keeps dimensional highlights.
|
||||
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
|
||||
|
||||
float safeContrast = max(contrast, 0.001);
|
||||
float4 base = (0.3 / safeContrast) * colour1;
|
||||
float4 paint = colour1 * c1p + colour2 * c2p + float4(c3p * colour3.rgb, c3p * colour1.a);
|
||||
return base + (1.0 - 0.3 / safeContrast) * paint + float4(light, light, light, light);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 screenSize = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float4 swirl = balatroSwirl(screenSize, context.uv * screenSize, context.time, context.startupRandom);
|
||||
return saturate(lerp(swirl, context.sourceColor, sourceMix));
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "black-and-white",
|
||||
"name": "Black and White",
|
||||
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
|
||||
"category": "Built-in",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": []
|
||||
}
|
||||
|
||||
16
shaders/broken-shader-example/shader.json
Normal file
16
shaders/broken-shader-example/shader.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "broken-shader-example",
|
||||
"name": "Broken Shader Example",
|
||||
"description": "Intentionally invalid shader package used to verify that bad shaders appear as errors without blocking the app.",
|
||||
"category": "Diagnostics",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "badToggle",
|
||||
"label": "Bad Toggle",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Intentionally unsupported parameter type used to test shader error handling."
|
||||
}
|
||||
]
|
||||
}
|
||||
4
shaders/broken-shader-example/shader.slang
Normal file
4
shaders/broken-shader-example/shader.slang
Normal file
@@ -0,0 +1,4 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return context.sourceColor;
|
||||
}
|
||||
@@ -2,62 +2,74 @@
|
||||
"id": "composition-guides",
|
||||
"name": "Composition Guides",
|
||||
"description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.",
|
||||
"category": "Utility",
|
||||
"category": "Scopes & Guides",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "showThirds",
|
||||
"label": "Rule of Thirds",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Shows vertical and horizontal thirds lines."
|
||||
},
|
||||
{
|
||||
"id": "showCrosshair",
|
||||
"label": "Center Crosshair",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Shows a center crosshair for lens/framing alignment."
|
||||
},
|
||||
{
|
||||
"id": "lineColor",
|
||||
"label": "Line Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Color used for guide lines and marks."
|
||||
},
|
||||
{
|
||||
"id": "lineOpacity",
|
||||
"label": "Line Opacity",
|
||||
"type": "float",
|
||||
"default": 0.65,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the guide lines."
|
||||
},
|
||||
{
|
||||
"id": "lineThicknessPixels",
|
||||
"label": "Line Thickness",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"default": 2,
|
||||
"min": 0.5,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Guide line width in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "crosshairSizePixels",
|
||||
"label": "Crosshair Size",
|
||||
"type": "float",
|
||||
"default": 54.0,
|
||||
"min": 8.0,
|
||||
"max": 240.0,
|
||||
"step": 1.0
|
||||
"default": 54,
|
||||
"min": 8,
|
||||
"max": 240,
|
||||
"step": 1,
|
||||
"description": "Length of each crosshair arm in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "crosshairGapPixels",
|
||||
"label": "Crosshair Gap",
|
||||
"type": "float",
|
||||
"default": 10.0,
|
||||
"min": 0.0,
|
||||
"max": 80.0,
|
||||
"step": 1.0
|
||||
"default": 10,
|
||||
"min": 0,
|
||||
"max": 80,
|
||||
"step": 1,
|
||||
"description": "Empty gap around the exact frame center."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
102
shaders/crt-bulge/shader.json
Normal file
102
shaders/crt-bulge/shader.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "crt-bulge",
|
||||
"name": "CRT Bulge",
|
||||
"description": "Warps the image like convex CRT glass, with optional rounded screen edges and vignette darkening.",
|
||||
"category": "Distortion",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "bulgeAmount",
|
||||
"label": "Bulge",
|
||||
"type": "float",
|
||||
"default": -0.04,
|
||||
"min": -0.5,
|
||||
"max": 0.8,
|
||||
"step": 0.01,
|
||||
"description": "Positive values swell the center outward; negative values pinch it inward."
|
||||
},
|
||||
{
|
||||
"id": "zoom",
|
||||
"label": "Zoom",
|
||||
"type": "float",
|
||||
"default": 1.04,
|
||||
"min": 0.5,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Scales the source before distortion, useful for hiding warped edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeRoundness",
|
||||
"label": "Edge Roundness",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0,
|
||||
"max": 0.35,
|
||||
"step": 0.01,
|
||||
"description": "Rounds the visible screen corners like older CRT glass."
|
||||
},
|
||||
{
|
||||
"id": "edgeFeather",
|
||||
"label": "Edge Feather",
|
||||
"type": "float",
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 24,
|
||||
"step": 0.1,
|
||||
"description": "Softens the rounded screen edge in pixels."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeFeather",
|
||||
"label": "Source Edge Feather",
|
||||
"type": "float",
|
||||
"default": 1.5,
|
||||
"min": 0,
|
||||
"max": 16,
|
||||
"step": 0.1,
|
||||
"description": "Antialiases warped source edges when the distortion reveals outside-frame pixels."
|
||||
},
|
||||
{
|
||||
"id": "vignetteAmount",
|
||||
"label": "Vignette",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Darkens the glass toward the screen edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeMode",
|
||||
"label": "Edge Mode",
|
||||
"type": "enum",
|
||||
"default": "black",
|
||||
"options": [
|
||||
{
|
||||
"value": "black",
|
||||
"label": "Black"
|
||||
},
|
||||
{
|
||||
"value": "clamp",
|
||||
"label": "Clamp"
|
||||
},
|
||||
{
|
||||
"value": "mirror",
|
||||
"label": "Mirror"
|
||||
}
|
||||
],
|
||||
"description": "Chooses how warped samples outside the source frame are filled."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used outside the curved screen or source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
71
shaders/crt-bulge/shader.slang
Normal file
71
shaders/crt-bulge/shader.slang
Normal file
@@ -0,0 +1,71 @@
|
||||
float mirroredCoordinate(float coordinate)
|
||||
{
|
||||
float wrapped = frac(coordinate * 0.5) * 2.0;
|
||||
return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
|
||||
}
|
||||
|
||||
float roundedBoxMask(float2 point, float2 halfSize, float radius, float feather)
|
||||
{
|
||||
float2 distanceToEdge = abs(point) - (halfSize - radius);
|
||||
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))) - radius;
|
||||
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
|
||||
float signedDistance = outsideDistance + insideDistance;
|
||||
return 1.0 - smoothstep(0.0, max(feather, 0.00001), signedDistance);
|
||||
}
|
||||
|
||||
float sourceBoundsMask(float2 uv, float2 resolution)
|
||||
{
|
||||
float2 pixel = 1.0 / max(resolution, float2(1.0, 1.0));
|
||||
float2 feather = pixel * max(sourceEdgeFeather, 0.0);
|
||||
float left = smoothstep(0.0, max(feather.x, 0.00001), uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - max(feather.x, 0.00001), 1.0, uv.x);
|
||||
float top = smoothstep(0.0, max(feather.y, 0.00001), uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - max(feather.y, 0.00001), 1.0, uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float2 applyBulge(float2 uv, float2 resolution)
|
||||
{
|
||||
float2 centered = uv * 2.0 - 1.0;
|
||||
float aspect = resolution.x / max(resolution.y, 1.0);
|
||||
float2 aspectCentered = float2(centered.x * aspect, centered.y);
|
||||
float radiusSq = dot(aspectCentered, aspectCentered);
|
||||
float amount = clamp(bulgeAmount, -0.95, 0.95);
|
||||
float scale = 1.0 / max(1.0 + amount * radiusSq, 0.05);
|
||||
return centered * scale / max(zoom, 0.001) * 0.5 + 0.5;
|
||||
}
|
||||
|
||||
float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
|
||||
{
|
||||
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||
|
||||
if (edgeMode == 1)
|
||||
return sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||
if (edgeMode == 2)
|
||||
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||
|
||||
float edgeMask = sourceBoundsMask(uv, resolution);
|
||||
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||
return lerp(outsideColor, color, edgeMask);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 sourceUv = applyBulge(context.uv, resolution);
|
||||
|
||||
bool insideSource = false;
|
||||
float4 color = sampleWarped(sourceUv, resolution, insideSource);
|
||||
|
||||
float2 centered = context.uv * 2.0 - 1.0;
|
||||
float feather = max(edgeFeather, 0.0) / min(resolution.x, resolution.y);
|
||||
float screenMask = roundedBoxMask(centered, float2(1.0, 1.0), saturate(edgeRoundness), feather);
|
||||
color = lerp(outsideColor, color, screenMask);
|
||||
|
||||
float2 aspectCentered = float2(centered.x * resolution.x / max(resolution.y, 1.0), centered.y);
|
||||
float edgeDistance = saturate(length(aspectCentered) * 0.72);
|
||||
float vignette = lerp(1.0, 1.0 - saturate(vignetteAmount), smoothstep(0.35, 1.05, edgeDistance));
|
||||
color.rgb *= vignette;
|
||||
|
||||
return saturate(color);
|
||||
}
|
||||
@@ -15,36 +15,52 @@
|
||||
"label": "Amount",
|
||||
"type": "float",
|
||||
"default": 0.45,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the blocky temporal smear."
|
||||
},
|
||||
{
|
||||
"id": "blockCount",
|
||||
"label": "Block Count",
|
||||
"type": "vec2",
|
||||
"default": [32.0, 18.0],
|
||||
"min": [2.0, 2.0],
|
||||
"max": [160.0, 120.0],
|
||||
"step": [1.0, 1.0]
|
||||
"default": [
|
||||
32,
|
||||
18
|
||||
],
|
||||
"min": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"max": [
|
||||
160,
|
||||
120
|
||||
],
|
||||
"step": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Number of glitch blocks across X and Y."
|
||||
},
|
||||
{
|
||||
"id": "tearAmount",
|
||||
"label": "Tear",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Horizontal scanline tearing intensity."
|
||||
},
|
||||
{
|
||||
"id": "chromaShift",
|
||||
"label": "Chroma Shift",
|
||||
"type": "float",
|
||||
"default": 1.8,
|
||||
"min": 0.0,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"min": 0,
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Separate color-channel offset in pixels."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 blocks = max(blockCount, float2(1.0, 1.0));
|
||||
float2 blockId = floor(context.uv * blocks);
|
||||
float n = hash21(blockId + floor(context.time * 8.0));
|
||||
float seed = context.startupRandom * 4096.0;
|
||||
float frameSeed = floor(context.time * 8.0);
|
||||
float n = hash21(blockId + float2(frameSeed + seed, frameSeed + seed * 0.17));
|
||||
float historyFrame = floor(lerp(1.0, 7.0, n));
|
||||
|
||||
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y), floor(context.time * 5.0)));
|
||||
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y) + seed * 0.37, floor(context.time * 5.0) + seed * 0.13));
|
||||
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
|
||||
float2 offset = float2(tear, (hash21(blockId + 19.0) * 2.0 - 1.0) * amount * 0.025);
|
||||
float2 offset = float2(tear, (hash21(blockId + float2(19.0 + seed, 19.0 + seed * 0.31)) * 2.0 - 1.0) * amount * 0.025);
|
||||
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
|
||||
|
||||
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "dvd-bounce",
|
||||
"name": "DVD Bounce",
|
||||
"description": "A transparent bouncing DVD logo sprite that changes color on each screen hit.",
|
||||
"category": "Built-in",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"textures": [
|
||||
{
|
||||
@@ -18,7 +18,8 @@
|
||||
"default": 0.28,
|
||||
"min": 0.12,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Logo size relative to the frame."
|
||||
},
|
||||
{
|
||||
"id": "bounceSpeed",
|
||||
@@ -27,34 +28,38 @@
|
||||
"default": 0.22,
|
||||
"min": 0.02,
|
||||
"max": 0.8,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "How fast the logo moves between edge hits."
|
||||
},
|
||||
{
|
||||
"id": "edgePadding",
|
||||
"label": "Edge Padding",
|
||||
"type": "float",
|
||||
"default": 0.018,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.08,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Inset distance from the frame edges."
|
||||
},
|
||||
{
|
||||
"id": "glowAmount",
|
||||
"label": "Glow",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.75,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Adds a soft colored glow around the logo."
|
||||
},
|
||||
{
|
||||
"id": "baseAlpha",
|
||||
"label": "Alpha",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.05,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall opacity of the overlay."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ float4 shadeVideo(ShaderContext context)
|
||||
float2 velocityPx = float2(
|
||||
max(20.0, bounceSpeed * minDimension * 1.00),
|
||||
max(24.0, bounceSpeed * minDimension * 0.77));
|
||||
float2 motionPx = context.time * velocityPx;
|
||||
float seed = context.startupRandom;
|
||||
float2 motionPx = (float2(context.time, context.time) + float2(seed * 32.7, seed * 40.3)) * velocityPx;
|
||||
float2 centerPx = minCenterPx + float2(
|
||||
pingPong(motionPx.x, travelPx.x),
|
||||
pingPong(motionPx.y, travelPx.y));
|
||||
@@ -38,7 +39,7 @@ float4 shadeVideo(ShaderContext context)
|
||||
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
|
||||
int totalHits = max(0, xHits + yHits);
|
||||
|
||||
float hue = frac(0.09 + float(totalHits) * 0.173);
|
||||
float hue = frac(0.09 + seed * 0.71 + float(totalHits) * 0.173);
|
||||
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
|
||||
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));
|
||||
|
||||
|
||||
115
shaders/ether/shader.json
Normal file
115
shaders/ether/shader.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"id": "ether",
|
||||
"name": "Ether",
|
||||
"description": "Raymarched ether field. Original by nimitz 2014 (twitter: @stormoid), adapted from https://www.shadertoy.com/view/MsjSW3.",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "depth",
|
||||
"label": "Depth",
|
||||
"type": "float",
|
||||
"default": 2.5,
|
||||
"min": 0.2,
|
||||
"max": 8,
|
||||
"step": 0.01,
|
||||
"description": "Raymarch depth through the ether volume."
|
||||
},
|
||||
{
|
||||
"id": "density",
|
||||
"label": "Density",
|
||||
"type": "float",
|
||||
"default": 0.7,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Density of the volumetric strands."
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts the generated effect brightness."
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"label": "Contrast",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts separation between dark and bright areas."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0.9,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the generated field in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "baseColor",
|
||||
"label": "Base Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.1,
|
||||
0.3,
|
||||
0.4,
|
||||
1
|
||||
],
|
||||
"description": "Low-energy color used in the generated field."
|
||||
},
|
||||
{
|
||||
"id": "energyColor",
|
||||
"label": "Energy Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
1,
|
||||
0.5,
|
||||
0.6,
|
||||
1
|
||||
],
|
||||
"description": "High-energy color used in the generated field."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
41
shaders/ether/shader.slang
Normal file
41
shaders/ether/shader.slang
Normal file
@@ -0,0 +1,41 @@
|
||||
float2x2 rotation2(float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return float2x2(c, -s, s, c);
|
||||
}
|
||||
|
||||
float etherMap(float3 p, float time)
|
||||
{
|
||||
p.xz = mul(rotation2(time * 0.4), p.xz);
|
||||
p.xy = mul(rotation2(time * 0.3), p.xy);
|
||||
|
||||
float3 q = p * 2.0 + time;
|
||||
float wave = sin(q.x + sin(q.z + sin(q.y))) * 0.5;
|
||||
return length(p + float3(sin(time * 0.7), sin(time * 0.7), sin(time * 0.7))) * log(length(p) + 1.0) + wave - 1.0;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 fragCoord = context.uv * resolution;
|
||||
float2 p = fragCoord / resolution.y - offset;
|
||||
float seed = context.startupRandom;
|
||||
float time = context.time * speed + seed * 41.0;
|
||||
|
||||
float3 color = float3(0.0, 0.0, 0.0);
|
||||
float d = depth;
|
||||
|
||||
for (int i = 0; i <= 5; ++i)
|
||||
{
|
||||
float3 rayPosition = float3(seed * 0.7 - 0.35, 0.23 - seed * 0.46, 5.0) + normalize(float3(p, -1.0)) * d;
|
||||
float rz = etherMap(rayPosition, time);
|
||||
float f = clamp((rz - etherMap(rayPosition + float3(0.1, 0.1, 0.1), time)) * 0.5, -0.1, 1.0);
|
||||
float3 light = baseColor.rgb + energyColor.rgb * 5.0 * f;
|
||||
color = color * light + smoothstep(2.5, 0.0, rz) * density * light;
|
||||
d += min(rz, 1.0);
|
||||
}
|
||||
|
||||
color = pow(max(color * brightness, float3(0.0, 0.0, 0.0)), float3(1.0 / max(contrast, 0.001)));
|
||||
return saturate(lerp(float4(color, 1.0), context.sourceColor, sourceMix));
|
||||
}
|
||||
@@ -2,41 +2,45 @@
|
||||
"id": "false-color",
|
||||
"name": "False Color",
|
||||
"description": "Maps luminance ranges to exposure-assist colors for camera and shader debugging.",
|
||||
"category": "Utility",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "blendAmount",
|
||||
"label": "Blend",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mix amount for the processed result."
|
||||
},
|
||||
{
|
||||
"id": "showLuma",
|
||||
"label": "Show Luma",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Shows grayscale luminance before applying false-color mapping."
|
||||
},
|
||||
{
|
||||
"id": "lift",
|
||||
"label": "Lift",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"default": 0,
|
||||
"min": -0.25,
|
||||
"max": 0.25,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Offsets luminance before false-color mapping."
|
||||
},
|
||||
{
|
||||
"id": "gain",
|
||||
"label": "Gain",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Scales luminance before false-color mapping."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
66
shaders/feedback-data-blocks/shader.json
Normal file
66
shaders/feedback-data-blocks/shader.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"id": "feedback-data-blocks",
|
||||
"name": "Feedback Data Blocks",
|
||||
"description": "Demonstrates coarse shader-local data storage by reserving eight 3x3 feedback cells for sampled colors and one hidden metadata cell for refresh state.",
|
||||
"category": "Feedback",
|
||||
"entryPoint": "storeProbeData",
|
||||
"passes": [
|
||||
{
|
||||
"id": "store",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "storeProbeData",
|
||||
"output": "dataBuffer"
|
||||
},
|
||||
{
|
||||
"id": "display",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "displayProbeData",
|
||||
"inputs": [
|
||||
"dataBuffer"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "store"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"id": "refresh",
|
||||
"label": "Refresh",
|
||||
"type": "trigger",
|
||||
"description": "Forces the stored probe colors to resample immediately."
|
||||
},
|
||||
{
|
||||
"id": "refreshSeconds",
|
||||
"label": "Refresh Seconds",
|
||||
"type": "float",
|
||||
"default": 15.0,
|
||||
"min": 1.0,
|
||||
"max": 60.0,
|
||||
"step": 0.1,
|
||||
"description": "Automatic interval for resampling all stored probe colors."
|
||||
},
|
||||
{
|
||||
"id": "overlayOpacity",
|
||||
"label": "Overlay Opacity",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the swatch overlay drawn from the stored data cells."
|
||||
},
|
||||
{
|
||||
"id": "swatchSize",
|
||||
"label": "Swatch Size",
|
||||
"type": "vec2",
|
||||
"default": [0.045, 0.055],
|
||||
"min": [0.02, 0.02],
|
||||
"max": [0.12, 0.12],
|
||||
"step": [0.001, 0.001],
|
||||
"description": "Size of the top-left preview swatches that show the stored cell values."
|
||||
}
|
||||
]
|
||||
}
|
||||
152
shaders/feedback-data-blocks/shader.slang
Normal file
152
shaders/feedback-data-blocks/shader.slang
Normal file
@@ -0,0 +1,152 @@
|
||||
static const int kProbeCount = 8;
|
||||
static const int kMetadataIndex = 8;
|
||||
|
||||
float2 probeUvForIndex(int index)
|
||||
{
|
||||
if (index == 0)
|
||||
return float2(0.18, 0.28);
|
||||
if (index == 1)
|
||||
return float2(0.39, 0.28);
|
||||
if (index == 2)
|
||||
return float2(0.61, 0.28);
|
||||
if (index == 3)
|
||||
return float2(0.82, 0.28);
|
||||
if (index == 4)
|
||||
return float2(0.18, 0.72);
|
||||
if (index == 5)
|
||||
return float2(0.39, 0.72);
|
||||
if (index == 6)
|
||||
return float2(0.61, 0.72);
|
||||
return float2(0.82, 0.72);
|
||||
}
|
||||
|
||||
float2 cellCenterPixelForIndex(int index)
|
||||
{
|
||||
return float2(1.0 + float(index) * 3.0, 1.0);
|
||||
}
|
||||
|
||||
float2 cellCenterUvForIndex(ShaderContext context, int index)
|
||||
{
|
||||
return (cellCenterPixelForIndex(index) + 0.5) / context.outputResolution;
|
||||
}
|
||||
|
||||
bool pixelIsInsideCell(float2 pixelCoord, int index)
|
||||
{
|
||||
float minX = float(index) * 3.0;
|
||||
float maxX = minX + 3.0;
|
||||
return pixelCoord.x >= minX && pixelCoord.x < maxX && pixelCoord.y >= 0.0 && pixelCoord.y < 3.0;
|
||||
}
|
||||
|
||||
float4 readStoredCell(ShaderContext context, int index)
|
||||
{
|
||||
if (context.feedbackAvailable <= 0)
|
||||
return float4(0.0, 0.0, 0.0, 0.0);
|
||||
return sampleFeedback(cellCenterUvForIndex(context, index));
|
||||
}
|
||||
|
||||
bool shouldRefreshStoredData(ShaderContext context)
|
||||
{
|
||||
if (context.feedbackAvailable <= 0)
|
||||
return true;
|
||||
|
||||
float4 metadata = readStoredCell(context, kMetadataIndex);
|
||||
float previousRefreshBucket = metadata.r;
|
||||
float previousTriggerCount = metadata.g;
|
||||
float refreshInterval = max(refreshSeconds, 0.001);
|
||||
float currentRefreshBucket = floor(context.time / refreshInterval);
|
||||
float currentTriggerCount = float(refresh);
|
||||
|
||||
return currentRefreshBucket > previousRefreshBucket + 0.5 || currentTriggerCount > previousTriggerCount + 0.5;
|
||||
}
|
||||
|
||||
float4 metadataValueForFrame(ShaderContext context, bool refreshNow)
|
||||
{
|
||||
float refreshInterval = max(refreshSeconds, 0.001);
|
||||
float currentRefreshBucket = floor(context.time / refreshInterval);
|
||||
float currentTriggerCount = float(refresh);
|
||||
|
||||
if (!refreshNow && context.feedbackAvailable > 0)
|
||||
return readStoredCell(context, kMetadataIndex);
|
||||
|
||||
return float4(currentRefreshBucket, currentTriggerCount, refreshTime, 1.0);
|
||||
}
|
||||
|
||||
float4 storedProbeValueForFrame(ShaderContext context, int index, bool refreshNow)
|
||||
{
|
||||
float3 liveColor = sampleLayerInput(probeUvForIndex(index)).rgb;
|
||||
if (refreshNow || context.feedbackAvailable <= 0)
|
||||
return float4(liveColor, 1.0);
|
||||
return readStoredCell(context, index);
|
||||
}
|
||||
|
||||
float4 storeProbeData(ShaderContext context)
|
||||
{
|
||||
// Reserve nine 3x3 texel cells along the top edge of the feedback surface:
|
||||
// eight cells for visible probe colors and one hidden metadata cell that
|
||||
// tracks the timed refresh bucket and last trigger count.
|
||||
float2 pixelCoord = floor(context.uv * context.outputResolution);
|
||||
bool refreshNow = shouldRefreshStoredData(context);
|
||||
|
||||
for (int index = 0; index < kProbeCount; ++index)
|
||||
{
|
||||
if (pixelIsInsideCell(pixelCoord, index))
|
||||
return storedProbeValueForFrame(context, index, refreshNow);
|
||||
}
|
||||
|
||||
if (pixelIsInsideCell(pixelCoord, kMetadataIndex))
|
||||
return metadataValueForFrame(context, refreshNow);
|
||||
|
||||
return float4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float rectMask(float2 uv, float2 minUv, float2 maxUv)
|
||||
{
|
||||
if (uv.x < minUv.x || uv.x > maxUv.x)
|
||||
return 0.0;
|
||||
if (uv.y < minUv.y || uv.y > maxUv.y)
|
||||
return 0.0;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float borderMask(float2 uv, float2 minUv, float2 maxUv, float thickness)
|
||||
{
|
||||
float outer = rectMask(uv, minUv, maxUv);
|
||||
float inner = rectMask(uv, minUv + thickness, maxUv - thickness);
|
||||
return saturate(outer - inner);
|
||||
}
|
||||
|
||||
float4 displayProbeData(ShaderContext context)
|
||||
{
|
||||
float3 baseColor = sampleLayerInput(context.uv).rgb;
|
||||
float3 swatchColor = baseColor;
|
||||
float swatchMask = 0.0;
|
||||
|
||||
float2 panelOrigin = float2(0.03, 0.04);
|
||||
float2 gap = float2(swatchSize.x + 0.012, swatchSize.y + 0.012);
|
||||
float borderThickness = min(swatchSize.x, swatchSize.y) * 0.08;
|
||||
|
||||
for (int index = 0; index < kProbeCount; ++index)
|
||||
{
|
||||
int column = index % 4;
|
||||
int row = index / 4;
|
||||
float2 swatchMin = panelOrigin + float2(float(column) * gap.x, float(row) * gap.y);
|
||||
float2 swatchMax = swatchMin + swatchSize;
|
||||
float3 storedColor = sampleVideo(cellCenterUvForIndex(context, index)).rgb;
|
||||
float fill = rectMask(context.uv, swatchMin, swatchMax);
|
||||
float outline = borderMask(context.uv, swatchMin, swatchMax, borderThickness);
|
||||
if (fill > 0.5)
|
||||
{
|
||||
swatchColor = storedColor;
|
||||
swatchMask = 1.0;
|
||||
}
|
||||
if (outline > 0.5)
|
||||
{
|
||||
swatchColor = float3(0.0, 0.0, 0.0);
|
||||
swatchMask = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
float opacity = saturate(overlayOpacity) * swatchMask;
|
||||
float3 displayColor = lerp(baseColor, swatchColor, opacity);
|
||||
return float4(saturate(displayColor), 1.0);
|
||||
}
|
||||
110
shaders/feedback-highlight-accumulator/shader.json
Normal file
110
shaders/feedback-highlight-accumulator/shader.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"id": "feedback-highlight-accumulator",
|
||||
"name": "Feedback Background Memory",
|
||||
"description": "Learns a persistent per-pixel background plate in shader-local feedback and compares the live frame against that evolving full-frame state.",
|
||||
"category": "Feedback",
|
||||
"entryPoint": "updateBackgroundModel",
|
||||
"passes": [
|
||||
{
|
||||
"id": "background",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "updateBackgroundModel",
|
||||
"output": "backgroundModel"
|
||||
},
|
||||
{
|
||||
"id": "display",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "displayBackgroundDifference",
|
||||
"inputs": [
|
||||
"backgroundModel"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "background"
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"id": "learnRate",
|
||||
"label": "Learn Rate",
|
||||
"type": "float",
|
||||
"default": 0.03,
|
||||
"min": 0.001,
|
||||
"max": 0.5,
|
||||
"step": 0.001,
|
||||
"description": "How quickly the stored background model adapts toward the current frame."
|
||||
},
|
||||
{
|
||||
"id": "differenceThreshold",
|
||||
"label": "Difference Threshold",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0.001,
|
||||
"max": 1.0,
|
||||
"step": 0.001,
|
||||
"description": "Minimum difference between the live frame and stored background before the overlay becomes visible."
|
||||
},
|
||||
{
|
||||
"id": "softness",
|
||||
"label": "Threshold Softness",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.001,
|
||||
"max": 0.5,
|
||||
"step": 0.001,
|
||||
"description": "Softens the transition around the difference threshold."
|
||||
},
|
||||
{
|
||||
"id": "overlayOpacity",
|
||||
"label": "Overlay Opacity",
|
||||
"type": "float",
|
||||
"default": 0.85,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the motion/difference overlay on top of the live image."
|
||||
},
|
||||
{
|
||||
"id": "backgroundMix",
|
||||
"label": "Background Mix",
|
||||
"type": "float",
|
||||
"default": 0.15,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"description": "Amount of the learned background model shown underneath the live source."
|
||||
},
|
||||
{
|
||||
"id": "overlayTint",
|
||||
"label": "Overlay Tint",
|
||||
"type": "color",
|
||||
"default": [
|
||||
1.0,
|
||||
0.45,
|
||||
0.08,
|
||||
1.0
|
||||
],
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Tint used for areas that differ from the learned background."
|
||||
}
|
||||
]
|
||||
}
|
||||
39
shaders/feedback-highlight-accumulator/shader.slang
Normal file
39
shaders/feedback-highlight-accumulator/shader.slang
Normal file
@@ -0,0 +1,39 @@
|
||||
float luminance(float3 color)
|
||||
{
|
||||
return dot(color, float3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
float4 updateBackgroundModel(ShaderContext context)
|
||||
{
|
||||
float3 liveColor = context.sourceColor.rgb;
|
||||
if (context.feedbackAvailable <= 0)
|
||||
return float4(liveColor, 1.0);
|
||||
|
||||
float3 previousBackground = sampleFeedback(context.uv).rgb;
|
||||
float rate = saturate(learnRate);
|
||||
float3 nextBackground = lerp(previousBackground, liveColor, rate);
|
||||
return float4(saturate(nextBackground), 1.0);
|
||||
}
|
||||
|
||||
float4 displayBackgroundDifference(ShaderContext context)
|
||||
{
|
||||
// In the display pass, context.sourceColor is the same-frame background
|
||||
// model produced by updateBackgroundModel().
|
||||
float3 backgroundModel = context.sourceColor.rgb;
|
||||
float3 liveColor = sampleLayerInput(context.uv).rgb;
|
||||
|
||||
float3 delta = abs(liveColor - backgroundModel);
|
||||
float difference = max(delta.r, max(delta.g, delta.b));
|
||||
float thresholdWidth = max(softness, 0.0001);
|
||||
float motionMask = smoothstep(
|
||||
differenceThreshold - thresholdWidth,
|
||||
differenceThreshold + thresholdWidth,
|
||||
difference);
|
||||
|
||||
float3 baseColor = lerp(liveColor, backgroundModel, saturate(backgroundMix));
|
||||
float3 overlayColor = overlayTint.rgb * max(luminance(liveColor), 0.15);
|
||||
float overlayAmount = motionMask * saturate(overlayOpacity) * overlayTint.a;
|
||||
float3 displayColor = lerp(baseColor, baseColor + overlayColor, overlayAmount);
|
||||
|
||||
return float4(saturate(displayColor), 1.0);
|
||||
}
|
||||
150
shaders/fisheye-equirectangular-mirror/shader.json
Normal file
150
shaders/fisheye-equirectangular-mirror/shader.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"id": "fisheye-equirectangular-mirror",
|
||||
"name": "Fisheye Equirectangular Mirror",
|
||||
"description": "Unwraps a single width-filled 16:9 fisheye lens into a 360x180 equirectangular map by mirroring the rear hemisphere into the same fisheye source.",
|
||||
"category": "Projection",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "lensFovDegrees",
|
||||
"label": "Lens FOV",
|
||||
"type": "float",
|
||||
"default": 190,
|
||||
"min": 1,
|
||||
"max": 220,
|
||||
"step": 0.1,
|
||||
"description": "Actual fisheye lens field of view in degrees."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Optical Center",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
|
||||
},
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Fisheye Radius",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0.5,
|
||||
0.8889
|
||||
],
|
||||
"min": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
||||
},
|
||||
{
|
||||
"id": "yawDegrees",
|
||||
"label": "Yaw",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the virtual view horizontally."
|
||||
},
|
||||
{
|
||||
"id": "pitchDegrees",
|
||||
"label": "Pitch",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the virtual view vertically."
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live roll rotation around the viewing axis."
|
||||
},
|
||||
{
|
||||
"id": "fisheyeModel",
|
||||
"label": "Fisheye Model",
|
||||
"type": "enum",
|
||||
"default": "equidistant",
|
||||
"options": [
|
||||
{
|
||||
"value": "equidistant",
|
||||
"label": "Equidistant"
|
||||
},
|
||||
{
|
||||
"value": "equisolid",
|
||||
"label": "Equisolid"
|
||||
},
|
||||
{
|
||||
"value": "stereographic",
|
||||
"label": "Stereographic"
|
||||
},
|
||||
{
|
||||
"value": "orthographic",
|
||||
"label": "Orthographic"
|
||||
}
|
||||
],
|
||||
"description": "Projection model used by the physical fisheye lens."
|
||||
},
|
||||
{
|
||||
"id": "edgeFill",
|
||||
"label": "Edge Fill",
|
||||
"type": "float",
|
||||
"default": 0.06,
|
||||
"min": 0,
|
||||
"max": 0.3,
|
||||
"step": 0.001,
|
||||
"description": "Extends edge samples outward to cover small missing areas."
|
||||
},
|
||||
{
|
||||
"id": "edgeBlur",
|
||||
"label": "Edge Blur",
|
||||
"type": "float",
|
||||
"default": 0.018,
|
||||
"min": 0,
|
||||
"max": 0.12,
|
||||
"step": 0.001,
|
||||
"description": "Softens the dilated edge fill."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
133
shaders/fisheye-equirectangular-mirror/shader.slang
Normal file
133
shaders/fisheye-equirectangular-mirror/shader.slang
Normal file
@@ -0,0 +1,133 @@
|
||||
static const float PI = 3.14159265358979323846;
|
||||
static const float TWO_PI = 6.28318530717958647692;
|
||||
|
||||
float radiansFromDegrees(float degrees)
|
||||
{
|
||||
return degrees * (PI / 180.0);
|
||||
}
|
||||
|
||||
float3 rotateX(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(ray.x, c * ray.y - s * ray.z, s * ray.y + c * ray.z);
|
||||
}
|
||||
|
||||
float3 rotateY(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * ray.x + s * ray.z, ray.y, -s * ray.x + c * ray.z);
|
||||
}
|
||||
|
||||
float3 rotateZ(float3 ray, float angle)
|
||||
{
|
||||
float s = sin(angle);
|
||||
float c = cos(angle);
|
||||
return float3(c * ray.x - s * ray.y, s * ray.x + c * ray.y, ray.z);
|
||||
}
|
||||
|
||||
float normalizedFisheyeRadius(float theta, float halfFov)
|
||||
{
|
||||
float safeHalfFov = max(halfFov, 0.0001);
|
||||
|
||||
// Match common fisheye projection families while keeping the selected FOV
|
||||
// normalized to the same source-image radius.
|
||||
if (fisheyeModel == 1)
|
||||
{
|
||||
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
||||
}
|
||||
else if (fisheyeModel == 2)
|
||||
{
|
||||
return tan(theta * 0.5) / max(tan(safeHalfFov * 0.5), 0.0001);
|
||||
}
|
||||
else if (fisheyeModel == 3)
|
||||
{
|
||||
return sin(theta) / max(sin(safeHalfFov), 0.0001);
|
||||
}
|
||||
|
||||
return theta / safeHalfFov;
|
||||
}
|
||||
|
||||
float3 equirectangularRay(float2 uv)
|
||||
{
|
||||
// Convert equirectangular UVs into longitude/latitude on the unit sphere.
|
||||
float longitude = (uv.x - 0.5) * TWO_PI;
|
||||
float latitude = (0.5 - uv.y) * PI;
|
||||
float latitudeCos = cos(latitude);
|
||||
|
||||
return normalize(float3(
|
||||
sin(longitude) * latitudeCos,
|
||||
sin(latitude),
|
||||
cos(longitude) * latitudeCos
|
||||
));
|
||||
}
|
||||
|
||||
float sourceUvOutsideDistance(float2 uv)
|
||||
{
|
||||
float2 lower = max(-uv, float2(0.0, 0.0));
|
||||
float2 upper = max(uv - 1.0, float2(0.0, 0.0));
|
||||
return max(max(lower.x, lower.y), max(upper.x, upper.y));
|
||||
}
|
||||
|
||||
float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
|
||||
{
|
||||
float outsideDistance = sourceUvOutsideDistance(sourceUv);
|
||||
if (outsideDistance <= 0.0)
|
||||
return sampleVideo(sourceUv);
|
||||
|
||||
float fillDistance = max(edgeFill, 0.0);
|
||||
if (outsideDistance > fillDistance)
|
||||
return outsideColor;
|
||||
|
||||
float2 clampedUv = saturate(sourceUv);
|
||||
float2 inward = clampedUv - sourceUv;
|
||||
float inwardLength = max(length(inward), 0.000001);
|
||||
inward /= inwardLength;
|
||||
|
||||
// Outside the fisheye image, sample back inward from the nearest edge so the
|
||||
// fill looks like stretched lens content instead of a hard color plate.
|
||||
float blurDistance = max(edgeBlur, 0.0);
|
||||
float4 color = sampleVideo(clampedUv) * 0.32;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.75)) * 0.20;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.20)) * 0.14;
|
||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.75)) * 0.08;
|
||||
|
||||
float edgeFade = smoothstep(fillDistance * 0.78, fillDistance, outsideDistance);
|
||||
return lerp(color, outsideColor, edgeFade);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float3 ray = equirectangularRay(context.uv);
|
||||
|
||||
ray = rotateZ(ray, radiansFromDegrees(rollDegrees));
|
||||
ray = rotateX(ray, radiansFromDegrees(-pitchDegrees));
|
||||
ray = rotateY(ray, radiansFromDegrees(yawDegrees));
|
||||
|
||||
// Mirror the rear hemisphere into the front-facing fisheye image so one
|
||||
// circular lens source fills both halves of the equirectangular output.
|
||||
ray.z = abs(ray.z);
|
||||
ray = normalize(ray);
|
||||
|
||||
float halfFov = radiansFromDegrees(clamp(lensFovDegrees, 1.0, 220.0) * 0.5);
|
||||
float theta = acos(clamp(ray.z, -1.0, 1.0));
|
||||
if (theta > halfFov)
|
||||
return outsideColor;
|
||||
|
||||
float phi = atan2(ray.y, ray.x);
|
||||
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
||||
|
||||
// Project the mirrored sphere ray back into the circular fisheye source.
|
||||
float2 sourceUv = float2(
|
||||
center.x + cos(phi) * fisheyeRadius * radius.x,
|
||||
center.y - sin(phi) * fisheyeRadius * radius.y
|
||||
);
|
||||
|
||||
float2 guard = 0.5 / max(context.inputResolution, float2(1.0, 1.0));
|
||||
if (edgeFill <= 0.0 && (sourceUv.x < -guard.x || sourceUv.x > 1.0 + guard.x || sourceUv.y < -guard.y || sourceUv.y > 1.0 + guard.y))
|
||||
return outsideColor;
|
||||
|
||||
return sampleEdgeFilledVideo(sourceUv, context);
|
||||
}
|
||||
@@ -9,91 +9,145 @@
|
||||
"id": "lensFovDegrees",
|
||||
"label": "Lens FOV",
|
||||
"type": "float",
|
||||
"default": 190.0,
|
||||
"min": 1.0,
|
||||
"max": 220.0,
|
||||
"step": 0.1
|
||||
"default": 190,
|
||||
"min": 1,
|
||||
"max": 220,
|
||||
"step": 0.1,
|
||||
"description": "Actual fisheye lens field of view in degrees."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Optical Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
|
||||
},
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Fisheye Radius",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.885],
|
||||
"min": [0.001, 0.001],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.885
|
||||
],
|
||||
"min": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeCut",
|
||||
"label": "Source Edge Cut",
|
||||
"type": "float",
|
||||
"default": 0.01,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.001,
|
||||
"description": "Cuts slightly inward from all four source-frame edges before sampling to hide empty border regions."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeFeather",
|
||||
"label": "Source Edge Feather",
|
||||
"type": "float",
|
||||
"default": 0.02,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.001,
|
||||
"description": "Softens the trimmed source edges into the outside color for easier background blending."
|
||||
},
|
||||
{
|
||||
"id": "virtualFovDegrees",
|
||||
"label": "Virtual FOV",
|
||||
"type": "float",
|
||||
"default": 75.0,
|
||||
"min": 1.0,
|
||||
"max": 175.0,
|
||||
"step": 0.1
|
||||
"default": 75,
|
||||
"min": 1,
|
||||
"max": 175,
|
||||
"step": 0.1,
|
||||
"description": "Field of view of the generated virtual camera."
|
||||
},
|
||||
{
|
||||
"id": "basePanDegrees",
|
||||
"label": "Base Pan",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Permanent horizontal alignment offset before live pan."
|
||||
},
|
||||
{
|
||||
"id": "baseTiltDegrees",
|
||||
"label": "Base Tilt",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Permanent vertical alignment offset before live tilt."
|
||||
},
|
||||
{
|
||||
"id": "baseRollDegrees",
|
||||
"label": "Base Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Permanent roll alignment offset before live roll."
|
||||
},
|
||||
{
|
||||
"id": "panDegrees",
|
||||
"label": "Pan",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live horizontal view rotation."
|
||||
},
|
||||
{
|
||||
"id": "tiltDegrees",
|
||||
"label": "Tilt",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Live vertical view rotation."
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live roll rotation around the viewing axis."
|
||||
},
|
||||
{
|
||||
"id": "fisheyeModel",
|
||||
@@ -101,11 +155,24 @@
|
||||
"type": "enum",
|
||||
"default": "equidistant",
|
||||
"options": [
|
||||
{ "value": "equidistant", "label": "Equidistant" },
|
||||
{ "value": "equisolid", "label": "Equisolid" },
|
||||
{ "value": "stereographic", "label": "Stereographic" },
|
||||
{ "value": "orthographic", "label": "Orthographic" }
|
||||
]
|
||||
{
|
||||
"value": "equidistant",
|
||||
"label": "Equidistant"
|
||||
},
|
||||
{
|
||||
"value": "equisolid",
|
||||
"label": "Equisolid"
|
||||
},
|
||||
{
|
||||
"value": "stereographic",
|
||||
"label": "Stereographic"
|
||||
},
|
||||
{
|
||||
"value": "orthographic",
|
||||
"label": "Orthographic"
|
||||
}
|
||||
],
|
||||
"description": "Projection model used by the physical fisheye lens."
|
||||
},
|
||||
{
|
||||
"id": "outputProjection",
|
||||
@@ -113,15 +180,28 @@
|
||||
"type": "enum",
|
||||
"default": "rectilinear",
|
||||
"options": [
|
||||
{ "value": "rectilinear", "label": "Rectilinear" },
|
||||
{ "value": "cylindrical", "label": "Cylindrical" }
|
||||
]
|
||||
{
|
||||
"value": "rectilinear",
|
||||
"label": "Rectilinear"
|
||||
},
|
||||
{
|
||||
"value": "cylindrical",
|
||||
"label": "Cylindrical"
|
||||
}
|
||||
],
|
||||
"description": "Chooses rectilinear perspective or cylindrical reprojection."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
||||
{
|
||||
float safeHalfFov = max(halfFov, 0.0001);
|
||||
|
||||
// Different fisheye lenses map angle to image radius differently. Normalize
|
||||
// each model by the selected half-FOV so the outer lens edge stays at 1.0.
|
||||
if (fisheyeModel == 1)
|
||||
{
|
||||
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
||||
@@ -59,6 +61,20 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
||||
return theta / safeHalfFov;
|
||||
}
|
||||
|
||||
float sourceUvRectMask(float2 uv, float2 inputResolution)
|
||||
{
|
||||
float2 pixel = 1.0 / max(inputResolution, float2(1.0, 1.0));
|
||||
float cut = max(sourceEdgeCut, 0.0);
|
||||
float feather = max(sourceEdgeFeather, 0.0);
|
||||
float2 featherSize = max(float2(feather, feather), pixel * 0.5);
|
||||
|
||||
float left = smoothstep(cut, cut + featherSize.x, uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - cut - featherSize.x, 1.0 - cut, uv.x);
|
||||
float top = smoothstep(cut, cut + featherSize.y, uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - cut - featherSize.y, 1.0 - cut, uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
|
||||
@@ -67,6 +83,8 @@ float4 shadeVideo(ShaderContext context)
|
||||
float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0));
|
||||
float tanHalfFov = tan(virtualFov * 0.5);
|
||||
|
||||
// Build a virtual output-camera ray, then rotate it into the fisheye lens
|
||||
// coordinate system before asking where that ray lands on the source image.
|
||||
float3 ray = outputProjection == 1
|
||||
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
|
||||
: buildRectilinearRay(screen, outputAspect, tanHalfFov);
|
||||
@@ -86,6 +104,7 @@ float4 shadeVideo(ShaderContext context)
|
||||
float phi = atan2(ray.y, ray.x);
|
||||
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
||||
|
||||
// Polar lens coordinates become UVs inside the circular fisheye image.
|
||||
float2 sourceUv = float2(
|
||||
center.x + cos(phi) * fisheyeRadius * radius.x,
|
||||
center.y - sin(phi) * fisheyeRadius * radius.y
|
||||
@@ -94,5 +113,7 @@ float4 shadeVideo(ShaderContext context)
|
||||
if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0)
|
||||
return outsideColor;
|
||||
|
||||
return sampleVideo(sourceUv);
|
||||
float sourceMask = sourceUvRectMask(sourceUv, context.inputResolution);
|
||||
float4 sourceColor = sampleVideo(sourceUv);
|
||||
return saturate(lerp(outsideColor, sourceColor, sourceMask));
|
||||
}
|
||||
|
||||
@@ -1,36 +1,59 @@
|
||||
{
|
||||
"id": "gaussian-blur",
|
||||
"name": "Gaussian Blur",
|
||||
"description": "Applies a simple Gaussian-style blur to the decoded video input.",
|
||||
"category": "Built-in",
|
||||
"description": "Applies a separable two-pass Gaussian-style blur to the decoded video input.",
|
||||
"category": "Transform",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "horizontal",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "blurHorizontal",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "blurHorizontal"
|
||||
},
|
||||
{
|
||||
"id": "vertical",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "blurVertical",
|
||||
"inputs": [
|
||||
"blurHorizontal"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Radius",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 8.0,
|
||||
"step": 0.1
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 0.1,
|
||||
"description": "Blur radius in pixels for each separable pass."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends between the original and blurred result."
|
||||
},
|
||||
{
|
||||
"id": "samples",
|
||||
"label": "Samples",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 25.0,
|
||||
"step": 1.0
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 25,
|
||||
"step": 1,
|
||||
"description": "Number of taps per direction; higher values cost more GPU time."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float4 gaussianBlurDirection(ShaderContext context, float2 direction)
|
||||
{
|
||||
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
|
||||
float blurRadius = max(radius, 0.0);
|
||||
float2 sampleStep = texel * blurRadius;
|
||||
float blurRadius = max(radius, 0.0) * saturate(strength);
|
||||
float2 sampleStep = texel * blurRadius * direction;
|
||||
int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5);
|
||||
|
||||
float4 center = sampleVideo(context.uv);
|
||||
float4 blur = float4(0.0, 0.0, 0.0, 0.0);
|
||||
float totalWeight = 0.0;
|
||||
|
||||
for (int y = -sampleRadius; y <= sampleRadius; ++y)
|
||||
for (int x = -sampleRadius; x <= sampleRadius; ++x)
|
||||
{
|
||||
for (int x = -sampleRadius; x <= sampleRadius; ++x)
|
||||
{
|
||||
float distanceSquared = float(x * x + y * y);
|
||||
float sigma = max(float(sampleRadius) * 0.5, 0.5);
|
||||
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
|
||||
float2 offset = float2(float(x), float(y)) * sampleStep;
|
||||
blur += sampleVideo(context.uv + offset) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
float distanceSquared = float(x * x);
|
||||
float sigma = max(float(sampleRadius) * 0.5, 0.5);
|
||||
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
|
||||
float2 offset = float(x) * sampleStep;
|
||||
blur += sampleVideo(context.uv + offset) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (sampleRadius == 0)
|
||||
@@ -29,7 +26,20 @@ float4 shadeVideo(ShaderContext context)
|
||||
}
|
||||
|
||||
blur /= max(totalWeight, 0.0001);
|
||||
|
||||
float mixValue = saturate(strength);
|
||||
return lerp(center, blur, mixValue);
|
||||
return blur;
|
||||
}
|
||||
|
||||
float4 blurHorizontal(ShaderContext context)
|
||||
{
|
||||
return gaussianBlurDirection(context, float2(1.0, 0.0));
|
||||
}
|
||||
|
||||
float4 blurVertical(ShaderContext context)
|
||||
{
|
||||
return gaussianBlurDirection(context, float2(0.0, 1.0));
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return blurVertical(context);
|
||||
}
|
||||
|
||||
@@ -1,90 +1,347 @@
|
||||
{
|
||||
"id": "greenscreen-key",
|
||||
"name": "Greenscreen Key",
|
||||
"description": "Keys out a green screen background and outputs transparent alpha for compositing.",
|
||||
"category": "Built-in",
|
||||
"description": "Production-style green/blue screen keyer with matte refinement, despill, edge treatment, and debug views.",
|
||||
"category": "Keying",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "rawMatte",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "buildRawMatte",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "rawMatte"
|
||||
},
|
||||
{
|
||||
"id": "refinedMatte",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "refineMatte",
|
||||
"inputs": [
|
||||
"rawMatte"
|
||||
],
|
||||
"output": "refinedMatte"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "applyKey",
|
||||
"inputs": [
|
||||
"refinedMatte"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "screenColor",
|
||||
"label": "Screen Color",
|
||||
"type": "color",
|
||||
"default": [0.15, 0.85, 0.2, 1.0],
|
||||
"min": [0.0, 0.0, 0.0, 0.0],
|
||||
"max": [1.0, 1.0, 1.0, 1.0],
|
||||
"step": [0.01, 0.01, 0.01, 0.01]
|
||||
"default": [
|
||||
0.15,
|
||||
0.85,
|
||||
0.2,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Target screen color to remove; use green or blue depending on the backdrop."
|
||||
},
|
||||
{
|
||||
"id": "threshold",
|
||||
"label": "Threshold",
|
||||
"label": "Screen Gain",
|
||||
"type": "float",
|
||||
"default": 0.24,
|
||||
"min": 0.01,
|
||||
"max": 0.8,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Higher values keep more foreground; lower values remove more screen."
|
||||
},
|
||||
{
|
||||
"id": "softness",
|
||||
"label": "Softness",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"default": 0.16,
|
||||
"min": 0.001,
|
||||
"max": 0.5,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Feathers the transition between foreground and keyed screen."
|
||||
},
|
||||
{
|
||||
"id": "edgeSoftness",
|
||||
"label": "Edge Softness",
|
||||
"id": "screenBalance",
|
||||
"label": "Screen Balance",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.0,
|
||||
"max": 0.4,
|
||||
"step": 0.005
|
||||
"default": 0.5,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Balances chroma-distance keying against color-direction keying."
|
||||
},
|
||||
{
|
||||
"id": "screenPreBlur",
|
||||
"label": "Screen PreBlur",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 0.1,
|
||||
"description": "Blurs source color before matte generation to reduce noisy edges."
|
||||
},
|
||||
{
|
||||
"id": "erodeDilate",
|
||||
"label": "Erode/Dilate",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"default": 0,
|
||||
"min": -0.3,
|
||||
"max": 0.3,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Negative erodes the matte; positive expands it."
|
||||
},
|
||||
{
|
||||
"id": "matteBlur",
|
||||
"label": "Matte Blur",
|
||||
"type": "float",
|
||||
"default": 1.25,
|
||||
"min": 0,
|
||||
"max": 6,
|
||||
"step": 0.1,
|
||||
"description": "Softens the generated matte after keying."
|
||||
},
|
||||
{
|
||||
"id": "matteGamma",
|
||||
"label": "Matte Gamma",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Shapes midtone opacity in the matte."
|
||||
},
|
||||
{
|
||||
"id": "matteContrast",
|
||||
"label": "Matte Contrast",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Increases or reduces matte separation around 50 percent alpha."
|
||||
},
|
||||
{
|
||||
"id": "blackCleanup",
|
||||
"label": "Black Cleanup",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Pushes semi-transparent dark matte areas toward transparent."
|
||||
},
|
||||
{
|
||||
"id": "whiteCleanup",
|
||||
"label": "White Cleanup",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Pushes semi-transparent light matte areas toward opaque."
|
||||
},
|
||||
{
|
||||
"id": "despill",
|
||||
"label": "Despill",
|
||||
"type": "float",
|
||||
"default": 0.45,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Removes screen-colored contamination from foreground edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeBoost",
|
||||
"label": "Edge Boost",
|
||||
"id": "despillBias",
|
||||
"label": "Despill Bias",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": -0.2,
|
||||
"max": 0.3,
|
||||
"step": 0.005
|
||||
"default": 0,
|
||||
"min": -0.5,
|
||||
"max": 0.5,
|
||||
"step": 0.005,
|
||||
"description": "Offsets spill detection when foreground colors are close to the screen color."
|
||||
},
|
||||
{
|
||||
"id": "spillTint",
|
||||
"label": "Spill Tint",
|
||||
"type": "color",
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Tint used when neutralizing spill."
|
||||
},
|
||||
{
|
||||
"id": "edgeRecover",
|
||||
"label": "Edge Recover",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Adds color recovery along semi-transparent matte edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeColor",
|
||||
"label": "Edge Color",
|
||||
"type": "color",
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Tint applied to recovered edge detail."
|
||||
},
|
||||
{
|
||||
"id": "clipBlack",
|
||||
"label": "Clip Black",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Matte values below this become transparent."
|
||||
},
|
||||
{
|
||||
"id": "clipWhite",
|
||||
"label": "Clip White",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.5,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Matte values above this become opaque."
|
||||
},
|
||||
{
|
||||
"id": "cropLeft",
|
||||
"label": "Crop Left",
|
||||
"description": "Trims the final matte from the left edge as a fraction of frame width.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropRight",
|
||||
"label": "Crop Right",
|
||||
"description": "Trims the final matte from the right edge as a fraction of frame width.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropTop",
|
||||
"label": "Crop Top",
|
||||
"description": "Trims the final matte from the top edge as a fraction of frame height.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropBottom",
|
||||
"label": "Crop Bottom",
|
||||
"description": "Trims the final matte from the bottom edge as a fraction of frame height.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "viewMode",
|
||||
"label": "View",
|
||||
"type": "enum",
|
||||
"default": "composite",
|
||||
"options": [
|
||||
{
|
||||
"value": "composite",
|
||||
"label": "Composite"
|
||||
},
|
||||
{
|
||||
"value": "matte",
|
||||
"label": "Matte"
|
||||
},
|
||||
{
|
||||
"value": "spill",
|
||||
"label": "Spill"
|
||||
},
|
||||
{
|
||||
"value": "despill",
|
||||
"label": "Despill"
|
||||
},
|
||||
{
|
||||
"value": "status",
|
||||
"label": "Status"
|
||||
}
|
||||
],
|
||||
"description": "Debug output mode for inspecting matte, spill, and despill stages."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,32 +9,216 @@ float luma709(float3 color)
|
||||
return dot(color, float3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float2 chroma709(float3 color)
|
||||
{
|
||||
float y = luma709(color);
|
||||
return float2((color.b - y) * 0.5647, (color.r - y) * 0.7132);
|
||||
}
|
||||
|
||||
float3 matteSampleColor(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float blur = max(screenPreBlur, 0.0);
|
||||
float3 center = saturate(sampleVideo(saturate(uv)).rgb);
|
||||
if (blur <= 0.0001)
|
||||
return center;
|
||||
|
||||
// Pre-blur only the color used for screen comparison; the final image keeps
|
||||
// its original detail and alpha is refined in a later pass.
|
||||
float2 radius = pixel * blur;
|
||||
float3 color = center * 0.36;
|
||||
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv - float2(radius.x, 0.0))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv + float2(0.0, radius.y))).rgb) * 0.16;
|
||||
color += saturate(sampleVideo(saturate(uv - float2(0.0, radius.y))).rgb) * 0.16;
|
||||
return color;
|
||||
}
|
||||
|
||||
float keyDistanceAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float3 color = matteSampleColor(uv, context);
|
||||
float3 keyColor = saturate(screenColor.rgb);
|
||||
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
|
||||
// Direction distance is less sensitive to brightness, while chroma distance
|
||||
// follows broadcast-style color difference; screenBalance blends the two.
|
||||
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
|
||||
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
|
||||
}
|
||||
|
||||
float rawAlphaAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float keyDistance = keyDistanceAt(uv, context);
|
||||
float matteCenter = threshold + erodeDilate;
|
||||
float matteFeather = max(softness, 0.0005);
|
||||
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, keyDistance);
|
||||
return saturate(alpha);
|
||||
}
|
||||
|
||||
float matteAlphaAt(float2 uv)
|
||||
{
|
||||
return saturate(sampleVideo(saturate(uv)).a);
|
||||
}
|
||||
|
||||
float refinedAlphaFromMatte(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float blur = max(matteBlur, 0.0);
|
||||
float aaRadius = max(blur, 0.65);
|
||||
float centerAlpha = matteAlphaAt(uv);
|
||||
float alpha = centerAlpha * 0.30;
|
||||
|
||||
if (aaRadius > 0.0001)
|
||||
{
|
||||
// A small fixed kernel smooths edges and collects min/max alpha for
|
||||
// black/white cleanup without needing dynamic loops or arrays.
|
||||
float2 radius = pixel * aaRadius;
|
||||
float2 halfRadius = radius * 0.5;
|
||||
float alphaMin = centerAlpha;
|
||||
float alphaMax = centerAlpha;
|
||||
float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv - radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
|
||||
alpha = lerp(alpha, alphaMin, saturate(blackCleanup));
|
||||
alpha = lerp(alpha, alphaMax, saturate(whiteCleanup));
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha = centerAlpha;
|
||||
}
|
||||
|
||||
// Final matte shaping happens after blur/cleanup so clip and contrast affect
|
||||
// the refined edge rather than the raw screen-distance estimate.
|
||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
|
||||
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
|
||||
return saturate(alpha);
|
||||
}
|
||||
|
||||
float spillAmountForColor(float3 color)
|
||||
{
|
||||
float3 keyColor = saturate(screenColor.rgb);
|
||||
// Measure spill as color energy aligned with the screen color minus the
|
||||
// strongest opposing channel, leaving neutral highlights mostly intact.
|
||||
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
|
||||
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
|
||||
return saturate(keyComponent - opposingComponent + despillBias);
|
||||
}
|
||||
|
||||
float3 despillColor(float3 color, float alpha)
|
||||
{
|
||||
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
|
||||
float spill = spillAmountForColor(color) * despill * (1.0 - alpha * 0.35);
|
||||
float neutral = luma709(color);
|
||||
float3 neutralized = color - keyColor * spill;
|
||||
neutralized = max(neutralized, float3(0.0, 0.0, 0.0));
|
||||
neutralized = lerp(neutralized, float3(neutral, neutral, neutral), spill * 0.18);
|
||||
neutralized = lerp(neutralized, neutralized * saturate(spillTint.rgb), saturate(spill));
|
||||
return saturate(neutralized);
|
||||
}
|
||||
|
||||
float cropMaskAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x);
|
||||
float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float4 buildRawMatte(ShaderContext context)
|
||||
{
|
||||
float4 src = context.sourceColor;
|
||||
float3 color = saturate(src.rgb);
|
||||
float alpha = rawAlphaAt(context.uv, context);
|
||||
return float4(color, alpha);
|
||||
}
|
||||
|
||||
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
|
||||
float3 sampleColor = safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001)));
|
||||
float4 refineMatte(ShaderContext context)
|
||||
{
|
||||
float4 raw = sampleVideo(context.uv);
|
||||
float alpha = refinedAlphaFromMatte(context.uv, context);
|
||||
return float4(saturate(raw.rgb), alpha);
|
||||
}
|
||||
|
||||
float chromaDistance = length(sampleColor - keyColor);
|
||||
float matteCenter = threshold - erodeDilate;
|
||||
float matteFeather = max(softness + edgeSoftness, 0.0005);
|
||||
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, chromaDistance);
|
||||
float4 applyKey(ShaderContext context)
|
||||
{
|
||||
float4 keyed = sampleVideo(context.uv);
|
||||
float3 color = saturate(keyed.rgb);
|
||||
float alpha = saturate(keyed.a);
|
||||
float spill = spillAmountForColor(color);
|
||||
float3 despilled = despillColor(color, alpha);
|
||||
float cropMask = cropMaskAt(context.uv, context);
|
||||
alpha *= cropMask;
|
||||
|
||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||
alpha = saturate(alpha + edgeBoost);
|
||||
// Edge recovery is strongest around 50% alpha, where fringing usually lives,
|
||||
// and fades away for solid foreground/background pixels.
|
||||
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
|
||||
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
|
||||
|
||||
float greenExcess = max(0.0, color.g - max(color.r, color.b));
|
||||
float spillReduction = greenExcess * despill;
|
||||
|
||||
float3 despilled = color;
|
||||
despilled.g = max(0.0, despilled.g - spillReduction);
|
||||
|
||||
float neutral = luma709(despilled);
|
||||
despilled.rb += spillReduction * 0.25;
|
||||
despilled = lerp(float3(neutral, neutral, neutral), despilled, 0.92);
|
||||
if (viewMode == 1)
|
||||
return float4(alpha, alpha, alpha, 1.0);
|
||||
if (viewMode == 2)
|
||||
return float4(spill, spill * 0.55, 0.0, 1.0);
|
||||
if (viewMode == 3)
|
||||
return float4(despilled, 1.0);
|
||||
if (viewMode == 4)
|
||||
{
|
||||
float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
|
||||
return float4(rawAlpha, alpha, spill, 1.0);
|
||||
}
|
||||
|
||||
float3 premultiplied = saturate(despilled) * alpha;
|
||||
return float4(premultiplied, alpha);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return applyKey(context);
|
||||
}
|
||||
|
||||
59
shaders/happy-accident/shader.json
Normal file
59
shaders/happy-accident/shader.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"id": "happy-accident",
|
||||
"name": "Happy Accident",
|
||||
"description": "Raymarched generative line field. CC0 original 'Clearly a bug' adapted from https://www.shadertoy.com/view/33cGDj.",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Overall size of the effect in the frame."
|
||||
},
|
||||
{
|
||||
"id": "raySteps",
|
||||
"label": "Ray Steps",
|
||||
"type": "float",
|
||||
"default": 77,
|
||||
"min": 8,
|
||||
"max": 77,
|
||||
"step": 1,
|
||||
"description": "Raymarch iteration count; higher values increase detail and GPU cost."
|
||||
},
|
||||
{
|
||||
"id": "intensity",
|
||||
"label": "Intensity",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Overall brightness of the accumulated raymarched light."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
68
shaders/happy-accident/shader.slang
Normal file
68
shaders/happy-accident/shader.slang
Normal file
@@ -0,0 +1,68 @@
|
||||
float happyNoise(float2 p)
|
||||
{
|
||||
return frac(dot(p, sin(p))) - 0.5;
|
||||
}
|
||||
|
||||
float2x2 rotateAroundZ(float angle)
|
||||
{
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return float2x2(c, s, -s, c);
|
||||
}
|
||||
|
||||
float2x2 happyAccidentMatrix(float3 originalPosition, float timeCos)
|
||||
{
|
||||
return float2x2(
|
||||
cos(originalPosition.x),
|
||||
sin(originalPosition.y),
|
||||
-sin(originalPosition.z),
|
||||
timeCos);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 fragCoord = context.uv * resolution;
|
||||
float2 normalizedCoord = (fragCoord - 0.5 * resolution) / resolution.y / max(scale, 0.001);
|
||||
float seed = context.startupRandom;
|
||||
float time = context.time * speed + seed * 53.0;
|
||||
float timeCos = cos(0.1 * time);
|
||||
float3 direction = normalize(float3(normalizedCoord, 1.0));
|
||||
float3 origin = float3(seed * 0.4 - 0.2, 0.2 - seed * 0.4, time);
|
||||
|
||||
float edgeAmount = saturate(length(normalizedCoord) * 0.55);
|
||||
float z = happyNoise(fragCoord + seed * resolution.yx) * lerp(0.22, 0.08, edgeAmount);
|
||||
float distanceToSurface = 0.0;
|
||||
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
|
||||
float clampedSteps = clamp(raySteps, 1.0, 77.0);
|
||||
|
||||
// Ray-march a folded procedural field. distanceToSurface advances the ray,
|
||||
// while inverse-distance accumulation creates the glowing filaments.
|
||||
for (int i = 0; i < 77; ++i)
|
||||
{
|
||||
if (float(i) >= clampedSteps)
|
||||
break;
|
||||
|
||||
z += 0.6 * distanceToSurface;
|
||||
|
||||
float3 position = origin + z * direction;
|
||||
float3 originalPosition = position;
|
||||
|
||||
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
|
||||
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
|
||||
|
||||
// Color comes from pre-fold space so the palette varies smoothly even as
|
||||
// the geometry folds into repeated cells.
|
||||
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
|
||||
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
|
||||
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
|
||||
|
||||
position = abs(frac(position) - 0.5);
|
||||
// Distance to a tiny box/cross primitive inside each repeated cell.
|
||||
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
|
||||
accumulated += palette.w * palette / distanceToSurface;
|
||||
}
|
||||
|
||||
float4 color = float4(tanh((accumulated.rgb * intensity) / 20000.0), 1.0);
|
||||
return saturate(lerp(color, context.sourceColor, sourceMix));
|
||||
}
|
||||
67
shaders/lift-gamma-gain/shader.json
Normal file
67
shaders/lift-gamma-gain/shader.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"id": "lift-gamma-gain",
|
||||
"name": "Lift Gamma Gain",
|
||||
"description": "Basic color grading controls for shadows, midtones, highlights, and overall RGB offset.",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "lift",
|
||||
"label": "Lift",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Adds color mostly to shadows."
|
||||
},
|
||||
{
|
||||
"id": "gamma",
|
||||
"label": "Gamma",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Balances midtone color response."
|
||||
},
|
||||
{
|
||||
"id": "gain",
|
||||
"label": "Gain",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Scales highlights and overall channel intensity."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "color",
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Adds a uniform color offset after lift/gamma/gain."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the grade with the original image."
|
||||
}
|
||||
]
|
||||
}
|
||||
20
shaders/lift-gamma-gain/shader.slang
Normal file
20
shaders/lift-gamma-gain/shader.slang
Normal file
@@ -0,0 +1,20 @@
|
||||
float3 applyLiftGammaGainOffset(float3 color)
|
||||
{
|
||||
float3 liftAdjust = (lift.rgb - 0.5) * 0.5;
|
||||
float3 offsetAdjust = (offset.rgb - 0.5) * 0.5;
|
||||
float3 gammaAdjust = exp2((gamma.rgb - 0.5) * 2.0);
|
||||
float3 gainAdjust = exp2((gain.rgb - 0.5) * 2.0);
|
||||
|
||||
float3 lifted = color + liftAdjust;
|
||||
float3 gained = lifted * gainAdjust;
|
||||
float3 corrected = pow(saturate(gained), 1.0 / max(gammaAdjust, float3(0.001)));
|
||||
return corrected + offsetAdjust;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 source = context.sourceColor;
|
||||
float3 graded = applyLiftGammaGainOffset(source.rgb);
|
||||
source.rgb = lerp(source.rgb, graded, strength);
|
||||
return saturate(source);
|
||||
}
|
||||
62
shaders/lut-apply/shader.json
Normal file
62
shaders/lut-apply/shader.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": "lut-apply",
|
||||
"name": "3D LUT Apply",
|
||||
"description": "Applies the packaged 33-point .cube LUT to the incoming video using tetrahedral interpolation and optional output dithering.",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"textures": [
|
||||
{
|
||||
"id": "lutTexture",
|
||||
"path": "test-lut.cube"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "lutStrength",
|
||||
"label": "LUT Strength",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends between the original image and the LUT result."
|
||||
},
|
||||
{
|
||||
"id": "preExposure",
|
||||
"label": "Pre Exposure",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": -4,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Exposure offset applied before the LUT lookup."
|
||||
},
|
||||
{
|
||||
"id": "postContrast",
|
||||
"label": "Post Contrast",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Contrast adjustment applied after the LUT lookup."
|
||||
},
|
||||
{
|
||||
"id": "ditherAmount",
|
||||
"label": "Output Dither",
|
||||
"type": "float",
|
||||
"default": 0.5,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Adds subtle output dither to reduce visible banding."
|
||||
},
|
||||
{
|
||||
"id": "clampInput",
|
||||
"label": "Clamp Input",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Clamps colors to 0-1 before the LUT lookup."
|
||||
}
|
||||
]
|
||||
}
|
||||
84
shaders/lut-apply/shader.slang
Normal file
84
shaders/lut-apply/shader.slang
Normal file
@@ -0,0 +1,84 @@
|
||||
static const float LUT_SIZE = 33.0;
|
||||
static const float LUT_LAST_INDEX = 32.0;
|
||||
|
||||
float3 sampleLutCell(float3 index)
|
||||
{
|
||||
float r = floor(index.r + 0.5);
|
||||
float g = floor(index.g + 0.5);
|
||||
float b = floor(index.b + 0.5);
|
||||
|
||||
// The 33^3 cube is packed as blue slices laid horizontally, with red across
|
||||
// each slice and green down the atlas.
|
||||
float atlasWidth = LUT_SIZE * LUT_SIZE;
|
||||
float2 lutUv;
|
||||
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
|
||||
lutUv.y = (g + 0.5) / LUT_SIZE;
|
||||
return lutTexture.Sample(lutUv).rgb;
|
||||
}
|
||||
|
||||
float3 applyLut33(float3 color)
|
||||
{
|
||||
float3 lutCoord = saturate(color) * LUT_LAST_INDEX;
|
||||
float3 baseIndex = floor(lutCoord);
|
||||
float3 nextIndex = min(baseIndex + 1.0, LUT_LAST_INDEX);
|
||||
float3 blend = lutCoord - baseIndex;
|
||||
|
||||
float3 c000 = sampleLutCell(float3(baseIndex.r, baseIndex.g, baseIndex.b));
|
||||
float3 c100 = sampleLutCell(float3(nextIndex.r, baseIndex.g, baseIndex.b));
|
||||
float3 c010 = sampleLutCell(float3(baseIndex.r, nextIndex.g, baseIndex.b));
|
||||
float3 c110 = sampleLutCell(float3(nextIndex.r, nextIndex.g, baseIndex.b));
|
||||
float3 c001 = sampleLutCell(float3(baseIndex.r, baseIndex.g, nextIndex.b));
|
||||
float3 c101 = sampleLutCell(float3(nextIndex.r, baseIndex.g, nextIndex.b));
|
||||
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
|
||||
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
|
||||
|
||||
// Tetrahedral interpolation chooses one of six paths through the cube.
|
||||
// This avoids the muddy diagonals that simple trilinear LUT sampling can
|
||||
// introduce for strong grades.
|
||||
if (blend.r > blend.g)
|
||||
{
|
||||
if (blend.g > blend.b)
|
||||
return c000 + blend.r * (c100 - c000) + blend.g * (c110 - c100) + blend.b * (c111 - c110);
|
||||
if (blend.r > blend.b)
|
||||
return c000 + blend.r * (c100 - c000) + blend.b * (c101 - c100) + blend.g * (c111 - c101);
|
||||
return c000 + blend.b * (c001 - c000) + blend.r * (c101 - c001) + blend.g * (c111 - c101);
|
||||
}
|
||||
|
||||
if (blend.b > blend.g)
|
||||
return c000 + blend.b * (c001 - c000) + blend.g * (c011 - c001) + blend.r * (c111 - c011);
|
||||
if (blend.b > blend.r)
|
||||
return c000 + blend.g * (c010 - c000) + blend.b * (c011 - c010) + blend.r * (c111 - c011);
|
||||
return c000 + blend.g * (c010 - c000) + blend.r * (c110 - c010) + blend.b * (c111 - c110);
|
||||
}
|
||||
|
||||
float hash12(float2 value)
|
||||
{
|
||||
float3 p = frac(float3(value.xyx) * 0.1031);
|
||||
p += dot(p, p.yzx + 33.33);
|
||||
return frac((p.x + p.y) * p.z);
|
||||
}
|
||||
|
||||
float3 outputDither(float2 pixel)
|
||||
{
|
||||
// Subtract paired hashes to center the dither around zero, then scale to
|
||||
// roughly one 8-bit code value.
|
||||
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
|
||||
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
|
||||
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));
|
||||
return float3(r, g, b) / 255.0;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 source = context.sourceColor;
|
||||
float3 inputColor = source.rgb * pow(2.0, preExposure);
|
||||
if (clampInput)
|
||||
inputColor = saturate(inputColor);
|
||||
|
||||
float3 lutColor = applyLut33(inputColor);
|
||||
float3 graded = lerp(inputColor, lutColor, lutStrength);
|
||||
graded = (graded - 0.5) * postContrast + 0.5;
|
||||
graded += outputDither(context.uv * context.outputResolution) * ditherAmount;
|
||||
|
||||
return float4(saturate(graded), source.a);
|
||||
}
|
||||
35940
shaders/lut-apply/test-lut.cube
Normal file
35940
shaders/lut-apply/test-lut.cube
Normal file
File diff suppressed because it is too large
Load Diff
49
shaders/multipass-test/shader.json
Normal file
49
shaders/multipass-test/shader.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"id": "multipass-test",
|
||||
"name": "Multipass Test",
|
||||
"description": "Diagnostic two-pass shader that generates a mask in pass one, then samples that named intermediate in pass two.",
|
||||
"category": "Utility",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "mask",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "buildMask",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "generatedMask"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "applyMask",
|
||||
"inputs": [
|
||||
"generatedMask"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "intensity",
|
||||
"label": "Intensity",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the second-pass diagnostic overlay."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 10,
|
||||
"min": 2,
|
||||
"max": 32,
|
||||
"step": 1,
|
||||
"description": "Size of the generated test pattern."
|
||||
}
|
||||
]
|
||||
}
|
||||
37
shaders/multipass-test/shader.slang
Normal file
37
shaders/multipass-test/shader.slang
Normal file
@@ -0,0 +1,37 @@
|
||||
float ringMask(float2 uv)
|
||||
{
|
||||
float2 centered = uv * 2.0 - 1.0;
|
||||
float radius = length(centered);
|
||||
float ring = 1.0 - smoothstep(0.015, 0.035, abs(radius - 0.55));
|
||||
float cross = 1.0 - smoothstep(0.006, 0.018, min(abs(centered.x), abs(centered.y)));
|
||||
return saturate(max(ring, cross));
|
||||
}
|
||||
|
||||
float gridMask(float2 uv)
|
||||
{
|
||||
float2 cell = abs(frac(uv * max(scale, 1.0)) - 0.5);
|
||||
float line = 1.0 - smoothstep(0.455, 0.495, max(cell.x, cell.y));
|
||||
return saturate(line * 0.55);
|
||||
}
|
||||
|
||||
float4 buildMask(ShaderContext context)
|
||||
{
|
||||
float mask = saturate(max(ringMask(context.uv), gridMask(context.uv)));
|
||||
return float4(context.sourceColor.rgb, mask);
|
||||
}
|
||||
|
||||
float4 applyMask(ShaderContext context)
|
||||
{
|
||||
float4 generated = sampleVideo(context.uv);
|
||||
float mask = generated.a;
|
||||
float checker = step(0.5, frac((context.uv.x + context.uv.y) * max(scale, 1.0)));
|
||||
float3 testColor = lerp(float3(0.0, 0.75, 1.0), float3(1.0, 0.1, 0.85), checker);
|
||||
float3 base = generated.rgb;
|
||||
float3 color = lerp(base, testColor, mask * saturate(intensity));
|
||||
return float4(color, 1.0);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return applyMask(context);
|
||||
}
|
||||
@@ -2,32 +2,52 @@
|
||||
"id": "pixelate",
|
||||
"name": "Pixelate",
|
||||
"description": "Reduces the effective X and Y pixel count independently to create a low-resolution blocky image.",
|
||||
"category": "Utility",
|
||||
"category": "Transform",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "pixelCount",
|
||||
"label": "Pixel Count",
|
||||
"type": "vec2",
|
||||
"default": [96.0, 54.0],
|
||||
"min": [2.0, 2.0],
|
||||
"max": [1920.0, 1080.0],
|
||||
"step": [1.0, 1.0]
|
||||
"default": [
|
||||
96,
|
||||
54
|
||||
],
|
||||
"min": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"max": [
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"step": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Number of pixel blocks across X and Y."
|
||||
},
|
||||
{
|
||||
"id": "gridAmount",
|
||||
"label": "Grid",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Visibility of the block grid lines."
|
||||
},
|
||||
{
|
||||
"id": "gridColor",
|
||||
"label": "Grid Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used for the pixel grid."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,35 +2,61 @@
|
||||
"id": "safe-area-guides",
|
||||
"name": "Safe Area Guides",
|
||||
"description": "Overlays broadcast action/title safe guides plus optional center marks and aspect matte.",
|
||||
"category": "Utility",
|
||||
"category": "Scopes & Guides",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{ "id": "showActionSafe", "label": "Action Safe", "type": "bool", "default": true },
|
||||
{ "id": "showTitleSafe", "label": "Title Safe", "type": "bool", "default": true },
|
||||
{ "id": "showCenter", "label": "Center Marks", "type": "bool", "default": true },
|
||||
{
|
||||
"id": "showActionSafe",
|
||||
"label": "Action Safe",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows the broadcast action-safe rectangle."
|
||||
},
|
||||
{
|
||||
"id": "showTitleSafe",
|
||||
"label": "Title Safe",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows the broadcast title-safe rectangle."
|
||||
},
|
||||
{
|
||||
"id": "showCenter",
|
||||
"label": "Center Marks",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows center marks for alignment."
|
||||
},
|
||||
{
|
||||
"id": "lineColor",
|
||||
"label": "Line Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Color used for guide lines and marks."
|
||||
},
|
||||
{
|
||||
"id": "lineOpacity",
|
||||
"label": "Line Opacity",
|
||||
"type": "float",
|
||||
"default": 0.65,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the guide lines."
|
||||
},
|
||||
{
|
||||
"id": "lineThicknessPixels",
|
||||
"label": "Line Thickness",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"default": 2,
|
||||
"min": 0.5,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Guide line width in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "aspectMode",
|
||||
@@ -38,20 +64,34 @@
|
||||
"type": "enum",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{ "value": "none", "label": "None" },
|
||||
{ "value": "239", "label": "2.39:1" },
|
||||
{ "value": "185", "label": "1.85:1" },
|
||||
{ "value": "square", "label": "1:1" }
|
||||
]
|
||||
{
|
||||
"value": "none",
|
||||
"label": "None"
|
||||
},
|
||||
{
|
||||
"value": "239",
|
||||
"label": "2.39:1"
|
||||
},
|
||||
{
|
||||
"value": "185",
|
||||
"label": "1.85:1"
|
||||
},
|
||||
{
|
||||
"value": "square",
|
||||
"label": "1:1"
|
||||
}
|
||||
],
|
||||
"description": "Adds an optional framing matte for common delivery ratios."
|
||||
},
|
||||
{
|
||||
"id": "matteOpacity",
|
||||
"label": "Matte Opacity",
|
||||
"type": "float",
|
||||
"default": 0.35,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the aspect-ratio matte outside the active image."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
111
shaders/singularity/shader.json
Normal file
111
shaders/singularity/shader.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"id": "singularity",
|
||||
"name": "Singularity",
|
||||
"description": "Whirling blackhole and accretion disk. Original by XorDev, adapted from https://www.shadertoy.com/view/3csSWB.",
|
||||
"category": "Generative",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 0.7,
|
||||
"min": 0.25,
|
||||
"max": 1.5,
|
||||
"step": 0.01,
|
||||
"description": "Overall size of the effect in the frame."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Gravity",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the lensing/gravity distortion."
|
||||
},
|
||||
{
|
||||
"id": "ringRadius",
|
||||
"label": "Ring Radius",
|
||||
"type": "float",
|
||||
"default": 0.7,
|
||||
"min": 0.2,
|
||||
"max": 1.4,
|
||||
"step": 0.01,
|
||||
"description": "Radius of the bright accretion ring."
|
||||
},
|
||||
{
|
||||
"id": "tightness",
|
||||
"label": "Tightness",
|
||||
"type": "float",
|
||||
"default": 1.35,
|
||||
"min": 0.5,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Concentration of the ring and spiral detail."
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts the generated effect brightness."
|
||||
},
|
||||
{
|
||||
"id": "colorShift",
|
||||
"label": "Color Shift",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": -2,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Cycles the generated color palette."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Center",
|
||||
"type": "vec2",
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the black hole center in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
55
shaders/singularity/shader.slang
Normal file
55
shaders/singularity/shader.slang
Normal file
@@ -0,0 +1,55 @@
|
||||
float2 singularitySpiral(float2 c, float time, float iterator)
|
||||
{
|
||||
float radiusSq = max(dot(c, c), 0.0001);
|
||||
float angle = 0.5 * log(radiusSq) + time * iterator;
|
||||
return float2(
|
||||
c.x * cos(angle + 0.0) + c.y * cos(angle + 11.0),
|
||||
c.x * cos(angle + 33.0) + c.y * cos(angle + 0.0)) / max(iterator, 0.001);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||
float2 fragCoord = context.uv * resolution;
|
||||
float safeScale = max(scale, 0.001);
|
||||
float safeRingRadius = max(ringRadius, 0.001);
|
||||
float safeTightness = max(tightness, 0.001);
|
||||
float seed = context.startupRandom;
|
||||
float time = context.time * speed + seed * 37.0;
|
||||
|
||||
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
|
||||
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||
|
||||
// Build a skewed coordinate system around an offset "black hole" so the
|
||||
// waves pinch and stretch instead of staying radially symmetric.
|
||||
float iterator = 0.2;
|
||||
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
|
||||
float2 blackholeCenter = p - iterator * diagonal;
|
||||
float gravity = iterator * strength / max(dot(blackholeCenter, blackholeCenter), 0.0001);
|
||||
float2 skew = diagonal / (0.1 + gravity);
|
||||
|
||||
float2 c = float2(p.x + p.y, p.x * skew.x + p.y * skew.y);
|
||||
float2 v = singularitySpiral(c, time, iterator);
|
||||
float2 waves = float2(0.0001, 0.0001);
|
||||
|
||||
// Iterative sine feedback creates the accretion texture; the iterator value
|
||||
// also damps later steps to keep the pattern stable.
|
||||
for (; iterator < 9.0; iterator += 1.0)
|
||||
{
|
||||
waves += 1.0 + sin(v);
|
||||
v += 0.7 * sin(v.yx * iterator + time) / iterator + 0.5;
|
||||
}
|
||||
|
||||
float diskRadius = length(sin(v / 0.3) * 0.4 + c * float2(2.0, 4.0));
|
||||
float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius;
|
||||
float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001);
|
||||
float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness;
|
||||
// Exponential falloff turns the accumulated wave field into bright rims and
|
||||
// a darker center without hard thresholds.
|
||||
float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift);
|
||||
float4 waveColor = waves.xyyx;
|
||||
|
||||
float4 color = 1.0 - exp(-redBlueGradient / max(waveColor, float4(0.0001, 0.0001, 0.0001, 0.0001)) / disk / centerDarkness / rim * brightness);
|
||||
color.a = 1.0;
|
||||
return saturate(lerp(color, context.sourceColor, sourceMix));
|
||||
}
|
||||
8
shaders/smpte-color-bars/shader.json
Normal file
8
shaders/smpte-color-bars/shader.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "smpte-color-bars",
|
||||
"name": "SMPTE Color Bars",
|
||||
"description": "Generates a procedural SMPTE RP 219-style 16:9 color bar test pattern matching the common Wikimedia 1920x1080 reference layout.",
|
||||
"category": "Calibration",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": []
|
||||
}
|
||||
90
shaders/smpte-color-bars/shader.slang
Normal file
90
shaders/smpte-color-bars/shader.slang
Normal file
@@ -0,0 +1,90 @@
|
||||
float3 hexColor(float r, float g, float b)
|
||||
{
|
||||
return float3(r, g, b) / 255.0;
|
||||
}
|
||||
|
||||
float3 smpteTop(float x)
|
||||
{
|
||||
if (x < 240.0)
|
||||
return hexColor(102.0, 102.0, 102.0);
|
||||
if (x < 445.0)
|
||||
return hexColor(191.0, 191.0, 191.0);
|
||||
if (x < 651.0)
|
||||
return hexColor(191.0, 191.0, 0.0);
|
||||
if (x < 857.0)
|
||||
return hexColor(0.0, 191.0, 191.0);
|
||||
if (x < 1063.0)
|
||||
return hexColor(0.0, 191.0, 0.0);
|
||||
if (x < 1269.0)
|
||||
return hexColor(191.0, 0.0, 191.0);
|
||||
if (x < 1475.0)
|
||||
return hexColor(191.0, 0.0, 0.0);
|
||||
if (x < 1680.0)
|
||||
return hexColor(0.0, 0.0, 191.0);
|
||||
return hexColor(102.0, 102.0, 102.0);
|
||||
}
|
||||
|
||||
float3 smpteMiddleA(float x)
|
||||
{
|
||||
if (x < 240.0)
|
||||
return hexColor(0.0, 255.0, 255.0);
|
||||
if (x < 445.0)
|
||||
return hexColor(0.0, 63.0, 105.0);
|
||||
if (x < 1680.0)
|
||||
return hexColor(191.0, 191.0, 191.0);
|
||||
return hexColor(0.0, 0.0, 255.0);
|
||||
}
|
||||
|
||||
float3 smpteMiddleB(float x)
|
||||
{
|
||||
if (x < 240.0)
|
||||
return hexColor(255.0, 255.0, 0.0);
|
||||
if (x < 445.0)
|
||||
return hexColor(65.0, 0.0, 119.0);
|
||||
if (x < 1475.0)
|
||||
{
|
||||
float ramp = saturate((x - 445.0) / (1475.0 - 445.0));
|
||||
return float3(ramp, ramp, ramp);
|
||||
}
|
||||
if (x < 1680.0)
|
||||
return float3(1.0, 1.0, 1.0);
|
||||
return hexColor(255.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
float3 smpteBottom(float x)
|
||||
{
|
||||
if (x < 240.0)
|
||||
return hexColor(38.0, 38.0, 38.0);
|
||||
if (x < 549.0)
|
||||
return float3(0.0, 0.0, 0.0);
|
||||
if (x < 960.0)
|
||||
return float3(1.0, 1.0, 1.0);
|
||||
if (x < 1268.0)
|
||||
return float3(0.0, 0.0, 0.0);
|
||||
if (x < 1337.0)
|
||||
return hexColor(5.0, 5.0, 5.0);
|
||||
if (x < 1405.0)
|
||||
return float3(0.0, 0.0, 0.0);
|
||||
if (x < 1474.0)
|
||||
return hexColor(10.0, 10.0, 10.0);
|
||||
if (x < 1680.0)
|
||||
return float3(0.0, 0.0, 0.0);
|
||||
return hexColor(38.0, 38.0, 38.0);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 uv = saturate(context.uv);
|
||||
float2 pixel = float2(uv.x, 1.0 - uv.y) * float2(1920.0, 1080.0);
|
||||
|
||||
if (pixel.y < 630.0)
|
||||
return float4(smpteTop(pixel.x), 1.0);
|
||||
|
||||
if (pixel.y < 720.0)
|
||||
return float4(smpteMiddleA(pixel.x), 1.0);
|
||||
|
||||
if (pixel.y < 810.0)
|
||||
return float4(smpteMiddleB(pixel.x), 1.0);
|
||||
|
||||
return float4(smpteBottom(pixel.x), 1.0);
|
||||
}
|
||||
21
shaders/solid-color/shader.json
Normal file
21
shaders/solid-color/shader.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "solid-color",
|
||||
"name": "Solid Color",
|
||||
"description": "Fills the frame with a single user-selected color.",
|
||||
"category": "Color",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "fillColor",
|
||||
"label": "Fill",
|
||||
"type": "color",
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Frame fill color; alpha is preserved for key-capable outputs."
|
||||
}
|
||||
]
|
||||
}
|
||||
4
shaders/solid-color/shader.slang
Normal file
4
shaders/solid-color/shader.slang
Normal file
@@ -0,0 +1,4 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return saturate(fillColor);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"id": "studio-color",
|
||||
"name": "Studio Color",
|
||||
"description": "A built-in sample shader package that demonstrates the runtime parameter contract.",
|
||||
"category": "Built-in",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-0.2, -0.2],
|
||||
"max": [0.2, 0.2],
|
||||
"step": [0.001, 0.001]
|
||||
},
|
||||
{
|
||||
"id": "tint",
|
||||
"label": "Tint",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
},
|
||||
{
|
||||
"id": "invert",
|
||||
"label": "Invert",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": "mode",
|
||||
"label": "Mode",
|
||||
"type": "enum",
|
||||
"default": "normal",
|
||||
"options": [
|
||||
{ "value": "normal", "label": "Normal" },
|
||||
{ "value": "luma", "label": "Luma" },
|
||||
{ "value": "posterize", "label": "Posterize" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 uv = clamp(context.uv + offset, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||
float4 color = sampleVideo(uv);
|
||||
|
||||
color.rgb *= brightness;
|
||||
color *= tint;
|
||||
|
||||
if (invert)
|
||||
color.rgb = 1.0 - color.rgb;
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
color.rgb = float3(luma, luma, luma);
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
color.rgb = floor(color.rgb * 4.0) / 4.0;
|
||||
}
|
||||
|
||||
return saturate(color);
|
||||
}
|
||||
@@ -15,33 +15,42 @@
|
||||
"label": "Echo Amount",
|
||||
"type": "float",
|
||||
"default": 0.55,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the temporal echoes."
|
||||
},
|
||||
{
|
||||
"id": "decay",
|
||||
"label": "Decay",
|
||||
"type": "float",
|
||||
"default": 0.72,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "How quickly older temporal echoes fade away."
|
||||
},
|
||||
{
|
||||
"id": "frameStride",
|
||||
"label": "Frame Stride",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 1.0,
|
||||
"max": 6.0,
|
||||
"step": 1.0
|
||||
"default": 2,
|
||||
"min": 1,
|
||||
"max": 6,
|
||||
"step": 1,
|
||||
"description": "Number of frames skipped between each echo sample."
|
||||
},
|
||||
{
|
||||
"id": "echoTint",
|
||||
"label": "Echo Tint",
|
||||
"type": "color",
|
||||
"default": [0.65, 0.85, 1.0, 1.0]
|
||||
"default": [
|
||||
0.65,
|
||||
0.85,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Tint applied to older echo frames."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "temporal-ghost-trail",
|
||||
"name": "Temporal Ghost Trail",
|
||||
"description": "Blends older pre-layer input frames into the current layer input for a soft temporal trail.",
|
||||
"category": "Built-in",
|
||||
"category": "Temporal",
|
||||
"entryPoint": "shadeVideo",
|
||||
"temporal": {
|
||||
"enabled": true,
|
||||
@@ -15,18 +15,20 @@
|
||||
"label": "Current Mix",
|
||||
"type": "float",
|
||||
"default": 0.72,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Contribution of the current frame in the temporal blend."
|
||||
},
|
||||
{
|
||||
"id": "trailMix",
|
||||
"label": "Trail Mix",
|
||||
"type": "float",
|
||||
"default": 0.28,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Contribution of older frames in the temporal blend."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "temporal-low-fps",
|
||||
"name": "Temporal Low FPS",
|
||||
"description": "Holds older source frames to create a deliberate choppy playback look.",
|
||||
"category": "Built-in",
|
||||
"category": "Temporal",
|
||||
"entryPoint": "shadeVideo",
|
||||
"temporal": {
|
||||
"enabled": true,
|
||||
@@ -14,19 +14,21 @@
|
||||
"id": "holdFrames",
|
||||
"label": "Hold Frames",
|
||||
"type": "float",
|
||||
"default": 3.0,
|
||||
"min": 0.0,
|
||||
"max": 7.0,
|
||||
"step": 0.1
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 7,
|
||||
"step": 0.1,
|
||||
"description": "How many previous frames to hold for the low-FPS effect."
|
||||
},
|
||||
{
|
||||
"id": "blendAmount",
|
||||
"label": "Blend",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mix amount for the processed result."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
shaders/text-overlay/fonts/AnalogMono.ttf
Normal file
BIN
shaders/text-overlay/fonts/AnalogMono.ttf
Normal file
Binary file not shown.
60
shaders/text-overlay/fonts/LICENSE.txt
Normal file
60
shaders/text-overlay/fonts/LICENSE.txt
Normal file
@@ -0,0 +1,60 @@
|
||||
Analog Mono Plus Pixel Font - License Agreement
|
||||
Copyright © Andrew Gleeson, 2026
|
||||
heygleeson@gmail.com
|
||||
|
||||
1. GRANT OF LICENSE
|
||||
|
||||
This Agreement is a license, not an agreement of sale. Licensee shall
|
||||
not acquire any copyright ownership or equivalent rights to any of the
|
||||
Licensed Content. Seller and the Licensed Content sources retain all
|
||||
right, title and interest in and to all of the copyrights, trademarks,
|
||||
and all other proprietary rights in the Licensed Content. All rights
|
||||
in and to Licensed Content not expressly granted in this agreement are
|
||||
retained by Seller or its suppliers.
|
||||
|
||||
Licensee is permitted to use the Licensed Content in unlimited
|
||||
commercial projects. A commercial project is one defined as a Work for
|
||||
Distribution launched with the capability to generate revenue, or
|
||||
intention to generate revenue through the sale of, licensing of, or
|
||||
otherwise intend to generate revenue directly from the Work for
|
||||
Distribution.
|
||||
|
||||
2. RESTRICTION ON USE
|
||||
|
||||
Licensed Content may not be used contrary to any restriction on use
|
||||
indicated herein.
|
||||
|
||||
Licensed Content may not be resold, sublicensed, assigned, transferred
|
||||
or otherwise made available to third parties except as incorporated
|
||||
into Works for Distribution.
|
||||
|
||||
Licensed Content may not be distributed to third parties as standalone
|
||||
files or in a way that unreasonably permits the recipient to extract
|
||||
the Licensed Content for use separately and apart from the Work for
|
||||
Distribution.
|
||||
|
||||
Licensee may not distribute the Licensed Content in any library or
|
||||
reusable template, including but not limited to game templates, website
|
||||
templates intended to allow reproduction by third parties on electronic
|
||||
or printed products.
|
||||
|
||||
Licensee may not distribute Licensed Content in a manner meant to enable
|
||||
third parties to create derivative works incorporating Licensed Content.
|
||||
|
||||
Licensee may not superficially modify the Licensed Content and sell it
|
||||
to others for consumption, reproduction or re-sale.
|
||||
|
||||
Licensee shall not use the Licensed Content in a manner that violates
|
||||
the law of any applicable jurisdiction.
|
||||
|
||||
Licensee shall not claim copyright or attribution of Licensed Content.
|
||||
|
||||
3. TERM AND TERMINATION
|
||||
|
||||
The license contained in this Agreement terminates automatically without
|
||||
notice from Seller if Licensee fails to comply with any provision of
|
||||
this Agreement. Upon termination, Licensee must with immediate effect
|
||||
stop using the Licensed Content, destroy, delete and remove the Licensed
|
||||
Content from Licensee’s premises, computer systems and storage. Licensee
|
||||
must also make all reasonable efforts to ensure that copies of the
|
||||
licensed content are removed from any locations it has been distributed to.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user