Skip to content

Commit

Permalink
Add semi-fixed timestep and physics time stretching for global timescale
Browse files Browse the repository at this point in the history
Fixes godotengine#24769
Fixes godotengine#24334

The main change in this PR is adding the option in project settings->physics to choose between the old fixed timestep and a new path for semi-fixed timestep. With semi-fixed timestep users can either choose a high physics fps and get the benefit of matching between physics and frame times, or low physics fps and have physics effectively driven by frame deltas.

There is also a minor refactor to the main::iteration function, notably moving the physics tick into a separate function, as well as a major refactor to main_timer_sync, separating the common components of timing (timescaling, limiting max physics ticks) from the details of the timestep functionality themselves, which are separated into 2 classes, MainTimerSync_JitterFix (the old fixed timestep) and MainTimerSync_SemiFixed.

There is also a modification to allow the existing global time_scale to change the speed of the game without affecting the physics tick rate (i.e. giving consistent physics at different timescales).
  • Loading branch information
lawnjelly committed Aug 26, 2019
1 parent adae2b0 commit 55ddf46
Show file tree
Hide file tree
Showing 6 changed files with 417 additions and 166 deletions.
5 changes: 5 additions & 0 deletions core/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ float Engine::get_time_scale() const {
return _time_scale;
}

bool Engine::get_physics_stretch_ticks() const {
return _physics_stretch_ticks;
}

Dictionary Engine::get_version_info() const {

Dictionary dict;
Expand Down Expand Up @@ -232,6 +236,7 @@ Engine::Engine() {
_fps = 1;
_target_fps = 0;
_time_scale = 1.0;
_physics_stretch_ticks = true;
_pixel_snap = false;
_physics_frames = 0;
_idle_frames = 0;
Expand Down
2 changes: 2 additions & 0 deletions core/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Engine {
float _fps;
int _target_fps;
float _time_scale;
bool _physics_stretch_ticks;
bool _pixel_snap;
uint64_t _physics_frames;
float _physics_interpolation_fraction;
Expand Down Expand Up @@ -100,6 +101,7 @@ class Engine {

void set_time_scale(float p_scale);
float get_time_scale() const;
bool get_physics_stretch_ticks() const;

void set_frame_delay(uint32_t p_msec);
uint32_t get_frame_delay() const;
Expand Down
95 changes: 61 additions & 34 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
#include "editor/project_manager.h"
#endif

#include <stdint.h>

/* Static members */

// Singletons
Expand Down Expand Up @@ -1026,6 +1028,10 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
Engine::get_singleton()->set_iterations_per_second(GLOBAL_DEF("physics/common/physics_fps", 60));
ProjectSettings::get_singleton()->set_custom_property_info("physics/common/physics_fps", PropertyInfo(Variant::INT, "physics/common/physics_fps", PROPERTY_HINT_RANGE, "1,120,1,or_greater"));
Engine::get_singleton()->set_physics_jitter_fix(GLOBAL_DEF("physics/common/physics_jitter_fix", 0.5));
GLOBAL_DEF("physics/common/timestep/method", "Jitter Fix");
ProjectSettings::get_singleton()->set_custom_property_info("physics/common/timestep/method", PropertyInfo(Variant::STRING, "physics/common/timestep/method", PROPERTY_HINT_ENUM, "Jitter Fix,Fixed,Semi Fixed"));
Engine::get_singleton()->_physics_stretch_ticks = GLOBAL_DEF("physics/common/timestep/timescale_stretch_ticks", true);

Engine::get_singleton()->set_target_fps(GLOBAL_DEF("debug/settings/fps/force_fps", 0));
ProjectSettings::get_singleton()->set_custom_property_info("debug/settings/fps/force_fps", PropertyInfo(Variant::INT, "debug/settings/fps/force_fps", PROPERTY_HINT_RANGE, "0,120,1,or_greater"));

Expand Down Expand Up @@ -1856,6 +1862,35 @@ bool Main::is_iterating() {
static uint64_t physics_process_max = 0;
static uint64_t idle_process_max = 0;

// returns usecs taken by the physics tick
uint64_t Main::physics_tick(float p_physics_delta) {
uint64_t physics_begin = OS::get_singleton()->get_ticks_usec();

PhysicsServer::get_singleton()->sync();
PhysicsServer::get_singleton()->flush_queries();

Physics2DServer::get_singleton()->sync();
Physics2DServer::get_singleton()->flush_queries();

if (OS::get_singleton()->get_main_loop()->iteration(p_physics_delta)) {
// UINT64_MAX indicates we want to stop the loop through the physics iterations
return UINT64_MAX;
}

message_queue->flush();

PhysicsServer::get_singleton()->step(p_physics_delta);

Physics2DServer::get_singleton()->end_sync();
Physics2DServer::get_singleton()->step(p_physics_delta);

message_queue->flush();

Engine::get_singleton()->_physics_frames++;

return OS::get_singleton()->get_ticks_usec() - physics_begin;
}

bool Main::iteration() {

//for now do not error on this
Expand All @@ -1865,21 +1900,21 @@ bool Main::iteration() {

uint64_t ticks = OS::get_singleton()->get_ticks_usec();
Engine::get_singleton()->_frame_ticks = ticks;
main_timer_sync.set_cpu_ticks_usec(ticks);
main_timer_sync.set_fixed_fps(fixed_fps);

uint64_t ticks_elapsed = ticks - last_ticks;

int physics_fps = Engine::get_singleton()->get_iterations_per_second();
float frame_slice = 1.0 / physics_fps;

float time_scale = Engine::get_singleton()->get_time_scale();
// main_timer_sync will deal with time_scale and limiting the max number of physics ticks
MainFrameTime advance;
main_timer_sync.advance(advance, physics_fps, ticks, fixed_fps);

MainFrameTime advance = main_timer_sync.advance(frame_slice, physics_fps);
double step = advance.idle_step;
double scaled_step = step * time_scale;
double scaled_step = advance.scaled_frame_delta;

Engine::get_singleton()->_frame_step = step;
// Note Engine::_frame_step was previously the step unadjusted for timescale.
// It was unused within Godot, although perhaps used in custom Modules, I'm assuming this was a bug
// as scaled step makes more sense.
Engine::get_singleton()->_frame_step = scaled_step;
Engine::get_singleton()->_physics_interpolation_fraction = advance.interpolation_fraction;

uint64_t physics_process_ticks = 0;
Expand All @@ -1889,50 +1924,40 @@ bool Main::iteration() {

last_ticks = ticks;

static const int max_physics_steps = 8;
if (fixed_fps == -1 && advance.physics_steps > max_physics_steps) {
step -= (advance.physics_steps - max_physics_steps) * frame_slice;
advance.physics_steps = max_physics_steps;
}

bool exit = false;

Engine::get_singleton()->_in_physics = true;

for (int iters = 0; iters < advance.physics_steps; ++iters) {
float physics_delta = advance.physics_fixed_step_delta;

uint64_t physics_begin = OS::get_singleton()->get_ticks_usec();
for (int iters = 0; iters < advance.physics_steps; ++iters) {

PhysicsServer::get_singleton()->sync();
PhysicsServer::get_singleton()->flush_queries();
// special case, if using variable physics timestep and the last physics step
if (advance.physics_variable_step && (iters == (advance.physics_steps - 1))) {
// substitute the variable delta
physics_delta = advance.physics_variable_step_delta;
}

Physics2DServer::get_singleton()->sync();
Physics2DServer::get_singleton()->flush_queries();
// returns the time taken by the physics tick
uint64_t physics_usecs = physics_tick(physics_delta);

if (OS::get_singleton()->get_main_loop()->iteration(frame_slice * time_scale)) {
// in the special case of wanting to exit the loop we are passing
// UINT64_MAX which will never occur normally.
if (physics_usecs == UINT64_MAX) {
exit = true;
break;
}

message_queue->flush();

PhysicsServer::get_singleton()->step(frame_slice * time_scale);

Physics2DServer::get_singleton()->end_sync();
Physics2DServer::get_singleton()->step(frame_slice * time_scale);

message_queue->flush();

physics_process_ticks = MAX(physics_process_ticks, OS::get_singleton()->get_ticks_usec() - physics_begin); // keep the largest one for reference
physics_process_max = MAX(OS::get_singleton()->get_ticks_usec() - physics_begin, physics_process_max);
Engine::get_singleton()->_physics_frames++;
// performance stats
physics_process_ticks = MAX(physics_process_ticks, physics_usecs); // keep the largest one for reference
physics_process_max = MAX(physics_usecs, physics_process_max);
}

Engine::get_singleton()->_in_physics = false;

uint64_t idle_begin = OS::get_singleton()->get_ticks_usec();

if (OS::get_singleton()->get_main_loop()->idle(step * time_scale)) {
if (OS::get_singleton()->get_main_loop()->idle(scaled_step)) {
exit = true;
}
message_queue->flush();
Expand Down Expand Up @@ -1965,6 +1990,8 @@ bool Main::iteration() {

if (script_debugger) {
if (script_debugger->is_profiling()) {
// note that frame_slice is original physics delta, before time_scale applied
float frame_slice = 1.0 / physics_fps;
script_debugger->profiling_set_frame_times(USEC_TO_SEC(frame_time), USEC_TO_SEC(idle_process_ticks), USEC_TO_SEC(physics_process_ticks), frame_slice);
}
script_debugger->idle_poll();
Expand Down
1 change: 1 addition & 0 deletions main/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Main {
static bool start();

static bool iteration();
static uint64_t physics_tick(float p_physics_delta);
static void force_redraw();

static bool is_iterating();
Expand Down
Loading

1 comment on commit 55ddf46

@huhund
Copy link

@huhund huhund commented on 55ddf46 Nov 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much. This is fixing something that has been bogging me for days. Hopefully some friendly soul has time to review this PR, something like this would be very useful.

Please sign in to comment.