/* 
 * 
 * Confidential Information of Telekinesys Research Limited (t/a Havok).  Not for disclosure or distribution without Havok's
 * prior written consent.This software contains code, techniques and know-how which is confidential and proprietary to Havok.
 * Level 2 and Level 3 source code contains trade secrets of Havok. Havok Software (C) Copyright 1999-2008 Telekinesys Research Limited t/a Havok. All Rights Reserved. Use of this software is subject to the terms of an end user license agreement.
 * 
 */

#include <Demos/demos.h>
#include <Demos/Animation/Api/Multithreading/SampleAndBlend/SampleAndBlendMultithreadingDemo.h>

#include <Common/Base/Reflection/hkClass.h>
#include <Common/Base/Container/LocalArray/hkLocalArray.h>

#include <Common/Base/Monitor/Spu/hkSpuMonitorCache.h>

// Serialization
#include <Common/Serialize/Util/hkLoader.h>
#include <Common/Serialize/Util/hkRootLevelContainer.h>
#include <Common/Base/Reflection/Registry/hkTypeInfoRegistry.h>

// Asset mgt
#include <Demos/DemoCommon/Utilities/Asset/hkAssetManagementUtil.h>

// Scene data
#include <Common/SceneData/Scene/hkxScene.h>
#include <Animation/Animation/Rig/hkaPose.h>

// Skeletal Animation
#include <Animation/Animation/Rig/hkaSkeleton.h>
#include <Animation/Animation/hkaAnimationContainer.h>
#include <Animation/Animation/Animation/WaveletCompressed/hkaWaveletSkeletalAnimation.h>
#include <Animation/Animation/Animation/DeltaCompressed/hkaDeltaCompressedSkeletalAnimation.h>
#include <Animation/Animation/Animation/Mirrored/hkaMirroredSkeletalAnimation.h>
#include <Animation/Animation/Animation/Mirrored/hkaMirroredSkeleton.h>
#include <Animation/Animation/Animation/SplineCompressed/hkaSplineSkeletalAnimation.h>

#include <Animation/Animation/Animation/Interleaved/hkaInterleavedSkeletalAnimation.h>
#include <Animation/Animation/Playback/Multithreaded/hkaMultithreadedAnimationUtils.h>
#include <Animation/Animation/Animation/hkaAnimationBinding.h>
#include <Animation/Animation/Playback/Control/Default/hkaDefaultAnimationControl.h>
#include <Animation/Animation/Playback/hkaAnimatedSkeleton.h>

// For debug drawing
#include <Demos/DemoCommon/Utilities/Animation/AnimationUtils.h>

// SPU for PS3

#if defined(HK_PLATFORM_HAS_SPU)
#include <Common/Base/Spu/Util/hkSpuUtil.h>
#include <Common/Base/Thread/JobQueue/hkJobQueue.h>
#endif // HK_PLATFORM_HAS_SPU

#include <Animation/Animation/Playback/Multithreaded/hkaAnimationJobs.h>
#include <Animation/Animation/Playback/Multithreaded/hkaAnimationJobQueueUtils.h>

#define NUM_SKELETONS 50

#if defined HK_PLATFORM_PS3
#define	SPURS_THREAD_GROUP_PRIORITY 250
#define SPURS_HANDLER_THREAD_PRIORITY 1
#define NUM_SPUS env->m_options->m_numSpus
#else
#define NUM_SPUS 1
#endif

#define NUM_ANIMATION_TYPES 5	// Change this if you add another type of animation (as two variants, CPU and SPU)

SampleAndBlendMultithreadingDemo::SampleAndBlendMultithreadingDemo( hkDemoEnvironment* env )
:	hkDefaultPhysicsDemo(env),
#if defined(HK_PLATFORM_HAS_SPU)
	m_jobQueue(&m_jobQueueCpuWaitSemaphore, &m_jobQueueTaskCompletionSemaphore, &m_jobQueueSpuWaitSemaphore),
	m_jobQueueCpuWaitSemaphore(0,1000), m_jobQueueTaskCompletionSemaphore(0, 1000), m_jobQueueSpuWaitSemaphore(0,1000),
#endif // HK_PLATFORM_HAS_SPU
	m_time(0.0f),
	m_convertLocalToModelOnSPU(true),
	m_semaphore(0,1000)
{
	hkpWorld::IgnoreForceMultithreadedSimulation ignoreForceMultithreaded;

	//
	// Setup the camera
	//
	{
		hkVector4 from(  -1.0f, -7.0f,  1.0f);
		hkVector4 to  (  0.0f,  0.0f,   0.0f);
		hkVector4 up  (  0.0f,  0.0f,   1.0f);
		setupDefaultCameras( env, from, to, up );
	}

	m_spuUtil = HK_NULL;

#if defined(HK_PLATFORM_HAS_SPU)
	//Init the job queue
	m_jobQueue.m_jobPopFunc    = hkaAnimationJobQueueUtils::popAnimationJob;
	m_jobQueue.m_finishJobFunc = hkaAnimationJobQueueUtils::finishAnimationJob;

	hkBool useSpu = (m_variantId < NUM_ANIMATION_TYPES);

	if(useSpu)
	{
		// If no SPUs force min SPUs (for single-threaded stats)
		m_numSpus = NUM_SPUS;
		if( m_numSpus == 0 )
		{
			m_numSpus = 1;
		}

		hkArray<hkSpuTaskParams> params;
		params.setSize(m_numSpus);
		for ( int i = 0; i < m_numSpus; ++i )
		{
			// Note that each spu thread uses 4 parameters. Here we set only 2 parameters, the job queue and an optional monitor cache.
			params[i].m_param1 = &m_jobQueue;

			// This parameter is only needed if you wish to collect timers from the SPU
			params[i].m_param2 = m_monitorCaches[i];
		}

		// Init the spu utility
		m_spuUtil = new hkSpuUtil();

#	if defined(HK_PLATFORM_PS3)
		extern char _binary_hkaSpu_elf_start[];
		void* elf = _binary_hkaSpu_elf_start;

		CellSpurs* spurs = (CellSpurs*)hkAlignedAllocate<char> ( CELL_SPURS_ALIGN, CELL_SPURS_SIZE , HK_MEMORY_CLASS_DEMO );
		int ret = cellSpursInitialize (spurs, m_numSpus, SPURS_THREAD_GROUP_PRIORITY, SPURS_HANDLER_THREAD_PRIORITY, false);
		if (ret)
		{
			HK_ERROR(0x73e432a2, "cellSpursInitialize failed :" << ret);
		}
#	else  // HK_PLATFORM_PS3
		HK_CELL_SPURS* spurs = HK_NULL;
		void* elf = (void*)(hkUlong)HK_TASK_ANIMATION;
#	endif // HK_PLATFORM_PS3

		hkSpuTaskSetParams unusedTaskSetParams;
		m_spuUtil->initSpursTaskset( m_numSpus, unusedTaskSetParams, spurs );
		m_spuUtil->setDefaultSpursTaskAndParams(params, elf );
		m_spuUtil->setNumActiveSpursTasks(m_numSpus);
	}
#endif // HK_PLATFORM_HAS_SPU

	m_loader = new hkLoader();

	// Get the rig
	{
		hkString assetFile = hkAssetManagementUtil::getFilePath("Resources/Animation/HavokGirl/hkRig.hkx");
		hkRootLevelContainer* container = m_loader->load( assetFile.cString() );
		HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset");
		hkaAnimationContainer* ac = reinterpret_cast<hkaAnimationContainer*>( container->findObjectByType( hkaAnimationContainerClass.getName() ));

		HK_ASSERT2(0x27343435, ac && (ac->m_numSkeletons > 0), "No skeleton loaded");
		m_skeleton = ac->m_skeletons[0];
	}

	// Get the animation and the binding
	{
		hkString assetFile = hkAssetManagementUtil::getFilePath("Resources/Animation/HavokGirl/hkRunLoop.hkx");
		hkRootLevelContainer* container = m_loader->load( assetFile.cString() );
		HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset");
		hkaAnimationContainer* ac = reinterpret_cast<hkaAnimationContainer*>( container->findObjectByType( hkaAnimationContainerClass.getName() ));

		HK_ASSERT2(0x27343435, ac && (ac->m_numAnimations > 0), "No animation loaded");
		m_animations[0] = ac->m_animations[0];

		HK_ASSERT2(0x27343435, ac && (ac->m_numBindings > 0), "No binding loaded");
		m_bindings[0] = ac->m_bindings[0];
	}

	// Get another animation and binding
	{
		hkString assetFile = hkAssetManagementUtil::getFilePath("Resources/Animation/HavokGirl/hkRunTurnLLoop.hkx");
		hkRootLevelContainer* container = m_loader->load( assetFile.cString() );
		HK_ASSERT2(0x27343437, container != HK_NULL , "Could not load asset");
		hkaAnimationContainer* ac = reinterpret_cast<hkaAnimationContainer*>( container->findObjectByType( hkaAnimationContainerClass.getName() ));

		HK_ASSERT2(0x27343435, ac && (ac->m_numAnimations > 0), "No animation loaded");
		m_animations[1] = ac->m_animations[0];

		HK_ASSERT2(0x27343435, ac && (ac->m_numBindings > 0), "No binding loaded");
		m_bindings[1] = ac->m_bindings[0];
	}


	{
		switch(m_variantId % NUM_ANIMATION_TYPES)
		{
			case 0: // Uncompressed
			{
				// No need to do anything
				break;
			}
			case 1: // Delta
			{
				hkaDeltaCompressedSkeletalAnimation::CompressionParams p;
				p.m_quantizationBits = 16;
				p.m_blockSize = 8;

				for (int i=0; i<2; i++)
				{
					hkaInterleavedSkeletalAnimation* uncompressedAnimation = static_cast<hkaInterleavedSkeletalAnimation*>(m_animations[i]);
					m_animations[i] = new hkaDeltaCompressedSkeletalAnimation(*uncompressedAnimation, p);
					m_bindings[i]->m_animation = m_animations[i];
				}
				break;
			}
			case 2: // Wavelet
			{
				hkaWaveletSkeletalAnimation::CompressionParams p;
				p.m_quantizationBits = 16;
				p.m_blockSize = 8;
				p.m_truncProp = .25;

				for (int i=0; i<2; i++)
				{
					hkaInterleavedSkeletalAnimation* uncompressedAnimation = static_cast<hkaInterleavedSkeletalAnimation*>(m_animations[i]);
					m_animations[i] = new hkaWaveletSkeletalAnimation(*uncompressedAnimation, p);
					m_bindings[i]->m_animation = m_animations[i];
				}
				break;
			}
			case 3: // Mirrored
			{
				hkaWaveletSkeletalAnimation::CompressionParams p;
				p.m_quantizationBits = 16;
				p.m_blockSize = 8;
				p.m_truncProp = .25;				
				
				for (int i=0; i<2; i++)
				{
					hkaInterleavedSkeletalAnimation* uncompressedAnimation = static_cast<hkaInterleavedSkeletalAnimation*>(m_animations[i]);
					hkaWaveletSkeletalAnimation* w = new hkaWaveletSkeletalAnimation(*uncompressedAnimation, p);
					makeMirroredAnimation( w, i );
					w->removeReference();
				}
				
				break;
			}
			case 4: // Spline
			{
				hkaSplineSkeletalAnimation::TrackCompressionParams p;
				hkaSplineSkeletalAnimation::AnimationCompressionParams a;

				for (int i=0; i<2; i++)
				{
					hkaInterleavedSkeletalAnimation* uncompressedAnimation = static_cast<hkaInterleavedSkeletalAnimation*>(m_animations[i]);
					m_animations[i] = new hkaSplineSkeletalAnimation( *uncompressedAnimation, p, a );
					m_bindings[i]->m_animation = m_animations[i];
				}

				break;
			}
		}
	}

	// Create an anim
	for ( int i = 0; i < NUM_SKELETONS; ++i )
	{
		// Create a new animated skeleton
		hkaAnimatedSkeleton* skeleton = new hkaAnimatedSkeleton( m_skeleton );
		m_activeSkeletons.pushBack( skeleton );

		for (int j=0; j<2; j++)
		{
			// Create an animation control
			hkaDefaultAnimationControl* ac = new hkaDefaultAnimationControl(m_bindings[j]);
			ac->setLocalTime( i * .2f );

			// Bind the control to the skeleton
			skeleton->addAnimationControl( ac );

			m_controls[j] = ac;

			// The animated skeleton now owns the control
			ac->removeReference();
		}
	}

	setupGraphics( );

	// Allocate output buffers
	m_poses.setSize( NUM_SKELETONS );
	for (int p=0; p < NUM_SKELETONS; p++)
	{
		m_poses[p] = new hkaPose(m_activeSkeletons[p]->getSkeleton());
		m_poses[p]->setToReferencePose();
	}
}

void SampleAndBlendMultithreadingDemo::makeMirroredAnimation(hkaSkeletalAnimation* original, int index )
{
	
	hkObjectArray< hkString > ltag;
	hkObjectArray< hkString > rtag;

	ltag.pushBack( " L " );
	ltag.pushBack( "EyeL" );

	rtag.pushBack( " R " );
	rtag.pushBack( "EyeR" );

	hkaMirroredSkeleton *mirroredSkeleton = new hkaMirroredSkeleton( m_skeleton );

	mirroredSkeleton->computeBonePairingFromNames( ltag, rtag );

	hkaMirroredSkeletalAnimation *mirroredAnimation = new hkaMirroredSkeletalAnimation( original, m_bindings[index], mirroredSkeleton );
	m_bindings[index] = mirroredAnimation->createMirroredBinding();
	m_animations[index] = mirroredAnimation;

	mirroredSkeleton->removeReference();

	hkVector4 v_mir;

	hkQsTransform Q_root( hkQsTransform::IDENTITY );
	hkQsTransform Q_child( hkQsTransform::IDENTITY );
	hkQsTransform Q_nubs( hkQsTransform::IDENTITY );

	v_mir.set( 0, 0, 1 );

	const hkReal s2o2 = hkMath::sqrt( 2.0f ) / 2.0f;

	Q_root.m_rotation.set( 0, 0, s2o2, -s2o2 );
	Q_child.m_rotation.set( 0.5f, 0.5f, 0.5f, -0.5f );
	Q_nubs.m_rotation.set( -s2o2, 0, -s2o2, 0 );

	mirroredSkeleton->setMirrorAxis( v_mir );
	mirroredSkeleton->setRootOrientation( Q_root );
	mirroredSkeleton->setChildOrientation( Q_child );
	mirroredSkeleton->setNubOrientation( Q_nubs );
	mirroredSkeleton->setMixedMode( false );
}

SampleAndBlendMultithreadingDemo::~SampleAndBlendMultithreadingDemo()
{
	delete m_loader;

	// If we created a mirrored animation, clean up binding
	if ((m_variantId % NUM_ANIMATION_TYPES) == 3)
	{
		hkaMirroredSkeletalAnimation::destroyMirroredBinding( m_bindings[0] );
		hkaMirroredSkeletalAnimation::destroyMirroredBinding( m_bindings[1] );
	}

	// If we created compressed animations, clean them up.
	if ((m_variantId % NUM_ANIMATION_TYPES) != 0)
	{
		m_animations[0]->removeReference();
		m_animations[1]->removeReference();
	}

	// Delete the active skeletons
	for (int s=0; s< m_activeSkeletons.getSize(); s++)
	{
		m_activeSkeletons[s]->removeReference();
		delete m_poses[s];
	}

	if ( m_variantId >= NUM_ANIMATION_TYPES)	// If we're on CPU, clean up jobqueue
	{
#if defined(HK_SIMULATE_SPU_DMA_ON_CPU)	
		delete m_jobQueue.m_data;
#endif
	}
}

hkDemo::Result SampleAndBlendMultithreadingDemo::stepDemo()
{
	hkBool useSpu = (m_variantId < NUM_ANIMATION_TYPES);

	if ( useSpu && m_env->m_gamePad->wasButtonPressed(HKG_PAD_BUTTON_3) )
	{
		m_convertLocalToModelOnSPU = !m_convertLocalToModelOnSPU;
	}

	HK_TIMER_BEGIN("StepAnimationsAndSampleSetup", HK_NULL);

	const int numSkeletons = m_activeSkeletons.getSize();

	// Step the skeletons - this happens in single threaded mode
	{
		hkReal firstWeight = .5f*(1.0f + hkMath::sin(m_time));
		for (int i = 0; i < numSkeletons; ++i )
		{
			hkaAnimatedSkeleton* inst = m_activeSkeletons[i];
			m_controls[0]->setMasterWeight(firstWeight);
			m_controls[1]->setMasterWeight(1.0f-firstWeight);

			// Advance the animation
			inst->stepDeltaTime( m_timestep );
		}
	}

	HK_TIMER_END();


	// Do the sampling and blending
	hkStopwatch timer;
	timer.start();

#if defined(HK_PLATFORM_HAS_SPU)
	// Set up the jobs
	hkObjectArray<hkaAnimationSampleAndCombineJob> sampleAndCombineJobs;
	sampleAndCombineJobs.setSize(numSkeletons);

	if(useSpu)
	{
		HK_TIMER_BEGIN("StepAnimationsAndSampleSetup", HK_NULL);
		// Use the job queue to run animation in multithreaded mode
		for (int i = 0; i < numSkeletons; ++i )
		{
			// We gather data for each active control and place it in a buffer to be used by the SPU
			// The buffer must persist in memory as long as the job is active.

			// Allocate space for the buffer (typically 50 bytes per active control)
			hkaMultithreadedAnimationUtils::allocateSampleAndCombineJob( m_activeSkeletons[i], sampleAndCombineJobs[i] );

			{
				// Get output - local or model
				hkQsTransform* outputTransforms = HK_NULL; 
				hkInt16* parentIndices = HK_NULL;	// Used as flag for job to do local-to-model conversion
				if (m_convertLocalToModelOnSPU)
				{
					outputTransforms = m_poses[i]->writeAccessPoseModelSpace().begin();
					parentIndices = m_skeleton->m_parentIndices;
				}
				else
				{
					outputTransforms = m_poses[i]->writeAccessPoseLocalSpace().begin();
					parentIndices = HK_NULL;
				}

				// Populate the buffer with data from the controls
				hkaMultithreadedAnimationUtils::createSampleAndCombineJob( m_activeSkeletons[i], 
					m_activeSkeletons[i]->getSkeleton()->m_numBones, outputTransforms, 
					m_activeSkeletons[i]->getSkeleton()->m_numFloatSlots, m_poses[i]->writeAccessFloatSlotValues().begin(),
					parentIndices,
					sampleAndCombineJobs[i] );

			}

			// Here we can (optionally) try and minimise the buffer size to avoid stack overflow for large animations.
			sampleAndCombineJobs[i].m_bufferSize = hkaMultithreadedAnimationUtils::getMaxSizeRequiredForSampleAndCombineJobBuffer(m_activeSkeletons[i]);

			// This semaphore is released on the SPU then the job is complete
			sampleAndCombineJobs[i].m_jobDone = &m_semaphore;

			// Add the job - execution begins immediately on the SPU. 
			// Since however this uses a critical section, and the job set up code above is realtively fast, 
			// we defer adding until all jobs are set up, and then use an addJobBatch - this willbe faster.
			// HK_TIMER_BEGIN("AddJob", HK_NULL);
			// m_jobQueue.addJob( *reinterpret_cast<hkJobQueue::JobQueueEntry*>(&sampleAndCombineJobs[i]), hkJobQueue::JOB_HIGH_PRIORITY, hkJobQueue::JOB_TYPE_HINT_SPU );
			// HK_TIMER_END();
		}

		// Add jobs as a batch
		{
			HK_TIMER_BEGIN("AddJobBatch", HK_NULL);
			hkArray<hkJobQueue::JobQueueEntry*> jobQueueEntries;
			jobQueueEntries.setSize(numSkeletons);
			for (int i = 0; i < numSkeletons; ++i )
			{
				jobQueueEntries[i] = reinterpret_cast<hkJobQueue::JobQueueEntry*>(&sampleAndCombineJobs[i]);
			}

			// Add the job - execution begins immediately on the SPU
			m_jobQueue.addJobBatch( jobQueueEntries, hkJobQueue::JOB_HIGH_PRIORITY, hkJobQueue::JOB_TYPE_HINT_SPU );
			HK_TIMER_END();
		}

		HK_TIMER_END();

		m_spuUtil->startDefaultSpursTasks();
	}
#endif  // HK_PLATFORM_HAS_SPU

	if ( !useSpu )
	{
		// Run animation on the PPU is single threaded mode
		for (int i = 0; i < numSkeletons; ++i )
		{
			m_activeSkeletons[i]->sampleAndCombineAnimations(m_poses[i]->writeAccessPoseLocalSpace().begin(), m_poses[i]->writeAccessFloatSlotValues().begin() );
		}
	}


#if defined(HK_PLATFORM_HAS_SPU)
	// Wait for all threads to finish and clean up job buffer
	if(useSpu)
	{
		// Wait for all jobs to complete
		for (int i = 0; i < numSkeletons; ++i )
		{
			m_semaphore.acquire();
		}

		// Wait for the actual tasks to finish. This makes sure all timer information will have finished dma-ing to main memory
		for (int i = 0; i < m_spuUtil->getNumActiveSpursTasks(); ++i)
		{
			m_jobQueue.m_taskCompletionSemaphore->acquire();
		}

		// Clean up the control buffers
		for (int i = 0; i < numSkeletons; ++i )
		{
			hkaMultithreadedAnimationUtils::deallocateSampleAndCombineJob( sampleAndCombineJobs[i] );
		}
	}
#endif  // defined(HK_PLATFORM_HAS_SPU)


	timer.stop();
	hkReal sampleTime = ( timer.getSplitSeconds() * 1000000 );

	// Display only if not being run with NULL renderer (e.g. bootstrapping for stats generation)
	if( hkString::strNcmp( m_env->m_options->m_renderer, "n", 1) != 0)
	{
		if (!m_convertLocalToModelOnSPU)		// To compare timings, make sure that we explcitly do conversion if not already done on SPU
		{
			HK_TIMER_BEGIN("syncModelSpace", HK_NULL);
			for (int i = 0; i < numSkeletons; ++i )
			{
				m_poses[i]->syncModelSpace();
			}
			HK_TIMER_END();
		}

		hkQsTransform worldFromModel (hkQsTransform::IDENTITY);
		for (int i = 0; i < numSkeletons; ++i )
		{
			worldFromModel.m_translation.set( hkReal(i - 1 - (NUM_SKELETONS>>2)), 0, 0);
			AnimationUtils::drawPose( *m_poses[i], worldFromModel );
		}
	
		const int h = m_env->m_window->getHeight();
		if ( useSpu )
		{
			m_env->m_textDisplay->outputText( (m_convertLocalToModelOnSPU?"LocalToModel On SPU (Hit \223 to change)":"LocalToModel On CPU  (Hit \223 to change)"), 10, h - 140 );
		}

		char buf[255];
		hkString::sprintf(buf, "Total sample time for %d characters (uSecs): %d", NUM_SKELETONS, (int)sampleTime );
		m_env->m_textDisplay->outputText( buf, 100, h - 40 );
	}

	m_time += m_timestep;

	return DEMO_OK;
}



void SampleAndBlendMultithreadingDemo::setLoadedElfsForTimerProcessing( hkUlong elfs[5] )
{
#if defined(HK_PLATFORM_PS3)
	extern char _binary_hkaSpu_elf_start[];
	elfs[HK_ELF_TYPE_ANIMATION] = (hkUlong)_binary_hkaSpu_elf_start;
#endif
}


#if defined(HK_COMPILER_MWERKS)
#	pragma force_active on
#	pragma fullpath_file on
#endif

static const char helpString[] = \
"Shows animation decompression and blending on the SPU" ;


#ifdef HK_PLATFORM_HAS_SPU
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "SPU - Uncompressed,     50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 0,       "Sampling on SPU",              "Sampling on SPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "SPU - Delta,            50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 1,   "Delta decompression on SPU",   "Delta decompression on SPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "SPU - Wavelet,          50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 2, "Wavelet decompression on SPU", "Wavelet on SPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "SPU - Mirrored Wavelet, 50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 3, "Mirrored on SPU", "Mirrored on SPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "SPU - Spline,           50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 4, "Spline compression on SPU", "Spline compression on SPU");
#endif // HK_PLATFORM_HAS_SPU
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "CPU - Uncompressed,     50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 5,       "Sampling on CPU",              "Sampling on CPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "CPU - Delta,            50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 6,   "Delta decompression on CPU",   "Delta decompression on CPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "CPU - Wavelet,          50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 7, "Wavelet decompression on CPU", "Wavelet decompression on CPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "CPU - Mirrored Wavelet, 50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 8, "Mirrored on CPU", "Mirrored on CPU");
HK_DECLARE_DEMO_VARIANT(SampleAndBlendMultithreadingDemo, ANIMATION_SERIALIZE, "CPU - Spline,           50 Skeletons of 50 Bones, 2 Blended Animations, No Cache", 9, "Spline compression on CPU", "Spline compression on CPU");

/*
* Havok SDK - NO SOURCE PC DOWNLOAD, BUILD(#20080529)
*
* Confidential Information of Havok.  (C) Copyright 1999-2008 
* Telekinesys Research Limited t/a Havok. All Rights Reserved. The Havok
* Logo, and the Havok buzzsaw logo are trademarks of Havok.  Title, ownership
* rights, and intellectual property rights in the Havok software remain in
* Havok and/or its suppliers.
*
* Use of this software for evaluation purposes is subject to and indicates 
* acceptance of the End User licence Agreement for this product. A copy of 
* the license is included with this software and is also available at
* www.havok.com/tryhavok
*
*/
