libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
WutInputDriver.cpp
1/****************************************************************************
2 * Platform Abstraction Layer (WUT driver)
3 * Daryl Borth 2026
4 * WutInputDriver.cpp
5 ***************************************************************************/
6#include "WutInputDriver.h"
7#include "../Platform.h"
8#include "../InputController.h"
9#include "../InputData.h"
10
11#include <vpad/input.h>
12#include <padscore/kpad.h>
13#include <padscore/wpad.h>
14#include <sysapp/switch.h>
15#include <cmath>
16#include <algorithm>
17
18// Full-strength pattern for real game/continuous rumble (force feedback).
19static uint8_t vpadRumblePattern[15] = {
20 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
21 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
22 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
23};
24
25// Lighter, shorter pattern for menu-hover ticks only - a light tap rather
26// than a full-strength buzz, since VPAD supports variable amplitude
27static uint8_t vpadMenuRumblePattern[6] = {
28 0x60, 0x60, 0x60, 0x60, 0x60, 0x60
29};
30
31static inline float clampf(float v, float lo, float hi) {
32 return (v < lo) ? lo : (v > hi) ? hi : v;
33}
34
35static void UpdateTiltAngles(float ax, float ay, float az, float& pitch, float& roll) {
36 const float kRadToDeg = 180.0f / (float)M_PI;
37 const float kMaxG = 1.1f;
38 const float x = clampf(ax, -1.0f, 1.0f);
39 const float y = clampf(ay, -1.0f, 1.0f);
40 const float z = clampf(az, -1.0f, 1.0f);
41
42 if (std::abs(ax) <= kMaxG) {
43 const float r = std::atan2(x, z) * kRadToDeg;
44 if (std::isfinite(r)) roll = r;
45 }
46 if (std::abs(ay) <= kMaxG) {
47 const float p = std::atan2(y, z) * kRadToDeg;
48 if (std::isfinite(p)) pitch = p;
49 }
50}
51
52// Copies the persistent per-profile state (held buttons + analog stick/
53// orientation) for hardware index `hw` from src into dst. Deliberately
54// excludes hw_buttons_d/hw_buttons_r - those are one-frame events
55static void CarryForwardHwProfile(InputPadData& dst, const InputPadData& src, uint32_t hw) {
56 dst.hw_connected[hw] = src.hw_connected[hw];
57 dst.hw_buttons_h[hw] = src.hw_buttons_h[hw];
58 dst.hw_stickX[hw] = src.hw_stickX[hw];
59 dst.hw_stickY[hw] = src.hw_stickY[hw];
60 dst.hw_substickX[hw] = src.hw_substickX[hw];
61 dst.hw_substickY[hw] = src.hw_substickY[hw];
62 dst.hw_triggerL[hw] = src.hw_triggerL[hw];
63 dst.hw_triggerR[hw] = src.hw_triggerR[hw];
64 dst.hw_gforceX[hw] = src.hw_gforceX[hw];
65 dst.hw_gforceY[hw] = src.hw_gforceY[hw];
66 dst.hw_gforceZ[hw] = src.hw_gforceZ[hw];
67 dst.hw_pitch[hw] = src.hw_pitch[hw];
68 dst.hw_roll[hw] = src.hw_roll[hw];
69 dst.hw_yaw[hw] = src.hw_yaw[hw];
70}
71
72/****************************************************************************
73 * Hardware Mapping Helpers
74 ***************************************************************************/
75static uint32_t MapVPADToGeneric(uint32_t vpad_btns) {
76 uint32_t mask = INPUT_BTN_NONE;
77 if (vpad_btns & VPAD_BUTTON_A) mask |= INPUT_BTN_A;
78 if (vpad_btns & VPAD_BUTTON_B) mask |= INPUT_BTN_B;
79 if (vpad_btns & VPAD_BUTTON_X) mask |= INPUT_BTN_X;
80 if (vpad_btns & VPAD_BUTTON_Y) mask |= INPUT_BTN_Y;
81 if (vpad_btns & VPAD_BUTTON_UP) mask |= INPUT_BTN_UP;
82 if (vpad_btns & VPAD_BUTTON_DOWN) mask |= INPUT_BTN_DOWN;
83 if (vpad_btns & VPAD_BUTTON_LEFT) mask |= INPUT_BTN_LEFT;
84 if (vpad_btns & VPAD_BUTTON_RIGHT) mask |= INPUT_BTN_RIGHT;
85 if (vpad_btns & VPAD_BUTTON_PLUS) mask |= INPUT_BTN_PLUS;
86 if (vpad_btns & VPAD_BUTTON_MINUS) mask |= INPUT_BTN_MINUS;
87 if (vpad_btns & VPAD_BUTTON_HOME) mask |= INPUT_BTN_HOME;
88 if (vpad_btns & VPAD_BUTTON_L) mask |= INPUT_TRIGGER_L;
89 if (vpad_btns & VPAD_BUTTON_R) mask |= INPUT_TRIGGER_R;
90 if (vpad_btns & VPAD_BUTTON_ZL) mask |= INPUT_TRIGGER_ZL;
91 if (vpad_btns & VPAD_BUTTON_ZR) mask |= INPUT_TRIGGER_ZR;
92 if (vpad_btns & VPAD_BUTTON_STICK_L) mask |= INPUT_THUMB_L;
93 if (vpad_btns & VPAD_BUTTON_STICK_R) mask |= INPUT_THUMB_R;
94 return mask;
95}
96
97static uint32_t MapKPADCoreToGeneric(uint32_t kpad_btns) {
98 uint32_t mask = INPUT_BTN_NONE;
99 if (kpad_btns & WPAD_BUTTON_A) mask |= INPUT_BTN_A;
100 if (kpad_btns & WPAD_BUTTON_B) mask |= INPUT_BTN_B;
101 if (kpad_btns & WPAD_BUTTON_1) mask |= INPUT_BTN_1;
102 if (kpad_btns & WPAD_BUTTON_2) mask |= INPUT_BTN_2;
103 if (kpad_btns & WPAD_BUTTON_UP) mask |= INPUT_BTN_UP;
104 if (kpad_btns & WPAD_BUTTON_DOWN) mask |= INPUT_BTN_DOWN;
105 if (kpad_btns & WPAD_BUTTON_LEFT) mask |= INPUT_BTN_LEFT;
106 if (kpad_btns & WPAD_BUTTON_RIGHT) mask |= INPUT_BTN_RIGHT;
107 if (kpad_btns & WPAD_BUTTON_PLUS) mask |= INPUT_BTN_PLUS;
108 if (kpad_btns & WPAD_BUTTON_MINUS) mask |= INPUT_BTN_MINUS;
109 if (kpad_btns & WPAD_BUTTON_HOME) mask |= INPUT_BTN_HOME;
110 return mask;
111}
112
113static uint32_t MapKPADProToGeneric(uint32_t pro_btns) {
114 uint32_t mask = INPUT_BTN_NONE;
115 if (pro_btns & WPAD_PRO_BUTTON_A) mask |= INPUT_BTN_A;
116 if (pro_btns & WPAD_PRO_BUTTON_B) mask |= INPUT_BTN_B;
117 if (pro_btns & WPAD_PRO_BUTTON_X) mask |= INPUT_BTN_X;
118 if (pro_btns & WPAD_PRO_BUTTON_Y) mask |= INPUT_BTN_Y;
119 if (pro_btns & WPAD_PRO_BUTTON_UP) mask |= INPUT_BTN_UP;
120 if (pro_btns & WPAD_PRO_BUTTON_DOWN) mask |= INPUT_BTN_DOWN;
121 if (pro_btns & WPAD_PRO_BUTTON_LEFT) mask |= INPUT_BTN_LEFT;
122 if (pro_btns & WPAD_PRO_BUTTON_RIGHT) mask |= INPUT_BTN_RIGHT;
123 if (pro_btns & WPAD_PRO_BUTTON_PLUS) mask |= INPUT_BTN_PLUS;
124 if (pro_btns & WPAD_PRO_BUTTON_MINUS) mask |= INPUT_BTN_MINUS;
125 if (pro_btns & WPAD_PRO_BUTTON_HOME) mask |= INPUT_BTN_HOME;
126 if (pro_btns & WPAD_PRO_TRIGGER_L) mask |= INPUT_TRIGGER_L;
127 if (pro_btns & WPAD_PRO_TRIGGER_R) mask |= INPUT_TRIGGER_R;
128 if (pro_btns & WPAD_PRO_TRIGGER_ZL) mask |= INPUT_TRIGGER_ZL;
129 if (pro_btns & WPAD_PRO_TRIGGER_ZR) mask |= INPUT_TRIGGER_ZR;
130 if (pro_btns & WPAD_PRO_BUTTON_STICK_L) mask |= INPUT_THUMB_L;
131 if (pro_btns & WPAD_PRO_BUTTON_STICK_R) mask |= INPUT_THUMB_R;
132 return mask;
133}
134
135static uint32_t MapKPADClassicToGeneric(uint32_t cls_btns) {
136 uint32_t mask = INPUT_BTN_NONE;
137 if (cls_btns & WPAD_CLASSIC_BUTTON_A) mask |= INPUT_BTN_A;
138 if (cls_btns & WPAD_CLASSIC_BUTTON_B) mask |= INPUT_BTN_B;
139 if (cls_btns & WPAD_CLASSIC_BUTTON_X) mask |= INPUT_BTN_X;
140 if (cls_btns & WPAD_CLASSIC_BUTTON_Y) mask |= INPUT_BTN_Y;
141 if (cls_btns & WPAD_CLASSIC_BUTTON_UP) mask |= INPUT_BTN_UP;
142 if (cls_btns & WPAD_CLASSIC_BUTTON_DOWN) mask |= INPUT_BTN_DOWN;
143 if (cls_btns & WPAD_CLASSIC_BUTTON_LEFT) mask |= INPUT_BTN_LEFT;
144 if (cls_btns & WPAD_CLASSIC_BUTTON_RIGHT) mask |= INPUT_BTN_RIGHT;
145 if (cls_btns & WPAD_CLASSIC_BUTTON_PLUS) mask |= INPUT_BTN_PLUS;
146 if (cls_btns & WPAD_CLASSIC_BUTTON_MINUS) mask |= INPUT_BTN_MINUS;
147 if (cls_btns & WPAD_CLASSIC_BUTTON_HOME) mask |= INPUT_BTN_HOME;
148 if (cls_btns & WPAD_CLASSIC_BUTTON_L) mask |= INPUT_TRIGGER_L;
149 if (cls_btns & WPAD_CLASSIC_BUTTON_R) mask |= INPUT_TRIGGER_R;
150 if (cls_btns & WPAD_CLASSIC_BUTTON_ZL) mask |= INPUT_TRIGGER_ZL;
151 if (cls_btns & WPAD_CLASSIC_BUTTON_ZR) mask |= INPUT_TRIGGER_ZR;
152 return mask;
153}
154
155static uint32_t MapKPADNunchukToGeneric(uint32_t wpad_btns) {
156 uint32_t mask = INPUT_BTN_NONE;
157
158 if (wpad_btns & WPAD_NUNCHUK_BUTTON_Z) mask |= INPUT_TRIGGER_ZL;
159 if (wpad_btns & WPAD_NUNCHUK_BUTTON_C) mask |= INPUT_TRIGGER_L;
160
161 return mask;
162}
163
164// One Euro Filter tuning for the Wiimote IR pointer (see OneEuroFilter.h).
165// KPAD delivers samples much faster than our ~60Hz update rate, so without
166// smoothing the raw per-sample noise (and the coarser stride of only
167// consuming one sample per frame) reads as a jerky cursor compared to
168// libogc's WPAD IR handling on Wii, which is already smoothed internally.
169//
170// minCutoff: smoothing strength when the pointer is essentially still.
171// Lower = less jitter at rest but more lag when motion starts. Chosen to
172// noticeably calm hand-tremor-level jitter without feeling "stuck".
173// beta: how quickly smoothing backs off as speed increases, in units of
174// screen-pixels/sec of underlying signal speed. Tuned so a deliberate,
175// fast pointer swipe across the screen tracks with negligible lag while a
176// small idle wobble is still heavily damped.
177static constexpr float IR_MIN_CUTOFF = 0.8f;
178static constexpr float IR_BETA = 0.015f;
179
180WutInputDriver::WutInputDriver() : drcTouchedPrev(false), drcLastTouchX(0.0f), drcLastTouchY(0.0f) {
181 for (int i = 0; i < 4; i++) {
182 rumbleRequest[i] = continuousRumble[i] = false;
183 menuRumbleFrames[i] = continuousRumbleCount[i] = silenceFrames[i] = menuRumbleGapFrames[i] = 0;
184 irFilterX[i].setParams(IR_MIN_CUTOFF, IR_BETA);
185 irFilterY[i].setParams(IR_MIN_CUTOFF, IR_BETA);
186 irSmoothInit[i] = false;
187 }
188}
189
190// KPADReadEx can return multiple buffered samples per call (newest to oldest).
191// We only need a small buffer - just enough to detect/skip a KPAD_ERROR_NO_SAMPLES
192// frame without discarding real motion.
193static constexpr uint32_t KPAD_SAMPLE_BUFFER = 4;
194
195WutInputDriver::~WutInputDriver() {
196 shutdown();
197}
198
200 KPADInit();
201 WPADEnableURCC(TRUE);
202 WPADEnableWiiRemote(TRUE);
203 VPADInit();
204 OSEnableHomeButtonMenu(FALSE);
205 InitUserInputControllers();
206}
207
209 for (int i = 0; i < 4; i++) {
210 WPADControlMotor((WPADChan)i, FALSE);
211 rumbleRequest[i] = continuousRumble[i] = false;
212 menuRumbleFrames[i] = continuousRumbleCount[i] = silenceFrames[i] = menuRumbleGapFrames[i] = 0;
213 }
214 VPADStopMotor(VPAD_CHAN_0);
215
216 // Restore the system default before handing control back to the OS/loader.
217 OSEnableHomeButtonMenu(TRUE);
218}
219
221 _SYSSwitchToHBMWithMode(0);
222}
223
224void WutInputDriver::setRumble(int channel, bool rumble) {
225 if (channel >= 0 && channel < 4) {
226 rumbleRequest[channel] = rumble;
227 }
228}
229
230void WutInputDriver::setContinuousRumble(int channel, bool continuous) {
231 if (channel >= 0 && channel < 4) continuousRumble[channel] = continuous;
232}
233
235 float screenWidth = (float)platform->getVideo()->getScreenWidth();
236 float screenHeight = (float)platform->getVideo()->getScreenHeight();
237
238 for (int i = 3; i >= 0; i--) {
239 InputPadData padData;
240
241 // VPAD Processing (GamePad, Channel 0 Only)
242 if (i == 0) {
243 VPADStatus vpadStatus;
244 VPADReadError vpadError;
245 VPADRead(VPAD_CHAN_0, &vpadStatus, 1, &vpadError);
246
247 if (vpadError == VPAD_READ_SUCCESS) {
248 padData.hw_connected[INPUT_HW_DRC] = true;
249 padData.battery_level = vpadStatus.battery * 25; // normalize to 0-100
250
251 padData.hw_buttons_d[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.trigger);
252 padData.hw_buttons_h[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.hold);
253 padData.hw_buttons_r[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.release);
254
255 padData.hw_stickX[INPUT_HW_DRC] = clampf(vpadStatus.leftStick.x, -1.0f, 1.0f);
256 padData.hw_stickY[INPUT_HW_DRC] = clampf(vpadStatus.leftStick.y, -1.0f, 1.0f);
257 padData.hw_substickX[INPUT_HW_DRC] = clampf(vpadStatus.rightStick.x, -1.0f, 1.0f);
258 padData.hw_substickY[INPUT_HW_DRC] = clampf(vpadStatus.rightStick.y, -1.0f, 1.0f);
259
260 padData.hw_gforceX[INPUT_HW_DRC] = vpadStatus.accelorometer.acc.x;
261 padData.hw_gforceY[INPUT_HW_DRC] = vpadStatus.accelorometer.acc.y;
262 padData.hw_gforceZ[INPUT_HW_DRC] = vpadStatus.accelorometer.acc.z;
263 padData.hw_pitch[INPUT_HW_DRC] = vpadStatus.angle.x * 360.0f;
264 padData.hw_roll[INPUT_HW_DRC] = vpadStatus.angle.y * 360.0f;
265 padData.hw_yaw[INPUT_HW_DRC] = vpadStatus.angle.z * 360.0f;
266
267 // Touch Screen & Pointer Coordinates Mapping
268 bool drcTouched = (vpadStatus.tpNormal.touched != 0);
269
270 if (drcTouched) {
271 VPADTouchData calib;
272 VPADGetTPCalibratedPoint(VPAD_CHAN_0, &calib, &vpadStatus.tpNormal);
273
274 // DRC native touch space is 1280x720 -> scale to virtual video bounds
275 float posX = clampf((float)calib.x * (screenWidth / 1280.0f), 0.0f, screenWidth);
276 float posY = clampf((float)calib.y * (screenHeight / 720.0f), 0.0f, screenHeight);
277
278 padData.isTouch = true;
279 padData.validPointer = true;
280 padData.cursor_x = posX;
281 padData.cursor_y = posY;
282
283 drcLastTouchX = posX;
284 drcLastTouchY = posY;
285 }
286
287 // Touch lifecycle button synthesis (INPUT_BTN_A)
288 if (drcTouched && !drcTouchedPrev) {
289 // Touch Down
290 padData.hw_buttons_d[INPUT_HW_DRC] |= INPUT_BTN_A;
291 padData.hw_buttons_h[INPUT_HW_DRC] |= INPUT_BTN_A;
292 } else if (drcTouched && drcTouchedPrev) {
293 // Touch Held
294 padData.hw_buttons_h[INPUT_HW_DRC] |= INPUT_BTN_A;
295 } else if (!drcTouched && drcTouchedPrev) {
296 // Touch Released
297 padData.hw_buttons_r[INPUT_HW_DRC] |= INPUT_BTN_A;
298
299 // Preserve pointer position on release frame
300 padData.isTouch = true;
301 padData.validPointer = true;
302 padData.cursor_x = drcLastTouchX;
303 padData.cursor_y = drcLastTouchY;
304 }
305
306 drcTouchedPrev = drcTouched;
307
308 // Remember this frame's held/analog state so a later tick
309 // with no new packet (still connected) can carry it forward
310 CarryForwardHwProfile(drcCache, padData, INPUT_HW_DRC);
311 drcCache.battery_level = padData.battery_level;
312 drcCache.isTouch = padData.isTouch;
313 drcCache.validPointer = padData.validPointer;
314 drcCache.cursor_x = padData.cursor_x;
315 drcCache.cursor_y = padData.cursor_y;
316 } else if (vpadError == VPAD_READ_NO_SAMPLES || vpadError == VPAD_READ_BUSY) {
317 // No new packet arrived this tick, but the GamePad hasn't
318 // been reported disconnected either
319 if (drcCache.hw_connected[INPUT_HW_DRC]) {
320 CarryForwardHwProfile(padData, drcCache, INPUT_HW_DRC);
321 padData.battery_level = drcCache.battery_level;
322 padData.isTouch = drcCache.isTouch;
323 padData.validPointer = drcCache.validPointer;
324 padData.cursor_x = drcCache.cursor_x;
325 padData.cursor_y = drcCache.cursor_y;
326 }
327 } else {
328 // A real error - treat as genuinely disconnected and stop carrying stale state forward
329 drcCache = InputPadData();
330 drcTouchedPrev = false;
331 }
332 }
333
334 // KPAD Processing (Wiimotes, Extensions & Pro Controllers)
335 KPADStatus kpadStatusBuf[KPAD_SAMPLE_BUFFER];
336 KPADError kpadError = KPAD_ERROR_UNINITIALIZED;
337 uint32_t kpadRead = KPADReadEx((KPADChan)i, kpadStatusBuf, KPAD_SAMPLE_BUFFER, &kpadError);
338
339 // KPADReadEx can return count > 0 on a frame with no new report (stale/
340 // cached data) - the error code, not the count, is what tells us the
341 // sample is actually fresh. Consuming it unconditionally is what was
342 // producing the periodic "skips a beat" stutter. Samples are ordered
343 // newest-to-oldest, so index 0 is the one we want.
344 if (kpadRead > 0 && kpadError == KPAD_ERROR_OK) {
345 KPADStatus& kpadStatus = kpadStatusBuf[0];
346
347 padData.hw_connected[INPUT_HW_WIIMOTE] = true;
348 padData.battery_level = WPADGetBatteryLevel((WPADChan)i) * 25; // normalize to 0-100 range
349 kpadCache[i].battery_level = padData.battery_level;
350 // Default assumption for this fresh frame: no valid IR pointer
351 kpadCache[i].validPointer = false;
352
353 padData.hw_gforceX[INPUT_HW_WIIMOTE] = kpadStatus.acc.x;
354 padData.hw_gforceY[INPUT_HW_WIIMOTE] = kpadStatus.acc.y;
355 padData.hw_gforceZ[INPUT_HW_WIIMOTE] = kpadStatus.acc.z;
356
357 // Last known angles come from the cache so they hold when an axis is out of range
358 float wmPitch = kpadCache[i].hw_pitch[INPUT_HW_WIIMOTE];
359 float wmRoll = kpadCache[i].hw_roll[INPUT_HW_WIIMOTE];
360 UpdateTiltAngles(kpadStatus.acc.x, kpadStatus.acc.y, kpadStatus.acc.z, wmPitch, wmRoll);
361 padData.hw_pitch[INPUT_HW_WIIMOTE] = wmPitch;
362 padData.hw_roll[INPUT_HW_WIIMOTE] = wmRoll;
363
364 if (kpadStatus.extensionType == WPAD_EXT_PRO_CONTROLLER) {
365 padData.hw_connected[INPUT_HW_WUPC] = true;
366 padData.hw_buttons_d[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.trigger);
367 padData.hw_buttons_h[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.hold);
368 padData.hw_buttons_r[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.release);
369
370 padData.hw_stickX[INPUT_HW_WUPC] = clampf(kpadStatus.pro.leftStick.x, -1.0f, 1.0f);
371 padData.hw_stickY[INPUT_HW_WUPC] = clampf(kpadStatus.pro.leftStick.y, -1.0f, 1.0f);
372 padData.hw_substickX[INPUT_HW_WUPC] = clampf(kpadStatus.pro.rightStick.x, -1.0f, 1.0f);
373 padData.hw_substickY[INPUT_HW_WUPC] = clampf(kpadStatus.pro.rightStick.y, -1.0f, 1.0f);
374
375 controller[i]->setSideways(false);
376 }
377 else if (kpadStatus.extensionType == WPAD_EXT_CLASSIC || kpadStatus.extensionType == WPAD_EXT_MPLUS_CLASSIC) {
378 padData.hw_connected[INPUT_HW_CLASSIC] = true;
379 padData.hw_buttons_d[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.trigger);
380 padData.hw_buttons_h[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.hold);
381 padData.hw_buttons_r[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.release);
382
383 padData.hw_stickX[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftStick.x, -1.0f, 1.0f);
384 padData.hw_stickY[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftStick.y, -1.0f, 1.0f);
385 padData.hw_substickX[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightStick.x, -1.0f, 1.0f);
386 padData.hw_substickY[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightStick.y, -1.0f, 1.0f);
387 padData.hw_triggerL[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftTrigger, 0.0f, 1.0f);
388 padData.hw_triggerR[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightTrigger, 0.0f, 1.0f);
389
390 controller[i]->setSideways(false);
391 }
392 else {
393 // Core Wiimote or Wiimote + Nunchuk
394 padData.hw_buttons_d[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.trigger);
395 padData.hw_buttons_h[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.hold);
396 padData.hw_buttons_r[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.release);
397
398
399 // Map IR pointer if active and DRC touch is not currently taking priority
400 if (kpadStatus.posValid && !padData.validPointer) {
401 float rawX = clampf((kpadStatus.pos.x * 0.5f + 0.5f) * screenWidth, 0.0f, screenWidth);
402 float rawY = clampf((kpadStatus.pos.y * 0.5f + 0.5f) * screenHeight, 0.0f, screenHeight);
403
404 float deltaTime = platform->getVideo()->getDeltaTime();
405 float smoothX, smoothY;
406
407 if (!irSmoothInit[i]) {
408 // First valid sample after acquiring (or re-acquiring) the sensor
409 // bar - snap straight to it instead of smoothing from a stale/zero
410 // position, which would otherwise show up as a visible snap-drag.
411 irFilterX[i].reset();
412 irFilterY[i].reset();
413 smoothX = irFilterX[i].filter(rawX, deltaTime);
414 smoothY = irFilterY[i].filter(rawY, deltaTime);
415 irSmoothInit[i] = true;
416 } else {
417 smoothX = irFilterX[i].filter(rawX, deltaTime);
418 smoothY = irFilterY[i].filter(rawY, deltaTime);
419 }
420
421 float cursorAngleDeg = std::atan2(kpadStatus.angle.y, kpadStatus.angle.x) * (180.0f / (float)M_PI);
422
423 padData.validPointer = true;
424 padData.isTouch = false;
425 padData.cursor_x = smoothX;
426 padData.cursor_y = smoothY;
427 padData.cursor_angle = cursorAngleDeg;
428
429 // Remember this frame's IR cursor so a later tick with no new packet won't cause it to drop out
430 kpadCache[i].validPointer = true;
431 kpadCache[i].isTouch = false;
432 kpadCache[i].cursor_x = smoothX;
433 kpadCache[i].cursor_y = smoothY;
434 kpadCache[i].cursor_angle = cursorAngleDeg;
435 } else if (!kpadStatus.posValid) {
436 // Sensor bar tracking lost - reset the filter so we don't drag the
437 // cursor toward a stale point when it's reacquired.
438 irSmoothInit[i] = false;
439 }
440
441 if (kpadStatus.extensionType == WPAD_EXT_NUNCHUK || kpadStatus.extensionType == WPAD_EXT_MPLUS_NUNCHUK) {
442 padData.hw_connected[INPUT_HW_NUNCHUK] = true;
443
444 padData.hw_buttons_d[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.trigger);
445 padData.hw_buttons_h[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.hold);
446 padData.hw_buttons_r[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.release);
447
448 padData.hw_stickX[INPUT_HW_NUNCHUK] = clampf(kpadStatus.nunchuk.stick.x, -1.0f, 1.0f);
449 padData.hw_stickY[INPUT_HW_NUNCHUK] = clampf(kpadStatus.nunchuk.stick.y, -1.0f, 1.0f);
450
451 padData.hw_gforceX[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.x;
452 padData.hw_gforceY[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.y;
453 padData.hw_gforceZ[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.z;
454
455 controller[i]->setSideways(false);
456 } else {
457 controller[i]->setSideways(getWiimoteOrientation() == WIIMOTE_ORIENTATION_HORIZONTAL);
458 }
459 }
460
461 // Remember this frame's held/analog state per profile so a later
462 // tick with no new packet (still connected) can carry it forward
463 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_WIIMOTE);
464 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_NUNCHUK);
465 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_CLASSIC);
466 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_WUPC);
467 } else if (kpadError == KPAD_ERROR_NO_SAMPLES || kpadError == KPAD_ERROR_BUSY) {
468 // No new packet arrived this tick, but the Wiimote hasn't been reported disconnected either
469 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_WIIMOTE);
470 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_NUNCHUK);
471 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_CLASSIC);
472 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_WUPC);
473
474 if (kpadCache[i].hw_connected[INPUT_HW_WIIMOTE]) {
475 padData.battery_level = kpadCache[i].battery_level;
476
477 // Carry the IR cursor forward too, but only if DRC touch hasn't already claimed pointer priority
478 if (!padData.validPointer && kpadCache[i].validPointer) {
479 padData.validPointer = true;
480 padData.isTouch = false;
481 padData.cursor_x = kpadCache[i].cursor_x;
482 padData.cursor_y = kpadCache[i].cursor_y;
483 padData.cursor_angle = kpadCache[i].cursor_angle;
484 }
485 }
486 } else {
487 // A real error - treat as genuinely disconnected
488 kpadCache[i] = InputPadData();
489 irSmoothInit[i] = false;
490 controller[i]->setSideways(false);
491 }
492
493 // Merge Aggregate State
494 for (uint32_t hw = 0; hw < INPUT_HW_MAX; hw++) {
495 if (!padData.hw_connected[hw]) continue;
496
497 padData.buttons_d |= padData.hw_buttons_d[hw];
498 padData.buttons_h |= padData.hw_buttons_h[hw];
499 padData.buttons_r |= padData.hw_buttons_r[hw];
500
501 if (std::abs(padData.hw_stickX[hw]) > std::abs(padData.stickX)) padData.stickX = padData.hw_stickX[hw];
502 if (std::abs(padData.hw_stickY[hw]) > std::abs(padData.stickY)) padData.stickY = padData.hw_stickY[hw];
503 if (std::abs(padData.hw_substickX[hw]) > std::abs(padData.substickX)) padData.substickX = padData.hw_substickX[hw];
504 if (std::abs(padData.hw_substickY[hw]) > std::abs(padData.substickY)) padData.substickY = padData.hw_substickY[hw];
505 if (std::abs(padData.hw_gforceX[hw]) > std::abs(padData.gforceX)) padData.gforceX = padData.hw_gforceX[hw];
506 if (std::abs(padData.hw_gforceY[hw]) > std::abs(padData.gforceY)) padData.gforceY = padData.hw_gforceY[hw];
507 if (std::abs(padData.hw_gforceZ[hw]) > std::abs(padData.gforceZ)) padData.gforceZ = padData.hw_gforceZ[hw];
508 if (std::abs(padData.hw_pitch[hw]) > std::abs(padData.pitch)) padData.pitch = padData.hw_pitch[hw];
509 if (std::abs(padData.hw_roll[hw]) > std::abs(padData.roll)) padData.roll = padData.hw_roll[hw];
510 if (std::abs(padData.hw_yaw[hw]) > std::abs(padData.yaw)) padData.yaw = padData.hw_yaw[hw];
511 }
512
513 // Update logical controller state
514 controller[i]->update(padData, platform->getVideo()->getDeltaTime());
515
516 bool allowRumble = isRumbleEnabled();
517
518 // Menu (hover) rumble: a short tick with an enforced silent gap afterward
519 static constexpr int kMenuRumbleOnFrames = 2; // ~33ms motor-on burst
520 static constexpr int kMenuRumbleGapFrames = 6; // ~100ms enforced silence after a tick
521
522 if (rumbleRequest[i]) {
523 rumbleRequest[i] = false;
524 if (menuRumbleFrames[i] == 0 && menuRumbleGapFrames[i] == 0) {
525 menuRumbleFrames[i] = kMenuRumbleOnFrames;
526 }
527 }
528
529 bool menuWantRumble = menuRumbleFrames[i] > 0;
530
531 if (menuRumbleFrames[i] > 0) {
532 menuRumbleFrames[i]--;
533 if (menuRumbleFrames[i] == 0) menuRumbleGapFrames[i] = kMenuRumbleGapFrames;
534 } else if (menuRumbleGapFrames[i] > 0) {
535 menuRumbleGapFrames[i]--;
536 }
537
538 bool wantRumble = menuWantRumble || continuousRumble[i];
539 bool motorOn = false;
540
541 // Hardware safety: never drive the motor for more than ~70 consecutive
542 // frames; force a short rest (5 frames) before it can run again.
543 if (silenceFrames[i] > 0) {
544 silenceFrames[i]--;
545 continuousRumbleCount[i] = 0;
546 } else {
547 if (wantRumble && allowRumble) {
548 continuousRumbleCount[i]++;
549 if (continuousRumbleCount[i] > 70) {
550 silenceFrames[i] = 5;
551 continuousRumbleCount[i] = 0;
552 } else {
553 motorOn = true;
554 }
555 } else {
556 continuousRumbleCount[i] = 0;
557 }
558 }
559
560 if (padData.hw_connected[INPUT_HW_WIIMOTE] || padData.hw_connected[INPUT_HW_WUPC]) {
561 WPADControlMotor((WPADChan)i, motorOn ? TRUE : FALSE);
562 }
563
564 if (i == 0) {
565 if (motorOn && padData.hw_connected[INPUT_HW_DRC]) {
566 if (wantRumble) {
567 VPADControlMotor(VPAD_CHAN_0, vpadRumblePattern, sizeof(vpadRumblePattern));
568 } else {
569 VPADControlMotor(VPAD_CHAN_0, vpadMenuRumblePattern, sizeof(vpadMenuRumblePattern));
570 }
571 } else {
572 VPADStopMotor(VPAD_CHAN_0);
573 }
574 }
575 }
576}
bool isRumbleEnabled() const
Definition InputDriver.h:42
int getWiimoteOrientation() const
Definition InputDriver.h:46
void update() override
Polls the hardware and dispatches InputPadData payloads to the UI.
void openHomeButtonOverlay()
Opens the Wii U system HOME-button overlay on demand.
void init() override
Initializes the controller subsystem.
void shutdown() override
Shuts the controller subsystem down.
void setRumble(int channel, bool rumble) override
Requests a rumble event on the specified controller channel.
void setContinuousRumble(int channel, bool continuous) override
Holds a channel's motor on (or releases it) until changed. Drivers cap an unbroken run and insert a s...
The universal input payload for UI elements.
Definition InputData.h:57
uint32_t buttons_d
Pressed this frame (down)
Definition InputData.h:61
float stickX
Left analog stick X, -1.0 to 1.0.
Definition InputData.h:64
float hw_roll[INPUT_HW_MAX]
Orientation roll, per profile.
Definition InputData.h:100
float gforceY
Accelerometer Y.
Definition InputData.h:69
uint32_t hw_buttons_d[INPUT_HW_MAX]
Buttons pressed this frame, per profile.
Definition InputData.h:86
float substickY
Right analog stick (C-Stick) Y, -1.0 to 1.0.
Definition InputData.h:67
float hw_gforceZ[INPUT_HW_MAX]
Accelerometer Z, per profile.
Definition InputData.h:98
float hw_triggerL[INPUT_HW_MAX]
Analog L trigger / shoulder, 0.0 - 1.0, per profile (GameCube, Classic, WUPC); 0 where the hardware h...
Definition InputData.h:93
float hw_triggerR[INPUT_HW_MAX]
Analog R trigger / shoulder, 0.0 - 1.0, per profile.
Definition InputData.h:94
float hw_stickY[INPUT_HW_MAX]
Left stick Y, per profile.
Definition InputData.h:90
float hw_substickX[INPUT_HW_MAX]
Right stick X, per profile.
Definition InputData.h:91
float pitch
Controller orientation pitch (Wiimote/Nunchuk)
Definition InputData.h:71
uint32_t battery_level
Controller battery level as reported by the backend.
Definition InputData.h:74
float cursor_x
Pointer X on the design canvas (Wiimote IR / GamePad touch)
Definition InputData.h:75
bool hw_connected[INPUT_HW_MAX]
Profile is connected and contributing.
Definition InputData.h:85
bool validPointer
True if the IR pointer or touch is active on screen.
Definition InputData.h:78
float cursor_angle
Pointer rotation (Wiimote roll)
Definition InputData.h:77
float yaw
Controller orientation yaw.
Definition InputData.h:73
bool isTouch
True if input is direct touch (VPad) vs IR pointer.
Definition InputData.h:79
float gforceX
Accelerometer X (Wiimote/Nunchuk/GamePad)
Definition InputData.h:68
float hw_stickX[INPUT_HW_MAX]
Left stick X, per profile.
Definition InputData.h:89
float hw_gforceY[INPUT_HW_MAX]
Accelerometer Y, per profile.
Definition InputData.h:97
float cursor_y
Pointer Y on the design canvas.
Definition InputData.h:76
float hw_gforceX[INPUT_HW_MAX]
Accelerometer X, per profile.
Definition InputData.h:96
float hw_yaw[INPUT_HW_MAX]
Orientation yaw, per profile.
Definition InputData.h:101
uint32_t hw_buttons_h[INPUT_HW_MAX]
Buttons held this frame, per profile.
Definition InputData.h:87
float substickX
Right analog stick (C-Stick) X, -1.0 to 1.0.
Definition InputData.h:66
float hw_pitch[INPUT_HW_MAX]
Orientation pitch, per profile.
Definition InputData.h:99
float roll
Controller orientation roll.
Definition InputData.h:72
float stickY
Left analog stick Y, -1.0 to 1.0.
Definition InputData.h:65
uint32_t buttons_r
Released this frame.
Definition InputData.h:63
float hw_substickY[INPUT_HW_MAX]
Right stick Y, per profile.
Definition InputData.h:92
uint32_t buttons_h
Held this frame.
Definition InputData.h:62
uint32_t hw_buttons_r[INPUT_HW_MAX]
Buttons released this frame, per profile.
Definition InputData.h:88
float gforceZ
Accelerometer Z.
Definition InputData.h:70