diff --git a/OSC/Test.json b/OSC/Test.json new file mode 100644 index 0000000..cb88f4d --- /dev/null +++ b/OSC/Test.json @@ -0,0 +1,104 @@ +{ + "createdWith": "Open Stage Control", + "version": "1.30.3", + "type": "session", + "content": { + "type": "root", + "lock": false, + "id": "root", + "visible": true, + "interaction": true, + "comments": "", + "width": "auto", + "height": "auto", + "colorText": "auto", + "colorWidget": "auto", + "alphaFillOn": "auto", + "borderRadius": "auto", + "padding": "auto", + "html": "", + "css": "", + "colorBg": "auto", + "layout": "default", + "justify": "start", + "gridTemplate": "", + "contain": true, + "scroll": true, + "innerPadding": true, + "tabsPosition": "top", + "hideMenu": false, + "variables": "@{parent.variables}", + "traversing": false, + "value": "", + "default": "", + "linkId": "", + "address": "auto", + "preArgs": "", + "typeTags": "", + "decimals": 2, + "target": "127.0.0.1:9000", + "ignoreDefaults": false, + "bypass": false, + "onCreate": "", + "onValue": "", + "onTouch": "", + "onPreload": "", + "widgets": [ + { + "type": "xy", + "top": 120, + "left": 120, + "lock": false, + "id": "fisheye_pan_tilt", + "visible": true, + "interaction": true, + "comments": "XY control for Fisheye Reproject pan and tilt.", + "width": 420, + "height": 420, + "expand": false, + "colorText": "auto", + "colorWidget": "auto", + "colorStroke": "auto", + "colorFill": "auto", + "alphaStroke": "auto", + "alphaFillOff": "auto", + "alphaFillOn": "auto", + "lineWidth": "auto", + "borderRadius": "auto", + "padding": "auto", + "html": "", + "css": "", + "design": "default", + "pips": true, + "snap": false, + "spring": false, + "rangeX": { + "min": -180, + "max": 180 + }, + "rangeY": { + "min": -120, + "max": 120 + }, + "logScaleX": false, + "logScaleY": false, + "sensitivity": 1, + "value": [0, 0], + "default": [0, 0], + "linkId": "", + "address": "/VideoShaderToys/fisheye-reproject/xy", + "preArgs": "", + "typeTags": "", + "decimals": "2f", + "target": "127.0.0.1:9000", + "ignoreDefaults": false, + "bypass": true, + "split": [], + "onCreate": "", + "onValue": "if (touch !== undefined) return;\nvar pan = Array.isArray(value) ? Number(value[0]) : 0;\nvar tilt = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/panDegrees', {type: 'f', value: pan});\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: 'f', value: tilt});", + "onTouch": "" + } + ], + "tabs": [] + } +} diff --git a/README.md b/README.md index 0219892..3a18a85 100644 --- a/README.md +++ b/README.md @@ -119,15 +119,20 @@ Current native test coverage includes: { "shaderLibrary": "shaders", "serverPort": 8080, - "videoFormat": "1080p", - "frameRate": "59.94", + "oscPort": 9000, + "inputVideoFormat": "1080p", + "inputFrameRate": "59.94", + "outputVideoFormat": "1080p", + "outputFrameRate": "59.94", "autoReload": true, "maxTemporalHistoryFrames": 12, "enableExternalKeying": true } ``` -`videoFormat` and `frameRate` select the DeckLink capture/playout display mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support. +`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support. + +Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present. The control UI is available at: diff --git a/SHADER_CONTRACT.md b/SHADER_CONTRACT.md index fd15464..fa1c460 100644 --- a/SHADER_CONTRACT.md +++ b/SHADER_CONTRACT.md @@ -133,7 +133,7 @@ Fields: - `uv`: normalized texture coordinates, usually `0..1`. - `sourceColor`: decoded RGBA source video at `uv`. - `inputResolution`: decoded input video resolution in pixels. -- `outputResolution`: output/render resolution in pixels. +- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode. - `time`: elapsed runtime time in seconds. - `frameCount`: incrementing frame counter. - `mixAmount`: runtime mix amount. diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp index 373f9d9..da032c3 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp @@ -192,6 +192,28 @@ bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::str return false; } +bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode) +{ + if (!iterator || !foundMode) + return false; + + *foundMode = NULL; + IDeckLinkDisplayMode* candidate = NULL; + while (iterator->Next(&candidate) == S_OK) + { + if (candidate->GetDisplayMode() == targetMode) + { + *foundMode = candidate; + return true; + } + + candidate->Release(); + candidate = NULL; + } + + return false; +} + class ScopedGlShader { public: @@ -295,8 +317,10 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : mCaptureDelegate(NULL), mPlayoutDelegate(NULL), mDLInput(NULL), mDLOutput(NULL), mDLKeyer(NULL), mPlayoutAllocator(NULL), - mFrameWidth(0), mFrameHeight(0), - mDisplayModeName("1080p59.94"), + mInputFrameWidth(0), mInputFrameHeight(0), + mOutputFrameWidth(0), mOutputFrameHeight(0), + mInputDisplayModeName("1080p59.94"), + mOutputDisplayModeName("1080p59.94"), mHasNoInputSource(true), mDeckLinkSupportsInternalKeying(false), mDeckLinkSupportsExternalKeying(false), @@ -307,10 +331,12 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : mDecodedTexture(0), mLayerTempTexture(0), mFBOTexture(0), + mOutputTexture(0), mUnpinnedTextureBuffer(0), mDecodeFrameBuf(0), mLayerTempFrameBuf(0), mIdFrameBuf(0), + mOutputFrameBuf(0), mIdColorBuf(0), mIdDepthBuf(0), mFullscreenVAO(0), @@ -407,6 +433,10 @@ OpenGLComposite::~OpenGLComposite() glDeleteTextures(1, &mLayerTempTexture); if (mFBOTexture != 0) glDeleteTextures(1, &mFBOTexture); + if (mOutputTexture != 0) + glDeleteTextures(1, &mOutputTexture); + if (mOutputFrameBuf != 0) + glDeleteFramebuffers(1, &mOutputFrameBuf); if (mUnpinnedTextureBuffer != 0) glDeleteBuffers(1, &mUnpinnedTextureBuffer); @@ -427,10 +457,14 @@ bool OpenGLComposite::InitDeckLink() IDeckLinkIterator* pDLIterator = NULL; IDeckLink* pDL = NULL; IDeckLinkProfileAttributes* deckLinkAttributes = NULL; - IDeckLinkDisplayModeIterator* pDLDisplayModeIterator = NULL; - IDeckLinkDisplayMode* pDLDisplayMode = NULL; - BMDDisplayMode displayMode = bmdModeHD1080p5994; // mode to use for capture and playout - std::string displayModeName = "1080p59.94"; + IDeckLinkDisplayModeIterator* pDLInputDisplayModeIterator = NULL; + IDeckLinkDisplayModeIterator* pDLOutputDisplayModeIterator = NULL; + IDeckLinkDisplayMode* pDLInputDisplayMode = NULL; + IDeckLinkDisplayMode* pDLOutputDisplayMode = NULL; + BMDDisplayMode inputDisplayMode = bmdModeHD1080p5994; + BMDDisplayMode outputDisplayMode = bmdModeHD1080p5994; + std::string inputDisplayModeName = "1080p59.94"; + std::string outputDisplayModeName = "1080p59.94"; int outputFrameRowBytes; HRESULT result; @@ -446,15 +480,23 @@ bool OpenGLComposite::InitDeckLink() if (mRuntimeHost) { - if (!ResolveConfiguredDisplayMode(mRuntimeHost->GetVideoFormat(), mRuntimeHost->GetFrameRate(), displayMode, displayModeName)) + if (!ResolveConfiguredDisplayMode(mRuntimeHost->GetInputVideoFormat(), mRuntimeHost->GetInputFrameRate(), inputDisplayMode, inputDisplayModeName)) { - const std::string error = "Unsupported DeckLink video format/frameRate in config/runtime-host.json: " + - mRuntimeHost->GetVideoFormat() + " / " + mRuntimeHost->GetFrameRate(); - MessageBoxA(NULL, error.c_str(), "DeckLink mode configuration error", MB_OK); + const std::string error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " + + mRuntimeHost->GetInputVideoFormat() + " / " + mRuntimeHost->GetInputFrameRate(); + MessageBoxA(NULL, error.c_str(), "DeckLink input mode configuration error", MB_OK); + return false; + } + if (!ResolveConfiguredDisplayMode(mRuntimeHost->GetOutputVideoFormat(), mRuntimeHost->GetOutputFrameRate(), outputDisplayMode, outputDisplayModeName)) + { + const std::string error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " + + mRuntimeHost->GetOutputVideoFormat() + " / " + mRuntimeHost->GetOutputFrameRate(); + MessageBoxA(NULL, error.c_str(), "DeckLink output mode configuration error", MB_OK); return false; } } - mDisplayModeName = displayModeName; + mInputDisplayModeName = inputDisplayModeName; + mOutputDisplayModeName = outputDisplayModeName; result = CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&pDLIterator); if (FAILED(result)) @@ -538,35 +580,48 @@ bool OpenGLComposite::InitDeckLink() goto error; } - if (mDLOutput->GetDisplayModeIterator(&pDLDisplayModeIterator) != S_OK) + if (mDLInput->GetDisplayModeIterator(&pDLInputDisplayModeIterator) != S_OK) { - MessageBox(NULL, _T("Cannot get Display Mode Iterator."), _T("DeckLink error."), MB_OK); + MessageBox(NULL, _T("Cannot get input Display Mode Iterator."), _T("DeckLink error."), MB_OK); goto error; } - while (pDLDisplayModeIterator->Next(&pDLDisplayMode) == S_OK) + if (!FindDeckLinkDisplayMode(pDLInputDisplayModeIterator, inputDisplayMode, &pDLInputDisplayMode)) { - if (pDLDisplayMode->GetDisplayMode() == displayMode) - break; - - pDLDisplayMode->Release(); - pDLDisplayMode = NULL; + const std::string error = "Cannot get specified input BMDDisplayMode for configured mode: " + inputDisplayModeName; + MessageBoxA(NULL, error.c_str(), "DeckLink input error.", MB_OK); + goto error; } - pDLDisplayModeIterator->Release(); - pDLDisplayModeIterator = NULL; + pDLInputDisplayModeIterator->Release(); + pDLInputDisplayModeIterator = NULL; - if (pDLDisplayMode == NULL) + if (mDLOutput->GetDisplayModeIterator(&pDLOutputDisplayModeIterator) != S_OK) { - const std::string error = "Cannot get specified BMDDisplayMode for configured mode: " + displayModeName; - MessageBoxA(NULL, error.c_str(), "DeckLink error.", MB_OK); + MessageBox(NULL, _T("Cannot get output Display Mode Iterator."), _T("DeckLink error."), MB_OK); goto error; } - mFrameWidth = pDLDisplayMode->GetWidth(); - mFrameHeight = pDLDisplayMode->GetHeight(); + if (!FindDeckLinkDisplayMode(pDLOutputDisplayModeIterator, outputDisplayMode, &pDLOutputDisplayMode)) + { + const std::string error = "Cannot get specified output BMDDisplayMode for configured mode: " + outputDisplayModeName; + MessageBoxA(NULL, error.c_str(), "DeckLink output error.", MB_OK); + goto error; + } + pDLOutputDisplayModeIterator->Release(); + pDLOutputDisplayModeIterator = NULL; + + mInputFrameWidth = pDLInputDisplayMode->GetWidth(); + mInputFrameHeight = pDLInputDisplayMode->GetHeight(); + mOutputFrameWidth = pDLOutputDisplayMode->GetWidth(); + mOutputFrameHeight = pDLOutputDisplayMode->GetHeight(); if (! CheckOpenGLExtensions()) goto error; + if (mInputFrameWidth != mOutputFrameWidth || mInputFrameHeight != mOutputFrameHeight) + { + mFastTransferExtensionAvailable = false; + OutputDebugStringA("Input/output dimensions differ; using regular OpenGL transfer fallback instead of fast transfer.\n"); + } if (! InitOpenGLState()) goto error; @@ -586,18 +641,18 @@ bool OpenGLComposite::InitDeckLink() mDeckLinkStatusMessage); } - pDLDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale); + pDLOutputDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale); - // Resize window to match video frame, but scale large formats down by half for viewing - if (mFrameWidth < 1920) - resizeWindow(mFrameWidth, mFrameHeight); + // Resize window to match output video frame, but scale large formats down by half for viewing. + if (mOutputFrameWidth < 1920) + resizeWindow(mOutputFrameWidth, mOutputFrameHeight); else - resizeWindow(mFrameWidth / 2, mFrameHeight / 2); + resizeWindow(mOutputFrameWidth / 2, mOutputFrameHeight / 2); if (mFastTransferExtensionAvailable) { // Initialize fast video frame transfers - if (! VideoFrameTransfer::initialize(mFrameWidth, mFrameHeight, mCaptureTexture, mFBOTexture)) + if (! VideoFrameTransfer::initialize(mInputFrameWidth, mInputFrameHeight, mCaptureTexture, mOutputTexture)) { MessageBox(NULL, _T("Cannot initialize video transfers."), _T("VideoFrameTransfer error."), MB_OK); goto error; @@ -608,7 +663,7 @@ bool OpenGLComposite::InitDeckLink() // Use custom allocators so we pin only once then recycle them CComPtr captureAllocator(new (std::nothrow) InputAllocatorPool(hGLDC, hGLRC)); - if (mDLInput->EnableVideoInputWithAllocatorProvider(displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault, captureAllocator) != S_OK) + if (mDLInput->EnableVideoInputWithAllocatorProvider(inputDisplayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault, captureAllocator) != S_OK) goto error; } @@ -616,13 +671,13 @@ bool OpenGLComposite::InitDeckLink() if (mDLInput->SetCallback(mCaptureDelegate) != S_OK) goto error; - if (mDLOutput->RowBytesForPixelFormat(bmdFormat8BitBGRA, mFrameWidth, &outputFrameRowBytes) != S_OK) + if (mDLOutput->RowBytesForPixelFormat(bmdFormat8BitBGRA, mOutputFrameWidth, &outputFrameRowBytes) != S_OK) goto error; // Use a custom allocator so we pin only once then recycle them - mPlayoutAllocator = new PinnedMemoryAllocator(hGLDC, hGLRC, VideoFrameTransfer::GPUtoCPU, 1, outputFrameRowBytes * mFrameHeight); + mPlayoutAllocator = new PinnedMemoryAllocator(hGLDC, hGLRC, VideoFrameTransfer::GPUtoCPU, 1, outputFrameRowBytes * mOutputFrameHeight); - if (mDLOutput->EnableVideoOutput(displayMode, bmdVideoOutputFlagDefault) != S_OK) + if (mDLOutput->EnableVideoOutput(outputDisplayMode, bmdVideoOutputFlagDefault) != S_OK) goto error; if (mDLOutput->QueryInterface(IID_IDeckLinkKeyer, (void**)&mDLKeyer) == S_OK && mDLKeyer != NULL) @@ -680,7 +735,7 @@ bool OpenGLComposite::InitDeckLink() if (mPlayoutAllocator->AllocateVideoBuffer(&outputFrameBuffer) != S_OK) goto error; - if (mDLOutput->CreateVideoFrameWithBuffer(mFrameWidth, mFrameHeight, outputFrameRowBytes, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, outputFrameBuffer, &outputFrame) != S_OK) + if (mDLOutput->CreateVideoFrameWithBuffer(mOutputFrameWidth, mOutputFrameHeight, outputFrameRowBytes, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, outputFrameBuffer, &outputFrame) != S_OK) goto error; mDLOutputVideoFrameQueue.push_back(outputFrame); @@ -723,10 +778,28 @@ error: pDL = NULL; } - if (pDLDisplayMode != NULL) + if (pDLInputDisplayMode != NULL) { - pDLDisplayMode->Release(); - pDLDisplayMode = NULL; + pDLInputDisplayMode->Release(); + pDLInputDisplayMode = NULL; + } + + if (pDLOutputDisplayMode != NULL) + { + pDLOutputDisplayMode->Release(); + pDLOutputDisplayMode = NULL; + } + + if (pDLInputDisplayModeIterator != NULL) + { + pDLInputDisplayModeIterator->Release(); + pDLInputDisplayModeIterator = NULL; + } + + if (pDLOutputDisplayModeIterator != NULL) + { + pDLOutputDisplayModeIterator->Release(); + pDLOutputDisplayModeIterator = NULL; } if (pDLIterator != NULL) @@ -757,9 +830,9 @@ void OpenGLComposite::paintGL() int destX = 0; int destY = 0; - if (mFrameWidth > 0 && mFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0) + if (mOutputFrameWidth > 0 && mOutputFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0) { - const double frameAspect = static_cast(mFrameWidth) / static_cast(mFrameHeight); + const double frameAspect = static_cast(mOutputFrameWidth) / static_cast(mOutputFrameHeight); const double viewAspect = static_cast(mViewWidth) / static_cast(mViewHeight); if (viewAspect > frameAspect) @@ -776,12 +849,12 @@ void OpenGLComposite::paintGL() } } - glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glViewport(0, 0, mViewWidth, mViewHeight); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBlitFramebuffer(0, 0, mOutputFrameWidth, mOutputFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); SwapBuffers(hGLDC); ValidateRect(hGLWnd, NULL); @@ -893,7 +966,7 @@ bool OpenGLComposite::InitOpenGLState() // Create texture with empty data, we will update it using glTexSubImage2D each frame. // The captured video is YCbCr 4:2:2 packed into a UYVY macropixel. OpenGL has no YCbCr format // so treat it as RGBA 4:4:4:4 by halving the width and using GL_RGBA internal format. - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mInputFrameWidth / 2, mInputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); glGenTextures(1, &mDecodedTexture); @@ -902,7 +975,7 @@ bool OpenGLComposite::InitOpenGLState() glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mInputFrameWidth, mInputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); glGenTextures(1, &mLayerTempTexture); @@ -911,7 +984,7 @@ bool OpenGLComposite::InitOpenGLState() glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mInputFrameWidth, mInputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); @@ -920,6 +993,7 @@ bool OpenGLComposite::InitOpenGLState() glGenFramebuffers(1, &mDecodeFrameBuf); glGenFramebuffers(1, &mLayerTempFrameBuf); glGenFramebuffers(1, &mIdFrameBuf); + glGenFramebuffers(1, &mOutputFrameBuf); glGenRenderbuffers(1, &mIdColorBuf); glGenRenderbuffers(1, &mIdDepthBuf); glGenVertexArrays(1, &mFullscreenVAO); @@ -952,11 +1026,11 @@ bool OpenGLComposite::InitOpenGLState() glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mInputFrameWidth, mInputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); // Attach a depth buffer glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mFrameWidth, mFrameHeight); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mInputFrameWidth, mInputFrameHeight); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf); @@ -970,6 +1044,23 @@ bool OpenGLComposite::InitOpenGLState() return false; } + glGenTextures(1, &mOutputTexture); + glBindTexture(GL_TEXTURE_2D, mOutputTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mOutputFrameWidth, mOutputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + + glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0); + glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (glStatus != GL_FRAMEBUFFER_COMPLETE) + { + MessageBox(NULL, _T("Cannot initialize output framebuffer."), _T("OpenGL initialization error."), MB_OK); + return false; + } + glBindTexture(GL_TEXTURE_2D, 0); glBindRenderbuffer(GL_RENDERBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0); @@ -991,7 +1082,7 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo { mHasNoInputSource = hasNoInputSource; if (mRuntimeHost) - mRuntimeHost->SetSignalStatus(!hasNoInputSource, mFrameWidth, mFrameHeight, mDisplayModeName); + mRuntimeHost->SetSignalStatus(!hasNoInputSource, mInputFrameWidth, mInputFrameHeight, mInputDisplayModeName); if (mHasNoInputSource) return; // don't transfer texture when there's no input @@ -1030,7 +1121,7 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo glBindTexture(GL_TEXTURE_2D, mCaptureTexture); // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mFrameWidth/2, mFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mInputFrameWidth / 2, mInputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); @@ -1064,6 +1155,10 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU); glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf); renderEffect(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mOutputFrameBuf); + glBlitFramebuffer(0, 0, mInputFrameWidth, mInputFrameHeight, 0, 0, mOutputFrameWidth, mOutputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf); glFlush(); if (mFastTransferExtensionAvailable) VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU); @@ -1101,7 +1196,7 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, // Finished with mCaptureTexture VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU); - if (! mPlayoutAllocator->transferFrame(pFrame, mFBOTexture)) + if (! mPlayoutAllocator->transferFrame(pFrame, mOutputTexture)) OutputDebugStringA("Playback: transferFrame() failed\n"); paintGL(); @@ -1111,8 +1206,8 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, } else { - glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf); - glReadPixels(0, 0, mFrameWidth, mFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); + glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf); + glReadPixels(0, 0, mOutputFrameWidth, mOutputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); paintGL(); } @@ -1158,7 +1253,7 @@ bool OpenGLComposite::Start() void* pFrame; outputVideoFrameBuffer->GetBytes((void**)&pFrame); - memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mFrameHeight); // 0 is black in RGBA format + memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mOutputFrameHeight); // 0 is black in BGRA format outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); outputVideoFrameBuffer->Release(); @@ -1347,7 +1442,7 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state, bool OpenGLComposite::compileLayerPrograms(int errorMessageSize, char* errorMessage) { - const std::vector layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector(); + const std::vector layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mInputFrameWidth, mInputFrameHeight) : std::vector(); std::string temporalError; if (!validateTemporalTextureUnitBudget(layerStates, temporalError)) { @@ -1650,7 +1745,7 @@ bool OpenGLComposite::createHistoryRing(HistoryRing& ring, unsigned effectiveLen glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mInputFrameWidth, mInputFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glGenFramebuffers(1, &slot.framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer); @@ -1768,7 +1863,7 @@ void OpenGLComposite::pushFramebufferToHistoryRing(GLuint sourceFramebuffer, His HistorySlot& targetSlot = ring.slots[ring.nextWriteIndex]; glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetSlot.framebuffer); - glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, 0, 0, mFrameWidth, mFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBlitFramebuffer(0, 0, mInputFrameWidth, mInputFrameHeight, 0, 0, mInputFrameWidth, mInputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); ring.nextWriteIndex = (ring.nextWriteIndex + 1) % ring.slots.size(); ring.filledCount = std::min(ring.filledCount + 1, ring.slots.size()); } @@ -1837,12 +1932,12 @@ void OpenGLComposite::renderEffect() glDisable(GL_DEPTH_TEST); renderDecodePass(); - const std::vector layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector(); + const std::vector layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mInputFrameWidth, mInputFrameHeight) : std::vector(); if (layerStates.empty() || mLayerPrograms.empty()) { glBindFramebuffer(GL_READ_FRAMEBUFFER, mDecodeFrameBuf); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mIdFrameBuf); - glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, 0, 0, mFrameWidth, mFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBlitFramebuffer(0, 0, mInputFrameWidth, mInputFrameHeight, 0, 0, mInputFrameWidth, mInputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf); } else @@ -1874,7 +1969,7 @@ void OpenGLComposite::renderEffect() void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state) { glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer); - glViewport(0, 0, mFrameWidth, mFrameHeight); + glViewport(0, 0, mInputFrameWidth, mInputFrameHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit); glBindTexture(GL_TEXTURE_2D, sourceTexture); @@ -1908,7 +2003,7 @@ void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinati void OpenGLComposite::renderDecodePass() { glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf); - glViewport(0, 0, mFrameWidth, mFrameHeight); + glViewport(0, 0, mInputFrameWidth, mInputFrameHeight); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit); glBindTexture(GL_TEXTURE_2D, mCaptureTexture); @@ -1918,9 +2013,9 @@ void OpenGLComposite::renderDecodePass() const GLint packedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uPackedVideoResolution"); const GLint decodedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uDecodedVideoResolution"); if (packedResolutionLocation >= 0) - glUniform2f(packedResolutionLocation, static_cast(mFrameWidth / 2), static_cast(mFrameHeight)); + glUniform2f(packedResolutionLocation, static_cast(mInputFrameWidth / 2), static_cast(mInputFrameHeight)); if (decodedResolutionLocation >= 0) - glUniform2f(decodedResolutionLocation, static_cast(mFrameWidth), static_cast(mFrameHeight)); + glUniform2f(decodedResolutionLocation, static_cast(mInputFrameWidth), static_cast(mInputFrameHeight)); glDrawArrays(GL_TRIANGLES, 0, 3); diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h index 6b24c37..c5f74ed 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.h @@ -118,9 +118,12 @@ private: BMDTimeValue mFrameDuration; BMDTimeScale mFrameTimescale; unsigned mTotalPlayoutFrames; - unsigned mFrameWidth; - unsigned mFrameHeight; - std::string mDisplayModeName; + unsigned mInputFrameWidth; + unsigned mInputFrameHeight; + unsigned mOutputFrameWidth; + unsigned mOutputFrameHeight; + std::string mInputDisplayModeName; + std::string mOutputDisplayModeName; bool mHasNoInputSource; std::string mDeckLinkOutputModelName; bool mDeckLinkSupportsInternalKeying; @@ -135,10 +138,12 @@ private: GLuint mDecodedTexture; GLuint mLayerTempTexture; GLuint mFBOTexture; + GLuint mOutputTexture; GLuint mUnpinnedTextureBuffer; GLuint mDecodeFrameBuf; GLuint mLayerTempFrameBuf; GLuint mIdFrameBuf; + GLuint mOutputFrameBuf; GLuint mIdColorBuf; GLuint mIdDepthBuf; GLuint mFullscreenVAO; diff --git a/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp b/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp index ba78415..ae0cb2b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OscServer.cpp @@ -114,7 +114,14 @@ void OscServer::ServerLoop() OscMessage message; std::string error; if (DecodeMessage(buffer.data(), byteCount, message, error)) - DispatchMessage(message, error); + { + if (!DispatchMessage(message, error) && !error.empty()) + OutputDebugStringA(("OSC dispatch failed: " + error + "\n").c_str()); + } + else if (!error.empty()) + { + OutputDebugStringA(("OSC decode failed: " + error + "\n").c_str()); + } } } @@ -152,6 +159,17 @@ bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& messa return true; } + if (valueType == 'd') + { + double value = 0.0; + if (!ReadFloat64(data, byteCount, offset, value)) + return false; + std::ostringstream stream; + stream << std::setprecision(17) << value; + message.valueJson = stream.str(); + return true; + } + if (valueType == 'i') { int value = 0; @@ -234,6 +252,21 @@ bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double return true; } +bool OscServer::ReadFloat64(const char* data, int byteCount, int& offset, double& value) +{ + if (offset + 8 > byteCount) + return false; + + const unsigned char* bytes = reinterpret_cast(data + offset); + uint64_t bits = 0; + for (int index = 0; index < 8; ++index) + bits = (bits << 8) | static_cast(bytes[index]); + + std::memcpy(&value, &bits, sizeof(value)); + offset += 8; + return true; +} + std::string OscServer::BuildJsonString(const std::string& value) { std::ostringstream stream; diff --git a/apps/LoopThroughWithOpenGLCompositing/OscServer.h b/apps/LoopThroughWithOpenGLCompositing/OscServer.h index da55959..5082b25 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OscServer.h +++ b/apps/LoopThroughWithOpenGLCompositing/OscServer.h @@ -40,6 +40,7 @@ private: static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value); static bool ReadInt32(const char* data, int byteCount, int& offset, int& value); static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value); + static bool ReadFloat64(const char* data, int byteCount, int& offset, double& value); static std::string BuildJsonString(const std::string& value); Callbacks mCallbacks; diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp index 97595a6..8e49e77 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp @@ -1185,17 +1185,56 @@ bool RuntimeHost::LoadConfig(std::string& error) if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) { if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) - mConfig.videoFormat = videoFormatValue->asString(); + { + mConfig.inputVideoFormat = videoFormatValue->asString(); + mConfig.outputVideoFormat = videoFormatValue->asString(); + } } if (const JsonValue* frameRateValue = configJson.find("frameRate")) { if (frameRateValue->isString() && !frameRateValue->asString().empty()) - mConfig.frameRate = frameRateValue->asString(); + { + mConfig.inputFrameRate = frameRateValue->asString(); + mConfig.outputFrameRate = frameRateValue->asString(); + } else if (frameRateValue->isNumber()) { std::ostringstream stream; stream << frameRateValue->asNumber(); - mConfig.frameRate = stream.str(); + mConfig.inputFrameRate = stream.str(); + mConfig.outputFrameRate = stream.str(); + } + } + if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat")) + { + if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty()) + mConfig.inputVideoFormat = inputVideoFormatValue->asString(); + } + if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate")) + { + if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty()) + mConfig.inputFrameRate = inputFrameRateValue->asString(); + else if (inputFrameRateValue->isNumber()) + { + std::ostringstream stream; + stream << inputFrameRateValue->asNumber(); + mConfig.inputFrameRate = stream.str(); + } + } + if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat")) + { + if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty()) + mConfig.outputVideoFormat = outputVideoFormatValue->asString(); + } + if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate")) + { + if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty()) + mConfig.outputFrameRate = outputFrameRateValue->asString(); + else if (outputFrameRateValue->isNumber()) + { + std::ostringstream stream; + stream << outputFrameRateValue->asNumber(); + mConfig.outputFrameRate = stream.str(); } } @@ -1480,8 +1519,10 @@ JsonValue RuntimeHost::BuildStateValue() const app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("maxTemporalHistoryFrames", JsonValue(static_cast(mConfig.maxTemporalHistoryFrames))); app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying)); - app.set("videoFormat", JsonValue(mConfig.videoFormat)); - app.set("frameRate", JsonValue(mConfig.frameRate)); + app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat)); + app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate)); + app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat)); + app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate)); root.set("app", app); JsonValue runtime = JsonValue::MakeObject(); diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h index 66f405b..016533b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h @@ -52,8 +52,10 @@ public: unsigned short GetOscPort() const { return mConfig.oscPort; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; } - const std::string& GetVideoFormat() const { return mConfig.videoFormat; } - const std::string& GetFrameRate() const { return mConfig.frameRate; } + const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; } + const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; } + const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; } + const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; } void SetServerPort(unsigned short port); bool AutoReloadEnabled() const { return mAutoReloadEnabled; } @@ -66,8 +68,10 @@ private: bool autoReload = true; unsigned maxTemporalHistoryFrames = 4; bool enableExternalKeying = false; - std::string videoFormat = "1080p"; - std::string frameRate = "59.94"; + std::string inputVideoFormat = "1080p"; + std::string inputFrameRate = "59.94"; + std::string outputVideoFormat = "1080p"; + std::string outputFrameRate = "59.94"; }; struct DeckLinkOutputStatus diff --git a/config/runtime-host.json b/config/runtime-host.json index ee38e78..5e3a55e 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -2,8 +2,10 @@ "shaderLibrary": "shaders", "serverPort": 8080, "oscPort": 9000, - "videoFormat": "1080p", - "frameRate": "59.94", + "inputVideoFormat": "1080p", + "inputFrameRate": "59.94", + "outputVideoFormat": "1080p", + "outputFrameRate": "59.94", "autoReload": true, "maxTemporalHistoryFrames": 12, "enableExternalKeying": true diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 94197e6..6c8403c 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -367,9 +367,13 @@ components: type: number enableExternalKeying: type: boolean - videoFormat: + inputVideoFormat: type: string - frameRate: + inputFrameRate: + type: string + outputVideoFormat: + type: string + outputFrameRate: type: string RuntimeStatus: type: object diff --git a/shaders/fisheye-reproject/shader.json b/shaders/fisheye-reproject/shader.json index 0be320b..1d5a9cd 100644 --- a/shaders/fisheye-reproject/shader.json +++ b/shaders/fisheye-reproject/shader.json @@ -41,6 +41,33 @@ "max": 175.0, "step": 0.1 }, + { + "id": "basePanDegrees", + "label": "Base Pan", + "type": "float", + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 0.1 + }, + { + "id": "baseTiltDegrees", + "label": "Base Tilt", + "type": "float", + "default": 0.0, + "min": -120.0, + "max": 120.0, + "step": 0.1 + }, + { + "id": "baseRollDegrees", + "label": "Base Roll", + "type": "float", + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 0.1 + }, { "id": "panDegrees", "label": "Pan", diff --git a/shaders/fisheye-reproject/shader.slang b/shaders/fisheye-reproject/shader.slang index 4147b2e..31e4381 100644 --- a/shaders/fisheye-reproject/shader.slang +++ b/shaders/fisheye-reproject/shader.slang @@ -71,6 +71,9 @@ float4 shadeVideo(ShaderContext context) ? buildCylindricalRay(screen, outputAspect, tanHalfFov) : buildRectilinearRay(screen, outputAspect, tanHalfFov); + ray = rotateZ(ray, radiansFromDegrees(baseRollDegrees)); + ray = rotateX(ray, radiansFromDegrees(-baseTiltDegrees)); + ray = rotateY(ray, radiansFromDegrees(basePanDegrees)); ray = rotateZ(ray, radiansFromDegrees(rollDegrees)); ray = rotateX(ray, radiansFromDegrees(-tiltDegrees)); ray = rotateY(ray, radiansFromDegrees(panDegrees)); diff --git a/tests/OscServerTests.cpp b/tests/OscServerTests.cpp index 77c731c..ba93908 100644 --- a/tests/OscServerTests.cpp +++ b/tests/OscServerTests.cpp @@ -43,6 +43,14 @@ void AppendFloat32(std::vector& packet, float value) AppendInt32(packet, static_cast(bits)); } +void AppendFloat64(std::vector& packet, double value) +{ + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + for (int shift = 56; shift >= 0; shift -= 8) + packet.push_back(static_cast((bits >> shift) & 0xff)); +} + std::vector BuildOscPacket(const std::string& address, const std::string& typeTags) { std::vector packet; @@ -89,6 +97,19 @@ void TestDecodeFloatMessage() Expect(message.valueJson.find("0.75") == 0, "float OSC value becomes JSON number"); } +void TestDecodeDoubleMessage() +{ + OscServer server; + std::vector packet = BuildOscPacket("/VideoShaderToys/fisheye-reproject/panDegrees", ",d"); + AppendFloat64(packet, 51.5); + + OscServerTestAccess::Message message; + std::string error; + Expect(OscServerTestAccess::Decode(server, packet, message, error), "double OSC message decodes"); + Expect(message.address == "/VideoShaderToys/fisheye-reproject/panDegrees", "double OSC address is preserved"); + Expect(message.valueJson.find("51.5") == 0, "double OSC value becomes JSON number"); +} + void TestDecodeIntStringAndBoolMessages() { OscServer server; @@ -161,6 +182,7 @@ void TestRejectsUnsupportedAddress() int main() { TestDecodeFloatMessage(); + TestDecodeDoubleMessage(); TestDecodeIntStringAndBoolMessages(); TestDispatchValidAddress(); TestRejectsUnsupportedAddress(); diff --git a/ui/src/components/LayerCard.jsx b/ui/src/components/LayerCard.jsx index 0c8bea2..939457e 100644 --- a/ui/src/components/LayerCard.jsx +++ b/ui/src/components/LayerCard.jsx @@ -2,6 +2,7 @@ import { GripVertical, Trash2 } from "lucide-react"; import { postJson } from "../api/controlApi"; import { ParameterField } from "./ParameterField"; +import { ShaderPicker } from "./ShaderPicker"; export function LayerCard({ layer, @@ -90,23 +91,17 @@ export function LayerCard({ {expanded ? (
- - + />
{layer.temporal?.enabled ? ( @@ -139,6 +134,7 @@ export function LayerCard({ {layer.parameters.map((parameter) => ( onLayerParameterChange(layer.id, parameterId, value)} /> diff --git a/ui/src/components/LayerStack.jsx b/ui/src/components/LayerStack.jsx index 8f21f42..befd93d 100644 --- a/ui/src/components/LayerStack.jsx +++ b/ui/src/components/LayerStack.jsx @@ -1,5 +1,6 @@ import { postJson } from "../api/controlApi"; import { LayerCard } from "./LayerCard"; +import { ShaderPicker } from "./ShaderPicker"; function moveItem(array, fromIndex, toIndex) { if (fromIndex < 0 || fromIndex >= array.length || toIndex < 0 || toIndex >= array.length) { @@ -131,18 +132,12 @@ export function LayerStack({
- - + onChange={setPendingShaderId} + />
diff --git a/ui/src/components/ParameterField.jsx b/ui/src/components/ParameterField.jsx index 1651718..cb75ffb 100644 --- a/ui/src/components/ParameterField.jsx +++ b/ui/src/components/ParameterField.jsx @@ -1,7 +1,64 @@ +import { Copy } from "lucide-react"; + import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue"; import { ParameterValueDisplay } from "./ParameterValueDisplay"; -export function ParameterField({ parameter, onParameterChange }) { +function ParameterHeader({ layer, parameter }) { + const layerKey = layer.shaderId || layer.shaderName || layer.id; + const oscRoute = `/VideoShaderToys/${layerKey}/${parameter.id}`; + + function copyRoute() { + if (navigator.clipboard) { + navigator.clipboard.writeText(oscRoute); + } + } + + return ( +
+ + +
+ ); +} + +function clamp01(value) { + return Math.max(0, Math.min(1, Number(value) || 0)); +} + +function colorComponentToHex(value) { + return Math.round(clamp01(value) * 255) + .toString(16) + .padStart(2, "0"); +} + +function colorValueToHex(value) { + const values = [...(value ?? [])]; + while (values.length < 3) { + values.push(0); + } + return `#${colorComponentToHex(values[0])}${colorComponentToHex(values[1])}${colorComponentToHex(values[2])}`; +} + +function hexToColorValue(hex, alpha) { + const sanitized = /^#[0-9a-fA-F]{6}$/.test(hex) ? hex.slice(1) : "000000"; + return [ + parseInt(sanitized.slice(0, 2), 16) / 255, + parseInt(sanitized.slice(2, 4), 16) / 255, + parseInt(sanitized.slice(4, 6), 16) / 255, + clamp01(alpha ?? 1), + ]; +} + +export function ParameterField({ layer, parameter, onParameterChange }) { const { appliedValue, beginInteraction, @@ -12,12 +69,12 @@ export function ParameterField({ parameter, onParameterChange }) { sendValue, } = useThrottledParameterValue(parameter, onParameterChange); - const label = ; + const header = ; if (parameter.type === "float") { return (
- {label} + {header}
- {label} + {header}
- {Array.from({ length: componentCount }, (_, index) => ( + {Array.from({ length: 2 }, (_, index) => ( + {header} +
+ sendValue(hexToColorValue(event.target.value, values[3]))} + onBlur={endInteraction} + /> + +
+ +
+ ); + } + if (parameter.type === "bool") { return (
- {label} + {header}