OXYGENHUB
◆ main openmsx / src / input / Mouse.cc
10.3 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "Mouse.hh"
2
3
#include "Event.hh"
4
#include "MSXEventDistributor.hh"
5
#include "StateChange.hh"
6
#include "StateChangeDistributor.hh"
7
8
#include "Clock.hh"
9
#include "serialize.hh"
10
#include "serialize_meta.hh"
11
12
#include "Display.hh"
13
14
#include "gl_vec.hh"
15
16
#include "unreachable.hh"
17
18
#include <SDL.h>
19
20
#include <algorithm>
21
#include <cassert>
22
23
namespace openmsx {
24
25
static constexpr int THRESHOLD = 2;
26
static constexpr int SCALE = 2; // Magic number for old version archive, see StateChange.hh / serialize::serialize<MouseState>
27
static constexpr int PHASE_XHIGH1 = 0;
28
static constexpr int PHASE_XLOW1  = 1;
29
static constexpr int PHASE_YHIGH1 = 2;
30
static constexpr int PHASE_YLOW1  = 3;
31
static constexpr int PHASE_XHIGH2 = 4;
32
static constexpr int PHASE_XLOW2  = 5;
33
static constexpr int PHASE_YHIGH2 = 6;
34
static constexpr int PHASE_YLOW2  = 7;
35
static constexpr int STROBE = 0x04;
36
37
/** Convert host mouse movement to a whole number of MSX pixels. The part
38
  * that doesn't (yet) add up to a full MSX pixel is remembered in
39
  * 'fraction', so that slow movements don't get lost.
40
  * Note: rounding in a uniform direction (instead of towards zero) gives
41
  * smoother output.
42
  */
43
static gl::ivec2 scaleHostMotion(gl::ivec2 delta, gl::vec2 scale, gl::vec2& fraction)
44
{
45
	assert((scale.x > 0.0f) && (scale.y > 0.0f));
46
	auto value = gl::vec2(delta) + fraction;
47
	auto result = floor(value / scale);
48
	fraction = value - (gl::vec2(result) * scale);
49
	return result;
50
}
51
52
53
Mouse::Mouse(MSXEventDistributor& eventDistributor_,
54
             StateChangeDistributor& stateChangeDistributor_,
55
	         Display& display_)
56
	: eventDistributor(eventDistributor_)
57
	, stateChangeDistributor(stateChangeDistributor_)
58
	, display(display_)
59
	, phase(PHASE_YLOW2)
60
{
61
}
62
63
Mouse::~Mouse()
64
{
65
	if (isPluggedIn()) {
66
		Mouse::unplugHelper(EmuTime::dummy());
67
	}
68
}
69
70
71
// Pluggable
72
zstring_view Mouse::getName() const
73
{
74
	return "mouse";
75
}
76
77
zstring_view Mouse::getDescription() const
78
{
79
	return "MSX mouse";
80
}
81
82
void Mouse::plugHelper(Connector& /*connector*/, EmuTime time)
83
{
84
	if (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(SDL_BUTTON_LEFT)) {
85
		// left mouse button pressed, joystick emulation mode
86
		mouseMode = false;
87
	} else {
88
		// not pressed, mouse mode
89
		mouseMode = true;
90
		lastTime = time;
91
	}
92
	plugHelper2();
93
}
94
95
void Mouse::plugHelper2()
96
{
97
	eventDistributor.registerEventListener(*this);
98
	stateChangeDistributor.registerListener(*this);
99
}
100
101
void Mouse::unplugHelper(EmuTime /*time*/)
102
{
103
	stateChangeDistributor.unregisterListener(*this);
104
	eventDistributor.unregisterEventListener(*this);
105
}
106
107
// JoystickDevice
108
uint8_t Mouse::read(EmuTime /*time*/)
109
{
110
	if (mouseMode) {
111
		switch (phase) {
112
		case PHASE_XHIGH1: case PHASE_XHIGH2:
113
			return ((xRel >> 4) & 0x0F) | status;
114
		case PHASE_XLOW1: case PHASE_XLOW2:
115
			return  (xRel       & 0x0F) | status;
116
		case PHASE_YHIGH1: case PHASE_YHIGH2:
117
			return ((yRel >> 4) & 0x0F) | status;
118
		case PHASE_YLOW1: case PHASE_YLOW2:
119
			return  (yRel       & 0x0F) | status;
120
		default:
121
			UNREACHABLE;
122
		}
123
	} else {
124
		emulateJoystick();
125
		return status;
126
	}
127
}
128
129
void Mouse::emulateJoystick()
130
{
131
	status &= ~(JOY_UP | JOY_DOWN | JOY_LEFT | JOY_RIGHT);
132
133
	int deltaX = curXRel; curXRel = 0;
134
	int deltaY = curYRel; curYRel = 0;
135
	int absX = (deltaX > 0) ? deltaX : -deltaX;
136
	int absY = (deltaY > 0) ? deltaY : -deltaY;
137
138
	if ((absX < THRESHOLD) && (absY < THRESHOLD)) {
139
		return;
140
	}
141
142
	// tan(pi/8) ~= 5/12
143
	if (deltaX > 0) {
144
		if (deltaY > 0) {
145
			if ((12 * absX) > (5 * absY)) {
146
				status |= JOY_RIGHT;
147
			}
148
			if ((12 * absY) > (5 * absX)) {
149
				status |= JOY_DOWN;
150
			}
151
		} else {
152
			if ((12 * absX) > (5 * absY)) {
153
				status |= JOY_RIGHT;
154
			}
155
			if ((12 * absY) > (5 * absX)) {
156
				status |= JOY_UP;
157
			}
158
		}
159
	} else {
160
		if (deltaY > 0) {
161
			if ((12 * absX) > (5 * absY)) {
162
				status |= JOY_LEFT;
163
			}
164
			if ((12 * absY) > (5 * absX)) {
165
				status |= JOY_DOWN;
166
			}
167
		} else {
168
			if ((12 * absX) > (5 * absY)) {
169
				status |= JOY_LEFT;
170
			}
171
			if ((12 * absY) > (5 * absX)) {
172
				status |= JOY_UP;
173
			}
174
		}
175
	}
176
}
177
178
void Mouse::write(uint8_t value, EmuTime time)
179
{
180
	if (mouseMode) {
181
		// TODO figure out the exact timeout value. Is there even such
182
		//   an exact value or can it vary between different mouse
183
		//   models?
184
		//
185
		// Initially we used a timeout of 1 full second. This caused bug
186
		//    [3520394] Mouse behaves badly (unusable) in HiBrid
187
		// Slightly lowering the value to around 0.94s was already
188
		// enough to fix that bug. Later we found that to make FRS's
189
		// joytest program work we need a value that is less than the
190
		// duration of one (NTSC) frame. See bug
191
		//    #474 Mouse doesn't work properly on Joytest v2.2
192
		// We still don't know the exact value that an actual MSX mouse
193
		// uses, but 1.5ms is also the timeout value that is used for
194
		// JoyMega, so it seems like a reasonable value.
195
		if ((time - lastTime) > EmuDuration::usec(1500)) {
196
			// Timeout to YLOW2 so that MSX software bypassing the BIOS
197
			// and potentially not performing the alternate cycle
198
			// (which normally is used for trackball detection)
199
			// still gets continuous delta updates in each scan round
200
			phase = PHASE_YLOW2;
201
		}
202
		lastTime = time;
203
204
		switch (phase) {
205
		case PHASE_XHIGH1: case PHASE_XHIGH2:
206
		case PHASE_YHIGH1: case PHASE_YHIGH2:
207
			if ((value & STROBE) == 0) ++phase;
208
			break;
209
		case PHASE_XLOW1: case PHASE_XLOW2:
210
			if ((value & STROBE) != 0) ++phase;
211
			break;
212
		case PHASE_YLOW1:
213
			if ((value & STROBE) != 0) {
214
				phase = PHASE_XHIGH2;
215
				// Keep the delta zero in the alternate cycle of the scan round
216
				// to avoid trackball detection for large (absolute) values
217
				// Timeout resets the state to YLOW2 so that each scan round syncs to XHIGH1
218
				xRel = yRel = 0;
219
			}
220
			break;
221
		case PHASE_YLOW2:
222
			if ((value & STROBE) != 0) {
223
				phase = PHASE_XHIGH1;
224
#if 0
225
				// Real MSX mice don't have overflow protection,
226
				// verified on a Philips SBC3810 MSX mouse.
227
				xrel = curxrel; yrel = curyrel;
228
				curxrel = 0; curyrel = 0;
229
#else
230
				// Nevertheless we do emulate it here. See
231
				// sdsnatcher's post of 30 aug 2018 for a
232
				// motivation for this difference:
233
				//   https://github.com/openMSX/openMSX/issues/892
234
				// Only introduce the next delta when we are about to enter the main cycle
235
				// to avoid trackball detection in the alternate cycle
236
				// See https://github.com/openMSX/openMSX/pull/1791#discussion_r1860252195
237
				xRel = std::clamp(curXRel, -127, 127);
238
				yRel = std::clamp(curYRel, -127, 127);
239
				curXRel -= xRel;
240
				curYRel -= yRel;
241
#endif
242
			}
243
			break;
244
		}
245
	} else {
246
		// ignore
247
	}
248
}
249
250
251
// MSXEventListener
252
void Mouse::signalMSXEvent(const Event& event, EmuTime time) noexcept
253
{
254
	visit(overloaded{
255
		[&](const MouseMotionEvent& e) {
256
			if (e.getX() || e.getY()) {
257
				// Scale host mouse movement such that the MSX pointer
258
				// travels the same distance on screen as the host
259
				// pointer. The scale differs per axis because MSX
260
				// pixels are not drawn square (see 'horizontal_stretch').
261
				auto scale = display.getMsxPixelSize().value_or(gl::vec2(SCALE));
262
				auto delta = scaleHostMotion(gl::ivec2(e.getX(), e.getY()),
263
				                             scale, fractional);
264
265
				// Note: hostXY is negated when converting to MsxXY
266
				createMouseStateChange(time, -delta.x, -delta.y, 0, 0);
267
			}
268
		},
269
		[&](const MouseButtonDownEvent& e) {
270
			switch (e.getButton()) {
271
			case SDL_BUTTON_LEFT:
272
				createMouseStateChange(time, 0, 0, JOY_BUTTONA, 0);
273
				break;
274
			case SDL_BUTTON_RIGHT:
275
				createMouseStateChange(time, 0, 0, JOY_BUTTONB, 0);
276
				break;
277
			default:
278
				// ignore other buttons
279
				break;
280
			}
281
		},
282
		[&](const MouseButtonUpEvent& e) {
283
			switch (e.getButton()) {
284
			case SDL_BUTTON_LEFT:
285
				createMouseStateChange(time, 0, 0, 0, JOY_BUTTONA);
286
				break;
287
			case SDL_BUTTON_RIGHT:
288
				createMouseStateChange(time, 0, 0, 0, JOY_BUTTONB);
289
				break;
290
			default:
291
				// ignore other buttons
292
				break;
293
			}
294
		},
295
		[](const EventBase&) { /*ignore*/ }
296
	}, event);
297
}
298
299
void Mouse::createMouseStateChange(
300
	EmuTime time, int deltaX, int deltaY, uint8_t press, uint8_t release)
301
{
302
	stateChangeDistributor.distributeNew<MouseState>(
303
		time, deltaX, deltaY, press, release);
304
}
305
306
void Mouse::signalStateChange(const StateChange& event)
307
{
308
	const auto* ms = std::get_if<MouseState>(&event);
309
	if (!ms) return;
310
311
	// Verified with a real MSX-mouse (Philips SBC3810):
312
	//   this value is not clipped to -128 .. 127.
313
	curXRel += ms->getDeltaX();
314
	curYRel += ms->getDeltaY();
315
	status = (status & ~ms->getPress()) | ms->getRelease();
316
}
317
318
void Mouse::stopReplay(EmuTime time) noexcept
319
{
320
	// TODO read actual host mouse button state
321
	int dx = 0 - curXRel;
322
	int dy = 0 - curYRel;
323
	uint8_t release = (JOY_BUTTONA | JOY_BUTTONB) & ~status;
324
	if ((dx != 0) || (dy != 0) || (release != 0)) {
325
		createMouseStateChange(time, dx, dy, 0, release);
326
	}
327
}
328
329
// version 1: Initial version, the variables curXRel, curYRel and status were
330
//            not serialized.
331
// version 2: Also serialize the above variables, this is required for
332
//            record/replay, see comment in Keyboard.cc for more details.
333
// version 3: variables '(cur){x,y}rel' are scaled to MSX coordinates
334
// version 4: simplified type of 'lastTime' from Clock<> to EmuTime
335
template<typename Archive>
336
void Mouse::serialize(Archive& ar, unsigned version)
337
{
338
	// (Only) for loading old savestates
339
	MouseState::absHostX = MouseState::absHostY = 0;
340
341
	if constexpr (Archive::IS_LOADER) {
342
		if (isPluggedIn()) {
343
			// Do this early, because if something goes wrong while loading
344
			// some state below, then unplugHelper() gets called and that
345
			// will assert when plugHelper2() wasn't called yet.
346
			plugHelper2();
347
		}
348
	}
349
350
	if (ar.versionBelow(version, 4)) {
351
		assert(Archive::IS_LOADER);
352
		Clock<1000> tmp(EmuTime::zero());
353
		ar.serialize("lastTime", tmp);
354
		lastTime = tmp.getTime();
355
	} else {
356
		ar.serialize("lastTime", lastTime);
357
	}
358
	ar.serialize("faze",      phase, // TODO fix spelling if there's ever a need
359
	                                 // to bump the serialization verion
360
	             "xrel",      xRel,
361
	             "yrel",      yRel,
362
	             "mouseMode", mouseMode);
363
	if (ar.versionAtLeast(version, 2)) {
364
		ar.serialize("curxrel", curXRel,
365
		             "curyrel", curYRel,
366
		             "status",  status);
367
	}
368
	if (ar.versionBelow(version, 3)) {
369
		xRel    /= SCALE;
370
		yRel    /= SCALE;
371
		curXRel /= SCALE;
372
		curYRel /= SCALE;
373
374
	}
375
	// no need to serialize absHostX,Y
376
}
377
INSTANTIATE_SERIALIZE_METHODS(Mouse);
378
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, Mouse, "Mouse");
379
380
} // namespace openmsx