diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 22711d7..a1043ad 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -171,7 +171,8 @@ jobs: run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force - name: Upload Runtime Package - uses: actions/upload-artifact@v4 + # Gitea/GHES-compatible runners do not support the v4 artifact backend yet. + uses: actions/upload-artifact@v3 with: name: VideoShader-windows-release path: dist/VideoShader.zip diff --git a/README.md b/README.md index 5bfc827..ee15199 100644 --- a/README.md +++ b/README.md @@ -249,9 +249,7 @@ If neither variable is set, the workflow falls back to the repo-local defaults u - Audio. - Improve text rendering. - Genlock. -- Don't hardfail on shader fail -- Find a better UI library. +- Find a better UI library for react. - Logs. - Continue source cleanup/refactoring. Pass 1 done -- Display the control URL in the Windows app, ideally clickable, without rendering it on the video output. - Support a separate sound shader `.slang` file in shader packages. diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp index 38591a2..a5cf5c3 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.cpp @@ -46,6 +46,10 @@ #include "resource.h" #include "OpenGLComposite.h" +#include +#include +#include + #ifndef WGL_CONTEXT_MAJOR_VERSION_ARB #define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 #endif @@ -65,6 +69,140 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList); +namespace +{ +const int kStatusStripHeight = 92; +const int kStatusPadding = 8; +const int kStatusLabelWidth = 58; +const int kStatusButtonWidth = 86; +const int kStatusRowHeight = 24; +const int kStatusGap = 6; +const UINT kCreateStatusStripMessage = WM_APP + 1; + +enum StatusControlId +{ + kControlUrlEditId = 2001, + kDocsUrlEditId = 2002, + kOscAddressEditId = 2003, + kOpenControlButtonId = 2004, + kOpenDocsButtonId = 2005 +}; + +struct StatusStripControls +{ + HWND panel = NULL; + HWND controlLabel = NULL; + HWND controlUrl = NULL; + HWND openControl = NULL; + HWND docsLabel = NULL; + HWND docsUrl = NULL; + HWND openDocs = NULL; + HWND oscLabel = NULL; + HWND oscAddress = NULL; +}; + +bool StatusStripCreated(const StatusStripControls& controls) +{ + return controls.panel != NULL; +} + +HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, int controlId) +{ + return CreateWindowExA( + 0, + className, + text, + WS_CHILD | WS_VISIBLE | style, + 0, + 0, + 0, + 0, + parent, + reinterpret_cast(static_cast(controlId)), + reinterpret_cast(GetWindowLongPtr(parent, GWLP_HINSTANCE)), + NULL); +} + +void CreateStatusStrip(HWND hWnd, StatusStripControls& controls) +{ + controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_NOTIFY, 0); + controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0); + controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY, kControlUrlEditId); + controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON, kOpenControlButtonId); + controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0); + controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY, kDocsUrlEditId); + controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON, kOpenDocsButtonId); + controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0); + controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY, kOscAddressEditId); + + HFONT guiFont = reinterpret_cast(GetStockObject(DEFAULT_GUI_FONT)); + HWND children[] = { + controls.controlLabel, + controls.controlUrl, + controls.openControl, + controls.docsLabel, + controls.docsUrl, + controls.openDocs, + controls.oscLabel, + controls.oscAddress + }; + for (HWND child : children) + { + if (child) + SendMessage(child, WM_SETFONT, reinterpret_cast(guiFont), TRUE); + } + + SetWindowTextA(controls.controlUrl, "Starting control server..."); + SetWindowTextA(controls.docsUrl, "Starting API docs..."); + SetWindowTextA(controls.oscAddress, "Starting OSC listener..."); +} + +void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls) +{ + RECT clientRect = {}; + if (!GetClientRect(hWnd, &clientRect) || !controls.panel) + return; + + const int clientWidth = static_cast(clientRect.right - clientRect.left); + const int clientHeight = static_cast(clientRect.bottom - clientRect.top); + const int panelTop = std::max(0, clientHeight - kStatusStripHeight); + MoveWindow(controls.panel, 0, panelTop, clientWidth, kStatusStripHeight, TRUE); + + const int rowX = kStatusPadding; + const int editX = rowX + kStatusLabelWidth + kStatusGap; + const int buttonX = std::max(editX, clientWidth - kStatusPadding - kStatusButtonWidth); + const int editWidth = std::max(80, buttonX - editX - kStatusGap); + const int oscWidth = std::max(80, clientWidth - editX - kStatusPadding); + const int row1 = panelTop + kStatusPadding; + const int row2 = row1 + kStatusRowHeight + kStatusGap; + const int row3 = row2 + kStatusRowHeight + kStatusGap; + + MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE); + MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE); +} + +void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite) +{ + if (!StatusStripCreated(controls)) + return; + + SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str()); + SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str()); + SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str()); +} + +void OpenUrl(const char* url) +{ + ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL); +} +} + void ShowUnhandledExceptionMessage(const char* prefix) { try @@ -203,6 +341,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) static HDC hDC = NULL; // Private GDI Device context static OpenGLComposite* pOpenGLComposite = NULL; static bool sInteractiveResize = false; + static StatusStripControls sStatusStrip; switch (message) { @@ -251,7 +390,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { wglMakeCurrent( NULL, NULL ); if (pOpenGLComposite->Start()) + { + PostMessage(hWnd, kCreateStatusStripMessage, 0, 0); break; // success + } MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR); } else @@ -273,6 +415,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } } + case kCreateStatusStripMessage: + if (pOpenGLComposite) + { + if (!StatusStripCreated(sStatusStrip)) + CreateStatusStrip(hWnd, sStatusStrip); + + UpdateStatusStrip(sStatusStrip, *pOpenGLComposite); + LayoutStatusStrip(hWnd, sStatusStrip); + InvalidateRect(hWnd, NULL, FALSE); + } + break; + case WM_DESTROY: try { @@ -313,6 +467,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_SIZE: try { + if (StatusStripCreated(sStatusStrip)) + LayoutStatusStrip(hWnd, sStatusStrip); if (pOpenGLComposite) pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam)); } @@ -361,6 +517,28 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case kOpenControlButtonId: + if (pOpenGLComposite) + { + std::string url = pOpenGLComposite->GetControlUrl(); + OpenUrl(url.c_str()); + } + break; + case kOpenDocsButtonId: + if (pOpenGLComposite) + { + std::string url = pOpenGLComposite->GetDocsUrl(); + OpenUrl(url.c_str()); + } + break; + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + break; + default: return (DefWindowProc(hWnd, message, wParam, lParam)); } diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h index 1fc5005..0b9e025 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h @@ -52,6 +52,11 @@ public: bool ResetLayerParameters(const std::string& layerId, std::string& error); bool SaveStackPreset(const std::string& presetName, std::string& error); bool LoadStackPreset(const std::string& presetName, std::string& error); + unsigned short GetControlServerPort() const; + unsigned short GetOscPort() const; + std::string GetControlUrl() const; + std::string GetDocsUrl() const; + std::string GetOscAddress() const; void resizeGL(WORD width, WORD height); void paintGL(); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp index f2a3a31..ac01bfd 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp @@ -5,6 +5,31 @@ std::string OpenGLComposite::GetRuntimeStateJson() const return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}"; } +unsigned short OpenGLComposite::GetControlServerPort() const +{ + return mRuntimeHost ? mRuntimeHost->GetServerPort() : 0; +} + +unsigned short OpenGLComposite::GetOscPort() const +{ + return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0; +} + +std::string OpenGLComposite::GetControlUrl() const +{ + return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/"; +} + +std::string OpenGLComposite::GetDocsUrl() const +{ + return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/docs"; +} + +std::string OpenGLComposite::GetOscAddress() const +{ + return "udp://127.0.0.1:" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}"; +} + bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error) { if (!mRuntimeHost->AddLayer(shaderId, error))