From 7213aada8d5fef72a93379a0d700bb1b678e4a70 Mon Sep 17 00:00:00 2001
From: neil4 <neilfore@hotmail.com>
Date: Tue, 10 Jan 2023 00:22:14 -0600
Subject: [PATCH] Add Preemptive Frames to Latency Settings (#14832)

---
 command.h                          |   7 +
 config.def.h                       |   2 +
 config.def.keybinds.h              |  21 ++
 configuration.c                    |   3 +
 configuration.h                    |   2 +
 input/input_defines.h              |   1 +
 intl/msg_hash_id.c                 |   4 +
 intl/msg_hash_lbl.h                |  16 ++
 intl/msg_hash_us.h                 |  68 +++++
 menu/cbs/menu_cbs_sublabel.c       |  20 ++
 menu/menu_displaylist.c            |  24 +-
 menu/menu_driver.c                 |   7 +
 menu/menu_setting.c                | 182 +++++++++++---
 msg_hash.h                         |  13 +
 network/netplay/netplay_frontend.c |   5 +
 retroarch.c                        | 107 ++++++--
 runloop.c                          | 384 +++++++++++++++++++++++++++++
 runloop.h                          |  36 ++-
 18 files changed, 855 insertions(+), 47 deletions(-)

diff --git a/command.h b/command.h
index 1be36000b6..fcced8bb95 100644
--- a/command.h
+++ b/command.h
@@ -261,6 +261,12 @@ enum event_command
    CMD_EVENT_BSV_RECORDING_TOGGLE,
    /* Toggle Run-Ahead. */
    CMD_EVENT_RUNAHEAD_TOGGLE,
+   /* Toggle Preemtive Frames. */
+   CMD_EVENT_PREEMPT_TOGGLE,
+   /* Deinitialize or Reinitialize Preemptive Frames. */
+   CMD_EVENT_PREEMPT_UPDATE,
+   /* Force Preemptive Frames to refill its state buffer. */
+   CMD_EVENT_PREEMPT_RESET_BUFFER,
    /* Toggle VRR runloop. */
    CMD_EVENT_VRR_RUNLOOP_TOGGLE,
    /* AI service. */
@@ -505,6 +511,7 @@ static const struct cmd_map map[] = {
 
    { "VRR_RUNLOOP_TOGGLE",     RARCH_VRR_RUNLOOP_TOGGLE },
    { "RUNAHEAD_TOGGLE",        RARCH_RUNAHEAD_TOGGLE },
+   { "PREEMPT_TOGGLE",         RARCH_PREEMPT_TOGGLE },
    { "FPS_TOGGLE",             RARCH_FPS_TOGGLE },
    { "STATISTICS_TOGGLE",      RARCH_STATISTICS_TOGGLE },
    { "AI_SERVICE",             RARCH_AI_SERVICE },
diff --git a/config.def.h b/config.def.h
index 2dcbbb2316..60edc9c7b3 100644
--- a/config.def.h
+++ b/config.def.h
@@ -1299,6 +1299,8 @@
 
 /* Hide warning messages when using the Run Ahead feature. */
 #define DEFAULT_RUN_AHEAD_HIDE_WARNINGS false
+/* Hide warning messages when using Preemptive Frames. */
+#define DEFAULT_PREEMPT_HIDE_WARNINGS   false
 
 /* Enable stdin/network command interface. */
 #define DEFAULT_NETWORK_CMD_ENABLE false
diff --git a/config.def.keybinds.h b/config.def.keybinds.h
index 1f37b95990..eaa274a549 100644
--- a/config.def.keybinds.h
+++ b/config.def.keybinds.h
@@ -543,6 +543,13 @@ static const struct retro_keybind retro_keybinds_1[] = {
       RARCH_RUNAHEAD_TOGGLE, NO_BTN, NO_BTN, 0,
       true
    },
+   {
+      NULL, NULL,
+      AXIS_NONE, AXIS_NONE, AXIS_NONE,
+      MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE, RETROK_UNKNOWN,
+      RARCH_PREEMPT_TOGGLE, NO_BTN, NO_BTN, 0,
+      true
+   },
    {
       NULL, NULL,
       AXIS_NONE, AXIS_NONE, AXIS_NONE,
@@ -1143,6 +1150,13 @@ static const struct retro_keybind retro_keybinds_1[] = {
       RARCH_RUNAHEAD_TOGGLE, NO_BTN, NO_BTN, 0,
       true
    },
+   {
+      NULL, NULL,
+      AXIS_NONE, AXIS_NONE, AXIS_NONE,
+      MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE, RETROK_UNKNOWN,
+      RARCH_PREEMPT_TOGGLE, NO_BTN, NO_BTN, 0,
+      true
+   },
    {
       NULL, NULL,
       AXIS_NONE, AXIS_NONE, AXIS_NONE,
@@ -1753,6 +1767,13 @@ static const struct retro_keybind retro_keybinds_1[] = {
       RARCH_RUNAHEAD_TOGGLE, NO_BTN, NO_BTN, 0,
       true
    },
+   {
+      NULL, NULL,
+      AXIS_NONE, AXIS_NONE, AXIS_NONE,
+      MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE, RETROK_UNKNOWN,
+      RARCH_PREEMPT_TOGGLE, NO_BTN, NO_BTN, 0,
+      true
+   },
    {
       NULL, NULL,
       AXIS_NONE, AXIS_NONE, AXIS_NONE,
diff --git a/configuration.c b/configuration.c
index c88271b1d1..bbad0dce67 100644
--- a/configuration.c
+++ b/configuration.c
@@ -357,6 +357,7 @@ const struct input_bind_map input_config_bind_map[RARCH_BIND_LIST_END_NULL] = {
 
    DECLARE_META_BIND(2, toggle_vrr_runloop,    RARCH_VRR_RUNLOOP_TOGGLE,     MENU_ENUM_LABEL_VALUE_INPUT_META_VRR_RUNLOOP_TOGGLE),
    DECLARE_META_BIND(2, runahead_toggle,       RARCH_RUNAHEAD_TOGGLE,        MENU_ENUM_LABEL_VALUE_INPUT_META_RUNAHEAD_TOGGLE),
+   DECLARE_META_BIND(2, preempt_toggle,        RARCH_PREEMPT_TOGGLE,         MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE),
    DECLARE_META_BIND(2, fps_toggle,            RARCH_FPS_TOGGLE,             MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE),
    DECLARE_META_BIND(2, toggle_statistics,     RARCH_STATISTICS_TOGGLE,      MENU_ENUM_LABEL_VALUE_INPUT_META_STATISTICS_TOGGLE),
    DECLARE_META_BIND(2, ai_service,            RARCH_AI_SERVICE,             MENU_ENUM_LABEL_VALUE_INPUT_META_AI_SERVICE),
@@ -1686,6 +1687,8 @@ static struct config_bool_setting *populate_settings_bool(
    SETTING_BOOL("run_ahead_enabled",             &settings->bools.run_ahead_enabled, true, false, false);
    SETTING_BOOL("run_ahead_secondary_instance",  &settings->bools.run_ahead_secondary_instance, true, DEFAULT_RUN_AHEAD_SECONDARY_INSTANCE, false);
    SETTING_BOOL("run_ahead_hide_warnings",       &settings->bools.run_ahead_hide_warnings, true, DEFAULT_RUN_AHEAD_HIDE_WARNINGS, false);
+   SETTING_BOOL("preemptive_frames_enable",      &settings->bools.preemptive_frames_enable, true, false, false);
+   SETTING_BOOL("preemptive_frames_hide_warnings", &settings->bools.preemptive_frames_hide_warnings, true, DEFAULT_PREEMPT_HIDE_WARNINGS, false);
    SETTING_BOOL("audio_sync",                    &settings->bools.audio_sync, true, DEFAULT_AUDIO_SYNC, false);
    SETTING_BOOL("video_shader_enable",           &settings->bools.video_shader_enable, true, DEFAULT_SHADER_ENABLE, false);
    SETTING_BOOL("video_shader_watch_files",      &settings->bools.video_shader_watch_files, true, DEFAULT_VIDEO_SHADER_WATCH_FILES, false);
diff --git a/configuration.h b/configuration.h
index 25d8f8525b..e8cb2d6aef 100644
--- a/configuration.h
+++ b/configuration.h
@@ -885,6 +885,8 @@ typedef struct settings
       bool run_ahead_enabled;
       bool run_ahead_secondary_instance;
       bool run_ahead_hide_warnings;
+      bool preemptive_frames_enable;
+      bool preemptive_frames_hide_warnings;
       bool pause_nonactive;
       bool pause_on_disconnect;
       bool block_sram_overwrite;
diff --git a/input/input_defines.h b/input/input_defines.h
index 0280cd114d..fa888a40db 100644
--- a/input/input_defines.h
+++ b/input/input_defines.h
@@ -120,6 +120,7 @@ enum
 
    RARCH_VRR_RUNLOOP_TOGGLE,
    RARCH_RUNAHEAD_TOGGLE,
+   RARCH_PREEMPT_TOGGLE,
    RARCH_FPS_TOGGLE,
    RARCH_STATISTICS_TOGGLE,
    RARCH_AI_SERVICE,
diff --git a/intl/msg_hash_id.c b/intl/msg_hash_id.c
index 8139863050..9e7dd94592 100644
--- a/intl/msg_hash_id.c
+++ b/intl/msg_hash_id.c
@@ -235,6 +235,10 @@ int msg_hash_get_help_id_enum(enum msg_hash_enums msg, char *s, size_t len)
              snprintf(s, len,
                    "Toggles Run-Ahead mode on/off.");
              break;
+          case RARCH_PREEMPT_TOGGLE:
+             snprintf(s, len,
+                   "Toggles Preemptive Frames on/off.");
+             break;
           default:
              if (string_is_empty(s))
                 strlcpy(s, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_INFORMATION_AVAILABLE), len);
diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h
index b9f6433342..57eb55f63b 100644
--- a/intl/msg_hash_lbl.h
+++ b/intl/msg_hash_lbl.h
@@ -3266,6 +3266,22 @@ MSG_HASH(
    MENU_ENUM_LABEL_RUN_AHEAD_FRAMES,
    "run_ahead_frames"
    )
+MSG_HASH(
+   MENU_ENUM_LABEL_PREEMPT_ENABLE,
+   "preemptive_frames_enable"
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_PREEMPT_UNSUPPORTED,
+   "preemptive_frames_unsupported"
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_PREEMPT_HIDE_WARNINGS,
+   "preemptive_frames_hide_warnings"
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_PREEMPT_FRAMES,
+   "preemptive_frames"
+   )
 MSG_HASH(
    MENU_ENUM_LABEL_SORT_SAVEFILES_ENABLE,
    "sort_savefiles_enable"
diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h
index 64ed225470..ea9dd9837a 100644
--- a/intl/msg_hash_us.h
+++ b/intl/msg_hash_us.h
@@ -3391,6 +3391,14 @@ MSG_HASH(
    MENU_ENUM_SUBLABEL_INPUT_META_RUNAHEAD_TOGGLE,
    "Switches Run-Ahead on/off."
    )
+MSG_HASH(
+   MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE,
+   "Preemptive Frames (Toggle)"
+   )
+MSG_HASH(
+   MENU_ENUM_SUBLABEL_INPUT_META_PREEMPT_TOGGLE,
+   "Switches Preemptive Frames on/off."
+   )
 
 MSG_HASH(
    MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE,
@@ -3720,6 +3728,38 @@ MSG_HASH(
    MENU_ENUM_SUBLABEL_RUN_AHEAD_HIDE_WARNINGS,
    "Hide the warning message that appears when using Run-Ahead and the core does not support save states."
    )
+MSG_HASH(
+   MENU_ENUM_LABEL_VALUE_PREEMPT_UNSUPPORTED,
+   "[Preemptive Frames Unavailable]"
+   )
+MSG_HASH(
+   MENU_ENUM_SUBLABEL_PREEMPT_UNSUPPORTED,
+   "Current core is incompatible with preemptive frames due to lack of deterministic save state support."
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_VALUE_PREEMPT_ENABLE,
+   "Run Preemptive Frames"
+   )
+MSG_HASH(
+   MENU_ENUM_SUBLABEL_PREEMPT_ENABLE,
+   "Rerun core logic with the latest input when the controller state changes. Faster than Run-Ahead, but does not prevent audio issues cores may have with loading states."
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_VALUE_PREEMPT_FRAMES,
+   "Number of Preemptive Frames"
+   )
+MSG_HASH(
+   MENU_ENUM_SUBLABEL_PREEMPT_FRAMES,
+   "The number of frames to rerun. Causes gameplay issues such as jitter if the number of lag frames internal to the game is exceeded."
+   )
+MSG_HASH(
+   MENU_ENUM_LABEL_VALUE_PREEMPT_HIDE_WARNINGS,
+   "Hide Preemptive Frames Warnings"
+   )
+MSG_HASH(
+   MENU_ENUM_SUBLABEL_PREEMPT_HIDE_WARNINGS,
+   "Hide the warning message that appears when a core is incompatible with preemptive frames."
+   )
 
 /* Settings > Core */
 
@@ -13902,6 +13942,34 @@ MSG_HASH(
    MSG_RUNAHEAD_FAILED_TO_CREATE_SECONDARY_INSTANCE,
    "Failed to create second instance. Run-Ahead will now use only one instance."
    )
+MSG_HASH(
+   MSG_PREEMPT_ENABLED,
+   "Preemptive Frames enabled. Latency frames removed: %u."
+   )
+MSG_HASH(
+   MSG_PREEMPT_DISABLED,
+   "Preemptive Frames disabled."
+   )
+MSG_HASH(
+   MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_SAVESTATES,
+   "Preemptive Frames has been disabled because this core does not support save states."
+   )
+MSG_HASH(
+   MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_PREEMPT,
+   "Preemptive Frames unavailable because this core lacks deterministic save state support."
+   )
+MSG_HASH(
+   MSG_PREEMPT_FAILED_TO_ALLOCATE,
+   "Failed to allocate memory for Preemptive Frames."
+   )
+MSG_HASH(
+   MSG_PREEMPT_FAILED_TO_SAVE_STATE,
+   "Failed to save state. Preemptive Frames has been disabled."
+   )
+MSG_HASH(
+   MSG_PREEMPT_FAILED_TO_LOAD_STATE,
+   "Failed to load state. Preemptive Frames has been disabled."
+   )
 MSG_HASH(
    MSG_SCANNING_OF_FILE_FINISHED,
    "Scanning of file finished"
diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c
index 7aed2f6a56..750b821ed4 100644
--- a/menu/cbs/menu_cbs_sublabel.c
+++ b/menu/cbs/menu_cbs_sublabel.c
@@ -433,6 +433,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_ui_companion_toggle,   ME
 
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_vrr_runloop_toggle,    MENU_ENUM_SUBLABEL_INPUT_META_VRR_RUNLOOP_TOGGLE)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_runahead_toggle,       MENU_ENUM_SUBLABEL_INPUT_META_RUNAHEAD_TOGGLE)
+DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_preempt_toggle,        MENU_ENUM_SUBLABEL_INPUT_META_PREEMPT_TOGGLE)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_fps_toggle,            MENU_ENUM_SUBLABEL_INPUT_META_FPS_TOGGLE)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_statistics_toggle,     MENU_ENUM_SUBLABEL_INPUT_META_STATISTICS_TOGGLE)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_meta_ai_service,            MENU_ENUM_SUBLABEL_INPUT_META_AI_SERVICE)
@@ -667,6 +668,10 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_run_ahead_enabled,             MENU_
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_run_ahead_secondary_instance,  MENU_ENUM_SUBLABEL_RUN_AHEAD_SECONDARY_INSTANCE)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_run_ahead_hide_warnings,       MENU_ENUM_SUBLABEL_RUN_AHEAD_HIDE_WARNINGS)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_run_ahead_frames,              MENU_ENUM_SUBLABEL_RUN_AHEAD_FRAMES)
+DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_preempt_unsupported,           MENU_ENUM_SUBLABEL_PREEMPT_UNSUPPORTED)
+DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_preempt_enable,                MENU_ENUM_SUBLABEL_PREEMPT_ENABLE)
+DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_preempt_hide_warnings,         MENU_ENUM_SUBLABEL_PREEMPT_HIDE_WARNINGS)
+DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_preempt_frames,                MENU_ENUM_SUBLABEL_PREEMPT_FRAMES)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_block_timeout,           MENU_ENUM_SUBLABEL_INPUT_BLOCK_TIMEOUT)
 DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_rewind,                        MENU_ENUM_SUBLABEL_REWIND_ENABLE)
 #ifdef HAVE_CHEATS
@@ -2254,6 +2259,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
             case RARCH_RUNAHEAD_TOGGLE:
                BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_runahead_toggle);
                return 0;
+            case RARCH_PREEMPT_TOGGLE:
+               BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_preempt_toggle);
+               return 0;
             case RARCH_FPS_TOGGLE:
                BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_meta_fps_toggle);
                return 0;
@@ -3831,6 +3839,18 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
          case MENU_ENUM_LABEL_RUN_AHEAD_FRAMES:
             BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_run_ahead_frames);
             break;
+         case MENU_ENUM_LABEL_PREEMPT_UNSUPPORTED:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_preempt_unsupported);
+            break;
+         case MENU_ENUM_LABEL_PREEMPT_ENABLE:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_preempt_enable);
+            break;
+         case MENU_ENUM_LABEL_PREEMPT_FRAMES:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_preempt_frames);
+            break;
+         case MENU_ENUM_LABEL_PREEMPT_HIDE_WARNINGS:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_preempt_hide_warnings);
+            break;
          case MENU_ENUM_LABEL_INPUT_BLOCK_TIMEOUT:
             BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_block_timeout);
             break;
diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c
index 85f4c45a57..e5d9b1acb7 100644
--- a/menu/menu_displaylist.c
+++ b/menu/menu_displaylist.c
@@ -9265,6 +9265,7 @@ unsigned menu_displaylist_build_list(
 #ifdef HAVE_RUNAHEAD
             bool runahead_supported       = true;
             bool runahead_enabled         = settings->bools.run_ahead_enabled;
+            bool preempt_enabled          = settings->bools.preemptive_frames_enable;
 #endif
             menu_displaylist_build_info_selective_t build_list[] = {
                {MENU_ENUM_LABEL_VIDEO_FRAME_DELAY,                     PARSE_ONLY_UINT, true },
@@ -9277,6 +9278,9 @@ unsigned menu_displaylist_build_list(
                {MENU_ENUM_LABEL_RUN_AHEAD_FRAMES,                      PARSE_ONLY_UINT, false },
                {MENU_ENUM_LABEL_RUN_AHEAD_SECONDARY_INSTANCE,          PARSE_ONLY_BOOL, false },
                {MENU_ENUM_LABEL_RUN_AHEAD_HIDE_WARNINGS,               PARSE_ONLY_BOOL, false },
+               {MENU_ENUM_LABEL_PREEMPT_ENABLE,                        PARSE_ONLY_BOOL, false },
+               {MENU_ENUM_LABEL_PREEMPT_FRAMES,                        PARSE_ONLY_UINT, false },
+               {MENU_ENUM_LABEL_PREEMPT_HIDE_WARNINGS,                 PARSE_ONLY_BOOL, false },
 #endif
             };
 
@@ -9330,6 +9334,7 @@ unsigned menu_displaylist_build_list(
                   switch (build_list[i].enum_idx)
                   {
                      case MENU_ENUM_LABEL_RUN_AHEAD_ENABLED:
+                     case MENU_ENUM_LABEL_PREEMPT_ENABLE:
                         build_list[i].checked = true;
                         break;
                      case MENU_ENUM_LABEL_RUN_AHEAD_FRAMES:
@@ -9338,6 +9343,11 @@ unsigned menu_displaylist_build_list(
                         if (runahead_enabled)
                            build_list[i].checked = true;
                         break;
+                     case MENU_ENUM_LABEL_PREEMPT_FRAMES:
+                     case MENU_ENUM_LABEL_PREEMPT_HIDE_WARNINGS:
+                        if (preempt_enabled)
+                           build_list[i].checked = true;
+                        break;
                      default:
                         break;
                   }
@@ -9356,13 +9366,21 @@ unsigned menu_displaylist_build_list(
             }
 
 #ifdef HAVE_RUNAHEAD
-            if (!runahead_supported &&
-                menu_entries_append(list,
+            if (!runahead_supported)
+            {
+               if (menu_entries_append(list,
                      msg_hash_to_str(MENU_ENUM_LABEL_VALUE_RUN_AHEAD_UNSUPPORTED),
                      msg_hash_to_str(MENU_ENUM_LABEL_RUN_AHEAD_UNSUPPORTED),
                      MENU_ENUM_LABEL_RUN_AHEAD_UNSUPPORTED,
                      FILE_TYPE_NONE, 0, 0, NULL))
-               count++;
+                  count++;
+                if (menu_entries_append(list,
+                     msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PREEMPT_UNSUPPORTED),
+                     msg_hash_to_str(MENU_ENUM_LABEL_PREEMPT_UNSUPPORTED),
+                     MENU_ENUM_LABEL_PREEMPT_UNSUPPORTED,
+                     FILE_TYPE_NONE, 0, 0, NULL))
+                  count++;
+            }
 #endif
             if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list,
                      MENU_ENUM_LABEL_GAMEMODE_ENABLE, PARSE_ONLY_BOOL, false) == 0)
diff --git a/menu/menu_driver.c b/menu/menu_driver.c
index bb3bd8c622..5e29fb499c 100644
--- a/menu/menu_driver.c
+++ b/menu/menu_driver.c
@@ -6982,6 +6982,13 @@ void retroarch_menu_running_finished(bool quit)
             command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &game_focus_cmd);
          }
       }
+
+#if HAVE_RUNAHEAD
+      /* Preemptive Frames isn't run behind the menu,
+       * so its savestate buffer is out of date. */
+      if (!settings->bools.menu_pause_libretro)
+         command_event(CMD_EVENT_PREEMPT_RESET_BUFFER, NULL);
+#endif
    }
 
    /* Ensure that menu screensaver is disabled when
diff --git a/menu/menu_setting.c b/menu/menu_setting.c
index e3d8f01d8c..3f97bd3408 100644
--- a/menu/menu_setting.c
+++ b/menu/menu_setting.c
@@ -8654,31 +8654,6 @@ static void general_write_handler(rarch_setting_t *setting)
                            default_aspect;
          }
          break;
-#if defined(HAVE_RUNAHEAD) && (defined(HAVE_DYNAMIC) || defined(HAVE_DYLIB))
-      case MENU_ENUM_LABEL_RUN_AHEAD_ENABLED:
-      case MENU_ENUM_LABEL_RUN_AHEAD_FRAMES:
-      case MENU_ENUM_LABEL_RUN_AHEAD_SECONDARY_INSTANCE:
-         {
-            settings_t *settings              = config_get_ptr();
-            bool run_ahead_enabled            = settings->bools.run_ahead_enabled;
-            unsigned run_ahead_frames         = settings->uints.run_ahead_frames;
-            bool run_ahead_secondary_instance = settings->bools.run_ahead_secondary_instance;
-
-            /* If any changes here will cause second
-             * instance runahead to be enabled, must
-             * re-apply cheats to ensure that they
-             * propagate to the newly-created secondary
-             * core */
-            if (run_ahead_enabled &&
-                (run_ahead_frames > 0) &&
-                run_ahead_secondary_instance &&
-                !retroarch_ctl(RARCH_CTL_IS_SECOND_CORE_LOADED, NULL) &&
-                retroarch_ctl(RARCH_CTL_IS_SECOND_CORE_AVAILABLE, NULL) &&
-                command_event(CMD_EVENT_LOAD_SECOND_CORE, NULL))
-               command_event(CMD_EVENT_CHEATS_APPLY, NULL);
-         }
-         break;
-#endif
       default:
          /* Special cases */
 
@@ -8713,6 +8688,104 @@ static void frontend_log_level_change_handler(rarch_setting_t *setting)
    verbosity_set_log_level(*setting->value.target.unsigned_integer);
 }
 
+#ifdef HAVE_RUNAHEAD
+static void runahead_change_handler(rarch_setting_t *setting)
+{
+   settings_t *settings              = config_get_ptr();
+   bool run_ahead_enabled            = settings->bools.run_ahead_enabled;
+   bool preempt_enabled              = settings->bools.preemptive_frames_enable;
+   bool refresh                      = false;
+#if (defined(HAVE_DYNAMIC) || defined(HAVE_DYLIB))
+   unsigned run_ahead_frames         = settings->uints.run_ahead_frames;
+   bool run_ahead_secondary_instance = settings->bools.run_ahead_secondary_instance;
+#endif
+
+   if (!setting)
+      return;
+
+   switch (setting->enum_idx)
+   {
+      case MENU_ENUM_LABEL_RUN_AHEAD_ENABLED:
+         if (run_ahead_enabled && preempt_enabled)
+         {
+            /* Disable preemptive frames and inform user */
+            settings->bools.preemptive_frames_enable = false;
+            runloop_preempt_deinit();
+            runloop_msg_queue_push(
+                  msg_hash_to_str(MSG_PREEMPT_DISABLED), 1, 100, false,
+                  NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+         }
+         menu_entries_ctl(MENU_ENTRIES_CTL_SET_REFRESH, &refresh);
+         /* fall-through */
+      case MENU_ENUM_LABEL_RUN_AHEAD_FRAMES:
+#if (defined(HAVE_DYNAMIC) || defined(HAVE_DYLIB))
+      case MENU_ENUM_LABEL_RUN_AHEAD_SECONDARY_INSTANCE:
+         /* If any changes here will cause second
+          * instance runahead to be enabled, must
+          * re-apply cheats to ensure that they
+          * propagate to the newly-created secondary
+          * core */
+         if (     run_ahead_enabled
+               && (run_ahead_frames > 0)
+               && run_ahead_secondary_instance
+               && !retroarch_ctl(RARCH_CTL_IS_SECOND_CORE_LOADED, NULL)
+               && retroarch_ctl(RARCH_CTL_IS_SECOND_CORE_AVAILABLE, NULL)
+               && command_event(CMD_EVENT_LOAD_SECOND_CORE, NULL))
+            command_event(CMD_EVENT_CHEATS_APPLY, NULL);
+#endif
+         break;
+      default:
+         break;
+   }
+}
+
+static void preempt_change_handler(rarch_setting_t *setting)
+{
+   settings_t *settings   = config_get_ptr();
+   bool preempt_enabled   = settings->bools.preemptive_frames_enable;
+   bool run_ahead_enabled = settings->bools.run_ahead_enabled;
+   preempt_t *preempt     = runloop_state_get_ptr()->preempt_data;
+   bool refresh           = false;
+   bool netplay_enabled;
+
+#ifdef HAVE_NETWORKING
+   netplay_enabled = netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL);
+#else
+   netplay_enabled = false;
+#endif
+
+   if (!setting)
+      return;
+
+   switch (setting->enum_idx)
+   {
+      case MENU_ENUM_LABEL_PREEMPT_ENABLE:
+         if (preempt_enabled && run_ahead_enabled)
+         {
+            /* Disable runahead and inform user */
+            settings->bools.run_ahead_enabled = false;
+            runloop_msg_queue_push(
+                  msg_hash_to_str(MSG_RUNAHEAD_DISABLED), 1, 100, false,
+                  NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+         }
+
+         if ((preempt_enabled != !!preempt) && !netplay_enabled)
+            command_event(CMD_EVENT_PREEMPT_UPDATE, NULL);
+
+         menu_entries_ctl(MENU_ENTRIES_CTL_SET_REFRESH, &refresh);
+         break;
+      case MENU_ENUM_LABEL_PREEMPT_FRAMES:
+         if (     preempt
+               && preempt->frames != settings->uints.run_ahead_frames
+               && !netplay_enabled)
+            command_event(CMD_EVENT_PREEMPT_UPDATE, NULL);
+         break;
+      default:
+         break;
+   }
+}
+#endif
+
 #ifdef HAVE_OVERLAY
 static void overlay_enable_toggle_change_handler(rarch_setting_t *setting)
 {
@@ -14793,6 +14866,7 @@ static bool setting_append_list(
          (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
          menu_settings_list_current_add_range(list, list_info, 1, 10, 0.1, true, true);
 
+#ifdef HAVE_RUNAHEAD
          CONFIG_BOOL(
                list, list_info,
                &settings->bools.run_ahead_enabled,
@@ -14808,9 +14882,7 @@ static bool setting_append_list(
                general_read_handler,
                SD_FLAG_NONE
                );
-         (*list)[list_info->index - 1].action_ok     = setting_bool_action_left_with_refresh;
-         (*list)[list_info->index - 1].action_left   = setting_bool_action_left_with_refresh;
-         (*list)[list_info->index - 1].action_right  = setting_bool_action_right_with_refresh;
+         (*list)[list_info->index - 1].change_handler = runahead_change_handler;
 
          CONFIG_UINT(
             list, list_info,
@@ -14826,7 +14898,8 @@ static bool setting_append_list(
          (*list)[list_info->index - 1].ui_type   = ST_UI_TYPE_UINT_COMBOBOX;
          (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
          (*list)[list_info->index - 1].offset_by = 1;
-         menu_settings_list_current_add_range(list, list_info, 1, 12, 1, true, true);
+         (*list)[list_info->index - 1].change_handler = runahead_change_handler;
+         menu_settings_list_current_add_range(list, list_info, 1, MAX_RUNAHEAD_FRAMES, 1, true, true);
 
 #if defined(HAVE_DYNAMIC) || defined(HAVE_DYLIB)
          CONFIG_BOOL(
@@ -14844,6 +14917,7 @@ static bool setting_append_list(
                general_read_handler,
                SD_FLAG_NONE
                );
+         (*list)[list_info->index - 1].change_handler = runahead_change_handler;
 #endif
 
          CONFIG_BOOL(
@@ -14862,6 +14936,56 @@ static bool setting_append_list(
                SD_FLAG_ADVANCED
                );
 
+         CONFIG_BOOL(
+               list, list_info,
+               &settings->bools.preemptive_frames_enable,
+               MENU_ENUM_LABEL_PREEMPT_ENABLE,
+               MENU_ENUM_LABEL_VALUE_PREEMPT_ENABLE,
+               false,
+               MENU_ENUM_LABEL_VALUE_OFF,
+               MENU_ENUM_LABEL_VALUE_ON,
+               &group_info,
+               &subgroup_info,
+               parent_group,
+               general_write_handler,
+               general_read_handler,
+               SD_FLAG_NONE);
+         (*list)[list_info->index - 1].change_handler = preempt_change_handler;
+
+         CONFIG_UINT(
+               list, list_info,
+               &settings->uints.run_ahead_frames,
+               MENU_ENUM_LABEL_PREEMPT_FRAMES,
+               MENU_ENUM_LABEL_VALUE_PREEMPT_FRAMES,
+               1,
+               &group_info,
+               &subgroup_info,
+               parent_group,
+               general_write_handler,
+               general_read_handler);
+         (*list)[list_info->index - 1].ui_type   = ST_UI_TYPE_UINT_COMBOBOX;
+         (*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
+         (*list)[list_info->index - 1].offset_by = 1;
+         (*list)[list_info->index - 1].change_handler = preempt_change_handler;
+         menu_settings_list_current_add_range(list, list_info, 1, MAX_RUNAHEAD_FRAMES, 1, true, true);
+
+         CONFIG_BOOL(
+               list, list_info,
+               &settings->bools.preemptive_frames_hide_warnings,
+               MENU_ENUM_LABEL_PREEMPT_HIDE_WARNINGS,
+               MENU_ENUM_LABEL_VALUE_PREEMPT_HIDE_WARNINGS,
+               DEFAULT_PREEMPT_HIDE_WARNINGS,
+               MENU_ENUM_LABEL_VALUE_OFF,
+               MENU_ENUM_LABEL_VALUE_ON,
+               &group_info,
+               &subgroup_info,
+               parent_group,
+               general_write_handler,
+               general_read_handler,
+               SD_FLAG_ADVANCED
+               );
+#endif
+
 #ifdef ANDROID
          CONFIG_UINT(
             list, list_info,
diff --git a/msg_hash.h b/msg_hash.h
index 1e7b136b30..780005e296 100644
--- a/msg_hash.h
+++ b/msg_hash.h
@@ -520,6 +520,13 @@ enum msg_hash_enums
    MSG_RUNAHEAD_FAILED_TO_SAVE_STATE,
    MSG_RUNAHEAD_FAILED_TO_LOAD_STATE,
    MSG_RUNAHEAD_FAILED_TO_CREATE_SECONDARY_INSTANCE,
+   MSG_PREEMPT_ENABLED,
+   MSG_PREEMPT_DISABLED,
+   MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_SAVESTATES,
+   MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_PREEMPT,
+   MSG_PREEMPT_FAILED_TO_ALLOCATE,
+   MSG_PREEMPT_FAILED_TO_SAVE_STATE,
+   MSG_PREEMPT_FAILED_TO_LOAD_STATE,
    MSG_MISSING_ASSETS,
    MSG_RGUI_MISSING_FONTS,
    MSG_RGUI_INVALID_LANGUAGE,
@@ -1033,6 +1040,7 @@ enum msg_hash_enums
 
    MENU_ENUM_LABEL_VALUE_INPUT_META_VRR_RUNLOOP_TOGGLE,
    MENU_ENUM_LABEL_VALUE_INPUT_META_RUNAHEAD_TOGGLE,
+   MENU_ENUM_LABEL_VALUE_INPUT_META_PREEMPT_TOGGLE,
    MENU_ENUM_LABEL_VALUE_INPUT_META_FPS_TOGGLE,
    MENU_ENUM_LABEL_VALUE_INPUT_META_STATISTICS_TOGGLE,
    MENU_ENUM_LABEL_VALUE_INPUT_META_AI_SERVICE,
@@ -1109,6 +1117,7 @@ enum msg_hash_enums
 
    MENU_ENUM_SUBLABEL_INPUT_META_VRR_RUNLOOP_TOGGLE,
    MENU_ENUM_SUBLABEL_INPUT_META_RUNAHEAD_TOGGLE,
+   MENU_ENUM_SUBLABEL_INPUT_META_PREEMPT_TOGGLE,
    MENU_ENUM_SUBLABEL_INPUT_META_FPS_TOGGLE,
    MENU_ENUM_SUBLABEL_INPUT_META_STATISTICS_TOGGLE,
    MENU_ENUM_SUBLABEL_INPUT_META_AI_SERVICE,
@@ -2444,6 +2453,10 @@ enum msg_hash_enums
    MENU_LABEL(RUN_AHEAD_SECONDARY_INSTANCE),
    MENU_LABEL(RUN_AHEAD_HIDE_WARNINGS),
    MENU_LABEL(RUN_AHEAD_FRAMES),
+   MENU_LABEL(PREEMPT_UNSUPPORTED),
+   MENU_LABEL(PREEMPT_ENABLE),
+   MENU_LABEL(PREEMPT_FRAMES),
+   MENU_LABEL(PREEMPT_HIDE_WARNINGS),
    MENU_LABEL(INPUT_BLOCK_TIMEOUT),
    MENU_LABEL(TURBO),
 
diff --git a/network/netplay/netplay_frontend.c b/network/netplay/netplay_frontend.c
index 88011acb5d..1229c1b2c2 100644
--- a/network/netplay/netplay_frontend.c
+++ b/network/netplay/netplay_frontend.c
@@ -8435,6 +8435,11 @@ void deinit_netplay(void)
       net_st->data              = NULL;
       net_st->flags            &= ~(NET_DRIVER_ST_FLAG_NETPLAY_ENABLED
                                 |   NET_DRIVER_ST_FLAG_NETPLAY_IS_CLIENT);
+
+#if HAVE_RUNAHEAD
+      /* Reinitialize preemptive frames if enabled */
+      runloop_preempt_init();
+#endif
    }
 
    free(net_st->client_info);
diff --git a/retroarch.c b/retroarch.c
index 806a7445da..62c8a5da78 100644
--- a/retroarch.c
+++ b/retroarch.c
@@ -2228,10 +2228,8 @@ bool command_event(enum event_command cmd, void *data)
          }
          break;
       case CMD_EVENT_RUNAHEAD_TOGGLE:
+#if HAVE_RUNAHEAD
          {
-            char msg[256];
-            msg[0] = '\0';
-
             if (!core_info_current_supports_runahead())
             {
                runloop_msg_queue_push(msg_hash_to_str(MSG_RUNAHEAD_CORE_DOES_NOT_SUPPORT_RUNAHEAD),
@@ -2249,25 +2247,86 @@ bool command_event(enum event_command cmd, void *data)
                      1, 100, false,
                      NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
             }
-            else if (!settings->bools.run_ahead_secondary_instance)
-            {
-               snprintf(msg, sizeof(msg), msg_hash_to_str(MSG_RUNAHEAD_ENABLED),
-                     settings->uints.run_ahead_frames);
-
-               runloop_msg_queue_push(
-                     msg, 1, 100, false,
-                     NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
-            }
             else
             {
-               snprintf(msg, sizeof(msg), msg_hash_to_str(MSG_RUNAHEAD_ENABLED_WITH_SECOND_INSTANCE),
-                     settings->uints.run_ahead_frames);
+               char msg[256];
 
+               if (!settings->bools.run_ahead_secondary_instance)
+               {
+                  snprintf(msg, sizeof(msg), msg_hash_to_str(MSG_RUNAHEAD_ENABLED),
+                        settings->uints.run_ahead_frames);
+
+                  runloop_msg_queue_push(
+                        msg, 1, 100, false,
+                        NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+               }
+               else
+               {
+                  snprintf(msg, sizeof(msg), msg_hash_to_str(MSG_RUNAHEAD_ENABLED_WITH_SECOND_INSTANCE),
+                        settings->uints.run_ahead_frames);
+
+                  runloop_msg_queue_push(
+                        msg, 1, 100, false,
+                        NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+               }
+
+               /* Disable preemptive frames */
+               settings->bools.preemptive_frames_enable = false;
+               runloop_preempt_deinit();
+            }
+         }
+#endif
+         break;
+      case CMD_EVENT_PREEMPT_TOGGLE:
+#if HAVE_RUNAHEAD
+         {
+            bool old_warn   = settings->bools.preemptive_frames_hide_warnings;
+            bool old_inited = runloop_st->preempt_data;
+
+            /* Toggle with warnings shown */
+            settings->bools.preemptive_frames_hide_warnings = false;
+
+            settings->bools.preemptive_frames_enable =
+                  !(settings->bools.preemptive_frames_enable);
+            command_event(CMD_EVENT_PREEMPT_UPDATE, NULL);
+
+            settings->bools.preemptive_frames_hide_warnings = old_warn;
+
+            if (old_inited && !runloop_st->preempt_data)
+            {
+               runloop_msg_queue_push(msg_hash_to_str(MSG_PREEMPT_DISABLED),
+                     1, 100, false,
+                     NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+            }
+            else if (runloop_st->preempt_data)
+            {
+               char msg[256];
+
+               snprintf(msg, sizeof(msg), msg_hash_to_str(MSG_PREEMPT_ENABLED),
+                        settings->uints.run_ahead_frames);
                runloop_msg_queue_push(
                      msg, 1, 100, false,
                      NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+
+               /* Disable runahead */
+               settings->bools.run_ahead_enabled = false;
             }
+            else /* Failed to init */
+               settings->bools.preemptive_frames_enable = false;
          }
+#endif
+         break;
+      case CMD_EVENT_PREEMPT_UPDATE:
+#if HAVE_RUNAHEAD
+         runloop_preempt_deinit();
+         runloop_preempt_init();
+#endif
+         break;
+      case CMD_EVENT_PREEMPT_RESET_BUFFER:
+#if HAVE_RUNAHEAD
+         if (runloop_st->preempt_data)
+            runloop_st->preempt_data->frame_count = 0;
+#endif
          break;
       case CMD_EVENT_RECORDING_TOGGLE:
          if (recording_st->enable)
@@ -2381,6 +2440,10 @@ bool command_event(enum event_command cmd, void *data)
 #endif
             if (!command_event_main_state(cmd))
                return false;
+
+#if HAVE_RUNAHEAD
+            command_event(CMD_EVENT_PREEMPT_RESET_BUFFER, NULL);
+#endif
          }
          break;
       case CMD_EVENT_UNDO_LOAD_STATE:
@@ -2427,6 +2490,9 @@ bool command_event(enum event_command cmd, void *data)
          if (settings->bools.video_frame_delay_auto)
             video_st->frame_delay_target = 0;
 
+#if HAVE_RUNAHEAD
+         command_event(CMD_EVENT_PREEMPT_RESET_BUFFER, NULL);
+#endif
          return false;
       case CMD_EVENT_SAVE_STATE:
       case CMD_EVENT_SAVE_STATE_TO_RAM:
@@ -2977,6 +3043,9 @@ bool command_event(enum event_command cmd, void *data)
              * RetroArch */
             if (!(runloop_st->flags & RUNLOOP_FLAG_RUNAHEAD_AVAILABLE))
                runloop_runahead_clear_variables(runloop_st);
+
+            /* Deallocate preemptive frames */
+            runloop_preempt_deinit();
 #endif
 
             if (hwr)
@@ -3512,6 +3581,10 @@ bool command_event(enum event_command cmd, void *data)
 
                return false;
             }
+#if HAVE_RUNAHEAD
+            /* Deinit preemptive frames; not compatible with netplay */
+            runloop_preempt_deinit();
+#endif
          }
          break;
       case CMD_EVENT_NETPLAY_DISCONNECT:
@@ -6171,6 +6244,12 @@ bool retroarch_main_init(int argc, char *argv[])
    audio_driver_load_system_sounds();
 #endif
 
+#if defined(HAVE_RUNAHEAD) && defined(HAVE_NETWORKING)
+   if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL))
+#elif defined(HAVE_RUNAHEAD)
+      runloop_preempt_init();
+#endif
+
    return true;
 
 error:
diff --git a/runloop.c b/runloop.c
index de75bf677c..fb1ebf3ba2 100644
--- a/runloop.c
+++ b/runloop.c
@@ -4974,6 +4974,380 @@ force_input_dirty:
    core_run();
    runloop_st->flags |=  RUNLOOP_FLAG_RUNAHEAD_FORCE_INPUT_DIRTY;
 }
+
+/* Preemptive Frames */
+
+static int16_t preempt_input_state(unsigned port,
+      unsigned device, unsigned index, unsigned id)
+{
+   settings_t *settings         = config_get_ptr();
+   runloop_state_t *runloop_st  = &runloop_state;
+   preempt_t *preempt           = runloop_st->preempt_data;
+   unsigned device_class        = device & RETRO_DEVICE_MASK;
+   unsigned *port_map           = settings->uints.input_remap_port_map[port];
+   uint8_t p;
+
+   switch (device_class)
+   {
+      case RETRO_DEVICE_ANALOG:
+         /* Add requested inputs to mask */
+         while ((p = *(port_map++)) < MAX_USERS)
+            preempt->analog_mask[p] |= (1 << (id + index * 2));
+         break;
+      case RETRO_DEVICE_LIGHTGUN:
+      case RETRO_DEVICE_MOUSE:
+      case RETRO_DEVICE_POINTER:
+         /* Set pointing device for this port */
+         while ((p = *(port_map++)) < MAX_USERS)
+            preempt->ptr_dev[p] = device_class;
+         break;
+      default:
+         break;
+   }
+
+   return input_driver_state_wrapper(port, device, index, id);
+}
+
+static const char* preempt_allocate(const uint8_t frames)
+{
+   runloop_state_t *runloop_st = &runloop_state;
+   preempt_t *preempt          = (preempt_t*)calloc(1, sizeof(preempt_t));
+   retro_ctx_size_info_t info;
+   uint8_t i;
+
+   if (!(runloop_st->preempt_data = preempt))
+      return msg_hash_to_str(MSG_PREEMPT_FAILED_TO_ALLOCATE);
+
+   core_serialize_size_special(&info);
+   if (!info.size)
+      return msg_hash_to_str(MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_SAVESTATES);
+
+   preempt->state_size = info.size;
+   preempt->frames     = frames;
+
+   for (i = 0; i < frames; i++)
+   {
+      preempt->buffer[i] = malloc(preempt->state_size);
+      if (!preempt->buffer[i])
+         return msg_hash_to_str(MSG_PREEMPT_FAILED_TO_ALLOCATE);
+   }
+
+   return NULL;
+}
+
+/**
+ * runloop_preempt_init:
+ *
+ * @return true on success, false on failure
+ *
+ * Allocates savestate buffer and sets overrides for preemptive frames.
+ **/
+bool runloop_preempt_init(void)
+{
+   runloop_state_t *runloop_st = &runloop_state;
+   settings_t *settings        = config_get_ptr();
+   const char *failed_str      = NULL;
+
+   if (     runloop_st->preempt_data
+         || !settings->bools.preemptive_frames_enable
+         || !settings->uints.run_ahead_frames
+         || !(runloop_st->current_core.flags & RETRO_CORE_FLAG_GAME_LOADED))
+      return false;
+
+   /* Check if supported - same requirements as runahead */
+   if (!core_info_current_supports_runahead())
+   {
+      failed_str = msg_hash_to_str(MSG_PREEMPT_CORE_DOES_NOT_SUPPORT_PREEMPT);
+      goto error;
+   }
+
+   /* Set flags to block runahead and request 'same instance' states */
+   runloop_st->flags &= ~(RUNLOOP_FLAG_RUNAHEAD_AVAILABLE
+         | RUNLOOP_FLAG_RUNAHEAD_SECONDARY_CORE_AVAILABLE);
+
+   /* Allocate - same 'frames' setting as runahead */
+   if ((failed_str = preempt_allocate(settings->uints.run_ahead_frames)))
+      goto error;
+
+   /* Only poll in preempt_run() */
+   runloop_st->current_core.retro_set_input_poll(retro_input_poll_null);
+   /* Track requested analog states and pointing device types */
+   runloop_st->current_core.retro_set_input_state(preempt_input_state);
+
+   return true;
+
+error:
+   runloop_preempt_deinit();
+
+   if (!config_get_ptr()->bools.preemptive_frames_hide_warnings)
+      runloop_msg_queue_push(
+            failed_str, 0, 2 * 60, true,
+            NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+   RARCH_WARN("[Preemptive Frames]: %s\n", failed_str);
+
+   return false;
+}
+
+/**
+ * runloop_preempt_deinit:
+ *
+ * Frees preempt object and unsets overrides.
+ **/
+void runloop_preempt_deinit(void)
+{
+   runloop_state_t *runloop_st       = &runloop_state;
+   preempt_t *preempt                = runloop_st->preempt_data;
+   struct retro_core_t *current_core = &runloop_st->current_core;
+   size_t i;
+
+   if (preempt == NULL)
+      return;
+
+   /* Free memory */
+   for (i = 0; i < preempt->frames; i++)
+      free(preempt->buffer[i]);
+
+   free(preempt);
+   runloop_st->preempt_data = NULL;
+
+   /* Undo overrides */
+   runloop_st->flags |= (RUNLOOP_FLAG_RUNAHEAD_AVAILABLE
+         | RUNLOOP_FLAG_RUNAHEAD_SECONDARY_CORE_AVAILABLE);
+
+   if (current_core->retro_set_input_poll)
+      current_core->retro_set_input_poll(runloop_st->input_poll_callback_original);
+   if (current_core->retro_set_input_state)
+      current_core->retro_set_input_state(runloop_st->retro_ctx.state_cb);
+}
+
+static INLINE bool preempt_analog_input_dirty(preempt_t *preempt,
+      retro_input_state_t state_cb, unsigned port, unsigned mapped_port)
+{
+   int16_t state[20] = {0};
+   uint8_t base, i;
+
+   /* axes */
+   for (i = 0; i < 2; i++)
+   {
+      base = i * 2;
+      if (preempt->analog_mask[port] & (1 << (base    )))
+         state[base    ] = state_cb(mapped_port, RETRO_DEVICE_ANALOG, i, 0);
+      if (preempt->analog_mask[port] & (1 << (base + 1)))
+         state[base + 1] = state_cb(mapped_port, RETRO_DEVICE_ANALOG, i, 1);
+   }
+
+   /* buttons */
+   for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++)
+   {
+      if (preempt->analog_mask[port] & (1 << (i + 4)))
+         state[i + 4] = state_cb(mapped_port, RETRO_DEVICE_ANALOG,
+               RETRO_DEVICE_INDEX_ANALOG_BUTTON, i);
+   }
+
+   if (memcmp(preempt->analog_state[port], state, sizeof(state)) == 0)
+      return false;
+
+   memcpy(preempt->analog_state[port], state, sizeof(state));
+   return true;
+}
+
+static INLINE bool preempt_ptr_input_dirty(preempt_t *preempt,
+      retro_input_state_t state_cb, unsigned device,
+      unsigned port, unsigned mapped_port)
+{
+   int16_t state[4]  = {0};
+   unsigned count_id = 0;
+   unsigned x_id     = 0;
+   unsigned id, max_id;
+
+   switch (device)
+   {
+      case RETRO_DEVICE_MOUSE:
+         max_id = RETRO_DEVICE_ID_MOUSE_BUTTON_5;
+         break;
+      case RETRO_DEVICE_LIGHTGUN:
+         x_id   = RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X;
+         max_id = RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT;
+         break;
+      case RETRO_DEVICE_POINTER:
+         max_id   = RETRO_DEVICE_ID_POINTER_PRESSED;
+         count_id = RETRO_DEVICE_ID_POINTER_COUNT;
+         break;
+      default:
+         return false;
+   }
+
+   /* x, y */
+   state[0] = state_cb(mapped_port, device, 0, x_id    );
+   state[1] = state_cb(mapped_port, device, 0, x_id + 1);
+
+   /* buttons */
+   for (id = 2; id <= max_id; id++)
+      state[2] |= state_cb(mapped_port, device, 0, id) ? 1 << id : 0;
+
+   /* ptr count */
+   if (count_id)
+      state[3] = state_cb(mapped_port, device, 0, count_id);
+
+   if (memcmp(preempt->ptrdev_state[port], state, sizeof(state)) == 0)
+      return false;
+
+   memcpy(preempt->ptrdev_state[port], state, sizeof(state));
+   return true;
+}
+
+static INLINE void preempt_input_poll(preempt_t *preempt)
+{
+   settings_t *settings           = config_get_ptr();
+   runloop_state_t *runloop_st    = &runloop_state;
+   retro_input_state_t state_cb   = input_driver_state_wrapper;
+   unsigned max_users             = settings->uints.input_max_users;
+   int16_t joypad_state;
+   unsigned p;
+
+   input_driver_poll();
+
+   /* Check for input state changes */
+   for (p = 0; p < max_users; p++)
+   {
+      unsigned mapped_port = settings->uints.input_remap_ports[p];
+      unsigned device      = settings->uints.input_libretro_device[mapped_port]
+                             & RETRO_DEVICE_MASK;
+
+      switch (device)
+      {
+         case RETRO_DEVICE_JOYPAD:
+         case RETRO_DEVICE_ANALOG:
+            /* Check full digital joypad */
+            joypad_state = state_cb(mapped_port, RETRO_DEVICE_JOYPAD,
+                  0, RETRO_DEVICE_ID_JOYPAD_MASK);
+            if (joypad_state != preempt->joypad_state[p])
+            {
+               preempt->joypad_state[p] = joypad_state;
+               runloop_st->flags |= RUNLOOP_FLAG_INPUT_IS_DIRTY;
+            }
+
+            /* Check requested analogs */
+            if (     preempt->analog_mask[p]
+                  && preempt_analog_input_dirty(preempt, state_cb, p, mapped_port))
+               runloop_st->flags |= RUNLOOP_FLAG_INPUT_IS_DIRTY;
+            break;
+         case RETRO_DEVICE_MOUSE:
+         case RETRO_DEVICE_LIGHTGUN:
+         case RETRO_DEVICE_POINTER:
+            /* Check full device state */
+            if (preempt_ptr_input_dirty(
+                  preempt, state_cb, preempt->ptr_dev[p], p, mapped_port))
+               runloop_st->flags |= RUNLOOP_FLAG_INPUT_IS_DIRTY;
+            break;
+         default:
+            break;
+      }
+   }
+
+   /* Clear requested inputs */
+   memset(preempt->analog_mask, 0, max_users * sizeof(uint32_t));
+   memset(preempt->ptr_dev, 0, max_users * sizeof(uint8_t));
+}
+
+static INLINE void preempt_suspend_av(bool suspend)
+{
+   audio_driver_state_t *audio_st = audio_state_get_ptr();
+   video_driver_state_t *video_st = video_state_get_ptr();
+
+   if (suspend)
+   {
+      audio_st->flags |=  AUDIO_FLAG_SUSPENDED;
+      video_st->flags &= ~VIDEO_FLAG_ACTIVE;
+   }
+   else
+   {
+      audio_st->flags &= ~AUDIO_FLAG_SUSPENDED;
+      video_st->flags |= VIDEO_FLAG_ACTIVE;
+   }
+}
+
+/* macro for preempt_run */
+#define PREEMPT_NEXT_PTR(x) ((x + 1) % preempt->frames)
+
+/**
+ * preempt_run:
+ * @preempt : pointer to preemptive frames object
+ *
+ * Call in place of core_run() for preemptive frames.
+ **/
+static void preempt_run(preempt_t *preempt)
+{
+   runloop_state_t     *runloop_st   = &runloop_state;
+   struct retro_core_t *current_core = &runloop_st->current_core;
+   const char *failed_str            = NULL;
+
+   /* Poll and check for dirty input */
+   preempt_input_poll(preempt);
+
+   runloop_st->flags |= RUNLOOP_FLAG_REQUEST_SPECIAL_SAVESTATE;
+
+   if ((runloop_st->flags & RUNLOOP_FLAG_INPUT_IS_DIRTY)
+         && preempt->frame_count >= preempt->frames)
+   {
+      /* Suspend A/V and run preemptive frames */
+      preempt_suspend_av(true);
+
+      if (!current_core->retro_unserialize(
+            preempt->buffer[preempt->start_ptr], preempt->state_size))
+      {
+         failed_str = msg_hash_to_str(MSG_PREEMPT_FAILED_TO_LOAD_STATE);
+         goto error;
+      }
+
+      current_core->retro_run();
+      preempt->replay_ptr = PREEMPT_NEXT_PTR(preempt->start_ptr);
+
+      while (preempt->replay_ptr != preempt->start_ptr)
+      {
+         if (!current_core->retro_serialize(
+               preempt->buffer[preempt->replay_ptr], preempt->state_size))
+         {
+            failed_str = msg_hash_to_str(MSG_PREEMPT_FAILED_TO_SAVE_STATE);
+            goto error;
+         }
+
+         current_core->retro_run();
+         preempt->replay_ptr = PREEMPT_NEXT_PTR(preempt->replay_ptr);
+      }
+
+      preempt_suspend_av(false);
+   }
+
+   /* Save current state and set start_ptr to oldest state */
+   if (!current_core->retro_serialize(
+         preempt->buffer[preempt->start_ptr], preempt->state_size))
+   {
+      failed_str = msg_hash_to_str(MSG_PREEMPT_FAILED_TO_SAVE_STATE);
+      goto error;
+   }
+   preempt->start_ptr = PREEMPT_NEXT_PTR(preempt->start_ptr);
+   runloop_st->flags &= ~(RUNLOOP_FLAG_REQUEST_SPECIAL_SAVESTATE
+         | RUNLOOP_FLAG_INPUT_IS_DIRTY);
+
+   /* Run normal frame */
+   current_core->retro_run();
+   preempt->frame_count++;
+   return;
+
+error:
+   runloop_st->flags &= ~(RUNLOOP_FLAG_REQUEST_SPECIAL_SAVESTATE
+         | RUNLOOP_FLAG_INPUT_IS_DIRTY);
+   preempt_suspend_av(false);
+   runloop_preempt_deinit();
+
+   if (!config_get_ptr()->bools.preemptive_frames_hide_warnings)
+      runloop_msg_queue_push(
+            failed_str, 0, 2 * 60, true,
+            NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+   RARCH_ERR("[Preemptive Frames]: %s\n", failed_str);
+}
+
 #endif
 
 static retro_time_t runloop_core_runtime_tick(
@@ -5583,6 +5957,8 @@ static void core_init_libretro_cbs(runloop_state_t *runloop_st,
    runloop_st->current_core.retro_set_input_state(state_cb);
    runloop_st->current_core.retro_set_input_poll(core_input_state_poll_maybe);
 
+   runloop_st->input_poll_callback_original    = core_input_state_poll_maybe;
+
    core_set_default_callbacks(cbs);
 
 #ifdef HAVE_NETWORKING
@@ -7169,6 +7545,9 @@ static enum runloop_state_enum runloop_check_state(
    /* Check if we have pressed the Run-Ahead toggle button */
    HOTKEY_CHECK(RARCH_RUNAHEAD_TOGGLE, CMD_EVENT_RUNAHEAD_TOGGLE, true, NULL);
 
+   /* Check if we have pressed the Preemptive Frames toggle button */
+   HOTKEY_CHECK(RARCH_PREEMPT_TOGGLE, CMD_EVENT_PREEMPT_TOGGLE, true, NULL);
+
    /* Check if we have pressed the AI Service toggle button */
    HOTKEY_CHECK(RARCH_AI_SERVICE, CMD_EVENT_AI_SERVICE_TOGGLE, true, NULL);
 
@@ -8004,6 +8383,8 @@ int runloop_iterate(void)
                run_ahead_num_frames,
                run_ahead_hide_warnings,
                run_ahead_secondary_instance);
+      else if (runloop_st->preempt_data)
+         preempt_run(runloop_st->preempt_data);
       else
 #endif
          core_run();
@@ -8522,6 +8903,9 @@ bool core_unserialize(retro_ctx_serialize_info_t *info)
 #ifdef HAVE_NETWORKING
    netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, info);
 #endif
+#if HAVE_RUNAHEAD
+   command_event(CMD_EVENT_PREEMPT_RESET_BUFFER, NULL);
+#endif
 
    return true;
 }
diff --git a/runloop.h b/runloop.h
index 94b4347322..3e99ca7a38 100644
--- a/runloop.h
+++ b/runloop.h
@@ -159,7 +159,9 @@ typedef struct core_options_callbacks
 } core_options_callbacks_t;
 
 #ifdef HAVE_RUNAHEAD
-typedef bool  (*runahead_load_state_function)(const void*, size_t);
+#define MAX_RUNAHEAD_FRAMES 12
+
+typedef bool(*runahead_load_state_function)(const void*, size_t);
 
 typedef void *(*constructor_t)(void);
 typedef void  (*destructor_t )(void*);
@@ -172,6 +174,33 @@ typedef struct my_list_t
    int capacity;
    int size;
 } my_list;
+
+typedef struct preemptive_frames_data
+{
+   /* Savestate buffer */
+   void* buffer[MAX_RUNAHEAD_FRAMES];
+   size_t state_size;
+
+   /* Number of latency frames to remove */
+   uint8_t frames;
+
+   /* Buffer indexes for replays */
+   uint8_t start_ptr;
+   uint8_t replay_ptr;
+
+   /* Frame count since buffer init/reset */
+   uint64_t frame_count;
+
+   /* Input states. Replays triggered on changes */
+   int16_t joypad_state[MAX_USERS];
+   int16_t analog_state[MAX_USERS][20];
+   int16_t ptrdev_state[MAX_USERS][4];
+
+   /* Pointing device requested */
+   uint8_t ptr_dev[MAX_USERS];
+   /* Mask of analog states requested */
+   uint32_t analog_mask[MAX_USERS];
+} preempt_t;
 #endif
 
 struct runloop
@@ -197,6 +226,7 @@ struct runloop
 #endif
    my_list *runahead_save_state_list;
    my_list *input_state_list;
+   preempt_t *preempt_data;
 #endif
 
 #ifdef HAVE_REWIND
@@ -208,6 +238,7 @@ struct runloop
    struct retro_subsystem_info subsystem_data[SUBSYSTEM_MAX_SUBSYSTEMS];
    struct retro_callbacks retro_ctx;                     /* ptr alignment */
    msg_queue_t msg_queue;                                /* ptr alignment */
+   retro_input_poll_t input_poll_callback_original;      /* ptr alignment */
    retro_input_state_t input_state_callback_original;    /* ptr alignment */
 #ifdef HAVE_RUNAHEAD
    function_t retro_reset_callback_original;             /* ptr alignment */
@@ -384,6 +415,9 @@ void runloop_event_deinit_core(void);
 
 #ifdef HAVE_RUNAHEAD
 void runloop_runahead_clear_variables(runloop_state_t *runloop_st);
+
+bool runloop_preempt_init(void);
+void runloop_preempt_deinit(void);
 #endif
 
 bool runloop_event_init_core(