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() {} // 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  ar.serialize("deltaY", deltaY);
45  ar.serialize("press", press);
46  ar.serialize("release", release);
47  }
48 private:
49  int deltaX, deltaY;
50  byte press, release;
51 };
52 REGISTER_POLYMORPHIC_CLASS(StateChange, TrackballState, "TrackballState");
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 string_ref 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 = targetDeltaY = 0;
94  currentDeltaX = 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 = Math::clip<-8, 7>(targetDeltaX - currentDeltaX);
139  currentDeltaX = 0;
140  } else {
141  targetDeltaY = Math::clip<-8, 7>(targetDeltaY - currentDeltaY);
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 const EmuDuration 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::signalEvent(const shared_ptr<const Event>& event,
208  EmuTime::param time)
209 {
210  switch (event->getType()) {
212  auto& mev = checked_cast<const MouseMotionEvent&>(*event);
213  static const 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  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  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  auto ts = dynamic_cast<TrackballState*>(event.get());
268  if (!ts) return;
269 
270  targetDeltaX = Math::clip<-8, 7>(targetDeltaX + ts->getDeltaX());
271  targetDeltaY = Math::clip<-8, 7>(targetDeltaY + ts->getDeltaY());
272  status = (status & ~ts->getPress()) | ts->getRelease();
273 }
274 
275 void Trackball::stopReplay(EmuTime::param time)
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  ar.serialize("targetDeltaX", targetDeltaX);
295  ar.serialize("targetDeltaY", targetDeltaY);
296  ar.serialize("currentDeltaX", currentDeltaX);
297  ar.serialize("currentDeltaY", currentDeltaY);
298  } else {
299  ar.serialize("deltaX", targetDeltaX);
300  ar.serialize("deltaY", targetDeltaY);
301  currentDeltaX = targetDeltaX;
302  currentDeltaY = targetDeltaY;
303  smooth = false;
304  }
305  ar.serialize("lastValue", lastValue);
306  ar.serialize("status", status);
307 
308  if (ar.isLoader() && isPluggedIn()) {
309  plugHelper(*getConnector(), EmuTime::dummy());
310  }
311 }
314 
315 } // namespace openmsx
static const unsigned RIGHT
Definition: InputEvents.hh:59
static EmuDuration msec(unsigned x)
Definition: EmuDuration.hh:38
static const int JOY_BUTTONB
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer,"CassettePlayer")
static param dummy()
Definition: EmuTime.hh:21
void unregisterListener(StateChangeListener &listener)
int clip(int x)
Clips x to the range [LO,HI].
Definition: Math.hh:34
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
const EmuTime & param
Definition: EmuTime.hh:20
void distributeNew(const EventPtr &event)
Deliver the event to all registered listeners MSX input devices should call the distributeNew() versi...
void registerListener(StateChangeListener &listener)
(Un)registers the given object to receive state change events.
void serialize(Archive &ar, unsigned)
Definition: Trackball.cc:40
void serialize(Archive &ar, unsigned version)
Definition: Trackball.cc:290
REGISTER_POLYMORPHIC_CLASS(DiskContainer, NowindRomDisk,"NowindRomDisk")
int getDeltaY() const
Definition: Trackball.cc:36
byte getPress() const
Definition: Trackball.cc:37
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:50
byte getRelease() const
Definition: Trackball.cc:38
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
static const unsigned LEFT
Definition: InputEvents.hh:57
unsigned char byte
8 bit unsigned integer
Definition: openmsx.hh:25
int getDeltaX() const
Definition: Trackball.cc:35
TrackballState(EmuTime::param time, int deltaX_, int deltaY_, byte press_, byte release_)
Definition: Trackball.cc:30
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
Definition: Pluggable.hh:44
static const int JOY_BUTTONA
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:802
Base class for all external MSX state changing events.
Definition: StateChange.hh:14
Trackball(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor)
Definition: Trackball.cc:55