openMSX
JoyMega.cc
Go to the documentation of this file.
1 #include "JoyMega.hh"
2 #include "PluggingController.hh"
3 #include "MSXEventDistributor.hh"
5 #include "Event.hh"
6 #include "InputEventGenerator.hh"
7 #include "StateChange.hh"
8 #include "serialize.hh"
9 #include "serialize_meta.hh"
10 #include "unreachable.hh"
11 #include "xrange.hh"
12 #include "build-info.hh"
13 #include <memory>
14 
15 namespace openmsx {
16 
17 #if PLATFORM_ANDROID
18 constexpr int THRESHOLD = 32768 / 4;
19 #else
20 constexpr int THRESHOLD = 32768 / 10;
21 #endif
22 
24  StateChangeDistributor& stateChangeDistributor,
25  PluggingController& controller)
26 {
27 #ifdef SDL_JOYSTICK_DISABLED
28  (void)eventDistributor;
29  (void)stateChangeDistributor;
30  (void)controller;
31 #else
32  for (auto i : xrange(SDL_NumJoysticks())) {
33  if (SDL_Joystick* joystick = SDL_JoystickOpen(i)) {
34  // Avoid devices that have axes but no buttons, like accelerometers.
35  // SDL 1.2.14 in Linux has an issue where it rejects a device from
36  // /dev/input/event* if it has no buttons but does not reject a
37  // device from /dev/input/js* if it has no buttons, while
38  // accelerometers do end up being symlinked as a joystick in
39  // practice.
40  if (InputEventGenerator::joystickNumButtons(joystick) != 0) {
41  controller.registerPluggable(
42  std::make_unique<JoyMega>(
43  eventDistributor,
44  stateChangeDistributor,
45  joystick));
46  }
47  }
48  }
49 #endif
50 }
51 
52 class JoyMegaState final : public StateChange
53 {
54 public:
55  JoyMegaState() = default; // for serialize
56  JoyMegaState(EmuTime::param time_, unsigned joyNum_,
57  unsigned press_, unsigned release_)
58  : StateChange(time_)
59  , joyNum(joyNum_), press(press_), release(release_)
60  {
61  assert((press != 0) || (release != 0));
62  assert((press & release) == 0);
63  }
64  [[nodiscard]] unsigned getJoystick() const { return joyNum; }
65  [[nodiscard]] unsigned getPress() const { return press; }
66  [[nodiscard]] unsigned getRelease() const { return release; }
67 
68  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
69  {
70  ar.template serializeBase<StateChange>(*this);
71  ar.serialize("joyNum", joyNum,
72  "press", press,
73  "release", release);
74  }
75 private:
76  unsigned joyNum;
77  unsigned press, release;
78 };
80 
81 #ifndef SDL_JOYSTICK_DISABLED
82 // Note: It's OK to open/close the same SDL_Joystick multiple times (we open it
83 // once per MSX machine). The SDL documentation doesn't state this, but I
84 // checked the implementation and a SDL_Joystick uses a 'reference count' on
85 // the open/close calls.
87  StateChangeDistributor& stateChangeDistributor_,
88  SDL_Joystick* joystick_)
89  : eventDistributor(eventDistributor_)
90  , stateChangeDistributor(stateChangeDistributor_)
91  , joystick(joystick_)
92  , joyNum(SDL_JoystickInstanceID(joystick_))
93  , name("joymegaX") // 'X' is filled in below
94  , desc(SDL_JoystickName(joystick_))
95  , lastTime(EmuTime::zero())
96 {
97  const_cast<std::string&>(name)[7] = char('1' + joyNum);
98 }
99 
101 {
102  if (isPluggedIn()) {
103  JoyMega::unplugHelper(EmuTime::dummy());
104  }
105  SDL_JoystickClose(joystick);
106 }
107 
108 // Pluggable
109 std::string_view JoyMega::getName() const
110 {
111  return name;
112 }
113 
114 std::string_view JoyMega::getDescription() const
115 {
116  return desc;
117 }
118 
119 void JoyMega::plugHelper(Connector& /*connector*/, EmuTime::param /*time*/)
120 {
121  plugHelper2();
122  status = calcInitialState();
123  cycle = 0;
124  // when mode button is pressed when joystick is plugged in, then
125  // act as a 3-button joypad (otherwise 6-button)
126  cycleMask = (status & 0x800) ? 7 : 1;
127 }
128 
129 void JoyMega::plugHelper2()
130 {
131  eventDistributor.registerEventListener(*this);
132  stateChangeDistributor.registerListener(*this);
133 }
134 
135 void JoyMega::unplugHelper(EmuTime::param /*time*/)
136 {
137  stateChangeDistributor.unregisterListener(*this);
138  eventDistributor.unregisterEventListener(*this);
139 }
140 
141 
142 // JoystickDevice
143 byte JoyMega::read(EmuTime::param time)
144 {
145  // See http://segaretro.org/Control_Pad_(Mega_Drive)
146  // and http://frs.badcoffee.info/hardware/joymega-en.html
147  // for a detailed description of the MegaDrive joystick.
148  checkTime(time);
149  switch (cycle) {
150  case 0: case 2: case 4:
151  // up/down/left/right/b/c
152  return (status & 0x00f) | ((status & 0x060) >> 1);
153  case 1: case 3:
154  // up/down/0/0/a/start
155  return (status & 0x013) | ((status & 0x080) >> 2);
156  case 5:
157  // 0/0/0/0/a/start
158  return (status & 0x010) | ((status & 0x080) >> 2);
159  case 6:
160  // z/y/x/mode/b/c
161  return ((status & 0x400) >> 10) | // z
162  ((status & 0xA00) >> 8) | // start+y
163  ((status & 0x100) >> 6) | // x
164  ((status & 0x060) >> 1); // c+b
165  case 7:
166  // 1/1/1/1/a/start
167  return (status & 0x010) | ((status & 0x080) >> 2) | 0x0f;
168  default:
169  UNREACHABLE; return 0;
170  }
171 }
172 
173 void JoyMega::write(byte value, EmuTime::param time)
174 {
175  checkTime(time);
176  lastTime = time;
177  if (((value >> 2) & 1) != (cycle & 1)) {
178  cycle = (cycle + 1) & cycleMask;
179  }
180  assert(((value >> 2) & 1) == (cycle & 1));
181 }
182 
183 void JoyMega::checkTime(EmuTime::param time)
184 {
185  if ((time - lastTime) > EmuDuration::usec(1500)) {
186  // longer than 1.5ms since last write -> reset cycle
187  cycle = 0;
188  }
189 }
190 
191 static constexpr unsigned encodeButton(unsigned button, byte cycleMask)
192 {
193  unsigned n = (cycleMask == 7) ? 7 : 3; // 6- or 3-button mode
194  return 1 << (4 + (button & n));
195 }
196 
197 unsigned JoyMega::calcInitialState()
198 {
199  unsigned result = 0xfff;
200  int xAxis = SDL_JoystickGetAxis(joystick, 0);
201  if (xAxis < -THRESHOLD) {
202  result &= ~JOY_LEFT;
203  } else if (xAxis > THRESHOLD) {
204  result &= ~JOY_RIGHT;
205  }
206 
207  int yAxis = SDL_JoystickGetAxis(joystick, 1);
208  if (yAxis < -THRESHOLD) {
209  result &= ~JOY_UP;
210  } else if (yAxis > THRESHOLD) {
211  result &= ~JOY_DOWN;
212  }
213 
214  for (auto button : xrange(InputEventGenerator::joystickNumButtons(joystick))) {
215  if (InputEventGenerator::joystickGetButton(joystick, button)) {
216  result &= ~encodeButton(button, 7);
217  }
218  }
219  return result;
220 }
221 
222 // MSXEventListener
223 void JoyMega::signalMSXEvent(const Event& event, EmuTime::param time) noexcept
224 {
225  const auto* joyEvent = get_if<JoystickEvent>(event);
226  if (!joyEvent) return;
227 
228  // TODO: It would be more efficient to make a dispatcher instead of
229  // sending the event to all joysticks.
230  if (joyEvent->getJoystick() != joyNum) return;
231 
233  [&](const JoystickAxisMotionEvent& e) {
234  int value = e.getValue();
235  switch (e.getAxis() & 1) {
236  case JoystickAxisMotionEvent::X_AXIS: // Horizontal
237  if (value < -THRESHOLD) {
238  // left, not right
239  createEvent(time, JOY_LEFT, JOY_RIGHT);
240  } else if (value > THRESHOLD) {
241  // not left, right
242  createEvent(time, JOY_RIGHT, JOY_LEFT);
243  } else {
244  // not left, not right
245  createEvent(time, 0, JOY_LEFT | JOY_RIGHT);
246  }
247  break;
248  case JoystickAxisMotionEvent::Y_AXIS: // Vertical
249  if (value < -THRESHOLD) {
250  // up, not down
251  createEvent(time, JOY_UP, JOY_DOWN);
252  } else if (value > THRESHOLD) {
253  // not up, down
254  createEvent(time, JOY_DOWN, JOY_UP);
255  } else {
256  // not up, not down
257  createEvent(time, 0, JOY_UP | JOY_DOWN);
258  }
259  break;
260  default:
261  // ignore other axis
262  break;
263  }
264  },
265  [&](const JoystickButtonDownEvent& e) {
266  createEvent(time, encodeButton(e.getButton(), cycleMask), 0);
267  },
268  [&](const JoystickButtonUpEvent& e) {
269  createEvent(time, 0, encodeButton(e.getButton(), cycleMask));
270  },
271  [&](const EventBase&) { UNREACHABLE; }
272  }, event);
273 }
274 
275 void JoyMega::createEvent(EmuTime::param time, unsigned press, unsigned release)
276 {
277  unsigned newStatus = (status & ~press) | release;
278  createEvent(time, newStatus);
279 }
280 
281 void JoyMega::createEvent(EmuTime::param time, unsigned newStatus)
282 {
283  unsigned diff = status ^ newStatus;
284  if (!diff) {
285  // event won't actually change the status, so ignore it
286  return;
287  }
288  // make sure we create an event with minimal changes
289  unsigned press = status & diff;
290  unsigned release = newStatus & diff;
291  stateChangeDistributor.distributeNew<JoyMegaState>(
292  time, joyNum, press, release);
293 }
294 
295 // StateChangeListener
296 void JoyMega::signalStateChange(const StateChange& event)
297 {
298  const auto* js = dynamic_cast<const JoyMegaState*>(&event);
299  if (!js) return;
300 
301  // TODO: It would be more efficient to make a dispatcher instead of
302  // sending the event to all joysticks.
303  // TODO an alternative is to log events based on the connector instead
304  // of the joystick. That would make it possible to replay on a
305  // different host without an actual SDL joystick connected.
306  if (js->getJoystick() != joyNum) return;
307 
308  status = (status & ~js->getPress()) | js->getRelease();
309 }
310 
311 void JoyMega::stopReplay(EmuTime::param time) noexcept
312 {
313  createEvent(time, calcInitialState());
314 }
315 
316 template<typename Archive>
317 void JoyMega::serialize(Archive& ar, unsigned /*version*/)
318 {
319  ar.serialize("lastTime", lastTime,
320  "status", status,
321  "cycle", cycle,
322  "cycleMask", cycleMask);
323  if constexpr (Archive::IS_LOADER) {
324  if (isPluggedIn()) {
325  plugHelper2();
326  }
327  }
328 }
331 
332 #endif // SDL_JOYSTICK_DISABLED
333 
334 } // namespace openmsx
Represents something you can plug devices into.
Definition: Connector.hh:21
static constexpr EmuDuration usec(unsigned x)
Definition: EmuDuration.hh:43
static int joystickNumButtons(SDL_Joystick *joystick)
Normally the following two functions simply delegate to SDL_JoystickNumButtons() and SDL_JoystickGetB...
static bool joystickGetButton(SDL_Joystick *joystick, int button)
unsigned getPress() const
Definition: JoyMega.cc:65
unsigned getRelease() const
Definition: JoyMega.cc:66
JoyMegaState(EmuTime::param time_, unsigned joyNum_, unsigned press_, unsigned release_)
Definition: JoyMega.cc:56
unsigned getJoystick() const
Definition: JoyMega.cc:64
void serialize(Archive &ar, unsigned)
Definition: JoyMega.cc:68
void unplugHelper(EmuTime::param time) override
Definition: JoyMega.cc:135
JoyMega(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, SDL_Joystick *joystick)
Definition: JoyMega.cc:86
byte read(EmuTime::param time) override
Read from the joystick device.
Definition: JoyMega.cc:143
std::string_view getName() const override
Name used to identify this pluggable.
Definition: JoyMega.cc:109
void write(byte value, EmuTime::param time) override
Write a value to the joystick device.
Definition: JoyMega.cc:173
static void registerAll(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, PluggingController &controller)
Definition: JoyMega.cc:23
~JoyMega() override
Definition: JoyMega.cc:100
void plugHelper(Connector &connector, EmuTime::param time) override
Definition: JoyMega.cc:119
std::string_view getDescription() const override
Description for this pluggable.
Definition: JoyMega.cc:114
void serialize(Archive &ar, unsigned version)
Definition: JoyMega.cc:317
static constexpr unsigned X_AXIS
Definition: Event.hh:243
static constexpr unsigned Y_AXIS
Definition: Event.hh:244
static constexpr int JOY_RIGHT
static constexpr int JOY_LEFT
static constexpr int JOY_DOWN
static constexpr int JOY_UP
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
void unregisterEventListener(MSXEventListener &listener)
Unregisters a previously registered event listener.
bool isPluggedIn() const
Returns true if this pluggable is currently plugged into a connector.
Definition: Pluggable.hh:49
Central administration of Connectors and Pluggables.
void registerPluggable(std::unique_ptr< Pluggable > pluggable)
Add a Pluggable to the registry.
void registerListener(StateChangeListener &listener)
(Un)registers the given object to receive state change events.
void distributeNew(EmuTime::param time, Args &&...args)
Deliver the event to all registered listeners MSX input devices should call the distributeNew() versi...
void unregisterListener(StateChangeListener &listener)
Base class for all external MSX state changing events.
Definition: StateChange.hh:20
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr int THRESHOLD
Definition: JoyMega.cc:20
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
auto visit(Visitor &&visitor, const Event &event)
Definition: Event.hh:653
REGISTER_POLYMORPHIC_CLASS(StateChange, AutofireStateChange, "AutofireStateChange")
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:998
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr auto xrange(T e)
Definition: xrange.hh:155