OXYGENHUB
◆ main openmsx / src / input / Trackball.cc
9.1 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "Trackball.hh"
2
3
#include "Event.hh"
4
#include "MSXEventDistributor.hh"
5
#include "StateChange.hh"
6
#include "StateChangeDistributor.hh"
7
8
#include "serialize.hh"
9
#include "serialize_meta.hh"
10
11
#include "narrow.hh"
12
13
#include <algorithm>
14
15
// * Implementation based on information we received from 'n_n'.
16
//   It might not be 100% accurate. But games like 'Hole in one' already work.
17
// * Initially the 'trackball detection' code didn't work properly in openMSX
18
//   while it did work in meisei. Meisei had some special cases implemented for
19
//   the first read after reset. After some investigation I figured out some
20
//   code without special cases that also works as expected. Most software
21
//   seems to work now, though the detailed behaviour is still not tested
22
//   against the real hardware.
23
24
namespace openmsx {
25
26
Trackball::Trackball(MSXEventDistributor& eventDistributor_,
27
                     StateChangeDistributor& stateChangeDistributor_)
28
	: eventDistributor(eventDistributor_)
29
	, stateChangeDistributor(stateChangeDistributor_)
30
{
31
}
32
33
Trackball::~Trackball()
34
{
35
	if (isPluggedIn()) {
36
		Trackball::unplugHelper(EmuTime::dummy());
37
	}
38
}
39
40
41
// Pluggable
42
zstring_view Trackball::getName() const
43
{
44
	return "trackball";
45
}
46
47
zstring_view Trackball::getDescription() const
48
{
49
	return "MSX Trackball";
50
}
51
52
void Trackball::plugHelper(Connector& /*connector*/, EmuTime time)
53
{
54
	eventDistributor.registerEventListener(*this);
55
	stateChangeDistributor.registerListener(*this);
56
	lastSync = time;
57
	targetDeltaX = 0;
58
	targetDeltaY = 0;
59
	currentDeltaX = 0;
60
	currentDeltaY = 0;
61
}
62
63
void Trackball::unplugHelper(EmuTime /*time*/)
64
{
65
	stateChangeDistributor.unregisterListener(*this);
66
	eventDistributor.unregisterEventListener(*this);
67
}
68
69
// JoystickDevice
70
uint8_t Trackball::read(EmuTime time)
71
{
72
	// From the Sony GB-7 Service manual:
73
	//  http://cdn.preterhuman.net/texts/computing/msx/sonygb7sm.pdf
74
	// * The counter seems to be 8-bit wide, though only 4 bits (bit 7 and
75
	//   2-0) are connected to the MSX. Looking at the M60226 block diagram
76
	//   in more detail shows that the (up/down-)counters have a 'HP'
77
	//   input, in the GB7 this input is hardwired to GND. My *guess* is
78
	//   that HP stands for either 'Half-' or 'High-precision' and that it
79
	//   selects between either 4 or 8 bits saturation.
80
	//   The bug report '#477 Trackball emulation overflow values too
81
	//   easily' contains a small movie. Some very rough calculations
82
	//   indicate that when you move a cursor 50 times per second, 8 pixels
83
	//   per step, you get about the same speed as in that move.
84
	//   So even though both 4 and 8 bit clipping *seem* to be possible,
85
	//   this code only implements 4 bit clipping.
86
	// * It also contains a test program to read the trackball position.
87
	//   This program first reads the (X or Y) value and only then toggles
88
	//   pin 8. This seems to suggest the actual (X or Y) value is always
89
	//   present on reads and toggling pin 8 resets this value and switches
90
	//   to the other axis.
91
	syncCurrentWithTarget(time);
92
	auto delta = (lastValue & 4) ? currentDeltaY : currentDeltaX;
93
	return (status & ~0x0F) | ((delta + 8) & 0x0F);
94
}
95
96
void Trackball::write(uint8_t value, EmuTime time)
97
{
98
	syncCurrentWithTarget(time);
99
	uint8_t diff = lastValue ^ value;
100
	lastValue = value;
101
	if (diff & 0x4) {
102
		// pin 8 flipped
103
		if (value & 4) {
104
			targetDeltaX = narrow<int8_t>(std::clamp(targetDeltaX - currentDeltaX, -8, 7));
105
			currentDeltaX = 0;
106
		} else {
107
			targetDeltaY = narrow<int8_t>(std::clamp(targetDeltaY - currentDeltaY, -8, 7));
108
			currentDeltaY = 0;
109
		}
110
	}
111
}
112
113
void Trackball::syncCurrentWithTarget(EmuTime time)
114
{
115
	// In the past we only had 'targetDeltaXY' (was named 'deltaXY' then).
116
	// 'currentDeltaXY' was introduced to (slightly) smooth-out the
117
	// discrete host mouse movement events over time. This method performs
118
	// that smoothing calculation.
119
	//
120
	// In emulation, the trackball movement is driven by host mouse
121
	// movement events. These events are discrete. So for example if we
122
	// receive a host mouse event with offset (3,-4) we immediately adjust
123
	// 'targetDeltaXY' by that offset. However in reality mouse movement
124
	// doesn't make such discrete jumps. If you look at small enough time
125
	// intervals you'll see that the offset smoothly varies from (0,0) to
126
	// (3,-4).
127
	//
128
	// Most often this discretization step doesn't matter. However the BIOS
129
	// routine GTPAD to read mouse/trackball (more specifically PAD(12) and
130
	// PAD(16) has an heuristic to distinguish the mouse from the trackball
131
	// protocol. I won't go into detail, but in short it's reading the
132
	// mouse/trackball two times shortly after each other and checks
133
	// whether the offset of the 2nd read is centered around 0 (for mouse)
134
	// or 8 (for trackball). There's only about 1 millisecond between the
135
	// two reads, so in reality the mouse/trackball won't have moved much
136
	// during that time. However in emulation because of the discretization
137
	// we can get unlucky and do see a large offset for the 2nd read. This
138
	// confuses the BIOS (it thinks it's talking to a mouse instead of
139
	// trackball) and it results in erratic trackball movement.
140
	//
141
	// Thus to work around this problem (=make the heuristic in the BIOS
142
	// work) we smear-out host mouse events over (emulated) time. Instead
143
	// of immediately following the host movement, we limit changes in the
144
	// emulated offsets to a rate of 1 step per millisecond. This
145
	// introduces some delay, but usually (for not too fast movements) it's
146
	// not noticeable.
147
148
	if (!smooth) {
149
		// for backwards-compatible replay files
150
		currentDeltaX = targetDeltaX;
151
		currentDeltaY = targetDeltaY;
152
		return;
153
	}
154
155
	static constexpr auto INTERVAL = EmuDuration::msec(1);
156
157
	auto maxSteps = narrow<int>((time - lastSync) / INTERVAL);
158
	lastSync += INTERVAL * maxSteps;
159
160
	if (targetDeltaX >= currentDeltaX) {
161
		currentDeltaX = narrow<int8_t>(std::min<int>(currentDeltaX + maxSteps, targetDeltaX));
162
	} else {
163
		currentDeltaX = narrow<int8_t>(std::max<int>(currentDeltaX - maxSteps, targetDeltaX));
164
	}
165
	if (targetDeltaY >= currentDeltaY) {
166
		currentDeltaY = narrow<int8_t>(std::min<int>(currentDeltaY + maxSteps, targetDeltaY));
167
	} else {
168
		currentDeltaY = narrow<int8_t>(std::max<int>(currentDeltaY - maxSteps, targetDeltaY));
169
	}
170
}
171
172
// MSXEventListener
173
void Trackball::signalMSXEvent(const Event& event,
174
                               EmuTime time) noexcept
175
{
176
	visit(overloaded{
177
		[&](const MouseMotionEvent& e) {
178
			constexpr int SCALE = 2;
179
			int dx = e.getX() / SCALE;
180
			int dy = e.getY() / SCALE;
181
			if ((dx != 0) || (dy != 0)) {
182
				createTrackballStateChange(time, dx, dy, 0, 0);
183
			}
184
		},
185
		[&](const MouseButtonDownEvent& e) {
186
			switch (e.getButton()) {
187
			case SDL_BUTTON_LEFT:
188
				createTrackballStateChange(time, 0, 0, JOY_BUTTONA, 0);
189
				break;
190
			case SDL_BUTTON_RIGHT:
191
				createTrackballStateChange(time, 0, 0, JOY_BUTTONB, 0);
192
				break;
193
			default:
194
				// ignore other buttons
195
				break;
196
			}
197
		},
198
		[&](const MouseButtonUpEvent& e) {
199
			switch (e.getButton()) {
200
			case SDL_BUTTON_LEFT:
201
				createTrackballStateChange(time, 0, 0, 0, JOY_BUTTONA);
202
				break;
203
			case SDL_BUTTON_RIGHT:
204
				createTrackballStateChange(time, 0, 0, 0, JOY_BUTTONB);
205
				break;
206
			default:
207
				// ignore other buttons
208
				break;
209
			}
210
		},
211
		[](const EventBase&) { /*ignore*/ }
212
	}, event);
213
}
214
215
void Trackball::createTrackballStateChange(
216
	EmuTime time, int deltaX, int deltaY, uint8_t press, uint8_t release)
217
{
218
	stateChangeDistributor.distributeNew<TrackballState>(
219
		time, deltaX, deltaY, press, release);
220
}
221
222
// StateChangeListener
223
void Trackball::signalStateChange(const StateChange& event)
224
{
225
	const auto* ts = std::get_if<TrackballState>(&event);
226
	if (!ts) return;
227
228
	targetDeltaX = narrow<int8_t>(std::clamp(targetDeltaX + ts->getDeltaX(), -8, 7));
229
	targetDeltaY = narrow<int8_t>(std::clamp(targetDeltaY + ts->getDeltaY(), -8, 7));
230
	status = (status & ~ts->getPress()) | ts->getRelease();
231
}
232
233
void Trackball::stopReplay(EmuTime time) noexcept
234
{
235
	syncCurrentWithTarget(time);
236
	// TODO Get actual mouse button(s) state. Is it worth the trouble?
237
	uint8_t release = (JOY_BUTTONA | JOY_BUTTONB) & ~status;
238
	if ((currentDeltaX != 0) || (currentDeltaY != 0) || (release != 0)) {
239
		stateChangeDistributor.distributeNew<TrackballState>(
240
			time, -currentDeltaX, -currentDeltaY, uint8_t(0), release);
241
	}
242
}
243
244
// version 1: initial version
245
// version 2: replaced deltaXY with targetDeltaXY and currentDeltaXY
246
template<typename Archive>
247
void Trackball::serialize(Archive& ar, unsigned version)
248
{
249
	if (ar.versionAtLeast(version, 2)) {
250
		ar.serialize("lastSync",      lastSync,
251
		             "targetDeltaX",  targetDeltaX,
252
		             "targetDeltaY",  targetDeltaY,
253
		             "currentDeltaX", currentDeltaX,
254
		             "currentDeltaY", currentDeltaY);
255
	} else {
256
		ar.serialize("deltaX", targetDeltaX,
257
		             "deltaY", targetDeltaY);
258
		currentDeltaX = targetDeltaX;
259
		currentDeltaY = targetDeltaY;
260
		smooth = false;
261
	}
262
	ar.serialize("lastValue", lastValue,
263
	             "status",    status);
264
265
	if constexpr (Archive::IS_LOADER) {
266
		if (isPluggedIn()) {
267
			plugHelper(*getConnector(), EmuTime::dummy());
268
		}
269
	}
270
}
271
INSTANTIATE_SERIALIZE_METHODS(Trackball);
272
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, Trackball, "Trackball");
273
274
} // namespace openmsx