#include "DeckLinkDisplayMode.h" #include "OpenGLComposite.h" #include "GLExtensions.h" #include "GlRenderConstants.h" #include "PngScreenshotWriter.h" #include "RenderEngine.h" #include "RuntimeParameterUtils.h" #include "RuntimeServices.h" #include "ShaderBuildQueue.h" #include "VideoBackend.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { } OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC), mScreenshotRequested(false) { InitializeCriticalSection(&pMutex); mRuntimeHost = std::make_unique(); mRuntimeStore = std::make_unique(*mRuntimeHost); mRuntimeSnapshotProvider = std::make_unique(*mRuntimeHost); mRuntimeCoordinator = std::make_unique(*mRuntimeStore); mRenderEngine = std::make_unique( *mRuntimeSnapshotProvider, mRuntimeHost->GetHealthTelemetry(), pMutex, hGLDC, hGLRC, [this]() { renderEffect(); }, [this]() { ProcessScreenshotRequest(); }, [this]() { paintGL(false); }); mVideoBackend = std::make_unique(*mRenderEngine, mRuntimeHost->GetHealthTelemetry()); mShaderBuildQueue = std::make_unique(*mRuntimeSnapshotProvider); mRuntimeServices = std::make_unique(); } OpenGLComposite::~OpenGLComposite() { if (mRuntimeServices) mRuntimeServices->Stop(); if (mShaderBuildQueue) mShaderBuildQueue->Stop(); if (mVideoBackend) mVideoBackend->ReleaseResources(); DeleteCriticalSection(&pMutex); } 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, *mRuntimeHost, 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(); broadcastRuntimeState(); mRuntimeServices->BeginPolling(*mRuntimeStore); return true; } bool OpenGLComposite::Start() { return mVideoBackend->Start(); } 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."); return true; } bool OpenGLComposite::ReloadShader(bool preserveFeedbackState) { return mRuntimeCoordinator && ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState)); } bool OpenGLComposite::RequestScreenshot(std::string& error) { (void)error; mScreenshotRequested.store(true); return true; } void OpenGLComposite::renderEffect() { ProcessRuntimePollResults(); std::vector appliedOscUpdates; std::vector completedOscCommits; if (mRuntimeServices) { std::string oscError; if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty()) OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str()); mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits); } std::vector overlayUpdates; overlayUpdates.reserve(appliedOscUpdates.size()); for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates) { overlayUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue }); } std::vector overlayCommitCompletions; overlayCommitCompletions.reserve(completedOscCommits.size()); for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits) { overlayCommitCompletions.push_back({ completedCommit.routeKey, completedCommit.generation }); } if (mRenderEngine) mRenderEngine->UpdateOscOverlayState(overlayUpdates, overlayCommitCompletions); const bool hasInputSource = mVideoBackend->HasInputSource(); std::vector layerStates; std::vector overlayCommitRequests; const double smoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0; mRenderEngine->ResolveRenderLayerStates( mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates(), mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), smoothing, &overlayCommitRequests, layerStates); if (mRuntimeServices) { for (const RenderEngine::OscOverlayCommitRequest& commitRequest : overlayCommitRequests) { std::string commitError; if (!mRuntimeServices->QueueOscCommit( commitRequest.routeKey, commitRequest.layerKey, commitRequest.parameterKey, commitRequest.value, commitRequest.generation, commitError) && !commitError.empty()) { OutputDebugStringA(("OSC commit queue failed: " + commitError + "\n").c_str()); } } } const unsigned historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0; mRenderEngine->RenderLayerStack( hasInputSource, layerStates, mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), mVideoBackend->CaptureTextureWidth(), mVideoBackend->InputPixelFormat(), historyCap); } void OpenGLComposite::ProcessScreenshotRequest() { if (!mScreenshotRequested.exchange(false)) return; const unsigned width = mVideoBackend ? mVideoBackend->OutputFrameWidth() : 0; const unsigned height = mVideoBackend ? mVideoBackend->OutputFrameHeight() : 0; if (width == 0 || height == 0) return; std::vector topDownPixels; if (!mRenderEngine->CaptureOutputFrameRgbaTopDown(width, height, topDownPixels)) return; try { const std::filesystem::path outputPath = BuildScreenshotPath(); std::filesystem::create_directories(outputPath.parent_path()); WritePngFileAsync(outputPath, width, height, std::move(topDownPixels)); } catch (const std::exception& exception) { OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str()); } } 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::ProcessRuntimePollResults() { if (!mRuntimeServices) return true; const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents(); if (events.failed) { ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimePollFailure(events.error)); return false; } if (events.registryChanged) broadcastRuntimeState(); if (!events.reloadRequested) { if (!mShaderBuildQueue || !mRenderEngine) return true; const RenderEngine::PreparedShaderBuildApplyResult buildResult = mRenderEngine->TryApplyReadyShaderBuild( *mShaderBuildQueue, mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), mRuntimeCoordinator && mRuntimeCoordinator->PreserveFeedbackOnNextShaderBuild()); if (!buildResult.hadReadyBuild) return true; if (!buildResult.applied) { ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildFailure(buildResult.errorMessage)); return false; } ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildSuccess()); return true; } ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimeReloadRequest()); return true; } void OpenGLComposite::RequestShaderBuild() { if (!mShaderBuildQueue || !mVideoBackend) return; mShaderBuildQueue->RequestBuild(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight()); } bool OpenGLComposite::ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error) { if (!result.accepted) { if (error) *error = result.errorMessage; return false; } if (result.compileStatusChanged && mRuntimeStore) mRuntimeStore->SetCompileStatus(result.compileStatusSucceeded, result.compileStatusMessage); if (result.clearReloadRequest && mRuntimeStore) mRuntimeStore->ClearReloadRequest(); if (mRuntimeCoordinator) mRuntimeCoordinator->ApplyCommittedStateMode(result.committedStateMode); if (result.clearTransientOscState) { if (mRenderEngine) mRenderEngine->ClearOscOverlayState(); if (mRuntimeServices) mRuntimeServices->ClearOscState(); } if (mRenderEngine) mRenderEngine->ApplyRuntimeCoordinatorRenderReset(result.renderResetScope); if (result.shaderBuildRequested) RequestShaderBuild(); if (result.runtimeStateBroadcastRequired) broadcastRuntimeState(); return true; } void OpenGLComposite::broadcastRuntimeState() { if (mRuntimeServices) mRuntimeServices->BroadcastState(); } bool OpenGLComposite::CheckOpenGLExtensions() { return true; } ////////////////////////////////////////////