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