openMSX
Mouse.cc
Go to the documentation of this file.
1 #include "Mouse.hh"
2 #include "MSXEventDistributor.hh"
4 #include "Event.hh"
5 #include "StateChange.hh"
6 #include "Clock.hh"
7 #include "serialize.hh"
8 #include "serialize_meta.hh"
9 #include "unreachable.hh"
10 #include <SDL.h>
11 #include <algorithm>
12 
13 namespace openmsx {
14 
15 constexpr int TRESHOLD = 2;
16 constexpr int SCALE = 2;
17 constexpr int PHASE_XHIGH = 0;
18 constexpr int PHASE_XLOW = 1;
19 constexpr int PHASE_YHIGH = 2;
20 constexpr int PHASE_YLOW = 3;
21 constexpr int STROBE = 0x04;
22 
23 
24 class MouseState final : public StateChange
25 {
26 public:
27  MouseState() = default; // for serialize
28  MouseState(EmuTime::param time_, int deltaX_, int deltaY_,
29  byte press_, byte release_)
30  : StateChange(time_)
31  , deltaX(deltaX_), deltaY(deltaY_)
32  , press(press_), release(release_) {}
33  [[nodiscard]] int getDeltaX() const { return deltaX; }
34  [[nodiscard]] int getDeltaY() const { return deltaY; }
35  [[nodiscard]] byte getPress() const { return press; }
36  [[nodiscard]] byte getRelease() const { return release; }
37  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
38  {
39  ar.template serializeBase<StateChange>(*this);
40  ar.serialize("deltaX", deltaX,
41  "deltaY", deltaY,
42  "press", press,
43  "release", release);
44  }
45 private:
46  int deltaX, deltaY;
47  byte press, release;
48 };
49 
51 
52 Mouse::Mouse(MSXEventDistributor& eventDistributor_,
53  StateChangeDistributor& stateChangeDistributor_)
54  : eventDistributor(eventDistributor_)
55  , stateChangeDistributor(stateChangeDistributor_)
56  , lastTime(EmuTime::zero())
57 {
58  status = JOY_BUTTONA | JOY_BUTTONB;
59  phase = PHASE_YLOW;
60  xrel = yrel = curxrel = curyrel = 0;
61  absHostX = absHostY = 0;
62  mouseMode = true;
63 }
64 
66 {
67  if (isPluggedIn()) {
68  Mouse::unplugHelper(EmuTime::dummy());
69  }
70 }
71 
72 
73 // Pluggable
74 std::string_view Mouse::getName() const
75 {
76  return "mouse";
77 }
78 
79 std::string_view Mouse::getDescription() const
80 {
81  return "MSX mouse";
82 }
83 
84 void Mouse::plugHelper(Connector& /*connector*/, EmuTime::param time)
85 {
86  if (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) {
87  // left mouse button pressed, joystick emulation mode
88  mouseMode = false;
89  } else {
90  // not pressed, mouse mode
91  mouseMode = true;
92  lastTime = time;
93  }
94  plugHelper2();
95 }
96 
97 void Mouse::plugHelper2()
98 {
99  eventDistributor.registerEventListener(*this);
100  stateChangeDistributor.registerListener(*this);
101 }
102 
103 void Mouse::unplugHelper(EmuTime::param /*time*/)
104 {
105  stateChangeDistributor.unregisterListener(*this);
106  eventDistributor.unregisterEventListener(*this);
107 }
108 
109 
110 // JoystickDevice
111 byte Mouse::read(EmuTime::param /*time*/)
112 {
113  if (mouseMode) {
114  switch (phase) {
115  case PHASE_XHIGH:
116  return ((xrel >> 4) & 0x0F) | status;
117  case PHASE_XLOW:
118  return (xrel & 0x0F) | status;
119  case PHASE_YHIGH:
120  return ((yrel >> 4) & 0x0F) | status;
121  case PHASE_YLOW:
122  return (yrel & 0x0F) | status;
123  default:
124  UNREACHABLE; return 0;
125  }
126  } else {
127  emulateJoystick();
128  return status;
129  }
130 }
131 
132 void Mouse::emulateJoystick()
133 {
134  status &= ~(JOY_UP | JOY_DOWN | JOY_LEFT | JOY_RIGHT);
135 
136  int deltaX = curxrel; curxrel = 0;
137  int deltaY = curyrel; curyrel = 0;
138  int absX = (deltaX > 0) ? deltaX : -deltaX;
139  int absY = (deltaY > 0) ? deltaY : -deltaY;
140 
141  if ((absX < TRESHOLD) && (absY < TRESHOLD)) {
142  return;
143  }
144 
145  // tan(pi/8) ~= 5/12
146  if (deltaX > 0) {
147  if (deltaY > 0) {
148  if ((12 * absX) > (5 * absY)) {
149  status |= JOY_RIGHT;
150  }
151  if ((12 * absY) > (5 * absX)) {
152  status |= JOY_DOWN;
153  }
154  } else {
155  if ((12 * absX) > (5 * absY)) {
156  status |= JOY_RIGHT;
157  }
158  if ((12 * absY) > (5 * absX)) {
159  status |= JOY_UP;
160  }
161  }
162  } else {
163  if (deltaY > 0) {
164  if ((12 * absX) > (5 * absY)) {
165  status |= JOY_LEFT;
166  }
167  if ((12 * absY) > (5 * absX)) {
168  status |= JOY_DOWN;
169  }
170  } else {
171  if ((12 * absX) > (5 * absY)) {
172  status |= JOY_LEFT;
173  }
174  if ((12 * absY) > (5 * absX)) {
175  status |= JOY_UP;
176  }
177  }
178  }
179 }
180 
181 void Mouse::write(byte value, EmuTime::param time)
182 {
183  if (mouseMode) {
184  // TODO figure out the exact timeout value. Is there even such
185  // an exact value or can it vary between different mouse
186  // models?
187  //
188  // Initially we used a timeout of 1 full second. This caused bug
189  // [3520394] Mouse behaves badly (unusable) in HiBrid
190  // Slightly lowering the value to around 0.94s was already
191  // enough to fix that bug. Later we found that to make FRS's
192  // joytest program work we need a value that is less than the
193  // duration of one (NTSC) frame. See bug
194  // #474 Mouse doesn't work properly on Joytest v2.2
195  // We still don't know the exact value that an actual MSX mouse
196  // uses, but 1.5ms is also the timeout value that is used for
197  // JoyMega, so it seems like a reasonable value.
198  if ((time - lastTime) > EmuDuration::usec(1500)) {
199  phase = PHASE_YLOW;
200  }
201  lastTime = time;
202 
203  switch (phase) {
204  case PHASE_XHIGH:
205  if ((value & STROBE) == 0) phase = PHASE_XLOW;
206  break;
207  case PHASE_XLOW:
208  if ((value & STROBE) != 0) phase = PHASE_YHIGH;
209  break;
210  case PHASE_YHIGH:
211  if ((value & STROBE) == 0) phase = PHASE_YLOW;
212  break;
213  case PHASE_YLOW:
214  if ((value & STROBE) != 0) {
215  phase = PHASE_XHIGH;
216 #if 0
217  // Real MSX mice don't have overflow protection,
218  // verified on a Philips SBC3810 MSX mouse.
219  xrel = curxrel; yrel = curyrel;
220  curxrel = 0; curyrel = 0;
221 #else
222  // Nevertheless we do emulate it here. See
223  // sdsnatcher's post of 30 aug 2018 for a
224  // motivation for this difference:
225  // https://github.com/openMSX/openMSX/issues/892
226  xrel = std::clamp(curxrel, -127, 127);
227  yrel = std::clamp(curyrel, -127, 127);
228  curxrel -= xrel;
229  curyrel -= yrel;
230 #endif
231  }
232  break;
233  }
234  } else {
235  // ignore
236  }
237 }
238 
239 
240 // MSXEventListener
241 void Mouse::signalMSXEvent(const Event& event, EmuTime::param time) noexcept
242 {
244  [&](const MouseMotionEvent& e) {
245  if (e.getX() || e.getY()) {
246  // note: X/Y are negated, do this already in this
247  // routine to keep replays bw-compat. In a new
248  // savestate version it may (or may not) be cleaner
249  // to perform this operation closer to the MSX code.
250  createMouseStateChange(time, -e.getX(), -e.getY(), 0, 0);
251  }
252  },
253  [&](const MouseButtonDownEvent& e) {
254  switch (e.getButton()) {
256  createMouseStateChange(time, 0, 0, JOY_BUTTONA, 0);
257  break;
259  createMouseStateChange(time, 0, 0, JOY_BUTTONB, 0);
260  break;
261  default:
262  // ignore other buttons
263  break;
264  }
265  },
266  [&](const MouseButtonUpEvent& e) {
267  switch (e.getButton()) {
269  createMouseStateChange(time, 0, 0, 0, JOY_BUTTONA);
270  break;
272  createMouseStateChange(time, 0, 0, 0, JOY_BUTTONB);
273  break;
274  default:
275  // ignore other buttons
276  break;
277  }
278  },
279  [](const EventBase&) { /*ignore*/ }
280  }, event);
281 }
282 
283 void Mouse::createMouseStateChange(
284  EmuTime::param time, int deltaX, int deltaY, byte press, byte release)
285 {
286  stateChangeDistributor.distributeNew<MouseState>(
287  time, deltaX, deltaY, press, release);
288 }
289 
290 void Mouse::signalStateChange(const StateChange& event)
291 {
292  const auto* ms = dynamic_cast<const MouseState*>(&event);
293  if (!ms) return;
294 
295  // This is almost the same as
296  // relMsxXY = ms->getDeltaXY() / SCALE
297  // except that it doesn't accumulate rounding errors
298  int oldMsxX = absHostX / SCALE;
299  int oldMsxY = absHostY / SCALE;
300  absHostX += ms->getDeltaX();
301  absHostY += ms->getDeltaY();
302  int newMsxX = absHostX / SCALE;
303  int newMsxY = absHostY / SCALE;
304  int relMsxX = newMsxX - oldMsxX;
305  int relMsxY = newMsxY - oldMsxY;
306 
307  // Verified with a real MSX-mouse (Philips SBC3810):
308  // this value is not clipped to -128 .. 127.
309  curxrel += relMsxX;
310  curyrel += relMsxY;
311  status = (status & ~ms->getPress()) | ms->getRelease();
312 }
313 
314 void Mouse::stopReplay(EmuTime::param time) noexcept
315 {
316  // TODO read actual host mouse button state
317  int dx = 0 - curxrel;
318  int dy = 0 - curyrel;
319  byte release = (JOY_BUTTONA | JOY_BUTTONB) & ~status;
320  if ((dx != 0) || (dy != 0) || (release != 0)) {
321  createMouseStateChange(time, dx, dy, 0, release);
322  }
323 }
324 
325 // version 1: Initial version, the variables curxrel, curyrel and status were
326 // not serialized.
327 // version 2: Also serialize the above variables, this is required for
328 // record/replay, see comment in Keyboard.cc for more details.
329 // version 3: variables '(cur){x,y}rel' are scaled to MSX coordinates
330 // version 4: simplified type of 'lastTime' from Clock<> to EmuTime
331 template<typename Archive>
332 void Mouse::serialize(Archive& ar, unsigned version)
333 {
334  if constexpr (Archive::IS_LOADER) {
335  if (isPluggedIn()) {
336  // Do this early, because if something goes wrong while loading
337  // some state below, then unplugHelper() gets called and that
338  // will assert when plugHelper2() wasn't called yet.
339  plugHelper2();
340  }
341  }
342 
343  if (ar.versionBelow(version, 4)) {
344  assert(Archive::IS_LOADER);
345  Clock<1000> tmp(EmuTime::zero());
346  ar.serialize("lastTime", tmp);
347  lastTime = tmp.getTime();
348  } else {
349  ar.serialize("lastTime", lastTime);
350  }
351  ar.serialize("faze", phase, // TODO fix spelling if there's ever a need
352  // to bump the serialization verion
353  "xrel", xrel,
354  "yrel", yrel,
355  "mouseMode", mouseMode);
356  if (ar.versionAtLeast(version, 2)) {
357  ar.serialize("curxrel", curxrel,
358  "curyrel", curyrel,
359  "status", status);
360  }
361  if (ar.versionBelow(version, 3)) {
362  xrel /= SCALE;
363  yrel /= SCALE;
364  curxrel /= SCALE;
365  curyrel /= SCALE;
366 
367  }
368  // no need to serialize absHostX,Y
369 }
372 
373 } // namespace openmsx
Represents a clock with a fixed frequency.
Definition: Clock.hh:19
constexpr EmuTime::param getTime() const
Gets the time at which the last clock tick occurred.
Definition: Clock.hh:46
static constexpr EmuDuration usec(unsigned x)
Definition: EmuDuration.hh:43
static constexpr int JOY_BUTTONA
static constexpr int JOY_RIGHT
static constexpr int JOY_LEFT
static constexpr int JOY_DOWN
static constexpr int JOY_BUTTONB
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.
static constexpr unsigned RIGHT
Definition: Event.hh:140
static constexpr unsigned LEFT
Definition: Event.hh:138
void serialize(Archive &ar, unsigned)
Definition: Mouse.cc:37
MouseState(EmuTime::param time_, int deltaX_, int deltaY_, byte press_, byte release_)
Definition: Mouse.cc:28
int getDeltaX() const
Definition: Mouse.cc:33
byte getPress() const
Definition: Mouse.cc:35
int getDeltaY() const
Definition: Mouse.cc:34
byte getRelease() const
Definition: Mouse.cc:36
Mouse(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor)
Definition: Mouse.cc:52
~Mouse() override
Definition: Mouse.cc:65
void serialize(Archive &ar, unsigned version)
Definition: Mouse.cc:332
bool isPluggedIn() const
Returns true if this pluggable is currently plugged into a connector.
Definition: Pluggable.hh:49
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
constexpr vecN< N, T > clamp(const vecN< N, T > &x, const vecN< N, T > &minVal, const vecN< N, T > &maxVal)
Definition: gl_vec.hh:296
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr int PHASE_YLOW
Definition: Mouse.cc:20
constexpr int PHASE_XHIGH
Definition: Mouse.cc:17
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
auto visit(Visitor &&visitor, const Event &event)
Definition: Event.hh:653
constexpr int SCALE
Definition: ArkanoidPad.cc:24
constexpr int TRESHOLD
Definition: Mouse.cc:15
REGISTER_POLYMORPHIC_CLASS(StateChange, AutofireStateChange, "AutofireStateChange")
constexpr int STROBE
Definition: Mouse.cc:21
constexpr int PHASE_YHIGH
Definition: Mouse.cc:19
constexpr int PHASE_XLOW
Definition: Mouse.cc:18
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:998
#define UNREACHABLE
Definition: unreachable.hh:38