Is it possible to run a process independently of FPS to be more millisecond accurate?

I’d like to run part of an app on a millisecond time step instead of frames per second. This part is independent of what is being rendered on screen, and for now, I’m running my app at 120 frames per second to get more accurate results.
To be more specific, I’m building a sequencer that sends OSC messages and it should be ms accurate. I though of trying out the audio classes of OF, but I’d rather not go there to avoid possible clashes with real-time priorities with other software.
Is it possible to sort of call a function after a certain amount of milliseconds have passed? Or is OF enslaved to the frame rate?

start a different thread and it will be FPS independent

ofThread

So if I set a loop inside the threadedFunction it will run constantly? My issue here is that I have to pass a bunch of vectors to the other thread, some of which are 2D or 3D, and looking around here seems that this is not an easy task. Any advide on that?

I love @kylemcdonald approach of triple buffering which was mentioned here in 2011

if you have a fixed number of items you can even use arrays and memcpy to duplicate data as fast as possible

here is a quick example you can modify to fit your needs if you want to use the audio thread to set your timer.

void audioOut(ofSoundBuffer & buffer) {
	setSeconds(buffer.getTickCount() * buffer.getNumFrames() / double(buffer.getSampleRate()));
}

yes, the loop inside the thread run constantly if you do while(true) {}

Just create a class, that inherits ofThread.

Write a constructor where you pass a pointer to your structure and keep this pointer.

in threadedFunction() you process data from the pointer

to expand SFR75’s answer above, having a loop in a thread will work, but it will hog a CPU to 100%. take a look at ofTimer (and it’s missing example) which inserts a sleep moment to “dilute the loop” to behave like a regular metronome.

that in itself will not solve the data sharing problem. You can use ofThreadChannel. Whilst not being the most evident class to use, it encapsulates as efficiently and safely as possible the related challenges.

if you choose to work with a plain Mutexes, wrap the lock()/unlock() calls on the smallest “grains of code” as possible, spending time locked() only when using the shared ressource. This will allow the fast thread to slip in and maintain it’s tightness and conversely minimize it’s impact on the main loop. Within a locked() area you are essentially choking the multithreading advantages of the threads partaking in locking. Smaller locked grains may give better chances to the system to make a finer time mesh with the threads.

on the consuming side it may be worthwhile to take a copy (i.e. buffer as per the 2011 discussion linked above) of the data for further processing instead of doing it while locked (ex: constructing and sending an OSC message). And i’ll insist on something stated in that thread, even if unrelated to threading: if your STL container size is dynamic, plan to reserve() enough memory for your “worst case”, and use emplace() to make sure that your stuff gets shoved straight in the container. That does not remove the need for locks, but helps preventing things moving around in memory, which may have an impact in fast regions, which is assumed with a method firing at 1000Hz.

finally on the producing side, you may be able to process things in a way that you can schedule them, instead of having the consuming thread “process” the data. I recently had to design something near this so i simplified my code into a quick example. In the update method (60fps) i’m randomly scheduling 50 events within the next 100ms (creating a sort of layered swarm of events averaging 50 per update cycle, 3000 messages per second). This gets consumed by a 1ms ofTimer that sends OSC Messages when the event’s time comes – at ofGetElapsedTime()'s resolution, which depends on your OS but it should be <1ms on most plaftorms by default. Without being designed for efficiency, the main thread takes ~3% and the fast thread ~2% of a CPU on a i7-9700 running linux (being received on localhost and printed to a terminal with oscdump). The messages are sent within less than 1ms of their expected moment. setting the ofTimer at 0.1ms augments proportionally the precision of the timing, and more or less doubles the CPU usage.

also take note of how to manage the thread upon ~destruction if you don’t want the OS to complain when the app quits.

#include "ofMain.h"
#include "ofxOsc.h"

class Scheduler : public ofThread {
public:

    Scheduler() {
        pipeline_.reserve(1000);
        to_fire_.reserve(1000);
        osc_sender_.setup("127.0.0.1", 1999);
        timer_.setPeriodicEvent(1000000);                   // 1 ms
        startThread();
    }

    auto schedule(double &moment, size_t &count) {
        std::scoped_lock lock(pipeline_mutex_);
        pipeline_.emplace(std::make_pair(moment, count));
    }
    auto get_pipeline_size() {
        std::scoped_lock lock(stats_mutex_);
        return pipeline_.size();                            // scoped_lock releases RAII-style
    }

private:

    std::mutex pipeline_mutex_;                             // to sync the pipeline read/write
    std::mutex stats_mutex_;                                // to access .size() to print in the other thread

    std::unordered_multimap<double, int> pipeline_;         // unordered allows reserve(); multimap permits simultaneous events (same key)
    std::vector<std::pair<double, int>> to_fire_;           // local copy (buffer) of transmitted data
    std::set<double> to_erase_;                             // std::set to prevent duplicates

    unsigned long counter_ = 0;
    ofxOscSender osc_sender_;

    ofTimer timer_;

    void threadedFunction() {
        while (isThreadRunning()) {

            auto now = ofGetElapsedTimef();
            {
                std::scoped_lock lock(pipeline_mutex_);

                /* could be std::lock or plain .lock() on the mutex but
                scoped_lock has advantages including forcing a scope,
                limiting the possibilities of entangling unlocks() */

                for (const auto & pair: pipeline_) {        // maintain lock during the iteration process
                    if (pair.first < now) {                 // find items in the past
                        to_fire_.emplace_back(pair);        // make a local copy for use outside the critical region
                        to_erase_.insert(pair.first);       // make a note of the key for erasure
                    }

                /* NB this is a simple and quick example to demonstrate
                the interaction between threads -- a proper scheduler would
                not iterate and compare all possible events at every pass */

                }
            }

            for (const auto & time: to_erase_) {
                std::scoped_lock lock(pipeline_mutex_);     // (here lock can be inside the loop)
                pipeline_.erase(time);                      // remove the to-be-processed elements from the pipeline
            }
            to_erase_.clear();                              // clear the list

            for (const auto & [time, val]: to_fire_) {      // leisurely process elements copied from the pipeline
                ofxOscMessage m;
                m.setAddress("/timer/elapsed");
                m.addInt64Arg(counter_++);
                m.addDoubleArg(now);
                m.addDoubleArg(time);
                m.addDoubleArg(val);
                m.addDoubleArg(now-time);
                osc_sender_.sendMessage(m);
            }

            {
                std::scoped_lock lock(stats_mutex_);        // lock to prevent .size() use in the ofLog in other thread
                to_fire_.clear();                           // clear the list
            }

            timer_.waitNext();                              // ofTimer magic
        }
    }
};

class ofApp : public ofBaseApp {

    Scheduler scheduler_;
    float next_log_ = 0;
    std::size_t counter_;

public:
    void update() {
        auto now = ofGetElapsedTimef();
        for (std::size_t i = 0; i < 50; i++) {
            double moment = now+(ofRandom(.001, .1));        // spread 50 events in the next 100ms to maintain a backlog
            counter_++;
            scheduler_.schedule(moment, counter_);
        }
        if (now > next_log_) {
            ofLogNotice("currently waiting in pipeline") << scheduler_.get_pipeline_size();
            next_log_ = now+1;
        }
    }

    void exit() {
        scheduler_.waitForThread();                           // without this the OS will have to crash the thread and will complain
    }
};

int main( ){
    ofSetupOpenGL(1024,768,OF_WINDOW);
    ofRunApp(new ofApp());
}

@burton and @SFR75 I like both ideas and I’ll try to implement them. So I’d like to create a threaded class that will utilize ofTimer, and I’ll give access by passing a pointer to a data structure which will include all the vectors I want to use in the threaded class. My question now is, should this data structure be created inside the ofApp class, or should it be global inside the .h file?

it should be something like that IMHO

ofApp.h

vector<ofPolygon> myVector; // this is your stucture. could be encapsulated in a class if you want
threadedClassName *myThreadedObj;  // pointer to your threaded class

ofApp.cpp

void setup() {
  ...
  myThreadedObj = new threadedClassName(&myVector);  // NB: you need to overload constructor
  myThreadedObj->startThread();

 }

well there are many factors to consider but the example above encapsulates everything in the Scheduler class, putting the implementation (private mutexes and buffers) into the Scheduler class, so that from the application’s point of view it just need to call the schedule() function on Scheduler and not know about how it’s implemented (it does not even have to know that things get locked()). in the example the data structure in question would be the pipeline_ multimap.

I made this work following your suggestions. So here’s the basic structure:
ofApp.h

typedef struct _sequencerVectors {
   // a bunch of vectors go here
} SequencerVectors;

class Sequencer : public ofThread {
  public:
    void setup(SequencerVectors *seqVec){
      timer.setPeriodicEvent(1000000); // this seems to be 1ms
    }

  private:
    void threadedFunction() {
      // in here I get access to the data structure like this seqVec->dataStructMember
      while (isThreadRunning()) {
        timer.waitNext(); //  this is executed every 1 ms
        // sequencer stuff here
      }
    }
    SequencerVectors *seqVec;
    ofTimer timer;
};

class ofApp : public ofBaseApp {
  public:
    // all OF functions here
    SequencerVectors seqVec;
    Sequencer sequencer;
};

ofApp.cpp

void setup(){
  sequencer.setup(&seqVec);
  // other stuff here
}

This is a combination of suggestions from both @SFR75 and @burton. Thank you guys for the help, as well as @dimitre for jumping in with his suggestion (I preferred not to use any audio though).

1 Like