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"
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
23using namespace gl;
24
25namespace openmsx {
26
27class TouchpadState final : public StateChange
28{
29public:
30 TouchpadState() = default; // for serialize
31 TouchpadState(EmuTime::param time_,
32 uint8_t x_, uint8_t y_, bool touch_, bool button_)
33 : StateChange(time_)
34 , x(x_), y(y_), touch(touch_), button(button_) {}
35 [[nodiscard]] uint8_t getX() const { return x; }
36 [[nodiscard]] uint8_t 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 }
48private:
49 uint8_t 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{
68 auto& interp = commandController.getInterpreter();
69 transformSetting.setChecker([this, &interp](const TclObject& newValue) {
70 try {
71 parseTransformMatrix(interp, newValue);
72 } catch (CommandException& e) {
73 throw CommandException(
74 "Invalid transformation matrix: ", e.getMessage());
75 }
76 });
77 try {
78 parseTransformMatrix(interp, transformSetting.getValue());
79 } catch (CommandException& e) {
80 // should only happen when settings.xml was manually edited
81 std::cerr << e.getMessage() << '\n';
82 // fill in safe default values
83 m[0][0] = 256.0f; m[1][0] = 0.0f; m[2][0] = 0.0f;
84 m[0][1] = 0.0f; m[1][1] = 256.0f; m[2][1] = 0.0f;
85 }
86}
87
89{
90 if (isPluggedIn()) {
91 Touchpad::unplugHelper(EmuTime::dummy());
92 }
93}
94
95void Touchpad::parseTransformMatrix(Interpreter& interp, const TclObject& value)
96{
97 if (value.getListLength(interp) != 2) {
98 throw CommandException("must have 2 rows");
99 }
100 for (auto i : xrange(2)) {
101 TclObject row = value.getListIndex(interp, i);
102 if (row.getListLength(interp) != 3) {
103 throw CommandException("each row must have 3 elements");
104 }
105 for (auto j : xrange(3)) {
106 m[j][i] = row.getListIndex(interp, j).getFloat(interp);
107 }
108 }
109}
110
111// Pluggable
112std::string_view Touchpad::getName() const
113{
114 return "touchpad";
115}
116
117std::string_view Touchpad::getDescription() const
118{
119 return "MSX Touchpad";
120}
121
122void Touchpad::plugHelper(Connector& /*connector*/, EmuTime::param /*time*/)
123{
124 eventDistributor.registerEventListener(*this);
125 stateChangeDistributor.registerListener(*this);
126}
127
128void Touchpad::unplugHelper(EmuTime::param /*time*/)
129{
130 stateChangeDistributor.unregisterListener(*this);
131 eventDistributor.unregisterEventListener(*this);
132}
133
134// JoystickDevice
135static constexpr uint8_t SENSE = JoystickDevice::RD_PIN1;
136static constexpr uint8_t EOC = JoystickDevice::RD_PIN2;
137static constexpr uint8_t SO = JoystickDevice::RD_PIN3;
138static constexpr uint8_t BUTTON = JoystickDevice::RD_PIN4;
139static constexpr uint8_t SCK = JoystickDevice::WR_PIN6;
140static constexpr uint8_t SI = JoystickDevice::WR_PIN7;
141static constexpr uint8_t CS = JoystickDevice::WR_PIN8;
142
143uint8_t Touchpad::read(EmuTime::param time)
144{
145 uint8_t result = SENSE | BUTTON; // 1-bit means not pressed
146 if (touch) result &= ~SENSE;
147 if (button) result &= ~BUTTON;
148
149 // EOC remains zero for 56 cycles after CS 0->1
150 // TODO at what clock frequency does the UPD7001 run?
151 // 400kHz is only the recommended value from the UPD7001 datasheet.
152 constexpr EmuDuration delta = Clock<400000>::duration(56);
153 if ((time - start) > delta) {
154 result |= EOC;
155 }
156
157 if (shift & 0x80) result |= SO;
158 if (last & CS) result |= SO;
159
160 return result | 0x30;
161}
162
163void Touchpad::write(uint8_t value, EmuTime::param time)
164{
165 uint8_t diff = last ^ value;
166 last = value;
167 if (diff & CS) {
168 if (value & CS) {
169 // CS 0->1
170 channel = shift & 3;
171 start = time; // to keep EOC=0 for 56 cycles
172 } else {
173 // CS 1->0
174 // Tested by SD-Snatcher (see RFE #252):
175 // When not touched X is always 0, and Y floats
176 // between 147 and 149 (mostly 148).
177 shift = (channel == 0) ? (touch ? x : 0)
178 : (channel == 3) ? (touch ? y : 148)
179 : 0; // channel 1 and 2 return 0
180 }
181 }
182 if (((value & (CS | SCK)) == SCK) && (diff & SCK)) {
183 // SC=0 & SCK 0->1
184 shift <<= 1;
185 shift |= (value & SI) != 0;
186 }
187}
188
189ivec2 Touchpad::transformCoords(ivec2 xy)
190{
191 if (const auto* output = display.getOutputSurface()) {
192 vec2 uv = vec2(xy) / vec2(output->getLogicalSize());
193 xy = ivec2(m * vec3(uv, 1.0f));
194 }
195 return clamp(xy, 0, 255);
196}
197
198// MSXEventListener
199void Touchpad::signalMSXEvent(const Event& event,
200 EmuTime::param time) noexcept
201{
202 ivec2 pos = hostPos;
203 auto b = hostButtons;
204
205 std::visit(overloaded{
206 [&](const MouseMotionEvent& e) {
207 pos = transformCoords(ivec2(e.getAbsX(), e.getAbsY()));
208 },
209 [&](const MouseButtonDownEvent& e) {
210 switch (e.getButton()) {
211 case SDL_BUTTON_LEFT:
212 b |= 1;
213 break;
214 case SDL_BUTTON_RIGHT:
215 b |= 2;
216 break;
217 default:
218 // ignore other buttons
219 break;
220 }
221 },
222 [&](const MouseButtonUpEvent& e) {
223 switch (e.getButton()) {
224 case SDL_BUTTON_LEFT:
225 b &= ~1;
226 break;
227 case SDL_BUTTON_RIGHT:
228 b &= ~2;
229 break;
230 default:
231 // ignore other buttons
232 break;
233 }
234 },
235 [](const EventBase&) { /*ignore*/ }
236 }, event);
237
238 if ((pos != hostPos) || (b != hostButtons)) {
239 hostPos = pos;
240 hostButtons = b;
241 createTouchpadStateChange(
242 time,
243 narrow_cast<uint8_t>(pos.x),
244 narrow_cast<uint8_t>(pos.y),
245 (hostButtons & 1) != 0,
246 (hostButtons & 2) != 0);
247 }
248}
249
250void Touchpad::createTouchpadStateChange(
251 EmuTime::param time, uint8_t x_, uint8_t y_, bool touch_, bool button_)
252{
253 stateChangeDistributor.distributeNew<TouchpadState>(
254 time, x_, y_, touch_, button_);
255}
256
257// StateChangeListener
258void Touchpad::signalStateChange(const StateChange& event)
259{
260 if (const auto* ts = dynamic_cast<const TouchpadState*>(&event)) {
261 x = ts->getX();
262 y = ts->getY();
263 touch = ts->getTouch();
264 button = ts->getButton();
265 }
266}
267
268void Touchpad::stopReplay(EmuTime::param time) noexcept
269{
270 // TODO Get actual mouse state. Is it worth the trouble?
271 if (x || y || touch || button) {
272 stateChangeDistributor.distributeNew<TouchpadState>(
273 time, uint8_t(0), uint8_t(0), false, false);
274 }
275}
276
277
278template<typename Archive>
279void Touchpad::serialize(Archive& ar, unsigned /*version*/)
280{
281 // no need to serialize hostX, hostY, hostButtons,
282 // transformSetting, m[][]
283 ar.serialize("start", start,
284 "x", x,
285 "y", y,
286 "touch", touch,
287 "button", button,
288 "shift", shift,
289 "channel", channel,
290 "last", last);
291
292 if constexpr (Archive::IS_LOADER) {
293 if (isPluggedIn()) {
294 plugHelper(*getConnector(), EmuTime::dummy());
295 }
296 }
297}
300
301} // 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:31
OutputSurface * getOutputSurface()
Definition Display.cc:97
static constexpr uint8_t WR_PIN8
static constexpr uint8_t RD_PIN1
static constexpr uint8_t WR_PIN6
static constexpr uint8_t RD_PIN3
static constexpr uint8_t RD_PIN4
static constexpr uint8_t WR_PIN7
static constexpr uint8_t RD_PIN2
void registerEventListener(MSXEventListener &listener)
Registers a given object to receive certain events.
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: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:146
const TclObject & getValue() const final
Gets the current value of this setting as a TclObject.
Definition Setting.hh:134
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.
unsigned getListLength(Interpreter &interp) const
Definition TclObject.cc:155
TclObject getListIndex(Interpreter &interp, unsigned index) const
Definition TclObject.cc:173
void serialize(Archive &ar, unsigned)
Definition Touchpad.cc:40
TouchpadState(EmuTime::param time_, uint8_t x_, uint8_t y_, bool touch_, bool button_)
Definition Touchpad.cc:31
bool getTouch() const
Definition Touchpad.cc:37
uint8_t getX() const
Definition Touchpad.cc:35
uint8_t getY() const
Definition Touchpad.cc:36
bool getButton() const
Definition Touchpad.cc:38
~Touchpad() override
Definition Touchpad.cc:88
void serialize(Archive &ar, unsigned version)
Definition Touchpad.cc:279
Touchpad(MSXEventDistributor &eventDistributor, StateChangeDistributor &stateChangeDistributor, Display &display, CommandController &commandController)
Definition Touchpad.cc:55
constexpr double e
Definition Math.hh:21
Definition gl_mat.hh:23
vecN< 3, float > vec3
Definition gl_vec.hh:383
vecN< 2, int > ivec2
Definition gl_vec.hh:385
vecN< 2, float > vec2
Definition gl_vec.hh:382
constexpr vecN< N, T > clamp(const vecN< N, T > &x, const vecN< N, T > &minVal, const vecN< N, T > &maxVal)
Definition gl_vec.hh:458
This file implemented 3 utility functions:
Definition Autofire.cc:11
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:445
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
#define REGISTER_POLYMORPHIC_INITIALIZER(BASE, CLASS, NAME)
#define REGISTER_POLYMORPHIC_CLASS(BASE, CLASS, NAME)
constexpr auto xrange(T e)
Definition xrange.hh:132