Can you say more about what your specific goals are? Could you drop your fps to frameRate(50) so that your sketch always hits the minimum? Do you need periodic ticks that are exactly on a certain time? Can you drop frames to reinforce alignment?
I once wrote a syncing class for a library that was meant to make simultaneous sketch output robustly similar when running on different hardware with unreliable frame rates. You might find it helpful. Keep in mind though, the main thing you can do within sketch code to make frame times more reliable or aligned is make them slower. Or you can buy faster hardware, or change your software environment, et cetera.
Syncer.java
package com.jeremydouglass.toolboxing.time;
/**
* A Syncer gives ways to measure lag and remediate it.
*
* A Sync lets sketches share an ideal set of target reference
* times or frames, and also lets sketches attempt to conform
* to these times / frames. It computes a target time for a
* given frame number, or a target frame number for a given time.
*
* For sketches that are able to maintain the given frame rate,
* Sync provides a stable set of references that can be provided
* as random seeds to produce synchronized pseudo-random behavior.
*
* When sketches drift or fall behind the target frameRate, Sync
* provides two mechanisms of enforcing alignment: alignFrame,
* which skips drawing frames to catch up, and alignMillis(),
* which also uses millisecond delays to further align a late
* frame to the next target.
* down a given frame (aligning it to the next target
* time) or skip one or more frames (catching up to a
* target frame).
* Processing's frameRate() assigns a *minimum* frame duration.
* This mean that frames run late, not early. This running late
* is called lag, and it may involve a slow drift over time,
* or a periodic long lag due to infrequent demanding operations,
* or consistent inability to hit the target speed every frame.
*
* Lag can be expressed in two ways -- frame lag and time lag.
*
* Frame lag:
* In frame lag, the current frame is less than expected.
* For example, drawing frame 8 while scheduled to draw frame 10.
*
* Time lag:
* In time lag, the current time is greater than expected.
* For example, drawing frame 10 at 210 when scheduled for 167.
*
* Lag may be remediated via alignment.
* Skip the sketch forward to the next scheduled frame.
* This has the effect of skipping frames.
*
* Sync may also reset the schedule to the current late frame.
*/
public interface Syncer {
public int startMillis = 0;
public long startTime = 0;
public int targetFrameRate = 60;
public int alignFrame();
//int alignMillis();
public int targetFrame();
public int targetMillis();
public long targetTime();
}
Sync.java
package com.jeremydouglass.toolboxing.time;
import processing.core.PApplet;
/**
*
*/
public class Sync implements Syncer {
private PApplet pa;
public long startTime;
public long syncStartTime;
public int targetFrameRate;
public int step;
/**
* Constructor
*
* Default is to use the sketch current `frameRate`.
* Note that after setup this value is dynamic, and
* may not be equal to the setting specified with
* `frameRate()`.
*
* @param pa Processing sketch PApplet ('this')
* @param targetFrameRate The target frame rate for sync to describe / enforce
*/
public Sync(PApplet pa, int targetFrameRate) {
this.pa = pa;
this.targetFrameRate = targetFrameRate;
this.step = (int)(1000/targetFrameRate);
this.startTime = System.currentTimeMillis();
this.syncStartTime = startTime + step - startTime%step;
PApplet.println("tfr", targetFrameRate, "step", step, "sync-start", syncStartTime);
PApplet.println();
}
/**
* If the current time is later than a threshold value,
* skip to the next aligned frame. When alignMillis is
* true, then delay until milliseconds are also aligned.
*
* Default threshold is half the step value. For example,
* a frameRate of 10fps has a step value of 100 per frame.
* Half the value is 50, so calling align when a frame is
* more than 50 milliseconds late will trigger a frame align
* (skipping one or more frame numbers) and millisecond align
* in the form of a short delay.
*
* Millisecond alignments are only approximate.
*
* @param pa Processing sketch PApplet ('this')
* @param minLateMillis Threshold lateness in milliseconds for triggering alignment
* @param alignMillis Set true to delay frame until milliseconds are better aligned. Default false.
*/
//public int alignMillis() {
// return this.alignMillis(this.step/2);
//}
//public int alignMillis(int minLateMillis) {
// int diff = this.diffMillis();
// if (diff > minLateMillis) {
// int framesSkipped = this.alignFrame();
// // this.skip();
// diff = this.diffMillis();
// if (diff > minLateMillis) {
// delay(diff);
// return diff + framesSkipped * step;
// }
// return framesSkipped * step;
// }
// return 0;
//}
//public int alignMillis(int minLateMillis) {
// int diff = this.diffMillis();
// if (diff > minLateMillis) {
// int framesSkipped = this.alignFrame();
// diff = this.diffMillis();
// if (diff > minLateMillis) {
// delay(diff);
// return diff + framesSkipped * step;
// }
// return framesSkipped * step;
// }
// return 0;
//}
/**
* If the current time is later than a threshold value,
* skip to the next aligned frame. When alignMillis is
* true, then delay until milliseconds are also aligned.
*/
public int alignFrame() {
int target = this.targetFrame();
if (pa.frameCount < target) {
return this.skip(target - pa.frameCount);
}
return 0;
}
/**
* Increment sketch frameCount by an amount. Defaults to 1.
* @return Count of frames skipped.
*/
public int skip() { // internal
return this.skip(1);
}
/**
* Increment sketch frameCount by an amount. Defaults to 1.
*
* @param count Count of frames to skip.
* @return Count of frames skipped.
*/
private int skip(int count) {
pa.frameCount += count;
return count;
}
/**
* Returns the difference between the current sketch frameCount and the target frame.
* @return Difference from target frame.
*/
public int diffFrame() { // internal
return this.targetFrame() - pa.frameCount;
}
/**
* Returns the difference between the current sketch millis and the target millis.
* @return Difference from target millis.
*/
public int diffMillis() { // internal
int diff = pa.millis() - targetMillis();
return diff;
}
/**
* Gives the target frame count for a given time in milliseconds.
* Defaults to the current sketch millis().
*
* @return The target frame count.
*/
public int targetFrame() {
return pa.millis()/this.step;
//return this.targetMillis()/this.targetFrameRate;
}
/**
* Gives the target millisecond time for a given frame.
* Defaults to the current sketch frameCount().
*
* @return The target millisecond time.
*/
public int targetMillis() {
return pa.frameCount * this.step;
//int millis = pa.millis();
//return millis - millis%step;
}
/**
* The target time, offset by the start time, given a current frame.
*
* @return The target millisecond time.
*/
public long targetTime() {
return syncStartTime + this.targetMillis();
}
}
Run sketch twice side-by-side to test:
import com.jeremydouglass.toolboxing.time.Sync;
Sync sync;
void setup() {
int fps = 10;
frameRate(fps);
sync = new Sync(this, fps);
}
void draw() {
// log frame skipping
int skip = sync.alignFrame();
if(skip>0){
print(" ", skip);
}
// fade old lines
fill(255,8);
rect(-2,-2,width+4,height+4);
// draw random line
randomSeed(sync.targetTime());
line(random(0, width), random(0, height),
random(0, width), random(0, height));
// simulate load (parallel across sketches)
delay((int)random(0, 100));
// simulate other lag (not parallel)
delay((int)random(0, 100));
}