From a98ca50dab5a0e7dd2006f6906fb7181f740a0d4 Mon Sep 17 00:00:00 2001 From: lawnjelly Date: Sat, 6 May 2023 19:35:38 +0100 Subject: [PATCH] Timestamped input buffering - prevent stalling and improve timing Input was resulting in stalling and ANRs on Android and possibly other platforms. This PR separates buffered input into two parts to prevent stalls. It also timestamps input events and applies them on the relevant logical physics tick, rather than attempting to apply them "realtime". --- core/os/input.cpp | 87 +++++++++++- core/os/input.h | 56 +++++++- core/os/os.h | 2 +- doc/classes/ProjectSettings.xml | 6 +- main/input_default.cpp | 241 ++++++++++++++++++++++++++------ main/input_default.h | 44 ++++-- main/main.cpp | 27 ++-- main/main.h | 1 - main/main_timer_sync.cpp | 17 ++- main/main_timer_sync.h | 6 + platform/android/os_android.cpp | 1 + platform/osx/os_osx.mm | 1 + platform/windows/os_windows.cpp | 1 + platform/x11/os_x11.cpp | 1 + 14 files changed, 413 insertions(+), 78 deletions(-) diff --git a/core/os/input.cpp b/core/os/input.cpp index 0df33f758bc1..a0e89fa0656f 100644 --- a/core/os/input.cpp +++ b/core/os/input.cpp @@ -44,6 +44,88 @@ Input *Input::get_singleton() { return singleton; } +void Input::flush_buffered_events_post_frame() { + if (data.use_legacy_flushing) { + // Matches old logic - if buffering, but not agile. + if (data.buffering_mode == BUFFERING_MODE_FRAME) { + force_flush_buffered_events(); + } + } +} + +void Input::flush_buffered_events() { + // This function was called in hacky fashion + // all over the shop on different platforms, + // and will interfere with agile input, + // so is now a NOOP, unless the user chooses + // legacy flushing. + // Using legacy flushing will muck up + // agile input, so is not recommended, + // and is only included temporarily to + // allow fixing bugs in beta. + if (data.use_legacy_flushing) { + force_flush_buffered_events(); + } +} + +void Input::set_use_accumulated_input(bool p_enable) { + data.use_accumulated_input = p_enable; + _update_buffering_mode(); +} + +bool Input::is_using_accumulated_input() const { + return data.use_accumulated_input; +} + +void Input::set_use_input_buffering(bool p_enable) { + data.use_buffering = p_enable; + _update_buffering_mode(); +} + +bool Input::is_using_input_buffering() const { + return data.use_buffering; +} + +void Input::set_use_agile_flushing(bool p_enable) { + data.use_agile = p_enable; + _update_buffering_mode(); +} + +bool Input::is_using_agile_flushing() const { + return data.use_agile; +} + +void Input::set_use_legacy_flushing(bool p_enable) { + data.use_legacy_flushing = p_enable; +} + +void Input::set_has_input_thread(bool p_has_thread) { + data.has_input_thread = p_has_thread; + _update_buffering_mode(); +} + +void Input::_update_buffering_mode() { + // Logic here may appear confusing but it is historical, + // and to prevent compat breaking. + // Accumulated input was added in such a way as to override + // the use_buffering setting. + // i.e. Buffering is used if use_buffering is false, but accumulated is true, + // as accumulating needs the previous event in order to work. + + // Additionally for agile input, it currently only makes sense to activate when + // the platform has a separate input thread, otherwise the extra processing + // on flush on each tick just wastes CPU. + if (data.use_accumulated_input || data.use_buffering) { + if (data.use_agile && data.has_input_thread) { + data.buffering_mode = BUFFERING_MODE_AGILE; + } else { + data.buffering_mode = BUFFERING_MODE_FRAME; + } + } else { + data.buffering_mode = BUFFERING_MODE_NONE; + } +} + void Input::set_mouse_mode(MouseMode p_mode) { ERR_FAIL_INDEX((int)p_mode, 5); OS::get_singleton()->set_mouse_mode((OS::MouseMode)p_mode); @@ -104,7 +186,10 @@ void Input::_bind_methods() { ClassDB::bind_method(D_METHOD("parse_input_event", "event"), &Input::parse_input_event); ClassDB::bind_method(D_METHOD("set_use_accumulated_input", "enable"), &Input::set_use_accumulated_input); ClassDB::bind_method(D_METHOD("is_using_accumulated_input"), &Input::is_using_accumulated_input); - ClassDB::bind_method(D_METHOD("flush_buffered_events"), &Input::flush_buffered_events); + + // For backward compatibility this calls the force method. + // Use this at your peril as it will mess up agile input any frame it is called. + ClassDB::bind_method(D_METHOD("flush_buffered_events"), &Input::force_flush_buffered_events); ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_mode"), "set_mouse_mode", "get_mouse_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_accumulated_input"), "set_use_accumulated_input", "is_using_accumulated_input"); diff --git a/core/os/input.h b/core/os/input.h index ba1cb045d280..185c452474b9 100644 --- a/core/os/input.h +++ b/core/os/input.h @@ -52,6 +52,33 @@ class Input : public Object { MOUSE_MODE_CONFINED_HIDDEN, }; + // There are three buffering modes. + // * No buffering (immediately process input) + // * or buffering and flush the input every frame, + // * or agile - buffering and flush the input every physics tick and every frame. + // The mode is decided logically in _update_buffering_mode() + // depending on the settings for + // use_accumulated_input, use_buffering, use_agile, and has_input_thread. + enum BufferingMode { + BUFFERING_MODE_NONE, + BUFFERING_MODE_FRAME, + BUFFERING_MODE_AGILE, + }; + +protected: + struct Data { + BufferingMode buffering_mode = BUFFERING_MODE_FRAME; + bool use_accumulated_input = true; + bool use_buffering = false; + bool use_agile = false; + bool use_legacy_flushing = false; + bool has_input_thread = false; + } data; + + bool has_input_thread() const { return data.has_input_thread; } + void _update_buffering_mode(); + +public: #undef CursorShape enum CursorShape { CURSOR_ARROW, @@ -142,11 +169,30 @@ class Input : public Object { virtual int get_joy_axis_index_from_string(String p_axis) = 0; virtual void parse_input_event(const Ref &p_event) = 0; - virtual void flush_buffered_events() = 0; - virtual bool is_using_input_buffering() = 0; - virtual void set_use_input_buffering(bool p_enable) = 0; - virtual bool is_using_accumulated_input() = 0; - virtual void set_use_accumulated_input(bool p_enable) = 0; + + // DO NOT call force_flush_buffered_events() in normal course of events, + // as it will break agile input on any frame it is called. + // Instead rely on flush_buffered_events_tick() and + // flush_buffered_events_frame() which are called from + // Main::iteration(). + virtual void force_flush_buffered_events() = 0; + virtual void flush_buffered_events_iteration() = 0; + virtual void flush_buffered_events_tick(uint64_t p_tick_timestamp) = 0; + virtual void flush_buffered_events_frame() = 0; + void flush_buffered_events(); + void flush_buffered_events_post_frame(); + + void set_use_accumulated_input(bool p_enable); + bool is_using_accumulated_input() const; + + void set_use_input_buffering(bool p_enable); + bool is_using_input_buffering() const; + + void set_use_agile_flushing(bool p_enable); + bool is_using_agile_flushing() const; + + void set_use_legacy_flushing(bool p_enable); + void set_has_input_thread(bool p_has_thread); Input(); }; diff --git a/core/os/os.h b/core/os/os.h index d67e9d61250e..24403efb2751 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -653,7 +653,7 @@ class OS { virtual int get_power_seconds_left(); virtual int get_power_percent_left(); - virtual void force_process_input(){}; + virtual void force_process_input() {} bool has_feature(const String &p_feature); void set_has_server_feature_callback(HasServerFeatureCallback p_callback); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 3912ae8bd0c0..81db4b6f125a 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -667,12 +667,16 @@ Default [InputEventAction] to move up in the UI. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. - + If [code]true[/code], key/touch/joystick events will be flushed just before every idle and physics frame. If [code]false[/code], such events will be flushed only once per idle frame, between iterations of the engine. Enabling this can greatly improve the responsiveness to input, specially in devices that need to run multiple physics frames per visible (idle) frame, because they can't run at the target frame rate. [b]Note:[/b] Currently implemented only in Android. + + If [code]true[/code], input events will be flushed on demand according to platform, instead of the new platform independent method. + [b]Note:[/b] Enabling this will break agile event flushing, which may reduce input timing resolution on some platforms. + If [code]true[/code], sends mouse input events when tapping or swiping on the touchscreen. diff --git a/main/input_default.cpp b/main/input_default.cpp index 0aea8ecaa100..b0977b53f53b 100644 --- a/main/input_default.cpp +++ b/main/input_default.cpp @@ -40,6 +40,135 @@ #include "core/os/thread.h" #endif +// Accumulating immediately rather than deferred at flush +// is slightly more efficient (because of less allocations / transfer to the main buffer etc) +// but is less accurate timing wise, so should only be used in frame buffering mode. +void InputEventBuffer::accumulate_or_push_event(Ref p_event, uint64_t p_timestamp) { + // Events can come in any time, including when we are preparing to read the incoming queue, + // so we must lock to prevent race condition. + MutexLock lock(data.incoming_mutex); + + LocalVector &incoming = data.incoming[data.incoming_write]; + + // First, attempt to accumulate. + if (incoming.size()) { + Event &prev = incoming[incoming.size() - 1]; + if (prev.event->accumulate(p_event)) { + return; + } + } + + // Accumulate failed, fall back to push. + incoming.resize(incoming.size() + 1); + Event &e = incoming[incoming.size() - 1]; + e.event = p_event; + e.timestamp = p_timestamp; +} + +void InputEventBuffer::push_event(Ref p_event, uint64_t p_timestamp) { + // Events can come in any time, including when we are preparing to read the incoming queue, + // so we must lock to prevent race condition. + MutexLock lock(data.incoming_mutex); + + LocalVector &incoming = data.incoming[data.incoming_write]; + incoming.resize(incoming.size() + 1); + Event &e = incoming[incoming.size() - 1]; + e.event = p_event; + e.timestamp = p_timestamp; +} + +void InputEventBuffer::_try_accumulate(uint64_t p_timestamp) { + // Try and accumulate events after the current front + // until we fail or pass the current timestamp. + List::Element *front = data.buffer.front(); + Event &front_event = front->get(); + + while (List::Element *next = data.buffer.front()->next()) { + const Event &next_event = next->get(); + if (next_event.timestamp > p_timestamp) { + // Don't want to accumulate events that are on the next tick.. + // want to keep some resolution to the events. + break; + } + if (front_event.event->accumulate(next_event.event)) { + // Remove the accumulated event from the buffer. + data.buffer.swap(front, next); + data.buffer.pop_front(); + // Check this does not invalidate front and front_event. + DEV_ASSERT(front == data.buffer.front()); + DEV_ASSERT(&front_event == &front->get()); + } else { + break; + } + } +} + +void InputEventBuffer::flush_events(uint64_t p_current_timestamp, InputDefault &r_input_handler, bool p_accumulate) { + // Flushing function is not re-entrant. + // This is unlikely to be called multithread, but this check should be cheap. + MutexLock lock(data.buffer_mutex); + + if (data.flushing) { + // only allow one flush at a time + return; + } + data.flushing = true; + + data.incoming_mutex.lock(); + SWAP(data.incoming_write, data.incoming_read); + data.incoming_mutex.unlock(); + + LocalVector &incoming = data.incoming[data.incoming_read]; + +// #define GODOT_DEBUG_INPUT_EVENT_BUFFER +#ifdef GODOT_DEBUG_INPUT_EVENT_BUFFER + String sz = "timestamp: " + itos(p_current_timestamp) + " incoming : " + itos(incoming.size()); +#endif + + for (uint32_t n = 0; n < incoming.size(); n++) { + // Copy to main buffer. + data.buffer.push_back(incoming[n]); + } + + // Prepare for more input next time, prevent leak. + incoming.clear(); + +#ifdef GODOT_DEBUG_INPUT_EVENT_BUFFER + uint32_t processed = 0; +#endif + + // Now we can read through the input buffer, up to the current time, and process. + while (data.buffer.front()) { + const Event &e = data.buffer.front()->get(); + + // Timestamp within range? + if (e.timestamp > p_current_timestamp) { + // We are up to date, process no more input on this tick / frame. + break; + } + + if (p_accumulate) { + _try_accumulate(p_current_timestamp); + } + + r_input_handler._parse_input_event_impl(e.event, false, false); +#ifdef GODOT_DEBUG_INPUT_EVENT_BUFFER + processed++; +#endif + + // Event processed, remove from buffer. + data.buffer.pop_front(); + } + +#ifdef GODOT_DEBUG_INPUT_EVENT_BUFFER + if ((p_current_timestamp != UINT64_MAX) && processed) { + print_line(sz + ", processed : " + itos(processed)); + } +#endif + + data.flushing = false; +} + void InputDefault::SpeedTrack::update(const Vector2 &p_delta_p) { uint64_t tick = OS::get_singleton()->get_ticks_usec(); uint32_t tdiff = tick - last_tick; @@ -314,7 +443,7 @@ Vector3 InputDefault::get_gyroscope() const { return gyroscope; } -void InputDefault::_parse_input_event_impl(const Ref &p_event, bool p_is_emulated) { +void InputDefault::_parse_input_event_impl(const Ref &p_event, bool p_is_emulated, bool p_unlock) { // This function does the final delivery of the input event to user land. // Regardless where the event came from originally, this has to happen on the main thread. DEV_ASSERT(Thread::get_caller_id() == Thread::get_main_id()); @@ -362,9 +491,13 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool touch_event->set_pressed(mb->is_pressed()); touch_event->set_position(mb->get_position()); touch_event->set_double_tap(mb->is_doubleclick()); - _THREAD_SAFE_UNLOCK_ - main_loop->input_event(touch_event); - _THREAD_SAFE_LOCK_ + if (p_unlock) { + _THREAD_SAFE_UNLOCK_ + main_loop->input_event(touch_event); + _THREAD_SAFE_LOCK_ + } else { + main_loop->input_event(touch_event); + } } } @@ -386,9 +519,13 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool drag_event->set_relative(relative); drag_event->set_speed(get_last_mouse_speed()); - _THREAD_SAFE_UNLOCK_ - main_loop->input_event(drag_event); - _THREAD_SAFE_LOCK_ + if (p_unlock) { + _THREAD_SAFE_UNLOCK_ + main_loop->input_event(drag_event); + _THREAD_SAFE_LOCK_ + } else { + main_loop->input_event(drag_event); + } } } @@ -434,7 +571,7 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool button_event->set_button_mask(mouse_button_mask & ~(1 << (BUTTON_LEFT - 1))); } - _parse_input_event_impl(button_event, true); + _parse_input_event_impl(button_event, true, p_unlock); } } } @@ -458,7 +595,7 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool motion_event->set_button_mask(mouse_button_mask); motion_event->set_pressure(1.f); - _parse_input_event_impl(motion_event, true); + _parse_input_event_impl(motion_event, true, p_unlock); } } @@ -484,9 +621,13 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool if (ge.is_valid()) { if (main_loop) { - _THREAD_SAFE_UNLOCK_ - main_loop->input_event(ge); - _THREAD_SAFE_LOCK_ + if (p_unlock) { + _THREAD_SAFE_UNLOCK_ + main_loop->input_event(ge); + _THREAD_SAFE_LOCK_ + } else { + main_loop->input_event(ge); + } } } @@ -516,9 +657,13 @@ void InputDefault::_parse_input_event_impl(const Ref &p_event, bool } if (main_loop) { - _THREAD_SAFE_UNLOCK_ - main_loop->input_event(p_event); - _THREAD_SAFE_LOCK_ + if (p_unlock) { + _THREAD_SAFE_UNLOCK_ + main_loop->input_event(p_event); + _THREAD_SAFE_LOCK_ + } else { + main_loop->input_event(p_event); + } } } @@ -678,7 +823,7 @@ void InputDefault::ensure_touch_mouse_raised() { button_event->set_button_index(BUTTON_LEFT); button_event->set_button_mask(mouse_button_mask & ~(1 << (BUTTON_LEFT - 1))); - _parse_input_event_impl(button_event, true); + _parse_input_event_impl(button_event, true, true); } } @@ -753,46 +898,52 @@ void InputDefault::parse_input_event(const Ref &p_event) { } #endif - if (use_accumulated_input) { - if (buffered_events.empty() || !buffered_events.back()->get()->accumulate(p_event)) { - buffered_events.push_back(p_event); - } - } else if (use_input_buffering) { - buffered_events.push_back(p_event); + if (data.buffering_mode == Input::BUFFERING_MODE_NONE) { + _parse_input_event_impl(p_event, false, true); } else { - _parse_input_event_impl(p_event, false); + // We can accumulate immediately on input if in frame mode, + // but if in agile / logical mode accumulation is deferred until flushing, + // so we can just push directly. + if ((data.buffering_mode == Input::BUFFERING_MODE_FRAME) && data.use_accumulated_input) { + _event_buffer.accumulate_or_push_event(p_event, OS::get_singleton()->get_ticks_usec()); + } else { + _event_buffer.push_event(p_event, OS::get_singleton()->get_ticks_usec()); + } } } -void InputDefault::flush_buffered_events() { - _THREAD_SAFE_METHOD_ - - while (buffered_events.front()) { - // The final delivery of the input event involves releasing the lock. - // While the lock is released, another thread may lock it and add new events to the back. - // Therefore, we get each event and pop it while we still have the lock, - // to ensure the list is in a consistent state. - List>::Element *E = buffered_events.front(); - Ref e = E->get(); - buffered_events.pop_front(); - _parse_input_event_impl(e, false); - } +void InputDefault::flush_buffered_events_ex(uint64_t p_up_to_timestamp) { + _event_buffer.flush_events(p_up_to_timestamp, *this, data.use_accumulated_input); } -bool InputDefault::is_using_input_buffering() { - return use_input_buffering; +void InputDefault::force_flush_buffered_events() { + flush_buffered_events_ex(UINT64_MAX); } -void InputDefault::set_use_input_buffering(bool p_enable) { - use_input_buffering = p_enable; +void InputDefault::flush_buffered_events_iteration() { + // legacy did not flush here. + if (data.use_legacy_flushing) { + return; + } + + if (data.buffering_mode == BUFFERING_MODE_FRAME) { + flush_buffered_events_ex(UINT64_MAX); + } } -bool InputDefault::is_using_accumulated_input() { - return use_accumulated_input; +void InputDefault::flush_buffered_events_tick(uint64_t p_tick_timestamp) { + if (data.buffering_mode == BUFFERING_MODE_AGILE) { + flush_buffered_events_ex(p_tick_timestamp); + } } -void InputDefault::set_use_accumulated_input(bool p_enable) { - use_accumulated_input = p_enable; +void InputDefault::flush_buffered_events_frame() { + // If we are in legacy mode, if not NONE or FRAME, + // then it will be AGILE, in which case legacy had a flush + // here, so the new logic works as before. + if (data.buffering_mode == BUFFERING_MODE_AGILE) { + flush_buffered_events_ex(UINT64_MAX); + } } void InputDefault::release_pressed_events() { @@ -811,8 +962,6 @@ void InputDefault::release_pressed_events() { } InputDefault::InputDefault() { - use_input_buffering = false; - use_accumulated_input = true; mouse_button_mask = 0; emulate_touch_from_mouse = false; emulate_mouse_from_touch = false; diff --git a/main/input_default.h b/main/input_default.h index 76cc8155dc74..e57fe3762383 100644 --- a/main/input_default.h +++ b/main/input_default.h @@ -33,9 +33,37 @@ #include "core/os/input.h" +class InputDefault; + +class InputEventBuffer { + struct Event { + uint64_t timestamp; + Ref event; + }; + + struct Data { + LocalVector incoming[2]; + uint32_t incoming_read = 0; + uint32_t incoming_write = 1; + Mutex incoming_mutex; + + List buffer; + Mutex buffer_mutex; + bool flushing = false; + } data; + + void _try_accumulate(uint64_t p_timestamp); + +public: + void accumulate_or_push_event(Ref p_event, uint64_t p_timestamp); + void push_event(Ref p_event, uint64_t p_timestamp); + void flush_events(uint64_t p_current_timestamp, InputDefault &r_input_handler, bool p_accumulate); +}; + class InputDefault : public Input { GDCLASS(InputDefault, Input); _THREAD_SAFE_CLASS_ + friend class InputEventBuffer; int mouse_button_mask; @@ -200,11 +228,9 @@ class InputDefault : public Input { void _button_event(int p_device, int p_index, bool p_pressed); void _axis_event(int p_device, int p_axis, float p_value); - void _parse_input_event_impl(const Ref &p_event, bool p_is_emulated); + void _parse_input_event_impl(const Ref &p_event, bool p_is_emulated, bool p_unlock); - List> buffered_events; - bool use_input_buffering; - bool use_accumulated_input; + InputEventBuffer _event_buffer; #ifdef DEBUG_ENABLED Set> frame_parsed_events; @@ -305,11 +331,11 @@ class InputDefault : public Input { String get_joy_guid_remapped(int p_device) const; void set_fallback_mapping(String p_guid); - virtual void flush_buffered_events(); - virtual bool is_using_input_buffering(); - virtual void set_use_input_buffering(bool p_enable); - virtual bool is_using_accumulated_input(); - virtual void set_use_accumulated_input(bool p_enable); + void flush_buffered_events_ex(uint64_t p_up_to_timestamp); + virtual void force_flush_buffered_events(); + virtual void flush_buffered_events_iteration(); + virtual void flush_buffered_events_tick(uint64_t p_tick_timestamp); + virtual void flush_buffered_events_frame(); virtual void release_pressed_events(); InputDefault(); diff --git a/main/main.cpp b/main/main.cpp index 52284788d5d5..d64f551b03a2 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1544,7 +1544,8 @@ Error Main::setup2(Thread::ID p_main_tid_override) { InputDefault *id = Object::cast_to(Input::get_singleton()); if (id) { - agile_input_event_flushing = GLOBAL_DEF("input_devices/buffering/agile_event_flushing", false); + id->set_use_agile_flushing(GLOBAL_DEF("input_devices/buffering/agile_event_flushing", true)); + id->set_use_legacy_flushing(GLOBAL_DEF("input_devices/buffering/legacy_event_flushing", false)); if (bool(GLOBAL_DEF("input_devices/pointing/emulate_touch_from_mouse", false)) && !(editor || project_manager)) { if (!OS::get_singleton()->has_touchscreen_ui_hint()) { @@ -2242,7 +2243,6 @@ uint32_t Main::hide_print_fps_attempts = 3; uint32_t Main::frame = 0; bool Main::force_redraw_requested = false; int Main::iterating = 0; -bool Main::agile_input_event_flushing = false; bool Main::is_iterating() { return iterating > 0; @@ -2311,15 +2311,18 @@ bool Main::iteration() { advance.physics_steps = max_physics_steps; } + // Ensure with frame input buffering, that input is flushed prior to ticks, + // so reasonably up to date input is available in both ticks and frame. + InputDefault::get_singleton()->flush_buffered_events_iteration(); + bool exit = false; for (int iters = 0; iters < advance.physics_steps; ++iters) { - if (InputDefault::get_singleton()->is_using_input_buffering() && agile_input_event_flushing) { - InputDefault::get_singleton()->flush_buffered_events(); - } - Engine::get_singleton()->_in_physics = true; + // With agile input buffering, new input is potentially available per tick. + InputDefault::get_singleton()->flush_buffered_events_tick(advance.get_logical_tick_time(iters)); + uint64_t physics_begin = OS::get_singleton()->get_ticks_usec(); PhysicsServer::get_singleton()->flush_queries(); @@ -2358,9 +2361,8 @@ bool Main::iteration() { Engine::get_singleton()->_in_physics = false; } - if (InputDefault::get_singleton()->is_using_input_buffering() && agile_input_event_flushing) { - InputDefault::get_singleton()->flush_buffered_events(); - } + // Flush any remaining buffered input that has not been flushed already. + InputDefault::get_singleton()->flush_buffered_events_frame(); uint64_t idle_begin = OS::get_singleton()->get_ticks_usec(); @@ -2448,10 +2450,9 @@ bool Main::iteration() { iterating--; - // Needed for OSs using input buffering regardless accumulation (like Android) - if (InputDefault::get_singleton()->is_using_input_buffering() && !agile_input_event_flushing) { - InputDefault::get_singleton()->flush_buffered_events(); - } + // This is only included for legacy compatibility. + // Not clear whether this is a sensible time to flush input. + InputDefault::get_singleton()->flush_buffered_events_post_frame(); if (fixed_fps != -1) { return exit; diff --git a/main/main.h b/main/main.h index f18d62457ed3..ba8a3c622842 100644 --- a/main/main.h +++ b/main/main.h @@ -46,7 +46,6 @@ class Main { static uint32_t frame; static bool force_redraw_requested; static int iterating; - static bool agile_input_event_flushing; public: static bool is_project_manager(); diff --git a/main/main_timer_sync.cpp b/main/main_timer_sync.cpp index d488e4c536ec..ea65c4cd9b23 100644 --- a/main/main_timer_sync.cpp +++ b/main/main_timer_sync.cpp @@ -518,5 +518,20 @@ void MainTimerSync::set_fixed_fps(int p_fixed_fps) { MainFrameTime MainTimerSync::advance(float p_frame_slice, int p_iterations_per_second) { float cpu_idle_step = get_cpu_idle_step(); - return advance_checked(p_frame_slice, p_iterations_per_second, cpu_idle_step); + MainFrameTime mft = advance_checked(p_frame_slice, p_iterations_per_second, cpu_idle_step); + + // Now backcalculate the logical timing of the first physics tick. + // This is used for processing input. + // It is approximate, but should be fine for input. + mft.usec_per_tick = 1000000 / p_iterations_per_second; + uint64_t leftover_usec = mft.interpolation_fraction * mft.usec_per_tick; + + // Note we are using the ACTUAL CPU time for this estimate, + // NOT the smoothed accumulated time. + // This is because the input timestamps are measured in realtime, + // and smoothed time / realtime can get out of sync. + mft.first_physics_tick_logical_time_usecs = current_cpu_ticks_usec; + mft.first_physics_tick_logical_time_usecs -= (mft.physics_steps * mft.usec_per_tick) + leftover_usec; + + return mft; } diff --git a/main/main_timer_sync.h b/main/main_timer_sync.h index f3459f5a5a3c..81d95ff548be 100644 --- a/main/main_timer_sync.h +++ b/main/main_timer_sync.h @@ -41,7 +41,13 @@ struct MainFrameTime { int physics_steps; // number of times to iterate the physics engine float interpolation_fraction; // fraction through the current physics tick + // Used for segmenting input processing to different physics ticks + // based on timestamps. + uint64_t first_physics_tick_logical_time_usecs = 0; + uint64_t usec_per_tick = 0; + void clamp_idle(float min_idle_step, float max_idle_step); + uint64_t get_logical_tick_time(uint32_t p_tick) const { return first_physics_tick_logical_time_usecs + (p_tick * usec_per_tick); } }; class MainTimerSync { diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 223cdcd020d6..b8f8a0501bae 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -227,6 +227,7 @@ Error OS_Android::initialize(const VideoMode &p_desired, int p_video_driver, int input = memnew(InputDefault); input->set_use_input_buffering(true); // Needed because events will come directly from the UI thread + input->set_has_input_thread(true); input->set_fallback_mapping(godot_java->get_input_fallback_mapping()); return OK; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index 9d4c03b983e8..a48f73ed5732 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -3415,6 +3415,7 @@ void _update_keyboard_layouts() { void OS_OSX::force_process_input() { process_events(); // get rid of pending events joypad_osx->process_joypads(); + input->force_flush_buffered_events(); } void OS_OSX::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 9f2c99c9816e..1096071199b7 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -3565,6 +3565,7 @@ void OS_Windows::swap_buffers() { void OS_Windows::force_process_input() { process_events(); // get rid of pending events + input->force_flush_buffered_events(); } void OS_Windows::run() { diff --git a/platform/x11/os_x11.cpp b/platform/x11/os_x11.cpp index 7ae24bff0929..9efb2d387d29 100644 --- a/platform/x11/os_x11.cpp +++ b/platform/x11/os_x11.cpp @@ -4011,6 +4011,7 @@ void OS_X11::force_process_input() { #ifdef JOYDEV_ENABLED joypad->process_joypads(); #endif + input->force_flush_buffered_events(); } void OS_X11::run() {