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 <iostream>
22 
23 using std::shared_ptr;
24 using namespace gl;
25 
26 namespace openmsx {
27 
28 class TouchpadState final : public StateChange
29 {
30 public:
31  TouchpadState() = default; // for serialize
32  TouchpadState(EmuTime::param time_,
33  byte x_, byte y_, bool touch_, bool button_)
34  : StateChange(time_)
35  , x(x_), y(y_), touch(touch_), button(button_) {}
36  byte getX() const { return x; }
37  byte getY() const { return y; }
38  bool getTouch() const { return touch; }
39  bool getButton() const { return button; }
40 
41  template<typename Archive> void serialize(Archive& ar, unsigned /*version*/)
42  {
43  ar.template serializeBase<StateChange>(*this);
44  ar.serialize("x", x,
45  "y", y,
46  "touch", touch,
47  "button", button);
48  }
49 private:
50  byte x, y;
51  bool touch, button;
52 };
54 
55 
57  StateChangeDistributor& stateChangeDistributor_,
58  Display& display_,
59  CommandController& commandController)
60  : eventDistributor(eventDistributor_)
61  , stateChangeDistributor(stateChangeDistributor_)
62  , display(display_)
63  , transformSetting(commandController,
64  "touchpad_transform_matrix",
65  "2x3 matrix to transform host mouse coordinates to "
66  "MSX touchpad coordinates, see manual for details",
67  "{ 256 0 0 } { 0 256 0 }")
68  , start(EmuTime::zero())
69  , hostButtons(0)
70  , x(0), y(0), touch(false), button(false)
71  , shift(0), channel(0), last(0)
72 {
73  auto& interp = commandController.getInterpreter();
74  transformSetting.setChecker([this, &interp](TclObject& newValue) {
75  try {
76  parseTransformMatrix(interp, newValue);
77  } catch (CommandException& e) {
78  throw CommandException(
79  "Invalid transformation matrix: ", e.getMessage());
80  }
81  });
82  try {
83  parseTransformMatrix(interp, transformSetting.getValue());
84  } catch (CommandException& e) {
85  // should only happen when settings.xml was manually edited
86  std::cerr << e.getMessage() << '\n';
87  // fill in safe default values
88  m[0][0] = 256.0f; m[1][0] = 0.0f; m[2][0] = 0.0f;
89  m[0][1] = 0.0f; m[1][1] = 256.0f; m[2][1] = 0.0f;
90  }
91 }
92 
94 {
95  if (isPluggedIn()) {
96  Touchpad::unplugHelper(EmuTime::dummy());
97  }
98 }
99 
100 void Touchpad::parseTransformMatrix(Interpreter& interp, const TclObject& value)
101 {
102  if (value.getListLength(interp) != 2) {
103  throw CommandException("must have 2 rows");
104  }
105  for (int i = 0; i < 2; ++i) {
106  TclObject row = value.getListIndex(interp, i);
107  if (row.getListLength(interp) != 3) {
108  throw CommandException("each row must have 3 elements");
109  }
110  for (int j = 0; j < 3; ++j) {
111  m[j][i] = row.getListIndex(interp, j).getDouble(interp);
112  }
113  }
114 }
115 
116 // Pluggable
117 const std::string& Touchpad::getName() const
118 {
119  static const std::string name("touchpad");
120  return name;
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
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)
207 {
208  ivec2 pos = hostPos;
209  int b = hostButtons;
210  switch (event->getType()) {
212  auto& mev = checked_cast<const MouseMotionEvent&>(*event);
213  pos = transformCoords(ivec2(mev.getAbsX(), mev.getAbsY()));
214  break;
215  }
217  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  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)
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
void setChecker(std::function< void(TclObject &)> checkFunc_)
Set value-check-callback.
Definition: Setting.hh:152
void serialize(Archive &ar, unsigned)
Definition: Touchpad.cc:41
static constexpr int RD_PIN1
REGISTER_POLYMORPHIC_INITIALIZER(Pluggable, CassettePlayer, "CassettePlayer")
Represents something you can plug devices into.
Definition: Connector.hh:20
const std::string & getMessage() const &
Definition: MSXException.hh:23
Represents the output window/screen of openMSX.
Definition: Display.hh:31
Connector * getConnector() const
Get the connector this Pluggable is plugged into.
Definition: Pluggable.hh:43
byte getX() const
Definition: Touchpad.cc:36
#define REGISTER_POLYMORPHIC_CLASS(BASE, CLASS, NAME)
constexpr byte SO
Definition: Touchpad.cc:143
bool isPluggedIn() const
Returns true if this pluggable is currently plugged into a connector.
Definition: Pluggable.hh:49
void unregisterListener(StateChangeListener &listener)
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
~Touchpad() override
Definition: Touchpad.cc:93
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:116
byte getY() const
Definition: Touchpad.cc:37
constexpr byte BUTTON
Definition: Touchpad.cc:144
static constexpr EmuDuration duration(unsigned ticks)
Calculates the duration of the given number of ticks at this clock&#39;s frequency.
Definition: Clock.hh:35
constexpr byte SCK
Definition: Touchpad.cc:145
static constexpr unsigned RIGHT
Definition: InputEvents.hh:64
bool getButton() const
Definition: Touchpad.cc:39
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.
constexpr byte SENSE
Definition: Touchpad.cc:141
bool getTouch() const
Definition: Touchpad.cc:38
constexpr byte EOC
Definition: Touchpad.cc:142
const TclObject & getValue() const final override
Gets the current value of this setting as a TclObject.
Definition: Setting.hh:135
void unregisterEventListener(MSXEventListener &listener)
Unregisters a previously registered event listener.
virtual Interpreter & getInterpreter()=0
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition: TclObject.cc:134
constexpr byte CS
Definition: Touchpad.cc:147
vecN< 2, int > ivec2
Definition: gl_vec.hh:142
constexpr byte SI
Definition: Touchpad.cc:146
void serialize(Archive &ar, unsigned version)
Definition: Touchpad.cc:290
double getDouble(Interpreter &interp) const
Definition: TclObject.cc:92
vecN< N, T > clamp(const vecN< N, T > &x, const vecN< N, T > &minVal, const vecN< N, T > &maxVal)
Definition: gl_vec.hh:296
static constexpr int RD_PIN2
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:981
static constexpr unsigned LEFT
Definition: InputEvents.hh:62
static constexpr int WR_PIN7
Base class for all external MSX state changing events.
Definition: StateChange.hh:13
vecN< 3, float > vec3
Definition: gl_vec.hh:140
static constexpr int RD_PIN3
constexpr KeyMatrixPosition x
Keyboard bindings.
Definition: Keyboard.cc:1377
static constexpr int WR_PIN6
OutputSurface * getOutputSurface()
Definition: Display.cc:118
vecN< 2, float > vec2
Definition: gl_vec.hh:139
static constexpr int WR_PIN8
Definition: gl_mat.hh:24
static constexpr int RD_PIN4
TouchpadState(EmuTime::param time_, byte x_, byte y_, bool touch_, bool button_)
Definition: Touchpad.cc:32