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