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