#include #include static double min(double a, double b) { return a <= b ? a : b; } static simloop_time_t ddt_from_fps(int fps) { static constexpr double NANOSECONDS = 1e9; return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps); } Simloop simloop_make(const SimloopArgs* args) { assert(args); assert(args->update_fps > 0); return (Simloop){ .frame = 0, .update = (SimloopTimeline){ .ddt = ddt_from_fps(args->update_fps), .time = 0, }, .render_ddt = ddt_from_fps(args->max_render_fps), }; } void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) { assert(sim); assert(out); sim->clock += dt; // Simulation update. // If the simulation falls behind the clock, we advance by a single ddt // increment per loop iteration here and give it a chance to catch up over // subsequent iterations. // This has the implication that percent_frame can fall out of range (>1) if // we are not careful with how it is defined. See the logic below. // If the delta is too large, then we simply warp the simulation to the wall // clock. This avoids the appearance of the simulation playing in fast-forward // as it tries to catch up. Large time spikes can typically occur at the start // of the simulation when the application loads assets, compiles shaders, etc. static const uint64_t max_catchup_frames = 10; const simloop_time_t delta = sim->clock - sim->update.time; const uint64_t delta_frames = delta / sim->update.ddt; const bool update_this_tick = delta >= sim->update.ddt; const bool warp = delta_frames > max_catchup_frames; sim->update.time = warp ? sim->clock : (sim->update.time + (update_this_tick ? sim->update.ddt : 0)); // Loop-state update. sim->frame += (update_this_tick ? 1 : 0); // Interpolator for smooth animation. // If the update falls behind the clock, then percent_frame can fall out of // range (>1) if we are not careful. We impose that it is strictly never >1 // to account for this case. assert(sim->update.ddt > 0); assert(sim->update.time <= sim->clock); out->percent_frame = min( 1., (double)(sim->clock - sim->update.time) / (double)sim->update.ddt); assert((0. <= out->percent_frame) && (out->percent_frame <= 1.)); // Render frame rate throttle. // Note that if no max render fps is given, then render_ddt is 0. The logic // works for both render_ddt>0 and =0. // Need to be careful with subtraction since the quantities are unsigned. // Subtract an epsilon to account for delays in thread scheduling. static const simloop_time_t eps = 50'000; // 50us out->throttle = (sim->render_ddt > (dt - eps)) ? (sim->render_ddt - eps - dt) : 0; out->frame = sim->frame; out->update_elapsed = sim->update.time; out->update_dt = sim->update.ddt; out->should_update = update_this_tick; }