12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229 |
- /*
- * ***** BEGIN GPL LICENSE BLOCK *****
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- *
- * The Original Code is Copyright (C) 2015, Blender Foundation
- * All rights reserved.
- *
- * The Original Code is: all of this file.
- *
- * Contributor(s): Blender Foundation.
- *
- * ***** END GPL LICENSE BLOCK *****
- */
- /** \file gameengine/VideoTexture/VideoDeckLink.cpp
- * \ingroup bgevideotex
- */
- #ifdef WITH_GAMEENGINE_DECKLINK
- // FFmpeg defines its own version of stdint.h on Windows.
- // Decklink needs FFmpeg, so it uses its version of stdint.h
- // this is necessary for INT64_C macro
- #ifndef __STDC_CONSTANT_MACROS
- #define __STDC_CONSTANT_MACROS
- #endif
- // this is necessary for UINTPTR_MAX (used by atomic-ops)
- #ifndef __STDC_LIMIT_MACROS
- #define __STDC_LIMIT_MACROS
- #ifdef __STDC_LIMIT_MACROS /* else it may be unused */
- #endif
- #endif
- #include <stdint.h>
- #include <string.h>
- #ifndef WIN32
- #include <sys/time.h>
- #include <sys/resource.h>
- #include <sys/mman.h>
- #endif
- #include "atomic_ops.h"
- #include "MEM_guardedalloc.h"
- #include "PIL_time.h"
- #include "VideoDeckLink.h"
- #include "DeckLink.h"
- #include "Exception.h"
- #include "KX_KetsjiEngine.h"
- #include "KX_PythonInit.h"
- extern ExceptionID DeckLinkInternalError;
- ExceptionID SourceVideoOnlyCapture, VideoDeckLinkBadFormat, VideoDeckLinkOpenCard, VideoDeckLinkDvpInternalError, VideoDeckLinkPinMemoryError;
- ExpDesc SourceVideoOnlyCaptureDesc(SourceVideoOnlyCapture, "This video source only allows live capture");
- ExpDesc VideoDeckLinkBadFormatDesc(VideoDeckLinkBadFormat, "Invalid or unsupported capture format, should be <mode>/<pixel>[/3D]");
- ExpDesc VideoDeckLinkOpenCardDesc(VideoDeckLinkOpenCard, "Cannot open capture card, check if driver installed");
- ExpDesc VideoDeckLinkDvpInternalErrorDesc(VideoDeckLinkDvpInternalError, "DVP API internal error, please report");
- ExpDesc VideoDeckLinkPinMemoryErrorDesc(VideoDeckLinkPinMemoryError, "Error pinning memory");
- #ifdef WIN32
- ////////////////////////////////////////////
- // SynInfo
- //
- // Sets up a semaphore which is shared between the GPU and CPU and used to
- // synchronise access to DVP buffers.
- #define DVP_CHECK(cmd) if ((cmd) != DVP_STATUS_OK) THRWEXCP(VideoDeckLinkDvpInternalError, S_OK)
- struct SyncInfo
- {
- SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment)
- {
- mSemUnaligned = (uint32_t*)malloc(semaphoreAllocSize + semaphoreAddrAlignment - 1);
- // Apply alignment constraints
- uint64_t val = (uint64_t)mSemUnaligned;
- val += semaphoreAddrAlignment - 1;
- val &= ~((uint64_t)semaphoreAddrAlignment - 1);
- mSem = (uint32_t*)val;
- // Initialise
- mSem[0] = 0;
- mReleaseValue = 0;
- mAcquireValue = 0;
- // Setup DVP sync object and import it
- DVPSyncObjectDesc syncObjectDesc;
- syncObjectDesc.externalClientWaitFunc = NULL;
- syncObjectDesc.sem = (uint32_t*)mSem;
- DVP_CHECK(dvpImportSyncObject(&syncObjectDesc, &mDvpSync));
- }
- ~SyncInfo()
- {
- dvpFreeSyncObject(mDvpSync);
- free((void*)mSemUnaligned);
- }
- volatile uint32_t* mSem;
- volatile uint32_t* mSemUnaligned;
- volatile uint32_t mReleaseValue;
- volatile uint32_t mAcquireValue;
- DVPSyncObjectHandle mDvpSync;
- };
- ////////////////////////////////////////////
- // TextureTransferDvp: transfer with GPUDirect
- ////////////////////////////////////////////
- class TextureTransferDvp : public TextureTransfer
- {
- public:
- TextureTransferDvp(DVPBufferHandle dvpTextureHandle, TextureDesc *pDesc, void *address, uint32_t allocatedSize)
- {
- DVPSysmemBufferDesc sysMemBuffersDesc;
- mExtSync = NULL;
- mGpuSync = NULL;
- mDvpSysMemHandle = 0;
- mDvpTextureHandle = 0;
- mTextureHeight = 0;
- mAllocatedSize = 0;
- mBuffer = NULL;
- if (!_PinBuffer(address, allocatedSize))
- THRWEXCP(VideoDeckLinkPinMemoryError, S_OK);
- mAllocatedSize = allocatedSize;
- mBuffer = address;
- try {
- if (!mBufferAddrAlignment) {
- DVP_CHECK(dvpGetRequiredConstantsGLCtx(&mBufferAddrAlignment, &mBufferGpuStrideAlignment,
- &mSemaphoreAddrAlignment, &mSemaphoreAllocSize,
- &mSemaphorePayloadOffset, &mSemaphorePayloadSize));
- }
- mExtSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
- mGpuSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
- sysMemBuffersDesc.width = pDesc->width;
- sysMemBuffersDesc.height = pDesc->height;
- sysMemBuffersDesc.stride = pDesc->stride;
- switch (pDesc->format) {
- case GL_RED_INTEGER:
- sysMemBuffersDesc.format = DVP_RED_INTEGER;
- break;
- default:
- sysMemBuffersDesc.format = DVP_BGRA;
- break;
- }
- switch (pDesc->type) {
- case GL_UNSIGNED_BYTE:
- sysMemBuffersDesc.type = DVP_UNSIGNED_BYTE;
- break;
- case GL_UNSIGNED_INT_2_10_10_10_REV:
- sysMemBuffersDesc.type = DVP_UNSIGNED_INT_2_10_10_10_REV;
- break;
- case GL_UNSIGNED_INT_8_8_8_8:
- sysMemBuffersDesc.type = DVP_UNSIGNED_INT_8_8_8_8;
- break;
- case GL_UNSIGNED_INT_10_10_10_2:
- sysMemBuffersDesc.type = DVP_UNSIGNED_INT_10_10_10_2;
- break;
- default:
- sysMemBuffersDesc.type = DVP_UNSIGNED_INT;
- break;
- }
- sysMemBuffersDesc.size = pDesc->width * pDesc->height * 4;
- sysMemBuffersDesc.bufAddr = mBuffer;
- DVP_CHECK(dvpCreateBuffer(&sysMemBuffersDesc, &mDvpSysMemHandle));
- DVP_CHECK(dvpBindToGLCtx(mDvpSysMemHandle));
- mDvpTextureHandle = dvpTextureHandle;
- mTextureHeight = pDesc->height;
- }
- catch (Exception &) {
- clean();
- throw;
- }
- }
- ~TextureTransferDvp()
- {
- clean();
- }
- virtual void PerformTransfer()
- {
- // perform the transfer
- // tell DVP that the old texture buffer will no longer be used
- dvpMapBufferEndAPI(mDvpTextureHandle);
- // do we need this?
- mGpuSync->mReleaseValue++;
- dvpBegin();
- // Copy from system memory to GPU texture
- dvpMapBufferWaitDVP(mDvpTextureHandle);
- dvpMemcpyLined(mDvpSysMemHandle, mExtSync->mDvpSync, mExtSync->mAcquireValue, DVP_TIMEOUT_IGNORED,
- mDvpTextureHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mTextureHeight);
- dvpMapBufferEndDVP(mDvpTextureHandle);
- dvpEnd();
- dvpMapBufferWaitAPI(mDvpTextureHandle);
- // the transfer is now complete and the texture is ready for use
- }
- private:
- static uint32_t mBufferAddrAlignment;
- static uint32_t mBufferGpuStrideAlignment;
- static uint32_t mSemaphoreAddrAlignment;
- static uint32_t mSemaphoreAllocSize;
- static uint32_t mSemaphorePayloadOffset;
- static uint32_t mSemaphorePayloadSize;
- void clean()
- {
- if (mDvpSysMemHandle) {
- dvpUnbindFromGLCtx(mDvpSysMemHandle);
- dvpDestroyBuffer(mDvpSysMemHandle);
- }
- if (mExtSync)
- delete mExtSync;
- if (mGpuSync)
- delete mGpuSync;
- if (mBuffer)
- _UnpinBuffer(mBuffer, mAllocatedSize);
- }
- SyncInfo* mExtSync;
- SyncInfo* mGpuSync;
- DVPBufferHandle mDvpSysMemHandle;
- DVPBufferHandle mDvpTextureHandle;
- uint32_t mTextureHeight;
- uint32_t mAllocatedSize;
- void* mBuffer;
- };
- uint32_t TextureTransferDvp::mBufferAddrAlignment;
- uint32_t TextureTransferDvp::mBufferGpuStrideAlignment;
- uint32_t TextureTransferDvp::mSemaphoreAddrAlignment;
- uint32_t TextureTransferDvp::mSemaphoreAllocSize;
- uint32_t TextureTransferDvp::mSemaphorePayloadOffset;
- uint32_t TextureTransferDvp::mSemaphorePayloadSize;
- #endif
- ////////////////////////////////////////////
- // TextureTransferOGL: transfer using standard OGL buffers
- ////////////////////////////////////////////
- class TextureTransferOGL : public TextureTransfer
- {
- public:
- TextureTransferOGL(GLuint texId, TextureDesc *pDesc, void *address)
- {
- memcpy(&mDesc, pDesc, sizeof(mDesc));
- mTexId = texId;
- mBuffer = address;
- // as we cache transfer object, we will create one texture to hold the buffer
- glGenBuffers(1, &mUnpinnedTextureBuffer);
- // create a storage for it
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer);
- glBufferData(GL_PIXEL_UNPACK_BUFFER, pDesc->size, NULL, GL_DYNAMIC_DRAW);
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
- }
- ~TextureTransferOGL()
- {
- glDeleteBuffers(1, &mUnpinnedTextureBuffer);
- }
- virtual void PerformTransfer()
- {
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer);
- glBufferSubData(GL_PIXEL_UNPACK_BUFFER, 0, mDesc.size, mBuffer);
- glBindTexture(GL_TEXTURE_2D, mTexId);
- // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDesc.width, mDesc.height, mDesc.format, mDesc.type, NULL);
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
- }
- private:
- // intermediate texture to receive the buffer
- GLuint mUnpinnedTextureBuffer;
- // target texture to receive the image
- GLuint mTexId;
- // buffer
- void *mBuffer;
- // characteristic of the image
- TextureDesc mDesc;
- };
- ////////////////////////////////////////////
- // TextureTransferPMB: transfer using pinned memory buffer
- ////////////////////////////////////////////
- class TextureTransferPMD : public TextureTransfer
- {
- public:
- TextureTransferPMD(GLuint texId, TextureDesc *pDesc, void *address, uint32_t allocatedSize)
- {
- memcpy(&mDesc, pDesc, sizeof(mDesc));
- mTexId = texId;
- mBuffer = address;
- mAllocatedSize = allocatedSize;
- _PinBuffer(address, allocatedSize);
- // as we cache transfer object, we will create one texture to hold the buffer
- glGenBuffers(1, &mPinnedTextureBuffer);
- // create a storage for it
- glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, mPinnedTextureBuffer);
- glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, pDesc->size, address, GL_STREAM_DRAW);
- glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0);
- }
- ~TextureTransferPMD()
- {
- glDeleteBuffers(1, &mPinnedTextureBuffer);
- if (mBuffer)
- _UnpinBuffer(mBuffer, mAllocatedSize);
- }
- virtual void PerformTransfer()
- {
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mPinnedTextureBuffer);
- glBindTexture(GL_TEXTURE_2D, mTexId);
- // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
- glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDesc.width, mDesc.height, mDesc.format, mDesc.type, NULL);
- // wait for the trasnfer to complete
- GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
- glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
- glDeleteSync(fence);
- glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
- }
- private:
- // intermediate texture to receive the buffer
- GLuint mPinnedTextureBuffer;
- // target texture to receive the image
- GLuint mTexId;
- // buffer
- void *mBuffer;
- // the allocated size
- uint32_t mAllocatedSize;
- // characteristic of the image
- TextureDesc mDesc;
- };
- bool TextureTransfer::_PinBuffer(void *address, uint32_t size)
- {
- #ifdef WIN32
- return VirtualLock(address, size);
- #elif defined(_POSIX_MEMLOCK_RANGE)
- return !mlock(address, size);
- #endif
- }
- void TextureTransfer::_UnpinBuffer(void* address, uint32_t size)
- {
- #ifdef WIN32
- VirtualUnlock(address, size);
- #elif defined(_POSIX_MEMLOCK_RANGE)
- munlock(address, size);
- #endif
- }
- ////////////////////////////////////////////
- // PinnedMemoryAllocator
- ////////////////////////////////////////////
- // static members
- bool PinnedMemoryAllocator::mGPUDirectInitialized = false;
- bool PinnedMemoryAllocator::mHasDvp = false;
- bool PinnedMemoryAllocator::mHasAMDPinnedMemory = false;
- size_t PinnedMemoryAllocator::mReservedProcessMemory = 0;
- bool PinnedMemoryAllocator::ReserveMemory(size_t size)
- {
- #ifdef WIN32
- // Increase the process working set size to allow pinning of memory.
- if (size <= mReservedProcessMemory)
- return true;
- SIZE_T dwMin = 0, dwMax = 0;
- HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, FALSE, GetCurrentProcessId());
- if (!hProcess)
- return false;
- // Retrieve the working set size of the process.
- if (!dwMin && !GetProcessWorkingSetSize(hProcess, &dwMin, &dwMax))
- return false;
- BOOL res = SetProcessWorkingSetSize(hProcess, (size - mReservedProcessMemory) + dwMin, (size - mReservedProcessMemory) + dwMax);
- if (!res)
- return false;
- mReservedProcessMemory = size;
- CloseHandle(hProcess);
- return true;
- #else
- struct rlimit rlim;
- if (getrlimit(RLIMIT_MEMLOCK, &rlim) == 0) {
- if (rlim.rlim_cur < size) {
- if (rlim.rlim_max < size)
- rlim.rlim_max = size;
- rlim.rlim_cur = size;
- return !setrlimit(RLIMIT_MEMLOCK, &rlim);
- }
- }
- return false;
- #endif
- }
- PinnedMemoryAllocator::PinnedMemoryAllocator(unsigned cacheSize, size_t memSize) :
- mRefCount(1U),
- #ifdef WIN32
- mDvpCaptureTextureHandle(0),
- #endif
- mTexId(0),
- mBufferCacheSize(cacheSize)
- {
- pthread_mutex_init(&mMutex, NULL);
- // do it once
- if (!mGPUDirectInitialized) {
- #ifdef WIN32
- // In windows, AMD_pinned_memory option is not available,
- // we must use special DVP API only available for Quadro cards
- const char* renderer = (const char *)glGetString(GL_RENDERER);
- mHasDvp = (strstr(renderer, "Quadro") != NULL);
- if (mHasDvp) {
- // In case the DLL is not in place, don't fail, just fallback on OpenGL
- if (dvpInitGLContext(DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT) != DVP_STATUS_OK) {
- printf("Warning: Could not initialize DVP context, fallback on OpenGL transfer.\nInstall dvp.dll to take advantage of nVidia GPUDirect.\n");
- mHasDvp = false;
- }
- }
- #endif
- if (GLEW_AMD_pinned_memory)
- mHasAMDPinnedMemory = true;
- mGPUDirectInitialized = true;
- }
- if (mHasDvp || mHasAMDPinnedMemory) {
- ReserveMemory(memSize);
- }
- }
- PinnedMemoryAllocator::~PinnedMemoryAllocator()
- {
- void *address;
- // first clean the cache if not already done
- while (!mBufferCache.empty()) {
- address = mBufferCache.back();
- mBufferCache.pop_back();
- _ReleaseBuffer(address);
- }
- // clean preallocated buffers
- while (!mAllocatedSize.empty()) {
- address = mAllocatedSize.begin()->first;
- _ReleaseBuffer(address);
- }
- #ifdef WIN32
- if (mDvpCaptureTextureHandle)
- dvpDestroyBuffer(mDvpCaptureTextureHandle);
- #endif
- }
- void PinnedMemoryAllocator::TransferBuffer(void* address, TextureDesc* texDesc, GLuint texId)
- {
- uint32_t allocatedSize = 0;
- TextureTransfer *pTransfer = NULL;
- Lock();
- if (mAllocatedSize.count(address) > 0)
- allocatedSize = mAllocatedSize[address];
- Unlock();
- if (!allocatedSize)
- // internal error!!
- return;
- if (mTexId != texId)
- {
- // first time we try to send data to the GPU, allocate a buffer for the texture
- glBindTexture(GL_TEXTURE_2D, texId);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
- glTexImage2D(GL_TEXTURE_2D, 0, texDesc->internalFormat, texDesc->width, texDesc->height, 0, texDesc->format, texDesc->type, NULL);
- glBindTexture(GL_TEXTURE_2D, 0);
- mTexId = texId;
- }
- #ifdef WIN32
- if (mHasDvp)
- {
- if (!mDvpCaptureTextureHandle)
- {
- // bind DVP to the OGL texture
- DVP_CHECK(dvpCreateGPUTextureGL(texId, &mDvpCaptureTextureHandle));
- }
- }
- #endif
- Lock();
- if (mPinnedBuffer.count(address) > 0)
- {
- pTransfer = mPinnedBuffer[address];
- }
- Unlock();
- if (!pTransfer)
- {
- #ifdef WIN32
- if (mHasDvp)
- pTransfer = new TextureTransferDvp(mDvpCaptureTextureHandle, texDesc, address, allocatedSize);
- else
- #endif
- if (mHasAMDPinnedMemory) {
- pTransfer = new TextureTransferPMD(texId, texDesc, address, allocatedSize);
- }
- else {
- pTransfer = new TextureTransferOGL(texId, texDesc, address);
- }
- if (pTransfer)
- {
- Lock();
- mPinnedBuffer[address] = pTransfer;
- Unlock();
- }
- }
- if (pTransfer)
- pTransfer->PerformTransfer();
- }
- // IUnknown methods
- HRESULT STDMETHODCALLTYPE PinnedMemoryAllocator::QueryInterface(REFIID /*iid*/, LPVOID* /*ppv*/)
- {
- return E_NOTIMPL;
- }
- ULONG STDMETHODCALLTYPE PinnedMemoryAllocator::AddRef(void)
- {
- return atomic_add_and_fetch_uint32(&mRefCount, 1U);
- }
- ULONG STDMETHODCALLTYPE PinnedMemoryAllocator::Release(void)
- {
- uint32_t newCount = atomic_sub_and_fetch_uint32(&mRefCount, 1U);
- if (newCount == 0)
- delete this;
- return (ULONG)newCount;
- }
- // IDeckLinkMemoryAllocator methods
- HRESULT STDMETHODCALLTYPE PinnedMemoryAllocator::AllocateBuffer(dl_size_t bufferSize, void* *allocatedBuffer)
- {
- Lock();
- if (mBufferCache.empty())
- {
- // Allocate memory on a page boundary
- // Note: aligned alloc exist in Blender but only for small alignment, use direct allocation then.
- // Note: the DeckLink API tries to allocate up to 65 buffer in advance, we will limit this to 3
- // because we don't need any caching
- if (mAllocatedSize.size() >= mBufferCacheSize)
- *allocatedBuffer = NULL;
- else {
- #ifdef WIN32
- *allocatedBuffer = VirtualAlloc(NULL, bufferSize, MEM_COMMIT | MEM_RESERVE | MEM_WRITE_WATCH, PAGE_READWRITE);
- #else
- if (posix_memalign(allocatedBuffer, 4096, bufferSize) != 0)
- *allocatedBuffer = NULL;
- #endif
- mAllocatedSize[*allocatedBuffer] = bufferSize;
- }
- }
- else {
- // Re-use most recently ReleaseBuffer'd address
- *allocatedBuffer = mBufferCache.back();
- mBufferCache.pop_back();
- }
- Unlock();
- return (*allocatedBuffer) ? S_OK : E_OUTOFMEMORY;
- }
- HRESULT STDMETHODCALLTYPE PinnedMemoryAllocator::ReleaseBuffer(void* buffer)
- {
- HRESULT result = S_OK;
- Lock();
- if (mBufferCache.size() < mBufferCacheSize) {
- mBufferCache.push_back(buffer);
- }
- else {
- result = _ReleaseBuffer(buffer);
- }
- Unlock();
- return result;
- }
- HRESULT PinnedMemoryAllocator::_ReleaseBuffer(void* buffer)
- {
- TextureTransfer *pTransfer;
- if (mAllocatedSize.count(buffer) == 0) {
- // Internal error!!
- return S_OK;
- }
- else {
- // No room left in cache, so un-pin (if it was pinned) and free this buffer
- if (mPinnedBuffer.count(buffer) > 0) {
- pTransfer = mPinnedBuffer[buffer];
- mPinnedBuffer.erase(buffer);
- delete pTransfer;
- }
- #ifdef WIN32
- VirtualFree(buffer, 0, MEM_RELEASE);
- #else
- free(buffer);
- #endif
- mAllocatedSize.erase(buffer);
- }
- return S_OK;
- }
- HRESULT STDMETHODCALLTYPE PinnedMemoryAllocator::Commit()
- {
- return S_OK;
- }
- HRESULT STDMETHODCALLTYPE PinnedMemoryAllocator::Decommit()
- {
- void *buffer;
- Lock();
- while (!mBufferCache.empty()) {
- // Cleanup any frames allocated and pinned in AllocateBuffer() but not freed in ReleaseBuffer()
- buffer = mBufferCache.back();
- mBufferCache.pop_back();
- _ReleaseBuffer(buffer);
- }
- Unlock();
- return S_OK;
- }
- ////////////////////////////////////////////
- // Capture Delegate Class
- ////////////////////////////////////////////
- CaptureDelegate::CaptureDelegate(VideoDeckLink* pOwner) : mpOwner(pOwner)
- {
- }
- HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket* /*audioPacket*/)
- {
- if (!inputFrame) {
- // It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
- return S_OK;
- }
- if ((inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource) {
- // let's not bother transferring frames if there is no source
- return S_OK;
- }
- mpOwner->VideoFrameArrived(inputFrame);
- return S_OK;
- }
- HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags)
- {
- return S_OK;
- }
- // macro for exception handling and logging
- #define CATCH_EXCP catch (Exception & exp) \
- { exp.report(); m_status = SourceError; }
- // class VideoDeckLink
- // constructor
- VideoDeckLink::VideoDeckLink (HRESULT * hRslt) : VideoBase(),
- mDLInput(NULL),
- mUse3D(false),
- mFrameWidth(0),
- mFrameHeight(0),
- mpAllocator(NULL),
- mpCaptureDelegate(NULL),
- mpCacheFrame(NULL),
- mClosing(false)
- {
- mDisplayMode = (BMDDisplayMode)0;
- mPixelFormat = (BMDPixelFormat)0;
- pthread_mutex_init(&mCacheMutex, NULL);
- }
- // destructor
- VideoDeckLink::~VideoDeckLink ()
- {
- LockCache();
- mClosing = true;
- if (mpCacheFrame)
- {
- mpCacheFrame->Release();
- mpCacheFrame = NULL;
- }
- UnlockCache();
- if (mDLInput != NULL)
- {
- // Cleanup for Capture
- mDLInput->StopStreams();
- mDLInput->SetCallback(NULL);
- mDLInput->DisableVideoInput();
- mDLInput->DisableAudioInput();
- mDLInput->FlushStreams();
- if (mDLInput->Release() != 0) {
- printf("Reference count not NULL on DeckLink device when closing it, please report!\n");
- }
- mDLInput = NULL;
- }
-
- if (mpAllocator)
- {
- // if the device was properly cleared, this should be 0
- if (mpAllocator->Release() != 0) {
- printf("Reference count not NULL on Allocator when closing it, please report!\n");
- }
- mpAllocator = NULL;
- }
- if (mpCaptureDelegate)
- {
- delete mpCaptureDelegate;
- mpCaptureDelegate = NULL;
- }
- }
- void VideoDeckLink::refresh(void)
- {
- m_avail = false;
- }
- // release components
- bool VideoDeckLink::release()
- {
- // release
- return true;
- }
- // open video file
- void VideoDeckLink::openFile (char *filename)
- {
- // only live capture on this device
- THRWEXCP(SourceVideoOnlyCapture, S_OK);
- }
- // open video capture device
- void VideoDeckLink::openCam (char *format, short camIdx)
- {
- IDeckLinkDisplayModeIterator* pDLDisplayModeIterator;
- BMDDisplayModeSupport modeSupport;
- IDeckLinkDisplayMode* pDLDisplayMode;
- IDeckLinkIterator* pIterator;
- BMDTimeValue frameDuration;
- BMDTimeScale frameTimescale;
- IDeckLink* pDL;
- uint32_t displayFlags, inputFlags;
- char *pPixel, *p3D, *pEnd, *pSize;
- size_t len;
- int i, modeIdx, cacheSize;
- // format is constructed as <displayMode>/<pixelFormat>[/3D][:<cacheSize>]
- // <displayMode> takes the form of BMDDisplayMode identifier minus the 'bmdMode' prefix.
- // This implementation understands all the modes defined in SDK 10.3.1 but you can alternatively
- // use the 4 characters internal representation of the mode (e.g. 'HD1080p24' == '24ps')
- // <pixelFormat> takes the form of BMDPixelFormat identifier minus the 'bmdFormat' prefix.
- // This implementation understand all the formats defined in SDK 10.32.1 but you can alternatively
- // use the 4 characters internal representation of the format (e.g. '10BitRGB' == 'r210')
- // Not all combinations of mode and pixel format are possible and it also depends on the card!
- // Use /3D postfix if you are capturing a 3D stream with frame packing
- // Example: To capture FullHD 1920x1080@24Hz with 3D packing and 4:4:4 10 bits RGB pixel format, use
- // "HD1080p24/10BitRGB/3D" (same as "24ps/r210/3D")
- // (this will be the normal capture format for FullHD on the DeckLink 4k extreme)
- if ((pSize = strchr(format, ':')) != NULL) {
- cacheSize = strtol(pSize+1, &pEnd, 10);
- }
- else {
- cacheSize = 8;
- pSize = format + strlen(format);
- }
- if ((pPixel = strchr(format, '/')) == NULL ||
- ((p3D = strchr(pPixel + 1, '/')) != NULL && strncmp(p3D, "/3D", pSize-p3D)))
- THRWEXCP(VideoDeckLinkBadFormat, S_OK);
- mUse3D = (p3D) ? true : false;
- // to simplify pixel format parsing
- if (!p3D)
- p3D = pSize;
- // read the mode
- len = (size_t)(pPixel - format);
- // accept integer display mode
- try {
- // throws if bad mode
- decklink_ReadDisplayMode(format, len, &mDisplayMode);
- // found a valid mode, remember that we do not look for an index
- modeIdx = -1;
- }
- catch (Exception &) {
- // accept also purely numerical mode as a mode index
- modeIdx = strtol(format, &pEnd, 10);
- if (pEnd != pPixel || modeIdx < 0)
- // not a pure number, give up
- throw;
- }
- // skip /
- pPixel++;
- len = (size_t)(p3D - pPixel);
- // throws if bad format
- decklink_ReadPixelFormat(pPixel, len, &mPixelFormat);
- // Caution: DeckLink API used from this point, make sure entity are released before throwing
- // open the card
- pIterator = BMD_CreateDeckLinkIterator();
- if (pIterator) {
- i = 0;
- while (pIterator->Next(&pDL) == S_OK) {
- if (i == camIdx) {
- if (pDL->QueryInterface(IID_IDeckLinkInput, (void**)&mDLInput) != S_OK)
- mDLInput = NULL;
- pDL->Release();
- break;
- }
- i++;
- pDL->Release();
- }
- pIterator->Release();
- }
- if (!mDLInput)
- THRWEXCP(VideoDeckLinkOpenCard, S_OK);
-
- // check if display mode and pixel format are supported
- if (mDLInput->GetDisplayModeIterator(&pDLDisplayModeIterator) != S_OK)
- THRWEXCP(DeckLinkInternalError, S_OK);
- pDLDisplayMode = NULL;
- displayFlags = (mUse3D) ? bmdDisplayModeSupports3D : 0;
- inputFlags = (mUse3D) ? bmdVideoInputDualStream3D : bmdVideoInputFlagDefault;
- while (pDLDisplayModeIterator->Next(&pDLDisplayMode) == S_OK)
- {
- if (modeIdx == 0 || pDLDisplayMode->GetDisplayMode() == mDisplayMode) {
- // in case we get here because of modeIdx, make sure we have mDisplayMode set
- mDisplayMode = pDLDisplayMode->GetDisplayMode();
- if ((pDLDisplayMode->GetFlags() & displayFlags) == displayFlags &&
- mDLInput->DoesSupportVideoMode(mDisplayMode, mPixelFormat, inputFlags, &modeSupport, NULL) == S_OK &&
- modeSupport == bmdDisplayModeSupported)
- {
- break;
- }
- }
- pDLDisplayMode->Release();
- pDLDisplayMode = NULL;
- if (modeIdx-- == 0) {
- // reached the correct mode index but it does not meet the pixel format, give up
- break;
- }
- }
- pDLDisplayModeIterator->Release();
- if (pDLDisplayMode == NULL)
- THRWEXCP(VideoDeckLinkBadFormat, S_OK);
- mFrameWidth = pDLDisplayMode->GetWidth();
- mFrameHeight = pDLDisplayMode->GetHeight();
- mTextureDesc.height = (mUse3D) ? 2 * mFrameHeight : mFrameHeight;
- pDLDisplayMode->GetFrameRate(&frameDuration, &frameTimescale);
- pDLDisplayMode->Release();
- // for information, in case the application wants to know
- m_size[0] = mFrameWidth;
- m_size[1] = mTextureDesc.height;
- m_frameRate = (float)frameTimescale / (float)frameDuration;
- switch (mPixelFormat)
- {
- case bmdFormat8BitYUV:
- // 2 pixels per word
- mTextureDesc.stride = mFrameWidth * 2;
- mTextureDesc.width = mFrameWidth / 2;
- mTextureDesc.internalFormat = GL_RGBA;
- mTextureDesc.format = GL_BGRA;
- mTextureDesc.type = GL_UNSIGNED_BYTE;
- break;
- case bmdFormat10BitYUV:
- // 6 pixels in 4 words, rounded to 48 pixels
- mTextureDesc.stride = ((mFrameWidth + 47) / 48) * 128;
- mTextureDesc.width = mTextureDesc.stride/4;
- mTextureDesc.internalFormat = GL_RGB10_A2;
- mTextureDesc.format = GL_BGRA;
- mTextureDesc.type = GL_UNSIGNED_INT_2_10_10_10_REV;
- break;
- case bmdFormat8BitARGB:
- mTextureDesc.stride = mFrameWidth * 4;
- mTextureDesc.width = mFrameWidth;
- mTextureDesc.internalFormat = GL_RGBA;
- mTextureDesc.format = GL_BGRA;
- mTextureDesc.type = GL_UNSIGNED_INT_8_8_8_8;
- break;
- case bmdFormat8BitBGRA:
- mTextureDesc.stride = mFrameWidth * 4;
- mTextureDesc.width = mFrameWidth;
- mTextureDesc.internalFormat = GL_RGBA;
- mTextureDesc.format = GL_BGRA;
- mTextureDesc.type = GL_UNSIGNED_BYTE;
- break;
- case bmdFormat10BitRGBXLE:
- // 1 pixel per word, rounded to 64 pixels
- mTextureDesc.stride = ((mFrameWidth + 63) / 64) * 256;
- mTextureDesc.width = mTextureDesc.stride/4;
- mTextureDesc.internalFormat = GL_RGB10_A2;
- mTextureDesc.format = GL_RGBA;
- mTextureDesc.type = GL_UNSIGNED_INT_10_10_10_2;
- break;
- case bmdFormat10BitRGBX:
- case bmdFormat10BitRGB:
- // 1 pixel per word, rounded to 64 pixels
- mTextureDesc.stride = ((mFrameWidth + 63) / 64) * 256;
- mTextureDesc.width = mTextureDesc.stride/4;
- mTextureDesc.internalFormat = GL_R32UI;
- mTextureDesc.format = GL_RED_INTEGER;
- mTextureDesc.type = GL_UNSIGNED_INT;
- break;
- case bmdFormat12BitRGB:
- case bmdFormat12BitRGBLE:
- // 8 pixels in 9 word
- mTextureDesc.stride = (mFrameWidth * 36) / 8;
- mTextureDesc.width = mTextureDesc.stride/4;
- mTextureDesc.internalFormat = GL_R32UI;
- mTextureDesc.format = GL_RED_INTEGER;
- mTextureDesc.type = GL_UNSIGNED_INT;
- break;
- default:
- // for unknown pixel format, this will be resolved when a frame arrives
- mTextureDesc.format = GL_RED_INTEGER;
- mTextureDesc.type = GL_UNSIGNED_INT;
- break;
- }
- // reserve memory for cache frame + 1 to accomodate for pixel format that we don't know yet
- // note: we can't use stride as it is not yet known if the pixel format is unknown
- // use instead the frame width as in worst case it's not much different (e.g. HD720/10BITYUV: 1296 pixels versus 1280)
- // note: some pixel format take more than 4 bytes take that into account (9/8 versus 1)
- mpAllocator = new PinnedMemoryAllocator(cacheSize, mFrameWidth*mTextureDesc.height * 4 * (1+cacheSize*9/8));
- if (mDLInput->SetVideoInputFrameMemoryAllocator(mpAllocator) != S_OK)
- THRWEXCP(DeckLinkInternalError, S_OK);
- mpCaptureDelegate = new CaptureDelegate(this);
- if (mDLInput->SetCallback(mpCaptureDelegate) != S_OK)
- THRWEXCP(DeckLinkInternalError, S_OK);
- if (mDLInput->EnableVideoInput(mDisplayMode, mPixelFormat, ((mUse3D) ? bmdVideoInputDualStream3D : bmdVideoInputFlagDefault)) != S_OK)
- // this shouldn't failed, we tested above
- THRWEXCP(DeckLinkInternalError, S_OK);
- // just in case it is needed to capture from certain cards, we don't check error because we don't need audio
- mDLInput->EnableAudioInput(bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, 2);
- // open base class
- VideoBase::openCam(format, camIdx);
- // ready to capture, will start when application calls play()
- }
- // play video
- bool VideoDeckLink::play (void)
- {
- try
- {
- // if object is able to play
- if (VideoBase::play())
- {
- mDLInput->FlushStreams();
- return (mDLInput->StartStreams() == S_OK);
- }
- }
- CATCH_EXCP;
- return false;
- }
- // pause video
- bool VideoDeckLink::pause (void)
- {
- try
- {
- if (VideoBase::pause())
- {
- mDLInput->PauseStreams();
- return true;
- }
- }
- CATCH_EXCP;
- return false;
- }
- // stop video
- bool VideoDeckLink::stop (void)
- {
- try
- {
- VideoBase::stop();
- mDLInput->StopStreams();
- return true;
- }
- CATCH_EXCP;
- return false;
- }
- // set video range
- void VideoDeckLink::setRange (double start, double stop)
- {
- }
- // set framerate
- void VideoDeckLink::setFrameRate (float rate)
- {
- }
- // image calculation
- // send cache frame directly to GPU
- void VideoDeckLink::calcImage (unsigned int texId, double ts)
- {
- IDeckLinkVideoInputFrame* pFrame;
- LockCache();
- pFrame = mpCacheFrame;
- mpCacheFrame = NULL;
- UnlockCache();
- if (pFrame) {
- // BUG: the dvpBindToGLCtx function fails the first time it is used, don't know why.
- // This causes an exception to be thrown.
- // This should be fixed but in the meantime we will catch the exception because
- // it is crucial that we release the frame to keep the reference count right on the DeckLink device
- try {
- uint32_t rowSize = pFrame->GetRowBytes();
- uint32_t textureSize = rowSize * pFrame->GetHeight();
- void* videoPixels = NULL;
- void* rightEyePixels = NULL;
- if (!mTextureDesc.stride) {
- // we could not compute the texture size earlier (unknown pixel size)
- // let's do it now
- mTextureDesc.stride = rowSize;
- mTextureDesc.width = mTextureDesc.stride / 4;
- }
- if (mTextureDesc.stride != rowSize) {
- // unexpected frame size, ignore
- // TBD: print a warning
- }
- else {
- pFrame->GetBytes(&videoPixels);
- if (mUse3D) {
- IDeckLinkVideoFrame3DExtensions *if3DExtensions = NULL;
- IDeckLinkVideoFrame *rightEyeFrame = NULL;
- if (pFrame->QueryInterface(IID_IDeckLinkVideoFrame3DExtensions, (void **)&if3DExtensions) == S_OK &&
- if3DExtensions->GetFrameForRightEye(&rightEyeFrame) == S_OK) {
- rightEyeFrame->GetBytes(&rightEyePixels);
- textureSize += ((uint64_t)rightEyePixels - (uint64_t)videoPixels);
- }
- if (rightEyeFrame)
- rightEyeFrame->Release();
- if (if3DExtensions)
- if3DExtensions->Release();
- }
- mTextureDesc.size = mTextureDesc.width * mTextureDesc.height * 4;
- if (mTextureDesc.size == textureSize) {
- // this means that both left and right frame are contiguous and that there is no padding
- // do the transfer
- mpAllocator->TransferBuffer(videoPixels, &mTextureDesc, texId);
- }
- }
- }
- catch (Exception &) {
- pFrame->Release();
- throw;
- }
- // this will trigger PinnedMemoryAllocator::RealaseBuffer
- pFrame->Release();
- }
- // currently we don't pass the image to the application
- m_avail = false;
- }
- // A frame is available from the board
- // Called from an internal thread, just pass the frame to the main thread
- void VideoDeckLink::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
- {
- IDeckLinkVideoInputFrame* pOldFrame = NULL;
- LockCache();
- if (!mClosing)
- {
- pOldFrame = mpCacheFrame;
- mpCacheFrame = inputFrame;
- inputFrame->AddRef();
- }
- UnlockCache();
- // old frame no longer needed, just release it
- if (pOldFrame)
- pOldFrame->Release();
- }
- // python methods
- // object initialization
- static int VideoDeckLink_init(PyObject *pySelf, PyObject *args, PyObject *kwds)
- {
- static const char *kwlist[] = { "format", "capture", NULL };
- PyImage *self = reinterpret_cast<PyImage*>(pySelf);
- // see openCam for a description of format
- char * format = NULL;
- // capture device number, i.e. DeckLink card number, default first one
- short capt = 0;
- if (!GLEW_VERSION_1_5) {
- PyErr_SetString(PyExc_RuntimeError, "VideoDeckLink requires at least OpenGL 1.5");
- return -1;
- }
- // get parameters
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|h",
- const_cast<char**>(kwlist), &format, &capt))
- return -1;
- try {
- // create video object
- Video_init<VideoDeckLink>(self);
- // open video source, control comes back to VideoDeckLink::openCam
- Video_open(getVideo(self), format, capt);
- }
- catch (Exception & exp) {
- exp.report();
- return -1;
- }
- // initialization succeded
- return 0;
- }
- // methods structure
- static PyMethodDef videoMethods[] =
- { // methods from VideoBase class
- {"play", (PyCFunction)Video_play, METH_NOARGS, "Play (restart) video"},
- {"pause", (PyCFunction)Video_pause, METH_NOARGS, "pause video"},
- {"stop", (PyCFunction)Video_stop, METH_NOARGS, "stop video (play will replay it from start)"},
- {"refresh", (PyCFunction)Video_refresh, METH_VARARGS, "Refresh video - get its status"},
- {NULL}
- };
- // attributes structure
- static PyGetSetDef videoGetSets[] =
- { // methods from VideoBase class
- {(char*)"status", (getter)Video_getStatus, NULL, (char*)"video status", NULL},
- {(char*)"framerate", (getter)Video_getFrameRate, NULL, (char*)"frame rate", NULL},
- // attributes from ImageBase class
- {(char*)"valid", (getter)Image_valid, NULL, (char*)"bool to tell if an image is available", NULL},
- {(char*)"image", (getter)Image_getImage, NULL, (char*)"image data", NULL},
- {(char*)"size", (getter)Image_getSize, NULL, (char*)"image size", NULL},
- {(char*)"scale", (getter)Image_getScale, (setter)Image_setScale, (char*)"fast scale of image (near neighbor)", NULL},
- {(char*)"flip", (getter)Image_getFlip, (setter)Image_setFlip, (char*)"flip image vertically", NULL},
- {(char*)"filter", (getter)Image_getFilter, (setter)Image_setFilter, (char*)"pixel filter", NULL},
- {NULL}
- };
- // python type declaration
- PyTypeObject VideoDeckLinkType =
- {
- PyVarObject_HEAD_INIT(NULL, 0)
- "VideoTexture.VideoDeckLink", /*tp_name*/
- sizeof(PyImage), /*tp_basicsize*/
- 0, /*tp_itemsize*/
- (destructor)Image_dealloc, /*tp_dealloc*/
- 0, /*tp_print*/
- 0, /*tp_getattr*/
- 0, /*tp_setattr*/
- 0, /*tp_compare*/
- 0, /*tp_repr*/
- 0, /*tp_as_number*/
- 0, /*tp_as_sequence*/
- 0, /*tp_as_mapping*/
- 0, /*tp_hash */
- 0, /*tp_call*/
- 0, /*tp_str*/
- 0, /*tp_getattro*/
- 0, /*tp_setattro*/
- &imageBufferProcs, /*tp_as_buffer*/
- Py_TPFLAGS_DEFAULT, /*tp_flags*/
- "DeckLink video source", /* tp_doc */
- 0, /* tp_traverse */
- 0, /* tp_clear */
- 0, /* tp_richcompare */
- 0, /* tp_weaklistoffset */
- 0, /* tp_iter */
- 0, /* tp_iternext */
- videoMethods, /* tp_methods */
- 0, /* tp_members */
- videoGetSets, /* tp_getset */
- 0, /* tp_base */
- 0, /* tp_dict */
- 0, /* tp_descr_get */
- 0, /* tp_descr_set */
- 0, /* tp_dictoffset */
- (initproc)VideoDeckLink_init, /* tp_init */
- 0, /* tp_alloc */
- Image_allocNew, /* tp_new */
- };
- ////////////////////////////////////////////
- // DeckLink Capture Delegate Class
- ////////////////////////////////////////////
- #endif // WITH_GAMEENGINE_DECKLINK
|