Add frame delta smoothing option

Frame deltas are currently measured by querying the OS timer each frame. This is subject to random error. Frame delta smoothing instead filters the delta read from the OS by replacing it with the refresh rate delta wherever possible.

This PR also contains code to estimate the refresh rate based on the input deltas, without reading the refresh rate from the host OS.
This commit is contained in:
lawnjelly 2021-04-30 19:48:08 +01:00
parent b0b2b7df31
commit 6914d7c6e0
9 changed files with 356 additions and 0 deletions

View file

@ -596,6 +596,14 @@ bool _OS::is_vsync_via_compositor_enabled() const {
return OS::get_singleton()->is_vsync_via_compositor_enabled();
}
void _OS::set_delta_smoothing(bool p_enabled) {
OS::get_singleton()->set_delta_smoothing(p_enabled);
}
bool _OS::is_delta_smoothing_enabled() const {
return OS::get_singleton()->is_delta_smoothing_enabled();
}
_OS::PowerState _OS::get_power_state() {
return _OS::PowerState(OS::get_singleton()->get_power_state());
}
@ -1369,6 +1377,9 @@ void _OS::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_vsync_via_compositor", "enable"), &_OS::set_vsync_via_compositor);
ClassDB::bind_method(D_METHOD("is_vsync_via_compositor_enabled"), &_OS::is_vsync_via_compositor_enabled);
ClassDB::bind_method(D_METHOD("set_delta_smoothing", "delta_smoothing_enabled"), &_OS::set_delta_smoothing);
ClassDB::bind_method(D_METHOD("is_delta_smoothing_enabled"), &_OS::is_delta_smoothing_enabled);
ClassDB::bind_method(D_METHOD("has_feature", "tag_name"), &_OS::has_feature);
ClassDB::bind_method(D_METHOD("get_power_state"), &_OS::get_power_state);
@ -1391,6 +1402,7 @@ void _OS::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::INT, "exit_code"), "set_exit_code", "get_exit_code");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vsync_enabled"), "set_use_vsync", "is_vsync_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vsync_via_compositor"), "set_vsync_via_compositor", "is_vsync_via_compositor_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "delta_smoothing"), "set_delta_smoothing", "is_delta_smoothing_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "low_processor_usage_mode"), "set_low_processor_usage_mode", "is_in_low_processor_usage_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "low_processor_usage_mode_sleep_usec"), "set_low_processor_usage_mode_sleep_usec", "get_low_processor_usage_mode_sleep_usec");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keep_screen_on"), "set_keep_screen_on", "is_keep_screen_on");

View file

@ -374,6 +374,9 @@ public:
void set_vsync_via_compositor(bool p_enable);
bool is_vsync_via_compositor_enabled() const;
void set_delta_smoothing(bool p_enabled);
bool is_delta_smoothing_enabled() const;
PowerState get_power_state();
int get_power_seconds_left();
int get_power_percent_left();

View file

@ -587,6 +587,14 @@ bool OS::is_vsync_via_compositor_enabled() const {
return _vsync_via_compositor;
}
void OS::set_delta_smoothing(bool p_enabled) {
_delta_smoothing_enabled = p_enabled;
}
bool OS::is_delta_smoothing_enabled() const {
return _delta_smoothing_enabled;
}
OS::PowerState OS::get_power_state() {
return POWERSTATE_UNKNOWN;
}
@ -798,6 +806,7 @@ OS::OS() {
_no_window = false;
_exit_code = 0;
_orientation = SCREEN_LANDSCAPE;
_delta_smoothing_enabled = false;
_render_thread_mode = RENDER_THREAD_SAFE;

View file

@ -62,6 +62,7 @@ class OS {
bool _allow_layered;
bool _use_vsync;
bool _vsync_via_compositor;
bool _delta_smoothing_enabled;
char *last_error;
@ -548,6 +549,9 @@ public:
void set_vsync_via_compositor(bool p_enable);
bool is_vsync_via_compositor_enabled() const;
void set_delta_smoothing(bool p_enabled);
bool is_delta_smoothing_enabled() const;
virtual OS::PowerState get_power_state();
virtual int get_power_seconds_left();
virtual int get_power_percent_left();

View file

@ -1104,6 +1104,9 @@
<member name="current_screen" type="int" setter="set_current_screen" getter="get_current_screen" default="0">
The current screen index (starting from 0).
</member>
<member name="delta_smoothing" type="bool" setter="set_delta_smoothing" getter="is_delta_smoothing_enabled" default="true">
If [code]true[/code], the engine filters the time delta measured between each frame, and attempts to compensate for random variation. This will only operate on systems where V-Sync is active.
</member>
<member name="exit_code" type="int" setter="set_exit_code" getter="get_exit_code" default="0">
The exit code passed to the OS when the main loop exits. By convention, an exit code of [code]0[/code] indicates success whereas a non-zero exit code indicates an error. For portability reasons, the exit code should be set between 0 and 125 (inclusive).
[b]Note:[/b] This value will be ignored if using [method SceneTree.quit] with an [code]exit_code[/code] argument passed.

View file

@ -244,6 +244,11 @@
<member name="application/config/windows_native_icon" type="String" setter="" getter="" default="&quot;&quot;">
Icon set in [code].ico[/code] format used on Windows to set the game's icon. This is done automatically on start by calling [method OS.set_native_icon].
</member>
<member name="application/run/delta_smoothing" type="bool" setter="" getter="" default="true">
Time samples for frame deltas are subject to random variation introduced by the platform, even when frames are displayed at regular intervals thanks to V-Sync. This can lead to jitter. Delta smoothing can often give a better result by filtering the input deltas to correct for minor fluctuations from the refresh rate.
[b]Note:[/b] Delta smoothing is only attempted when [member display/window/vsync/use_vsync] is switched on, as it does not work well without V-Sync.
It may take several seconds at a stable frame rate before the smoothing is initially activated. It will only be active on machines where performance is adequate to render frames at the refresh rate.
</member>
<member name="application/run/disable_stderr" type="bool" setter="" getter="" default="false">
If [code]true[/code], disables printing to standard error. If [code]true[/code], this also hides error and warning messages printed by [method @GDScript.push_error] and [method @GDScript.push_warning]. See also [member application/run/disable_stdout].
Changes to this setting will only be applied upon restarting the application.

View file

@ -267,6 +267,8 @@ void Main::print_help(const char *p_binary) {
OS::get_singleton()->print(" --no-window Run with invisible window. Useful together with --script.\n");
OS::get_singleton()->print(" --enable-vsync-via-compositor When vsync is enabled, vsync via the OS' window compositor (Windows only).\n");
OS::get_singleton()->print(" --disable-vsync-via-compositor Disable vsync via the OS' window compositor (Windows only).\n");
OS::get_singleton()->print(" --enable-delta-smoothing When vsync is enabled, enabled frame delta smoothing.\n");
OS::get_singleton()->print(" --disable-delta-smoothing Disable frame delta smoothing.\n");
OS::get_singleton()->print(" --tablet-driver Tablet input driver (");
for (int i = 0; i < OS::get_singleton()->get_tablet_driver_count(); i++) {
if (i != 0) {
@ -417,6 +419,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
bool use_custom_res = true;
bool force_res = false;
bool saw_vsync_via_compositor_override = false;
bool delta_smoothing_override = false;
#ifdef TOOLS_ENABLED
bool found_project = false;
#endif
@ -637,6 +640,12 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
} else if (I->get() == "--disable-vsync-via-compositor") {
video_mode.vsync_via_compositor = false;
saw_vsync_via_compositor_override = true;
} else if (I->get() == "--enable-delta-smoothing") {
OS::get_singleton()->set_delta_smoothing(true);
delta_smoothing_override = true;
} else if (I->get() == "--disable-delta-smoothing") {
OS::get_singleton()->set_delta_smoothing(false);
delta_smoothing_override = true;
#endif
} else if (I->get() == "--profiling") { // enable profiling
@ -1191,6 +1200,11 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
OS::get_singleton()->set_low_processor_usage_mode_sleep_usec(GLOBAL_DEF("application/run/low_processor_mode_sleep_usec", 6900)); // Roughly 144 FPS
ProjectSettings::get_singleton()->set_custom_property_info("application/run/low_processor_mode_sleep_usec", PropertyInfo(Variant::INT, "application/run/low_processor_mode_sleep_usec", PROPERTY_HINT_RANGE, "0,33200,1,or_greater")); // No negative numbers
GLOBAL_DEF("application/run/delta_smoothing", true);
if (!delta_smoothing_override) {
OS::get_singleton()->set_delta_smoothing(GLOBAL_GET("application/run/delta_smoothing"));
}
GLOBAL_DEF("display/window/ios/hide_home_indicator", true);
GLOBAL_DEF("input_devices/pointing/ios/touch_delay", 0.150);

View file

@ -30,6 +30,9 @@
#include "main_timer_sync.h"
#include "core/math/math_funcs.h"
#include "core/os/os.h"
void MainFrameTime::clamp_idle(float min_idle_step, float max_idle_step) {
if (idle_step < min_idle_step) {
idle_step = min_idle_step;
@ -40,6 +43,244 @@ void MainFrameTime::clamp_idle(float min_idle_step, float max_idle_step) {
/////////////////////////////////
void MainTimerSync::DeltaSmoother::update_refresh_rate_estimator(int p_delta) {
// the calling code should prevent 0 or negative values of delta
// (preventing divide by zero)
// note that if the estimate gets locked, and something external changes this
// (e.g. user changes to non-vsync in the OS), then the results may be less than ideal,
// but usually it will detect this via the FPS measurement and not attempt smoothing.
// This should be a rare occurrence anyway, and will be cured next time user restarts game.
if (_estimate_locked) {
return;
}
// First average the delta over NUM_READINGS
_estimator_total_delta += p_delta;
_estimator_delta_readings++;
const int NUM_READINGS = 60;
if (_estimator_delta_readings < NUM_READINGS) {
return;
}
// use average
p_delta = _estimator_total_delta / NUM_READINGS;
// reset the averager for next time
_estimator_delta_readings = 0;
_estimator_total_delta = 0;
///////////////////////////////
int fps = Math::round(1000000.0 / p_delta);
// initial estimation, to speed up converging, special case we will estimate the refresh rate
// from the first average FPS reading
if (_estimated_fps == 0) {
// below 50 might be chugging loading stuff, or else
// dropping loads of frames, so the estimate will be inaccurate
if (fps >= 50) {
_estimated_fps = fps;
#ifdef GODOT_DEBUG_DELTA_SMOOTHER
print_line("initial guess (average measured) refresh rate: " + itos(fps));
#endif
} else {
// can't get started until above 50
return;
}
}
// we hit our exact estimated refresh rate.
// increase our confidence in the estimate.
if (fps == _estimated_fps) {
// note that each hit is an average of NUM_READINGS frames
_hits_at_estimated++;
if (_estimate_complete && _hits_at_estimated == 20) {
_estimate_locked = true;
#ifdef GODOT_DEBUG_DELTA_SMOOTHER
print_line("estimate LOCKED at " + itos(_estimated_fps) + " fps");
#endif
return;
}
// if we are getting pretty confident in this estimate, decide it is complete
// (it can still be increased later, and possibly lowered but only for a short time)
if ((!_estimate_complete) && (_hits_at_estimated > 2)) {
// when the estimate is complete we turn on smoothing
if (_estimated_fps) {
_estimate_complete = true;
_vsync_delta = 1000000 / _estimated_fps;
#ifdef GODOT_DEBUG_DELTA_SMOOTHER
print_line("estimate complete. vsync_delta " + itos(_vsync_delta) + ", fps " + itos(_estimated_fps));
#endif
}
}
#ifdef GODOT_DEBUG_DELTA_SMOOTHER
if ((_hits_at_estimated % (400 / NUM_READINGS)) == 0) {
String sz = "hits at estimated : " + itos(_hits_at_estimated) + ", above : " + itos(_hits_above_estimated) + "( " + itos(_hits_one_above_estimated) + " ), below : " + itos(_hits_below_estimated) + " (" + itos(_hits_one_below_estimated) + " )";
print_line(sz);
}
#endif
return;
}
const int SIGNIFICANCE_UP = 1;
const int SIGNIFICANCE_DOWN = 2;
// we are not usually interested in slowing the estimate
// but we may have overshot, so make it possible to reduce
if (fps < _estimated_fps) {
// micro changes
if (fps == (_estimated_fps - 1)) {
_hits_one_below_estimated++;
if ((_hits_one_below_estimated > _hits_at_estimated) && (_hits_one_below_estimated > SIGNIFICANCE_DOWN)) {
_estimated_fps--;
made_new_estimate();
}
return;
} else {
_hits_below_estimated++;
// don't allow large lowering if we are established at a refresh rate, as it will probably be dropped frames
bool established = _estimate_complete && (_hits_at_estimated > 10);
// macro changes
// note there is a large barrier to macro lowering. That is because it is more likely to be dropped frames
// than mis-estimation of the refresh rate.
if (!established) {
if (((_hits_below_estimated / 8) > _hits_at_estimated) && (_hits_below_estimated > SIGNIFICANCE_DOWN)) {
// decrease the estimate
_estimated_fps--;
made_new_estimate();
}
}
return;
}
}
// Changes increasing the estimate.
// micro changes
if (fps == (_estimated_fps + 1)) {
_hits_one_above_estimated++;
if ((_hits_one_above_estimated > _hits_at_estimated) && (_hits_one_above_estimated > SIGNIFICANCE_UP)) {
_estimated_fps++;
made_new_estimate();
}
return;
} else {
_hits_above_estimated++;
// macro changes
if ((_hits_above_estimated > _hits_at_estimated) && (_hits_above_estimated > SIGNIFICANCE_UP)) {
// increase the estimate
int change = fps - _estimated_fps;
change /= 2;
change = MAX(1, change);
_estimated_fps += change;
made_new_estimate();
}
return;
}
}
bool MainTimerSync::DeltaSmoother::fps_allows_smoothing(int p_delta) {
_measurement_time += p_delta;
_measurement_frame_count++;
if (_measurement_frame_count == _measurement_end_frame) {
// only switch on or off if the estimate is complete
if (_estimate_complete) {
int64_t time_passed = _measurement_time - _measurement_start_time;
// average delta
time_passed /= MEASURE_FPS_OVER_NUM_FRAMES;
// estimate fps
if (time_passed) {
float fps = 1000000.0f / time_passed;
float ratio = fps / (float)_estimated_fps;
//print_line("ratio : " + String(Variant(ratio)));
if ((ratio > 0.95) && (ratio < 1.05)) {
_measurement_allows_smoothing = true;
} else {
_measurement_allows_smoothing = false;
}
}
} // estimate complete
// new start time for next iteration
_measurement_start_time = _measurement_time;
_measurement_end_frame += MEASURE_FPS_OVER_NUM_FRAMES;
}
return _measurement_allows_smoothing;
}
int MainTimerSync::DeltaSmoother::smooth_delta(int p_delta) {
// Conditions to disable smoothing.
// Note that vsync is a request, it cannot be relied on, the OS may override this.
// If the OS turns vsync on without vsync in the app, smoothing will not be enabled.
// If the OS turns vsync off with sync enabled in the app, the smoothing must detect this
// via the error metric and switch off.
if (!OS::get_singleton()->is_delta_smoothing_enabled() || !OS::get_singleton()->is_vsync_enabled() || Engine::get_singleton()->is_editor_hint()) {
return p_delta;
}
// keep a running guesstimate of the FPS, and turn off smoothing if
// conditions not close to the estimated FPS
if (!fps_allows_smoothing(p_delta)) {
return p_delta;
}
// we can't cope with negative deltas .. OS bug on some hardware
// and also very small deltas caused by vsync being off.
// This could possibly be part of a hiccup, this value isn't fixed in stone...
if (p_delta < 1000) {
return p_delta;
}
// note still some vsync off will still get through to this point...
// and we need to cope with it by not converging the estimator / and / or not smoothing
update_refresh_rate_estimator(p_delta);
// no smoothing until we know what the refresh rate is
if (!_estimate_complete) {
return p_delta;
}
// accumulate the time we have available to use
_leftover_time += p_delta;
// how many vsyncs units can we fit?
int units = _leftover_time / _vsync_delta;
// a delta must include minimum 1 vsync
// (if it is less than that, it is either random error or we are no longer running at the vsync rate,
// in which case we should switch off delta smoothing, or re-estimate the refresh rate)
units = MAX(units, 1);
_leftover_time -= units * _vsync_delta;
// print_line("units " + itos(units) + ", leftover " + itos(_leftover_time/1000) + " ms");
return units * _vsync_delta;
}
/////////////////////////////////////
// returns the fraction of p_frame_slice required for the timer to overshoot
// before advance_core considers changing the physics_steps return from
// the typical values as defined by typical_physics_steps
@ -195,6 +436,8 @@ float MainTimerSync::get_cpu_idle_step() {
uint64_t cpu_ticks_elapsed = current_cpu_ticks_usec - last_cpu_ticks_usec;
last_cpu_ticks_usec = current_cpu_ticks_usec;
cpu_ticks_elapsed = _delta_smoother.smooth_delta(cpu_ticks_elapsed);
return cpu_ticks_elapsed / 1000000.0;
}

View file

@ -33,6 +33,9 @@
#include "core/engine.h"
// define these to get more debugging logs for the delta smoothing
//#define GODOT_DEBUG_DELTA_SMOOTHER
struct MainFrameTime {
float idle_step; // time to advance idles for (argument to process())
int physics_steps; // number of times to iterate the physics engine
@ -42,6 +45,66 @@ struct MainFrameTime {
};
class MainTimerSync {
class DeltaSmoother {
public:
// pass the recorded delta, returns a smoothed delta
int smooth_delta(int p_delta);
private:
void update_refresh_rate_estimator(int p_delta);
bool fps_allows_smoothing(int p_delta);
// estimated vsync delta (monitor refresh rate)
int64_t _vsync_delta = 16666;
// keep track of accumulated time so we know how many vsyncs to advance by
int64_t _leftover_time = 0;
// keep a rough measurement of the FPS as we run.
// If this drifts a long way below or above the refresh rate, the machine
// is struggling to keep up, and we can switch off smoothing. This
// also deals with the case that the user has overridden the vsync in the GPU settings,
// in which case we don't want to try smoothing.
static const int MEASURE_FPS_OVER_NUM_FRAMES = 64;
int64_t _measurement_time = 0;
int64_t _measurement_frame_count = 0;
int64_t _measurement_end_frame = MEASURE_FPS_OVER_NUM_FRAMES;
int64_t _measurement_start_time = 0;
bool _measurement_allows_smoothing = true;
// we can estimate the fps by growing it on condition
// that a large proportion of frames are higher than the current estimate.
int _estimated_fps = 0;
int _hits_at_estimated = 0;
int _hits_above_estimated = 0;
int _hits_below_estimated = 0;
int _hits_one_above_estimated = 0;
int _hits_one_below_estimated = 0;
bool _estimate_complete = false;
bool _estimate_locked = false;
// data for averaging the delta over a second or so
// to prevent spurious values
int _estimator_total_delta = 0;
int _estimator_delta_readings = 0;
void made_new_estimate() {
_hits_above_estimated = 0;
_hits_at_estimated = 0;
_hits_below_estimated = 0;
_hits_one_above_estimated = 0;
_hits_one_below_estimated = 0;
_estimate_complete = false;
#ifdef GODOT_DEBUG_DELTA_SMOOTHER
print_line("estimated fps " + itos(_estimated_fps));
#endif
}
} _delta_smoother;
// wall clock time measured on the main thread
uint64_t last_cpu_ticks_usec;
uint64_t current_cpu_ticks_usec;