OSC updates and video resolution fixes
This commit is contained in:
104
OSC/Test.json
Normal file
104
OSC/Test.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
11
README.md
11
README.md
@@ -119,15 +119,20 @@ Current native test coverage includes:
|
|||||||
{
|
{
|
||||||
"shaderLibrary": "shaders",
|
"shaderLibrary": "shaders",
|
||||||
"serverPort": 8080,
|
"serverPort": 8080,
|
||||||
"videoFormat": "1080p",
|
"oscPort": 9000,
|
||||||
"frameRate": "59.94",
|
"inputVideoFormat": "1080p",
|
||||||
|
"inputFrameRate": "59.94",
|
||||||
|
"outputVideoFormat": "1080p",
|
||||||
|
"outputFrameRate": "59.94",
|
||||||
"autoReload": true,
|
"autoReload": true,
|
||||||
"maxTemporalHistoryFrames": 12,
|
"maxTemporalHistoryFrames": 12,
|
||||||
"enableExternalKeying": true
|
"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:
|
The control UI is available at:
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Fields:
|
|||||||
- `uv`: normalized texture coordinates, usually `0..1`.
|
- `uv`: normalized texture coordinates, usually `0..1`.
|
||||||
- `sourceColor`: decoded RGBA source video at `uv`.
|
- `sourceColor`: decoded RGBA source video at `uv`.
|
||||||
- `inputResolution`: decoded input video resolution in pixels.
|
- `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.
|
- `time`: elapsed runtime time in seconds.
|
||||||
- `frameCount`: incrementing frame counter.
|
- `frameCount`: incrementing frame counter.
|
||||||
- `mixAmount`: runtime mix amount.
|
- `mixAmount`: runtime mix amount.
|
||||||
|
|||||||
@@ -192,6 +192,28 @@ bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::str
|
|||||||
return false;
|
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
|
class ScopedGlShader
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -295,8 +317,10 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mCaptureDelegate(NULL), mPlayoutDelegate(NULL),
|
mCaptureDelegate(NULL), mPlayoutDelegate(NULL),
|
||||||
mDLInput(NULL), mDLOutput(NULL), mDLKeyer(NULL),
|
mDLInput(NULL), mDLOutput(NULL), mDLKeyer(NULL),
|
||||||
mPlayoutAllocator(NULL),
|
mPlayoutAllocator(NULL),
|
||||||
mFrameWidth(0), mFrameHeight(0),
|
mInputFrameWidth(0), mInputFrameHeight(0),
|
||||||
mDisplayModeName("1080p59.94"),
|
mOutputFrameWidth(0), mOutputFrameHeight(0),
|
||||||
|
mInputDisplayModeName("1080p59.94"),
|
||||||
|
mOutputDisplayModeName("1080p59.94"),
|
||||||
mHasNoInputSource(true),
|
mHasNoInputSource(true),
|
||||||
mDeckLinkSupportsInternalKeying(false),
|
mDeckLinkSupportsInternalKeying(false),
|
||||||
mDeckLinkSupportsExternalKeying(false),
|
mDeckLinkSupportsExternalKeying(false),
|
||||||
@@ -307,10 +331,12 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mDecodedTexture(0),
|
mDecodedTexture(0),
|
||||||
mLayerTempTexture(0),
|
mLayerTempTexture(0),
|
||||||
mFBOTexture(0),
|
mFBOTexture(0),
|
||||||
|
mOutputTexture(0),
|
||||||
mUnpinnedTextureBuffer(0),
|
mUnpinnedTextureBuffer(0),
|
||||||
mDecodeFrameBuf(0),
|
mDecodeFrameBuf(0),
|
||||||
mLayerTempFrameBuf(0),
|
mLayerTempFrameBuf(0),
|
||||||
mIdFrameBuf(0),
|
mIdFrameBuf(0),
|
||||||
|
mOutputFrameBuf(0),
|
||||||
mIdColorBuf(0),
|
mIdColorBuf(0),
|
||||||
mIdDepthBuf(0),
|
mIdDepthBuf(0),
|
||||||
mFullscreenVAO(0),
|
mFullscreenVAO(0),
|
||||||
@@ -407,6 +433,10 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
glDeleteTextures(1, &mLayerTempTexture);
|
glDeleteTextures(1, &mLayerTempTexture);
|
||||||
if (mFBOTexture != 0)
|
if (mFBOTexture != 0)
|
||||||
glDeleteTextures(1, &mFBOTexture);
|
glDeleteTextures(1, &mFBOTexture);
|
||||||
|
if (mOutputTexture != 0)
|
||||||
|
glDeleteTextures(1, &mOutputTexture);
|
||||||
|
if (mOutputFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mOutputFrameBuf);
|
||||||
if (mUnpinnedTextureBuffer != 0)
|
if (mUnpinnedTextureBuffer != 0)
|
||||||
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
||||||
|
|
||||||
@@ -427,10 +457,14 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
IDeckLinkIterator* pDLIterator = NULL;
|
IDeckLinkIterator* pDLIterator = NULL;
|
||||||
IDeckLink* pDL = NULL;
|
IDeckLink* pDL = NULL;
|
||||||
IDeckLinkProfileAttributes* deckLinkAttributes = NULL;
|
IDeckLinkProfileAttributes* deckLinkAttributes = NULL;
|
||||||
IDeckLinkDisplayModeIterator* pDLDisplayModeIterator = NULL;
|
IDeckLinkDisplayModeIterator* pDLInputDisplayModeIterator = NULL;
|
||||||
IDeckLinkDisplayMode* pDLDisplayMode = NULL;
|
IDeckLinkDisplayModeIterator* pDLOutputDisplayModeIterator = NULL;
|
||||||
BMDDisplayMode displayMode = bmdModeHD1080p5994; // mode to use for capture and playout
|
IDeckLinkDisplayMode* pDLInputDisplayMode = NULL;
|
||||||
std::string displayModeName = "1080p59.94";
|
IDeckLinkDisplayMode* pDLOutputDisplayMode = NULL;
|
||||||
|
BMDDisplayMode inputDisplayMode = bmdModeHD1080p5994;
|
||||||
|
BMDDisplayMode outputDisplayMode = bmdModeHD1080p5994;
|
||||||
|
std::string inputDisplayModeName = "1080p59.94";
|
||||||
|
std::string outputDisplayModeName = "1080p59.94";
|
||||||
int outputFrameRowBytes;
|
int outputFrameRowBytes;
|
||||||
HRESULT result;
|
HRESULT result;
|
||||||
|
|
||||||
@@ -446,15 +480,23 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
|
|
||||||
if (mRuntimeHost)
|
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: " +
|
const std::string error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
mRuntimeHost->GetVideoFormat() + " / " + mRuntimeHost->GetFrameRate();
|
mRuntimeHost->GetInputVideoFormat() + " / " + mRuntimeHost->GetInputFrameRate();
|
||||||
MessageBoxA(NULL, error.c_str(), "DeckLink mode configuration error", MB_OK);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mDisplayModeName = displayModeName;
|
mInputDisplayModeName = inputDisplayModeName;
|
||||||
|
mOutputDisplayModeName = outputDisplayModeName;
|
||||||
|
|
||||||
result = CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&pDLIterator);
|
result = CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&pDLIterator);
|
||||||
if (FAILED(result))
|
if (FAILED(result))
|
||||||
@@ -538,35 +580,48 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
goto error;
|
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;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (pDLDisplayModeIterator->Next(&pDLDisplayMode) == S_OK)
|
if (!FindDeckLinkDisplayMode(pDLInputDisplayModeIterator, inputDisplayMode, &pDLInputDisplayMode))
|
||||||
{
|
{
|
||||||
if (pDLDisplayMode->GetDisplayMode() == displayMode)
|
const std::string error = "Cannot get specified input BMDDisplayMode for configured mode: " + inputDisplayModeName;
|
||||||
break;
|
MessageBoxA(NULL, error.c_str(), "DeckLink input error.", MB_OK);
|
||||||
|
goto error;
|
||||||
pDLDisplayMode->Release();
|
|
||||||
pDLDisplayMode = NULL;
|
|
||||||
}
|
}
|
||||||
pDLDisplayModeIterator->Release();
|
pDLInputDisplayModeIterator->Release();
|
||||||
pDLDisplayModeIterator = NULL;
|
pDLInputDisplayModeIterator = NULL;
|
||||||
|
|
||||||
if (pDLDisplayMode == NULL)
|
if (mDLOutput->GetDisplayModeIterator(&pDLOutputDisplayModeIterator) != S_OK)
|
||||||
{
|
{
|
||||||
const std::string error = "Cannot get specified BMDDisplayMode for configured mode: " + displayModeName;
|
MessageBox(NULL, _T("Cannot get output Display Mode Iterator."), _T("DeckLink error."), MB_OK);
|
||||||
MessageBoxA(NULL, error.c_str(), "DeckLink error.", MB_OK);
|
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
mFrameWidth = pDLDisplayMode->GetWidth();
|
if (!FindDeckLinkDisplayMode(pDLOutputDisplayModeIterator, outputDisplayMode, &pDLOutputDisplayMode))
|
||||||
mFrameHeight = pDLDisplayMode->GetHeight();
|
{
|
||||||
|
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())
|
if (! CheckOpenGLExtensions())
|
||||||
goto error;
|
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())
|
if (! InitOpenGLState())
|
||||||
goto error;
|
goto error;
|
||||||
@@ -586,18 +641,18 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
mDeckLinkStatusMessage);
|
mDeckLinkStatusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
pDLDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale);
|
pDLOutputDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale);
|
||||||
|
|
||||||
// Resize window to match video frame, but scale large formats down by half for viewing
|
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||||
if (mFrameWidth < 1920)
|
if (mOutputFrameWidth < 1920)
|
||||||
resizeWindow(mFrameWidth, mFrameHeight);
|
resizeWindow(mOutputFrameWidth, mOutputFrameHeight);
|
||||||
else
|
else
|
||||||
resizeWindow(mFrameWidth / 2, mFrameHeight / 2);
|
resizeWindow(mOutputFrameWidth / 2, mOutputFrameHeight / 2);
|
||||||
|
|
||||||
if (mFastTransferExtensionAvailable)
|
if (mFastTransferExtensionAvailable)
|
||||||
{
|
{
|
||||||
// Initialize fast video frame transfers
|
// 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);
|
MessageBox(NULL, _T("Cannot initialize video transfers."), _T("VideoFrameTransfer error."), MB_OK);
|
||||||
goto error;
|
goto error;
|
||||||
@@ -608,7 +663,7 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
// Use custom allocators so we pin only once then recycle them
|
// Use custom allocators so we pin only once then recycle them
|
||||||
CComPtr<IDeckLinkVideoBufferAllocatorProvider> captureAllocator(new (std::nothrow) InputAllocatorPool(hGLDC, hGLRC));
|
CComPtr<IDeckLinkVideoBufferAllocatorProvider> 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;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,13 +671,13 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
if (mDLInput->SetCallback(mCaptureDelegate) != S_OK)
|
if (mDLInput->SetCallback(mCaptureDelegate) != S_OK)
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
if (mDLOutput->RowBytesForPixelFormat(bmdFormat8BitBGRA, mFrameWidth, &outputFrameRowBytes) != S_OK)
|
if (mDLOutput->RowBytesForPixelFormat(bmdFormat8BitBGRA, mOutputFrameWidth, &outputFrameRowBytes) != S_OK)
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
// Use a custom allocator so we pin only once then recycle them
|
// 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;
|
goto error;
|
||||||
|
|
||||||
if (mDLOutput->QueryInterface(IID_IDeckLinkKeyer, (void**)&mDLKeyer) == S_OK && mDLKeyer != NULL)
|
if (mDLOutput->QueryInterface(IID_IDeckLinkKeyer, (void**)&mDLKeyer) == S_OK && mDLKeyer != NULL)
|
||||||
@@ -680,7 +735,7 @@ bool OpenGLComposite::InitDeckLink()
|
|||||||
if (mPlayoutAllocator->AllocateVideoBuffer(&outputFrameBuffer) != S_OK)
|
if (mPlayoutAllocator->AllocateVideoBuffer(&outputFrameBuffer) != S_OK)
|
||||||
goto error;
|
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;
|
goto error;
|
||||||
|
|
||||||
mDLOutputVideoFrameQueue.push_back(outputFrame);
|
mDLOutputVideoFrameQueue.push_back(outputFrame);
|
||||||
@@ -723,10 +778,28 @@ error:
|
|||||||
pDL = NULL;
|
pDL = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pDLDisplayMode != NULL)
|
if (pDLInputDisplayMode != NULL)
|
||||||
{
|
{
|
||||||
pDLDisplayMode->Release();
|
pDLInputDisplayMode->Release();
|
||||||
pDLDisplayMode = NULL;
|
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)
|
if (pDLIterator != NULL)
|
||||||
@@ -757,9 +830,9 @@ void OpenGLComposite::paintGL()
|
|||||||
int destX = 0;
|
int destX = 0;
|
||||||
int destY = 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<double>(mFrameWidth) / static_cast<double>(mFrameHeight);
|
const double frameAspect = static_cast<double>(mOutputFrameWidth) / static_cast<double>(mOutputFrameHeight);
|
||||||
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
|
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
|
||||||
|
|
||||||
if (viewAspect > frameAspect)
|
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);
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
glViewport(0, 0, mViewWidth, mViewHeight);
|
glViewport(0, 0, mViewWidth, mViewHeight);
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
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);
|
SwapBuffers(hGLDC);
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
@@ -893,7 +966,7 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
// Create texture with empty data, we will update it using glTexSubImage2D each frame.
|
// 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
|
// 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.
|
// 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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
glGenTextures(1, &mDecodedTexture);
|
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_MAG_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
glGenTextures(1, &mLayerTempTexture);
|
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_MAG_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
|
||||||
@@ -920,6 +993,7 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
glGenFramebuffers(1, &mDecodeFrameBuf);
|
glGenFramebuffers(1, &mDecodeFrameBuf);
|
||||||
glGenFramebuffers(1, &mLayerTempFrameBuf);
|
glGenFramebuffers(1, &mLayerTempFrameBuf);
|
||||||
glGenFramebuffers(1, &mIdFrameBuf);
|
glGenFramebuffers(1, &mIdFrameBuf);
|
||||||
|
glGenFramebuffers(1, &mOutputFrameBuf);
|
||||||
glGenRenderbuffers(1, &mIdColorBuf);
|
glGenRenderbuffers(1, &mIdColorBuf);
|
||||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||||
glGenVertexArrays(1, &mFullscreenVAO);
|
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_MAG_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 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
|
// Attach a depth buffer
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
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);
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
|
||||||
|
|
||||||
@@ -970,6 +1044,23 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
return false;
|
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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
@@ -991,7 +1082,7 @@ void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bo
|
|||||||
{
|
{
|
||||||
mHasNoInputSource = hasNoInputSource;
|
mHasNoInputSource = hasNoInputSource;
|
||||||
if (mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
mRuntimeHost->SetSignalStatus(!hasNoInputSource, mFrameWidth, mFrameHeight, mDisplayModeName);
|
mRuntimeHost->SetSignalStatus(!hasNoInputSource, mInputFrameWidth, mInputFrameHeight, mInputDisplayModeName);
|
||||||
|
|
||||||
if (mHasNoInputSource)
|
if (mHasNoInputSource)
|
||||||
return; // don't transfer texture when there's no input
|
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);
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
|
|
||||||
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
|
// 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);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||||
@@ -1064,6 +1155,10 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
|
|||||||
VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU);
|
VideoFrameTransfer::beginTextureInUse(VideoFrameTransfer::GPUtoCPU);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
renderEffect();
|
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();
|
glFlush();
|
||||||
if (mFastTransferExtensionAvailable)
|
if (mFastTransferExtensionAvailable)
|
||||||
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU);
|
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::GPUtoCPU);
|
||||||
@@ -1101,7 +1196,7 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
|
|||||||
// Finished with mCaptureTexture
|
// Finished with mCaptureTexture
|
||||||
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
||||||
|
|
||||||
if (! mPlayoutAllocator->transferFrame(pFrame, mFBOTexture))
|
if (! mPlayoutAllocator->transferFrame(pFrame, mOutputTexture))
|
||||||
OutputDebugStringA("Playback: transferFrame() failed\n");
|
OutputDebugStringA("Playback: transferFrame() failed\n");
|
||||||
|
|
||||||
paintGL();
|
paintGL();
|
||||||
@@ -1111,8 +1206,8 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf);
|
||||||
glReadPixels(0, 0, mFrameWidth, mFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame);
|
glReadPixels(0, 0, mOutputFrameWidth, mOutputFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame);
|
||||||
paintGL();
|
paintGL();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,7 +1253,7 @@ bool OpenGLComposite::Start()
|
|||||||
|
|
||||||
void* pFrame;
|
void* pFrame;
|
||||||
outputVideoFrameBuffer->GetBytes((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->EndAccess(bmdBufferAccessWrite);
|
||||||
outputVideoFrameBuffer->Release();
|
outputVideoFrameBuffer->Release();
|
||||||
@@ -1347,7 +1442,7 @@ bool OpenGLComposite::compileSingleLayerProgram(const RuntimeRenderState& state,
|
|||||||
|
|
||||||
bool OpenGLComposite::compileLayerPrograms(int errorMessageSize, char* errorMessage)
|
bool OpenGLComposite::compileLayerPrograms(int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector<RuntimeRenderState>();
|
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mInputFrameWidth, mInputFrameHeight) : std::vector<RuntimeRenderState>();
|
||||||
std::string temporalError;
|
std::string temporalError;
|
||||||
if (!validateTemporalTextureUnitBudget(layerStates, 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_MAG_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 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);
|
glGenFramebuffers(1, &slot.framebuffer);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||||
@@ -1768,7 +1863,7 @@ void OpenGLComposite::pushFramebufferToHistoryRing(GLuint sourceFramebuffer, His
|
|||||||
HistorySlot& targetSlot = ring.slots[ring.nextWriteIndex];
|
HistorySlot& targetSlot = ring.slots[ring.nextWriteIndex];
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetSlot.framebuffer);
|
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.nextWriteIndex = (ring.nextWriteIndex + 1) % ring.slots.size();
|
||||||
ring.filledCount = std::min<std::size_t>(ring.filledCount + 1, ring.slots.size());
|
ring.filledCount = std::min<std::size_t>(ring.filledCount + 1, ring.slots.size());
|
||||||
}
|
}
|
||||||
@@ -1837,12 +1932,12 @@ void OpenGLComposite::renderEffect()
|
|||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
renderDecodePass();
|
renderDecodePass();
|
||||||
|
|
||||||
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mFrameWidth, mFrameHeight) : std::vector<RuntimeRenderState>();
|
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost ? mRuntimeHost->GetLayerRenderStates(mInputFrameWidth, mInputFrameHeight) : std::vector<RuntimeRenderState>();
|
||||||
if (layerStates.empty() || mLayerPrograms.empty())
|
if (layerStates.empty() || mLayerPrograms.empty())
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mDecodeFrameBuf);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mIdFrameBuf);
|
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);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1874,7 +1969,7 @@ void OpenGLComposite::renderEffect()
|
|||||||
void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state)
|
void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state)
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
||||||
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
glViewport(0, 0, mInputFrameWidth, mInputFrameHeight);
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
||||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||||
@@ -1908,7 +2003,7 @@ void OpenGLComposite::renderShaderProgram(GLuint sourceTexture, GLuint destinati
|
|||||||
void OpenGLComposite::renderDecodePass()
|
void OpenGLComposite::renderDecodePass()
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
glViewport(0, 0, mInputFrameWidth, mInputFrameHeight);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
|
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
@@ -1918,9 +2013,9 @@ void OpenGLComposite::renderDecodePass()
|
|||||||
const GLint packedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uPackedVideoResolution");
|
const GLint packedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uPackedVideoResolution");
|
||||||
const GLint decodedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uDecodedVideoResolution");
|
const GLint decodedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uDecodedVideoResolution");
|
||||||
if (packedResolutionLocation >= 0)
|
if (packedResolutionLocation >= 0)
|
||||||
glUniform2f(packedResolutionLocation, static_cast<float>(mFrameWidth / 2), static_cast<float>(mFrameHeight));
|
glUniform2f(packedResolutionLocation, static_cast<float>(mInputFrameWidth / 2), static_cast<float>(mInputFrameHeight));
|
||||||
if (decodedResolutionLocation >= 0)
|
if (decodedResolutionLocation >= 0)
|
||||||
glUniform2f(decodedResolutionLocation, static_cast<float>(mFrameWidth), static_cast<float>(mFrameHeight));
|
glUniform2f(decodedResolutionLocation, static_cast<float>(mInputFrameWidth), static_cast<float>(mInputFrameHeight));
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
|
||||||
|
|||||||
@@ -118,9 +118,12 @@ private:
|
|||||||
BMDTimeValue mFrameDuration;
|
BMDTimeValue mFrameDuration;
|
||||||
BMDTimeScale mFrameTimescale;
|
BMDTimeScale mFrameTimescale;
|
||||||
unsigned mTotalPlayoutFrames;
|
unsigned mTotalPlayoutFrames;
|
||||||
unsigned mFrameWidth;
|
unsigned mInputFrameWidth;
|
||||||
unsigned mFrameHeight;
|
unsigned mInputFrameHeight;
|
||||||
std::string mDisplayModeName;
|
unsigned mOutputFrameWidth;
|
||||||
|
unsigned mOutputFrameHeight;
|
||||||
|
std::string mInputDisplayModeName;
|
||||||
|
std::string mOutputDisplayModeName;
|
||||||
bool mHasNoInputSource;
|
bool mHasNoInputSource;
|
||||||
std::string mDeckLinkOutputModelName;
|
std::string mDeckLinkOutputModelName;
|
||||||
bool mDeckLinkSupportsInternalKeying;
|
bool mDeckLinkSupportsInternalKeying;
|
||||||
@@ -135,10 +138,12 @@ private:
|
|||||||
GLuint mDecodedTexture;
|
GLuint mDecodedTexture;
|
||||||
GLuint mLayerTempTexture;
|
GLuint mLayerTempTexture;
|
||||||
GLuint mFBOTexture;
|
GLuint mFBOTexture;
|
||||||
|
GLuint mOutputTexture;
|
||||||
GLuint mUnpinnedTextureBuffer;
|
GLuint mUnpinnedTextureBuffer;
|
||||||
GLuint mDecodeFrameBuf;
|
GLuint mDecodeFrameBuf;
|
||||||
GLuint mLayerTempFrameBuf;
|
GLuint mLayerTempFrameBuf;
|
||||||
GLuint mIdFrameBuf;
|
GLuint mIdFrameBuf;
|
||||||
|
GLuint mOutputFrameBuf;
|
||||||
GLuint mIdColorBuf;
|
GLuint mIdColorBuf;
|
||||||
GLuint mIdDepthBuf;
|
GLuint mIdDepthBuf;
|
||||||
GLuint mFullscreenVAO;
|
GLuint mFullscreenVAO;
|
||||||
|
|||||||
@@ -114,7 +114,14 @@ void OscServer::ServerLoop()
|
|||||||
OscMessage message;
|
OscMessage message;
|
||||||
std::string error;
|
std::string error;
|
||||||
if (DecodeMessage(buffer.data(), byteCount, message, 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;
|
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')
|
if (valueType == 'i')
|
||||||
{
|
{
|
||||||
int value = 0;
|
int value = 0;
|
||||||
@@ -234,6 +252,21 @@ bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double
|
|||||||
return true;
|
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<const unsigned char*>(data + offset);
|
||||||
|
uint64_t bits = 0;
|
||||||
|
for (int index = 0; index < 8; ++index)
|
||||||
|
bits = (bits << 8) | static_cast<uint64_t>(bytes[index]);
|
||||||
|
|
||||||
|
std::memcpy(&value, &bits, sizeof(value));
|
||||||
|
offset += 8;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string OscServer::BuildJsonString(const std::string& value)
|
std::string OscServer::BuildJsonString(const std::string& value)
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ private:
|
|||||||
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
|
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 ReadInt32(const char* data, int byteCount, int& offset, int& value);
|
||||||
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& 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);
|
static std::string BuildJsonString(const std::string& value);
|
||||||
|
|
||||||
Callbacks mCallbacks;
|
Callbacks mCallbacks;
|
||||||
|
|||||||
@@ -1185,17 +1185,56 @@ bool RuntimeHost::LoadConfig(std::string& error)
|
|||||||
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
||||||
{
|
{
|
||||||
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
|
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 (const JsonValue* frameRateValue = configJson.find("frameRate"))
|
||||||
{
|
{
|
||||||
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
||||||
mConfig.frameRate = frameRateValue->asString();
|
{
|
||||||
|
mConfig.inputFrameRate = frameRateValue->asString();
|
||||||
|
mConfig.outputFrameRate = frameRateValue->asString();
|
||||||
|
}
|
||||||
else if (frameRateValue->isNumber())
|
else if (frameRateValue->isNumber())
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << frameRateValue->asNumber();
|
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("autoReload", JsonValue(mAutoReloadEnabled));
|
||||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
||||||
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
||||||
app.set("videoFormat", JsonValue(mConfig.videoFormat));
|
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
|
||||||
app.set("frameRate", JsonValue(mConfig.frameRate));
|
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
|
||||||
|
app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat));
|
||||||
|
app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate));
|
||||||
root.set("app", app);
|
root.set("app", app);
|
||||||
|
|
||||||
JsonValue runtime = JsonValue::MakeObject();
|
JsonValue runtime = JsonValue::MakeObject();
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ public:
|
|||||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
||||||
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
||||||
const std::string& GetVideoFormat() const { return mConfig.videoFormat; }
|
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
|
||||||
const std::string& GetFrameRate() const { return mConfig.frameRate; }
|
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);
|
void SetServerPort(unsigned short port);
|
||||||
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
||||||
|
|
||||||
@@ -66,8 +68,10 @@ private:
|
|||||||
bool autoReload = true;
|
bool autoReload = true;
|
||||||
unsigned maxTemporalHistoryFrames = 4;
|
unsigned maxTemporalHistoryFrames = 4;
|
||||||
bool enableExternalKeying = false;
|
bool enableExternalKeying = false;
|
||||||
std::string videoFormat = "1080p";
|
std::string inputVideoFormat = "1080p";
|
||||||
std::string frameRate = "59.94";
|
std::string inputFrameRate = "59.94";
|
||||||
|
std::string outputVideoFormat = "1080p";
|
||||||
|
std::string outputFrameRate = "59.94";
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DeckLinkOutputStatus
|
struct DeckLinkOutputStatus
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
"shaderLibrary": "shaders",
|
"shaderLibrary": "shaders",
|
||||||
"serverPort": 8080,
|
"serverPort": 8080,
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"videoFormat": "1080p",
|
"inputVideoFormat": "1080p",
|
||||||
"frameRate": "59.94",
|
"inputFrameRate": "59.94",
|
||||||
|
"outputVideoFormat": "1080p",
|
||||||
|
"outputFrameRate": "59.94",
|
||||||
"autoReload": true,
|
"autoReload": true,
|
||||||
"maxTemporalHistoryFrames": 12,
|
"maxTemporalHistoryFrames": 12,
|
||||||
"enableExternalKeying": true
|
"enableExternalKeying": true
|
||||||
|
|||||||
@@ -367,9 +367,13 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
enableExternalKeying:
|
enableExternalKeying:
|
||||||
type: boolean
|
type: boolean
|
||||||
videoFormat:
|
inputVideoFormat:
|
||||||
type: string
|
type: string
|
||||||
frameRate:
|
inputFrameRate:
|
||||||
|
type: string
|
||||||
|
outputVideoFormat:
|
||||||
|
type: string
|
||||||
|
outputFrameRate:
|
||||||
type: string
|
type: string
|
||||||
RuntimeStatus:
|
RuntimeStatus:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -41,6 +41,33 @@
|
|||||||
"max": 175.0,
|
"max": 175.0,
|
||||||
"step": 0.1
|
"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",
|
"id": "panDegrees",
|
||||||
"label": "Pan",
|
"label": "Pan",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
|
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
|
||||||
: buildRectilinearRay(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 = rotateZ(ray, radiansFromDegrees(rollDegrees));
|
||||||
ray = rotateX(ray, radiansFromDegrees(-tiltDegrees));
|
ray = rotateX(ray, radiansFromDegrees(-tiltDegrees));
|
||||||
ray = rotateY(ray, radiansFromDegrees(panDegrees));
|
ray = rotateY(ray, radiansFromDegrees(panDegrees));
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ void AppendFloat32(std::vector<char>& packet, float value)
|
|||||||
AppendInt32(packet, static_cast<int>(bits));
|
AppendInt32(packet, static_cast<int>(bits));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppendFloat64(std::vector<char>& 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<char>((bits >> shift) & 0xff));
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<char> BuildOscPacket(const std::string& address, const std::string& typeTags)
|
std::vector<char> BuildOscPacket(const std::string& address, const std::string& typeTags)
|
||||||
{
|
{
|
||||||
std::vector<char> packet;
|
std::vector<char> packet;
|
||||||
@@ -89,6 +97,19 @@ void TestDecodeFloatMessage()
|
|||||||
Expect(message.valueJson.find("0.75") == 0, "float OSC value becomes JSON number");
|
Expect(message.valueJson.find("0.75") == 0, "float OSC value becomes JSON number");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDecodeDoubleMessage()
|
||||||
|
{
|
||||||
|
OscServer server;
|
||||||
|
std::vector<char> 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()
|
void TestDecodeIntStringAndBoolMessages()
|
||||||
{
|
{
|
||||||
OscServer server;
|
OscServer server;
|
||||||
@@ -161,6 +182,7 @@ void TestRejectsUnsupportedAddress()
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
TestDecodeFloatMessage();
|
TestDecodeFloatMessage();
|
||||||
|
TestDecodeDoubleMessage();
|
||||||
TestDecodeIntStringAndBoolMessages();
|
TestDecodeIntStringAndBoolMessages();
|
||||||
TestDispatchValidAddress();
|
TestDispatchValidAddress();
|
||||||
TestRejectsUnsupportedAddress();
|
TestRejectsUnsupportedAddress();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { GripVertical, Trash2 } from "lucide-react";
|
|||||||
|
|
||||||
import { postJson } from "../api/controlApi";
|
import { postJson } from "../api/controlApi";
|
||||||
import { ParameterField } from "./ParameterField";
|
import { ParameterField } from "./ParameterField";
|
||||||
|
import { ShaderPicker } from "./ShaderPicker";
|
||||||
|
|
||||||
export function LayerCard({
|
export function LayerCard({
|
||||||
layer,
|
layer,
|
||||||
@@ -90,23 +91,17 @@ export function LayerCard({
|
|||||||
{expanded ? (
|
{expanded ? (
|
||||||
<div className="layer-card__body">
|
<div className="layer-card__body">
|
||||||
<div className="layer-card__field">
|
<div className="layer-card__field">
|
||||||
<label htmlFor={`shader-${layer.id}`}>Shader</label>
|
<ShaderPicker
|
||||||
<select
|
|
||||||
id={`shader-${layer.id}`}
|
id={`shader-${layer.id}`}
|
||||||
|
shaders={shaders}
|
||||||
value={layer.shaderId}
|
value={layer.shaderId}
|
||||||
onChange={(event) =>
|
onChange={(shaderId) =>
|
||||||
postJson("/api/layers/set-shader", {
|
postJson("/api/layers/set-shader", {
|
||||||
layerId: layer.id,
|
layerId: layer.id,
|
||||||
shaderId: event.target.value,
|
shaderId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
{shaders.map((shader) => (
|
|
||||||
<option key={shader.id} value={shader.id}>
|
|
||||||
{shader.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{layer.temporal?.enabled ? (
|
{layer.temporal?.enabled ? (
|
||||||
@@ -139,6 +134,7 @@ export function LayerCard({
|
|||||||
{layer.parameters.map((parameter) => (
|
{layer.parameters.map((parameter) => (
|
||||||
<ParameterField
|
<ParameterField
|
||||||
key={`${layer.id}:${parameter.id}`}
|
key={`${layer.id}:${parameter.id}`}
|
||||||
|
layer={layer}
|
||||||
parameter={parameter}
|
parameter={parameter}
|
||||||
onParameterChange={(parameterId, value) => onLayerParameterChange(layer.id, parameterId, value)}
|
onParameterChange={(parameterId, value) => onLayerParameterChange(layer.id, parameterId, value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { postJson } from "../api/controlApi";
|
import { postJson } from "../api/controlApi";
|
||||||
import { LayerCard } from "./LayerCard";
|
import { LayerCard } from "./LayerCard";
|
||||||
|
import { ShaderPicker } from "./ShaderPicker";
|
||||||
|
|
||||||
function moveItem(array, fromIndex, toIndex) {
|
function moveItem(array, fromIndex, toIndex) {
|
||||||
if (fromIndex < 0 || fromIndex >= array.length || toIndex < 0 || toIndex >= array.length) {
|
if (fromIndex < 0 || fromIndex >= array.length || toIndex < 0 || toIndex >= array.length) {
|
||||||
@@ -131,18 +132,12 @@ export function LayerStack({
|
|||||||
</div>
|
</div>
|
||||||
<div className="layer-card__body">
|
<div className="layer-card__body">
|
||||||
<div className="layer-card__field">
|
<div className="layer-card__field">
|
||||||
<label htmlFor="add-layer-select">Shader</label>
|
<ShaderPicker
|
||||||
<select
|
id="add-layer"
|
||||||
id="add-layer-select"
|
shaders={shaders}
|
||||||
value={pendingShaderId}
|
value={pendingShaderId}
|
||||||
onChange={(event) => setPendingShaderId(event.target.value)}
|
onChange={setPendingShaderId}
|
||||||
>
|
/>
|
||||||
{shaders.map((shader) => (
|
|
||||||
<option key={shader.id} value={shader.id}>
|
|
||||||
{shader.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,64 @@
|
|||||||
|
import { Copy } from "lucide-react";
|
||||||
|
|
||||||
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
|
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
|
||||||
import { ParameterValueDisplay } from "./ParameterValueDisplay";
|
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 (
|
||||||
|
<div className="parameter__header">
|
||||||
|
<label>{parameter.label}</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="parameter__osc"
|
||||||
|
title="Copy OSC route"
|
||||||
|
aria-label={`Copy OSC route ${oscRoute}`}
|
||||||
|
onClick={copyRoute}
|
||||||
|
>
|
||||||
|
<span>{oscRoute}</span>
|
||||||
|
<Copy size={13} strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
const {
|
||||||
appliedValue,
|
appliedValue,
|
||||||
beginInteraction,
|
beginInteraction,
|
||||||
@@ -12,12 +69,12 @@ export function ParameterField({ parameter, onParameterChange }) {
|
|||||||
sendValue,
|
sendValue,
|
||||||
} = useThrottledParameterValue(parameter, onParameterChange);
|
} = useThrottledParameterValue(parameter, onParameterChange);
|
||||||
|
|
||||||
const label = <label>{parameter.label}</label>;
|
const header = <ParameterHeader layer={layer} parameter={parameter} />;
|
||||||
|
|
||||||
if (parameter.type === "float") {
|
if (parameter.type === "float") {
|
||||||
return (
|
return (
|
||||||
<section className="parameter">
|
<section className="parameter">
|
||||||
{label}
|
{header}
|
||||||
<div className="parameter__pair">
|
<div className="parameter__pair">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -52,18 +109,17 @@ export function ParameterField({ parameter, onParameterChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter.type === "vec2" || parameter.type === "color") {
|
if (parameter.type === "vec2") {
|
||||||
const componentCount = parameter.type === "color" ? 4 : 2;
|
|
||||||
const values = [...(draftValue ?? [])];
|
const values = [...(draftValue ?? [])];
|
||||||
while (values.length < componentCount) {
|
while (values.length < 2) {
|
||||||
values.push(0);
|
values.push(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="parameter">
|
<section className="parameter">
|
||||||
{label}
|
{header}
|
||||||
<div className="parameter__pair">
|
<div className="parameter__pair">
|
||||||
{Array.from({ length: componentCount }, (_, index) => (
|
{Array.from({ length: 2 }, (_, index) => (
|
||||||
<input
|
<input
|
||||||
key={index}
|
key={index}
|
||||||
type="number"
|
type="number"
|
||||||
@@ -86,10 +142,50 @@ export function ParameterField({ parameter, onParameterChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameter.type === "color") {
|
||||||
|
const values = [...(draftValue ?? [])];
|
||||||
|
while (values.length < 4) {
|
||||||
|
values.push(values.length === 3 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="parameter">
|
||||||
|
{header}
|
||||||
|
<div className="parameter__color-row">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={colorValueToHex(values)}
|
||||||
|
onFocus={beginInteraction}
|
||||||
|
onChange={(event) => sendValue(hexToColorValue(event.target.value, values[3]))}
|
||||||
|
onBlur={endInteraction}
|
||||||
|
/>
|
||||||
|
<label className="parameter__alpha">
|
||||||
|
<span>Alpha</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={parameter.min?.[3] ?? 0}
|
||||||
|
max={parameter.max?.[3] ?? 1}
|
||||||
|
step={parameter.step?.[3] ?? 0.01}
|
||||||
|
value={values[3]}
|
||||||
|
onFocus={beginInteraction}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = [...values];
|
||||||
|
next[3] = Number(event.target.value);
|
||||||
|
sendValue(next);
|
||||||
|
}}
|
||||||
|
onBlur={endInteraction}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (parameter.type === "bool") {
|
if (parameter.type === "bool") {
|
||||||
return (
|
return (
|
||||||
<section className="parameter">
|
<section className="parameter">
|
||||||
{label}
|
{header}
|
||||||
<label className="toggle toggle--field">
|
<label className="toggle toggle--field">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -108,7 +204,7 @@ export function ParameterField({ parameter, onParameterChange }) {
|
|||||||
if (parameter.type === "enum") {
|
if (parameter.type === "enum") {
|
||||||
return (
|
return (
|
||||||
<section className="parameter">
|
<section className="parameter">
|
||||||
{label}
|
{header}
|
||||||
<select
|
<select
|
||||||
value={draftValue}
|
value={draftValue}
|
||||||
onFocus={beginInteraction}
|
onFocus={beginInteraction}
|
||||||
|
|||||||
94
ui/src/components/ShaderPicker.jsx
Normal file
94
ui/src/components/ShaderPicker.jsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { ChevronDown, Search } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
function matchesShader(shader, query) {
|
||||||
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
|
if (!normalizedQuery) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [shader.name, shader.id, shader.category, shader.description]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => value.toLowerCase().includes(normalizedQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShaderPicker({ id, label = "Shader", shaders, value, onChange }) {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const filteredShaders = useMemo(
|
||||||
|
() => shaders.filter((shader) => matchesShader(shader, query)),
|
||||||
|
[query, shaders],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedShader = shaders.find((shader) => shader.id === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shader-picker">
|
||||||
|
<div className="shader-picker__topline">
|
||||||
|
<label id={`${id}-label`}>{label}</label>
|
||||||
|
{selectedShader ? <span className="shader-picker__selected">{selectedShader.name}</span> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shader-picker__trigger"
|
||||||
|
aria-labelledby={`${id}-label`}
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span className="shader-picker__name">{selectedShader?.name ?? "Choose shader"}</span>
|
||||||
|
<span className="shader-picker__meta">
|
||||||
|
{selectedShader
|
||||||
|
? `${selectedShader.category ? `${selectedShader.category} / ` : ""}${selectedShader.id}`
|
||||||
|
: "Search available shaders"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ChevronDown size={16} strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open ? (
|
||||||
|
<div className="shader-picker__popover">
|
||||||
|
<div className="shader-picker__search">
|
||||||
|
<Search size={16} strokeWidth={1.75} aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
id={`${id}-search`}
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
placeholder="Search shaders"
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shader-picker__list" role="listbox" aria-label={label}>
|
||||||
|
{filteredShaders.length > 0 ? (
|
||||||
|
filteredShaders.map((shader) => (
|
||||||
|
<button
|
||||||
|
key={shader.id}
|
||||||
|
type="button"
|
||||||
|
className={`shader-picker__option${shader.id === value ? " shader-picker__option--selected" : ""}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={shader.id === value}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(shader.id);
|
||||||
|
setOpen(false);
|
||||||
|
setQuery("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="shader-picker__name">{shader.name}</span>
|
||||||
|
<span className="shader-picker__meta">
|
||||||
|
{shader.category ? `${shader.category} / ` : ""}
|
||||||
|
{shader.id}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="shader-picker__empty">No shaders found</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,8 +29,9 @@ export function StatusPanels({ app, performance, runtime, video }) {
|
|||||||
<KvList
|
<KvList
|
||||||
values={[
|
values={[
|
||||||
["Signal", video.hasSignal ? "Present" : "Missing"],
|
["Signal", video.hasSignal ? "Present" : "Missing"],
|
||||||
["Mode", video.modeName || "Unknown"],
|
["Input Mode", video.modeName || "Unknown"],
|
||||||
["Resolution", `${video.width || 0} x ${video.height || 0}`],
|
["Input Resolution", `${video.width || 0} x ${video.height || 0}`],
|
||||||
|
["Output Mode", `${app.outputVideoFormat || "Unknown"}${app.outputFrameRate ? ` ${app.outputFrameRate}` : ""}`],
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ input[type="range"] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="color"] {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #303a4d;
|
||||||
|
background: #101722;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
select,
|
select,
|
||||||
@@ -347,6 +356,128 @@ pre {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shader-picker {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__topline {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__selected {
|
||||||
|
min-width: 0;
|
||||||
|
color: #98aad0;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__search {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__search svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: #98aad0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__search input {
|
||||||
|
padding-left: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
background: #121b28;
|
||||||
|
border-color: #26364e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__trigger > span {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__trigger svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #98aad0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__popover {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #273246;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d141f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #273246;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #0c121b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__option {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
background: #121b28;
|
||||||
|
border-color: #26364e;
|
||||||
|
align-content: center;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__option--selected {
|
||||||
|
background: #233b5f;
|
||||||
|
border-color: #6d95d8;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(109, 149, 216, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__name,
|
||||||
|
.shader-picker__meta {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__meta,
|
||||||
|
.shader-picker__empty {
|
||||||
|
color: #98aad0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shader-picker__empty {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.layer-card__subheader button {
|
.layer-card__subheader button {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
@@ -354,19 +485,58 @@ pre {
|
|||||||
|
|
||||||
.parameter-grid {
|
.parameter-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter {
|
.parameter {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 7px;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border: 1px solid #273246;
|
border: 1px solid #273246;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #0f151f;
|
background: #0f151f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.parameter__header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(90px, auto) minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__osc {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 2px 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #98aad0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__osc:hover:not(:disabled) {
|
||||||
|
background: transparent;
|
||||||
|
color: #c4d6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__osc span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__osc svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.parameter__value {
|
.parameter__value {
|
||||||
color: #98aad0;
|
color: #98aad0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -378,8 +548,28 @@ pre {
|
|||||||
|
|
||||||
.parameter__pair {
|
.parameter__pair {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(auto-fit, minmax(82px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__pair input[type="range"] {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__color-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(92px, 0.42fr) minmax(120px, 0.58fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter__alpha {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
color: #98aad0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
|
|||||||
Reference in New Issue
Block a user