Folder fixes
All checks were successful
CI / React UI Build (push) Successful in 37s
CI / Native Windows Build And Tests (push) Successful in 2m18s
CI / Windows Release Package (push) Successful in 2m0s

This commit is contained in:
2026-05-18 14:57:25 +10:00
parent f461a05c65
commit 1d4eb7a34c
47 changed files with 28243 additions and 1211 deletions

85
.vscode/launch.json vendored
View File

@@ -1,89 +1,6 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Debug LoopThroughWithOpenGLCompositing",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"environment": [
{
"name": "VST_DISABLE_INPUT_CAPTURE",
"value": "1"
}
],
"console": "internalConsole",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"requireExactSource": true,
"logging": {
"moduleLoad": true
},
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
},
{
"name": "Debug LoopThroughWithOpenGLCompositing - sync readback experiment",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"environment": [
{
"name": "VST_OUTPUT_READBACK_MODE",
"value": "sync"
}
],
"console": "internalConsole",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"requireExactSource": true,
"logging": {
"moduleLoad": true
},
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
},
{
"name": "Debug LoopThroughWithOpenGLCompositing - cached output experiment",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"environment": [
{
"name": "VST_OUTPUT_READBACK_MODE",
"value": "cached_only"
}
],
"console": "internalConsole",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"requireExactSource": true,
"logging": {
"moduleLoad": true
},
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
},
{
"name": "Debug DeckLinkRenderCadenceProbe",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\DeckLinkRenderCadenceProbe.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"environment": [],
"console": "externalTerminal",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"requireExactSource": true,
"logging": {
"moduleLoad": true
},
"preLaunchTask": "Build DeckLinkRenderCadenceProbe Debug x64"
},
{ {
"name": "Debug RenderCadenceCompositor", "name": "Debug RenderCadenceCompositor",
"type": "cppvsdbg", "type": "cppvsdbg",
@@ -91,7 +8,7 @@
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe", "program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
"args": [], "args": [],
"stopAtEntry": false, "stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug", "cwd": "${workspaceFolder}",
"environment": [], "environment": [],
"console": "externalTerminal", "console": "externalTerminal",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug", "symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",

92
.vscode/tasks.json vendored
View File

@@ -2,60 +2,19 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "Build LoopThroughWithOpenGLCompositing Debug x64", "label": "Configure Debug x64",
"type": "process", "type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe", "command": "cmake",
"args": [ "args": [
"--build", "--preset",
"${workspaceFolder}\\build\\vs2022-x64-debug", "vs2022-x64-debug"
"--config",
"Debug",
"--target",
"LoopThroughWithOpenGLCompositing",
"--parallel"
], ],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "Build LoopThroughWithOpenGLCompositing Release x64",
"type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-release",
"--config",
"Release",
"--target",
"LoopThroughWithOpenGLCompositing",
"--parallel"
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Build DeckLinkRenderCadenceProbe Debug x64",
"type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-debug",
"--config",
"Debug",
"--target",
"DeckLinkRenderCadenceProbe",
"--parallel"
],
"group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "Build RenderCadenceCompositor Debug x64", "label": "Build RenderCadenceCompositor Debug x64",
"type": "process", "type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe", "command": "cmake",
"args": [ "args": [
"--build", "--build",
"${workspaceFolder}\\build\\vs2022-x64-debug", "${workspaceFolder}\\build\\vs2022-x64-debug",
@@ -65,13 +24,50 @@
"RenderCadenceCompositor", "RenderCadenceCompositor",
"--parallel" "--parallel"
], ],
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": "Configure Debug x64",
"problemMatcher": "$msCompile"
},
{
"label": "Build RenderCadenceCompositor Release x64",
"type": "process",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-release",
"--config",
"Release",
"--target",
"RenderCadenceCompositor",
"--parallel"
],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
}, },
{ {
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64", "label": "Run Native Tests Debug x64",
"type": "process", "type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe", "command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-debug",
"--config",
"Debug",
"--target",
"RUN_TESTS",
"--parallel"
],
"group": "test",
"dependsOn": "Configure Debug x64",
"problemMatcher": "$msCompile"
},
{
"label": "Clean Debug x64",
"type": "process",
"command": "cmake",
"args": [ "args": [
"--build", "--build",
"${workspaceFolder}\\build\\vs2022-x64-debug", "${workspaceFolder}\\build\\vs2022-x64-debug",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
#pragma once
#include <windows.h>
class UniqueHandle
{
public:
explicit UniqueHandle(HANDLE handle = NULL) : mHandle(handle) {}
~UniqueHandle() { reset(); }
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
UniqueHandle(UniqueHandle&& other) noexcept : mHandle(other.release()) {}
UniqueHandle& operator=(UniqueHandle&& other) noexcept
{
if (this != &other)
reset(other.release());
return *this;
}
HANDLE get() const { return mHandle; }
bool valid() const { return mHandle != NULL && mHandle != INVALID_HANDLE_VALUE; }
HANDLE release()
{
HANDLE handle = mHandle;
mHandle = NULL;
return handle;
}
void reset(HANDLE handle = NULL)
{
if (valid())
CloseHandle(mHandle);
mHandle = handle;
}
private:
HANDLE mHandle;
};

View File

@@ -0,0 +1,42 @@
#pragma once
#include <winsock2.h>
class UniqueSocket
{
public:
explicit UniqueSocket(SOCKET socket = INVALID_SOCKET) : mSocket(socket) {}
~UniqueSocket() { reset(); }
UniqueSocket(const UniqueSocket&) = delete;
UniqueSocket& operator=(const UniqueSocket&) = delete;
UniqueSocket(UniqueSocket&& other) noexcept : mSocket(other.release()) {}
UniqueSocket& operator=(UniqueSocket&& other) noexcept
{
if (this != &other)
reset(other.release());
return *this;
}
SOCKET get() const { return mSocket; }
bool valid() const { return mSocket != INVALID_SOCKET; }
SOCKET release()
{
SOCKET socket = mSocket;
mSocket = INVALID_SOCKET;
return socket;
}
void reset(SOCKET socket = INVALID_SOCKET)
{
if (valid())
closesocket(mSocket);
mSocket = socket;
}
private:
SOCKET mSocket;
};

212
src/render/GLExtensions.cpp Normal file
View File

@@ -0,0 +1,212 @@
/* -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-
*/
//
// GLExtensions.cpp
// LoopThroughWithOpenGLCompositing
//
#include "GLExtensions.h"
PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers;
PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers;
PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer;
PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage;
PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers;
PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers;
PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer;
PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D;
PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer;
PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus;
PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer;
PFNGLFENCESYNCPROC glFenceSync;
PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
PFNGLDELETESYNCPROC glDeleteSync;
PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBUFFERDATAPROC glBufferData;
PFNGLMAPBUFFERPROC glMapBuffer;
PFNGLUNMAPBUFFERPROC glUnmapBuffer;
PFNGLBUFFERSUBDATAPROC glBufferSubData;
PFNGLBINDBUFFERBASEPROC glBindBufferBase;
PFNGLACTIVETEXTUREPROC glActiveTexture;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
PFNGLCREATESHADERPROC glCreateShader;
PFNGLDELETESHADERPROC glDeleteShader;
PFNGLDELETEPROGRAMPROC glDeleteProgram;
PFNGLSHADERSOURCEPROC glShaderSource;
PFNGLCOMPILESHADERPROC glCompileShader;
PFNGLGETSHADERIVPROC glGetShaderiv;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
PFNGLCREATEPROGRAMPROC glCreateProgram;
PFNGLATTACHSHADERPROC glAttachShader;
PFNGLLINKPROGRAMPROC glLinkProgram;
PFNGLGETPROGRAMIVPROC glGetProgramiv;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
PFNGLUSEPROGRAMPROC glUseProgram;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex;
PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding;
PFNGLUNIFORM1IPROC glUniform1i;
PFNGLUNIFORM1FPROC glUniform1f;
PFNGLUNIFORM2FPROC glUniform2f;
PFNGLUNIFORM4FPROC glUniform4f;
bool ResolveGLExtensions()
{
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffers");
if (!glGenFramebuffers)
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffersEXT");
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffers");
if (!glGenRenderbuffers)
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffersEXT");
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbuffer");
if (!glBindRenderbuffer)
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbufferEXT");
glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorage");
if (!glRenderbufferStorage)
glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorageEXT");
glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffers");
if (!glDeleteFramebuffers)
glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffersEXT");
glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffers");
if (!glDeleteRenderbuffers)
glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffersEXT");
glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebuffer");
if (!glBindFramebuffer)
glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebufferEXT");
glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2D");
if (!glFramebufferTexture2D)
glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2DEXT");
glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbuffer");
if (!glFramebufferRenderbuffer)
glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbufferEXT");
glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatus");
if (!glCheckFramebufferStatus)
glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatusEXT");
glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) wglGetProcAddress("glBlitFramebuffer");
if (!glBlitFramebuffer)
glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) wglGetProcAddress("glBlitFramebufferEXT");
glFenceSync = (PFNGLFENCESYNCPROC) wglGetProcAddress("glFenceSync");
glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) wglGetProcAddress("glClientWaitSync");
glDeleteSync = (PFNGLDELETESYNCPROC) wglGetProcAddress("glDeleteSync");
glGenBuffers = (PFNGLGENBUFFERSPROC) wglGetProcAddress("glGenBuffers");
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
glMapBuffer = (PFNGLMAPBUFFERPROC) wglGetProcAddress("glMapBuffer");
glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) wglGetProcAddress("glUnmapBuffer");
glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData");
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase");
glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture");
glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC) wglGetProcAddress("glGenVertexArrays");
glDeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC) wglGetProcAddress("glDeleteVertexArrays");
glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC) wglGetProcAddress("glBindVertexArray");
glCreateShader = (PFNGLCREATESHADERPROC) wglGetProcAddress("glCreateShader");
glDeleteShader = (PFNGLDELETESHADERPROC) wglGetProcAddress("glDeleteShader");
glDeleteProgram = (PFNGLDELETEPROGRAMPROC) wglGetProcAddress("glDeleteProgram");
glShaderSource = (PFNGLSHADERSOURCEPROC) wglGetProcAddress("glShaderSource");
glCompileShader = (PFNGLCOMPILESHADERPROC) wglGetProcAddress("glCompileShader");
glGetShaderiv = (PFNGLGETSHADERIVPROC) wglGetProcAddress("glGetShaderiv");
glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC) wglGetProcAddress("glGetShaderInfoLog");
glCreateProgram = (PFNGLCREATEPROGRAMPROC) wglGetProcAddress("glCreateProgram");
glAttachShader = (PFNGLATTACHSHADERPROC) wglGetProcAddress("glAttachShader");
glLinkProgram = (PFNGLLINKPROGRAMPROC) wglGetProcAddress("glLinkProgram");
glGetProgramiv = (PFNGLGETPROGRAMIVPROC) wglGetProcAddress("glGetProgramiv");
glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC) wglGetProcAddress("glGetProgramInfoLog");
glUseProgram = (PFNGLUSEPROGRAMPROC) wglGetProcAddress("glUseProgram");
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) wglGetProcAddress("glGetUniformLocation");
glGetUniformBlockIndex = (PFNGLGETUNIFORMBLOCKINDEXPROC) wglGetProcAddress("glGetUniformBlockIndex");
glUniformBlockBinding = (PFNGLUNIFORMBLOCKBINDINGPROC) wglGetProcAddress("glUniformBlockBinding");
glUniform1i = (PFNGLUNIFORM1IPROC) wglGetProcAddress("glUniform1i");
glUniform1f = (PFNGLUNIFORM1FPROC) wglGetProcAddress("glUniform1f");
glUniform2f = (PFNGLUNIFORM2FPROC) wglGetProcAddress("glUniform2f");
glUniform4f = (PFNGLUNIFORM4FPROC) wglGetProcAddress("glUniform4f");
return glGenFramebuffers
&& glGenRenderbuffers
&& glBindRenderbuffer
&& glRenderbufferStorage
&& glDeleteFramebuffers
&& glDeleteRenderbuffers
&& glBindFramebuffer
&& glFramebufferTexture2D
&& glFramebufferRenderbuffer
&& glCheckFramebufferStatus
&& glBlitFramebuffer
&& glFenceSync
&& glClientWaitSync
&& glDeleteSync
&& glGenBuffers
&& glDeleteBuffers
&& glBindBuffer
&& glBufferData
&& glMapBuffer
&& glUnmapBuffer
&& glBufferSubData
&& glBindBufferBase
&& glActiveTexture
&& glGenVertexArrays
&& glDeleteVertexArrays
&& glBindVertexArray
&& glCreateShader
&& glDeleteShader
&& glDeleteProgram
&& glShaderSource
&& glCompileShader
&& glGetShaderiv
&& glGetShaderInfoLog
&& glCreateProgram
&& glAttachShader
&& glLinkProgram
&& glGetProgramiv
&& glGetProgramInfoLog
&& glUseProgram
&& glGetUniformLocation
&& glGetUniformBlockIndex
&& glUniformBlockBinding
&& glUniform1i
&& glUniform1f
&& glUniform2f
&& glUniform4f
;
}

201
src/render/GLExtensions.h Normal file
View File

@@ -0,0 +1,201 @@
/* -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-
*/
//
// GLExtensions.h
// LoopThroughWithOpenGLCompositing
//
#ifndef __GLEXTENSIONS_H__
#define __GLEXTENSIONS_H__
#include <windows.h>
#include <gl/gl.h>
#ifndef APIENTRYP
#define APIENTRYP APIENTRY *
#endif
#define GL_BGRA 0x80E1
#define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367
#define GL_STREAM_DRAW 0x88E0
#define GL_STREAM_READ 0x88E1
#define GL_STREAM_COPY 0x88E2
#define GL_DYNAMIC_DRAW 0x88E8
#define GL_UNIFORM_BUFFER 0x8A11
#define GL_RGBA8 0x8058
#define GL_RGBA16F 0x881A
#define GL_TEXTURE0 0x84C0
#define GL_ACTIVE_TEXTURE 0x84E0
#define GL_ARRAY_BUFFER 0x8892
#define GL_PIXEL_PACK_BUFFER 0x88EB
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF
#define GL_FRAGMENT_SHADER 0x8B30
#define GL_VERTEX_SHADER 0x8B31
#define GL_COMPILE_STATUS 0x8B81
#define GL_LINK_STATUS 0x8B82
#define GL_INVALID_INDEX 0xFFFFFFFFu
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
#define GL_RENDERBUFFER_EXT 0x8D41
#define GL_FRAMEBUFFER_EXT 0x8D40
#define GL_FRAMEBUFFER_COMPLETE_EXT 0x8CD5
#define GL_COLOR_ATTACHMENT0_EXT 0x8CE0
#define GL_READ_FRAMEBUFFER 0x8CA8
#define GL_DRAW_FRAMEBUFFER 0x8CA9
#define GL_RENDERBUFFER 0x8D41
#define GL_FRAMEBUFFER 0x8D40
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#define GL_COLOR_ATTACHMENT0 0x8CE0
#define GL_DEPTH_COMPONENT24 0x81A6
#define GL_CLAMP_TO_EDGE 0x812F
#define GL_DEPTH_ATTACHMENT_EXT 0x8D00
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_ALREADY_SIGNALED 0x911A
#define GL_TIMEOUT_EXPIRED 0x911B
#define GL_CONDITION_SATISFIED 0x911C
#define GL_WAIT_FAILED 0x911D
#define GL_READ_ONLY 0x88B8
typedef struct __GLsync *GLsync;
typedef unsigned __int64 GLuint64;
typedef ptrdiff_t GLintptr;
typedef ptrdiff_t GLsizeiptr;
typedef char GLchar;
typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERPROC) (GLenum target, GLenum access);
typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target);
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
typedef GLuint (APIENTRYP PFNGLCREATESHADERPROC) (GLenum type);
typedef void (APIENTRYP PFNGLDELETESHADERPROC) (GLuint shader);
typedef void (APIENTRYP PFNGLDELETEPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLGETPROGRAMIVPROC) (GLuint program, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);
typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name);
typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const GLchar* *string, const GLint *length);
typedef void (APIENTRYP PFNGLUSEPROGRAMPROC) (GLuint program);
typedef void (APIENTRYP PFNGLUNIFORM1FPROC) (GLint location, GLfloat v0);
typedef void (APIENTRYP PFNGLUNIFORM1IPROC) (GLint location, GLint v0);
typedef void (APIENTRYP PFNGLUNIFORM2FPROC) (GLint location, GLfloat v0, GLfloat v1);
typedef void (APIENTRYP PFNGLUNIFORM4FPROC) (GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
typedef GLuint (APIENTRYP PFNGLGETUNIFORMBLOCKINDEXPROC) (GLuint program, const GLchar* uniformBlockName);
typedef void (APIENTRYP PFNGLUNIFORMBLOCKBINDINGPROC) (GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
typedef void (APIENTRYP PFNGLACTIVETEXTUREPROC) (GLenum texture);
typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
typedef void (APIENTRYP PFNGLBUFFERSUBDATAPROC) (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);
typedef void (APIENTRYP PFNGLBINDVERTEXARRAYPROC) (GLuint array);
typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint* arrays);
typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint* arrays);
typedef GLsync (APIENTRYP PFNGLFENCESYNCPROC) (GLenum condition, GLbitfield flags);
typedef void (APIENTRYP PFNGLDELETESYNCPROC) (GLsync sync);
typedef GLenum (APIENTRYP PFNGLCLIENTWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout);
typedef void (APIENTRYP PFNGLBINDRENDERBUFFERPROC) (GLenum target, GLuint renderbuffer);
typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSPROC) (GLsizei n, const GLuint *renderbuffers);
typedef void (APIENTRYP PFNGLGENRENDERBUFFERSPROC) (GLsizei n, GLuint *renderbuffers);
typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (APIENTRYP PFNGLBINDFRAMEBUFFERPROC) (GLenum target, GLuint framebuffer);
typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSPROC) (GLsizei n, const GLuint *framebuffers);
typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSPROC) (GLsizei n, GLuint *framebuffers);
typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSPROC) (GLenum target);
typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
typedef void (APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFERPROC) (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
typedef void (APIENTRYP PFNGLBLITFRAMEBUFFERPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
extern PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers;
extern PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers;
extern PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer;
extern PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage;
extern PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers;
extern PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers;
extern PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer;
extern PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D;
extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer;
extern PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus;
extern PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer;
extern PFNGLFENCESYNCPROC glFenceSync;
extern PFNGLCLIENTWAITSYNCPROC glClientWaitSync;
extern PFNGLDELETESYNCPROC glDeleteSync;
extern PFNGLGENBUFFERSPROC glGenBuffers;
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
extern PFNGLBINDBUFFERPROC glBindBuffer;
extern PFNGLBUFFERDATAPROC glBufferData;
extern PFNGLMAPBUFFERPROC glMapBuffer;
extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
extern PFNGLACTIVETEXTUREPROC glActiveTexture;
extern PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
extern PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
extern PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
extern PFNGLCREATESHADERPROC glCreateShader;
extern PFNGLDELETESHADERPROC glDeleteShader;
extern PFNGLDELETEPROGRAMPROC glDeleteProgram;
extern PFNGLSHADERSOURCEPROC glShaderSource;
extern PFNGLCOMPILESHADERPROC glCompileShader;
extern PFNGLGETSHADERIVPROC glGetShaderiv;
extern PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
extern PFNGLCREATEPROGRAMPROC glCreateProgram;
extern PFNGLATTACHSHADERPROC glAttachShader;
extern PFNGLLINKPROGRAMPROC glLinkProgram;
extern PFNGLGETPROGRAMIVPROC glGetProgramiv;
extern PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
extern PFNGLUSEPROGRAMPROC glUseProgram;
extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
extern PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex;
extern PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding;
extern PFNGLUNIFORM1IPROC glUniform1i;
extern PFNGLUNIFORM1FPROC glUniform1f;
extern PFNGLUNIFORM2FPROC glUniform2f;
extern PFNGLUNIFORM4FPROC glUniform4f;
bool ResolveGLExtensions();
#endif // __GLEXTENSIONS_H__

48
src/render/Std140Buffer.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstddef>
#include <cstring>
#include <vector>
inline std::size_t AlignStd140(std::size_t offset, std::size_t alignment)
{
const std::size_t mask = alignment - 1;
return (offset + mask) & ~mask;
}
template <typename TValue>
inline void AppendStd140Value(std::vector<unsigned char>& buffer, std::size_t alignment, const TValue& value)
{
const std::size_t offset = AlignStd140(buffer.size(), alignment);
if (buffer.size() < offset + sizeof(TValue))
buffer.resize(offset + sizeof(TValue), 0);
std::memcpy(buffer.data() + offset, &value, sizeof(TValue));
}
inline void AppendStd140Float(std::vector<unsigned char>& buffer, float value)
{
AppendStd140Value(buffer, 4, value);
}
inline void AppendStd140Int(std::vector<unsigned char>& buffer, int value)
{
AppendStd140Value(buffer, 4, value);
}
inline void AppendStd140Vec2(std::vector<unsigned char>& buffer, float x, float y)
{
const std::size_t offset = AlignStd140(buffer.size(), 8);
if (buffer.size() < offset + sizeof(float) * 2)
buffer.resize(offset + sizeof(float) * 2, 0);
float values[2] = { x, y };
std::memcpy(buffer.data() + offset, values, sizeof(values));
}
inline void AppendStd140Vec4(std::vector<unsigned char>& buffer, float x, float y, float z, float w)
{
const std::size_t offset = AlignStd140(buffer.size(), 16);
if (buffer.size() < offset + sizeof(float) * 4)
buffer.resize(offset + sizeof(float) * 4, 0);
float values[4] = { x, y, z, w };
std::memcpy(buffer.data() + offset, values, sizeof(values));
}

674
src/runtime/RuntimeJson.cpp Normal file
View File

@@ -0,0 +1,674 @@
#include "stdafx.h"
#include "RuntimeJson.h"
#include <cerrno>
#include <cctype>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <limits>
#include <sstream>
namespace
{
int HexDigitValue(char ch)
{
if (ch >= '0' && ch <= '9')
return ch - '0';
if (ch >= 'a' && ch <= 'f')
return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F')
return ch - 'A' + 10;
return -1;
}
bool IsHighSurrogate(unsigned int codePoint)
{
return codePoint >= 0xD800 && codePoint <= 0xDBFF;
}
bool IsLowSurrogate(unsigned int codePoint)
{
return codePoint >= 0xDC00 && codePoint <= 0xDFFF;
}
void AppendUtf8(unsigned int codePoint, std::ostringstream& output)
{
if (codePoint <= 0x7F)
{
output << static_cast<char>(codePoint);
}
else if (codePoint <= 0x7FF)
{
output << static_cast<char>(0xC0 | ((codePoint >> 6) & 0x1F));
output << static_cast<char>(0x80 | (codePoint & 0x3F));
}
else if (codePoint <= 0xFFFF)
{
output << static_cast<char>(0xE0 | ((codePoint >> 12) & 0x0F));
output << static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
output << static_cast<char>(0x80 | (codePoint & 0x3F));
}
else
{
output << static_cast<char>(0xF0 | ((codePoint >> 18) & 0x07));
output << static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
output << static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
output << static_cast<char>(0x80 | (codePoint & 0x3F));
}
}
void AppendControlEscape(unsigned char ch, std::ostringstream& output)
{
const char* digits = "0123456789ABCDEF";
output << "\\u00" << digits[(ch >> 4) & 0x0F] << digits[ch & 0x0F];
}
class JsonParser
{
public:
JsonParser(const std::string& text, std::string& error)
: mText(text), mError(error), mPosition(0)
{
}
bool parse(JsonValue& value)
{
skipWhitespace();
if (!parseValue(value))
return false;
skipWhitespace();
if (mPosition != mText.size())
{
setError("Unexpected trailing characters in JSON input.");
return false;
}
return true;
}
private:
bool parseValue(JsonValue& value)
{
if (mPosition >= mText.size())
{
setError("Unexpected end of JSON input.");
return false;
}
char ch = mText[mPosition];
if (ch == '{')
return parseObject(value);
if (ch == '[')
return parseArray(value);
if (ch == '"')
{
std::string stringValue;
if (!parseString(stringValue))
return false;
value = JsonValue(stringValue);
return true;
}
if (ch == 't')
return parseLiteral("true", JsonValue(true), value);
if (ch == 'f')
return parseLiteral("false", JsonValue(false), value);
if (ch == 'n')
return parseLiteral("null", JsonValue(), value);
if (ch == '-' || std::isdigit(static_cast<unsigned char>(ch)))
return parseNumber(value);
setError("Unexpected token while parsing JSON.");
return false;
}
bool parseObject(JsonValue& value)
{
value = JsonValue::MakeObject();
++mPosition;
skipWhitespace();
if (consume('}'))
return true;
while (mPosition < mText.size())
{
std::string key;
if (!parseString(key))
return false;
skipWhitespace();
if (!consume(':'))
{
setError("Expected ':' after JSON object key.");
return false;
}
skipWhitespace();
JsonValue item;
if (!parseValue(item))
return false;
value.set(key, item);
skipWhitespace();
if (consume('}'))
return true;
if (!consume(','))
{
setError("Expected ',' or '}' in JSON object.");
return false;
}
skipWhitespace();
}
setError("Unexpected end of JSON object.");
return false;
}
bool parseArray(JsonValue& value)
{
value = JsonValue::MakeArray();
++mPosition;
skipWhitespace();
if (consume(']'))
return true;
while (mPosition < mText.size())
{
JsonValue item;
if (!parseValue(item))
return false;
value.pushBack(item);
skipWhitespace();
if (consume(']'))
return true;
if (!consume(','))
{
setError("Expected ',' or ']' in JSON array.");
return false;
}
skipWhitespace();
}
setError("Unexpected end of JSON array.");
return false;
}
bool parseString(std::string& value)
{
if (!consume('"'))
{
setError("Expected string literal.");
return false;
}
std::ostringstream result;
while (mPosition < mText.size())
{
char ch = mText[mPosition++];
if (ch == '"')
{
value = result.str();
return true;
}
if (ch == '\\')
{
if (mPosition >= mText.size())
{
setError("Unexpected end of escaped JSON string.");
return false;
}
char escaped = mText[mPosition++];
switch (escaped)
{
case '"': result << '"'; break;
case '\\': result << '\\'; break;
case '/': result << '/'; break;
case 'b': result << '\b'; break;
case 'f': result << '\f'; break;
case 'n': result << '\n'; break;
case 'r': result << '\r'; break;
case 't': result << '\t'; break;
case 'u':
if (!parseUnicodeEscape(result))
return false;
break;
default:
setError("Invalid escape sequence in JSON string.");
return false;
}
}
else
{
if (static_cast<unsigned char>(ch) < 0x20)
{
setError("Unescaped control character in JSON string.");
return false;
}
result << ch;
}
}
setError("Unexpected end of JSON string.");
return false;
}
bool parseHexCodePoint(unsigned int& codePoint)
{
if (mPosition + 4 > mText.size())
{
setError("Unexpected end of Unicode escape sequence.");
return false;
}
codePoint = 0;
for (int i = 0; i < 4; ++i)
{
const int digit = HexDigitValue(mText[mPosition + i]);
if (digit < 0)
{
setError("Invalid Unicode escape sequence in JSON string.");
return false;
}
codePoint = (codePoint << 4) | static_cast<unsigned int>(digit);
}
mPosition += 4;
return true;
}
bool parseUnicodeEscape(std::ostringstream& result)
{
unsigned int codePoint = 0;
if (!parseHexCodePoint(codePoint))
return false;
if (IsHighSurrogate(codePoint))
{
if (mPosition + 2 > mText.size() || mText[mPosition] != '\\' || mText[mPosition + 1] != 'u')
{
setError("High surrogate Unicode escape must be followed by a low surrogate.");
return false;
}
mPosition += 2;
unsigned int lowSurrogate = 0;
if (!parseHexCodePoint(lowSurrogate))
return false;
if (!IsLowSurrogate(lowSurrogate))
{
setError("High surrogate Unicode escape must be followed by a low surrogate.");
return false;
}
codePoint = 0x10000 + (((codePoint - 0xD800) << 10) | (lowSurrogate - 0xDC00));
}
else if (IsLowSurrogate(codePoint))
{
setError("Low surrogate Unicode escape without preceding high surrogate.");
return false;
}
AppendUtf8(codePoint, result);
return true;
}
bool parseNumber(JsonValue& value)
{
std::size_t start = mPosition;
if (mText[mPosition] == '-')
++mPosition;
if (mPosition >= mText.size())
{
setError("Invalid JSON number.");
return false;
}
if (mText[mPosition] == '0')
{
++mPosition;
if (mPosition < mText.size() && std::isdigit(static_cast<unsigned char>(mText[mPosition])))
{
setError("JSON numbers must not contain leading zeroes.");
return false;
}
}
else if (mText[mPosition] >= '1' && mText[mPosition] <= '9')
{
while (mPosition < mText.size() && std::isdigit(static_cast<unsigned char>(mText[mPosition])))
++mPosition;
}
else
{
setError("Invalid JSON number.");
return false;
}
if (mPosition < mText.size() && mText[mPosition] == '.')
{
++mPosition;
if (mPosition >= mText.size() || !std::isdigit(static_cast<unsigned char>(mText[mPosition])))
{
setError("JSON number fraction must contain at least one digit.");
return false;
}
while (mPosition < mText.size() && std::isdigit(static_cast<unsigned char>(mText[mPosition])))
++mPosition;
}
if (mPosition < mText.size() && (mText[mPosition] == 'e' || mText[mPosition] == 'E'))
{
++mPosition;
if (mPosition < mText.size() && (mText[mPosition] == '+' || mText[mPosition] == '-'))
++mPosition;
if (mPosition >= mText.size() || !std::isdigit(static_cast<unsigned char>(mText[mPosition])))
{
setError("JSON number exponent must contain at least one digit.");
return false;
}
while (mPosition < mText.size() && std::isdigit(static_cast<unsigned char>(mText[mPosition])))
++mPosition;
}
std::string token = mText.substr(start, mPosition - start);
char* endPtr = nullptr;
errno = 0;
double parsed = strtod(token.c_str(), &endPtr);
if (endPtr == token.c_str() || *endPtr != '\0' || errno == ERANGE || !std::isfinite(parsed))
{
setError("Invalid JSON number.");
return false;
}
value = JsonValue(parsed);
return true;
}
bool parseLiteral(const char* literal, const JsonValue& literalValue, JsonValue& value)
{
std::size_t length = strlen(literal);
if (mText.compare(mPosition, length, literal) != 0)
{
setError("Invalid JSON literal.");
return false;
}
mPosition += length;
value = literalValue;
return true;
}
void skipWhitespace()
{
while (mPosition < mText.size() && std::isspace(static_cast<unsigned char>(mText[mPosition])))
++mPosition;
}
bool consume(char expected)
{
if (mPosition < mText.size() && mText[mPosition] == expected)
{
++mPosition;
return true;
}
return false;
}
void setError(const std::string& error)
{
if (mError.empty())
mError = error;
}
const std::string& mText;
std::string& mError;
std::size_t mPosition;
};
void SerializeJsonImpl(const JsonValue& value, std::ostringstream& output, bool pretty, int indentLevel)
{
auto indent = [&](int level) {
if (!pretty)
return;
for (int i = 0; i < level; ++i)
output << " ";
};
switch (value.type())
{
case JsonValue::Type::Null:
output << "null";
break;
case JsonValue::Type::Boolean:
output << (value.asBoolean() ? "true" : "false");
break;
case JsonValue::Type::Number:
{
double number = value.asNumber();
if (std::isfinite(number))
{
output << std::setprecision(15) << number;
}
else
{
output << "0";
}
break;
}
case JsonValue::Type::String:
{
output << '"';
for (char ch : value.asString())
{
switch (ch)
{
case '"': output << "\\\""; break;
case '\\': output << "\\\\"; break;
case '\b': output << "\\b"; break;
case '\f': output << "\\f"; break;
case '\n': output << "\\n"; break;
case '\r': output << "\\r"; break;
case '\t': output << "\\t"; break;
default:
if (static_cast<unsigned char>(ch) < 0x20)
AppendControlEscape(static_cast<unsigned char>(ch), output);
else
output << ch;
break;
}
}
output << '"';
break;
}
case JsonValue::Type::Array:
{
output << "[";
const std::vector<JsonValue>& array = value.asArray();
if (!array.empty())
{
if (pretty)
output << "\n";
for (std::size_t i = 0; i < array.size(); ++i)
{
indent(indentLevel + 1);
SerializeJsonImpl(array[i], output, pretty, indentLevel + 1);
if (i + 1 != array.size())
output << ",";
if (pretty)
output << "\n";
}
indent(indentLevel);
}
output << "]";
break;
}
case JsonValue::Type::Object:
{
output << "{";
const std::map<std::string, JsonValue>& object = value.asObject();
if (!object.empty())
{
if (pretty)
output << "\n";
std::size_t index = 0;
for (const auto& item : object)
{
indent(indentLevel + 1);
SerializeJsonImpl(JsonValue(item.first), output, pretty, indentLevel + 1);
output << (pretty ? ": " : ":");
SerializeJsonImpl(item.second, output, pretty, indentLevel + 1);
if (++index != object.size())
output << ",";
if (pretty)
output << "\n";
}
indent(indentLevel);
}
output << "}";
break;
}
}
}
}
JsonValue::JsonValue()
: mType(Type::Null), mBooleanValue(false), mNumberValue(0.0)
{
}
JsonValue::JsonValue(bool value)
: mType(Type::Boolean), mBooleanValue(value), mNumberValue(0.0)
{
}
JsonValue::JsonValue(double value)
: mType(Type::Number), mBooleanValue(false), mNumberValue(value)
{
}
JsonValue::JsonValue(const char* value)
: mType(Type::String), mBooleanValue(false), mNumberValue(0.0), mStringValue(value ? value : "")
{
}
JsonValue::JsonValue(const std::string& value)
: mType(Type::String), mBooleanValue(false), mNumberValue(0.0), mStringValue(value)
{
}
JsonValue JsonValue::MakeArray()
{
JsonValue value;
value.reset(Type::Array);
return value;
}
JsonValue JsonValue::MakeObject()
{
JsonValue value;
value.reset(Type::Object);
return value;
}
bool JsonValue::asBoolean(bool fallback) const
{
return mType == Type::Boolean ? mBooleanValue : fallback;
}
double JsonValue::asNumber(double fallback) const
{
return mType == Type::Number ? mNumberValue : fallback;
}
const std::string& JsonValue::asString() const
{
static const std::string emptyString;
return mType == Type::String ? mStringValue : emptyString;
}
const std::vector<JsonValue>& JsonValue::asArray() const
{
static const std::vector<JsonValue> emptyArray;
return mType == Type::Array ? mArrayValue : emptyArray;
}
const std::map<std::string, JsonValue>& JsonValue::asObject() const
{
static const std::map<std::string, JsonValue> emptyObject;
return mType == Type::Object ? mObjectValue : emptyObject;
}
std::vector<JsonValue>& JsonValue::array()
{
if (mType != Type::Array)
reset(Type::Array);
return mArrayValue;
}
std::map<std::string, JsonValue>& JsonValue::object()
{
if (mType != Type::Object)
reset(Type::Object);
return mObjectValue;
}
void JsonValue::pushBack(const JsonValue& value)
{
array().push_back(value);
}
void JsonValue::set(const std::string& key, const JsonValue& value)
{
object()[key] = value;
}
const JsonValue* JsonValue::find(const std::string& key) const
{
if (mType != Type::Object)
return nullptr;
auto iterator = mObjectValue.find(key);
return iterator != mObjectValue.end() ? &iterator->second : nullptr;
}
void JsonValue::reset(Type type)
{
mType = type;
mBooleanValue = false;
mNumberValue = 0.0;
mStringValue.clear();
mArrayValue.clear();
mObjectValue.clear();
}
bool ParseJson(const std::string& text, JsonValue& value, std::string& error)
{
error.clear();
JsonParser parser(text, error);
return parser.parse(value);
}
std::string SerializeJson(const JsonValue& value, bool pretty)
{
std::ostringstream output;
SerializeJsonImpl(value, output, pretty, 0);
return output.str();
}
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;
}

65
src/runtime/RuntimeJson.h Normal file
View File

@@ -0,0 +1,65 @@
#pragma once
#include <map>
#include <string>
#include <vector>
class JsonValue
{
public:
enum class Type
{
Null,
Boolean,
Number,
String,
Array,
Object
};
JsonValue();
explicit JsonValue(bool value);
explicit JsonValue(double value);
explicit JsonValue(const char* value);
explicit JsonValue(const std::string& value);
static JsonValue MakeArray();
static JsonValue MakeObject();
Type type() const { return mType; }
bool isNull() const { return mType == Type::Null; }
bool isBoolean() const { return mType == Type::Boolean; }
bool isNumber() const { return mType == Type::Number; }
bool isString() const { return mType == Type::String; }
bool isArray() const { return mType == Type::Array; }
bool isObject() const { return mType == Type::Object; }
bool asBoolean(bool fallback = false) const;
double asNumber(double fallback = 0.0) const;
const std::string& asString() const;
const std::vector<JsonValue>& asArray() const;
const std::map<std::string, JsonValue>& asObject() const;
std::vector<JsonValue>& array();
std::map<std::string, JsonValue>& object();
void pushBack(const JsonValue& value);
void set(const std::string& key, const JsonValue& value);
const JsonValue* find(const std::string& key) const;
private:
void reset(Type type);
Type mType;
bool mBooleanValue;
double mNumberValue;
std::string mStringValue;
std::vector<JsonValue> mArrayValue;
std::map<std::string, JsonValue> mObjectValue;
};
bool ParseJson(const std::string& text, JsonValue& value, std::string& error);
std::string SerializeJson(const JsonValue& value, bool pretty = false);
std::vector<double> JsonArrayToNumbers(const JsonValue& value);

View File

@@ -0,0 +1,196 @@
#include "stdafx.h"
#include "RuntimeParameterUtils.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <vector>
namespace
{
std::string TrimText(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::string NormalizeTextValue(const std::string& text, unsigned maxLength)
{
std::string normalized;
normalized.reserve(std::min<std::size_t>(text.size(), maxLength));
for (unsigned char ch : text)
{
if (ch < 32 || ch > 126)
continue;
if (normalized.size() >= maxLength)
break;
normalized.push_back(static_cast<char>(ch));
}
return normalized;
}
}
std::string MakeSafePresetFileStem(const std::string& presetName)
{
std::string trimmed = TrimText(presetName);
std::string safe;
safe.reserve(trimmed.size());
for (unsigned char ch : trimmed)
{
if (std::isalnum(ch))
safe.push_back(static_cast<char>(std::tolower(ch)));
else if (ch == ' ' || ch == '-' || ch == '_')
{
if (safe.empty() || safe.back() == '-')
continue;
safe.push_back('-');
}
}
while (!safe.empty() && safe.back() == '-')
safe.pop_back();
return safe;
}
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition)
{
ShaderParameterValue value;
switch (definition.type)
{
case ShaderParameterType::Float:
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
break;
case ShaderParameterType::Vec2:
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
break;
case ShaderParameterType::Color:
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
break;
case ShaderParameterType::Boolean:
value.booleanValue = definition.defaultBoolean;
break;
case ShaderParameterType::Enum:
value.enumValue = definition.defaultEnumValue;
break;
case ShaderParameterType::Text:
value.textValue = NormalizeTextValue(definition.defaultTextValue, definition.maxLength);
break;
case ShaderParameterType::Trigger:
value.numberValues = { 0.0, -1000000.0 };
break;
}
return value;
}
bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error)
{
normalizedValue = DefaultValueForDefinition(definition);
switch (definition.type)
{
case ShaderParameterType::Float:
{
if (!value.isNumber())
{
error = "Expected numeric value for float parameter '" + definition.id + "'.";
return false;
}
double number = value.asNumber();
if (!IsFiniteNumber(number))
{
error = "Float parameter '" + definition.id + "' must be finite.";
return false;
}
if (!definition.minNumbers.empty())
number = std::max(number, definition.minNumbers.front());
if (!definition.maxNumbers.empty())
number = std::min(number, definition.maxNumbers.front());
normalizedValue.numberValues = { number };
return true;
}
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
std::vector<double> numbers = JsonArrayToNumbers(value);
const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4;
if (numbers.size() != expectedSize)
{
error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'.";
return false;
}
for (std::size_t index = 0; index < numbers.size(); ++index)
{
if (!IsFiniteNumber(numbers[index]))
{
error = "Parameter '" + definition.id + "' contains a non-finite value.";
return false;
}
if (index < definition.minNumbers.size())
numbers[index] = std::max(numbers[index], definition.minNumbers[index]);
if (index < definition.maxNumbers.size())
numbers[index] = std::min(numbers[index], definition.maxNumbers[index]);
}
normalizedValue.numberValues = numbers;
return true;
}
case ShaderParameterType::Boolean:
if (!value.isBoolean())
{
error = "Expected boolean value for parameter '" + definition.id + "'.";
return false;
}
normalizedValue.booleanValue = value.asBoolean();
return true;
case ShaderParameterType::Enum:
{
if (!value.isString())
{
error = "Expected string value for enum parameter '" + definition.id + "'.";
return false;
}
const std::string selectedValue = value.asString();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == selectedValue)
{
normalizedValue.enumValue = selectedValue;
return true;
}
}
error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'.";
return false;
}
case ShaderParameterType::Text:
if (!value.isString())
{
error = "Expected string value for text parameter '" + definition.id + "'.";
return false;
}
normalizedValue.textValue = NormalizeTextValue(value.asString(), definition.maxLength);
return true;
case ShaderParameterType::Trigger:
if (!value.isNumber() && !value.isBoolean())
{
error = "Expected numeric or boolean value for trigger parameter '" + definition.id + "'.";
return false;
}
normalizedValue.numberValues = { value.isNumber() ? std::max(0.0, std::floor(value.asNumber())) : 0.0, -1000000.0 };
return true;
}
return false;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <string>
std::string MakeSafePresetFileStem(const std::string& presetName);
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition);
bool NormalizeAndValidateParameterValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error);

View File

@@ -0,0 +1,326 @@
#include "stdafx.h"
#include "ShaderCompiler.h"
#include "NativeHandles.h"
#include <fstream>
#include <cctype>
#include <regex>
#include <sstream>
#include <vector>
namespace
{
std::string ReplaceAll(std::string text, const std::string& from, const std::string& to)
{
std::size_t offset = 0;
while ((offset = text.find(from, offset)) != std::string::npos)
{
text.replace(offset, from.length(), to);
offset += to.length();
}
return text;
}
std::string SlangCBufferTypeForParameter(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "float";
case ShaderParameterType::Vec2: return "float2";
case ShaderParameterType::Color: return "float4";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "int";
case ShaderParameterType::Text: return "";
case ShaderParameterType::Trigger: return "int";
}
return "float";
}
std::string CapitalizeIdentifier(const std::string& identifier)
{
if (identifier.empty())
return identifier;
std::string text = identifier;
text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
return text;
}
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type == ShaderParameterType::Text)
continue;
if (definition.type == ShaderParameterType::Trigger)
{
source << "\tint " << definition.id << ";\n";
source << "\tfloat " << definition.id << "Time;\n";
continue;
}
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
}
return source.str();
}
std::string BuildHistorySamplerDeclarations(const std::string& samplerPrefix, unsigned historyLength)
{
std::ostringstream source;
for (unsigned index = 0; index < historyLength; ++index)
source << "Sampler2D<float4> " << samplerPrefix << index << ";\n";
return source.str();
}
std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset>& textureAssets)
{
std::ostringstream source;
for (const ShaderTextureAsset& textureAsset : textureAssets)
source << "Sampler2D<float4> " << textureAsset.id << ";\n";
if (!textureAssets.empty())
source << "\n";
return source.str();
}
std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
source << "Sampler2D<float4> " << definition.id << "Texture;\n";
}
if (source.tellp() > 0)
source << "\n";
return source.str();
}
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
const std::string suffix = CapitalizeIdentifier(definition.id);
source
<< "float sample" << suffix << "(float2 uv)\n"
<< "{\n"
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
<< "\t\treturn 0.0;\n"
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
<< "}\n\n"
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
<< "{\n"
<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
<< "}\n\n";
}
return source.str();
}
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
{
std::ostringstream source;
for (unsigned index = 0; index < historyLength; ++index)
source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n";
return source.str();
}
}
ShaderCompiler::ShaderCompiler(
const std::filesystem::path& repoRoot,
const std::filesystem::path& wrapperPath,
const std::filesystem::path& generatedGlslPath,
const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames)
: mRepoRoot(repoRoot),
mWrapperPath(wrapperPath),
mGeneratedGlslPath(generatedGlslPath),
mPatchedGlslPath(patchedGlslPath),
mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
{
std::string wrapperSource;
if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false;
if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error))
return false;
fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error);
if (fragmentShaderSource.empty())
return false;
if (!PatchGeneratedGlsl(fragmentShaderSource, error))
return false;
if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error))
return false;
return true;
}
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
{
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
wrapperSource = ReadTextFile(templatePath, error);
if (wrapperSource.empty())
return false;
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_SAMPLER}}", shaderPackage.feedback.enabled ? "Sampler2D<float4> gFeedbackState;\n" : "");
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_HELPER}}",
shaderPackage.feedback.enabled
? "float4 sampleFeedback(float2 tc)\n{\n\tif (gFeedbackAvailable <= 0)\n\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n\treturn gFeedbackState.Sample(tc);\n}\n"
: "float4 sampleFeedback(float2 tc)\n{\n\treturn float4(0.0, 0.0, 0.0, 0.0);\n}\n");
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
return true;
}
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{
char slangRootBuffer[MAX_PATH] = {};
const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer)));
if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
{
std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyRoot))
{
error = "3rdParty directory was not found under the repository root.";
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
error = "Could not find slangc.exe under 3rdParty.";
return false;
}
bool ShaderCompiler::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const
{
std::filesystem::path compilerPath;
if (!FindSlangCompiler(compilerPath, error))
return false;
std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string()
+ "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\"";
STARTUPINFOA startupInfo = {};
PROCESS_INFORMATION processInfo = {};
startupInfo.cb = sizeof(startupInfo);
std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
mutableCommandLine.push_back('\0');
if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo))
{
error = "Failed to launch slangc.exe.";
return false;
}
UniqueHandle processHandle(processInfo.hProcess);
UniqueHandle threadHandle(processInfo.hThread);
WaitForSingleObject(processHandle.get(), INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processHandle.get(), &exitCode);
if (exitCode != 0)
{
error = "slangc.exe returned a non-zero exit code while compiling the active shader package.";
return false;
}
return true;
}
bool ShaderCompiler::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const
{
if (shaderText.find("#version 450") == std::string::npos)
{
error = "Generated GLSL did not include the expected version header.";
return false;
}
shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core");
shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;");
shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord");
std::smatch match;
std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)");
if (std::regex_search(shaderText, match, outRegex))
{
const std::string outputName = match[1].str();
shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;");
shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor =");
}
return true;
}
std::string ShaderCompiler::ReadTextFile(const std::filesystem::path& path, std::string& error) const
{
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();
}
bool ShaderCompiler::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const
{
std::error_code fsError;
std::filesystem::create_directories(path.parent_path(), fsError);
std::ofstream output(path, std::ios::binary);
if (!output)
{
error = "Could not write file: " + path.string();
return false;
}
output << contents;
return output.good();
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <string>
class ShaderCompiler
{
public:
ShaderCompiler(
const std::filesystem::path& repoRoot,
const std::filesystem::path& wrapperPath,
const std::filesystem::path& generatedGlslPath,
const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames);
bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const;
private:
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const;
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const;
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
private:
std::filesystem::path mRepoRoot;
std::filesystem::path mWrapperPath;
std::filesystem::path mGeneratedGlslPath;
std::filesystem::path mPatchedGlslPath;
unsigned mMaxTemporalHistoryFrames;
};

View File

@@ -0,0 +1,818 @@
#include "stdafx.h"
#include "ShaderPackageRegistry.h"
#include "RuntimeJson.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <fstream>
#include <sstream>
namespace
{
std::string Trim(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
--end;
return text.substr(start, end - start);
}
bool IsFiniteNumber(double value)
{
return std::isfinite(value) != 0;
}
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
{
if (typeName == "float")
{
type = ShaderParameterType::Float;
return true;
}
if (typeName == "vec2")
{
type = ShaderParameterType::Vec2;
return true;
}
if (typeName == "color")
{
type = ShaderParameterType::Color;
return true;
}
if (typeName == "bool")
{
type = ShaderParameterType::Boolean;
return true;
}
if (typeName == "enum")
{
type = ShaderParameterType::Enum;
return true;
}
if (typeName == "text")
{
type = ShaderParameterType::Text;
return true;
}
if (typeName == "trigger")
{
type = ShaderParameterType::Trigger;
return true;
}
return false;
}
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
{
if (sourceName == "source")
{
source = TemporalHistorySource::Source;
return true;
}
if (sourceName == "preLayerInput")
{
source = TemporalHistorySource::PreLayerInput;
return true;
}
if (sourceName == "none")
{
source = TemporalHistorySource::None;
return true;
}
return false;
}
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{
std::ifstream input(path, std::ios::binary);
if (!input)
{
error = "Could not open file: " + path.string();
return std::string();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
{
return manifestPath.string();
}
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue || !fieldValue->isString())
{
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(object, fieldName, value, manifestPath, error))
return false;
if (Trim(value).empty())
{
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue)
{
value = fallback;
return true;
}
if (!fieldValue->isString())
{
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isArray())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isObject())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (value.isNumber())
{
numbers.push_back(value.asNumber());
return true;
}
if (value.isArray())
{
numbers = JsonArrayToNumbers(value);
if (numbers.size() != value.asArray().size())
{
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
for (char ch : identifier)
{
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
if (!(std::isalnum(unsignedCh) || ch == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
}
return true;
}
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
return false;
shaderPackage.directoryPath = manifestPath.parent_path();
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
shaderPackage.manifestPath = manifestPath;
return true;
}
bool ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* passesValue = nullptr;
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
return false;
if (!passesValue)
{
// Existing shader packages are treated as a single implicit pass, so
// multipass support does not require manifest churn.
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = shaderPackage.entryPoint;
pass.sourcePath = shaderPackage.shaderPath;
pass.outputName = "layerOutput";
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
return true;
}
if (passesValue->asArray().empty())
{
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& passJson : passesValue->asArray())
{
if (!passJson.isObject())
{
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string passId;
std::string sourcePath;
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
{
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
return false;
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
{
if (existingPass.id == passId)
{
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
}
ShaderPassDefinition pass;
pass.id = passId;
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
return false;
const JsonValue* inputsValue = nullptr;
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
return false;
if (inputsValue)
{
for (const JsonValue& inputValue : inputsValue->asArray())
{
if (!inputValue.isString())
{
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
return false;
}
pass.inputNames.push_back(inputValue.asString());
}
}
// Keep source validation in the registry. Bad pass declarations then
// appear as unavailable shaders instead of failing at render time.
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
}
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
return true;
}
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* texturesValue = nullptr;
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
return false;
if (!texturesValue)
return true;
for (const JsonValue& textureJson : texturesValue->asArray())
{
if (!textureJson.isObject())
{
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string textureId;
std::string texturePath;
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
{
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
return false;
ShaderTextureAsset textureAsset;
textureAsset.id = textureId;
textureAsset.path = shaderPackage.directoryPath / texturePath;
if (!std::filesystem::exists(textureAsset.path))
{
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
return false;
}
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
shaderPackage.textureAssets.push_back(textureAsset);
}
return true;
}
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fontsValue = nullptr;
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
return false;
if (!fontsValue)
return true;
for (const JsonValue& fontJson : fontsValue->asArray())
{
if (!fontJson.isObject())
{
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string fontId;
std::string fontPath;
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
{
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
return false;
ShaderFontAsset fontAsset;
fontAsset.id = fontId;
fontAsset.path = shaderPackage.directoryPath / fontPath;
if (!std::filesystem::exists(fontAsset.path))
{
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
return false;
}
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
shaderPackage.fontAssets.push_back(fontAsset);
}
return true;
}
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* temporalValue = nullptr;
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
return false;
if (!temporalValue)
return true;
const JsonValue* enabledValue = temporalValue->find("enabled");
if (!enabledValue || !enabledValue->asBoolean(false))
return true;
std::string historySourceName;
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
{
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
return false;
}
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
if (!historyLengthValue || !historyLengthValue->isNumber())
{
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
return false;
}
TemporalHistorySource historySource = TemporalHistorySource::None;
if (!ParseTemporalHistorySource(historySourceName, historySource))
{
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
const double requestedHistoryLength = historyLengthValue->asNumber();
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
{
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
return false;
}
shaderPackage.temporal.enabled = true;
shaderPackage.temporal.historySource = historySource;
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
return true;
}
bool ParseFeedbackSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* feedbackValue = nullptr;
if (!OptionalObjectField(manifestJson, "feedback", feedbackValue, manifestPath, error))
return false;
if (!feedbackValue)
return true;
const JsonValue* enabledValue = feedbackValue->find("enabled");
if (!enabledValue || !enabledValue->asBoolean(false))
return true;
shaderPackage.feedback.enabled = true;
if (!OptionalStringField(*feedbackValue, "writePass", shaderPackage.feedback.writePassId, "", manifestPath, error))
return false;
if (shaderPackage.feedback.writePassId.empty())
{
if (shaderPackage.passes.empty())
{
error = "Feedback-enabled shader has no passes to target in: " + ManifestPathMessage(manifestPath);
return false;
}
shaderPackage.feedback.writePassId = shaderPackage.passes.back().id;
}
if (!ValidateShaderIdentifier(shaderPackage.feedback.writePassId, "feedback.writePass", manifestPath, error))
return false;
const auto passIt = std::find_if(shaderPackage.passes.begin(), shaderPackage.passes.end(),
[&shaderPackage](const ShaderPassDefinition& pass) { return pass.id == shaderPackage.feedback.writePassId; });
if (passIt == shaderPackage.passes.end())
{
error = "Feedback writePass '" + shaderPackage.feedback.writePassId + "' does not match any declared pass in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
{
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
return true;
}
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* defaultValue = parameterJson.find("default");
if (!defaultValue)
return true;
if (definition.type == ShaderParameterType::Boolean)
{
if (!defaultValue->isBoolean())
{
error = "Boolean parameter default must be a boolean for: " + definition.id;
return false;
}
definition.defaultBoolean = defaultValue->asBoolean(false);
return true;
}
if (definition.type == ShaderParameterType::Enum)
{
if (!defaultValue->isString())
{
error = "Enum parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultEnumValue = defaultValue->asString();
return true;
}
if (definition.type == ShaderParameterType::Text)
{
if (!defaultValue->isString())
{
error = "Text parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultTextValue = defaultValue->asString();
return true;
}
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
}
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* optionsValue = nullptr;
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
{
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& optionJson : optionsValue->asArray())
{
if (!optionJson.isObject())
{
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
ShaderParameterOption option;
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
{
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
return false;
}
definition.enumOptions.push_back(option);
}
bool defaultFound = definition.defaultEnumValue.empty();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == definition.defaultEnumValue)
{
defaultFound = true;
break;
}
}
if (!defaultFound)
{
error = "Enum parameter default is not present in its option list for: " + definition.id;
return false;
}
return true;
}
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
if (!parameterJson.isObject())
{
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string typeName;
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
{
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ParseShaderParameterType(typeName, definition.type))
{
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
{
return false;
}
if (definition.type == ShaderParameterType::Text)
{
if (const JsonValue* fontValue = parameterJson.find("font"))
{
if (!fontValue->isString())
{
error = "Text parameter 'font' must be a string for: " + definition.id;
return false;
}
definition.fontId = fontValue->asString();
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
return false;
}
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
{
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
{
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
return false;
}
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
}
}
if (definition.type == ShaderParameterType::Enum)
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
return true;
}
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* parametersValue = nullptr;
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
return false;
if (!parametersValue)
return true;
for (const JsonValue& parameterJson : parametersValue->asArray())
{
ShaderParameterDefinition definition;
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
return false;
shaderPackage.parameters.push_back(definition);
}
return true;
}
std::string UniqueUnavailableShaderId(const std::filesystem::path& manifestPath, const std::string& parsedId)
{
const std::string fallbackId = manifestPath.parent_path().filename().string();
const std::string baseId = parsedId.empty() ? fallbackId : parsedId;
return baseId + "@invalid:" + fallbackId;
}
ShaderPackageStatus BuildUnavailableStatus(const std::filesystem::path& manifestPath, const ShaderPackage& partialPackage, const std::string& packageError)
{
ShaderPackageStatus status;
status.id = UniqueUnavailableShaderId(manifestPath, partialPackage.id);
status.displayName = !partialPackage.displayName.empty() ? partialPackage.displayName : manifestPath.parent_path().filename().string();
status.description = partialPackage.description;
status.category = !partialPackage.category.empty() ? partialPackage.category : "Unavailable";
status.available = false;
status.error = packageError;
return status;
}
ShaderPackageStatus BuildAvailableStatus(const ShaderPackage& shaderPackage)
{
ShaderPackageStatus status;
status.id = shaderPackage.id;
status.displayName = shaderPackage.displayName;
status.description = shaderPackage.description;
status.category = shaderPackage.category;
status.available = true;
return status;
}
}
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderPackageRegistry::Scan(
const std::filesystem::path& shaderRoot,
std::map<std::string, ShaderPackage>& packagesById,
std::vector<std::string>& packageOrder,
std::vector<ShaderPackageStatus>& packageStatuses,
std::string& error) const
{
packagesById.clear();
packageOrder.clear();
packageStatuses.clear();
if (!std::filesystem::exists(shaderRoot))
{
error = "Shader library directory does not exist: " + shaderRoot.string();
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseManifest(manifestPath, shaderPackage, error))
{
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
error.clear();
continue;
}
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
continue;
}
packageOrder.push_back(shaderPackage.id);
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
return left.displayName < right.displayName;
});
return true;
}
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
if (!manifestJson.isObject())
{
error = "Shader manifest root must be an object: " + manifestPath.string();
return false;
}
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false;
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
return false;
shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
}
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseFeedbackSettings(manifestJson, shaderPackage, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <map>
#include <string>
#include <vector>
class ShaderPackageRegistry
{
public:
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
bool Scan(
const std::filesystem::path& shaderRoot,
std::map<std::string, ShaderPackage>& packagesById,
std::vector<std::string>& packageOrder,
std::vector<ShaderPackageStatus>& packageStatuses,
std::string& error) const;
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
private:
unsigned mMaxTemporalHistoryFrames;
};

159
src/shader/ShaderTypes.h Normal file
View File

@@ -0,0 +1,159 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <vector>
enum class ShaderParameterType
{
Float,
Vec2,
Color,
Boolean,
Enum,
Text,
Trigger
};
struct ShaderParameterOption
{
std::string value;
std::string label;
};
struct ShaderParameterDefinition
{
std::string id;
std::string label;
std::string description;
ShaderParameterType type = ShaderParameterType::Float;
std::vector<double> defaultNumbers;
std::vector<double> minNumbers;
std::vector<double> maxNumbers;
std::vector<double> stepNumbers;
bool defaultBoolean = false;
std::string defaultEnumValue;
std::string defaultTextValue;
std::string fontId;
unsigned maxLength = 64;
std::vector<ShaderParameterOption> enumOptions;
};
struct ShaderParameterValue
{
std::vector<double> numberValues;
bool booleanValue = false;
std::string enumValue;
std::string textValue;
};
enum class TemporalHistorySource
{
None,
Source,
PreLayerInput
};
struct TemporalSettings
{
bool enabled = false;
TemporalHistorySource historySource = TemporalHistorySource::None;
unsigned requestedHistoryLength = 0;
unsigned effectiveHistoryLength = 0;
};
struct FeedbackSettings
{
bool enabled = false;
std::string writePassId;
};
struct ShaderTextureAsset
{
std::string id;
std::filesystem::path path;
std::filesystem::file_time_type writeTime;
};
struct ShaderFontAsset
{
std::string id;
std::filesystem::path path;
std::filesystem::file_time_type writeTime;
};
struct ShaderPassDefinition
{
std::string id;
std::string entryPoint;
std::filesystem::path sourcePath;
std::filesystem::file_time_type sourceWriteTime;
std::vector<std::string> inputNames;
std::string outputName;
};
struct ShaderPassBuildSource
{
std::string passId;
std::string fragmentShaderSource;
std::vector<std::string> inputNames;
std::string outputName;
};
struct ShaderPackage
{
std::string id;
std::string displayName;
std::string description;
std::string category;
std::string entryPoint;
std::filesystem::path directoryPath;
std::filesystem::path shaderPath;
std::filesystem::path manifestPath;
std::vector<ShaderPassDefinition> passes;
std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
TemporalSettings temporal;
FeedbackSettings feedback;
std::filesystem::file_time_type shaderWriteTime;
std::filesystem::file_time_type manifestWriteTime;
};
struct ShaderPackageStatus
{
std::string id;
std::string displayName;
std::string description;
std::string category;
bool available = false;
std::string error;
};
struct RuntimeRenderState
{
std::string layerId;
std::string shaderId;
std::string shaderName;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
double timeSeconds = 0.0;
double utcTimeSeconds = 0.0;
double utcOffsetSeconds = 0.0;
double startupRandom = 0.0;
double frameCount = 0.0;
double mixAmount = 1.0;
double bypass = 0.0;
unsigned inputWidth = 0;
unsigned inputHeight = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
bool isTemporal = false;
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
unsigned requestedTemporalHistoryLength = 0;
unsigned effectiveTemporalHistoryLength = 0;
FeedbackSettings feedback;
};

13
src/stdafx.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>

20433
src/video/DeckLinkAPI_h.h Normal file

File diff suppressed because it is too large Load Diff

486
src/video/DeckLinkAPI_i.c Normal file
View File

@@ -0,0 +1,486 @@
/* this ALWAYS GENERATED file contains the IIDs and CLSIDs */
/* link this file in with the server and any clients */
/* File created by MIDL compiler version 8.01.0628 */
/* at Tue Jan 19 14:14:07 2038
*/
/* Compiler settings for ..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\include\DeckLinkAPI.idl:
Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0628
protocol : all , ms_ext, c_ext, robust
error checks: allocation ref bounds_check enum stub_data
VC __declspec() decoration level:
__declspec(uuid()), __declspec(selectany), __declspec(novtable)
DECLSPEC_UUID(), MIDL_INTERFACE()
*/
/* @@MIDL_FILE_HEADING( ) */
#ifdef __cplusplus
extern "C"{
#endif
#include <rpc.h>
#include <rpcndr.h>
#ifdef _MIDL_USE_GUIDDEF_
#ifndef INITGUID
#define INITGUID
#include <guiddef.h>
#undef INITGUID
#else
#include <guiddef.h>
#endif
#define MIDL_DEFINE_GUID(type,name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) \
DEFINE_GUID(name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8)
#else // !_MIDL_USE_GUIDDEF_
#ifndef __IID_DEFINED__
#define __IID_DEFINED__
typedef struct _IID
{
unsigned long x;
unsigned short s1;
unsigned short s2;
unsigned char c[8];
} IID;
#endif // __IID_DEFINED__
#ifndef CLSID_DEFINED
#define CLSID_DEFINED
typedef IID CLSID;
#endif // CLSID_DEFINED
#define MIDL_DEFINE_GUID(type,name,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) \
EXTERN_C __declspec(selectany) const type name = {l,w1,w2,{b1,b2,b3,b4,b5,b6,b7,b8}}
#endif // !_MIDL_USE_GUIDDEF_
MIDL_DEFINE_GUID(IID, LIBID_DeckLinkAPI,0xD864517A,0xEDD5,0x466D,0x86,0x7D,0xC8,0x19,0xF1,0xC0,0x52,0xBB);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkTimecode,0xBC6CFBD3,0x8317,0x4325,0xAC,0x1C,0x12,0x16,0x39,0x1E,0x93,0x40);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDisplayModeIterator,0x9C88499F,0xF601,0x4021,0xB8,0x0B,0x03,0x2E,0x4E,0xB4,0x1C,0x35);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDisplayMode,0x3EB2C1AB,0x0A3D,0x4523,0xA3,0xAD,0xF4,0x0D,0x7F,0xB1,0x4E,0x78);
MIDL_DEFINE_GUID(IID, IID_IDeckLink,0xC418FBDD,0x0587,0x48ED,0x8F,0xE5,0x64,0x0F,0x0A,0x14,0xAF,0x91);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration,0x5a68ffd4,0x1c12,0x4ede,0xa6,0xd2,0x45,0x45,0x1d,0x38,0x5f,0xc1);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderConfiguration,0x138050E5,0xC60A,0x4552,0xBF,0x3F,0x0F,0x35,0x80,0x49,0x32,0x7E);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDeckControlStatusCallback,0x53436FFB,0xB434,0x4906,0xBA,0xDC,0xAE,0x30,0x60,0xFF,0xE8,0xEF);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDeckControl,0x8E1C3ACE,0x19C7,0x4E00,0x8B,0x92,0xD8,0x04,0x31,0xD9,0x58,0xBE);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingDeviceNotificationCallback,0xF9531D64,0x3305,0x4B29,0xA3,0x87,0x7F,0x74,0xBB,0x0D,0x0E,0x84);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingH264InputCallback,0x823C475F,0x55AE,0x46F9,0x89,0x0C,0x53,0x7C,0xC5,0xCE,0xDC,0xCA);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingDiscovery,0x2C837444,0xF989,0x4D87,0x90,0x1A,0x47,0xC8,0xA3,0x6D,0x09,0x6D);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingVideoEncodingMode,0x1AB8035B,0xCD13,0x458D,0xB6,0xDF,0x5E,0x8F,0x7C,0x21,0x41,0xD9);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingMutableVideoEncodingMode,0x19BF7D90,0x1E0A,0x400D,0xB2,0xC6,0xFF,0xC4,0xE7,0x8A,0xD4,0x9D);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingVideoEncodingModePresetIterator,0x7AC731A3,0xC950,0x4AD0,0x80,0x4A,0x83,0x77,0xAA,0x51,0xC6,0xC4);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingDeviceInput,0x24B6B6EC,0x1727,0x44BB,0x98,0x18,0x34,0xFF,0x08,0x6A,0xCF,0x98);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingH264NALPacket,0xE260E955,0x14BE,0x4395,0x97,0x75,0x9F,0x02,0xCC,0x0A,0x9D,0x89);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingAudioPacket,0xD9EB5902,0x1AD2,0x43F4,0x9E,0x2C,0x3C,0xFA,0x50,0xB5,0xEE,0x19);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingMPEG2TSPacket,0x91810D1C,0x4FB3,0x4AAA,0xAE,0x56,0xFA,0x30,0x1D,0x3D,0xFA,0x4C);
MIDL_DEFINE_GUID(IID, IID_IBMDStreamingH264NALParser,0x5867F18C,0x5BFA,0x4CCC,0xB2,0xA7,0x9D,0xFD,0x14,0x04,0x17,0xD2);
MIDL_DEFINE_GUID(CLSID, CLSID_CBMDStreamingDiscovery,0x23A4EDF5,0xA0E5,0x432C,0x94,0xEF,0x3B,0xAB,0xB5,0xF8,0x1C,0x82);
MIDL_DEFINE_GUID(CLSID, CLSID_CBMDStreamingH264NALParser,0x7753EFBD,0x951C,0x407C,0x97,0xA5,0x23,0xC7,0x37,0xB7,0x3B,0x52);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoOutputCallback,0x5BE6DF26,0x02CE,0x433E,0x99,0xD9,0x9A,0x87,0xC3,0xAC,0x17,0x1F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInputCallback,0x3A94F075,0xC37D,0x4BA8,0xBC,0xC0,0x1D,0x77,0x8C,0x8F,0x88,0x1B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderInputCallback,0xACF13E61,0xF4A0,0x4974,0xA6,0xA7,0x59,0xAF,0xF6,0x26,0x8B,0x31);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBufferAllocator,0xF35DFA8D,0x9078,0x4622,0x95,0xBB,0x56,0x89,0x40,0x54,0xEB,0x0F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBufferAllocatorProvider,0x6DF6F20A,0xD8DF,0x45D2,0x89,0x14,0x38,0x3C,0xE7,0xE6,0x24,0x3F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAudioOutputCallback,0x403C681B,0x7F46,0x4A12,0xB9,0x93,0x2B,0xB1,0x27,0x08,0x4E,0xE6);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIterator,0x50FB36CD,0x3063,0x4B73,0xBD,0xBB,0x95,0x80,0x87,0xF2,0xD8,0xBA);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAPIInformation,0x7BEA3C68,0x730D,0x4322,0xAF,0x34,0x8A,0x71,0x52,0xB5,0x32,0xA4);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPFlowAttributes,0xCDA938DA,0x6479,0x40C6,0xB2,0xEC,0xA3,0x57,0x9B,0x3A,0xEE,0xCD);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPFlowStatus,0x31C41656,0x4992,0x4396,0xBB,0xE9,0x5F,0x84,0x06,0xAA,0xB5,0xAF);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPFlowSetting,0x86DD9174,0x27D3,0x4032,0xB2,0xAD,0x60,0x67,0xC3,0xBB,0x24,0x24);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPFlow,0xC5FC83C7,0x5B8E,0x42A7,0x9A,0x40,0x7C,0x06,0x59,0x55,0xD4,0xE1);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPFlowIterator,0xBD296AB2,0xA5C5,0x4153,0x88,0x8F,0xAA,0xB1,0xFD,0xBD,0x8A,0x5C);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkOutput,0x5F227C95,0x39D7,0x46C7,0x8B,0x7D,0x9C,0x81,0x79,0x5F,0xBB,0xE4);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput,0x6A515F8A,0xFBCE,0x4853,0xB0,0xF7,0x2A,0x09,0xDB,0x1E,0xCA,0x0B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkIPExtensions,0x46CF7903,0xA9FD,0x4D0B,0x8F,0xFC,0x01,0x03,0x72,0x2A,0xB4,0x42);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkHDMIInputEDID,0xABBBACBC,0x45BC,0x4665,0x9D,0x92,0xAC,0xE6,0xE5,0xA9,0x79,0x02);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderInput,0x46C1332E,0x6FD9,0x472A,0x85,0x91,0xFE,0x59,0xC2,0x21,0x92,0xE1);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBuffer,0x81F03D70,0xDE13,0x4B17,0x87,0x3A,0xC8,0xAC,0x96,0x89,0xC6,0x82);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrame,0x6502091C,0x615F,0x4F51,0xBA,0xF6,0x45,0xC4,0x25,0x6D,0xD5,0xB0);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkMutableVideoFrame,0xCF9EB134,0x0374,0x4C5B,0x95,0xFA,0x1E,0xC1,0x48,0x19,0xFF,0x62);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrame3DExtensions,0xD4DBE9C6,0xB4D2,0x49D3,0xAB,0xF2,0xB4,0xE8,0x6C,0x73,0x91,0xB0);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameMetadataExtensions,0xE232A5B7,0x4DB4,0x44C9,0x91,0x52,0xF4,0x7C,0x12,0xE5,0xF0,0x51);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameMutableMetadataExtensions,0xCC198FC6,0x8298,0x4419,0x94,0x2D,0x83,0x57,0xEC,0x35,0x5E,0x58);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoInputFrame,0xC9ADD3D2,0xBE52,0x488D,0xAB,0x2D,0x7F,0xDE,0xF7,0xAF,0x0C,0x95);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAncillaryPacket,0xF5C0D498,0x5CD3,0x4C77,0x97,0x73,0x8E,0xFA,0x20,0xBB,0x33,0x4B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAncillaryPacketIterator,0x10F1AA88,0x54BE,0x42F7,0xB9,0xF8,0xEC,0x2F,0x5F,0x09,0x95,0x51);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameAncillaryPackets,0x8A72D630,0x8070,0x4D05,0x8A,0x93,0xE6,0x0C,0x40,0xEE,0x08,0x8A);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameAncillary,0x732E723C,0xD1A4,0x4E29,0x9E,0x8E,0x4A,0x88,0x79,0x7A,0x00,0x04);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderPacket,0xB693F36C,0x316E,0x4AF1,0xB6,0xC2,0xF3,0x89,0xA4,0xBC,0xA6,0x20);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderVideoPacket,0x4E7FD944,0xE8C7,0x4EAC,0xB8,0xC0,0x7B,0x77,0xF8,0x0F,0x5A,0xE0);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderAudioPacket,0x49E8EDC8,0x693B,0x4E14,0x8E,0xF6,0x12,0xC6,0x58,0xF5,0xA0,0x7A);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkH265NALPacket,0x639C8E0B,0x68D5,0x4BDE,0xA6,0xD4,0x95,0xF3,0xAE,0xAF,0xF2,0xE7);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAudioInputPacket,0xE43D5870,0x2894,0x11DE,0x8C,0x30,0x08,0x00,0x20,0x0C,0x9A,0x66);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkScreenPreviewCallback,0xD4FA2345,0x9FBA,0x4497,0x95,0xC3,0xC0,0xC3,0xCE,0xD3,0xCD,0xA8);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkGLScreenPreviewHelper,0xCEB778E2,0xC202,0x4EC8,0x90,0x85,0x0C,0xD2,0x85,0xCC,0x55,0x22);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDX9ScreenPreviewHelper,0xF2DD78CA,0x2921,0x4AC2,0xB5,0xBC,0xBF,0xDC,0xC2,0x03,0x5A,0x1F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkWPFDX9ScreenPreviewHelper,0xC59346CD,0x9326,0x4266,0xAC,0x2D,0x5C,0x19,0x0F,0x57,0x99,0xEE);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkNotificationCallback,0xb002a1ec,0x070d,0x4288,0x82,0x89,0xbd,0x5d,0x36,0xe5,0xff,0x0d);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkNotification,0x1d70faac,0xfd27,0x4866,0x9d,0xe6,0x09,0x39,0xd1,0xe4,0xc7,0xf1);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfileAttributes,0xF47551D7,0xAD22,0x47AF,0xBC,0xFD,0x6B,0xE8,0x8A,0xA8,0x79,0xD9);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfileIterator,0x29E5A8C0,0x8BE4,0x46EB,0x93,0xAC,0x31,0xDA,0xAB,0x5B,0x7B,0xF2);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfile,0x16093466,0x674A,0x432B,0x9D,0xA0,0x1A,0xC2,0xC5,0xA8,0x24,0x1C);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfileCallback,0xA4F9341E,0x97AA,0x4E04,0x89,0x35,0x15,0xF8,0x09,0x89,0x8C,0xEA);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfileManager,0x30D41429,0x3998,0x4B6D,0x84,0xF8,0x78,0xC9,0x4A,0x79,0x7C,0x6E);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkStatistics,0x21CB2ED1,0x4429,0x42BE,0xAA,0xF3,0x22,0xA3,0xB1,0xDD,0x3A,0xE0);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkStatus,0x2A04A635,0xED42,0x41EF,0x93,0x42,0x0E,0x11,0xF8,0xCF,0x6B,0x5E);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkKeyer,0x89AFCAF5,0x65F8,0x421E,0x98,0xF7,0x96,0xFE,0x5F,0x5B,0xFB,0xA3);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoConversion,0x94C536D6,0xC821,0x42F5,0xA6,0x00,0xC6,0x66,0x29,0x95,0x51,0x01);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDeviceNotificationCallback,0x4997053B,0x0ADF,0x4CC8,0xAC,0x70,0x7A,0x50,0xC4,0xBE,0x72,0x8F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDiscovery,0xCDBF631C,0xBC76,0x45FA,0xB4,0x4D,0xC5,0x50,0x59,0xBC,0x61,0x01);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkIterator,0xBA6C6F44,0x6DA5,0x4DCE,0x94,0xAA,0xEE,0x2D,0x13,0x72,0xA6,0x76);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkAPIInformation,0x263CA19F,0xED09,0x482E,0x9F,0x9D,0x84,0x00,0x57,0x83,0xA2,0x37);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkGLScreenPreviewHelper,0x1E332DAE,0x0D04,0x49EB,0xB8,0xA1,0xB6,0xE0,0x0B,0x2B,0x6B,0xD0);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkGL3ScreenPreviewHelper,0x166804E4,0x15EF,0x4BFD,0xB6,0x23,0xB5,0xBA,0x92,0x16,0x67,0xC5);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkDX9ScreenPreviewHelper,0x0EB111ED,0xADA6,0x43A6,0x8B,0x16,0xCA,0x5D,0x27,0xEE,0xA1,0x5E);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkWPFDX9ScreenPreviewHelper,0x5E64496D,0x4BB2,0x45D5,0x9B,0x63,0xBF,0x1B,0x46,0x3B,0x18,0xAF);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkVideoConversion,0x771AD62D,0x671F,0x4442,0xAC,0x90,0xB0,0x70,0xC5,0x41,0x09,0x0A);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkDiscovery,0x22FBFC33,0x8D07,0x495C,0xA5,0xBF,0xDA,0xB5,0xEA,0x9B,0x82,0xDB);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkVideoFrameAncillaryPackets,0x6F47097E,0xB390,0x4650,0xBC,0xB6,0xC4,0xD5,0x2F,0xAA,0x16,0x43);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkStatus_v15_3_1,0x5F558200,0x4028,0x49BC,0xBE,0xAC,0xDB,0x3F,0xA4,0xA9,0x6E,0x46);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration_v15_3_1,0x912F634B,0x2D4E,0x40A4,0x8A,0xAB,0x8D,0x80,0xB7,0x3F,0x12,0x89);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBuffer_v15_3_1,0xCCB4B64A,0x5C86,0x4E02,0xB7,0x78,0x88,0x5D,0x35,0x27,0x09,0xFE);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBufferAllocator_v15_3_1,0x3481A4DF,0x2B11,0x4E55,0xAC,0x61,0x83,0x6B,0x87,0x98,0x5E,0x9A);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoBufferAllocatorProvider_v15_3_1,0x08B80403,0xBFF2,0x49D0,0xB4,0x48,0x8C,0x90,0x8B,0x9E,0x9F,0xC9);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput_v15_3_1,0x4095DB82,0xE294,0x4B8C,0xAA,0xA8,0x3B,0x9E,0x80,0xC4,0x93,0x36);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkOutput_v15_3_1,0x1A8077F1,0x9FE2,0x4533,0x81,0x47,0x22,0x94,0x30,0x5E,0x25,0x3F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoConversion_v15_3_1,0xA48755D9,0x8BD5,0x4727,0xA1,0xE9,0x06,0x9F,0xDE,0xDB,0xA6,0xE9);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkNotification_v15_3_1,0xB85DF4C8,0xBDF5,0x47C1,0x80,0x64,0x28,0x16,0x2E,0xBD,0xD4,0xEB);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkVideoConversion_v15_3_1,0x89BA47BD,0x1FE2,0x4D76,0x9B,0xFE,0xDE,0x85,0x04,0x9C,0x49,0x87);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkProfileAttributes_v15_3_1,0x17D4BF8E,0x4911,0x473A,0x80,0xA0,0x73,0x1C,0xF6,0xFF,0x34,0x5B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoOutputCallback_v14_2_1,0x20AA5225,0x1958,0x47CB,0x82,0x0B,0x80,0xA8,0xD5,0x21,0xA6,0xEE);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInputCallback_v14_2_1,0xC6FCE4C9,0xC4E4,0x4047,0x82,0xFB,0x5D,0x23,0x82,0x32,0xA9,0x02);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkMemoryAllocator_v14_2_1,0xB36EB6E7,0x9D29,0x4AA8,0x92,0xEF,0x84,0x3B,0x87,0xA2,0x89,0xE8);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkOutput_v14_2_1,0xBE2D9020,0x461E,0x442F,0x84,0xB7,0xE9,0x49,0xCB,0x95,0x3B,0x9D);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput_v14_2_1,0xC21CDB6E,0xF414,0x46E4,0xA6,0x36,0x80,0xA5,0x66,0xE0,0xED,0x37);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderInput_v14_2_1,0xF222551D,0x13DF,0x4FD8,0xB5,0x87,0x9D,0x4F,0x19,0xEC,0x12,0xC9);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrame_v14_2_1,0x3F716FE0,0xF023,0x4111,0xBE,0x5D,0xEF,0x44,0x14,0xC0,0x5B,0x17);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkMutableVideoFrame_v14_2_1,0x69E2639F,0x40DA,0x4E19,0xB6,0xF2,0x20,0xAC,0xE8,0x15,0xC3,0x90);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrame3DExtensions_v14_2_1,0xDA0F7E4A,0xEDC7,0x48A8,0x9C,0xDD,0x2D,0xB5,0x1C,0x72,0x9C,0xD7);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoInputFrame_v14_2_1,0x05CFE374,0x537C,0x4094,0x9A,0x57,0x68,0x05,0x25,0x11,0x8F,0x44);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkScreenPreviewCallback_v14_2_1,0xB1D3F49A,0x85FE,0x4C5D,0x95,0xC8,0x0B,0x5D,0x5D,0xCC,0xD4,0x38);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkGLScreenPreviewHelper_v14_2_1,0x504E2209,0xCAC7,0x4C1A,0x9F,0xB4,0xC5,0xBB,0x62,0x74,0xD2,0x2F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkDX9ScreenPreviewHelper_v14_2_1,0x2094B522,0xD1A1,0x40C0,0x9A,0xC7,0x1C,0x01,0x22,0x18,0xEF,0x02);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkWPFDX9ScreenPreviewHelper_v14_2_1,0xAD8EC84A,0x7DDE,0x11E9,0x8F,0x9E,0x2A,0x86,0xE4,0x08,0x5A,0x59);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoConversion_v14_2_1,0x3BBCB8A2,0xDA2C,0x42D9,0xB5,0xD8,0x88,0x08,0x36,0x44,0xE9,0x9A);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkGLScreenPreviewHelper_v14_2_1,0xF63E77C7,0xB655,0x4A4A,0x9A,0xD0,0x3C,0xA8,0x5D,0x39,0x43,0x43);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkGL3ScreenPreviewHelper_v14_2_1,0x00696A71,0xEBC7,0x491F,0xAC,0x02,0x18,0xD3,0x39,0x3F,0x33,0xF0);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkDX9ScreenPreviewHelper_v14_2_1,0xCC010023,0xE01D,0x4525,0x9D,0x59,0x80,0xC8,0xAB,0x3D,0xC7,0xA0);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkWPFDX9ScreenPreviewHelper_v14_2_1,0xEF2A8478,0x7DDF,0x11E9,0x8F,0x9E,0x2A,0x86,0xE4,0x08,0x5A,0x59);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkVideoConversion_v14_2_1,0x7DBBBB11,0x5B7B,0x467D,0xAE,0xA4,0xCE,0xA4,0x68,0xFD,0x36,0x8C);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInputCallback_v11_5_1,0xDD04E5EC,0x7415,0x42AB,0xAE,0x4A,0xE8,0x0C,0x4D,0xFC,0x04,0x4A);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput_v11_5_1,0x9434C6E4,0xB15D,0x4B1C,0x97,0x9E,0x66,0x1E,0x3D,0xDC,0xB4,0xB9);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration_v10_11,0xEF90380B,0x4AE5,0x4346,0x90,0x77,0xE2,0x88,0xE1,0x49,0xF1,0x29);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAttributes_v10_11,0xABC11843,0xD966,0x44CB,0x96,0xE2,0xA1,0xCB,0x5D,0x31,0x35,0xC4);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkNotification_v10_11,0x0A1FB207,0xE215,0x441B,0x9B,0x19,0x6F,0xA1,0x57,0x59,0x46,0xC5);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkOutput_v10_11,0xCC5C8A6E,0x3F2F,0x4B3A,0x87,0xEA,0xFD,0x78,0xAF,0x30,0x05,0x64);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput_v10_11,0xAF22762B,0xDFAC,0x4846,0xAA,0x79,0xFA,0x88,0x83,0x56,0x09,0x95);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderInput_v10_11,0x270587DA,0x6B7D,0x42E7,0xA1,0xF0,0x6D,0x85,0x3F,0x58,0x11,0x85);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkIterator_v10_11,0x87D2693F,0x8D4A,0x45C7,0xB4,0x3F,0x10,0xAC,0xBA,0x25,0xE6,0x8F);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkDiscovery_v10_11,0x652615D4,0x26CD,0x4514,0xB1,0x61,0x2F,0xD5,0x07,0x2E,0xD0,0x08);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration_v10_9,0xCB71734A,0xFE37,0x4E8D,0x8E,0x13,0x80,0x21,0x33,0xA1,0xC3,0xF2);
MIDL_DEFINE_GUID(CLSID, CLSID_CBMDStreamingDiscovery_v10_8,0x0CAA31F6,0x8A26,0x40B0,0x86,0xA4,0xBF,0x58,0xDC,0xCA,0x71,0x0C);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration_v10_4,0x1E69FCF6,0x4203,0x4936,0x80,0x76,0x2A,0x9F,0x4C,0xFD,0x50,0xCB);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkConfiguration_v10_2,0xC679A35B,0x610C,0x4D09,0xB7,0x48,0x1D,0x04,0x78,0x10,0x0F,0xC0);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAncillaryPacket_v15_2,0xCC5BBF7E,0x029C,0x4D3B,0x91,0x58,0x60,0x00,0xEF,0x5E,0x36,0x70);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkAncillaryPacketIterator_v15_2,0x3FC8994B,0x88FB,0x4C17,0x96,0x8F,0x9A,0xAB,0x69,0xD9,0x64,0xA7);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameAncillaryPackets_v15_2,0x6C186C0F,0x459E,0x41D8,0xAE,0xE2,0x48,0x12,0xD8,0x1A,0xEE,0x68);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkVideoFrameAncillaryPackets_v15_2,0xF891AD29,0xD0C2,0x46E9,0xA9,0x26,0x4E,0x2D,0x0D,0xD8,0xCF,0xAD);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkVideoFrameMetadataExtensions_v11_5,0xD5973DC9,0x6432,0x46D0,0x8F,0x0B,0x24,0x96,0xF8,0xA1,0x23,0x8F);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkOutput_v11_4,0x065A0F6C,0xC508,0x4D0D,0xB9,0x19,0xF5,0xEB,0x0E,0xBF,0xC9,0x6B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkInput_v11_4,0x2A88CF76,0xF494,0x4216,0xA7,0xEF,0xDC,0x74,0xEE,0xB8,0x38,0x82);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkIterator_v10_8,0x1F2E109A,0x8F4F,0x49E4,0x92,0x03,0x13,0x55,0x95,0xCB,0x6F,0xA5);
MIDL_DEFINE_GUID(CLSID, CLSID_CDeckLinkDiscovery_v10_8,0x1073A05C,0xD885,0x47E9,0xB3,0xC6,0x12,0x9B,0x3F,0x9F,0x64,0x8B);
MIDL_DEFINE_GUID(IID, IID_IDeckLinkEncoderConfiguration_v10_5,0x67455668,0x0848,0x45DF,0x8D,0x8E,0x35,0x0A,0x77,0xC9,0xA0,0x28);
#undef MIDL_DEFINE_GUID
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,146 @@
#include "DeckLinkDisplayMode.h"
#include <cctype>
std::string NormalizeModeToken(const std::string& value)
{
std::string normalized;
for (unsigned char ch : value)
{
if (std::isalnum(ch))
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
return normalized;
}
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName)
{
VideoFormat videoMode;
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode))
return false;
displayMode = videoMode.displayMode;
displayModeName = videoMode.displayName;
return true;
}
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
{
const std::string formatToken = NormalizeModeToken(videoFormat);
const std::string frameToken = NormalizeModeToken(frameRate);
const std::string combinedToken = formatToken + frameToken;
struct ModeOption
{
const char* token;
BMDDisplayMode mode;
const char* displayName;
};
static const ModeOption options[] =
{
{ "720p50", bmdModeHD720p50, "720p50" },
{ "hd720p50", bmdModeHD720p50, "720p50" },
{ "720p5994", bmdModeHD720p5994, "720p59.94" },
{ "hd720p5994", bmdModeHD720p5994, "720p59.94" },
{ "720p60", bmdModeHD720p60, "720p60" },
{ "hd720p60", bmdModeHD720p60, "720p60" },
{ "1080i50", bmdModeHD1080i50, "1080i50" },
{ "hd1080i50", bmdModeHD1080i50, "1080i50" },
{ "1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "hd1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "1080i60", bmdModeHD1080i6000, "1080i60" },
{ "hd1080i60", bmdModeHD1080i6000, "1080i60" },
{ "1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "hd1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "1080p24", bmdModeHD1080p24, "1080p24" },
{ "hd1080p24", bmdModeHD1080p24, "1080p24" },
{ "1080p25", bmdModeHD1080p25, "1080p25" },
{ "hd1080p25", bmdModeHD1080p25, "1080p25" },
{ "1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "hd1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "1080p30", bmdModeHD1080p30, "1080p30" },
{ "hd1080p30", bmdModeHD1080p30, "1080p30" },
{ "1080p50", bmdModeHD1080p50, "1080p50" },
{ "hd1080p50", bmdModeHD1080p50, "1080p50" },
{ "1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "hd1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "1080p60", bmdModeHD1080p6000, "1080p60" },
{ "hd1080p60", bmdModeHD1080p6000, "1080p60" },
{ "2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "4k2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "2160p24", bmdMode4K2160p24, "2160p24" },
{ "4k2160p24", bmdMode4K2160p24, "2160p24" },
{ "2160p25", bmdMode4K2160p25, "2160p25" },
{ "4k2160p25", bmdMode4K2160p25, "2160p25" },
{ "2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "4k2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "2160p30", bmdMode4K2160p30, "2160p30" },
{ "4k2160p30", bmdMode4K2160p30, "2160p30" },
{ "2160p50", bmdMode4K2160p50, "2160p50" },
{ "4k2160p50", bmdMode4K2160p50, "2160p50" },
{ "2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "4k2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "2160p60", bmdMode4K2160p60, "2160p60" },
{ "4k2160p60", bmdMode4K2160p60, "2160p60" }
};
for (const ModeOption& option : options)
{
if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token))
{
videoMode.displayMode = option.mode;
videoMode.displayName = option.displayName;
return true;
}
}
return false;
}
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error)
{
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
{
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
inputVideoFormat + " / " + inputFrameRate;
return false;
}
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
{
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
outputVideoFormat + " / " + outputFrameRate;
return false;
}
return true;
}
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
{
if (!iterator || !foundMode)
return false;
*foundMode = NULL;
IDeckLinkDisplayMode* candidate = NULL;
while (iterator->Next(&candidate) == S_OK)
{
if (candidate->GetDisplayMode() == targetMode)
{
*foundMode = candidate;
return true;
}
candidate->Release();
candidate = NULL;
}
return false;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include <string>
struct FrameSize
{
unsigned width = 0;
unsigned height = 0;
bool IsEmpty() const { return width == 0 || height == 0; }
};
inline bool operator==(const FrameSize& left, const FrameSize& right)
{
return left.width == right.width && left.height == right.height;
}
inline bool operator!=(const FrameSize& left, const FrameSize& right)
{
return !(left == right);
}
struct VideoFormat
{
BMDDisplayMode displayMode = bmdModeHD1080p5994;
std::string displayName = "1080p59.94";
};
struct VideoFormatSelection
{
VideoFormat input;
VideoFormat output;
};
std::string NormalizeModeToken(const std::string& value);
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName);
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode);
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error);
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);

View File

@@ -0,0 +1,100 @@
#include "DeckLinkFrameTransfer.h"
#include "DeckLinkSession.h"
////////////////////////////////////////////
// DeckLink Capture Delegate Class
////////////////////////////////////////////
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner),
mRefCount(1)
{
}
HRESULT CaptureDelegate::QueryInterface(REFIID, LPVOID* ppv)
{
*ppv = NULL;
return E_NOINTERFACE;
}
ULONG CaptureDelegate::AddRef()
{
return InterlockedIncrement(&mRefCount);
}
ULONG CaptureDelegate::Release()
{
int newCount = InterlockedDecrement(&mRefCount);
if (newCount == 0)
delete this;
return newCount;
}
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket*)
{
if (!inputFrame)
{
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
return S_OK;
}
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
return S_OK;
}
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
{
return S_OK;
}
////////////////////////////////////////////
// DeckLink Playout Delegate Class
////////////////////////////////////////////
PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner),
mRefCount(1)
{
}
HRESULT PlayoutDelegate::QueryInterface(REFIID, LPVOID* ppv)
{
*ppv = NULL;
return E_NOINTERFACE;
}
ULONG PlayoutDelegate::AddRef()
{
return InterlockedIncrement(&mRefCount);
}
ULONG PlayoutDelegate::Release()
{
int newCount = InterlockedDecrement(&mRefCount);
if (newCount == 0)
delete this;
return newCount;
}
HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
{
switch (result)
{
case bmdOutputFrameDisplayedLate:
case bmdOutputFrameDropped:
case bmdOutputFrameCompleted:
case bmdOutputFrameFlushed:
// Late/drop counts are recorded by VideoBackend; keep this callback lean.
break;
default:
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
}
m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
return S_OK;
}
HRESULT PlayoutDelegate::ScheduledPlaybackHasStopped()
{
return S_OK;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <windows.h>
#include <atomic>
#include "DeckLinkAPI_h.h"
class DeckLinkSession;
////////////////////////////////////////////
// Capture Delegate Class
////////////////////////////////////////////
class CaptureDelegate : public IDeckLinkInputCallback
{
DeckLinkSession* m_pOwner;
LONG mRefCount;
public:
CaptureDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket);
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
};
////////////////////////////////////////////
// Render Delegate Class
////////////////////////////////////////////
class PlayoutDelegate : public IDeckLinkVideoOutputCallback
{
DeckLinkSession* m_pOwner;
LONG mRefCount;
public:
PlayoutDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped();
};

View File

@@ -0,0 +1,933 @@
#include "DeckLinkSession.h"
#include <atlbase.h>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <new>
#include <sstream>
#include <utility>
#include <vector>
namespace
{
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
{
public:
SystemMemoryDeckLinkVideoBuffer(void* bytes, unsigned long long sizeBytes) :
mBytes(bytes),
mSizeBytes(sizeBytes),
mRefCount(1)
{
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override
{
if (ppv == nullptr)
return E_POINTER;
if (iid == IID_IUnknown || iid == IID_IDeckLinkVideoBuffer)
{
*ppv = static_cast<IDeckLinkVideoBuffer*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override
{
return ++mRefCount;
}
ULONG STDMETHODCALLTYPE Release() override
{
const ULONG refCount = --mRefCount;
if (refCount == 0)
delete this;
return refCount;
}
HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override
{
if (buffer == nullptr)
return E_POINTER;
*buffer = mBytes;
return mBytes != nullptr ? S_OK : E_FAIL;
}
HRESULT STDMETHODCALLTYPE GetSize(unsigned long long* bufferSize) override
{
if (bufferSize == nullptr)
return E_POINTER;
*bufferSize = mSizeBytes;
return S_OK;
}
HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags) override
{
return S_OK;
}
HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags) override
{
return S_OK;
}
private:
void* mBytes = nullptr;
unsigned long long mSizeBytes = 0;
std::atomic<ULONG> mRefCount;
};
std::string BstrToUtf8(BSTR value)
{
if (value == nullptr)
return std::string();
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
if (requiredBytes <= 1)
return std::string();
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
return std::string();
return std::string(utf8Name.data());
}
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (input == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = input->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoInputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (output == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
}
DeckLinkSession::~DeckLinkSession()
{
ReleaseResources();
}
void DeckLinkSession::ReleaseResources()
{
if (input != nullptr)
input->SetCallback(nullptr);
captureDelegate.Release();
input.Release();
if (output != nullptr)
output->SetScheduledFrameCompletionCallback(nullptr);
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
keyer.Release();
playoutDelegate.Release();
outputVideoFrameQueue.clear();
output.Release();
}
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
CComPtr<IDeckLinkIterator> deckLinkIterator;
CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode;
mState.inputDisplayModeName = videoModes.input.displayName;
mState.outputDisplayModeName = videoModes.output.displayName;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
if (FAILED(result))
{
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
return false;
}
CComPtr<IDeckLink> deckLink;
while (deckLinkIterator->Next(&deckLink) == S_OK)
{
int64_t duplexMode;
bool deviceSupportsInternalKeying = false;
bool deviceSupportsExternalKeying = false;
std::string modelName;
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
{
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
deckLink.Release();
continue;
}
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
BOOL attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
deviceSupportsInternalKeying = (attributeFlag != FALSE);
attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
deviceSupportsExternalKeying = (attributeFlag != FALSE);
CComBSTR modelNameBstr;
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
modelName = BstrToUtf8(modelNameBstr);
if (result != S_OK || duplexMode == bmdDuplexInactive)
{
deckLink.Release();
continue;
}
bool inputUsed = false;
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
inputUsed = true;
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
{
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
output.Release();
else
{
mState.outputModelName = modelName;
mState.supportsInternalKeying = deviceSupportsInternalKeying;
mState.supportsExternalKeying = deviceSupportsExternalKeying;
}
}
deckLink.Release();
if (output && input)
break;
}
if (!output)
{
error = "Expected an Output DeckLink device";
ReleaseResources();
return false;
}
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
{
error = "Cannot get input Display Mode Iterator.";
ReleaseResources();
return false;
}
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
{
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
ReleaseResources();
return false;
}
inputDisplayModeIterator.Release();
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
{
error = "Cannot get output Display Mode Iterator.";
ReleaseResources();
return false;
}
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
{
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
ReleaseResources();
return false;
}
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
mState.inputFrameSize = inputMode
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
: mState.outputFrameSize;
if (!input)
mState.inputDisplayModeName = "No input - black frame";
BMDTimeValue frameDuration = 0;
BMDTimeScale frameTimescale = 0;
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
mScheduler.Configure(frameDuration, frameTimescale, mPlayoutPolicy);
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
mState.outputPackTextureWidth = mState.outputFrameSize.width;
mState.hasInputDevice = input != nullptr;
mState.hasInputSource = false;
return true;
}
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
if (!output)
{
error = "Expected an Output DeckLink device";
return false;
}
mState.formatStatusMessage.clear();
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
if (input != nullptr && !inputTenBitSupported)
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
mState.outputPixelFormat = outputAlphaRequired
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
if (outputAlphaRequired && outputTenBitYuvaSupported)
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
else if (outputAlphaRequired)
mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
else if (!outputTenBitSupported)
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
int deckLinkOutputRowBytes = 0;
if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
{
error = "DeckLink output setup failed while calculating output row bytes.";
return false;
}
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
mState.outputPackTextureWidth = OutputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
: mState.outputFrameSize.width;
if (InputIsTenBit())
{
int deckLinkInputRowBytes = 0;
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
else
mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
}
else
{
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
}
mState.captureTextureWidth = InputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
: mState.inputFrameSize.width / 2u;
std::ostringstream status;
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
if (!mState.formatStatusMessage.empty())
status << " " << mState.formatStatusMessage;
mState.formatStatusMessage = status.str();
return true;
}
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
{
mInputFrameCallback = std::move(callback);
if (!input)
{
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
{
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
{
std::ostringstream status;
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
mState.formatStatusMessage = status.str();
goto input_enabled;
}
}
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
input.Release();
mState.hasInputDevice = false;
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
input_enabled:
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
if (captureDelegate == nullptr)
{
error = "DeckLink input setup failed while creating the capture callback.";
return false;
}
if (input->SetCallback(captureDelegate) != S_OK)
{
error = "DeckLink input setup failed while installing the capture callback.";
return false;
}
return true;
}
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
mOutputFrameCallback = std::move(callback);
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
{
error = "DeckLink output setup failed while enabling video output.";
return false;
}
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
mState.keyerInterfaceAvailable = true;
if (externalKeyingEnabled)
{
if (!mState.supportsExternalKeying)
{
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
}
else if (!mState.keyerInterfaceAvailable)
{
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
}
else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
{
mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
}
else
{
mState.externalKeyingActive = true;
mState.statusMessage = "External keying is active on the selected DeckLink output.";
}
}
else if (mState.supportsExternalKeying)
{
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
}
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
mPlayoutPolicy = policy;
for (unsigned i = 0; i < policy.outputFramePoolSize; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
if (output->CreateVideoFrame(mState.outputFrameSize.width, mState.outputFrameSize.height, mState.outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
{
error = "DeckLink output setup failed while creating an output video frame.";
return false;
}
outputVideoFrameQueue.push_back(outputFrame);
}
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
if (playoutDelegate == nullptr)
{
error = "DeckLink output setup failed while creating the playout callback.";
return false;
}
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
{
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
return false;
}
if (!mState.formatStatusMessage.empty())
mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
return true;
}
double DeckLinkSession::FrameBudgetMilliseconds() const
{
return mScheduler.FrameBudgetMilliseconds();
}
bool DeckLinkSession::AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame)
{
if (outputVideoFrameQueue.empty())
return false;
outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.pop_front();
return outputVideoFrame != nullptr;
}
bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame)
{
if (outputVideoFrame == nullptr)
return false;
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
return false;
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
return false;
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes(&pFrame);
frame.bytes = pFrame;
frame.rowBytes = outputVideoFrame->GetRowBytes();
frame.width = mState.outputFrameSize.width;
frame.height = mState.outputFrameSize.height;
frame.pixelFormat = mState.outputPixelFormat;
outputVideoFrame->AddRef();
frame.nativeFrame = outputVideoFrame;
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
return true;
}
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
if (outputVideoFrame == nullptr || output == nullptr)
{
++mState.deckLinkScheduleFailureCount;
return false;
}
if (mScheduleRealignmentPending)
{
RealignScheduleCursorToPlayback();
mScheduleRealignmentPending = false;
}
UpdateScheduleLeadTelemetry();
MaybeRealignScheduleCursorForLowLead();
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
const auto scheduleStart = std::chrono::steady_clock::now();
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
const auto scheduleEnd = std::chrono::steady_clock::now();
mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
if (result != S_OK)
++mState.deckLinkScheduleFailureCount;
RefreshBufferedVideoFrameCount();
return result == S_OK;
}
void DeckLinkSession::UpdateScheduleLeadTelemetry()
{
if (output == nullptr)
{
mState.deckLinkScheduleLeadAvailable = false;
return;
}
BMDTimeValue streamTime = 0;
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
{
mState.deckLinkScheduleLeadAvailable = false;
return;
}
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
: 0;
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
mState.deckLinkScheduleLeadAvailable = true;
mState.deckLinkPlaybackStreamTime = streamTime;
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
}
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
{
if (!mState.deckLinkScheduleLeadAvailable)
return;
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
{
mProactiveScheduleRealignmentArmed = true;
return;
}
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
return;
RealignScheduleCursorToPlayback();
mProactiveScheduleRealignmentArmed = false;
}
void DeckLinkSession::RealignScheduleCursorToPlayback()
{
if (output == nullptr)
return;
BMDTimeValue streamTime = 0;
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
return;
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
++mState.deckLinkScheduleRealignmentCount;
UpdateScheduleLeadTelemetry();
}
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
{
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
return false;
CComPtr<IDeckLinkVideoBuffer> videoBuffer;
videoBuffer.Attach(new (std::nothrow) SystemMemoryDeckLinkVideoBuffer(
frame.bytes,
static_cast<unsigned long long>(frame.rowBytes) * static_cast<unsigned long long>(frame.height)));
if (videoBuffer == nullptr)
return false;
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
const BMDPixelFormat pixelFormat = DeckLinkPixelFormatForVideoIO(frame.pixelFormat);
if (output->CreateVideoFrameWithBuffer(
frame.width,
frame.height,
frame.rowBytes,
pixelFormat,
bmdFrameFlagFlipVertical,
videoBuffer,
&outputVideoFrame) != S_OK)
{
return false;
}
IDeckLinkVideoFrame* scheduledFrame = outputVideoFrame;
{
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
mScheduledSystemFrameBuffers[scheduledFrame] = frame.bytes;
}
if (ScheduleFrame(outputVideoFrame))
return true;
{
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
mScheduledSystemFrameBuffers.erase(scheduledFrame);
}
return false;
}
bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
if (outputVideoFrame == nullptr)
return false;
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
return false;
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
return false;
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
return ScheduleFrame(outputVideoFrame);
}
void DeckLinkSession::RefreshBufferedVideoFrameCount()
{
if (output == nullptr)
{
mState.actualDeckLinkBufferedFramesAvailable = false;
return;
}
unsigned int bufferedFrameCount = 0;
if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
{
mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
mState.actualDeckLinkBufferedFramesAvailable = true;
}
else
{
mState.actualDeckLinkBufferedFramesAvailable = false;
}
}
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
return AcquireNextOutputVideoFrame(outputVideoFrame) && PopulateOutputFrame(outputVideoFrame, frame);
}
void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
{
IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
if (outputVideoFrameBuffer != nullptr)
{
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
outputVideoFrameBuffer->Release();
}
frame.nativeBuffer = nullptr;
frame.bytes = nullptr;
}
VideoPlayoutRecoveryDecision DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth)
{
return mScheduler.AccountForCompletionResult(completionResult, readyQueueDepth);
}
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
if (frame.nativeFrame == nullptr)
return ScheduleSystemMemoryFrame(frame);
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
const bool scheduled = ScheduleFrame(outputVideoFrame);
if (outputVideoFrame != nullptr)
outputVideoFrame->Release();
return scheduled;
}
bool DeckLinkSession::PrepareOutputSchedule()
{
mScheduler.Reset();
RefreshBufferedVideoFrameCount();
return output != nullptr;
}
bool DeckLinkSession::StartInputStreams()
{
if (!input)
return true;
if (input->StartStreams() != S_OK)
{
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
bool DeckLinkSession::StartScheduledPlayback()
{
if (!output)
{
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
{
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
RefreshBufferedVideoFrameCount();
return true;
}
bool DeckLinkSession::Start()
{
if (!output)
{
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameQueue.empty())
{
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
mPlayoutPolicy = policy;
if (!PrepareOutputSchedule())
return false;
for (unsigned i = 0; i < policy.targetPrerollFrames; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
{
MessageBoxA(NULL, "Could not acquire a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (!ScheduleBlackFrame(outputVideoFrame))
{
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
}
return StartInputStreams() && StartScheduledPlayback();
}
bool DeckLinkSession::Stop()
{
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
if (input)
{
input->StopStreams();
input->DisableVideoInput();
}
if (output)
{
output->StopScheduledPlayback(0, NULL, 0);
output->DisableVideoOutput();
}
return true;
}
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
{
mState.hasInputSource = !hasNoInputSource;
if (hasNoInputSource || mInputFrameCallback == nullptr)
{
VideoIOFrame frame;
frame.width = mState.inputFrameSize.width;
frame.height = mState.inputFrameSize.height;
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
if (mInputFrameCallback)
mInputFrameCallback(frame);
return;
}
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
void* videoPixels = nullptr;
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
return;
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
return;
inputFrameBuffer->GetBytes(&videoPixels);
VideoIOFrame frame;
frame.bytes = videoPixels;
frame.rowBytes = inputFrame->GetRowBytes();
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
mInputFrameCallback(frame);
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
}
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
{
RefreshBufferedVideoFrameCount();
void* completedSystemBuffer = nullptr;
if (completedFrame != nullptr)
{
bool externalSystemFrame = false;
{
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
auto externalFrame = mScheduledSystemFrameBuffers.find(completedFrame);
if (externalFrame != mScheduledSystemFrameBuffers.end())
{
completedSystemBuffer = externalFrame->second;
mScheduledSystemFrameBuffers.erase(externalFrame);
externalSystemFrame = true;
}
}
if (!externalSystemFrame)
{
CComPtr<IDeckLinkMutableVideoFrame> reusableFrame;
if (completedFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, reinterpret_cast<void**>(&reusableFrame)) == S_OK &&
reusableFrame != nullptr)
{
outputVideoFrameQueue.push_back(reusableFrame);
}
}
}
if (!mOutputFrameCallback)
return;
VideoIOCompletion completion;
completion.result = TranslateCompletionResult(completionResult);
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
{
if (mScheduleRealignmentArmed)
{
mScheduleRealignmentPending = true;
mScheduleRealignmentArmed = false;
}
}
else if (completion.result == VideoIOCompletionResult::Completed)
{
mScheduleRealignmentArmed = true;
}
completion.outputFrameBuffer = completedSystemBuffer;
mOutputFrameCallback(completion);
}
VideoIOCompletionResult DeckLinkSession::TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult)
{
switch (completionResult)
{
case bmdOutputFrameDisplayedLate:
return VideoIOCompletionResult::DisplayedLate;
case bmdOutputFrameDropped:
return VideoIOCompletionResult::Dropped;
case bmdOutputFrameFlushed:
return VideoIOCompletionResult::Flushed;
case bmdOutputFrameCompleted:
return VideoIOCompletionResult::Completed;
default:
return VideoIOCompletionResult::Unknown;
}
}

102
src/video/DeckLinkSession.h Normal file
View File

@@ -0,0 +1,102 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "DeckLinkDisplayMode.h"
#include "DeckLinkFrameTransfer.h"
#include "DeckLinkVideoIOFormat.h"
#include "VideoIOFormat.h"
#include "VideoIOTypes.h"
#include "VideoPlayoutPolicy.h"
#include "VideoPlayoutScheduler.h"
#include <atlbase.h>
#include <deque>
#include <mutex>
#include <string>
#include <unordered_map>
class OpenGLComposite;
class DeckLinkSession : public VideoIODevice
{
public:
DeckLinkSession() = default;
~DeckLinkSession();
void ReleaseResources() override;
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
bool PrepareOutputSchedule() override;
bool StartInputStreams() override;
bool StartScheduledPlayback() override;
bool Start() override;
bool Stop() override;
bool HasInputDevice() const { return mState.hasInputDevice; }
bool HasInputSource() const { return mState.hasInputSource; }
void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
const std::string& OutputModelName() const { return mState.outputModelName; }
bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
const std::string& StatusMessage() const { return mState.statusMessage; }
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
const VideoIOState& State() const override { return mState; }
VideoIOState& MutableState() override { return mState; }
double FrameBudgetMilliseconds() const;
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth) override;
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
void EndOutputFrame(VideoIOOutputFrame& frame) override;
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
private:
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
void UpdateScheduleLeadTelemetry();
void MaybeRealignScheduleCursorForLowLead();
void RealignScheduleCursorToPlayback();
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
void RefreshBufferedVideoFrameCount();
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
CComPtr<CaptureDelegate> captureDelegate;
CComPtr<PlayoutDelegate> playoutDelegate;
CComPtr<IDeckLinkInput> input;
CComPtr<IDeckLinkOutput> output;
CComPtr<IDeckLinkKeyer> keyer;
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
std::mutex mScheduledSystemFrameMutex;
std::unordered_map<IDeckLinkVideoFrame*, void*> mScheduledSystemFrameBuffers;
VideoIOState mState;
VideoPlayoutPolicy mPlayoutPolicy;
VideoPlayoutScheduler mScheduler;
bool mScheduleRealignmentPending = false;
bool mScheduleRealignmentArmed = true;
bool mProactiveScheduleRealignmentArmed = true;
InputFrameCallback mInputFrameCallback;
OutputFrameCallback mOutputFrameCallback;
};

View File

@@ -0,0 +1,28 @@
#include "DeckLinkVideoIOFormat.h"
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
{
switch (format)
{
case VideoIOPixelFormat::V210:
return bmdFormat10BitYUV;
case VideoIOPixelFormat::Yuva10:
return bmdFormat10BitYUVA;
case VideoIOPixelFormat::Bgra8:
return bmdFormat8BitBGRA;
case VideoIOPixelFormat::Uyvy8:
default:
return bmdFormat8BitYUV;
}
}
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
{
if (format == bmdFormat10BitYUV)
return VideoIOPixelFormat::V210;
if (format == bmdFormat10BitYUVA)
return VideoIOPixelFormat::Yuva10;
if (format == bmdFormat8BitBGRA)
return VideoIOPixelFormat::Bgra8;
return VideoIOPixelFormat::Uyvy8;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "VideoIOFormat.h"
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);

View File

@@ -0,0 +1,89 @@
#include "OutputProductionController.h"
#include <algorithm>
namespace
{
std::size_t ClampReadyLimit(unsigned value, std::size_t capacity)
{
const std::size_t requested = static_cast<std::size_t>(value);
if (capacity == 0)
return requested;
return (std::min)(requested, capacity);
}
}
OutputProductionController::OutputProductionController(const VideoPlayoutPolicy& policy) :
mPolicy(NormalizeVideoPlayoutPolicy(policy))
{
}
void OutputProductionController::Configure(const VideoPlayoutPolicy& policy)
{
mPolicy = NormalizeVideoPlayoutPolicy(policy);
}
OutputProductionDecision OutputProductionController::Decide(const OutputProductionPressure& pressure) const
{
OutputProductionDecision decision;
const std::size_t configuredMaxReadyFrames = static_cast<std::size_t>(mPolicy.maxReadyFrames);
const std::size_t effectiveMaxReadyFrames = pressure.readyQueueCapacity > 0
? (std::min)(configuredMaxReadyFrames, pressure.readyQueueCapacity)
: configuredMaxReadyFrames;
const std::size_t effectiveTargetReadyFrames = (std::min)(
ClampReadyLimit(mPolicy.targetReadyFrames, pressure.readyQueueCapacity),
effectiveMaxReadyFrames);
decision.targetReadyFrames = effectiveTargetReadyFrames;
decision.maxReadyFrames = effectiveMaxReadyFrames;
if (effectiveMaxReadyFrames == 0)
{
decision.action = OutputProductionAction::Throttle;
decision.reason = "no-ready-frame-capacity";
return decision;
}
if (pressure.readyQueueDepth >= effectiveMaxReadyFrames)
{
decision.action = OutputProductionAction::Throttle;
decision.reason = "ready-queue-full";
return decision;
}
if (pressure.readyQueueDepth < effectiveTargetReadyFrames)
{
decision.action = OutputProductionAction::Produce;
decision.requestedFrames = effectiveTargetReadyFrames - pressure.readyQueueDepth;
decision.reason = "ready-queue-below-target";
return decision;
}
if ((pressure.lateStreak > 0 || pressure.dropStreak > 0 || pressure.readyQueueUnderrunCount > 0) &&
pressure.readyQueueDepth < effectiveMaxReadyFrames)
{
decision.action = OutputProductionAction::Produce;
decision.requestedFrames = 1;
decision.reason = "playout-pressure";
return decision;
}
decision.action = OutputProductionAction::Wait;
decision.reason = "ready-queue-at-target";
return decision;
}
const char* OutputProductionActionName(OutputProductionAction action)
{
switch (action)
{
case OutputProductionAction::Produce:
return "Produce";
case OutputProductionAction::Throttle:
return "Throttle";
case OutputProductionAction::Wait:
default:
return "Wait";
}
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include "VideoPlayoutPolicy.h"
#include <cstddef>
#include <cstdint>
#include <string>
enum class OutputProductionAction
{
Produce,
Wait,
Throttle
};
struct OutputProductionPressure
{
std::size_t readyQueueDepth = 0;
std::size_t readyQueueCapacity = 0;
uint64_t readyQueueUnderrunCount = 0;
uint64_t lateStreak = 0;
uint64_t dropStreak = 0;
};
struct OutputProductionDecision
{
OutputProductionAction action = OutputProductionAction::Wait;
std::size_t requestedFrames = 0;
std::size_t targetReadyFrames = 0;
std::size_t maxReadyFrames = 0;
std::string reason;
};
class OutputProductionController
{
public:
explicit OutputProductionController(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
void Configure(const VideoPlayoutPolicy& policy);
OutputProductionDecision Decide(const OutputProductionPressure& pressure) const;
private:
VideoPlayoutPolicy mPolicy;
};
const char* OutputProductionActionName(OutputProductionAction action);

View File

@@ -0,0 +1,102 @@
#include "RenderCadenceController.h"
#include <algorithm>
#include <cmath>
void RenderCadenceController::Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy)
{
mTargetFrameDuration = IsPositive(targetFrameDuration) ? targetFrameDuration : std::chrono::milliseconds(1);
mPolicy = policy;
if (mPolicy.skipThresholdFrames < 1.0)
mPolicy.skipThresholdFrames = 1.0;
Reset(firstRenderTime);
}
void RenderCadenceController::Reset(TimePoint firstRenderTime)
{
mNextRenderTime = firstRenderTime;
mNextFrameIndex = 0;
mMetrics = RenderCadenceMetrics();
}
RenderCadenceDecision RenderCadenceController::Tick(TimePoint now)
{
RenderCadenceDecision decision;
decision.frameIndex = mNextFrameIndex;
decision.renderTargetTime = mNextRenderTime;
decision.nextRenderTime = mNextRenderTime;
if (now < mNextRenderTime)
{
decision.action = RenderCadenceAction::Wait;
decision.waitDuration = mNextRenderTime - now;
decision.reason = "waiting-for-next-render-tick";
return decision;
}
const Duration lateness = now - mNextRenderTime;
const uint64_t skippedTicks = SkippedTicksForLateness(lateness);
if (skippedTicks > 0)
{
decision.skippedTicks = skippedTicks;
decision.frameIndex = mNextFrameIndex + skippedTicks;
decision.renderTargetTime = mNextRenderTime + (mTargetFrameDuration * skippedTicks);
decision.reason = "late-skip-render-ticks";
mMetrics.skippedTickCount += skippedTicks;
}
else
{
decision.reason = IsPositive(lateness) ? "late-render-now" : "on-time-render";
}
decision.action = RenderCadenceAction::Render;
decision.lateness = now > decision.renderTargetTime
? now - decision.renderTargetTime
: Duration::zero();
mNextFrameIndex = decision.frameIndex + 1;
mNextRenderTime = decision.renderTargetTime + mTargetFrameDuration;
decision.nextRenderTime = mNextRenderTime;
++mMetrics.renderedFrameCount;
mMetrics.nextFrameIndex = mNextFrameIndex;
mMetrics.lastLateness = decision.lateness;
if (IsPositive(decision.lateness))
{
++mMetrics.lateFrameCount;
mMetrics.maxLateness = (std::max)(mMetrics.maxLateness, decision.lateness);
}
return decision;
}
uint64_t RenderCadenceController::SkippedTicksForLateness(Duration lateness) const
{
if (!mPolicy.skipLateTicks || !IsPositive(lateness) || !IsPositive(mTargetFrameDuration))
return 0;
const double lateFrames = static_cast<double>(lateness.count()) / static_cast<double>(mTargetFrameDuration.count());
if (lateFrames < mPolicy.skipThresholdFrames)
return 0;
const uint64_t elapsedTicks = static_cast<uint64_t>(std::floor(lateFrames));
if (elapsedTicks == 0)
return 0;
return (std::min)(elapsedTicks, mPolicy.maxSkippedTicksPerDecision);
}
bool RenderCadenceController::IsPositive(Duration duration)
{
return duration > Duration::zero();
}
const char* RenderCadenceActionName(RenderCadenceAction action)
{
switch (action)
{
case RenderCadenceAction::Render:
return "Render";
case RenderCadenceAction::Wait:
default:
return "Wait";
}
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include <chrono>
#include <cstdint>
enum class RenderCadenceAction
{
Wait,
Render
};
struct RenderCadencePolicy
{
bool skipLateTicks = true;
uint64_t maxSkippedTicksPerDecision = 4;
double skipThresholdFrames = 2.0;
};
struct RenderCadenceDecision
{
RenderCadenceAction action = RenderCadenceAction::Wait;
uint64_t frameIndex = 0;
uint64_t skippedTicks = 0;
std::chrono::steady_clock::time_point renderTargetTime;
std::chrono::steady_clock::time_point nextRenderTime;
std::chrono::steady_clock::duration waitDuration = std::chrono::steady_clock::duration::zero();
std::chrono::steady_clock::duration lateness = std::chrono::steady_clock::duration::zero();
const char* reason = "waiting-for-next-render-tick";
};
struct RenderCadenceMetrics
{
uint64_t nextFrameIndex = 0;
uint64_t renderedFrameCount = 0;
uint64_t skippedTickCount = 0;
uint64_t lateFrameCount = 0;
std::chrono::steady_clock::duration lastLateness = std::chrono::steady_clock::duration::zero();
std::chrono::steady_clock::duration maxLateness = std::chrono::steady_clock::duration::zero();
};
class RenderCadenceController
{
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
using Duration = Clock::duration;
void Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy = RenderCadencePolicy());
void Reset(TimePoint firstRenderTime);
RenderCadenceDecision Tick(TimePoint now);
Duration TargetFrameDuration() const { return mTargetFrameDuration; }
TimePoint NextRenderTime() const { return mNextRenderTime; }
uint64_t NextFrameIndex() const { return mNextFrameIndex; }
const RenderCadenceMetrics& Metrics() const { return mMetrics; }
private:
uint64_t SkippedTicksForLateness(Duration lateness) const;
static bool IsPositive(Duration duration);
Duration mTargetFrameDuration = std::chrono::milliseconds(16);
TimePoint mNextRenderTime;
uint64_t mNextFrameIndex = 0;
RenderCadencePolicy mPolicy;
RenderCadenceMetrics mMetrics;
};
const char* RenderCadenceActionName(RenderCadenceAction action);

View File

@@ -0,0 +1,93 @@
#include "RenderOutputQueue.h"
RenderOutputQueue::RenderOutputQueue(const VideoPlayoutPolicy& policy) :
mPolicy(NormalizeVideoPlayoutPolicy(policy))
{
}
void RenderOutputQueue::Configure(const VideoPlayoutPolicy& policy)
{
std::lock_guard<std::mutex> lock(mMutex);
mPolicy = NormalizeVideoPlayoutPolicy(policy);
while (mReadyFrames.size() > CapacityLocked())
{
ReleaseFrame(mReadyFrames.front());
mReadyFrames.pop_front();
++mDroppedCount;
}
}
bool RenderOutputQueue::Push(RenderOutputFrame frame)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mReadyFrames.size() >= CapacityLocked())
{
ReleaseFrame(mReadyFrames.front());
mReadyFrames.pop_front();
++mDroppedCount;
}
mReadyFrames.push_back(frame);
++mPushedCount;
return true;
}
bool RenderOutputQueue::TryPop(RenderOutputFrame& frame)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mReadyFrames.empty())
{
++mUnderrunCount;
return false;
}
frame = mReadyFrames.front();
mReadyFrames.pop_front();
++mPoppedCount;
return true;
}
bool RenderOutputQueue::DropOldestFrame()
{
std::lock_guard<std::mutex> lock(mMutex);
if (mReadyFrames.empty())
return false;
ReleaseFrame(mReadyFrames.front());
mReadyFrames.pop_front();
++mDroppedCount;
return true;
}
void RenderOutputQueue::Clear()
{
std::lock_guard<std::mutex> lock(mMutex);
for (RenderOutputFrame& frame : mReadyFrames)
ReleaseFrame(frame);
mReadyFrames.clear();
}
RenderOutputQueueMetrics RenderOutputQueue::GetMetrics() const
{
std::lock_guard<std::mutex> lock(mMutex);
RenderOutputQueueMetrics metrics;
metrics.depth = mReadyFrames.size();
metrics.capacity = CapacityLocked();
metrics.pushedCount = mPushedCount;
metrics.poppedCount = mPoppedCount;
metrics.droppedCount = mDroppedCount;
metrics.underrunCount = mUnderrunCount;
return metrics;
}
std::size_t RenderOutputQueue::CapacityLocked() const
{
return static_cast<std::size_t>(mPolicy.maxReadyFrames);
}
void RenderOutputQueue::ReleaseFrame(RenderOutputFrame& frame)
{
if (frame.releaseFrame)
frame.releaseFrame(frame.frame);
frame.releaseFrame = {};
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include "VideoIOTypes.h"
#include "VideoPlayoutPolicy.h"
#include <cstdint>
#include <deque>
#include <functional>
#include <mutex>
struct RenderOutputFrame
{
VideoIOOutputFrame frame;
uint64_t frameIndex = 0;
bool stale = false;
std::function<void(VideoIOOutputFrame& frame)> releaseFrame;
};
struct RenderOutputQueueMetrics
{
std::size_t depth = 0;
std::size_t capacity = 0;
uint64_t pushedCount = 0;
uint64_t poppedCount = 0;
uint64_t droppedCount = 0;
uint64_t underrunCount = 0;
};
class RenderOutputQueue
{
public:
explicit RenderOutputQueue(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
void Configure(const VideoPlayoutPolicy& policy);
bool Push(RenderOutputFrame frame);
bool TryPop(RenderOutputFrame& frame);
bool DropOldestFrame();
void Clear();
RenderOutputQueueMetrics GetMetrics() const;
private:
std::size_t CapacityLocked() const;
static void ReleaseFrame(RenderOutputFrame& frame);
mutable std::mutex mMutex;
VideoPlayoutPolicy mPolicy;
std::deque<RenderOutputFrame> mReadyFrames;
uint64_t mPushedCount = 0;
uint64_t mPoppedCount = 0;
uint64_t mDroppedCount = 0;
uint64_t mUnderrunCount = 0;
};

View File

@@ -0,0 +1,260 @@
#include "SystemOutputFramePool.h"
#include <algorithm>
namespace
{
SystemOutputFramePoolConfig NormalizeConfig(SystemOutputFramePoolConfig config)
{
if (config.rowBytes == 0)
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
return config;
}
}
SystemOutputFramePool::SystemOutputFramePool(const SystemOutputFramePoolConfig& config)
{
Configure(config);
}
void SystemOutputFramePool::Configure(const SystemOutputFramePoolConfig& config)
{
std::lock_guard<std::mutex> lock(mMutex);
mConfig = NormalizeConfig(config);
mReadySlots.clear();
mSlots.clear();
mSlots.resize(mConfig.capacity);
const std::size_t byteCount = FrameByteCount();
for (StoredSlot& slot : mSlots)
{
slot.bytes.resize(byteCount);
slot.state = OutputFrameSlotState::Free;
++slot.generation;
}
mAcquireMissCount = 0;
mReadyUnderrunCount = 0;
}
SystemOutputFramePoolConfig SystemOutputFramePool::Config() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mConfig;
}
bool SystemOutputFramePool::AcquireFreeSlot(OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
if (mSlots[index].state != OutputFrameSlotState::Free)
continue;
mSlots[index].state = OutputFrameSlotState::Rendering;
++mSlots[index].generation;
FillOutputSlotLocked(index, slot);
return true;
}
slot = OutputFrameSlot();
++mAcquireMissCount;
return false;
}
bool SystemOutputFramePool::AcquireRenderingSlot(OutputFrameSlot& slot)
{
return AcquireFreeSlot(slot);
}
bool SystemOutputFramePool::PublishReadySlot(const OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!TransitionSlotLocked(slot, OutputFrameSlotState::Rendering, OutputFrameSlotState::Completed))
return false;
mReadySlots.push_back(slot.index);
return true;
}
bool SystemOutputFramePool::PublishCompletedSlot(const OutputFrameSlot& slot)
{
return PublishReadySlot(slot);
}
bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
while (!mReadySlots.empty())
{
const std::size_t index = mReadySlots.front();
mReadySlots.pop_front();
if (index >= mSlots.size() || mSlots[index].state != OutputFrameSlotState::Completed)
continue;
FillOutputSlotLocked(index, slot);
return true;
}
slot = OutputFrameSlot();
++mReadyUnderrunCount;
return false;
}
bool SystemOutputFramePool::ConsumeCompletedSlot(OutputFrameSlot& slot)
{
return ConsumeReadySlot(slot);
}
bool SystemOutputFramePool::MarkScheduled(const OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!IsValidSlotLocked(slot))
return false;
if (mSlots[slot.index].state != OutputFrameSlotState::Completed)
return false;
RemoveReadyIndexLocked(slot.index);
mSlots[slot.index].state = OutputFrameSlotState::Scheduled;
return true;
}
bool SystemOutputFramePool::MarkScheduledByBuffer(void* bytes)
{
if (bytes == nullptr)
return false;
std::lock_guard<std::mutex> lock(mMutex);
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
if (mSlots[index].bytes.empty() || mSlots[index].bytes.data() != bytes)
continue;
if (mSlots[index].state != OutputFrameSlotState::Completed)
return false;
RemoveReadyIndexLocked(index);
mSlots[index].state = OutputFrameSlotState::Scheduled;
return true;
}
return false;
}
bool SystemOutputFramePool::ReleaseSlot(const OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state == OutputFrameSlotState::Free)
return false;
return ReleaseSlotByIndexLocked(slot.index);
}
bool SystemOutputFramePool::ReleaseScheduledSlot(const OutputFrameSlot& slot)
{
std::lock_guard<std::mutex> lock(mMutex);
return TransitionSlotLocked(slot, OutputFrameSlotState::Scheduled, OutputFrameSlotState::Free);
}
bool SystemOutputFramePool::ReleaseSlotByBuffer(void* bytes)
{
if (bytes == nullptr)
return false;
std::lock_guard<std::mutex> lock(mMutex);
for (std::size_t index = 0; index < mSlots.size(); ++index)
{
if (!mSlots[index].bytes.empty() && mSlots[index].bytes.data() == bytes)
return ReleaseSlotByIndexLocked(index);
}
return false;
}
void SystemOutputFramePool::Clear()
{
std::lock_guard<std::mutex> lock(mMutex);
mReadySlots.clear();
for (StoredSlot& slot : mSlots)
{
slot.state = OutputFrameSlotState::Free;
++slot.generation;
}
}
SystemOutputFramePoolMetrics SystemOutputFramePool::GetMetrics() const
{
std::lock_guard<std::mutex> lock(mMutex);
SystemOutputFramePoolMetrics metrics;
metrics.capacity = mSlots.size();
metrics.readyCount = mReadySlots.size();
metrics.acquireMissCount = mAcquireMissCount;
metrics.readyUnderrunCount = mReadyUnderrunCount;
for (const StoredSlot& slot : mSlots)
{
switch (slot.state)
{
case OutputFrameSlotState::Free:
++metrics.freeCount;
break;
case OutputFrameSlotState::Rendering:
++metrics.renderingCount;
++metrics.acquiredCount;
break;
case OutputFrameSlotState::Completed:
++metrics.completedCount;
break;
case OutputFrameSlotState::Scheduled:
++metrics.scheduledCount;
break;
}
}
return metrics;
}
bool SystemOutputFramePool::IsValidSlotLocked(const OutputFrameSlot& slot) const
{
return slot.index < mSlots.size() && mSlots[slot.index].generation == slot.generation;
}
bool SystemOutputFramePool::TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState)
{
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state != expectedState)
return false;
mSlots[slot.index].state = nextState;
return true;
}
void SystemOutputFramePool::FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot)
{
StoredSlot& storedSlot = mSlots[index];
slot.index = index;
slot.generation = storedSlot.generation;
slot.frame.bytes = storedSlot.bytes.empty() ? nullptr : storedSlot.bytes.data();
slot.frame.rowBytes = static_cast<long>(mConfig.rowBytes);
slot.frame.width = mConfig.width;
slot.frame.height = mConfig.height;
slot.frame.pixelFormat = mConfig.pixelFormat;
slot.frame.nativeFrame = nullptr;
slot.frame.nativeBuffer = slot.frame.bytes;
}
void SystemOutputFramePool::RemoveReadyIndexLocked(std::size_t index)
{
mReadySlots.erase(std::remove(mReadySlots.begin(), mReadySlots.end(), index), mReadySlots.end());
}
bool SystemOutputFramePool::ReleaseSlotByIndexLocked(std::size_t index)
{
if (index >= mSlots.size() || mSlots[index].state == OutputFrameSlotState::Free)
return false;
RemoveReadyIndexLocked(index);
mSlots[index].state = OutputFrameSlotState::Free;
return true;
}
std::size_t SystemOutputFramePool::FrameByteCount() const
{
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include "VideoIOTypes.h"
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
enum class OutputFrameSlotState
{
Free,
Rendering,
Completed,
Scheduled
};
struct SystemOutputFramePoolConfig
{
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
unsigned rowBytes = 0;
std::size_t capacity = 0;
};
struct OutputFrameSlot
{
VideoIOOutputFrame frame;
std::size_t index = 0;
uint64_t generation = 0;
};
struct SystemOutputFramePoolMetrics
{
std::size_t capacity = 0;
std::size_t freeCount = 0;
std::size_t renderingCount = 0;
std::size_t completedCount = 0;
std::size_t scheduledCount = 0;
std::size_t acquiredCount = 0;
std::size_t readyCount = 0;
std::size_t consumedCount = 0;
uint64_t acquireMissCount = 0;
uint64_t readyUnderrunCount = 0;
};
class SystemOutputFramePool
{
public:
SystemOutputFramePool() = default;
explicit SystemOutputFramePool(const SystemOutputFramePoolConfig& config);
void Configure(const SystemOutputFramePoolConfig& config);
SystemOutputFramePoolConfig Config() const;
bool AcquireFreeSlot(OutputFrameSlot& slot);
bool AcquireRenderingSlot(OutputFrameSlot& slot);
bool PublishReadySlot(const OutputFrameSlot& slot);
bool PublishCompletedSlot(const OutputFrameSlot& slot);
bool ConsumeReadySlot(OutputFrameSlot& slot);
bool ConsumeCompletedSlot(OutputFrameSlot& slot);
bool MarkScheduled(const OutputFrameSlot& slot);
bool MarkScheduledByBuffer(void* bytes);
bool ReleaseSlot(const OutputFrameSlot& slot);
bool ReleaseScheduledSlot(const OutputFrameSlot& slot);
bool ReleaseSlotByBuffer(void* bytes);
void Clear();
SystemOutputFramePoolMetrics GetMetrics() const;
private:
struct StoredSlot
{
std::vector<unsigned char> bytes;
OutputFrameSlotState state = OutputFrameSlotState::Free;
uint64_t generation = 1;
};
bool IsValidSlotLocked(const OutputFrameSlot& slot) const;
bool TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState);
void FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot);
void RemoveReadyIndexLocked(std::size_t index);
bool ReleaseSlotByIndexLocked(std::size_t index);
std::size_t FrameByteCount() const;
mutable std::mutex mMutex;
SystemOutputFramePoolConfig mConfig;
std::vector<StoredSlot> mSlots;
std::deque<std::size_t> mReadySlots;
uint64_t mAcquireMissCount = 0;
uint64_t mReadyUnderrunCount = 0;
};

1095
src/video/VideoBackend.cpp Normal file

File diff suppressed because it is too large Load Diff

161
src/video/VideoBackend.h Normal file
View File

@@ -0,0 +1,161 @@
#pragma once
#include "OutputProductionController.h"
#include "RenderCadenceController.h"
#include "RenderOutputQueue.h"
#include "SystemOutputFramePool.h"
#include "VideoBackendLifecycle.h"
#include "VideoIOTypes.h"
#include "VideoPlayoutPolicy.h"
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
class HealthTelemetry;
class OpenGLVideoIOBridge;
class RenderEngine;
class RuntimeEventDispatcher;
class VideoIODevice;
class VideoBackend
{
public:
VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher);
~VideoBackend();
void ReleaseResources();
VideoBackendLifecycleState LifecycleState() const;
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
bool ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
bool Start();
bool Stop();
const VideoIOState& State() const;
VideoIOState& MutableState();
bool BeginOutputFrame(VideoIOOutputFrame& frame);
void EndOutputFrame(VideoIOOutputFrame& frame);
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth);
void RecordBackendPlayoutHealth(VideoIOCompletionResult result, const VideoPlayoutRecoveryDecision& recoveryDecision);
bool HasInputDevice() const;
bool HasInputSource() const;
unsigned InputFrameWidth() const;
unsigned InputFrameHeight() const;
unsigned OutputFrameWidth() const;
unsigned OutputFrameHeight() const;
unsigned CaptureTextureWidth() const;
unsigned OutputPackTextureWidth() const;
VideoIOPixelFormat InputPixelFormat() const;
const std::string& InputDisplayModeName() const;
const std::string& OutputModelName() const;
bool SupportsInternalKeying() const;
bool SupportsExternalKeying() const;
bool KeyerInterfaceAvailable() const;
bool ExternalKeyingActive() const;
const std::string& StatusMessage() const;
bool ShouldPrioritizeOutputOverPreview() const;
void SetStatusMessage(const std::string& message);
void PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage = std::string());
void ReportNoInputDeviceSignalStatus();
private:
void HandleInputFrame(const VideoIOFrame& frame);
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
void StartOutputCompletionWorker();
void StopOutputCompletionWorker();
void OutputCompletionWorkerMain();
void StartOutputProducerWorker();
void StopOutputProducerWorker();
void OutputProducerWorkerMain();
void NotifyOutputProducer();
bool WarmupOutputPreroll();
std::chrono::milliseconds OutputProducerWakeInterval() const;
void ProcessOutputFrameCompletion(const VideoIOCompletion& completion);
std::size_t ProduceReadyOutputFrames(const VideoIOCompletion& completion, std::size_t maxFrames);
OutputProductionPressure BuildOutputProductionPressure(const RenderOutputQueueMetrics& metrics) const;
bool RenderReadyOutputFrame(const VideoIOState& state, const VideoIOCompletion& completion);
std::size_t ScheduleReadyOutputFramesToTarget();
bool ScheduleReadyOutputFrame();
bool ScheduleBlackUnderrunFrame();
void RecordFramePacing(VideoIOCompletionResult completionResult);
void RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics);
void RecordDeckLinkBufferTelemetry();
void RecordSystemMemoryPlayoutStats();
void RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds);
bool ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message);
bool ApplyLifecycleFailure(const std::string& message);
void PublishBackendStateChanged(const std::string& state, const std::string& message);
void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state);
void PublishInputFrameArrived(const VideoIOFrame& frame);
void PublishOutputFrameScheduled(const VideoIOOutputFrame& frame);
void PublishOutputFrameCompleted(const VideoIOCompletion& completion);
void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit);
static std::string CompletionResultName(VideoIOCompletionResult result);
static std::string PixelFormatName(VideoIOPixelFormat pixelFormat);
static bool IsEnvironmentFlagEnabled(const char* name);
HealthTelemetry& mHealthTelemetry;
RuntimeEventDispatcher& mRuntimeEventDispatcher;
VideoBackendLifecycle mLifecycle;
VideoPlayoutPolicy mPlayoutPolicy;
OutputProductionController mOutputProductionController;
RenderCadenceController mRenderCadenceController;
RenderOutputQueue mReadyOutputQueue;
SystemOutputFramePool mSystemOutputFramePool;
std::unique_ptr<VideoIODevice> mVideoIODevice;
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
std::mutex mOutputCompletionMutex;
std::condition_variable mOutputCompletionCondition;
std::deque<VideoIOCompletion> mPendingOutputCompletions;
std::thread mOutputCompletionWorker;
std::mutex mOutputProducerMutex;
std::condition_variable mOutputProducerCondition;
std::thread mOutputProducerWorker;
VideoIOCompletion mLastOutputProductionCompletion;
std::chrono::steady_clock::time_point mLastOutputProductionTime;
std::mutex mOutputProductionMutex;
std::mutex mOutputSchedulingMutex;
mutable std::mutex mOutputMetricsMutex;
bool mOutputCompletionWorkerRunning = false;
bool mOutputCompletionWorkerStopping = false;
bool mOutputProducerWorkerRunning = false;
bool mOutputProducerWorkerStopping = false;
bool mInputCaptureDisabled = false;
uint64_t mNextReadyOutputFrameIndex = 0;
uint64_t mInputFrameIndex = 0;
uint64_t mOutputFrameScheduleIndex = 0;
uint64_t mOutputFrameCompletionIndex = 0;
bool mHasLastInputSignal = false;
bool mLastInputSignal = false;
unsigned mLastInputSignalWidth = 0;
unsigned mLastInputSignalHeight = 0;
std::string mLastInputSignalModeName;
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
double mCompletionIntervalMilliseconds = 0.0;
double mSmoothedCompletionIntervalMilliseconds = 0.0;
double mMaxCompletionIntervalMilliseconds = 0.0;
bool mHasReadyQueueDepthBaseline = false;
std::size_t mMinReadyQueueDepth = 0;
std::size_t mMaxReadyQueueDepth = 0;
uint64_t mReadyQueueZeroDepthCount = 0;
double mOutputRenderMilliseconds = 0.0;
double mSmoothedOutputRenderMilliseconds = 0.0;
double mMaxOutputRenderMilliseconds = 0.0;
double mOutputFrameAcquireMilliseconds = 0.0;
double mOutputFrameRenderRequestMilliseconds = 0.0;
double mOutputFrameEndAccessMilliseconds = 0.0;
uint64_t mLastLateStreak = 0;
uint64_t mLastDropStreak = 0;
uint64_t mLateFrameCount = 0;
uint64_t mDroppedFrameCount = 0;
uint64_t mFlushedFrameCount = 0;
};

View File

@@ -0,0 +1,123 @@
#include "VideoBackendLifecycle.h"
VideoBackendLifecycleState VideoBackendLifecycle::State() const
{
return mState;
}
const std::string& VideoBackendLifecycle::FailureReason() const
{
return mFailureReason;
}
VideoBackendLifecycleTransition VideoBackendLifecycle::TransitionTo(VideoBackendLifecycleState next, const std::string& reason)
{
VideoBackendLifecycleTransition transition;
transition.previous = mState;
transition.current = next;
transition.reason = reason;
transition.accepted = CanTransition(mState, next);
if (!transition.accepted)
{
transition.current = mState;
transition.errorMessage = std::string("Invalid video backend lifecycle transition from ") +
StateName(mState) + " to " + StateName(next) + ".";
return transition;
}
mState = next;
transition.current = mState;
if (mState != VideoBackendLifecycleState::Failed)
mFailureReason.clear();
return transition;
}
VideoBackendLifecycleTransition VideoBackendLifecycle::Fail(const std::string& reason)
{
VideoBackendLifecycleTransition transition = TransitionTo(VideoBackendLifecycleState::Failed, reason);
if (transition.accepted)
mFailureReason = reason;
return transition;
}
bool VideoBackendLifecycle::CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next)
{
if (current == next)
return true;
switch (current)
{
case VideoBackendLifecycleState::Uninitialized:
return next == VideoBackendLifecycleState::Discovering ||
next == VideoBackendLifecycleState::Stopped ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Discovering:
return next == VideoBackendLifecycleState::Discovered ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Discovered:
return next == VideoBackendLifecycleState::Configuring ||
next == VideoBackendLifecycleState::Stopped ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Configuring:
return next == VideoBackendLifecycleState::Configured ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Configured:
return next == VideoBackendLifecycleState::Prerolling ||
next == VideoBackendLifecycleState::Stopped ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Prerolling:
return next == VideoBackendLifecycleState::Running ||
next == VideoBackendLifecycleState::Stopping ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Running:
return next == VideoBackendLifecycleState::Degraded ||
next == VideoBackendLifecycleState::Stopping ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Degraded:
return next == VideoBackendLifecycleState::Running ||
next == VideoBackendLifecycleState::Stopping ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Stopping:
return next == VideoBackendLifecycleState::Stopped ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Stopped:
return next == VideoBackendLifecycleState::Discovering ||
next == VideoBackendLifecycleState::Failed;
case VideoBackendLifecycleState::Failed:
return next == VideoBackendLifecycleState::Stopped ||
next == VideoBackendLifecycleState::Discovering;
default:
return false;
}
}
const char* VideoBackendLifecycle::StateName(VideoBackendLifecycleState state)
{
switch (state)
{
case VideoBackendLifecycleState::Uninitialized:
return "uninitialized";
case VideoBackendLifecycleState::Discovering:
return "discovering";
case VideoBackendLifecycleState::Discovered:
return "discovered";
case VideoBackendLifecycleState::Configuring:
return "configuring";
case VideoBackendLifecycleState::Configured:
return "configured";
case VideoBackendLifecycleState::Prerolling:
return "prerolling";
case VideoBackendLifecycleState::Running:
return "running";
case VideoBackendLifecycleState::Degraded:
return "degraded";
case VideoBackendLifecycleState::Stopping:
return "stopping";
case VideoBackendLifecycleState::Stopped:
return "stopped";
case VideoBackendLifecycleState::Failed:
return "failed";
default:
return "unknown";
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <string>
enum class VideoBackendLifecycleState
{
Uninitialized,
Discovering,
Discovered,
Configuring,
Configured,
Prerolling,
Running,
Degraded,
Stopping,
Stopped,
Failed
};
struct VideoBackendLifecycleTransition
{
VideoBackendLifecycleState previous = VideoBackendLifecycleState::Uninitialized;
VideoBackendLifecycleState current = VideoBackendLifecycleState::Uninitialized;
bool accepted = false;
std::string reason;
std::string errorMessage;
};
class VideoBackendLifecycle
{
public:
VideoBackendLifecycleState State() const;
const std::string& FailureReason() const;
VideoBackendLifecycleTransition TransitionTo(VideoBackendLifecycleState next, const std::string& reason);
VideoBackendLifecycleTransition Fail(const std::string& reason);
static bool CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next);
static const char* StateName(VideoBackendLifecycleState state);
private:
VideoBackendLifecycleState mState = VideoBackendLifecycleState::Uninitialized;
std::string mFailureReason;
};

170
src/video/VideoIOFormat.cpp Normal file
View File

@@ -0,0 +1,170 @@
#include "VideoIOFormat.h"
#include <algorithm>
#include <cmath>
#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif
namespace
{
uint16_t Clamp10(int value, int minimum, int maximum)
{
return static_cast<uint16_t>(std::max(minimum, std::min(maximum, value)));
}
uint32_t MakeV210Word(uint16_t a, uint16_t b, uint16_t c)
{
return (static_cast<uint32_t>(a) & 0x3ffu)
| ((static_cast<uint32_t>(b) & 0x3ffu) << 10)
| ((static_cast<uint32_t>(c) & 0x3ffu) << 20);
}
void StoreWord(std::array<uint8_t, 16>& bytes, std::size_t wordIndex, uint32_t word)
{
const std::size_t offset = wordIndex * 4;
bytes[offset + 0] = static_cast<uint8_t>(word & 0xffu);
bytes[offset + 1] = static_cast<uint8_t>((word >> 8) & 0xffu);
bytes[offset + 2] = static_cast<uint8_t>((word >> 16) & 0xffu);
bytes[offset + 3] = static_cast<uint8_t>((word >> 24) & 0xffu);
}
uint32_t LoadWord(const std::array<uint8_t, 16>& bytes, std::size_t wordIndex)
{
const std::size_t offset = wordIndex * 4;
return static_cast<uint32_t>(bytes[offset + 0])
| (static_cast<uint32_t>(bytes[offset + 1]) << 8)
| (static_cast<uint32_t>(bytes[offset + 2]) << 16)
| (static_cast<uint32_t>(bytes[offset + 3]) << 24);
}
uint16_t Component(uint32_t word, unsigned index)
{
return static_cast<uint16_t>((word >> (index * 10)) & 0x3ffu);
}
}
const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
{
switch (format)
{
case VideoIOPixelFormat::V210:
return "10-bit YUV v210";
case VideoIOPixelFormat::Yuva10:
return "10-bit YUVA Ay10";
case VideoIOPixelFormat::Bgra8:
return "8-bit BGRA";
case VideoIOPixelFormat::Uyvy8:
default:
return "8-bit YUV UYVY";
}
}
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
{
return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
}
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
{
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8;
}
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
{
switch (format)
{
case VideoIOPixelFormat::Uyvy8:
return 2u;
case VideoIOPixelFormat::Bgra8:
return 4u;
case VideoIOPixelFormat::Yuva10:
return 4u;
case VideoIOPixelFormat::V210:
default:
return 0u;
}
}
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
{
if (format == VideoIOPixelFormat::V210)
return MinimumV210RowBytes(frameWidth);
if (format == VideoIOPixelFormat::Yuva10)
return MinimumYuva10RowBytes(frameWidth);
return frameWidth * VideoIOBytesPerPixel(format);
}
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
{
return (rowBytes + 3u) / 4u;
}
unsigned MinimumV210RowBytes(unsigned frameWidth)
{
return ((frameWidth + 5u) / 6u) * 16u;
}
unsigned MinimumYuva10RowBytes(unsigned frameWidth)
{
return ((frameWidth + 63u) / 64u) * 256u;
}
unsigned ActiveV210WordsForWidth(unsigned frameWidth)
{
return ((frameWidth + 5u) / 6u) * 4u;
}
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue)
{
red = std::max(0.0f, std::min(1.0f, red));
green = std::max(0.0f, std::min(1.0f, green));
blue = std::max(0.0f, std::min(1.0f, blue));
const float y = 0.2126f * red + 0.7152f * green + 0.0722f * blue;
const float cb = (blue - y) / 1.8556f + 0.5f;
const float cr = (red - y) / 1.5748f + 0.5f;
V210CodeValues values;
values.y = Clamp10(static_cast<int>(std::lround(64.0f + y * 876.0f)), 64, 940);
values.cb = Clamp10(static_cast<int>(std::lround(64.0f + cb * 896.0f)), 64, 960);
values.cr = Clamp10(static_cast<int>(std::lround(64.0f + cr * 896.0f)), 64, 960);
return values;
}
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block)
{
std::array<uint8_t, 16> bytes = {};
StoreWord(bytes, 0, MakeV210Word(block.cb[0], block.y[0], block.cr[0]));
StoreWord(bytes, 1, MakeV210Word(block.y[1], block.cb[1], block.y[2]));
StoreWord(bytes, 2, MakeV210Word(block.cr[1], block.y[3], block.cb[2]));
StoreWord(bytes, 3, MakeV210Word(block.y[4], block.cr[2], block.y[5]));
return bytes;
}
V210SixPixelBlock UnpackV210Block(const std::array<uint8_t, 16>& bytes)
{
const uint32_t word0 = LoadWord(bytes, 0);
const uint32_t word1 = LoadWord(bytes, 1);
const uint32_t word2 = LoadWord(bytes, 2);
const uint32_t word3 = LoadWord(bytes, 3);
V210SixPixelBlock block;
block.cb[0] = Component(word0, 0);
block.y[0] = Component(word0, 1);
block.cr[0] = Component(word0, 2);
block.y[1] = Component(word1, 0);
block.cb[1] = Component(word1, 1);
block.y[2] = Component(word1, 2);
block.cr[1] = Component(word2, 0);
block.y[3] = Component(word2, 1);
block.cb[2] = Component(word2, 2);
block.y[4] = Component(word3, 0);
block.cr[2] = Component(word3, 1);
block.y[5] = Component(word3, 2);
return block;
}

39
src/video/VideoIOFormat.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <array>
#include <cstdint>
enum class VideoIOPixelFormat
{
Uyvy8,
V210,
Yuva10,
Bgra8
};
struct V210CodeValues
{
uint16_t y = 64;
uint16_t cb = 512;
uint16_t cr = 512;
};
struct V210SixPixelBlock
{
std::array<uint16_t, 6> y = {};
std::array<uint16_t, 3> cb = {};
std::array<uint16_t, 3> cr = {};
};
const char* VideoIOPixelFormatName(VideoIOPixelFormat format);
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format);
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
unsigned MinimumV210RowBytes(unsigned frameWidth);
unsigned MinimumYuva10RowBytes(unsigned frameWidth);
unsigned ActiveV210WordsForWidth(unsigned frameWidth);
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue);
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block);
V210SixPixelBlock UnpackV210Block(const std::array<uint8_t, 16>& bytes);

164
src/video/VideoIOTypes.h Normal file
View File

@@ -0,0 +1,164 @@
#pragma once
#include "DeckLinkDisplayMode.h"
#include "VideoIOFormat.h"
#include <cstdint>
#include <functional>
#include <string>
enum class VideoIOBackend
{
DeckLink
};
enum class VideoIOCompletionResult
{
Completed,
DisplayedLate,
Dropped,
Flushed,
Unknown
};
struct VideoIOConfig
{
VideoFormatSelection videoModes;
bool externalKeyingEnabled = false;
bool preferTenBit = true;
};
struct VideoIOState
{
FrameSize inputFrameSize;
FrameSize outputFrameSize;
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8;
unsigned inputFrameRowBytes = 0;
unsigned outputFrameRowBytes = 0;
unsigned captureTextureWidth = 0;
unsigned outputPackTextureWidth = 0;
std::string inputDisplayModeName = "1080p59.94";
std::string outputDisplayModeName = "1080p59.94";
std::string outputModelName;
std::string statusMessage;
std::string formatStatusMessage;
bool hasInputDevice = false;
bool hasInputSource = false;
bool supportsInternalKeying = false;
bool supportsExternalKeying = false;
bool keyerInterfaceAvailable = false;
bool externalKeyingActive = false;
double frameBudgetMilliseconds = 0.0;
bool actualDeckLinkBufferedFramesAvailable = false;
uint64_t actualDeckLinkBufferedFrames = 0;
double deckLinkScheduleCallMilliseconds = 0.0;
uint64_t deckLinkScheduleFailureCount = 0;
bool deckLinkScheduleLeadAvailable = false;
int64_t deckLinkPlaybackStreamTime = 0;
uint64_t deckLinkPlaybackFrameIndex = 0;
uint64_t deckLinkNextScheduleFrameIndex = 0;
int64_t deckLinkScheduleLeadFrames = 0;
uint64_t deckLinkScheduleRealignmentCount = 0;
};
struct VideoIOFrame
{
void* bytes = nullptr;
long rowBytes = 0;
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8;
bool hasNoInputSource = false;
};
struct VideoIOOutputFrame
{
void* bytes = nullptr;
long rowBytes = 0;
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
void* nativeFrame = nullptr;
void* nativeBuffer = nullptr;
};
struct VideoIOCompletion
{
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
void* outputFrameBuffer = nullptr;
};
struct VideoIOScheduleTime
{
int64_t streamTime = 0;
int64_t duration = 0;
int64_t timeScale = 0;
uint64_t frameIndex = 0;
};
struct VideoPlayoutRecoveryDecision
{
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
uint64_t completedFrameIndex = 0;
uint64_t scheduledFrameIndex = 0;
uint64_t readyQueueDepth = 0;
uint64_t scheduledLeadFrames = 0;
uint64_t measuredLagFrames = 0;
uint64_t catchUpFrames = 0;
uint64_t lateStreak = 0;
uint64_t dropStreak = 0;
};
class VideoIODevice
{
public:
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
virtual ~VideoIODevice() = default;
virtual void ReleaseResources() = 0;
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
virtual bool PrepareOutputSchedule() = 0;
virtual bool StartInputStreams() = 0;
virtual bool StartScheduledPlayback() = 0;
virtual bool Start() = 0;
virtual bool Stop() = 0;
virtual const VideoIOState& State() const = 0;
virtual VideoIOState& MutableState() = 0;
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
virtual VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth) = 0;
bool HasInputDevice() const { return State().hasInputDevice; }
bool HasInputSource() const { return State().hasInputSource; }
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
const std::string& OutputModelName() const { return State().outputModelName; }
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
const std::string& StatusMessage() const { return State().statusMessage; }
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
enum class VideoUnderrunBehavior
{
ReuseLastCompletedFrame,
BlackFrame
};
struct VideoPlayoutPolicy
{
unsigned outputFramePoolSize = 10;
unsigned targetPrerollFrames = 4;
unsigned targetReadyFrames = 2;
unsigned maxReadyFrames = 4;
unsigned minimumSpareDeviceFrames = 1;
uint64_t lateOrDropCatchUpFrames = 0;
VideoUnderrunBehavior underrunBehavior = VideoUnderrunBehavior::ReuseLastCompletedFrame;
bool adaptiveHeadroomEnabled = false;
};
inline VideoPlayoutPolicy NormalizeVideoPlayoutPolicy(VideoPlayoutPolicy policy)
{
if (policy.outputFramePoolSize == 0)
policy.outputFramePoolSize = 1;
if (policy.targetPrerollFrames == 0)
policy.targetPrerollFrames = 1;
if (policy.targetReadyFrames == 0)
policy.targetReadyFrames = 1;
if (policy.maxReadyFrames < policy.targetReadyFrames)
policy.maxReadyFrames = policy.targetReadyFrames;
const unsigned minimumOutputFramePoolSize = policy.targetPrerollFrames + policy.maxReadyFrames + policy.minimumSpareDeviceFrames;
if (policy.outputFramePoolSize < minimumOutputFramePoolSize)
policy.outputFramePoolSize = minimumOutputFramePoolSize;
return policy;
}

View File

@@ -0,0 +1,108 @@
#include "VideoPlayoutScheduler.h"
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
{
Configure(frameDuration, timeScale, VideoPlayoutPolicy());
}
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy)
{
mFrameDuration = frameDuration;
mTimeScale = timeScale;
mPolicy = NormalizeVideoPlayoutPolicy(policy);
Reset();
}
void VideoPlayoutScheduler::Reset()
{
mScheduledFrameIndex = 0;
mCompletedFrameIndex = 0;
mLateStreak = 0;
mDropStreak = 0;
}
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
{
VideoIOScheduleTime time;
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
time.duration = mFrameDuration;
time.timeScale = mTimeScale;
time.frameIndex = mScheduledFrameIndex;
++mScheduledFrameIndex;
return time;
}
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
{
if (mFrameDuration <= 0 || streamTime < 0)
return;
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
if (minimumScheduleIndex > mScheduledFrameIndex)
mScheduledFrameIndex = minimumScheduleIndex;
}
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
{
++mCompletedFrameIndex;
if (result == VideoIOCompletionResult::DisplayedLate)
++mLateStreak;
else
mLateStreak = 0;
if (result == VideoIOCompletionResult::Dropped)
++mDropStreak;
else
mDropStreak = 0;
const uint64_t measuredLagFrames = MeasureLag(result, readyQueueDepth);
const uint64_t catchUpFrames = measuredLagFrames < mPolicy.lateOrDropCatchUpFrames
? measuredLagFrames
: mPolicy.lateOrDropCatchUpFrames;
if (catchUpFrames > 0)
mScheduledFrameIndex += catchUpFrames;
VideoPlayoutRecoveryDecision decision;
decision.result = result;
decision.completedFrameIndex = mCompletedFrameIndex;
decision.scheduledFrameIndex = mScheduledFrameIndex;
decision.readyQueueDepth = readyQueueDepth;
decision.scheduledLeadFrames = mScheduledFrameIndex > mCompletedFrameIndex
? mScheduledFrameIndex - mCompletedFrameIndex
: 0;
decision.measuredLagFrames = measuredLagFrames;
decision.catchUpFrames = catchUpFrames;
decision.lateStreak = mLateStreak;
decision.dropStreak = mDropStreak;
return decision;
}
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
{
return mTimeScale != 0
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
: 0.0;
}
uint64_t VideoPlayoutScheduler::MeasureLag(VideoIOCompletionResult result, uint64_t readyQueueDepth) const
{
if (result != VideoIOCompletionResult::DisplayedLate && result != VideoIOCompletionResult::Dropped)
return 0;
uint64_t lagFrames = 1;
if (result == VideoIOCompletionResult::DisplayedLate && mLateStreak > lagFrames)
lagFrames = mLateStreak;
if (result == VideoIOCompletionResult::Dropped && mDropStreak * 2 > lagFrames)
lagFrames = mDropStreak * 2;
if (mCompletedFrameIndex >= mScheduledFrameIndex)
{
const uint64_t scheduleLagFrames = mCompletedFrameIndex - mScheduledFrameIndex + 1;
if (scheduleLagFrames > lagFrames)
lagFrames = scheduleLagFrames;
}
if (readyQueueDepth < mPolicy.targetReadyFrames && mPolicy.targetReadyFrames - readyQueueDepth > lagFrames)
lagFrames = mPolicy.targetReadyFrames - readyQueueDepth;
return lagFrames;
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include "VideoIOTypes.h"
#include "VideoPlayoutPolicy.h"
#include <cstdint>
class VideoPlayoutScheduler
{
public:
void Configure(int64_t frameDuration, int64_t timeScale);
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
void Reset();
VideoIOScheduleTime NextScheduleTime();
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
double FrameBudgetMilliseconds() const;
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
int64_t FrameDuration() const { return mFrameDuration; }
uint64_t LateStreak() const { return mLateStreak; }
uint64_t DropStreak() const { return mDropStreak; }
int64_t TimeScale() const { return mTimeScale; }
const VideoPlayoutPolicy& Policy() const { return mPolicy; }
private:
uint64_t MeasureLag(VideoIOCompletionResult result, uint64_t readyQueueDepth) const;
int64_t mFrameDuration = 0;
int64_t mTimeScale = 0;
uint64_t mScheduledFrameIndex = 0;
uint64_t mCompletedFrameIndex = 0;
uint64_t mLateStreak = 0;
uint64_t mDropStreak = 0;
VideoPlayoutPolicy mPolicy;
};

97
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,97 @@
add_video_shader_test(RenderCadenceCompositorClockTests
"${SRC_DIR}/render/RenderCadenceClock.cpp"
"${TEST_DIR}/RenderCadenceCompositorClockTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorJsonWriterTests
"${SRC_DIR}/json/JsonWriter.cpp"
"${TEST_DIR}/RenderCadenceCompositorJsonWriterTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorLoggerTests
"${SRC_DIR}/logging/Logger.cpp"
"${TEST_DIR}/RenderCadenceCompositorLoggerTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorTelemetryTests
"${SRC_DIR}/json/JsonWriter.cpp"
"${TEST_DIR}/RenderCadenceCompositorTelemetryTests.cpp"
)
add_video_shader_test(RuntimeJsonTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${TEST_DIR}/RuntimeJsonTests.cpp"
)
add_video_shader_test(RuntimeParameterUtilsTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
"${TEST_DIR}/RuntimeParameterUtilsTests.cpp"
)
add_video_shader_test(ShaderPackageRegistryTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/ShaderPackageRegistryTests.cpp"
)
add_video_shader_test(ShaderSlangValidationTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/shader/ShaderCompiler.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/ShaderSlangValidationTests.cpp"
)
add_video_shader_test(Std140BufferTests
"${TEST_DIR}/Std140BufferTests.cpp"
)
add_video_shader_test(VideoIOFormatTests
"${SRC_DIR}/video/DeckLinkVideoIOFormat.cpp"
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${TEST_DIR}/VideoIOFormatTests.cpp"
)
add_video_shader_test(VideoPlayoutSchedulerTests
"${SRC_DIR}/video/VideoPlayoutScheduler.cpp"
"${TEST_DIR}/VideoPlayoutSchedulerTests.cpp"
)
add_video_shader_test(OutputProductionControllerTests
"${SRC_DIR}/video/OutputProductionController.cpp"
"${TEST_DIR}/OutputProductionControllerTests.cpp"
)
add_video_shader_test(RenderOutputQueueTests
"${SRC_DIR}/video/RenderOutputQueue.cpp"
"${TEST_DIR}/RenderOutputQueueTests.cpp"
)
add_video_shader_test(RenderCadenceControllerTests
"${SRC_DIR}/video/RenderCadenceController.cpp"
"${TEST_DIR}/RenderCadenceControllerTests.cpp"
)
add_video_shader_test(SystemOutputFramePoolTests
"${SRC_DIR}/video/SystemOutputFramePool.cpp"
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${TEST_DIR}/SystemOutputFramePoolTests.cpp"
)
add_video_shader_test(VideoBackendLifecycleTests
"${SRC_DIR}/video/VideoBackendLifecycle.cpp"
"${TEST_DIR}/VideoBackendLifecycleTests.cpp"
)
add_video_shader_test(VideoIODeviceFakeTests
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${TEST_DIR}/VideoIODeviceFakeTests.cpp"
)
set_tests_properties(RenderCadenceCompositorLoggerTests PROPERTIES
ENVIRONMENT "VIDEO_SHADER_TEST_LOG_TO_CONSOLE=0"
)
set_tests_properties(ShaderSlangValidationTests PROPERTIES
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
)