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