This commit is contained in:
2026-05-02 15:49:45 +10:00
parent 8b7aa1971e
commit 8d01ea4a3c
12 changed files with 454 additions and 239 deletions

View File

@@ -41,9 +41,11 @@
#include "OpenGLComposite.h"
#include "GLExtensions.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <initguid.h>
DEFINE_GUID(IID_PinnedMemoryAllocator,
@@ -51,7 +53,20 @@ DEFINE_GUID(IID_PinnedMemoryAllocator,
namespace
{
const char* kFragmentShaderFilename = "video_effect.frag";
const char* kSlangShaderRelativePath = "apps/LoopThroughWithOpenGLCompositing/video_effect.slang";
const char* kRuntimeShaderCacheDirectory = "shader_cache";
const char* kRuntimeRawShaderFilename = "video_effect.raw.frag";
const char* kRuntimePatchedShaderFilename = "video_effect.frag";
const char* kVertexShaderSource =
"#version 130\n"
"out vec2 vTexCoord;\n"
"void main()\n"
"{\n"
" vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n"
" vec2 texCoords[3] = vec2[3](vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0));\n"
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
" vTexCoord = texCoords[gl_VertexID];\n"
"}\n";
std::string GetExecutableDirectory()
{
@@ -68,6 +83,19 @@ std::string GetExecutableDirectory()
return path.substr(0, slashIndex);
}
bool ReplaceAll(std::string& text, const std::string& from, const std::string& to)
{
bool replaced = false;
std::string::size_type startPos = 0;
while ((startPos = text.find(from, startPos)) != std::string::npos)
{
text.replace(startPos, from.length(), to);
startPos += to.length();
replaced = true;
}
return replaced;
}
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
@@ -97,6 +125,158 @@ bool LoadTextFile(const std::string& path, std::string& contents, std::string& e
return true;
}
std::filesystem::path FindRepoRoot()
{
std::vector<std::filesystem::path> rootsToTry;
char currentDirBuffer[MAX_PATH] = {};
if (GetCurrentDirectoryA(MAX_PATH, currentDirBuffer) > 0)
rootsToTry.push_back(std::filesystem::path(currentDirBuffer));
std::string executableDirectory = GetExecutableDirectory();
if (!executableDirectory.empty())
rootsToTry.push_back(std::filesystem::path(executableDirectory));
for (const std::filesystem::path& startPath : rootsToTry)
{
std::filesystem::path candidate = startPath;
for (int depth = 0; depth < 8 && !candidate.empty(); ++depth)
{
if (std::filesystem::exists(candidate / kSlangShaderRelativePath))
return candidate;
candidate = candidate.parent_path();
}
}
return std::filesystem::path();
}
bool FindSlangCompiler(const std::filesystem::path& repoRoot, std::filesystem::path& slangCompilerPath, std::string& error)
{
std::filesystem::path thirdPartyPath = repoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyPath))
{
error = "Could not locate the 3rdParty directory from the application runtime path.";
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(thirdPartyPath))
{
if (!entry.is_directory())
continue;
std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
slangCompilerPath = candidate;
return true;
}
}
error = "Could not find slangc.exe under 3rdParty.";
return false;
}
bool RunProcessAndWait(const std::string& commandLine, std::string& error)
{
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, NULL, &startupInfo, &processInfo))
{
error = "Failed to start slangc.exe.";
return false;
}
WaitForSingleObject(processInfo.hProcess, INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processInfo.hProcess, &exitCode);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
if (exitCode != 0)
{
error = "slangc.exe returned a non-zero exit code while compiling the runtime shader.";
return false;
}
return true;
}
bool PatchGeneratedSlangGLSL(std::string& shaderText, std::string& error)
{
bool replacedVersion = ReplaceAll(shaderText, "#version 450", "#version 130");
ReplaceAll(shaderText, "#extension GL_EXT_samplerless_texture_functions : require\n", "");
ReplaceAll(shaderText, "layout(row_major) uniform;\n", "");
ReplaceAll(shaderText, "layout(row_major) buffer;\n", "");
ReplaceAll(shaderText, "layout(binding = 0)\nuniform texture2D UYVYtex_0;", "uniform sampler2D UYVYtex;");
ReplaceAll(shaderText, "layout(location = 0)\nout vec4 entryPointParam_fragmentMain_0;\n", "");
ReplaceAll(shaderText, "layout(location = 0)\nin vec2 input_texCoord_0;\n", "in vec2 vTexCoord;\n");
ReplaceAll(shaderText, "UYVYtex_0", "UYVYtex");
ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord");
ReplaceAll(shaderText, "entryPointParam_fragmentMain_0 =", "gl_FragColor =");
if (!replacedVersion)
{
error = "Generated Slang GLSL did not contain the expected version header.";
return false;
}
return true;
}
bool BuildFragmentShaderSourceFromSlang(std::string& shaderSource, std::string& error)
{
std::filesystem::path repoRoot = FindRepoRoot();
if (repoRoot.empty())
{
error = "Could not locate the repository root to load video_effect.slang.";
return false;
}
std::filesystem::path slangSourcePath = repoRoot / kSlangShaderRelativePath;
if (!std::filesystem::exists(slangSourcePath))
{
error = "Could not find video_effect.slang.";
return false;
}
std::filesystem::path slangCompilerPath;
if (!FindSlangCompiler(repoRoot, slangCompilerPath, error))
return false;
std::filesystem::path shaderCachePath = std::filesystem::path(GetExecutableDirectory()) / kRuntimeShaderCacheDirectory;
std::filesystem::create_directories(shaderCachePath);
std::filesystem::path rawShaderPath = shaderCachePath / kRuntimeRawShaderFilename;
std::filesystem::path patchedShaderPath = shaderCachePath / kRuntimePatchedShaderFilename;
std::string commandLine = "\"" + slangCompilerPath.string() + "\" \"" + slangSourcePath.string()
+ "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + rawShaderPath.string() + "\"";
if (!RunProcessAndWait(commandLine, error))
return false;
if (!LoadTextFile(rawShaderPath.string(), shaderSource, error))
return false;
if (!PatchGeneratedSlangGLSL(shaderSource, error))
return false;
std::ofstream patchedShaderOutput(patchedShaderPath.string().c_str(), std::ios::binary);
if (patchedShaderOutput)
patchedShaderOutput << shaderSource;
return true;
}
}
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
@@ -109,6 +289,10 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
mFastTransferExtensionAvailable(false),
mCaptureTexture(0),
mFBOTexture(0),
mProgram(0),
mVertexShader(0),
mFragmentShader(0),
mUYVYtexUniform(-1),
mRotateAngle(0.0f),
mRotateAngleRate(0.0f)
{
@@ -164,6 +348,8 @@ OpenGLComposite::~OpenGLComposite()
mPlayoutAllocator = NULL;
}
destroyShaderProgram();
DeleteCriticalSection(&pMutex);
}
@@ -177,7 +363,6 @@ bool OpenGLComposite::InitDeckLink()
IDeckLinkDisplayMode* pDLDisplayMode = NULL;
BMDDisplayMode displayMode = bmdModeHD1080p5994; // mode to use for capture and playout
int outputFrameRowBytes;
float fps;
HRESULT result;
result = CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&pDLIterator);
@@ -267,10 +452,7 @@ bool OpenGLComposite::InitDeckLink()
if (! InitOpenGLState())
goto error;
// Compute a rotate angle rate so box will spin at a rate independent of video mode frame rate
pDLDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale);
fps = (float)mFrameTimescale / (float)mFrameDuration;
mRotateAngleRate = 35.0f / fps; // rotate box through 35 degrees every second
// Resize window to match video frame, but scale large formats down by half for viewing
if (mFrameWidth < 1920)
@@ -413,7 +595,7 @@ bool OpenGLComposite::InitOpenGLState()
if (! ResolveGLExtensions())
return false;
// Prepare the shader used to perform colour space conversion on the video texture
// Prepare the runtime shader program generated from the Slang source file.
char compilerErrorMessage[1024];
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
{
@@ -421,13 +603,8 @@ bool OpenGLComposite::InitOpenGLState()
return false;
}
// Setup the scene
glShadeModel( GL_SMOOTH ); // Enable smooth shading
glClearColor( 0.0f, 0.0f, 0.0f, 0.5f ); // Black background
glClearDepth( 1.0f ); // Depth buffer setup
glEnable( GL_DEPTH_TEST ); // Enable depth testing
glDepthFunc( GL_LEQUAL ); // Type of depth test to do
glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
glDisable(GL_DEPTH_TEST);
if (! mFastTransferExtensionAvailable)
{
@@ -546,8 +723,8 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo
inputFrameBuffer->Release();
}
// Draw the captured video frame texture onto a box, rendering to the off-screen frame buffer.
// Read the rendered scene back from the frame buffer and schedule it for playout.
// Render the live video texture through the runtime shader into the off-screen framebuffer.
// Read the result back from the frame buffer and schedule it for playout.
void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
{
EnterCriticalSection(&pMutex);
@@ -560,111 +737,9 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
// make GL context current in this thread
wglMakeCurrent( hGLDC, hGLRC );
// Draw OpenGL scene to the off-screen frame buffer
// Draw the effect output to the off-screen framebuffer.
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf);
// Setup view and projection
GLfloat aspectRatio = (GLfloat)mFrameWidth / (GLfloat)mFrameHeight;
glViewport (0, 0, mFrameWidth, mFrameHeight);
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 45.0f, aspectRatio, 0.1f, 100.0f );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glScalef( aspectRatio, 1.0f, 1.0f ); // Scale x for correct aspect ratio
glTranslatef( 0.0f, 0.0f, -4.0f ); // Move into screen
glRotatef( mRotateAngle, 1.0f, 1.0f, 1.0f ); // Rotate model around a vector
mRotateAngle -= mRotateAngleRate; // update the rotation angle for next iteration
glFinish(); // Ensure changes to GL state are complete
// Draw a colourful frame around the front face of the box
// (provides a pleasing nesting effect when you connect the playout output to the capture input)
glBegin(GL_QUAD_STRIP);
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 1.2f, 1.2f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glColor3f( 0.0f, 0.0f, 1.0f );
glVertex3f( 1.2f, -1.2f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glColor3f( 0.0f, 1.0f, 0.0f );
glVertex3f(-1.2f, -1.2f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glColor3f( 1.0f, 1.0f, 0.0f );
glVertex3f(-1.2f, 1.2f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glColor3f( 1.0f, 0.0f, 0.0f );
glVertex3f( 1.2f, 1.2f, 1.0f);
glVertex3f( 1.0f, 1.0f, 1.0f);
glEnd();
if (mHasNoInputSource)
{
// Draw a big X when no input is available on capture
glBegin( GL_QUADS );
glColor3f( 1.0f, 0.0f, 1.0f );
glVertex3f( 0.8f, 0.9f, 1.0f );
glVertex3f( 0.9f, 0.8f, 1.0f );
glColor3f( 1.0f, 1.0f, 0.0f );
glVertex3f( -0.8f, -0.9f, 1.0f );
glVertex3f( -0.9f, -0.8f, 1.0f );
glColor3f( 1.0f, 0.0f, 1.0f );
glVertex3f( -0.8f, 0.9f, 1.0f );
glVertex3f( -0.9f, 0.8f, 1.0f );
glColor3f( 1.0f, 1.0f, 0.0f );
glVertex3f( 0.8f, -0.9f, 1.0f );
glVertex3f( 0.9f, -0.8f, 1.0f );
glEnd();
}
else
{
if (mFastTransferExtensionAvailable)
{
// Signal that we're about to draw using mCaptureTexture onto mFBOTexture
VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::CPUtoGPU);
}
// Pass texture unit 0 to the fragment shader as a uniform variable
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
glUseProgram(mProgram);
GLint locUYVYtex = glGetUniformLocation(mProgram, "UYVYtex");
glUniform1i(locUYVYtex, 0); // Bind texture unit 0
// Draw front and back faces of box applying video texture to each face
glBegin(GL_QUADS);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f ); // Top right of front side
glTexCoord2f(0.0f, 0.0f); glVertex3f( -1.0f, 1.0f, 1.0f ); // Top left of front side
glTexCoord2f(0.0f, 1.0f); glVertex3f( -1.0f, -1.0f, 1.0f ); // Bottom left of front side
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, -1.0f, 1.0f ); // Bottom right of front side
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f ); // Top right of back side
glTexCoord2f(0.0f, 1.0f); glVertex3f( -1.0f, -1.0f, -1.0f ); // Top left of back side
glTexCoord2f(0.0f, 0.0f); glVertex3f( -1.0f, 1.0f, -1.0f ); // Bottom left of back side
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, -1.0f ); // Bottom right of back side
glEnd();
// Draw left and right sides of box with partially transparent video texture
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBegin(GL_QUADS);
glTexCoord2f(0.1f, 0.0f); glVertex3f( -1.0f, 1.0f, 1.0f ); // Top right of left side
glTexCoord2f(1.0f, 0.0f); glVertex3f( -1.0f, 1.0f, -1.0f ); // Top left of left side
glTexCoord2f(1.0f, 1.0f); glVertex3f( -1.0f, -1.0f, -1.0f ); // Bottom left of left side
glTexCoord2f(0.1f, 1.0f); glVertex3f( -1.0f, -1.0f, 1.0f ); // Bottom right of left side
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, -1.0f ); // Top right of right side
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f ); // Top left of right side
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, 1.0f ); // Bottom left of right side
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f ); // Bottom right of right side
glEnd();
glDisable(GL_BLEND);
glUseProgram(0);
glDisable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
}
renderEffect();
IDeckLinkVideoBuffer* outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
@@ -772,26 +847,93 @@ bool OpenGLComposite::Stop()
return true;
}
// Setup fragment shader to take YCbCr 4:2:2 video texture in UYVY macropixel format
// and perform colour space conversion to RGBA in the GPU.
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
bool OpenGLComposite::ReloadShader()
{
GLsizei errorBufferSize;
GLint compileResult, linkResult;
std::string shaderPath = GetExecutableDirectory();
std::string fragmentShaderSource;
std::string loadError;
char compilerErrorMessage[1024];
if (shaderPath.empty())
EnterCriticalSection(&pMutex);
wglMakeCurrent(hGLDC, hGLRC);
bool success = compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&pMutex);
if (!success)
MessageBoxA(NULL, compilerErrorMessage, "Slang shader reload failed", MB_OK);
return success;
}
void OpenGLComposite::destroyShaderProgram()
{
if (mProgram != 0)
{
CopyErrorMessage("Could not determine executable directory for fragment shader loading.", errorMessageSize, errorMessage);
return false;
glDeleteProgram(mProgram);
mProgram = 0;
}
shaderPath += "\\";
shaderPath += kFragmentShaderFilename;
if (mFragmentShader != 0)
{
glDeleteShader(mFragmentShader);
mFragmentShader = 0;
}
if (!LoadTextFile(shaderPath, fragmentShaderSource, loadError))
if (mVertexShader != 0)
{
glDeleteShader(mVertexShader);
mVertexShader = 0;
}
mUYVYtexUniform = -1;
}
void OpenGLComposite::renderEffect()
{
glViewport(0, 0, mFrameWidth, mFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (mHasNoInputSource)
return;
if (mFastTransferExtensionAvailable)
{
// Signal that we're about to draw using mCaptureTexture onto mFBOTexture.
VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::CPUtoGPU);
}
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
glUseProgram(mProgram);
if (mUYVYtexUniform >= 0)
glUniform1i(mUYVYtexUniform, 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindTexture(GL_TEXTURE_2D, 0);
glDisable(GL_TEXTURE_2D);
if (mFastTransferExtensionAvailable)
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
}
// Compile a fullscreen shader pass from the runtime Slang source. The Slang compiler
// emits modern GLSL which we patch into a compatibility-profile shader that can run
// inside the sample's WGL context.
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
std::string fragmentShaderSource;
std::string loadError;
const char* vertexSource = kVertexShaderSource;
if (!BuildFragmentShaderSourceFromSlang(fragmentShaderSource, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
@@ -799,30 +941,50 @@ bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMes
const char* fragmentSource = fragmentShaderSource.c_str();
mFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(mFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(mFragmentShader);
glGetShaderiv(mFragmentShader, GL_COMPILE_STATUS, &compileResult);
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader);
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(mFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
return false;
}
mProgram = glCreateProgram();
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader);
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false;
}
glAttachShader(mProgram, mFragmentShader);
glLinkProgram(mProgram);
glGetProgramiv(mProgram, GL_LINK_STATUS, &linkResult);
GLuint newProgram = glCreateProgram();
glAttachShader(newProgram, newVertexShader);
glAttachShader(newProgram, newFragmentShader);
glLinkProgram(newProgram);
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(mProgram, errorMessageSize, &errorBufferSize, errorMessage);
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage);
glDeleteProgram(newProgram);
glDeleteShader(newVertexShader);
glDeleteShader(newFragmentShader);
return false;
}
destroyShaderProgram();
mProgram = newProgram;
mVertexShader = newVertexShader;
mFragmentShader = newFragmentShader;
mUYVYtexUniform = glGetUniformLocation(mProgram, "UYVYtex");
return true;
}