Skip to content

Instantly share code, notes, and snippets.

@paulhoux
Last active February 26, 2019 22:42
Show Gist options
  • Save paulhoux/2e4502c3540c4c5c4cb5718d74023516 to your computer and use it in GitHub Desktop.
Save paulhoux/2e4502c3540c4c5c4cb5718d74023516 to your computer and use it in GitHub Desktop.
Example of a background loading thread with a progress bar. Meant to be used with Cinder: https://libcinder.org
#include "cinder/Utilities.h"
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace app;
using namespace std;
class ProgressBarApp : public App {
public:
void setup() override;
void cleanup() override;
void draw() override;
void drawTextures();
void drawProgress();
void drawSpinner();
float getProgress() const;
private:
void threadFn( gl::ContextRef ctx );
std::unique_ptr<std::thread> mThread; // Our background loading thread.
size_t mCount{ 0 }; // The number of files to load.
std::vector<fs::path> mQueue; // The list of files to load.
mutable std::mutex mQueueMutex; // Controls access to the queue.
std::vector<gl::Texture2dRef> mTextures; // The list of loaded textures.
mutable std::mutex mTexturesMutex; // Controls access to the loaded textures.
std::atomic_bool mTerminated{ true }; // Will make sure our background thread is properly terminated.
};
void ProgressBarApp::setup()
{
// Look in the assets folder and find all PNG files in there.
const auto assets = getAssetPath( "" );
for( const auto &entry : fs::directory_iterator( assets ) ) {
const auto &file = entry.path();
if( is_regular_file( file ) && file.extension() == ".png" )
mQueue.push_back( file );
}
// Count the files.
mCount = mQueue.size();
// Now create a shared OpenGL context, that we use to load and create textures on a background thread.
gl::ContextRef backgroundCtx = gl::Context::create( gl::context() );
// Create our background thread, which will load the images in the queue.
mTerminated = false;
mThread = std::make_unique<std::thread>( std::bind( &ProgressBarApp::threadFn, this, backgroundCtx ) );
}
void ProgressBarApp::cleanup()
{
// Terminate our background thread.
mTerminated = true;
// Now wait until it is actually terminated.
if( mThread && mThread->joinable() )
mThread->join();
// Done.
mThread.reset();
}
void ProgressBarApp::draw()
{
// Clear the window.
gl::clear();
gl::color( 1, 1, 1 );
// Draw the loaded textures.
drawTextures();
if( getProgress() < 1.0f ) {
// Draw the progress bar.
drawProgress();
// Draw an animated spinner.
drawSpinner();
}
}
void ProgressBarApp::drawTextures()
{
// Make sure we're the only ones accessing the list of textures.
// The background thread might access the list at the same time and that would be bad.
std::lock_guard<std::mutex> lock( mTexturesMutex );
const int cols = getWindowWidth() / 100;
for( int i = 0; i < int( mTextures.size() ); ++i ) {
const int x = ( i % cols ) * 100;
const int y = ( i / cols ) * 100;
const auto &texture = mTextures[i];
const auto area = Area::proportionalFit( texture->getBounds(), { x, y, x + 100, y + 100 }, true, true );
gl::draw( texture, area );
}
// Note: 'lock' will go out of scope and release the lock for us.
}
void ProgressBarApp::drawProgress()
{
gl::ScopedModelMatrix scpModel;
const auto size = vec2( 256, 16 );
gl::translate( getWindowCenter() );
gl::translate( -0.5f * size );
gl::drawSolidRect( { 0, 0, getProgress() * size.x, size.y } );
gl::drawStrokedRect( { 0, 0, size.x, size.y } );
}
void ProgressBarApp::drawSpinner()
{
gl::ScopedModelMatrix scpModel;
const float angle = float( getElapsedSeconds() );
const float step = glm::radians( 30.0f );
const float radius = 25.0f;
gl::translate( getWindowCenter() + vec2( 0, 100 ) );
for( int i = 0; i < 12; ++i ) {
gl::drawSolidCircle( radius * vec2( glm::sin( i * step - angle ), glm::cos( i * step - angle ) ), 3.0f );
}
}
float ProgressBarApp::getProgress() const
{
// First, we need to make sure we're the only ones accessing the queue.
// The background thread might access the queue at the same time and that would be bad.
std::lock_guard<std::mutex> lock( mQueueMutex );
// Now calculate and return the progress. It's a value between 0 and 1.
if( mCount > 0 )
return float( mCount - mQueue.size() ) / mCount;
return 1.0f;
}
void ProgressBarApp::threadFn( gl::ContextRef ctx )
{
// Let's name the thread, making it easier to debug it.
setThreadName( "Loader thread" );
// Enable the OpenGL context.
ctx->makeCurrent();
// Now load all images one by one.
while( !mTerminated ) {
// Let's first wait a little, making it easier to see how it works.
std::this_thread::sleep_for( std::chrono::milliseconds( 200 ) );
// Make sure we're the only ones accessing the queue.
// The main thread might access the queue at the same time and that would be bad.
std::unique_lock<std::mutex> lockQueue( mQueueMutex );
// If we're done, go back to sleep. 'lockQueue' will go out of scope and release the lock for us.
if( mQueue.empty() )
continue;
// Now grab a file from the queue.
const auto file = mQueue.back();
mQueue.pop_back();
// We're done with the queue, so we can release our lock now.
lockQueue.unlock();
// Load the image.
const auto texture = gl::Texture2d::create( loadImage( file ) );
// Make sure OpenGL has finished uploading it to the GPU.
auto fence = gl::Sync::create();
fence->clientWaitSync();
// Make sure we're the only ones accessing the list of textures.
// The main thread might access the list at the same time and that would be bad.
std::unique_lock<std::mutex> lockTextures( mTexturesMutex );
// Finally, add it to the list of loaded textures.
mTextures.push_back( texture );
// Note: 'lockTextures' will go out of scope and release the lock for us.
}
// Bye bye!
}
CINDER_APP( ProgressBarApp, RendererGl )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment