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