diff --git a/ui/xemu-input.c b/ui/xemu-input.c new file mode 100644 index 0000000000..36afa9d8ac --- /dev/null +++ b/ui/xemu-input.c @@ -0,0 +1,477 @@ +/* + * xemu Input Management + * + * Copyright (C) 2020 Matt Borgerson + * + * This program 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 Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +#include "xemu-input.h" +#include "xemu-notifications.h" +#include "xemu-settings.h" + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "hw/qdev-core.h" +#include "hw/qdev-properties.h" +#include "qapi/error.h" +#include "monitor/qdev.h" +#include "qapi/qmp/qdict.h" +#include "qemu/option.h" +#include "qemu/config-file.h" + +#if 0 +#define DPRINTF printf +#else +#define DPRINTF(...) +#endif + +int num_available_controllers; +struct controller_state *available_controllers; +struct controller_state *bound_controllers[4] = { NULL, NULL, NULL, NULL }; +int test_mode; + +static const enum xemu_settings_keys port_index_to_settings_key_map[] = { + XEMU_SETTINGS_INPUT_CONTROLLER_1_GUID, + XEMU_SETTINGS_INPUT_CONTROLLER_2_GUID, + XEMU_SETTINGS_INPUT_CONTROLLER_3_GUID, + XEMU_SETTINGS_INPUT_CONTROLLER_4_GUID, +}; + +void xemu_input_init(void) +{ + if (SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { + fprintf(stderr, "Failed to initialize SDL gamecontroller subsystem\n"); + exit(1); + } + + if (SDL_Init(SDL_INIT_HAPTIC) < 0) { + fprintf(stderr, "Failed to initialize SDL haptic subsystem\n"); + exit(1); + } + + // Create the keyboard input (always first) + struct controller_state *new_con = malloc(sizeof(struct controller_state)); + memset(new_con, 0, sizeof(struct controller_state)); + new_con->type = INPUT_DEVICE_SDL_KEYBOARD; + new_con->name = "Keyboard"; + new_con->bound = -1; + + // Check to see if we should auto-bind the keyboard + int port = xemu_input_get_controller_default_bind_port(new_con, 0); + if (port >= 0) { + xemu_input_bind(port, new_con, 0); + char buf[128]; + snprintf(buf, sizeof(buf), "Connected '%s' to port %d", new_con->name, port+1); + xemu_queue_notification(buf); + } + + available_controllers = new_con; + num_available_controllers = 1; +} + +int xemu_input_get_controller_default_bind_port(struct controller_state *state, int start) +{ + char guid[35] = { 0 }; + if (state->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + SDL_JoystickGetGUIDString(state->sdl_joystick_guid, guid, sizeof(guid)); + } else if (state->type == INPUT_DEVICE_SDL_KEYBOARD) { + snprintf(guid, sizeof(guid), "keyboard"); + } + + for (int i = start; i < 4; i++) { + const char *this_port; + xemu_settings_get_string(port_index_to_settings_key_map[i], &this_port); + if (strcmp(guid, this_port) == 0) { + return i; + } + } + + return -1; +} + +void xemu_input_process_sdl_events(const SDL_Event *event) +{ + if (event->type == SDL_CONTROLLERDEVICEADDED) { + DPRINTF("Controller Added: %d\n", event->cdevice.which); + + // Attempt to open the added controller + SDL_GameController *sdl_con; + sdl_con = SDL_GameControllerOpen(event->cdevice.which); + if (sdl_con == NULL) { + DPRINTF("Could not open joystick %d as a game controller\n", event->cdevice.which); + return; + } + + // Success! Create a new node to track this controller and continue init + struct controller_state *new_con = malloc(sizeof(struct controller_state)); + memset(new_con, 0, sizeof(struct controller_state)); + new_con->type = INPUT_DEVICE_SDL_GAMECONTROLLER; + new_con->name = SDL_GameControllerName(sdl_con); + new_con->sdl_gamecontroller = sdl_con; + new_con->sdl_joystick = SDL_GameControllerGetJoystick(new_con->sdl_gamecontroller); + new_con->sdl_joystick_id = SDL_JoystickInstanceID(new_con->sdl_joystick); + new_con->sdl_joystick_guid = SDL_JoystickGetGUID(new_con->sdl_joystick); + new_con->sdl_haptic = SDL_HapticOpenFromJoystick(new_con->sdl_joystick); + new_con->sdl_haptic_effect_id = -1; + new_con->bound = -1; + + char guid_buf[35] = { 0 }; + SDL_JoystickGetGUIDString(new_con->sdl_joystick_guid, guid_buf, sizeof(guid_buf)); + DPRINTF("Opened %s (%s)\n", new_con->name, guid_buf); + + // Add to the list of controllers + if (available_controllers == NULL) { + available_controllers = new_con; + } else { + struct controller_state *iter = available_controllers; + while (iter->next != NULL) iter = iter->next; + iter->next = new_con; + } + num_available_controllers++; + + // Do not replace binding for a currently bound device. In the case that + // the same GUID is specified multiple times, on different ports, allow + // any available port to be bound. + // + // This can happen naturally with X360 wireless receiver, in which each + // controller gets the same GUID (go figure). We cannot remember which + // controller is which in this case, but we can try to tolerate this + // situation by binding to any previously bound port with this GUID. The + // upside in this case is that a person can use the same GUID on all + // ports and just needs to bind to the receiver and never needs to hit + // this dialog. + int port = 0; + while (1) { + port = xemu_input_get_controller_default_bind_port(new_con, port); + if (port < 0) { + // No (additional) default mappings + break; + } + if (xemu_input_get_bound(port) != NULL) { + // Something already bound here, try again for another port + port++; + continue; + } + xemu_input_bind(port, new_con, 0); + char buf[128]; + snprintf(buf, sizeof(buf), "Connected '%s' to port %d", new_con->name, port+1); + xemu_queue_notification(buf); + break; + } + + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + DPRINTF("Controller Removed: %d\n", event->cdevice.which); + int handled = 0; + struct controller_state *iter, *prev; + for (iter=available_controllers, prev=NULL; iter != NULL; prev = iter, iter = iter->next) { + if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue; + + if (iter->sdl_joystick_id == event->cdevice.which) { + DPRINTF("Device removed: %s\n", iter->name); + + // Disconnect + if (iter->bound >= 0) { + // Queue a notification to inform user controller disconnected + // FIXME: Probably replace with a callback registration thing, + // but this works well enough for now. + char buf[128]; + snprintf(buf, sizeof(buf), "Port %d disconnected", iter->bound+1); + xemu_queue_notification(buf); + + // Unbind the controller, but don't save the unbinding in + // case the controller is reconnected + xemu_input_bind(iter->bound, NULL, 0); + } + + // Unlink + if (prev) { + prev->next = iter->next; + } else { + available_controllers = iter->next; + } + num_available_controllers--; + + // Deallocate + // FIXME: Check to release any SDL handles, etc + free(iter); + + handled = 1; + break; + } + } + if (!handled) { + DPRINTF("Could not find handle for joystick instance\n"); + } + } else if (event->type == SDL_CONTROLLERDEVICEREMAPPED) { + DPRINTF("Controller Remapped: %d\n", event->cdevice.which); + } +} + +void xemu_input_update_controllers(void) +{ + struct controller_state *iter; + for (iter=available_controllers; iter != NULL; iter=iter->next) { + if (iter->bound < 0) { + // Don't process unbound input devices + continue; + } + if (iter->type == INPUT_DEVICE_SDL_KEYBOARD) { + xemu_input_update_sdl_kbd_controller_state(iter); + } else if (iter->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + xemu_input_update_sdl_controller_state(iter); + xemu_input_update_rumble(iter); + } + } +} + +void xemu_input_update_sdl_kbd_controller_state(struct controller_state *state) +{ + state->buttons = 0; + memset(state->axis, 0, sizeof(state->axis)); + + const uint8_t *kbd = SDL_GetKeyboardState(NULL); + const int sdl_kbd_button_map[15] = { + SDL_SCANCODE_A, + SDL_SCANCODE_B, + SDL_SCANCODE_X, + SDL_SCANCODE_Y, + SDL_SCANCODE_LEFT, + SDL_SCANCODE_UP, + SDL_SCANCODE_RIGHT, + SDL_SCANCODE_DOWN, + SDL_SCANCODE_BACKSPACE, + SDL_SCANCODE_RETURN, + SDL_SCANCODE_1, + SDL_SCANCODE_2, + SDL_SCANCODE_3, + SDL_SCANCODE_4, + SDL_SCANCODE_5 + }; + + for (int i = 0; i < 15; i++) { + state->buttons |= kbd[sdl_kbd_button_map[i]] << i; + } + + /* + W = LTrig + E + S F + D + */ + if (kbd[SDL_SCANCODE_E]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = 32767; + if (kbd[SDL_SCANCODE_S]) state->axis[CONTROLLER_AXIS_LSTICK_X] = -32768; + if (kbd[SDL_SCANCODE_F]) state->axis[CONTROLLER_AXIS_LSTICK_X] = 32767; + if (kbd[SDL_SCANCODE_D]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = -32768; + if (kbd[SDL_SCANCODE_W]) state->axis[CONTROLLER_AXIS_LTRIG] = 32767; + + /* + O = RTrig + I + J L + K + */ + if (kbd[SDL_SCANCODE_I]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = 32767; + if (kbd[SDL_SCANCODE_J]) state->axis[CONTROLLER_AXIS_RSTICK_X] = -32768; + if (kbd[SDL_SCANCODE_L]) state->axis[CONTROLLER_AXIS_RSTICK_X] = 32767; + if (kbd[SDL_SCANCODE_K]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = -32768; + if (kbd[SDL_SCANCODE_O]) state->axis[CONTROLLER_AXIS_RTRIG] = 32767; +} + +void xemu_input_update_sdl_controller_state(struct controller_state *state) +{ + state->buttons = 0; + memset(state->axis, 0, sizeof(state->axis)); + + const SDL_GameControllerButton sdl_button_map[15] = { + SDL_CONTROLLER_BUTTON_A, + SDL_CONTROLLER_BUTTON_B, + SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_Y, + SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SDL_CONTROLLER_BUTTON_BACK, + SDL_CONTROLLER_BUTTON_START, + SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SDL_CONTROLLER_BUTTON_LEFTSTICK, + SDL_CONTROLLER_BUTTON_RIGHTSTICK, + SDL_CONTROLLER_BUTTON_GUIDE + }; + + for (int i = 0; i < 15; i++) { + state->buttons |= SDL_GameControllerGetButton(state->sdl_gamecontroller, sdl_button_map[i]) << i; + } + + const SDL_GameControllerAxis sdl_axis_map[6] = { + SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SDL_CONTROLLER_AXIS_TRIGGERRIGHT, + SDL_CONTROLLER_AXIS_LEFTX, + SDL_CONTROLLER_AXIS_LEFTY, + SDL_CONTROLLER_AXIS_RIGHTX, + SDL_CONTROLLER_AXIS_RIGHTY, + }; + + for (int i = 0; i < 6; i++) { + state->axis[i] = SDL_GameControllerGetAxis(state->sdl_gamecontroller, sdl_axis_map[i]); + } + + // FIXME: Check range + state->axis[CONTROLLER_AXIS_LSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_LSTICK_Y]; + state->axis[CONTROLLER_AXIS_RSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_RSTICK_Y]; +} + +void xemu_input_update_rumble(struct controller_state *state) +{ + if (state->sdl_haptic == NULL) { + // Haptic not supported for this joystick + return; + } + + memset(&state->sdl_haptic_effect, 0, sizeof(state->sdl_haptic_effect)); + state->sdl_haptic_effect.type = SDL_HAPTIC_LEFTRIGHT; + state->sdl_haptic_effect.periodic.length = 16; + state->sdl_haptic_effect.leftright.large_magnitude = state->rumble_l; + state->sdl_haptic_effect.leftright.small_magnitude = state->rumble_r; + if (state->sdl_haptic_effect_id == -1) { + state->sdl_haptic_effect_id = SDL_HapticNewEffect(state->sdl_haptic, &state->sdl_haptic_effect); + SDL_HapticRunEffect(state->sdl_haptic, state->sdl_haptic_effect_id, 1); + } else { + SDL_HapticUpdateEffect(state->sdl_haptic, state->sdl_haptic_effect_id, &state->sdl_haptic_effect); + } +} + +struct controller_state *xemu_input_get_bound(int index) +{ + return bound_controllers[index]; +} + +void xemu_input_bind(int index, struct controller_state *state, int save) +{ + // FIXME: Attempt to disable rumble when unbinding so it's not left + // in rumble mode + + // Unbind existing controller + if (bound_controllers[index]) { + assert(bound_controllers[index]->device != NULL); + Error *err = NULL; + qdev_unplug((DeviceState *)bound_controllers[index]->device, &err); + assert(err == NULL); + + bound_controllers[index]->bound = -1; + bound_controllers[index]->device = NULL; + bound_controllers[index] = NULL; + } + + // Save this controller's GUID in settings for auto re-connect + if (save) { + char guid_buf[35] = { 0 }; + if (state) { + if (state->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + SDL_JoystickGetGUIDString(state->sdl_joystick_guid, guid_buf, sizeof(guid_buf)); + } else if (state->type == INPUT_DEVICE_SDL_KEYBOARD) { + snprintf(guid_buf, sizeof(guid_buf), "keyboard"); + } + } + xemu_settings_set_string(port_index_to_settings_key_map[index], guid_buf); + xemu_settings_save(); + } + + // Bind new controller + if (state) { + if (state->bound >= 0) { + // Device was already bound to another port. Unbind it. + xemu_input_bind(state->bound, NULL, 1); + } + + bound_controllers[index] = state; + bound_controllers[index]->bound = index; + + const int port_map[4] = {3, 4, 1, 2}; + + QDict *qdict = qdict_new(); + + // Specify device driver + qdict_put_str(qdict, "driver", "usb-xbox-gamepad"); + + // Specify device identifier + char *tmp = g_strdup_printf("gamepad_%d", index+1); + qdict_put_str(qdict, "id", tmp); + g_free(tmp); + + // Specify index/port + qdict_put_int(qdict, "index", index); + qdict_put_int(qdict, "port", port_map[index]); + + // Create the device + QemuOpts *opts = qemu_opts_from_qdict(qemu_find_opts("device"), qdict, &error_abort); + DeviceState *dev = qdev_device_add(opts, &error_abort); + assert(dev); + + // Unref for eventual cleanup + qobject_unref(qdict); + object_unref(OBJECT(dev)); + + state->device = dev; + } +} + +#if 0 +static void xemu_input_print_controller_state(struct controller_state *state) +{ + DPRINTF(" A = %d, B = %d, X = %d, Y = %d\n" + " Left = %d, Up = %d, Right = %d, Down = %d\n" + " Back = %d, Start = %d, White = %d, Black = %d\n" + "Lstick = %d, Rstick = %d, Guide = %d\n" + "\n" + "LTrig = %.3f, RTrig = %.3f\n" + "LStickX = %.3f, RStickX = %.3f\n" + "LStickY = %.3f, RStickY = %.3f\n\n", + !!(state->buttons & CONTROLLER_BUTTON_A), + !!(state->buttons & CONTROLLER_BUTTON_B), + !!(state->buttons & CONTROLLER_BUTTON_X), + !!(state->buttons & CONTROLLER_BUTTON_Y), + !!(state->buttons & CONTROLLER_BUTTON_DPAD_LEFT), + !!(state->buttons & CONTROLLER_BUTTON_DPAD_UP), + !!(state->buttons & CONTROLLER_BUTTON_DPAD_RIGHT), + !!(state->buttons & CONTROLLER_BUTTON_DPAD_DOWN), + !!(state->buttons & CONTROLLER_BUTTON_BACK), + !!(state->buttons & CONTROLLER_BUTTON_START), + !!(state->buttons & CONTROLLER_BUTTON_WHITE), + !!(state->buttons & CONTROLLER_BUTTON_BLACK), + !!(state->buttons & CONTROLLER_BUTTON_LSTICK), + !!(state->buttons & CONTROLLER_BUTTON_RSTICK), + !!(state->buttons & CONTROLLER_BUTTON_GUIDE), + state->axis[CONTROLLER_AXIS_LTRIG], + state->axis[CONTROLLER_AXIS_RTRIG], + state->axis[CONTROLLER_AXIS_LSTICK_X], + state->axis[CONTROLLER_AXIS_RSTICK_X], + state->axis[CONTROLLER_AXIS_LSTICK_Y], + state->axis[CONTROLLER_AXIS_RSTICK_Y] + ); +} +#endif + + +void xemu_input_set_test_mode(int enabled) +{ + test_mode = enabled; +} + +int xemu_input_get_test_mode(void) +{ + return test_mode; +} diff --git a/ui/xemu-input.h b/ui/xemu-input.h new file mode 100644 index 0000000000..069dd705af --- /dev/null +++ b/ui/xemu-input.h @@ -0,0 +1,118 @@ +/* + * xemu Input Management + * + * This is the main input abstraction layer for xemu, which is basically just a + * wrapper around SDL2 GameController/Keyboard API to map specifically to an + * Xbox gamepad and support automatic binding, hotplugging, and removal at + * runtime. + * + * Copyright (C) 2020 Matt Borgerson + * + * This program 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 Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +#ifndef XEMU_INPUT_H +#define XEMU_INPUT_H + +#include + +enum controller_state_buttons_mask { + CONTROLLER_BUTTON_A = (1 << 0), + CONTROLLER_BUTTON_B = (1 << 1), + CONTROLLER_BUTTON_X = (1 << 2), + CONTROLLER_BUTTON_Y = (1 << 3), + CONTROLLER_BUTTON_DPAD_LEFT = (1 << 4), + CONTROLLER_BUTTON_DPAD_UP = (1 << 5), + CONTROLLER_BUTTON_DPAD_RIGHT = (1 << 6), + CONTROLLER_BUTTON_DPAD_DOWN = (1 << 7), + CONTROLLER_BUTTON_BACK = (1 << 8), + CONTROLLER_BUTTON_START = (1 << 9), + CONTROLLER_BUTTON_WHITE = (1 << 10), + CONTROLLER_BUTTON_BLACK = (1 << 11), + CONTROLLER_BUTTON_LSTICK = (1 << 12), + CONTROLLER_BUTTON_RSTICK = (1 << 13), + CONTROLLER_BUTTON_GUIDE = (1 << 14), // Extension +}; + +#define CONTROLLER_STATE_BUTTON_ID_TO_MASK(x) (1<