openMSX
Touchpad.cc
Go to the documentation of this file.
1 // This implementation is based on the reverse-engineering effort done
2 // by 'SD-snatcher'. See here:
3 // http://www.msx.org/news/en/msx-touchpad-protocol-reverse-engineered
4 // http://www.msx.org/wiki/Touchpad
5 // Also thanks to Manuel Bilderbeek for donating a Philips NMS-1150 that
6 // made this effort possible.
7 
8 #include "Touchpad.hh"
9 #include "MSXEventDistributor.hh"
11 #include "InputEvents.hh"
12 #include "StateChange.hh"
13 #include "Display.hh"
14 #include "OutputSurface.hh"
15 #include "CommandController.hh"
16 #include "CommandException.hh"
17 #include "Clock.hh"
18 #include "checked_cast.hh"
19 #include "serialize.hh"
20 #include "serialize_meta.hh"
21 #include "xrange.hh"
22 #include <iostream>
23 
24 using std::shared_ptr;
25 using namespace gl;
26 
27 namespace openmsx {
28 
29 class TouchpadState final : public StateChange
30 {
31 public:
32  TouchpadState() = default; // for serialize
33  TouchpadState(EmuTime::param time_,
34  byte x_, byte y_, bool touch_, bool button_)
35  : StateChange(time_)
36  , x(x_), y(y_), touch(touch_), button(button_) {}
37  [[nodiscard]] byte getX() const { return x; }
38  [[nodiscard]] byte getY() const { return y; }
39  [[nodiscard]] bool getTouch() const { return touch; }
40  [[nodiscard]] bool getButton() const { return button; }
41 
42  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
43  {
44  ar.template serializeBase<StateChange>(*this);
45  ar.serialize("x", x,
46  "y", y,
47  "touch", touch,
48  "button", button);
49  }
50 private:
51  byte x, y;
52  bool touch, button;
53 };
55 
56 
58  StateChangeDistributor& stateChangeDistributor_,
59  Display& display_,
60  CommandController& commandController)
61  : eventDistributor(eventDistributor_)
62  , stateChangeDistributor(stateChangeDistributor_)
63  , display(display_)
64  , transformSetting(commandController,
65  "touchpad_transform_matrix",
66  "2x3 matrix to transform host mouse coordinates to "
67  "MSX touchpad coordinates, see manual for details",
68  "{ 256 0 0 } { 0 256 0 }")
69  , start(EmuTime::zero())
70  , hostButtons(0)
71  , x(0), y(0), touch(false), button(false)
72  , shift(0), channel(0), last(0)
73 {
74  auto& interp = commandController.getInterpreter();
75  transformSetting.setChecker([this, &interp](TclObject& newValue) {
76  try {
77  parseTransformMatrix(interp, newValue);
78  } catch (CommandException& e) {
79  throw CommandException(
80  "Invalid transformation matrix: ", e.getMessage());
81  }
82  });
83  try {
84  parseTransformMatrix(interp, transformSetting.getValue());
85  } catch (CommandException& e) {
86  // should only happen when settings.xml was manually edited
87  std::cerr << e.getMessage() << '\n';
88  // fill in safe default values
89  m[0][0] = 256.0f; m[1][0] = 0.0f; m[2][0] = 0.0f;
90  m[0][1] = 0.0f; m[1][1] = 256.0f; m[2][1] = 0.0f;
91  }
92 }
93 
95 {
96  if (isPluggedIn()) {
97  Touchpad::unplugHelper(EmuTime::dummy());
98  }
99 }
100 
101 void Touchpad::parseTransformMatrix(Interpreter& interp, const TclObject& value)
102 {
103  if (value.getListLength(interp) != 2) {
104  throw CommandException("must have 2 rows");
105  }
106  for (auto i : xrange(2)) {
107  TclObject row = value.getListIndex(interp, i);
108  if (row.getListLength(interp) != 3) {
109  throw CommandException("each row must have 3 elements");
110  }
111  for (auto j : xrange(3)) {
112  m[j][i] = row.getListIndex(interp, j).getDouble(interp);
113  }
114  }
115 }
116 
117 // Pluggable
118 std::string_view Touchpad::getName() const
119 {
120  return "touchpad";
121 }
122 
123 std::string_view Touchpad::getDescription() const
124 {
125  return "MSX Touchpad";
126 }
127 
128 void Touchpad::plugHelper(Connector& /*connector*/, EmuTime::param /*time*/)
129 {
130  eventDistributor.registerEventListener(*this);
131  stateChangeDistributor.registerListener(*this);
132 }
133 
134 void Touchpad::unplugHelper(EmuTime::param /*time*/)
135 {
136  stateChangeDistributor.unregisterListener(*this);
137  eventDistributor.unregisterEventListener(*this);
138 }
139 
140 // JoystickDevice
142 constexpr byte EOC = JoystickDevice::RD_PIN2;
143 constexpr byte SO = JoystickDevice::RD_PIN3;
145 constexpr byte SCK = JoystickDevice::WR_PIN6;
146 constexpr byte SI = JoystickDevice::WR_PIN7;
147 constexpr byte CS = JoystickDevice::WR_PIN8;
148 
149 byte Touchpad::read(EmuTime::param time)
150 {
151  byte result = SENSE | BUTTON; // 1-bit means not pressed
152  if (touch) result &= ~SENSE;
153  if (button) result &= ~BUTTON;
154 
155  // EOC remains zero for 56 cycles after CS 0->1
156  // TODO at what clock frequency does the UPD7001 run?
157  // 400kHz is only the recommended value from the UPD7001 datasheet.
158  constexpr EmuDuration delta = Clock<400000>::duration(56);
159  if ((time - start) > delta) {
160  result |= EOC;
161  }
162 
163  if (shift & 0x80) result |= SO;
164  if (last & CS) result |= SO;
165 
166  return result | 0x30;
167 }
168 
169 void Touchpad::write(byte value, EmuTime::param time)
170 {
171  byte diff = last ^ value;
172  last = value;
173  if (diff & CS) {
174  if (value & CS) {
175  // CS 0->1
176  channel = shift & 3;
177  start = time; // to keep EOC=0 for 56 cycles
178  } else {
179  // CS 1->0
180  // Tested by SD-Snatcher (see RFE #252):
181  // When not touched X is always 0, and Y floats
182  // between 147 and 149 (mostly 148).
183  shift = (channel == 0) ? (touch ? x : 0)
184  : (channel == 3) ? (touch ? y : 148)
185  : 0; // channel 1 and 2 return 0
186  }
187  }
188  if (((value & (CS | SCK)) == SCK) && (diff & SCK)) {
189  // SC=0 & SCK 0->1
190  shift <<= 1;
191  shift |= (value & SI) != 0;
192  }
193 }
194 
195 ivec2 Touchpad::transformCoords(ivec2 xy)
196 {
197  if (auto* output = display.getOutputSurface()) {
198  vec2 uv = vec2(xy) / vec2(output->getLogicalSize());
199  xy = ivec2(m * vec3(uv, 1.0f));
200  }
201  return clamp(xy, 0, 255);
202 }
203 
204 // MSXEventListener
205 void Touchpad::signalMSXEvent(const shared_ptr<const Event>& event,
206  EmuTime::param time) noexcept
207 {
208  ivec2 pos = hostPos;
209  int b = hostButtons;
210  switch (event->getType()) {
212  const auto& mev = checked_cast<const MouseMotionEvent&>(*event);
213  pos = transformCoords(ivec2(mev.getAbsX(), mev.getAbsY()));
214  break;
215  }
217  const auto& butEv = checked_cast<const MouseButtonEvent&>(*event);
218  switch (butEv.getButton()) {
220  b |= 1;
221  break;
223  b |= 2;
224  break;
225  default:
226  // ignore other buttons
227  break;
228  }
229  break;
230  }
232  const auto& butEv = checked_cast<const MouseButtonEvent&>(*event);
233  switch (butEv.getButton()) {
235  b &= ~1;
236  break;
238  b &= ~2;
239  break;
240  default:
241  // ignore other buttons
242  break;
243  }
244  break;
245  }
246  default:
247  // ignore
248  break;
249  }
250  if ((pos != hostPos) || (b != hostButtons)) {
251  hostPos = pos;
252  hostButtons = b;
253  createTouchpadStateChange(
254  time, pos[0], pos[1],
255  (hostButtons & 1) != 0,
256  (hostButtons & 2) != 0);
257  }
258 }
259 
260 void Touchpad::createTouchpadStateChange(
261  EmuTime::param time, byte x_, byte y_, bool touch_, bool button_)
262 {
263  stateChangeDistributor.distributeNew(std::make_shared<TouchpadState>(
264  time, x_, y_, touch_, button_));
265 }
266 
267 // StateChangeListener
268 void Touchpad::signalStateChange(const shared_ptr<StateChange>& event)
269 {
270  if (auto* ts = dynamic_cast<TouchpadState*>(event.get())) {
271  x = ts->getX();
272  y = ts->getY();
273  touch = ts->getTouch();
274  button = ts->getButton();
275  }
276 }
277 
278 void Touchpad::stopReplay(EmuTime::param time) noexcept
279 {
280  // TODO Get actual mouse state. Is it worth the trouble?
281  if (x || y || touch || button) {
282  stateChangeDistributor.distributeNew(
283  std::make_shared<TouchpadState>(
284  time, 0, 0, false, false));
285  }
286 }
287 
288 
289 template<typename Archive>
290 void Touchpad::serialize(Archive& ar, unsigned /*version*/)
291 {
292  // no need to serialize hostX, hostY, hostButtons,
293  // transformSetting, m[][]
294  ar.serialize("start", start,
295  "x", x,
296  "y", y,
297  "touch", touch,
298  "button", button,
299  "shift", shift,
300  "channel", channel,
301  "last", last);
302 
303  if (ar.isLoader() && isPluggedIn()) {
304  plugHelper(*getConnector(), EmuTime::dummy());
305  }
306 }
309 
310 } // namespace openmsx
static constexpr EmuDuration duration(unsigned ticks)
Calculates the duration of the given number of ticks at this clock's frequency.
Definition: Clock.hh:35
virtual Interpreter & getInterpreter()=0
Represents the output window/screen of openMSX.
Definition: Display.hh:33
OutputSurface * getOutputSurface()
Definition: Display.cc:119
static constexpr int RD_PIN4
static constexpr int RD_PIN3
static constexpr int RD_PIN2
static constexpr int WR_PIN8
static constexpr int WR_PIN7
static constexpr int RD_PIN1
static constexpr int WR_PIN6
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
void unregisterEventListener(MSXEventListener &listener)
Unregisters a previously registered event listener.
const std::string & getMessage() const &
Definition: MSXException.hh:23
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 setChecker(std::function< void(TclObject &)> checkFunc_)
Set value-check-callback.
Definition: Setting.hh:160
const TclObject & getValue() const final
Gets the current value of this setting as a TclObject.
Definition: Setting.hh:142
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
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:125
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition: TclObject.cc:143
byte getY() const
Definition: Touchpad.cc:38
void serialize(Archive &ar, unsigned)
Definition: Touchpad.cc:42
byte getX() const
Definition: Touchpad.cc:37
bool getTouch() const
Definition: Touchpad.cc:39
TouchpadState(EmuTime::param time_, byte x_, byte y_, bool touch_, bool button_)
Definition: Touchpad.cc:33
bool getButton() const
Definition: Touchpad.cc:40
~Touchpad() override
Definition: Touchpad.cc:94
void serialize(Archive &ar, unsigned version)
Definition: Touchpad.cc:290
Definition: gl_mat.hh:23
vecN< 3, float > vec3
Definition: gl_vec.hh:140
vecN< 2, int > ivec2
Definition: gl_vec.hh:142
vecN< 2, float > vec2
Definition: gl_vec.hh:139
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
constexpr byte BUTTON
Definition: Touchpad.cc:144
@ 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 byte SCK
Definition: Touchpad.cc:145
constexpr byte EOC
Definition: Touchpad.cc:142
constexpr byte CS
Definition: Touchpad.cc:147
constexpr KeyMatrixPosition x
Keyboard bindings.
Definition: Keyboard.cc:124
constexpr byte SO
Definition: Touchpad.cc:143
constexpr byte SI
Definition: Touchpad.cc:146
constexpr byte SENSE
Definition: Touchpad.cc:141
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:983
#define REGISTER_POLYMORPHIC_CLASS(BASE, CLASS, NAME)
constexpr auto xrange(T e)
Definition: xrange.hh:155