gtk: Support adding/removing joysticks at runtime

Reworked how/where SDL events are polled:
  - poll_joystick_events is now a static member of JoyDevice so it can be
    called from outside when needed (preference window for config and
    caliration).
  - S9xProcessEvents calls JoyDevice::poll_joystick_events directly so
    events are polled when no joysticks are attached.
  - JoyDevice::poll_joystick_events handles SDL_JOYDEVICE{ADDED,REMOVED}
    events.
  - Individual JoyDevice no longer call poll_joystick_events from
    get_events.

Reworked how attached joysticks are maintained in Snes9xConfig:
  - Use a map for joysticks keyed on SDL JoystickID (instance id in sdl
    parlance), which is stable while a joystick is attached instead of
    an array keyed on device_index.
    The instance id is what poll_joystick_events gets with every
    event (except for SDL_JOYDEVICEADDED which gets a device_index...)
    Instance id is an incrementing int starting from 0, they are never reused.
    i.e. each attach/dettach/attach cycle yields a new id.
    Whereas device index are reused and can "move".
  - On SDL_JOYDEVICEADDED the joystick is handed a "joynum", that is, an
    int from 0 to NUM_JOYPADS-1. A new joystick always get the lowest
    available joynum.
    (joynum was already a member of JoyDevice but wasn't initialized,
    this seemed like a proper way to use it.)
  - On SDL_JOYDEVICEREMOVED, the joystick associated with the instance
    id is simply removed from the map.

All this allows for the following behaviors.
It is possible to start without any joystick, add one joystick and it works.
(disconnect/reconnect cycles with a single joystick also work)

Joystick numbers are "stable" while they remain connected. For example:
 - Start with joystick0 and joystick1 connected
 - if joystick0 is disconnected, joystick1 keeps its number and keeps
   working
 - if joystick0 (or any new joystick) is connected at this time,
   it gets to become joystick0

If all joysticks are disconnected while snes9x is running, the order of
the "reconnections" will determine the joystick number of each joystick.

I think there is room for improvement still, with regards to code
organization. For instance, there could be a "JoyDevices" class which
would handle all the attached JoyDevice. This would allow moving all the
"joystick_*" methods from Snes9xConfig to that new class, and
poll_joystick_events could also be moved there.
The functionality wouldn't change, but the intent/ownership would probably be clearer.
This commit is contained in:
Jean Raby 2022-02-15 23:14:11 -05:00
parent d7dc9acf2f
commit cdbf783fc4
5 changed files with 124 additions and 56 deletions

View File

@ -209,20 +209,71 @@ int Snes9xConfig::load_defaults()
void Snes9xConfig::joystick_register_centers()
{
for (auto &j : joystick)
j.register_centers();
for (auto it=joysticks.begin(); it != joysticks.end(); it++)
it->second->register_centers();
}
void Snes9xConfig::flush_joysticks()
{
for (auto &j : joystick)
j.flush();
for (auto it=joysticks.begin(); it != joysticks.end(); it++)
it->second->flush();
}
void Snes9xConfig::set_joystick_mode(int mode)
{
for (auto &j : joystick)
j.mode = mode;
for (auto it=joysticks.begin(); it != joysticks.end(); it++)
it->second->mode = mode;
}
bool Snes9xConfig::joystick_add(int sdl_device_index)
{
// Find lowest unused joynum for new joystick
std::array<bool, NUM_JOYPADS> joynums;
int joynum = -1;
for (auto it = joysticks.begin(); it != joysticks.end(); it++)
{
joynums[it->second->joynum] = true;
}
for (unsigned int i=0; i< NUM_JOYPADS; i++)
{
if (!joynums[i])
{
joynum = i;
break;
}
}
if (joynum == -1)
{
printf("Joystick slots are full, cannot add joystick (device index %d)\n", sdl_device_index);
return false;
}
auto ujd = std::make_unique<JoyDevice>();
ujd->set_sdl_joystick(sdl_device_index, joynum);
printf("Joystick %d, %s", ujd->joynum+1, ujd->description.c_str());
joysticks[ujd->instance_id] = std::move(ujd);
return true;
}
bool Snes9xConfig::joystick_remove(SDL_JoystickID instance_id)
{
if (!joysticks.count(instance_id))
{
printf("joystick_remove: invalid instance id %d", instance_id);
return false;
}
printf("Removed joystick %d, %s", joysticks[instance_id]->joynum+1, joysticks[instance_id]->description.c_str());
joysticks.erase(instance_id);
return true;
}
JoyDevice *Snes9xConfig::joystick_get(SDL_JoystickID instance_id)
{
if (joysticks.count(instance_id)){
return joysticks[instance_id].get();
}
return NULL;
}
int Snes9xConfig::save_config_file()

View File

@ -61,6 +61,9 @@ class Snes9xConfig
void flush_joysticks();
void set_joystick_mode(int mode);
void joystick_register_centers();
bool joystick_add(int sdl_device_index);
bool joystick_remove(SDL_JoystickID instance_id);
JoyDevice *joystick_get(SDL_JoystickID instance_id);
/* Screen options */
bool full_screen_on_open;
@ -168,7 +171,8 @@ class Snes9xConfig
bool use_sync_control;
#endif
std::vector<JoyDevice> joystick;
std::map<SDL_JoystickID, std::unique_ptr<JoyDevice>> joysticks;
int joystick_threshold;
};

View File

@ -426,6 +426,39 @@ s9xcommand_t S9xGetPortCommandT(const char *name)
return cmd;
}
void JoyDevice::poll_joystick_events()
{
SDL_Event event;
JoyDevice *jd;
while (SDL_PollEvent(&event))
{
switch(event.type) {
case SDL_JOYAXISMOTION:
jd = gui_config->joystick_get(event.jaxis.which);
break;
case SDL_JOYHATMOTION:
jd = gui_config->joystick_get(event.jhat.which);
break;
case SDL_JOYBUTTONUP:
case SDL_JOYBUTTONDOWN:
jd = gui_config->joystick_get(event.jbutton.which);
break;
case SDL_JOYDEVICEADDED:
gui_config->joystick_add(event.jdevice.which);
continue;
case SDL_JOYDEVICEREMOVED:
gui_config->joystick_remove(event.jdevice.which);
continue;
}
if (jd)
{
jd->handle_event(&event);
}
}
}
void S9xProcessEvents(bool8 block)
{
JoyEvent event;
@ -433,11 +466,12 @@ void S9xProcessEvents(bool8 block)
if (S9xGrabJoysticks())
{
for (size_t i = 0; i < gui_config->joystick.size(); i++)
JoyDevice::poll_joystick_events();
for (auto it = gui_config->joysticks.begin(); it != gui_config->joysticks.end(); it++)
{
while (gui_config->joystick[i].get_event(&event))
while (it->second->get_event(&event))
{
binding = Binding(i, event.parameter, 0);
binding = Binding(it->second->joynum, event.parameter, 0);
S9xReportButton(binding.hex(), event.state == JOY_PRESSED ? 1 : 0);
gui_config->screensaver_needs_reset = true;
}
@ -447,37 +481,15 @@ void S9xProcessEvents(bool8 block)
}
}
static void poll_joystick_events()
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
if (event.type == SDL_JOYAXISMOTION)
{
gui_config->joystick[event.jaxis.which].handle_event(&event);
}
else if (event.type == SDL_JOYHATMOTION)
{
gui_config->joystick[event.jhat.which].handle_event(&event);
}
else if (event.type == SDL_JOYBUTTONUP ||
event.type == SDL_JOYBUTTONDOWN)
{
gui_config->joystick[event.jbutton.which].handle_event(&event);
}
}
}
void S9xInitInputDevices()
{
SDL_Init(SDL_INIT_JOYSTICK);
size_t num_joysticks = SDL_NumJoysticks();
gui_config->joystick.resize(num_joysticks);
for (size_t i = 0; i < num_joysticks; i++)
{
gui_config->joystick[i].set_sdl_joystick_num(i);
gui_config->joystick_add(i);
}
//First plug in both, they'll change later as needed
@ -487,7 +499,7 @@ void S9xInitInputDevices()
void S9xDeinitInputDevices()
{
gui_config->joystick.clear();
gui_config->joysticks.clear();
SDL_Quit();
}
@ -500,26 +512,28 @@ JoyDevice::JoyDevice()
JoyDevice::~JoyDevice()
{
if (enabled)
if (filedes)
{
SDL_JoystickClose(filedes);
}
}
bool JoyDevice::set_sdl_joystick_num(unsigned int device_num)
bool JoyDevice::set_sdl_joystick(unsigned int sdl_device_index, int new_joynum)
{
if ((int)device_num >= SDL_NumJoysticks())
if ((int)sdl_device_index >= SDL_NumJoysticks())
{
enabled = false;
return false;
}
filedes = SDL_JoystickOpen(device_num);
filedes = SDL_JoystickOpen(sdl_device_index);
if (!filedes)
return false;
enabled = true;
instance_id = SDL_JoystickInstanceID(filedes);
joynum = new_joynum;
int num_axes = SDL_JoystickNumAxes(filedes);
int num_hats = SDL_JoystickNumHats(filedes);
@ -534,12 +548,14 @@ bool JoyDevice::set_sdl_joystick_num(unsigned int device_num)
calibration[i].center = 0;
}
printf("Joystick %d, %s:\n %d axes, %d buttons, %d hats\n",
device_num + 1,
SDL_JoystickName(filedes),
SDL_JoystickNumButtons(filedes),
num_axes,
num_hats);
description = SDL_JoystickName(filedes);
description += ": ";
description += std::to_string(SDL_JoystickNumButtons(filedes));
description += " buttons, ";
description += std::to_string(num_axes);
description += " axes, ";
description += std::to_string(num_hats);
description += " hats\n";
for (auto &i : axis)
i = 0;
@ -732,8 +748,6 @@ void JoyDevice::handle_event(SDL_Event *event)
int JoyDevice::get_event(JoyEvent *event)
{
poll_events();
if (queue.empty())
return 0;
@ -745,11 +759,6 @@ int JoyDevice::get_event(JoyEvent *event)
return 1;
}
void JoyDevice::poll_events()
{
poll_joystick_events();
}
void JoyDevice::flush()
{
SDL_Event event;

View File

@ -100,9 +100,12 @@ class JoyDevice
void flush();
void handle_event(SDL_Event *event);
void register_centers();
bool set_sdl_joystick_num(unsigned int device_num);
bool set_sdl_joystick(unsigned int device_index, int slot);
static void poll_joystick_events();
std::string description;
SDL_Joystick *filedes;
SDL_JoystickID instance_id;
std::queue<JoyEvent> queue;
int mode;
int joynum;
@ -112,10 +115,10 @@ class JoyDevice
bool enabled;
private:
void poll_events();
void add_event(unsigned int parameter, unsigned int state);
};
void S9xDeinitInputDevices();
Binding S9xGetBindingByName(const char *name);
bool S9xIsMousePluggedIn();

View File

@ -50,15 +50,16 @@ gboolean poll_joystick(gpointer data)
Binding binding;
int focus;
for (size_t i = 0; i < window->config->joystick.size(); i++)
JoyDevice::poll_joystick_events();
for (auto it = window->config->joysticks.begin(); it != window->config->joysticks.end(); it++)
{
while (window->config->joystick[i].get_event(&event))
while (it->second->get_event(&event))
{
if (event.state == JOY_PRESSED)
{
if ((focus = window->get_focused_binding()) >= 0)
{
binding = Binding(i,
binding = Binding(it->second->joynum,
event.parameter,
window->config->joystick_threshold);