/* -LICENSE-START-
 ** Copyright (c) 2012 Blackmagic Design
 **
 ** Permission is hereby granted, free of charge, to any person or organization
 ** obtaining a copy of the software and accompanying documentation covered by
 ** this license (the "Software") to use, reproduce, display, distribute,
 ** execute, and transmit the Software, and to prepare derivative works of the
 ** Software, and to permit third-parties to whom the Software is furnished to
 ** do so, all subject to the following:
 **
 ** The copyright notices in the Software and this entire statement, including
 ** the above license grant, this restriction and the following disclaimer,
 ** must be included in all copies of the Software, in whole or in part, and
 ** all derivative works of the Software, unless such copies or derivative
 ** works are solely in the form of machine-executable object code generated by
 ** a source language processor.
 **
 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 ** DEALINGS IN THE SOFTWARE.
 ** -LICENSE-END-
 */

#include "OpenGLComposite.h"
#include "GLExtensions.h"


OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
	hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
	mCaptureDelegate(NULL), mPlayoutDelegate(NULL),
	mDLInput(NULL), mDLOutput(NULL),
	mCaptureAllocator(NULL), mPlayoutAllocator(NULL),
	mFrameWidth(0), mFrameHeight(0),
	mHasNoInputSource(true),
	mFastTransferExtensionAvailable(false),
	mCaptureTexture(0),
	mFBOTexture(0),
	mRotateAngle(0.0f),
	mRotateAngleRate(0.0f)
{
	InitializeCriticalSection(&pMutex);
}

OpenGLComposite::~OpenGLComposite()
{
	if (mDLInput != NULL)
	{
		// Cleanup for Capture
		mDLInput->SetCallback(NULL);

		mDLInput->Release();
		mDLInput = NULL;
	}

	delete mCaptureDelegate;
	delete mCaptureAllocator;

	if (mDLOutput != NULL)
	{
		// Cleanup for Playout
		mDLOutput->SetScheduledFrameCompletionCallback(NULL);

		mDLOutput->Release();
		mDLOutput = NULL;
	}

	delete mPlayoutDelegate;
	delete mPlayoutAllocator;

	DeleteCriticalSection(&pMutex);
}

bool OpenGLComposite::InitDeckLink()
{
	bool							bSuccess = false;
	IDeckLinkIterator*				pDLIterator = NULL;
	IDeckLink*						pDL = NULL;
	IDeckLinkDisplayModeIterator*	pDLDisplayModeIterator = NULL;
	IDeckLinkDisplayMode*			pDLDisplayMode = NULL;
	BMDDisplayMode					displayMode = bmdModeHD1080i5994;		// mode to use for capture and playout
	float							fps;
	HRESULT							result;

	result = CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&pDLIterator);
	if (FAILED(result))
	{
		MessageBox(NULL, _T("Please install the Blackmagic DeckLink drivers to use the features of this application."), _T("This application requires the DeckLink drivers installed."), MB_OK);
		return false;
	}

	while (pDLIterator->Next(&pDL) == S_OK)
	{
		// Use first board found as capture device, second board will be playout device
		if (! mDLInput)
		{
			if (pDL->QueryInterface(IID_IDeckLinkInput, (void**)&mDLInput) != S_OK)
				goto error;
		}
		else if (! mDLOutput)
		{
			if (pDL->QueryInterface(IID_IDeckLinkOutput, (void**)&mDLOutput) != S_OK)
				goto error;
		}
	}

	if (! mDLOutput || ! mDLInput)
	{
		MessageBox(NULL, _T("Expected both Input and Output DeckLink devices"), _T("This application requires two DeckLink devices."), MB_OK);
		goto error;
	}

	if (mDLOutput->GetDisplayModeIterator(&pDLDisplayModeIterator) != S_OK)
	{
		MessageBox(NULL, _T("Cannot get Display Mode Iterator."), _T("DeckLink error."), MB_OK);
		goto error;
	}

	while (pDLDisplayModeIterator->Next(&pDLDisplayMode) == S_OK)
	{
		if (pDLDisplayMode->GetDisplayMode() == displayMode)
			break;

		pDLDisplayMode->Release();
		pDLDisplayMode = NULL;
	}
	pDLDisplayModeIterator->Release();

	if (pDLDisplayMode == NULL)
	{
		MessageBox(NULL, _T("Cannot get specified BMDDisplayMode."), _T("DeckLink error."), MB_OK);
		goto error;
	}

	mFrameWidth = pDLDisplayMode->GetWidth();
	mFrameHeight = pDLDisplayMode->GetHeight();

	if (! CheckOpenGLExtensions())
		goto error;

	if (! InitOpenGLState())
		goto error;

	// Compute a rotate angle rate so box will spin at a rate independent of video mode frame rate
	pDLDisplayMode->GetFrameRate(&mFrameDuration, &mFrameTimescale);
	fps = (float)mFrameTimescale / (float)mFrameDuration;
	mRotateAngleRate = 35.0f / fps;			// rotate box through 35 degrees every second

	// Resize window to match video frame, but scale large formats down by half for viewing
	if (mFrameWidth < 1920)
		resizeWindow(mFrameWidth, mFrameHeight);
	else
		resizeWindow(mFrameWidth / 2, mFrameHeight / 2);

	if (mFastTransferExtensionAvailable)
	{
		// Initialize fast video frame transfers
		if (! VideoFrameTransfer::initialize(mFrameWidth, mFrameHeight, mCaptureTexture, mFBOTexture))
		{
			MessageBox(NULL, _T("Cannot initialize video transfers."), _T("VideoFrameTransfer error."), MB_OK);
			goto error;
		}
	}

	// Capture will use a user-supplied frame memory allocator
	mCaptureAllocator = new PinnedMemoryAllocator(hGLDC, hGLRC, VideoFrameTransfer::CPUtoGPU, 3);

	if (mDLInput->SetVideoInputFrameMemoryAllocator(mCaptureAllocator) != S_OK)
		goto error;

	if (mDLInput->EnableVideoInput(displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) != S_OK)
		goto error;

	mCaptureDelegate = new CaptureDelegate(this);
	if (mDLInput->SetCallback(mCaptureDelegate) != S_OK)
		goto error;

	// Playout will use a user-supplied frame memory allocator
	mPlayoutAllocator = new PinnedMemoryAllocator(hGLDC, hGLRC, VideoFrameTransfer::GPUtoCPU, 1);

	if (mDLOutput->SetVideoOutputFrameMemoryAllocator(mPlayoutAllocator) != S_OK)
		goto error;

	if (mDLOutput->EnableVideoOutput(displayMode, bmdVideoOutputFlagDefault) != S_OK)
		goto error;

	// Create a queue of 10 IDeckLinkMutableVideoFrame objects to use for scheduling output video frames.
	// The ScheduledFrameCompleted() callback will immediately schedule a new frame using the next video frame from this queue.
	for (int i = 0; i < 10; i++)
	{
		// The frame read back from the GPU frame buffer and used for the playout video frame is in BGRA format.
		// The BGRA frame will be converted on playout to YCbCr either in hardware on most DeckLink cards or in software
		// within the DeckLink API for DeckLink devices without this hardware conversion.
		// If you want RGB 4:4:4 format to be played out "over the wire" in SDI, turn on the "Use 4:4:4 SDI" in the control
		// panel or turn on the bmdDeckLinkConfig444SDIVideoOutput flag using the IDeckLinkConfiguration interface.
		IDeckLinkMutableVideoFrame* outputFrame;
		if (mDLOutput->CreateVideoFrame(mFrameWidth, mFrameHeight, mFrameWidth*4, bmdFormat8BitBGRA, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
			goto error;

		mDLOutputVideoFrameQueue.push_back(outputFrame);
	}

	mPlayoutDelegate = new PlayoutDelegate(this);
	if (mPlayoutDelegate == NULL)
		goto error;

	if (mDLOutput->SetScheduledFrameCompletionCallback(mPlayoutDelegate) != S_OK)
		goto error;

	bSuccess = true;

error:
	if (!bSuccess)
	{
		if (mDLInput != NULL)
		{
			mDLInput->Release();
			mDLInput = NULL;
		}
		if (mDLOutput != NULL)
		{
			mDLOutput->Release();
			mDLOutput = NULL;
		}
	}

	if (pDL != NULL)
	{
		pDL->Release();
		pDL = NULL;
	}

	if (pDLIterator != NULL)
	{
		pDLIterator->Release();
		pDLIterator = NULL;
	}

	return bSuccess;
}

void OpenGLComposite::paintGL()
{
	// The DeckLink API provides IDeckLinkGLScreenPreviewHelper as a convenient way to view the playout video frames
	// in a window.  However, it performs a copy from host memory to the GPU which is wasteful in this case since
	// we already have the rendered frame to be played out sitting in the GPU in the mIdFrameBuf frame buffer.

	// Simply copy the off-screen frame buffer to on-screen frame buffer, scaling to the viewing window size.
	glBindFramebufferEXT(GL_READ_FRAMEBUFFER, mIdFrameBuf);
	glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
	glViewport(0, 0, mViewWidth, mViewHeight);
	glBlitFramebufferEXT(0, 0, mFrameWidth, mFrameHeight, 0, 0, mViewWidth, mViewHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);

	SwapBuffers(hGLDC);
	ValidateRect(hGLWnd, NULL);
}

void OpenGLComposite::resizeGL(WORD width, WORD height)
{
	// We don't set the project or model matrices here since the window data is copied directly from
	// an off-screen FBO in paintGL().  Just save the width and height for use in paintGL().
	mViewWidth = width;
	mViewHeight = height;
}

void OpenGLComposite::resizeWindow(int width, int height)
{
	RECT r;
	if (GetWindowRect(hGLWnd, &r))
	{
		SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
	}
}

bool OpenGLComposite::InitOpenGLState()
{
	if (! ResolveGLExtensions())
		return false;

	// Prepare the shader used to perform colour space conversion on the video texture
	char compilerErrorMessage[1024];
	if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
	{
		MessageBoxA(NULL, compilerErrorMessage, "OpenGL Shader failed to compile", MB_OK);
		return false;
	}

	// Setup the scene
	glShadeModel( GL_SMOOTH );					// Enable smooth shading
	glClearColor( 0.0f, 0.0f, 0.0f, 0.5f );		// Black background
	glClearDepth( 1.0f );						// Depth buffer setup
	glEnable( GL_DEPTH_TEST );					// Enable depth testing
	glDepthFunc( GL_LEQUAL );					// Type of depth test to do
	glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );

	if (! mFastTransferExtensionAvailable)
	{
		glGenBuffers(1, &mUnpinnedTextureBuffer);
	}

	// Setup the texture which will hold the captured video frame pixels
	glEnable(GL_TEXTURE_2D);
	glGenTextures(1, &mCaptureTexture);
	glBindTexture(GL_TEXTURE_2D, mCaptureTexture);

	// Parameters to control how texels are sampled from the texture
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

	// Create texture with empty data, we will update it using glTexSubImage2D each frame.
	// The captured video is YCbCr 4:2:2 packed into a UYVY macropixel.  OpenGL has no YCbCr format
	// so treat it as RGBA 4:4:4:4 by halving the width and using GL_RGBA internal format.
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
	glBindTexture(GL_TEXTURE_2D, 0);


	// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
	// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
	glGenFramebuffersEXT(1, &mIdFrameBuf);
	glGenRenderbuffersEXT(1, &mIdColorBuf);
	glGenRenderbuffersEXT(1, &mIdDepthBuf);

	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf);

	// Texture for FBO
	glGenTextures(1, &mFBOTexture);
	glBindTexture(GL_TEXTURE_2D, mFBOTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);

	// Attach a depth buffer
	glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, mIdDepthBuf);
	glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, mFrameWidth, mFrameHeight);

	glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, mIdDepthBuf);

	// Attach the texture which stores the playback image
	glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, mFBOTexture, 0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glDisable(GL_TEXTURE_2D);

	GLenum glStatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
	if (glStatus != GL_FRAMEBUFFER_COMPLETE_EXT)
	{
		MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK);
		return false;
	}

	return true;
}

//
// Update the captured video frame texture
//
void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
{
	mHasNoInputSource = hasNoInputSource;
	if (mHasNoInputSource)
		return;							// don't transfer texture when there's no input

	EnterCriticalSection(&pMutex);

	long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight();
	void* videoPixels;
	inputFrame->GetBytes(&videoPixels);

	wglMakeCurrent( hGLDC, hGLRC );		// make OpenGL context current in this thread

	if (mFastTransferExtensionAvailable)
	{
		if (! mCaptureAllocator->transferFrame(videoPixels, mCaptureTexture))
			OutputDebugStringA("Capture: transferFrame() failed\n");
	}
	else
	{
		glEnable(GL_TEXTURE_2D);

		// Use a straightforward texture buffer
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mUnpinnedTextureBuffer);
		glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW);
		glBindTexture(GL_TEXTURE_2D, mCaptureTexture);

		// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mFrameWidth/2, mFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);

		glBindTexture(GL_TEXTURE_2D, 0);
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
		glDisable(GL_TEXTURE_2D);
	}

	wglMakeCurrent( NULL, NULL );

	LeaveCriticalSection(&pMutex);
}

// Draw the captured video frame texture onto a box, rendering to the off-screen frame buffer.
// Read the rendered scene back from the frame buffer and schedule it for playout.
void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
{
	EnterCriticalSection(&pMutex);

	// Get the first frame from the queue
	IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front();
	mDLOutputVideoFrameQueue.push_back(outputVideoFrame);
	mDLOutputVideoFrameQueue.pop_front();

	void*	pFrame;
	outputVideoFrame->GetBytes(&pFrame);

	long rowbytes = outputVideoFrame->GetRowBytes();
	long height = outputVideoFrame->GetHeight();
	long memSize = rowbytes * height;

	// make GL context current in this thread
	wglMakeCurrent( hGLDC, hGLRC );

	// Draw OpenGL scene to the off-screen frame buffer
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, mIdFrameBuf);

	// Setup view and projection
	GLfloat aspectRatio = (GLfloat)mFrameWidth / (GLfloat)mFrameHeight;
	glViewport (0, 0, mFrameWidth, mFrameHeight);
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective( 45.0f, aspectRatio, 0.1f, 100.0f );
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();

	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
	glScalef( aspectRatio, 1.0f, 1.0f );			// Scale x for correct aspect ratio
	glTranslatef( 0.0f, 0.0f, -4.0f );				// Move into screen
	glRotatef( mRotateAngle, 1.0f, 1.0f, 1.0f );	// Rotate model around a vector
	mRotateAngle -= mRotateAngleRate;				// update the rotation angle for next iteration
	glFinish();										// Ensure changes to GL state are complete

	// Draw a colourful frame around the front face of the box
	// (provides a pleasing nesting effect when you connect the playout output to the capture input)
	glBegin(GL_QUAD_STRIP);
	glColor3f( 1.0f, 0.0f, 0.0f );
	glVertex3f( 1.2f,  1.2f, 1.0f);
	glVertex3f( 1.0f,  1.0f, 1.0f);
	glColor3f( 0.0f, 0.0f, 1.0f );
	glVertex3f( 1.2f, -1.2f, 1.0f);
	glVertex3f( 1.0f, -1.0f, 1.0f);
	glColor3f( 0.0f, 1.0f, 0.0f );
	glVertex3f(-1.2f, -1.2f, 1.0f);
	glVertex3f(-1.0f, -1.0f, 1.0f);
	glColor3f( 1.0f, 1.0f, 0.0f );
	glVertex3f(-1.2f,  1.2f, 1.0f);
	glVertex3f(-1.0f,  1.0f, 1.0f);
	glColor3f( 1.0f, 0.0f, 0.0f );
	glVertex3f( 1.2f,  1.2f, 1.0f);
	glVertex3f( 1.0f,  1.0f, 1.0f);
	glEnd();

	if (mHasNoInputSource)
	{
		// Draw a big X when no input is available on capture
		glBegin( GL_QUADS );
		glColor3f( 1.0f, 0.0f, 1.0f );
		glVertex3f(  0.8f,  0.9f,  1.0f );
		glVertex3f(  0.9f,  0.8f,  1.0f );
		glColor3f( 1.0f, 1.0f, 0.0f );
		glVertex3f( -0.8f, -0.9f,  1.0f );
		glVertex3f( -0.9f, -0.8f,  1.0f );
		glColor3f( 1.0f, 0.0f, 1.0f );
		glVertex3f( -0.8f,  0.9f,  1.0f );
		glVertex3f( -0.9f,  0.8f,  1.0f );
		glColor3f( 1.0f, 1.0f, 0.0f );
		glVertex3f(  0.8f, -0.9f,  1.0f );
		glVertex3f(  0.9f, -0.8f,  1.0f );
		glEnd();
	}
	else
	{
		if (mFastTransferExtensionAvailable)
		{
			// Signal that we're about to draw using mCaptureTexture onto mFBOTexture
			mCaptureAllocator->beginTextureInUse();
		}

		// Pass texture unit 0 to the fragment shader as a uniform variable
		glEnable(GL_TEXTURE_2D);
		glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
		glUseProgram(mProgram);
		GLint locUYVYtex = glGetUniformLocation(mProgram, "UYVYtex");
		glUniform1i(locUYVYtex, 0);		// Bind texture unit 0

		// Draw front and back faces of box applying video texture to each face
		glBegin(GL_QUADS);
		glTexCoord2f(1.0f, 0.0f);	glVertex3f(  1.0f,  1.0f,  1.0f );		// Top right of front side
		glTexCoord2f(0.0f, 0.0f);	glVertex3f( -1.0f,  1.0f,  1.0f );		// Top left of front side
		glTexCoord2f(0.0f, 1.0f);	glVertex3f( -1.0f, -1.0f,  1.0f );		// Bottom left of front side
		glTexCoord2f(1.0f, 1.0f);	glVertex3f(  1.0f, -1.0f,  1.0f );		// Bottom right of front side

		glTexCoord2f(1.0f, 1.0f);	glVertex3f(  1.0f, -1.0f, -1.0f );		// Top right of back side
		glTexCoord2f(0.0f, 1.0f);	glVertex3f( -1.0f, -1.0f, -1.0f );		// Top left of back side
		glTexCoord2f(0.0f, 0.0f);	glVertex3f( -1.0f,  1.0f, -1.0f );		// Bottom left of back side
		glTexCoord2f(1.0f, 0.0f);	glVertex3f(  1.0f,  1.0f, -1.0f );		// Bottom right of back side
		glEnd();

		// Draw left and right sides of box with partially transparent video texture
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glBegin(GL_QUADS);
		glTexCoord2f(0.1f, 0.0f);	glVertex3f( -1.0f,  1.0f,  1.0f );		// Top right of left side
		glTexCoord2f(1.0f, 0.0f);	glVertex3f( -1.0f,  1.0f, -1.0f );		// Top left of left side
		glTexCoord2f(1.0f, 1.0f);	glVertex3f( -1.0f, -1.0f, -1.0f );		// Bottom left of left side
		glTexCoord2f(0.1f, 1.0f);	glVertex3f( -1.0f, -1.0f,  1.0f );		// Bottom right of left side

		glTexCoord2f(1.0f, 0.0f);	glVertex3f(  1.0f,  1.0f, -1.0f );		// Top right of right side
		glTexCoord2f(0.0f, 0.0f);	glVertex3f(  1.0f,  1.0f,  1.0f );		// Top left of right side
		glTexCoord2f(0.0f, 1.0f);	glVertex3f(  1.0f, -1.0f,  1.0f );		// Bottom left of right side
		glTexCoord2f(1.0f, 1.0f);	glVertex3f(  1.0f, -1.0f, -1.0f );		// Bottom right of right side
		glEnd();
		glDisable(GL_BLEND);

		glUseProgram(0);
		glDisable(GL_TEXTURE_2D);
		glBindTexture(GL_TEXTURE_2D, 0);
	}

	if (mFastTransferExtensionAvailable)
	{
		// Finished with mCaptureTexture
		mCaptureAllocator->endTextureInUse();

		if (! mPlayoutAllocator->transferFrame(pFrame, mFBOTexture))
			OutputDebugStringA("Playback: transferFrame() failed\n");

		paintGL();

		// Wait for transfer to system memory to complete
		mPlayoutAllocator->waitForTransferComplete(pFrame);
	}
	else
	{
		glReadPixels(0, 0, mFrameWidth, mFrameHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame);
		paintGL();
	}

	// If the last completed frame was late or dropped, bump the scheduled time further into the future
	if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped)
		mTotalPlayoutFrames += 2;

	// Schedule the next frame for playout
	HRESULT hr = mDLOutput->ScheduleVideoFrame(outputVideoFrame, (mTotalPlayoutFrames * mFrameDuration), mFrameDuration, mFrameTimescale);
	if (SUCCEEDED(hr))
		mTotalPlayoutFrames++;

	wglMakeCurrent( NULL, NULL );

	LeaveCriticalSection(&pMutex);
}

bool OpenGLComposite::Start()
{
	mTotalPlayoutFrames = 0;

	// Preroll frames
	for (unsigned i = 0; i < 5; i++)
	{
		// Take each video frame from the front of the queue and move it to the back
		IDeckLinkMutableVideoFrame* outputVideoFrame = mDLOutputVideoFrameQueue.front();
		mDLOutputVideoFrameQueue.push_back(outputVideoFrame);
		mDLOutputVideoFrameQueue.pop_front();

		// Start with a black frame for playout
		void*	pFrame;
		outputVideoFrame->GetBytes((void**)&pFrame);
		memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mFrameHeight);		// 0 is black in RGBA format

		if (mDLOutput->ScheduleVideoFrame(outputVideoFrame, (mTotalPlayoutFrames * mFrameDuration), mFrameDuration, mFrameTimescale) != S_OK)
			return false;

		mTotalPlayoutFrames++;
	}

	mDLInput->StartStreams();
	mDLOutput->StartScheduledPlayback(0, mFrameTimescale, 1.0);

	return true;
}

bool OpenGLComposite::Stop()
{
	mDLInput->StopStreams();
	mDLInput->DisableVideoInput();

	mDLOutput->StopScheduledPlayback(0, NULL, 0);
	mDLOutput->DisableVideoOutput();

	return true;
}

// Setup fragment shader to take YCbCr 4:2:2 video texture in UYVY macropixel format
// and perform colour space conversion to RGBA in the GPU.
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
{
	GLsizei		errorBufferSize;
	GLint		compileResult, linkResult;
	const char*	fragmentSource =
		"#version 130 \n"
		"uniform sampler2D UYVYtex; \n"		// UYVY macropixel texture passed as RGBA format
		"void main(void) \n"
		"{\n"
		"	float tx, ty, Y, Cb, Cr, r, g, b; \n"
		"	tx = gl_TexCoord[0].x; \n"
		"	ty = gl_TexCoord[0].y; \n"

			// The UYVY texture appears to the shader with 1/2 the true width since we used RGBA format to pass UYVY
		"	int true_width = textureSize(UYVYtex, 0).x * 2; \n"

			// For U0 Y0 V0 Y1 macropixel, lookup Y0 or Y1 based on whether
			// the original texture x coord is even or odd.
		"	if (fract(floor(tx * true_width + 0.5) / 2.0) > 0.0) \n"
		"		Y = texture2D(UYVYtex, vec2(tx,ty)).a; \n"		// odd so choose Y1
		"	else \n"
		"		Y = texture2D(UYVYtex, vec2(tx,ty)).g; \n"		// even so choose Y0
		"	Cb = texture2D(UYVYtex, vec2(tx,ty)).b; \n"
		"	Cr = texture2D(UYVYtex, vec2(tx,ty)).r; \n"

			// Y: Undo 1/256 texture value scaling and scale [16..235] to [0..1] range
			// C: Undo 1/256 texture value scaling and scale [16..240] to [-0.5 .. + 0.5] range
		"	Y = (Y * 256.0 - 16.0) / 219.0; \n"
		"	Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5; \n"
		"	Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5; \n"
			// Convert to RGB using Rec.709 conversion matrix (see eq 26.7 in Poynton 2003)
		"	r = Y + 1.5748 * Cr; \n"
		"	g = Y - 0.1873 * Cb - 0.4681 * Cr; \n"
		"	b = Y + 1.8556 * Cb; \n"

			// Set alpha to 0.7 for partial transparency when GL_BLEND is enabled
		"	gl_FragColor = vec4(r, g, b, 0.7); \n"
		"}\n";

	mFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

	glShaderSource(mFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
	glCompileShader(mFragmentShader);

	glGetShaderiv(mFragmentShader, GL_COMPILE_STATUS, &compileResult);
	if (compileResult == GL_FALSE)
	{
		glGetShaderInfoLog(mFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	mProgram = glCreateProgram();

	glAttachShader(mProgram, mFragmentShader);
	glLinkProgram(mProgram);

	glGetProgramiv(mProgram, GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		glGetProgramInfoLog(mProgram, errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	return true;
}

bool OpenGLComposite::CheckOpenGLExtensions()
{
	const GLubyte* strExt;
	bool hasFBO;

	// The GL_EXT_framebuffer_object extension is required but GL_AMD_pinned_memory is optional
	strExt = glGetString (GL_EXTENSIONS);
	hasFBO = strstr((char*)strExt, "GL_EXT_framebuffer_object") != NULL;

	mFastTransferExtensionAvailable = VideoFrameTransfer::checkFastMemoryTransferAvailable();

	if (!hasFBO)
	{
		MessageBox(NULL, _T("Required OpenGL extension \"GL_EXT_framebuffer_object\" is not available."), _T("OpenGL initialization error."), MB_OK);
		return false;
	}

	if (!mFastTransferExtensionAvailable)
		OutputDebugStringA("Fast memory transfer extension not available, using regular OpenGL transfer fallback instead\n");

	return true;
}

////////////////////////////////////////////
// PinnedMemoryAllocator
////////////////////////////////////////////

// PinnedMemoryAllocator implements the IDeckLinkMemoryAllocator interface and can be used instead of the
// built-in frame allocator, by setting with SetVideoInputFrameMemoryAllocator() or SetVideoOutputFrameMemoryAllocator().
//
// For this sample application a custom frame memory allocator is used to ensure each address
// of frame memory is aligned on a 4kB boundary required by the OpenGL pinned memory extension.
// If the pinned memory extension is not available, this allocator will still be used and
// demonstrates how to cache frame allocations for efficiency.
//
// The frame cache delays the releasing of buffers until the cache fills up, thereby avoiding an
// allocate plus pin operation for every frame, followed by an unpin and deallocate on every frame.

PinnedMemoryAllocator::PinnedMemoryAllocator(HDC hdc, HGLRC hglrc, VideoFrameTransfer::Direction direction, unsigned cacheSize) :
	mHGLDC(hdc),
	mHGLRC(hglrc),
	mRefCount(1),
	mDirection(direction),
	mFrameCacheSize(cacheSize)		// large cache size will keep more memory pinned and may result in out of memory errors
{
}

PinnedMemoryAllocator::~PinnedMemoryAllocator()
{
}

bool PinnedMemoryAllocator::transferFrame(void* address, GLuint gpuTexture)
{
	// Catch attempt to pin and transfer memory we didn't allocate
	if (mAllocatedSize.count(address) == 0)
		return false;

	if (mFrameTransfer.count(address) == 0)
	{
		// VideoFrameTransfer prepares and pins address
		mFrameTransfer[address] = new VideoFrameTransfer(mAllocatedSize[address], address, mDirection);
	}

	return mFrameTransfer[address]->performFrameTransfer();
}

void PinnedMemoryAllocator::waitForTransferComplete(void* address)
{
	if (mAllocatedSize.count(address) && mFrameTransfer.count(address))
		mFrameTransfer[address]->waitForTransferComplete();
}

void PinnedMemoryAllocator::beginTextureInUse()
{
	VideoFrameTransfer::beginTextureInUse(mDirection);
}

void PinnedMemoryAllocator::endTextureInUse()
{
	VideoFrameTransfer::endTextureInUse(mDirection);
}

void PinnedMemoryAllocator::unPinAddress(void* address)
{
	// un-pin address only if it has been pinned for transfer
	if (mFrameTransfer.count(address) > 0)
	{
		wglMakeCurrent( mHGLDC, mHGLRC );

		mFrameTransfer.erase(address);

		wglMakeCurrent( NULL, NULL );
	}
}

// IUnknown methods
HRESULT STDMETHODCALLTYPE	PinnedMemoryAllocator::QueryInterface(REFIID /*iid*/, LPVOID* /*ppv*/)
{
	return E_NOTIMPL;
}

ULONG STDMETHODCALLTYPE		PinnedMemoryAllocator::AddRef(void)
{
	return InterlockedIncrement(&mRefCount);
}

ULONG STDMETHODCALLTYPE		PinnedMemoryAllocator::Release(void)
{
	int newCount = InterlockedDecrement(&mRefCount);
	if (newCount == 0)
		delete this;
	return newCount;
}

// IDeckLinkMemoryAllocator methods
HRESULT STDMETHODCALLTYPE	PinnedMemoryAllocator::AllocateBuffer (unsigned long bufferSize, void* *allocatedBuffer)
{
	if (mFrameCache.empty())
	{
		// Allocate memory on a page boundary
		*allocatedBuffer = VirtualAlloc(NULL, bufferSize, MEM_COMMIT | MEM_RESERVE | MEM_WRITE_WATCH, PAGE_READWRITE);

		if (! *allocatedBuffer)
			return E_OUTOFMEMORY;

		mAllocatedSize[*allocatedBuffer] = bufferSize;
	}
	else
	{
		// Re-use most recently ReleaseBuffer'd address
		*allocatedBuffer = mFrameCache.back();
		mFrameCache.pop_back();
	}
	return S_OK;
}

HRESULT STDMETHODCALLTYPE	PinnedMemoryAllocator::ReleaseBuffer (void* buffer)
{
	if (mFrameCache.size() < mFrameCacheSize)
	{
		mFrameCache.push_back(buffer);
	}
	else
	{
		// No room left in cache, so un-pin (if it was pinned) and free this buffer
		unPinAddress(buffer);
		VirtualFree(buffer, 0, MEM_RELEASE);

		mAllocatedSize.erase(buffer);
	}
	return S_OK;
}

HRESULT STDMETHODCALLTYPE	PinnedMemoryAllocator::Commit ()
{
	return S_OK;
}

HRESULT STDMETHODCALLTYPE	PinnedMemoryAllocator::Decommit ()
{
	while (! mFrameCache.empty())
	{
		// Cleanup any frames allocated and pinned in AllocateBuffer() but not freed in ReleaseBuffer()
		unPinAddress( mFrameCache.back() );
		VirtualFree(mFrameCache.back(), 0, MEM_RELEASE);
		mFrameCache.pop_back();
	}
	return S_OK;
}

////////////////////////////////////////////
// DeckLink Capture Delegate Class
////////////////////////////////////////////
CaptureDelegate::CaptureDelegate(OpenGLComposite* pOwner) : m_pOwner(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;
	}

	bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;

	m_pOwner->VideoFrameArrived(inputFrame, hasNoInputSource);
	return S_OK;
}

HRESULT	CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags)
{
	return S_OK;
}

////////////////////////////////////////////
// DeckLink Playout Delegate Class
////////////////////////////////////////////
PlayoutDelegate::PlayoutDelegate (OpenGLComposite* pOwner) : m_pOwner(pOwner)
{
}

HRESULT	PlayoutDelegate::ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
{
	// Don't log bmdOutputFrameFlushed result since it is expected when Stop() is called
	if (result != bmdOutputFrameCompleted && result != bmdOutputFrameFlushed)
	{
		switch (result)
		{
			case bmdOutputFrameDisplayedLate:
				OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Displayed Late\n");
				break;
			case bmdOutputFrameDropped:
				OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Dropped\n");
				break;
			default:
				OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
		}
	}

	m_pOwner->PlayoutFrameCompleted(completedFrame, result);
	return S_OK;
}

HRESULT	PlayoutDelegate::ScheduledPlaybackHasStopped ()
{
	return S_OK;
}
