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