diff --git a/Makefile.common b/Makefile.common
index 4a872356bd0..3277ff0a3e3 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -2431,6 +2431,12 @@ ifeq ($(HAVE_WEBOS), 1)
DEFINES += -DWEBOS
endif
+ifeq ($(HAVE_TEST_DRIVERS), 1)
+ DEFINES += -DHAVE_TEST_DRIVERS
+ OBJ += input/drivers_joypad/test_joypad.o
+endif
+
+
#####################################
### Android Play Feature Delivery ###
### (Play Store build core ###
diff --git a/configuration.c b/configuration.c
index 8a5e272926a..425740ee251 100644
--- a/configuration.c
+++ b/configuration.c
@@ -1645,6 +1645,10 @@ static struct config_path_setting *populate_settings_path(
SETTING_PATH("bottom_assets_directory", settings->paths.directory_bottom_assets, true, NULL, true);
#endif
+#ifdef HAVE_TEST_DRIVERS
+ SETTING_PATH("test_input_file_joypad", settings->paths.test_input_file_joypad, false, NULL, true);
+#endif
+
SETTING_ARRAY("log_dir", settings->paths.log_dir, true, NULL, true);
SETTING_ARRAY("app_icon", settings->paths.app_icon, true, NULL, true);
diff --git a/configuration.h b/configuration.h
index ce7a5a42d7f..6228682990d 100644
--- a/configuration.h
+++ b/configuration.h
@@ -574,6 +574,9 @@ typedef struct settings
char streaming_title[PATH_MAX_LENGTH];
#ifdef _3DS
char directory_bottom_assets[PATH_MAX_LENGTH];
+#endif
+#ifdef HAVE_TEST_DRIVERS
+ char test_input_file_joypad[PATH_MAX_LENGTH];
#endif
char log_dir[PATH_MAX_LENGTH];
char app_icon[PATH_MAX_LENGTH];
diff --git a/griffin/griffin.c b/griffin/griffin.c
index 7465480a139..426e249e931 100644
--- a/griffin/griffin.c
+++ b/griffin/griffin.c
@@ -730,6 +730,10 @@ INPUT
#endif
#endif
+#ifdef HAVE_TEST_DRIVERS
+#include "../input/drivers_joypad/test_joypad.c"
+#endif
+
/*============================================================
INPUT (HID)
============================================================ */
diff --git a/input/drivers_joypad/test_joypad.c b/input/drivers_joypad/test_joypad.c
new file mode 100644
index 00000000000..ababb249fbd
--- /dev/null
+++ b/input/drivers_joypad/test_joypad.c
@@ -0,0 +1,454 @@
+/* RetroArch - A frontend for libretro.
+ * Copyright (C) 2011-2017 - Daniel De Matteis
+ * Copyright (C) 2016-2019 - Brad Parker
+ *
+ * RetroArch is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Found-
+ * ation, either version 3 of the License, or (at your option) any later version.
+ *
+ * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with RetroArch.
+ * If not, see .
+ */
+
+/* Possible improvement list:
+ * Add vid/pid to autoconf profile step
+ * Multiple device autoconf profiles, with different analog/digital setup, analog buttons, extra buttons, unconfigured buttons...
+ */
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include "../../config.def.h"
+#include "../../verbosity.h"
+#include "../input_driver.h"
+#include "../../tasks/tasks_internal.h"
+#include "../../gfx/video_driver.h"
+
+#define MAX_TEST_STEPS 200
+#define NUM_BUTTONS 32
+#ifndef MAX_AXIS
+#define MAX_AXIS 10
+#endif
+
+typedef struct
+{
+ char* name;
+ uint32_t button_state;
+ int32_t axis_state[MAX_AXIS];
+} test_joypad_data;
+
+static test_joypad_data test_joypads[MAX_USERS];
+
+typedef struct
+{
+ unsigned frame;
+ unsigned action;
+ unsigned param_num;
+ char param_str[255];
+ bool handled;
+} input_test_step_t;
+
+static input_test_step_t input_test_steps[MAX_TEST_STEPS];
+
+static unsigned current_frame = 0;
+static unsigned next_teststep_frame = 0;
+static unsigned current_test_step = 0;
+static unsigned last_test_step = MAX_TEST_STEPS + 1;
+static uint32_t input_state_validated = 0;
+static uint32_t combo_state_validated = 0;
+static bool dump_state_blocked = false;
+
+/************************************/
+/* JSON Helpers for test input file */
+/************************************/
+
+typedef struct
+{
+ unsigned *current_entry_uint_val;
+ char **current_entry_str_val;
+ unsigned frame;
+ unsigned action;
+ unsigned param_num;
+ char *param_str;
+} ITifJSONContext;
+
+static bool ITifJSONObjectEndHandler(void* context)
+{
+ ITifJSONContext *pCtx = (ITifJSONContext*)context;
+
+ /* Too long input is handled elsewhere, it should not lead to parse error */
+ if (current_test_step >= MAX_TEST_STEPS)
+ return true;
+
+ /* Copy values read from JSON file + fill defaults */
+ if (pCtx->frame == 0xffff)
+ input_test_steps[current_test_step].frame = input_test_steps[current_test_step-1].frame + 60;
+ else
+ input_test_steps[current_test_step].frame = pCtx->frame;
+
+ input_test_steps[current_test_step].action = pCtx->action;
+ input_test_steps[current_test_step].param_num = pCtx->param_num;
+ input_test_steps[current_test_step].handled = false;
+
+ if (!string_is_empty(pCtx->param_str))
+ strlcpy(
+ input_test_steps[current_test_step].param_str, pCtx->param_str,
+ sizeof(input_test_steps[current_test_step].param_str));
+ else
+ input_test_steps[current_test_step].param_str[0] = '\0';
+
+ current_test_step++;
+ last_test_step = current_test_step;
+ pCtx->frame = 0xffff;
+ return true;
+}
+
+static bool ITifJSONObjectMemberHandler(void* context, const char *pValue, size_t length)
+{
+ ITifJSONContext *pCtx = (ITifJSONContext*)context;
+
+ /* something went wrong */
+ if (pCtx->current_entry_str_val)
+ return false;
+
+ if (length)
+ {
+ if (string_is_equal(pValue, "frame"))
+ pCtx->current_entry_uint_val = &pCtx->frame;
+ else if (string_is_equal(pValue, "action"))
+ pCtx->current_entry_uint_val = &pCtx->action;
+ else if (string_is_equal(pValue, "param_num"))
+ pCtx->current_entry_uint_val = &pCtx->param_num;
+ else if (string_is_equal(pValue, "param_str"))
+ pCtx->current_entry_str_val = &pCtx->param_str;
+ /* ignore unknown members */
+ }
+
+ return true;
+}
+
+static bool ITifJSONNumberHandler(void* context, const char *pValue, size_t length)
+{
+ ITifJSONContext *pCtx = (ITifJSONContext*)context;
+
+ if (pCtx->current_entry_uint_val && length && !string_is_empty(pValue))
+ *pCtx->current_entry_uint_val = string_to_unsigned(pValue);
+ /* ignore unknown members */
+
+ pCtx->current_entry_uint_val = NULL;
+
+ return true;
+}
+
+static bool ITifJSONStringHandler(void* context, const char *pValue, size_t length)
+{
+ ITifJSONContext *pCtx = (ITifJSONContext*)context;
+
+ if (pCtx->current_entry_str_val && length && !string_is_empty(pValue))
+ {
+ if (*pCtx->current_entry_str_val)
+ free(*pCtx->current_entry_str_val);
+
+ *pCtx->current_entry_str_val = strdup(pValue);
+ }
+ /* ignore unknown members */
+
+ pCtx->current_entry_str_val = NULL;
+
+ return true;
+}
+
+/* Parses test input file referenced by file_path.
+ * Does nothing if test input file does not exist. */
+static bool input_test_file_read(const char* file_path)
+{
+ bool success = false;
+ ITifJSONContext context = {0};
+ RFILE *file = NULL;
+ rjson_t* parser;
+
+ /* Sanity check */
+ if ( string_is_empty(file_path)
+ || !path_is_valid(file_path)
+ )
+ {
+ RARCH_DBG("[Test input driver]: No test input file supplied.\n");
+ return false;
+ }
+
+ /* Attempt to open test input file */
+ file = filestream_open(
+ file_path,
+ RETRO_VFS_FILE_ACCESS_READ,
+ RETRO_VFS_FILE_ACCESS_HINT_NONE);
+
+ if (!file)
+ {
+ RARCH_ERR("[Test input driver]: Failed to open test input file: \"%s\".\n",
+ file_path);
+ return false;
+ }
+
+ /* Initialise JSON parser */
+ if (!(parser = rjson_open_rfile(file)))
+ {
+ RARCH_ERR("[Test input driver]: Failed to create JSON parser.\n");
+ goto end;
+ }
+
+ /* Configure parser */
+ rjson_set_options(parser, RJSON_OPTION_ALLOW_UTF8BOM);
+
+ /* Read file */
+ if (rjson_parse(parser, &context,
+ ITifJSONObjectMemberHandler,
+ ITifJSONStringHandler,
+ ITifJSONNumberHandler,
+ NULL, ITifJSONObjectEndHandler, NULL, NULL, /* object/array handlers */
+ NULL, NULL) /* unused boolean/null handlers */
+ != RJSON_DONE)
+ {
+ if (rjson_get_source_context_len(parser))
+ {
+ RARCH_ERR(
+ "[Test input driver]: Error parsing chunk of test input file: %s\n---snip---\n%.*s\n---snip---\n",
+ file_path,
+ rjson_get_source_context_len(parser),
+ rjson_get_source_context_buf(parser));
+ }
+ RARCH_WARN(
+ "[Test input driver]: Error parsing test input file: %s\n",
+ file_path);
+ RARCH_ERR(
+ "[Test input driver]: Error: Invalid JSON at line %d, column %d - %s.\n",
+ (int)rjson_get_source_line(parser),
+ (int)rjson_get_source_column(parser),
+ (*rjson_get_error(parser) ? rjson_get_error(parser) : "format error"));
+ }
+
+ /* Free parser */
+ rjson_free(parser);
+
+ success = true;
+end:
+ /* Clean up leftover strings */
+ if (context.param_str)
+ free(context.param_str);
+
+ /* Close log file */
+ filestream_close(file);
+
+ if (last_test_step >= MAX_TEST_STEPS)
+ {
+ RARCH_WARN("[Test input driver]: too long test input json, maximum size: %d\n",MAX_TEST_STEPS);
+ }
+ for (current_test_step = 0; current_test_step < last_test_step; current_test_step++)
+ {
+ RARCH_DBG(
+ "[Test input driver]: test step %02d read from file: frame %d, action %x, num %x, str %s\n",
+ current_test_step,
+ input_test_steps[current_test_step].frame,
+ input_test_steps[current_test_step].action,
+ input_test_steps[current_test_step].param_num,
+ input_test_steps[current_test_step].param_str);
+ }
+ current_test_step = 0;
+ return success;
+}
+
+/********************************/
+/* Test input file handling end */
+/********************************/
+
+static const char *test_joypad_name(unsigned pad)
+{
+ if (pad >= MAX_USERS || string_is_empty(test_joypads[pad].name))
+ return NULL;
+
+ return test_joypads[pad].name;
+}
+
+
+static void test_joypad_autodetect_add(unsigned autoconf_pad)
+{
+ input_autoconfigure_connect(
+ test_joypad_name(autoconf_pad),
+ NULL,
+ "test",
+ autoconf_pad,
+ 0,
+ 0
+ );
+}
+
+static void *test_joypad_init(void *data)
+{
+ settings_t *settings = config_get_ptr();
+ unsigned i;
+
+ input_test_file_read(settings->paths.test_input_file_joypad);
+ if (last_test_step > MAX_TEST_STEPS)
+ last_test_step = 0;
+
+ for(i=0; i 0)
+ continue;
+ if (input_test_steps[i].action == 1)
+ {
+ test_joypads[input_test_steps[i].param_num].name = input_test_steps[i].param_str;
+ test_joypad_autodetect_add(input_test_steps[i].param_num);
+ input_test_steps[i].handled = true;
+ }
+
+ }
+ return (void*)-1;
+}
+
+static int32_t test_joypad_button(unsigned port_num, uint16_t joykey)
+{
+ int16_t ret = 0;
+ if (port_num >= DEFAULT_MAX_PADS)
+ return 0;
+ if (joykey < NUM_BUTTONS)
+ return (BIT32_GET(test_joypads[port_num].button_state, joykey));
+
+ return 0;
+}
+
+static int16_t test_joypad_axis(unsigned port_num, uint32_t joyaxis)
+{
+ /*RARCH_DBG("test_joypad_axis %d / %u\n",port_num, joyaxis);*/
+ if (port_num >= DEFAULT_MAX_PADS)
+ return 0;
+ if (AXIS_NEG_GET(joyaxis) < MAX_AXIS)
+ {
+ /* Kernel returns values in range [-0x7fff, 0x7fff]. */
+ int16_t val = test_joypads[port_num].axis_state[AXIS_NEG_GET(joyaxis)];
+ if (val < 0)
+ return val;
+ }
+ else if (AXIS_POS_GET(joyaxis) < MAX_AXIS)
+ {
+ int16_t val = test_joypads[port_num].axis_state[AXIS_POS_GET(joyaxis)];
+ if (val > 0)
+ return val;
+ }
+ return 0;
+
+}
+
+static int16_t test_joypad_state(
+ rarch_joypad_info_t *joypad_info,
+ const struct retro_keybind *binds,
+ unsigned port)
+{
+
+ unsigned i;
+ int16_t ret = 0;
+ uint16_t port_idx = joypad_info->joy_idx;
+
+ if (port_idx < DEFAULT_MAX_PADS)
+ {
+ for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++)
+ {
+ /* Auto-binds are per joypad, not per user. */
+ const uint16_t joykey = (binds[i].joykey != NO_BTN)
+ ? binds[i].joykey : joypad_info->auto_binds[i].joykey;
+ /* Test input driver uses same button layout internally as RA, so no conversion is needed */
+ if (joykey != NO_BTN && (test_joypads[port_idx].button_state & (1 << i)))
+ {
+ ret |= ( 1 << i);
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+static void test_joypad_poll(void)
+{
+
+ video_driver_state_t *video_st = video_state_get_ptr();
+ uint64_t curr_frame = video_st->frame_count;
+ unsigned i;
+
+ for (i=0; i input_test_steps[i].frame)
+ {
+ if (input_test_steps[i].action == 1)
+ {
+ test_joypads[input_test_steps[i].param_num].name = input_test_steps[i].param_str;
+ test_joypad_autodetect_add(input_test_steps[i].param_num);
+ input_test_steps[i].handled = true;
+ }
+ else if(input_test_steps[i].action >= 16 && input_test_steps[i].action <= 31)
+ {
+ unsigned targetpad = input_test_steps[i].action - 16;
+ test_joypads[targetpad].button_state |= input_test_steps[i].param_num;
+ input_test_steps[i].handled = true;
+ RARCH_DBG("[Test input driver]: Pressing device %d buttons %x, new state %x.\n",targetpad,input_test_steps[i].param_num,test_joypads[targetpad].button_state);
+ }
+ else if(input_test_steps[i].action >= 32 && input_test_steps[i].action <= 47)
+ {
+ unsigned targetpad = input_test_steps[i].action - 32;
+ test_joypads[targetpad].button_state &= ~input_test_steps[i].param_num;
+ input_test_steps[i].handled = true;
+ RARCH_DBG("[Test input driver]: Releasing device %d buttons %x, new state %x.\n",targetpad,input_test_steps[i].param_num,test_joypads[targetpad].button_state);
+ }
+ else if(input_test_steps[i].action >= 1000 && input_test_steps[i].action <= 1000+MAX_AXIS*DEFAULT_MAX_PADS)
+ {
+ unsigned targetpad = (input_test_steps[i].action - 1000)/MAX_AXIS;
+ unsigned targetaxis = input_test_steps[i].action - 1000 - (targetpad*MAX_AXIS);
+ if (targetpad < DEFAULT_MAX_PADS && targetaxis < MAX_AXIS)
+ test_joypads[targetpad].axis_state[targetaxis] = (int16_t) input_test_steps[i].param_num;
+ input_test_steps[i].handled = true;
+ RARCH_DBG("[Test input driver]: Setting axis device %d axis %d value %d.\n",targetpad, targetaxis, (int16_t)input_test_steps[i].param_num);
+ }
+ else
+ {
+ input_test_steps[i].handled = true;
+ RARCH_WARN("[Test input driver]: Unrecognized action %d in step %d, skipping\n",input_test_steps[i].action,i);
+ }
+
+ }
+ }
+}
+
+static bool test_joypad_query_pad(unsigned pad)
+{
+ return (pad < MAX_USERS);
+}
+
+static void test_joypad_destroy(void)
+{
+
+}
+
+input_device_driver_t test_joypad = {
+ test_joypad_init,
+ test_joypad_query_pad,
+ test_joypad_destroy,
+ test_joypad_button,
+ test_joypad_state,
+ NULL, /* get_buttons */
+ test_joypad_axis,
+ test_joypad_poll,
+ NULL, /* rumble */
+ NULL, /* rumble_gain */
+ test_joypad_name,
+ "test",
+};
diff --git a/input/input_driver.c b/input/input_driver.c
index 957ee3f901f..dc524d3037e 100644
--- a/input/input_driver.c
+++ b/input/input_driver.c
@@ -280,6 +280,9 @@ input_device_driver_t *joypad_drivers[] = {
#endif
#ifdef EMSCRIPTEN
&rwebpad_joypad,
+#endif
+#ifdef HAVE_TEST_DRIVERS
+ &test_joypad,
#endif
&null_joypad,
NULL,
diff --git a/input/input_driver.h b/input/input_driver.h
index 6fa75a3b353..99f5abf9bc1 100644
--- a/input/input_driver.h
+++ b/input/input_driver.h
@@ -1113,6 +1113,7 @@ extern input_device_driver_t qnx_joypad;
extern input_device_driver_t mfi_joypad;
extern input_device_driver_t dos_joypad;
extern input_device_driver_t rwebpad_joypad;
+extern input_device_driver_t test_joypad;
#ifdef HAVE_HID
extern hid_driver_t iohidmanager_hid;
diff --git a/qb/config.params.sh b/qb/config.params.sh
index b3143897060..d261c8f6f37 100644
--- a/qb/config.params.sh
+++ b/qb/config.params.sh
@@ -203,3 +203,4 @@ HAVE_CRTSWITCHRES=auto # CRT mode switching support (requires C++11)
HAVE_MEMFD_CREATE=auto # libc supports memfd_create
C89_CRTSWITCHRES=no
HAVE_MICROPHONE=yes # Microphone support
+HAVE_TEST_DRIVERS=yes # Test input driver
diff --git a/tasks/task_autodetect.c b/tasks/task_autodetect.c
index aa68e3b6c0d..bf96d2a50a2 100644
--- a/tasks/task_autodetect.c
+++ b/tasks/task_autodetect.c
@@ -481,7 +481,11 @@ static void input_autoconfigure_connect_handler(retro_task_t *task)
else if (string_is_equal(autoconfig_handle->device_info.joypad_driver,
"sdl2"))
fallback_device_name = "Standard Gamepad";
-
+#ifdef HAVE_TEST_DRIVERS
+ else if (string_is_equal(autoconfig_handle->device_info.joypad_driver,
+ "test"))
+ fallback_device_name = "Test Gamepad";
+#endif
if (!string_is_empty(fallback_device_name) &&
!string_is_equal(autoconfig_handle->device_info.name,
fallback_device_name))
diff --git a/tests-other/all_binds_empty.cfg b/tests-other/all_binds_empty.cfg
new file mode 100644
index 00000000000..501f971baf2
--- /dev/null
+++ b/tests-other/all_binds_empty.cfg
@@ -0,0 +1,386 @@
+input_exit_emulator = "escape"
+input_max_users = "10"
+
+input_ai_service = "nul"
+input_ai_service_axis = "nul"
+input_ai_service_btn = "nul"
+input_ai_service_mbtn = "nul"
+input_allow_turbo_dpad = "false"
+input_analog_deadzone = "0.000000"
+input_analog_sensitivity = "1.000000"
+input_audio_mute = "nul"
+input_audio_mute_axis = "nul"
+input_audio_mute_btn = "nul"
+input_audio_mute_mbtn = "nul"
+input_auto_game_focus = "nul"
+input_auto_mouse_grab = "false"
+input_autodetect_enable = "true"
+input_axis_threshold = "0.500000"
+input_bind_hold = "0"
+input_bind_timeout = "5"
+input_cheat_index_minus = "nul"
+input_cheat_index_minus_axis = "nul"
+input_cheat_index_minus_btn = "nul"
+input_cheat_index_minus_mbtn = "nul"
+input_cheat_index_plus = "nul"
+input_cheat_index_plus_axis = "nul"
+input_cheat_index_plus_btn = "nul"
+input_cheat_index_plus_mbtn = "nul"
+input_cheat_toggle = "nul"
+input_cheat_toggle_axis = "nul"
+input_cheat_toggle_btn = "nul"
+input_cheat_toggle_mbtn = "nul"
+input_close_content = "nul"
+input_close_content_axis = "nul"
+input_close_content_btn = "nul"
+input_close_content_mbtn = "nul"
+input_desktop_menu_toggle = "nul"
+input_desktop_menu_toggle_axis = "nul"
+input_desktop_menu_toggle_btn = "nul"
+input_desktop_menu_toggle_mbtn = "nul"
+input_disk_eject_toggle = "nul"
+input_disk_eject_toggle_axis = "nul"
+input_disk_eject_toggle_btn = "nul"
+input_disk_eject_toggle_mbtn = "nul"
+input_disk_next = "nul"
+input_disk_next_axis = "nul"
+input_disk_next_btn = "nul"
+input_disk_next_mbtn = "nul"
+input_disk_prev = "nul"
+input_disk_prev_axis = "nul"
+input_disk_prev_btn = "nul"
+input_disk_prev_mbtn = "nul"
+input_enable_hotkey = "nul"
+input_enable_hotkey_axis = "nul"
+input_enable_hotkey_btn = "nul"
+input_enable_hotkey_mbtn = "nul"
+input_exit_emulator_axis = "nul"
+input_exit_emulator_btn = "nul"
+input_exit_emulator_mbtn = "nul"
+input_fps_toggle = "nul"
+input_fps_toggle_axis = "nul"
+input_fps_toggle_btn = "nul"
+input_fps_toggle_mbtn = "nul"
+input_frame_advance = "nul"
+input_frame_advance_axis = "nul"
+input_frame_advance_btn = "nul"
+input_frame_advance_mbtn = "nul"
+input_game_focus_toggle = "nul"
+input_game_focus_toggle_axis = "nul"
+input_game_focus_toggle_btn = "nul"
+input_game_focus_toggle_mbtn = "nul"
+input_grab_mouse_toggle = "nul"
+input_grab_mouse_toggle_axis = "nul"
+input_grab_mouse_toggle_btn = "nul"
+input_grab_mouse_toggle_mbtn = "nul"
+input_halt_replay = "nul"
+input_halt_replay_axis = "nul"
+input_halt_replay_btn = "nul"
+input_halt_replay_mbtn = "nul"
+input_hold_fast_forward = "nul"
+input_hold_fast_forward_axis = "nul"
+input_hold_fast_forward_btn = "nul"
+input_hold_fast_forward_mbtn = "nul"
+input_hold_slowmotion = "nul"
+input_hold_slowmotion_axis = "nul"
+input_hold_slowmotion_btn = "nul"
+input_hold_slowmotion_mbtn = "nul"
+input_load_state = "nul"
+input_load_state_axis = "nul"
+input_load_state_btn = "nul"
+input_load_state_mbtn = "nul"
+input_menu_toggle = "nul"
+input_menu_toggle_axis = "nul"
+input_menu_toggle_btn = "nul"
+input_menu_toggle_gamepad_combo = "nul"
+input_menu_toggle_mbtn = "nul"
+input_movie_record_toggle = "nul"
+input_movie_record_toggle_axis = "nul"
+input_movie_record_toggle_btn = "nul"
+input_movie_record_toggle_mbtn = "nul"
+input_netplay_fade_chat_toggle = "nul"
+input_netplay_fade_chat_toggle_axis = "nul"
+input_netplay_fade_chat_toggle_btn = "nul"
+input_netplay_fade_chat_toggle_mbtn = "nul"
+input_netplay_game_watch = "nul"
+input_netplay_game_watch_axis = "nul"
+input_netplay_game_watch_btn = "nul"
+input_netplay_game_watch_mbtn = "nul"
+input_netplay_host_toggle = "nul"
+input_netplay_host_toggle_axis = "nul"
+input_netplay_host_toggle_btn = "nul"
+input_netplay_host_toggle_mbtn = "nul"
+input_netplay_ping_toggle = "nul"
+input_netplay_ping_toggle_axis = "nul"
+input_netplay_ping_toggle_btn = "nul"
+input_netplay_ping_toggle_mbtn = "nul"
+input_netplay_player_chat = "nul"
+input_netplay_player_chat_axis = "nul"
+input_netplay_player_chat_btn = "nul"
+input_netplay_player_chat_mbtn = "nul"
+input_osk_toggle = "nul"
+input_osk_toggle_axis = "nul"
+input_osk_toggle_btn = "nul"
+input_osk_toggle_mbtn = "nul"
+input_overlay = ""
+input_overlay_enable = "false"
+input_overlay_next = "nul"
+input_overlay_next_axis = "nul"
+input_overlay_next_btn = "nul"
+input_overlay_next_mbtn = "nul"
+input_pause_toggle = "nul"
+input_pause_toggle_axis = "nul"
+input_pause_toggle_btn = "nul"
+input_pause_toggle_mbtn = "nul"
+input_play_replay = "nul"
+input_play_replay_axis = "nul"
+input_play_replay_btn = "nul"
+input_play_replay_mbtn = "nul"
+input_player1_a = "x"
+input_player1_a_axis = "nul"
+input_player1_a_btn = "nul"
+input_player1_a_mbtn = "nul"
+input_player1_analog_dpad_mode = "1"
+input_player1_b = "z"
+input_player1_b_axis = "nul"
+input_player1_b_btn = "nul"
+input_player1_b_mbtn = "nul"
+input_player1_down = "down"
+input_player1_down_axis = "nul"
+input_player1_down_btn = "nul"
+input_player1_down_mbtn = "nul"
+input_player1_gun_aux_a = "nul"
+input_player1_gun_aux_a_axis = "nul"
+input_player1_gun_aux_a_btn = "nul"
+input_player1_gun_aux_a_mbtn = "nul"
+input_player1_gun_aux_b = "nul"
+input_player1_gun_aux_b_axis = "nul"
+input_player1_gun_aux_b_btn = "nul"
+input_player1_gun_aux_b_mbtn = "nul"
+input_player1_gun_aux_c = "nul"
+input_player1_gun_aux_c_axis = "nul"
+input_player1_gun_aux_c_btn = "nul"
+input_player1_gun_aux_c_mbtn = "nul"
+input_player1_gun_dpad_down = "nul"
+input_player1_gun_dpad_down_axis = "nul"
+input_player1_gun_dpad_down_btn = "nul"
+input_player1_gun_dpad_down_mbtn = "nul"
+input_player1_gun_dpad_left = "nul"
+input_player1_gun_dpad_left_axis = "nul"
+input_player1_gun_dpad_left_btn = "nul"
+input_player1_gun_dpad_left_mbtn = "nul"
+input_player1_gun_dpad_right = "nul"
+input_player1_gun_dpad_right_axis = "nul"
+input_player1_gun_dpad_right_btn = "nul"
+input_player1_gun_dpad_right_mbtn = "nul"
+input_player1_gun_dpad_up = "nul"
+input_player1_gun_dpad_up_axis = "nul"
+input_player1_gun_dpad_up_btn = "nul"
+input_player1_gun_dpad_up_mbtn = "nul"
+input_player1_gun_offscreen_shot = "nul"
+input_player1_gun_offscreen_shot_axis = "nul"
+input_player1_gun_offscreen_shot_btn = "nul"
+input_player1_gun_offscreen_shot_mbtn = "2"
+input_player1_gun_select = "nul"
+input_player1_gun_select_axis = "nul"
+input_player1_gun_select_btn = "nul"
+input_player1_gun_select_mbtn = "nul"
+input_player1_gun_start = "nul"
+input_player1_gun_start_axis = "nul"
+input_player1_gun_start_btn = "nul"
+input_player1_gun_start_mbtn = "nul"
+input_player1_gun_trigger = "nul"
+input_player1_gun_trigger_axis = "nul"
+input_player1_gun_trigger_btn = "nul"
+input_player1_gun_trigger_mbtn = "1"
+input_player1_joypad_index = "0"
+input_player1_l = "q"
+input_player1_l2 = "nul"
+input_player1_l2_axis = "nul"
+input_player1_l2_btn = "nul"
+input_player1_l2_mbtn = "nul"
+input_player1_l3 = "nul"
+input_player1_l3_axis = "nul"
+input_player1_l3_btn = "nul"
+input_player1_l3_mbtn = "nul"
+input_player1_l_axis = "nul"
+input_player1_l_btn = "nul"
+input_player1_l_mbtn = "nul"
+input_player1_l_x_minus = "nul"
+input_player1_l_x_minus_axis = "nul"
+input_player1_l_x_minus_btn = "nul"
+input_player1_l_x_minus_mbtn = "nul"
+input_player1_l_x_plus = "nul"
+input_player1_l_x_plus_axis = "nul"
+input_player1_l_x_plus_btn = "nul"
+input_player1_l_x_plus_mbtn = "nul"
+input_player1_l_y_minus = "nul"
+input_player1_l_y_minus_axis = "nul"
+input_player1_l_y_minus_btn = "nul"
+input_player1_l_y_minus_mbtn = "nul"
+input_player1_l_y_plus = "nul"
+input_player1_l_y_plus_axis = "nul"
+input_player1_l_y_plus_btn = "nul"
+input_player1_l_y_plus_mbtn = "nul"
+input_player1_left = "left"
+input_player1_left_axis = "nul"
+input_player1_left_btn = "nul"
+input_player1_left_mbtn = "nul"
+input_player1_mouse_index = "0"
+input_player1_r = "w"
+input_player1_r2 = "nul"
+input_player1_r2_axis = "nul"
+input_player1_r2_btn = "nul"
+input_player1_r2_mbtn = "nul"
+input_player1_r3 = "nul"
+input_player1_r3_axis = "nul"
+input_player1_r3_btn = "nul"
+input_player1_r3_mbtn = "nul"
+input_player1_r_axis = "nul"
+input_player1_r_btn = "nul"
+input_player1_r_mbtn = "nul"
+input_player1_r_x_minus = "nul"
+input_player1_r_x_minus_axis = "nul"
+input_player1_r_x_minus_btn = "nul"
+input_player1_r_x_minus_mbtn = "nul"
+input_player1_r_x_plus = "nul"
+input_player1_r_x_plus_axis = "nul"
+input_player1_r_x_plus_btn = "nul"
+input_player1_r_x_plus_mbtn = "nul"
+input_player1_r_y_minus = "nul"
+input_player1_r_y_minus_axis = "nul"
+input_player1_r_y_minus_btn = "nul"
+input_player1_r_y_minus_mbtn = "nul"
+input_player1_r_y_plus = "nul"
+input_player1_r_y_plus_axis = "nul"
+input_player1_r_y_plus_btn = "nul"
+input_player1_r_y_plus_mbtn = "nul"
+input_player1_right = "right"
+input_player1_right_axis = "nul"
+input_player1_right_btn = "nul"
+input_player1_right_mbtn = "nul"
+input_player1_select = "rshift"
+input_player1_select_axis = "nul"
+input_player1_select_btn = "nul"
+input_player1_select_mbtn = "nul"
+input_player1_start = "enter"
+input_player1_start_axis = "nul"
+input_player1_start_btn = "nul"
+input_player1_start_mbtn = "nul"
+input_player1_turbo = "nul"
+input_player1_turbo_axis = "nul"
+input_player1_turbo_btn = "nul"
+input_player1_turbo_mbtn = "nul"
+input_player1_up = "up"
+input_player1_up_axis = "nul"
+input_player1_up_btn = "nul"
+input_player1_up_mbtn = "nul"
+input_player1_x = "s"
+input_player1_x_axis = "nul"
+input_player1_x_btn = "nul"
+input_player1_x_mbtn = "nul"
+input_player1_y = "a"
+input_player1_y_axis = "nul"
+input_player1_y_btn = "nul"
+input_player1_y_mbtn = "nul"
+input_poll_type_behavior = "1"
+input_preempt_toggle = "nul"
+input_preempt_toggle_axis = "nul"
+input_preempt_toggle_btn = "nul"
+input_preempt_toggle_mbtn = "nul"
+input_quit_gamepad_combo = "0"
+input_record_replay = "nul"
+input_record_replay_axis = "nul"
+input_record_replay_btn = "nul"
+input_record_replay_mbtn = "nul"
+input_recording_toggle = "nul"
+input_recording_toggle_axis = "nul"
+input_recording_toggle_btn = "nul"
+input_recording_toggle_mbtn = "nul"
+input_remap_binds_enable = "true"
+input_replay_slot_decrease = "nul"
+input_replay_slot_decrease_axis = "nul"
+input_replay_slot_decrease_btn = "nul"
+input_replay_slot_decrease_mbtn = "nul"
+input_replay_slot_increase = "nul"
+input_replay_slot_increase_axis = "nul"
+input_replay_slot_increase_btn = "nul"
+input_replay_slot_increase_mbtn = "nul"
+input_reset = "nul"
+input_reset_axis = "nul"
+input_reset_btn = "nul"
+input_reset_mbtn = "nul"
+input_rewind = "nul"
+input_rewind_axis = "nul"
+input_rewind_btn = "nul"
+input_rewind_mbtn = "nul"
+input_rumble_gain = "100"
+input_runahead_toggle = "nul"
+input_runahead_toggle_axis = "nul"
+input_runahead_toggle_btn = "nul"
+input_runahead_toggle_mbtn = "nul"
+input_save_state = "nul"
+input_save_state_axis = "nul"
+input_save_state_btn = "nul"
+input_save_state_mbtn = "nul"
+input_screenshot = "nul"
+input_screenshot_axis = "nul"
+input_screenshot_btn = "nul"
+input_screenshot_mbtn = "nul"
+input_send_debug_info = "nul"
+input_send_debug_info_axis = "nul"
+input_send_debug_info_btn = "nul"
+input_send_debug_info_mbtn = "nul"
+input_sensors_enable = "true"
+input_shader_next = "nul"
+input_shader_next_axis = "nul"
+input_shader_next_btn = "nul"
+input_shader_next_mbtn = "nul"
+input_shader_prev = "nul"
+input_shader_prev_axis = "nul"
+input_shader_prev_btn = "nul"
+input_shader_prev_mbtn = "nul"
+input_shader_toggle = "nul"
+input_shader_toggle_axis = "nul"
+input_shader_toggle_btn = "nul"
+input_shader_toggle_mbtn = "nul"
+input_state_slot_decrease = "nul"
+input_state_slot_decrease_axis = "nul"
+input_state_slot_decrease_btn = "nul"
+input_state_slot_decrease_mbtn = "nul"
+input_state_slot_increase = "nul"
+input_state_slot_increase_axis = "nul"
+input_state_slot_increase_btn = "nul"
+input_state_slot_increase_mbtn = "nul"
+input_streaming_toggle = "nul"
+input_streaming_toggle_axis = "nul"
+input_streaming_toggle_btn = "nul"
+input_streaming_toggle_mbtn = "nul"
+input_toggle_fast_forward = "nul"
+input_toggle_fast_forward_axis = "nul"
+input_toggle_fast_forward_btn = "nul"
+input_toggle_fast_forward_mbtn = "nul"
+input_toggle_fullscreen = "nul"
+input_toggle_fullscreen_axis = "nul"
+input_toggle_fullscreen_btn = "nul"
+input_toggle_fullscreen_mbtn = "nul"
+input_toggle_slowmotion = "nul"
+input_toggle_slowmotion_axis = "nul"
+input_toggle_slowmotion_btn = "nul"
+input_toggle_slowmotion_mbtn = "nul"
+input_toggle_statistics = "nul"
+input_toggle_statistics_axis = "nul"
+input_toggle_statistics_btn = "nul"
+input_toggle_statistics_mbtn = "nul"
+input_toggle_vrr_runloop = "nul"
+input_toggle_vrr_runloop_axis = "nul"
+input_toggle_vrr_runloop_btn = "nul"
+input_toggle_vrr_runloop_mbtn = "nul"
+input_volume_down = "nul"
+input_volume_down_axis = "nul"
+input_volume_down_btn = "nul"
+input_volume_down_mbtn = "nul"
+input_volume_up = "nul"
+input_volume_up_axis = "nul"
+input_volume_up_btn = "nul"
+input_volume_up_mbtn = "nul"
diff --git a/tests-other/autoconf/Testpad.cfg b/tests-other/autoconf/Testpad.cfg
new file mode 100644
index 00000000000..fecf9e23f65
--- /dev/null
+++ b/tests-other/autoconf/Testpad.cfg
@@ -0,0 +1,27 @@
+input_driver = "test"
+input_device = "Test joypad device"
+input_b_btn = "0"
+input_y_btn = "1"
+input_select_btn = "2"
+input_start_btn = "3"
+input_up_btn = "4"
+input_down_btn = "5"
+input_left_btn = "6"
+input_right_btn = "7"
+input_a_btn = "8"
+input_x_btn = "9"
+input_l_btn = "10"
+input_r_btn = "11"
+input_l2_btn = "12"
+input_r2_btn = "13"
+input_l3_btn = "14"
+input_r3_btn = "15"
+input_l_x_plus_axis = "+0"
+input_l_x_minus_axis = "-0"
+input_l_y_plus_axis = "+1"
+input_l_y_minus_axis = "-1"
+input_r_x_plus_axis = "+2"
+input_r_x_minus_axis = "-2"
+input_r_y_plus_axis = "+3"
+input_r_y_minus_axis = "-3"
+
diff --git a/cores/libretro-net-retropad/controllertest.ratst b/tests-other/netretropad_all_inputs.ratst
similarity index 100%
rename from cores/libretro-net-retropad/controllertest.ratst
rename to tests-other/netretropad_all_inputs.ratst
diff --git a/tests-other/test_input_joypad.ratst b/tests-other/test_input_joypad.ratst
new file mode 100644
index 00000000000..1b8d08222c8
--- /dev/null
+++ b/tests-other/test_input_joypad.ratst
@@ -0,0 +1,227 @@
+[
+{
+ "action": 1,
+ "param_num": 0,
+ "param_str": "Test joypad device"
+},
+{
+ "action": 1,
+ "param_num": 1,
+ "param_str": "Test joypad device",
+ "frame": 0
+},
+{
+ "action": 1,
+ "param_num": 2,
+ "param_str": "Test joypad device"
+},
+{
+ "action": 16,
+ "param_num": 256,
+ "frame": 330
+},
+{
+ "action": 32,
+ "param_num": 256
+},
+{
+ "action": 16,
+ "param_num": 1
+},
+{
+ "action": 32,
+ "param_num": 1
+},
+{
+ "action": 16,
+ "param_num": 512
+},
+{
+ "action": 32,
+ "param_num": 512
+},
+{
+ "action": 16,
+ "param_num": 2
+},
+{
+ "action": 32,
+ "param_num": 2
+},
+{
+ "action": 16,
+ "param_num": 4
+},
+{
+ "action": 32,
+ "param_num": 4
+},
+{
+ "action": 16,
+ "param_num": 8
+},
+{
+ "action": 32,
+ "param_num": 8
+},
+{
+ "action": 16,
+ "param_num": 16
+},
+{
+ "action": 32,
+ "param_num": 16
+},
+{
+ "action": 16,
+ "param_num": 32
+},
+{
+ "action": 32,
+ "param_num": 32
+},
+{
+ "action": 16,
+ "param_num": 64
+},
+{
+ "action": 32,
+ "param_num": 64
+},
+{
+ "action": 16,
+ "param_num": 128
+},
+{
+ "action": 32,
+ "param_num": 128
+},
+{
+ "action": 16,
+ "param_num": 1024
+},
+{
+ "action": 32,
+ "param_num": 1024
+},
+{
+ "action": 16,
+ "param_num": 2048
+},
+{
+ "action": 32,
+ "param_num": 2048
+},
+{
+ "action": 16,
+ "param_num": 4096
+},
+{
+ "action": 32,
+ "param_num": 4096
+},
+{
+ "action": 16,
+ "param_num": 8192
+},
+{
+ "action": 32,
+ "param_num": 8192
+},
+{
+ "action": 16,
+ "param_num": 16384
+},
+{
+ "action": 32,
+ "param_num": 16384
+},
+{
+ "action": 16,
+ "param_num": 32768
+},
+{
+ "action": 32,
+ "param_num": 32768
+},
+{
+ "action": 1001,
+ "param_num": 60000
+},
+{
+ "action": 1001,
+ "param_num": 32768
+},
+{
+ "action": 1001,
+ "param_num": 6000
+},
+{
+ "action": 1001,
+ "param_num": 32767
+},
+{
+ "action": 1001,
+ "param_num": 0
+},
+{
+ "action": 1000,
+ "param_num": 60000
+},
+{
+ "action": 1000,
+ "param_num": 32768
+},
+{
+ "action": 1000,
+ "param_num": 6000
+},
+{
+ "action": 1000,
+ "param_num": 32767
+},
+{
+ "action": 1000,
+ "param_num": 0
+},
+{
+ "action": 1003,
+ "param_num": 60000
+},
+{
+ "action": 1003,
+ "param_num": 32768
+},
+{
+ "action": 1003,
+ "param_num": 6000
+},
+{
+ "action": 1003,
+ "param_num": 32767
+},
+{
+ "action": 1003,
+ "param_num": 0
+},
+{
+ "action": 1002,
+ "param_num": 60000
+},
+{
+ "action": 1002,
+ "param_num": 32768
+},
+{
+ "action": 1002,
+ "param_num": 6000
+},
+{
+ "action": 1002,
+ "param_num": 32767
+},
+{
+ "action": 1002,
+ "param_num": 0
+}
+]
\ No newline at end of file
diff --git a/tests-other/testinput.cfg b/tests-other/testinput.cfg
new file mode 100644
index 00000000000..c3bf68f368b
--- /dev/null
+++ b/tests-other/testinput.cfg
@@ -0,0 +1,4 @@
+input_joypad_driver = "test"
+test_input_file_joypad = "tests-other/test_input_joypad.ratst"
+joypad_autoconfig_dir = "tests-other/autoconf"
+config_save_on_exit = "false"
\ No newline at end of file