blender/source/gameengine/VideoTexture/VideoDeckLink.cpp
2016-07-02 10:08:33 +10:00

1229 lines
36 KiB
C++

/*
* ***** 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