#include "DeckLinkDisplayMode.h" #include "OpenGLComposite.h" #include "GLExtensions.h" #include "PngScreenshotWriter.h" #include "RenderEngine.h" #include "RuntimeCoordinator.h" #include "RuntimeEventDispatcher.h" #include "RuntimeServiceLiveBridge.h" #include "RuntimeServices.h" #include "RuntimeSnapshotProvider.h" #include "RuntimeStore.h" #include "RuntimeUpdateController.h" #include "ShaderBuildQueue.h" #include "VideoBackend.h" #include #include #include #include #include #include #include #include OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC) { mRuntimeStore = std::make_unique(); mRuntimeEventDispatcher = std::make_unique(); mRuntimeSnapshotProvider = std::make_unique(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher); mRuntimeCoordinator = std::make_unique(*mRuntimeStore, *mRuntimeEventDispatcher); mRenderEngine = std::make_unique( *mRuntimeSnapshotProvider, mRuntimeStore->GetHealthTelemetry(), hGLDC, hGLRC, [this]() { renderEffect(); }, []() {}, [this]() { paintGL(false); }); mVideoBackend = std::make_unique(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher); mShaderBuildQueue = std::make_unique(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher); mRuntimeServices = std::make_unique(*mRuntimeEventDispatcher); mRuntimeUpdateController = std::make_unique( *mRuntimeStore, *mRuntimeCoordinator, *mRuntimeEventDispatcher, *mRuntimeServices, *mRenderEngine, *mShaderBuildQueue, *mVideoBackend); } OpenGLComposite::~OpenGLComposite() { if (mRuntimeServices) mRuntimeServices->Stop(); if (mShaderBuildQueue) mShaderBuildQueue->Stop(); if (mVideoBackend) mVideoBackend->ReleaseResources(); } bool OpenGLComposite::InitDeckLink() { return InitVideoIO(); } bool OpenGLComposite::InitVideoIO() { VideoFormatSelection videoModes; std::string initFailureReason; if (mRuntimeStore && mRuntimeStore->GetRuntimeRepositoryRoot().empty()) { std::string runtimeError; if (!mRuntimeStore->InitializeStore(runtimeError)) { MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK); return false; } } if (mRuntimeStore) { if (!ResolveConfiguredVideoFormats( mRuntimeStore->GetConfiguredInputVideoFormat(), mRuntimeStore->GetConfiguredInputFrameRate(), mRuntimeStore->GetConfiguredOutputVideoFormat(), mRuntimeStore->GetConfiguredOutputFrameRate(), videoModes, initFailureReason)) { MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK); return false; } } if (!mVideoBackend->DiscoverDevicesAndModes(videoModes, initFailureReason)) { const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application." ? "This application requires the DeckLink drivers installed." : "DeckLink initialization failed"; MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR); return false; } const bool outputAlphaRequired = mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(); if (!mVideoBackend->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason)) goto error; if (! CheckOpenGLExtensions()) { initFailureReason = "OpenGL extension checks failed."; goto error; } if (! InitOpenGLState()) { initFailureReason = "OpenGL state initialization failed."; goto error; } mVideoBackend->PublishStatus( mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), mVideoBackend->OutputModelName().empty() ? "DeckLink output device selected." : ("Selected output device: " + mVideoBackend->OutputModelName())); // Resize window to match output video frame, but scale large formats down by half for viewing. if (mVideoBackend->OutputFrameWidth() < 1920) resizeWindow(mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()); else resizeWindow(mVideoBackend->OutputFrameWidth() / 2, mVideoBackend->OutputFrameHeight() / 2); if (!mVideoBackend->ConfigureInput(videoModes.input, initFailureReason)) { goto error; } if (!mVideoBackend->HasInputDevice()) mVideoBackend->ReportNoInputDeviceSignalStatus(); if (!mVideoBackend->ConfigureOutput(videoModes.output, mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), initFailureReason)) { goto error; } mVideoBackend->PublishStatus( mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), mVideoBackend->StatusMessage()); return true; error: if (!initFailureReason.empty()) MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR); mVideoBackend->ReleaseResources(); return false; } void OpenGLComposite::paintGL(bool force) { if (!force) { if (IsIconic(hGLWnd)) return; } const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u; if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight())) { ValidateRect(hGLWnd, NULL); return; } ValidateRect(hGLWnd, NULL); } void OpenGLComposite::resizeGL(WORD width, WORD height) { // We don't set the project or model matrices here since the window data is copied directly from // an off-screen FBO in paintGL(). Just save the width and height for use in paintGL(). mRenderEngine->ResizeView(width, height); } void OpenGLComposite::resizeWindow(int width, int height) { RECT r; if (GetWindowRect(hGLWnd, &r)) { SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0); } } bool OpenGLComposite::InitOpenGLState() { if (! ResolveGLExtensions()) return false; std::string runtimeError; if (mRuntimeStore->GetRuntimeRepositoryRoot().empty() && !mRuntimeStore->InitializeStore(runtimeError)) { MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK); return false; } if (!mRuntimeServices->Start(*this, *mRuntimeStore, runtimeError)) { MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK); return false; } // Prepare the runtime shader program generated from the active shader package. char compilerErrorMessage[1024]; if (!mRenderEngine->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage)) { MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK); return false; } if (!mRenderEngine->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage)) { MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK); return false; } std::string rendererError; if (!mRenderEngine->InitializeResources( mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), mVideoBackend->CaptureTextureWidth(), mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight(), mVideoBackend->OutputPackTextureWidth(), rendererError)) { MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK); return false; } if (!mRenderEngine->CompileLayerPrograms(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage)) { MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK); return false; } mRuntimeStore->SetCompileStatus(true, "Shader layers compiled successfully."); mRenderEngine->ResetTemporalHistoryState(); mRenderEngine->ResetShaderFeedbackState(); mRuntimeUpdateController->BroadcastRuntimeState(); mRuntimeServices->BeginPolling(*mRuntimeCoordinator); return true; } bool OpenGLComposite::Start() { if (!mRenderEngine->StartRenderThread()) return false; if (mVideoBackend->Start()) return true; mRenderEngine->StopRenderThread(); return false; } bool OpenGLComposite::Stop() { if (mRuntimeServices) mRuntimeServices->Stop(); const bool wasExternalKeyingActive = mVideoBackend->ExternalKeyingActive(); mVideoBackend->Stop(); if (wasExternalKeyingActive) mVideoBackend->PublishStatus( mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), "External keying has been disabled."); if (mRenderEngine) mRenderEngine->StopRenderThread(); return true; } bool OpenGLComposite::ReloadShader(bool preserveFeedbackState) { return mRuntimeCoordinator && mRuntimeUpdateController && mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState)); } bool OpenGLComposite::RequestScreenshot(std::string& error) { if (!mRenderEngine || !mVideoBackend) { error = "The render engine is not ready."; return false; } const unsigned width = mVideoBackend->OutputFrameWidth(); const unsigned height = mVideoBackend->OutputFrameHeight(); if (width == 0 || height == 0) { error = "The output frame size is not available."; return false; } std::filesystem::path outputPath; try { outputPath = BuildScreenshotPath(); std::filesystem::create_directories(outputPath.parent_path()); } catch (const std::exception& exception) { error = exception.what(); return false; } if (!mRenderEngine->RequestScreenshotCapture( width, height, [outputPath](unsigned captureWidth, unsigned captureHeight, std::vector topDownPixels) { try { WritePngFileAsync(outputPath, captureWidth, captureHeight, std::move(topDownPixels)); } catch (const std::exception& exception) { OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str()); } })) { error = "Screenshot capture request failed."; return false; } return true; } void OpenGLComposite::renderEffect() { if (mRuntimeUpdateController) mRuntimeUpdateController->ProcessRuntimeWork(); const RenderFrameInput frameInput = BuildRenderFrameInput(); RenderFrame(frameInput); } RenderFrameInput OpenGLComposite::BuildRenderFrameInput() const { RenderFrameInput frameInput; frameInput.useCommittedLayerStates = mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates(); frameInput.hasInputSource = mVideoBackend->HasInputSource(); frameInput.renderWidth = mVideoBackend->InputFrameWidth(); frameInput.renderHeight = mVideoBackend->InputFrameHeight(); frameInput.inputFrameWidth = mVideoBackend->InputFrameWidth(); frameInput.inputFrameHeight = mVideoBackend->InputFrameHeight(); frameInput.captureTextureWidth = mVideoBackend->CaptureTextureWidth(); frameInput.inputPixelFormat = mVideoBackend->InputPixelFormat(); frameInput.historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0; frameInput.oscSmoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0; return frameInput; } void OpenGLComposite::RenderFrame(const RenderFrameInput& frameInput) { RenderFrameState frameState; if (mRuntimeServices) { RuntimeServiceLiveBridge::PrepareLiveRenderFrameState( *mRuntimeServices, *mRenderEngine, frameInput, frameState); } else { mRenderEngine->ResolveRenderFrameState(frameInput, nullptr, frameState); } mRenderEngine->RenderPreparedFrame(frameState); } std::filesystem::path OpenGLComposite::BuildScreenshotPath() const { const std::filesystem::path root = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty() ? mRuntimeStore->GetRuntimeDataRoot() : std::filesystem::current_path(); const auto now = std::chrono::system_clock::now(); const auto milliseconds = std::chrono::duration_cast(now.time_since_epoch()) % 1000; const std::time_t nowTime = std::chrono::system_clock::to_time_t(now); std::tm localTime = {}; localtime_s(&localTime, &nowTime); std::ostringstream filename; filename << "video-shader-toys-" << std::put_time(&localTime, "%Y%m%d-%H%M%S") << "-" << std::setw(3) << std::setfill('0') << milliseconds.count() << ".png"; return root / "screenshots" / filename.str(); } bool OpenGLComposite::CheckOpenGLExtensions() { return true; }