Skip to content

Instantly share code, notes, and snippets.

@darksylinc
Created August 21, 2023 02:56
Show Gist options
  • Save darksylinc/7e7edd98e467d3241cb52d492180f3e9 to your computer and use it in GitHub Desktop.
Save darksylinc/7e7edd98e467d3241cb52d492180f3e9 to your computer and use it in GitHub Desktop.
Triple Buffer vs Double buffer simulator
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <algorithm>
#include <deque>
// Change these parameters, recompile and run to see
// the results
// PARAMETER BEGIN
static const size_t kNumBuffers = 2u;
static const size_t kNumSwapchains = 3u;
static const size_t kVBlank = 16u;
static const size_t kCpuTime = 7u;
static const size_t kGpuTime = 17u;
static const size_t kCpuFrameVariance = 2u;
static const size_t kGpuFrameVariance = 2u;
// PARAMETER END
/// Returns value in range [0; bound]
static uint32_t bounded_rand( uint32_t bound )
{
uint32_t threshold = -bound % bound;
for( ;; )
{
uint32_t r = (uint32_t)rand();
if( r >= threshold )
return r % bound;
}
}
static size_t calculateFrameTime( size_t _timeToTake, const size_t variance )
{
int32_t timeToTake = int32_t( _timeToTake );
const int32_t randomVariance =
int32_t( bounded_rand( uint32_t( variance * 2u ) ) ) - int32_t( variance );
timeToTake += randomVariance;
timeToTake = std::max<int32_t>( timeToTake, 0 );
return (size_t)timeToTake;
}
struct SubmittedToGpuWork
{
size_t bufferIdx;
size_t cpuTimeStart;
size_t timeToTake;
size_t tickSinceLast;
size_t getCpuFinishedWork() const { return cpuTimeStart + timeToTake; }
};
struct SubmittedSwapchain
{
SubmittedToGpuWork cpuSubmission;
size_t gpuTimeStart;
size_t timeToTake;
size_t swapchainIdx;
size_t getFinishedWork() const { return gpuTimeStart + timeToTake; }
};
static SubmittedSwapchain lockedSwapchain;
static size_t cpuTicksBusy = 0u;
static bool bFence[kNumBuffers];
static std::deque<SubmittedToGpuWork> submittedToGpuWork;
static bool bGpuWorking = false;
static SubmittedSwapchain workInProgress;
static std::deque<SubmittedSwapchain> finishedWork;
static std::deque<size_t> availableSwapchains;
int main()
{
srand( 101 ); // Deterministic output
const size_t numTicks = 1000u;
size_t currFrameIdx = 0u;
lockedSwapchain.cpuSubmission.cpuTimeStart = 0;
lockedSwapchain.gpuTimeStart = 0;
lockedSwapchain.swapchainIdx = kNumSwapchains - 1u;
for( size_t i = 0u; i < kNumBuffers; ++i )
bFence[i] = true;
for( size_t i = 0u; i < kNumSwapchains - 1u; ++i )
availableSwapchains.push_back( i );
size_t tickStart = 0u;
size_t worstLag = 0u;
size_t totalLag = 0u;
size_t hitVBlanks = 0u;
size_t missedVBlanks = 0u;
double totalFps = 0;
for( size_t i = 0u; i < numTicks; ++i )
{
if( i != 0u && ( i % kVBlank ) == 0u )
{
// Time to present
bool bBlankHit = false;
if( !finishedWork.empty() )
{
const SubmittedSwapchain &work = finishedWork.front();
if( i >= work.getFinishedWork() )
{
availableSwapchains.push_back( lockedSwapchain.swapchainIdx );
assert( availableSwapchains.size() <= kNumSwapchains - 1u );
lockedSwapchain = work;
finishedWork.pop_front();
bBlankHit = true;
const size_t lag = i - lockedSwapchain.cpuSubmission.cpuTimeStart;
printf(
"FRAME PRESENTED! t = %i; timeStart = %i; worst_case_lag = %i; mspf = %i; "
" fps = %.2f\n",
(int)i, (int)lockedSwapchain.cpuSubmission.cpuTimeStart, (int)lag,
(int)lockedSwapchain.cpuSubmission.tickSinceLast,
1000.0f / (float)lockedSwapchain.cpuSubmission.tickSinceLast );
worstLag = std::max( worstLag, lag );
totalLag += lag;
if( hitVBlanks >= 3u )
totalFps += 1000.0 / (double)lockedSwapchain.cpuSubmission.tickSinceLast;
++hitVBlanks;
}
}
if( !bBlankHit )
{
printf( "VBLANK MISSED! t = %i\n", (int)i );
++missedVBlanks;
}
}
if( bFence[currFrameIdx] && cpuTicksBusy == 0u )
{
// Start CPU work
SubmittedToGpuWork work;
work.bufferIdx = currFrameIdx;
work.cpuTimeStart = i;
work.timeToTake = calculateFrameTime( kCpuTime, kCpuFrameVariance );
work.tickSinceLast = std::max<size_t>( i - tickStart, 1u );
tickStart = i;
cpuTicksBusy = kCpuTime;
submittedToGpuWork.push_back( work );
bFence[currFrameIdx] = false;
assert( submittedToGpuWork.size() <= kNumBuffers );
currFrameIdx = ( currFrameIdx + 1u ) % kNumBuffers;
}
if( cpuTicksBusy > 0u )
--cpuTicksBusy;
if( !submittedToGpuWork.empty() && !availableSwapchains.empty() && !bGpuWorking )
{
// We can only do one GPU job per tick
const SubmittedToGpuWork &work = submittedToGpuWork.front();
if( i >= work.getCpuFinishedWork() )
{
// GPU work started.
SubmittedSwapchain gpuWork;
gpuWork.swapchainIdx = availableSwapchains.front();
gpuWork.cpuSubmission = work;
gpuWork.timeToTake = calculateFrameTime( kGpuTime, kGpuFrameVariance );
gpuWork.gpuTimeStart = i;
workInProgress = gpuWork;
bGpuWorking = true;
availableSwapchains.pop_front();
submittedToGpuWork.pop_front();
}
}
if( ( i + 1u ) >= workInProgress.getFinishedWork() && bGpuWorking )
{
// Signal CPU it can start using this bufferIdx.
bFence[workInProgress.cpuSubmission.bufferIdx] = true;
finishedWork.push_back( workInProgress );
bGpuWorking = false;
}
}
printf( "\nSummary:\n" );
printf( "Total VBLANKs hits = %i; missed = %i\n", (int)hitVBlanks, (int)missedVBlanks );
printf( "Avg FPS = %.02f\n", totalFps / double( hitVBlanks - 3u ) );
printf( "Avg Lag = %.02f; Worst Lag = %i\n", (double)totalLag / double( hitVBlanks ),
(int)worstLag );
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment