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