diff --git a/CMakeLists.txt b/CMakeLists.txt index 69efb7c..6bb6ef3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ set(APP_SOURCES "${APP_DIR}/gl/OpenGLComposite.cpp" "${APP_DIR}/gl/OpenGLComposite.h" "${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp" + "${APP_DIR}/gl/OpenGLDeckLinkBridge.cpp" + "${APP_DIR}/gl/OpenGLDeckLinkBridge.h" "${APP_DIR}/gl/OpenGLRenderPass.cpp" "${APP_DIR}/gl/OpenGLRenderPass.h" "${APP_DIR}/gl/OpenGLRenderer.cpp" diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index cdb38a8..3d9d059 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -45,6 +45,7 @@ #include "OpenGLComposite.h" #include "GLExtensions.h" #include "GlRenderConstants.h" +#include "OpenGLDeckLinkBridge.h" #include "OpenGLRenderPass.h" #include "OpenGLShaderPrograms.h" #include "OscServer.h" @@ -61,6 +62,15 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : { InitializeCriticalSection(&pMutex); mRuntimeHost = std::make_unique(); + mDeckLinkBridge = std::make_unique( + *mDeckLink, + *mRenderer, + *mRuntimeHost, + pMutex, + hGLDC, + hGLRC, + [this]() { renderEffect(); }, + [this]() { paintGL(); }); mRenderPass = std::make_unique(*mRenderer); mShaderPrograms = std::make_unique(*mRenderer, *mRuntimeHost); mControlServer = std::make_unique(); @@ -277,158 +287,15 @@ bool OpenGLComposite::InitOpenGLState() return true; } -// -// Update the captured video frame texture -// +// DeckLink delegates still target OpenGLComposite; the bridge owns the per-frame work. void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) { - mDeckLink->hasNoInputSource = hasNoInputSource; - if (mRuntimeHost) - mRuntimeHost->SetSignalStatus(!hasNoInputSource, mDeckLink->inputFrameWidth, mDeckLink->inputFrameHeight, mDeckLink->inputDisplayModeName); - - if (mDeckLink->hasNoInputSource) - return; // don't transfer texture when there's no input - - long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight(); - IDeckLinkVideoBuffer* inputFrameBuffer = NULL; - void* videoPixels; - - if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK) - return; - - if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK) - { - inputFrameBuffer->Release(); - return; - } - inputFrameBuffer->GetBytes(&videoPixels); - - EnterCriticalSection(&pMutex); - - wglMakeCurrent( hGLDC, hGLRC ); // make OpenGL context current in this thread - - if (mRenderer->FastTransferAvailable()) - { - CComQIPtr allocator(inputFrameBuffer); - if (!allocator || !allocator->transferFrame(videoPixels, mRenderer->CaptureTexture())) - OutputDebugStringA("Capture: transferFrame() failed\n"); - - allocator->waitForTransferComplete(videoPixels); - } - else - { - // Use a straightforward texture buffer - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer->UnpinnedTextureBuffer()); - glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW); - glBindTexture(GL_TEXTURE_2D, mRenderer->CaptureTexture()); - - // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink->inputFrameWidth / 2, mDeckLink->inputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); - - glBindTexture(GL_TEXTURE_2D, 0); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - } - - wglMakeCurrent( NULL, NULL ); - - LeaveCriticalSection(&pMutex); - - inputFrameBuffer->EndAccess(bmdBufferAccessRead); - inputFrameBuffer->Release(); + mDeckLinkBridge->VideoFrameArrived(inputFrame, hasNoInputSource); } -// 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); - - // Get the first frame from the queue - IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink->outputVideoFrameQueue.front(); - mDeckLink->outputVideoFrameQueue.push_back(outputVideoFrame); - mDeckLink->outputVideoFrameQueue.pop_front(); - - // make GL context current in this thread - wglMakeCurrent( hGLDC, hGLRC ); - - // Draw the effect output to the off-screen framebuffer. - const auto renderStartTime = std::chrono::steady_clock::now(); - if (mRenderer->FastTransferAvailable()) - VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU); - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->CompositeFramebuffer()); - renderEffect(); - glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->CompositeFramebuffer()); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer->OutputFramebuffer()); - glBlitFramebuffer(0, 0, mDeckLink->inputFrameWidth, mDeckLink->inputFrameHeight, 0, 0, mDeckLink->outputFrameWidth, mDeckLink->outputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); - glBindFramebuffer(GL_FRAMEBUFFER, mRenderer->OutputFramebuffer()); - glFlush(); - if (mRenderer->FastTransferAvailable()) - VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU); - const auto renderEndTime = std::chrono::steady_clock::now(); - if (mRuntimeHost) - { - const double frameBudgetMilliseconds = mDeckLink->frameTimescale != 0 - ? (static_cast(mDeckLink->frameDuration) * 1000.0) / static_cast(mDeckLink->frameTimescale) - : 0.0; - const double renderMilliseconds = std::chrono::duration_cast>(renderEndTime - renderStartTime).count(); - mRuntimeHost->SetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); - } - if (mRuntimeHost) - mRuntimeHost->AdvanceFrame(); - - IDeckLinkVideoBuffer* outputVideoFrameBuffer; - if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) - { - LeaveCriticalSection(&pMutex); - return; - } - - if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK) - { - outputVideoFrameBuffer->Release(); - LeaveCriticalSection(&pMutex); - return; - } - - void* pFrame; - outputVideoFrameBuffer->GetBytes(&pFrame); - - if (mRenderer->FastTransferAvailable()) - { - // Finished sampling the capture texture for this frame. - if (!mDeckLink->hasNoInputSource) - VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); - - if (! mDeckLink->playoutAllocator->transferFrame(pFrame, mRenderer->OutputTexture())) - OutputDebugStringA("Playback: transferFrame() failed\n"); - - paintGL(); - - // Wait for transfer to system memory to complete - mDeckLink->playoutAllocator->waitForTransferComplete(pFrame); - } - else - { - glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer()); - glReadPixels(0, 0, mDeckLink->outputFrameWidth, mDeckLink->outputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); - paintGL(); - } - - outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); - outputVideoFrameBuffer->Release(); - - // If the last completed frame was late or dropped, bump the scheduled time further into the future - if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped) - mDeckLink->totalPlayoutFrames += 2; - - // Schedule the next frame for playout - HRESULT hr = mDeckLink->output->ScheduleVideoFrame(outputVideoFrame, (mDeckLink->totalPlayoutFrames * mDeckLink->frameDuration), mDeckLink->frameDuration, mDeckLink->frameTimescale); - if (SUCCEEDED(hr)) - mDeckLink->totalPlayoutFrames++; - - wglMakeCurrent( NULL, NULL ); - - LeaveCriticalSection(&pMutex); + mDeckLinkBridge->PlayoutFrameCompleted(completedFrame, completionResult); } bool OpenGLComposite::Start() diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h index 5fbeff7..923fbda 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h @@ -63,12 +63,10 @@ #include #include -class PlayoutDelegate; -class CaptureDelegate; -class PinnedMemoryAllocator; class ControlServer; class DeckLinkSession; class OscServer; +class OpenGLDeckLinkBridge; class OpenGLRenderPass; class OpenGLShaderPrograms; @@ -116,6 +114,7 @@ private: std::unique_ptr mDeckLink; std::unique_ptr mRenderer; std::unique_ptr mRuntimeHost; + std::unique_ptr mDeckLinkBridge; std::unique_ptr mRenderPass; std::unique_ptr mShaderPrograms; std::unique_ptr mControlServer; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp new file mode 100644 index 0000000..6608aae --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp @@ -0,0 +1,177 @@ +#include "OpenGLDeckLinkBridge.h" + +#include "DeckLinkFrameTransfer.h" +#include "DeckLinkSession.h" +#include "OpenGLRenderer.h" +#include "RuntimeHost.h" +#include "VideoFrameTransfer.h" + +#include +#include +#include + +OpenGLDeckLinkBridge::OpenGLDeckLinkBridge( + DeckLinkSession& deckLink, + OpenGLRenderer& renderer, + RuntimeHost& runtimeHost, + CRITICAL_SECTION& mutex, + HDC hdc, + HGLRC hglrc, + RenderEffectCallback renderEffect, + PaintCallback paint) : + mDeckLink(deckLink), + mRenderer(renderer), + mRuntimeHost(runtimeHost), + mMutex(mutex), + mHdc(hdc), + mHglrc(hglrc), + mRenderEffect(renderEffect), + mPaint(paint) +{ +} + +void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) +{ + mDeckLink.hasNoInputSource = hasNoInputSource; + mRuntimeHost.SetSignalStatus(!hasNoInputSource, mDeckLink.inputFrameWidth, mDeckLink.inputFrameHeight, mDeckLink.inputDisplayModeName); + + if (mDeckLink.hasNoInputSource) + return; // don't transfer texture when there's no input + + long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight(); + IDeckLinkVideoBuffer* inputFrameBuffer = NULL; + void* videoPixels; + + if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK) + return; + + if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK) + { + inputFrameBuffer->Release(); + return; + } + inputFrameBuffer->GetBytes(&videoPixels); + + EnterCriticalSection(&mMutex); + + wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread + + if (mRenderer.FastTransferAvailable()) + { + CComQIPtr allocator(inputFrameBuffer); + if (!allocator || !allocator->transferFrame(videoPixels, mRenderer.CaptureTexture())) + OutputDebugStringA("Capture: transferFrame() failed\n"); + + allocator->waitForTransferComplete(videoPixels); + } + else + { + // Use a straightforward texture buffer + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.UnpinnedTextureBuffer()); + glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW); + glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture()); + + // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.inputFrameWidth / 2, mDeckLink.inputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + + glBindTexture(GL_TEXTURE_2D, 0); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + } + + wglMakeCurrent(NULL, NULL); + + LeaveCriticalSection(&mMutex); + + inputFrameBuffer->EndAccess(bmdBufferAccessRead); + inputFrameBuffer->Release(); +} + +void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) +{ + (void)completedFrame; + + EnterCriticalSection(&mMutex); + + // Get the first frame from the queue + IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink.outputVideoFrameQueue.front(); + mDeckLink.outputVideoFrameQueue.push_back(outputVideoFrame); + mDeckLink.outputVideoFrameQueue.pop_front(); + + // make GL context current in this thread + wglMakeCurrent(mHdc, mHglrc); + + // Draw the effect output to the off-screen framebuffer. + const auto renderStartTime = std::chrono::steady_clock::now(); + if (mRenderer.FastTransferAvailable()) + VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); + mRenderEffect(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer()); + glBlitFramebuffer(0, 0, mDeckLink.inputFrameWidth, mDeckLink.inputFrameHeight, 0, 0, mDeckLink.outputFrameWidth, mDeckLink.outputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer()); + glFlush(); + if (mRenderer.FastTransferAvailable()) + VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU); + const auto renderEndTime = std::chrono::steady_clock::now(); + const double frameBudgetMilliseconds = mDeckLink.frameTimescale != 0 + ? (static_cast(mDeckLink.frameDuration) * 1000.0) / static_cast(mDeckLink.frameTimescale) + : 0.0; + const double renderMilliseconds = std::chrono::duration_cast>(renderEndTime - renderStartTime).count(); + mRuntimeHost.SetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); + mRuntimeHost.AdvanceFrame(); + + IDeckLinkVideoBuffer* outputVideoFrameBuffer; + if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) + { + LeaveCriticalSection(&mMutex); + return; + } + + if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK) + { + outputVideoFrameBuffer->Release(); + LeaveCriticalSection(&mMutex); + return; + } + + void* pFrame; + outputVideoFrameBuffer->GetBytes(&pFrame); + + if (mRenderer.FastTransferAvailable()) + { + // Finished sampling the capture texture for this frame. + if (!mDeckLink.hasNoInputSource) + VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); + + if (!mDeckLink.playoutAllocator->transferFrame(pFrame, mRenderer.OutputTexture())) + OutputDebugStringA("Playback: transferFrame() failed\n"); + + mPaint(); + + // Wait for transfer to system memory to complete + mDeckLink.playoutAllocator->waitForTransferComplete(pFrame); + } + else + { + glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer()); + glReadPixels(0, 0, mDeckLink.outputFrameWidth, mDeckLink.outputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); + mPaint(); + } + + outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); + outputVideoFrameBuffer->Release(); + + // If the last completed frame was late or dropped, bump the scheduled time further into the future + if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped) + mDeckLink.totalPlayoutFrames += 2; + + // Schedule the next frame for playout + HRESULT hr = mDeckLink.output->ScheduleVideoFrame(outputVideoFrame, (mDeckLink.totalPlayoutFrames * mDeckLink.frameDuration), mDeckLink.frameDuration, mDeckLink.frameTimescale); + if (SUCCEEDED(hr)) + mDeckLink.totalPlayoutFrames++; + + wglMakeCurrent(NULL, NULL); + + LeaveCriticalSection(&mMutex); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h new file mode 100644 index 0000000..55803e4 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h @@ -0,0 +1,41 @@ +#pragma once + +#include "DeckLinkAPI_h.h" + +#include + +#include + +class DeckLinkSession; +class OpenGLRenderer; +class RuntimeHost; + +class OpenGLDeckLinkBridge +{ +public: + using RenderEffectCallback = std::function; + using PaintCallback = std::function; + + OpenGLDeckLinkBridge( + DeckLinkSession& deckLink, + OpenGLRenderer& renderer, + RuntimeHost& runtimeHost, + CRITICAL_SECTION& mutex, + HDC hdc, + HGLRC hglrc, + RenderEffectCallback renderEffect, + PaintCallback paint); + + void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); + void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult); + +private: + DeckLinkSession& mDeckLink; + OpenGLRenderer& mRenderer; + RuntimeHost& mRuntimeHost; + CRITICAL_SECTION& mMutex; + HDC mHdc; + HGLRC mHglrc; + RenderEffectCallback mRenderEffect; + PaintCallback mPaint; +};