Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'source/gameengine/VideoTexture/VideoDeckLink.cpp')
-rw-r--r--source/gameengine/VideoTexture/VideoDeckLink.cpp1228
1 files changed, 1228 insertions, 0 deletions
diff --git a/source/gameengine/VideoTexture/VideoDeckLink.cpp b/source/gameengine/VideoTexture/VideoDeckLink.cpp
new file mode 100644
index 00000000000..4f5e34896fc
--- /dev/null
+++ b/source/gameengine/VideoTexture/VideoDeckLink.cpp
@@ -0,0 +1,1228 @@
+/*
+ * ***** 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_uint32(&mRefCount, 1U);
+}
+
+ULONG STDMETHODCALLTYPE PinnedMemoryAllocator::Release(void)
+{
+ uint32_t newCount = atomic_sub_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
+