libgui 3.00
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 OSEnableHomeButtonMenu(FALSE);
204 InitUserInputControllers();
205}
206
208 for (int i = 0; i < 4; i++) {
209 WPADControlMotor((WPADChan)i, FALSE);
210 rumbleRequest[i] = continuousRumble[i] = false;
211 menuRumbleFrames[i] = continuousRumbleCount[i] = silenceFrames[i] = menuRumbleGapFrames[i] = 0;
212 }
213 VPADStopMotor(VPAD_CHAN_0);
214
215 // Restore the system default before handing control back to the OS/loader.
216 OSEnableHomeButtonMenu(TRUE);
217}
218
220 _SYSSwitchToHBMWithMode(0);
221}
222
223void WutInputDriver::setRumble(int channel, bool rumble) {
224 if (channel >= 0 && channel < 4) {
225 rumbleRequest[channel] = rumble;
226 }
227}
228
229void WutInputDriver::setContinuousRumble(int channel, bool continuous) {
230 if (channel >= 0 && channel < 4) continuousRumble[channel] = continuous;
231}
232
234 float screenWidth = (float)platform->getVideo()->getScreenWidth();
235 float screenHeight = (float)platform->getVideo()->getScreenHeight();
236
237 for (int i = 3; i >= 0; i--) {
238 InputPadData padData;
239
240 // VPAD Processing (GamePad, Channel 0 Only)
241 if (i == 0) {
242 VPADStatus vpadStatus;
243 VPADReadError vpadError;
244 VPADRead(VPAD_CHAN_0, &vpadStatus, 1, &vpadError);
245
246 if (vpadError == VPAD_READ_SUCCESS) {
247 padData.hw_connected[INPUT_HW_DRC] = true;
248 padData.battery_level = vpadStatus.battery * 25; // normalize to 0-100
249
250 padData.hw_buttons_d[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.trigger);
251 padData.hw_buttons_h[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.hold);
252 padData.hw_buttons_r[INPUT_HW_DRC] = MapVPADToGeneric(vpadStatus.release);
253
254 padData.hw_stickX[INPUT_HW_DRC] = clampf(vpadStatus.leftStick.x, -1.0f, 1.0f);
255 padData.hw_stickY[INPUT_HW_DRC] = clampf(vpadStatus.leftStick.y, -1.0f, 1.0f);
256 padData.hw_substickX[INPUT_HW_DRC] = clampf(vpadStatus.rightStick.x, -1.0f, 1.0f);
257 padData.hw_substickY[INPUT_HW_DRC] = clampf(vpadStatus.rightStick.y, -1.0f, 1.0f);
258
259 padData.hw_gforceX[INPUT_HW_DRC] = vpadStatus.accelerometer.acc.x;
260 padData.hw_gforceY[INPUT_HW_DRC] = vpadStatus.accelerometer.acc.y;
261 padData.hw_gforceZ[INPUT_HW_DRC] = vpadStatus.accelerometer.acc.z;
262 padData.hw_pitch[INPUT_HW_DRC] = vpadStatus.angle.x * 360.0f;
263 padData.hw_roll[INPUT_HW_DRC] = vpadStatus.angle.y * 360.0f;
264 padData.hw_yaw[INPUT_HW_DRC] = vpadStatus.angle.z * 360.0f;
265
266 // Touch Screen & Pointer Coordinates Mapping
267 bool drcTouched = (vpadStatus.tpNormal.touched != 0);
268
269 if (drcTouched) {
270 VPADTouchData calib;
271 VPADGetTPCalibratedPoint(VPAD_CHAN_0, &calib, &vpadStatus.tpNormal);
272
273 // DRC native touch space is 1280x720 -> scale to virtual video bounds
274 float posX = clampf((float)calib.x * (screenWidth / 1280.0f), 0.0f, screenWidth);
275 float posY = clampf((float)calib.y * (screenHeight / 720.0f), 0.0f, screenHeight);
276
277 padData.isTouch = true;
278 padData.validPointer = true;
279 padData.cursor_x = posX;
280 padData.cursor_y = posY;
281
282 drcLastTouchX = posX;
283 drcLastTouchY = posY;
284 }
285
286 // Touch lifecycle button synthesis (INPUT_BTN_A)
287 if (drcTouched && !drcTouchedPrev) {
288 // Touch Down
289 padData.hw_buttons_d[INPUT_HW_DRC] |= INPUT_BTN_A;
290 padData.hw_buttons_h[INPUT_HW_DRC] |= INPUT_BTN_A;
291 } else if (drcTouched && drcTouchedPrev) {
292 // Touch Held
293 padData.hw_buttons_h[INPUT_HW_DRC] |= INPUT_BTN_A;
294 } else if (!drcTouched && drcTouchedPrev) {
295 // Touch Released
296 padData.hw_buttons_r[INPUT_HW_DRC] |= INPUT_BTN_A;
297
298 // Preserve pointer position on release frame
299 padData.isTouch = true;
300 padData.validPointer = true;
301 padData.cursor_x = drcLastTouchX;
302 padData.cursor_y = drcLastTouchY;
303 }
304
305 drcTouchedPrev = drcTouched;
306
307 // Remember this frame's held/analog state so a later tick
308 // with no new packet (still connected) can carry it forward
309 CarryForwardHwProfile(drcCache, padData, INPUT_HW_DRC);
310 drcCache.battery_level = padData.battery_level;
311 drcCache.isTouch = padData.isTouch;
312 drcCache.validPointer = padData.validPointer;
313 drcCache.cursor_x = padData.cursor_x;
314 drcCache.cursor_y = padData.cursor_y;
315 } else if (vpadError == VPAD_READ_NO_SAMPLES || vpadError == VPAD_READ_BUSY) {
316 // No new packet arrived this tick, but the GamePad hasn't
317 // been reported disconnected either
318 if (drcCache.hw_connected[INPUT_HW_DRC]) {
319 CarryForwardHwProfile(padData, drcCache, INPUT_HW_DRC);
320 padData.battery_level = drcCache.battery_level;
321 padData.isTouch = drcCache.isTouch;
322 padData.validPointer = drcCache.validPointer;
323 padData.cursor_x = drcCache.cursor_x;
324 padData.cursor_y = drcCache.cursor_y;
325 }
326 } else {
327 // A real error - treat as genuinely disconnected and stop carrying stale state forward
328 drcCache = InputPadData();
329 drcTouchedPrev = false;
330 }
331 }
332
333 // KPAD Processing (Wiimotes, Extensions & Pro Controllers)
334 KPADStatus kpadStatusBuf[KPAD_SAMPLE_BUFFER];
335 KPADError kpadError = KPAD_ERROR_UNINITIALIZED;
336 uint32_t kpadRead = KPADReadEx((KPADChan)i, kpadStatusBuf, KPAD_SAMPLE_BUFFER, &kpadError);
337
338 // KPADReadEx can return count > 0 on a frame with no new report (stale/
339 // cached data) - the error code, not the count, is what tells us the
340 // sample is actually fresh. Consuming it unconditionally is what was
341 // producing the periodic "skips a beat" stutter. Samples are ordered
342 // newest-to-oldest, so index 0 is the one we want.
343 if (kpadRead > 0 && kpadError == KPAD_ERROR_OK) {
344 KPADStatus& kpadStatus = kpadStatusBuf[0];
345
346 padData.hw_connected[INPUT_HW_WIIMOTE] = true;
347 padData.battery_level = WPADGetBatteryLevel((WPADChan)i) * 25; // normalize to 0-100 range
348 kpadCache[i].battery_level = padData.battery_level;
349 // Default assumption for this fresh frame: no valid IR pointer
350 kpadCache[i].validPointer = false;
351
352 padData.hw_gforceX[INPUT_HW_WIIMOTE] = kpadStatus.acc.x;
353 padData.hw_gforceY[INPUT_HW_WIIMOTE] = kpadStatus.acc.y;
354 padData.hw_gforceZ[INPUT_HW_WIIMOTE] = kpadStatus.acc.z;
355
356 // Last known angles come from the cache so they hold when an axis is out of range
357 float wmPitch = kpadCache[i].hw_pitch[INPUT_HW_WIIMOTE];
358 float wmRoll = kpadCache[i].hw_roll[INPUT_HW_WIIMOTE];
359 UpdateTiltAngles(kpadStatus.acc.x, kpadStatus.acc.y, kpadStatus.acc.z, wmPitch, wmRoll);
360 padData.hw_pitch[INPUT_HW_WIIMOTE] = wmPitch;
361 padData.hw_roll[INPUT_HW_WIIMOTE] = wmRoll;
362
363 if (kpadStatus.extensionType == WPAD_EXT_PRO_CONTROLLER) {
364 padData.hw_connected[INPUT_HW_WUPC] = true;
365 padData.hw_buttons_d[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.trigger);
366 padData.hw_buttons_h[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.hold);
367 padData.hw_buttons_r[INPUT_HW_WUPC] = MapKPADProToGeneric(kpadStatus.pro.release);
368
369 padData.hw_stickX[INPUT_HW_WUPC] = clampf(kpadStatus.pro.leftStick.x, -1.0f, 1.0f);
370 padData.hw_stickY[INPUT_HW_WUPC] = clampf(kpadStatus.pro.leftStick.y, -1.0f, 1.0f);
371 padData.hw_substickX[INPUT_HW_WUPC] = clampf(kpadStatus.pro.rightStick.x, -1.0f, 1.0f);
372 padData.hw_substickY[INPUT_HW_WUPC] = clampf(kpadStatus.pro.rightStick.y, -1.0f, 1.0f);
373
374 controller[i]->setSideways(false);
375 }
376 else if (kpadStatus.extensionType == WPAD_EXT_CLASSIC || kpadStatus.extensionType == WPAD_EXT_MPLUS_CLASSIC) {
377 padData.hw_connected[INPUT_HW_CLASSIC] = true;
378 padData.hw_buttons_d[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.trigger);
379 padData.hw_buttons_h[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.hold);
380 padData.hw_buttons_r[INPUT_HW_CLASSIC] = MapKPADClassicToGeneric(kpadStatus.classic.release);
381
382 padData.hw_stickX[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftStick.x, -1.0f, 1.0f);
383 padData.hw_stickY[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftStick.y, -1.0f, 1.0f);
384 padData.hw_substickX[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightStick.x, -1.0f, 1.0f);
385 padData.hw_substickY[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightStick.y, -1.0f, 1.0f);
386 padData.hw_triggerL[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.leftTrigger, 0.0f, 1.0f);
387 padData.hw_triggerR[INPUT_HW_CLASSIC] = clampf(kpadStatus.classic.rightTrigger, 0.0f, 1.0f);
388
389 controller[i]->setSideways(false);
390 }
391 else {
392 // Core Wiimote or Wiimote + Nunchuk
393 padData.hw_buttons_d[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.trigger);
394 padData.hw_buttons_h[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.hold);
395 padData.hw_buttons_r[INPUT_HW_WIIMOTE] = MapKPADCoreToGeneric(kpadStatus.release);
396
397
398 // Map IR pointer if active and DRC touch is not currently taking priority
399 if (kpadStatus.posValid && !padData.validPointer) {
400 float rawX = clampf((kpadStatus.pos.x * 0.5f + 0.5f) * screenWidth, 0.0f, screenWidth);
401 float rawY = clampf((kpadStatus.pos.y * 0.5f + 0.5f) * screenHeight, 0.0f, screenHeight);
402
403 float deltaTime = platform->getVideo()->getDeltaTime();
404 float smoothX, smoothY;
405
406 if (!irSmoothInit[i]) {
407 // First valid sample after acquiring (or re-acquiring) the sensor
408 // bar - snap straight to it instead of smoothing from a stale/zero
409 // position, which would otherwise show up as a visible snap-drag.
410 irFilterX[i].reset();
411 irFilterY[i].reset();
412 smoothX = irFilterX[i].filter(rawX, deltaTime);
413 smoothY = irFilterY[i].filter(rawY, deltaTime);
414 irSmoothInit[i] = true;
415 } else {
416 smoothX = irFilterX[i].filter(rawX, deltaTime);
417 smoothY = irFilterY[i].filter(rawY, deltaTime);
418 }
419
420 float cursorAngleDeg = std::atan2(kpadStatus.angle.y, kpadStatus.angle.x) * (180.0f / (float)M_PI);
421
422 padData.validPointer = true;
423 padData.isTouch = false;
424 padData.cursor_x = smoothX;
425 padData.cursor_y = smoothY;
426 padData.cursor_angle = cursorAngleDeg;
427
428 // Remember this frame's IR cursor so a later tick with no new packet won't cause it to drop out
429 kpadCache[i].validPointer = true;
430 kpadCache[i].isTouch = false;
431 kpadCache[i].cursor_x = smoothX;
432 kpadCache[i].cursor_y = smoothY;
433 kpadCache[i].cursor_angle = cursorAngleDeg;
434 } else if (!kpadStatus.posValid) {
435 // Sensor bar tracking lost - reset the filter so we don't drag the
436 // cursor toward a stale point when it's reacquired.
437 irSmoothInit[i] = false;
438 }
439
440 if (kpadStatus.extensionType == WPAD_EXT_NUNCHUK || kpadStatus.extensionType == WPAD_EXT_MPLUS_NUNCHUK) {
441 padData.hw_connected[INPUT_HW_NUNCHUK] = true;
442
443 padData.hw_buttons_d[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.trigger);
444 padData.hw_buttons_h[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.hold);
445 padData.hw_buttons_r[INPUT_HW_NUNCHUK] = MapKPADNunchukToGeneric(kpadStatus.nunchuk.release);
446
447 padData.hw_stickX[INPUT_HW_NUNCHUK] = clampf(kpadStatus.nunchuk.stick.x, -1.0f, 1.0f);
448 padData.hw_stickY[INPUT_HW_NUNCHUK] = clampf(kpadStatus.nunchuk.stick.y, -1.0f, 1.0f);
449
450 padData.hw_gforceX[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.x;
451 padData.hw_gforceY[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.y;
452 padData.hw_gforceZ[INPUT_HW_NUNCHUK] = kpadStatus.nunchuk.acc.z;
453
454 controller[i]->setSideways(false);
455 } else {
456 controller[i]->setSideways(getWiimoteOrientation() == WIIMOTE_ORIENTATION_HORIZONTAL);
457 }
458 }
459
460 // Remember this frame's held/analog state per profile so a later
461 // tick with no new packet (still connected) can carry it forward
462 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_WIIMOTE);
463 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_NUNCHUK);
464 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_CLASSIC);
465 CarryForwardHwProfile(kpadCache[i], padData, INPUT_HW_WUPC);
466 } else if (kpadError == KPAD_ERROR_NO_SAMPLES || kpadError == KPAD_ERROR_BUSY) {
467 // No new packet arrived this tick, but the Wiimote hasn't been reported disconnected either
468 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_WIIMOTE);
469 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_NUNCHUK);
470 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_CLASSIC);
471 CarryForwardHwProfile(padData, kpadCache[i], INPUT_HW_WUPC);
472
473 if (kpadCache[i].hw_connected[INPUT_HW_WIIMOTE]) {
474 padData.battery_level = kpadCache[i].battery_level;
475
476 // Carry the IR cursor forward too, but only if DRC touch hasn't already claimed pointer priority
477 if (!padData.validPointer && kpadCache[i].validPointer) {
478 padData.validPointer = true;
479 padData.isTouch = false;
480 padData.cursor_x = kpadCache[i].cursor_x;
481 padData.cursor_y = kpadCache[i].cursor_y;
482 padData.cursor_angle = kpadCache[i].cursor_angle;
483 }
484 }
485 } else {
486 // A real error - treat as genuinely disconnected
487 kpadCache[i] = InputPadData();
488 irSmoothInit[i] = false;
489 controller[i]->setSideways(false);
490 }
491
492 // Merge Aggregate State
493 for (uint32_t hw = 0; hw < INPUT_HW_MAX; hw++) {
494 if (!padData.hw_connected[hw]) continue;
495
496 padData.buttons_d |= padData.hw_buttons_d[hw];
497 padData.buttons_h |= padData.hw_buttons_h[hw];
498 padData.buttons_r |= padData.hw_buttons_r[hw];
499
500 if (std::abs(padData.hw_stickX[hw]) > std::abs(padData.stickX)) padData.stickX = padData.hw_stickX[hw];
501 if (std::abs(padData.hw_stickY[hw]) > std::abs(padData.stickY)) padData.stickY = padData.hw_stickY[hw];
502 if (std::abs(padData.hw_substickX[hw]) > std::abs(padData.substickX)) padData.substickX = padData.hw_substickX[hw];
503 if (std::abs(padData.hw_substickY[hw]) > std::abs(padData.substickY)) padData.substickY = padData.hw_substickY[hw];
504 if (std::abs(padData.hw_gforceX[hw]) > std::abs(padData.gforceX)) padData.gforceX = padData.hw_gforceX[hw];
505 if (std::abs(padData.hw_gforceY[hw]) > std::abs(padData.gforceY)) padData.gforceY = padData.hw_gforceY[hw];
506 if (std::abs(padData.hw_gforceZ[hw]) > std::abs(padData.gforceZ)) padData.gforceZ = padData.hw_gforceZ[hw];
507 if (std::abs(padData.hw_pitch[hw]) > std::abs(padData.pitch)) padData.pitch = padData.hw_pitch[hw];
508 if (std::abs(padData.hw_roll[hw]) > std::abs(padData.roll)) padData.roll = padData.hw_roll[hw];
509 if (std::abs(padData.hw_yaw[hw]) > std::abs(padData.yaw)) padData.yaw = padData.hw_yaw[hw];
510 }
511
512 // Update logical controller state
513 controller[i]->update(padData, platform->getVideo()->getDeltaTime());
514
515 bool allowRumble = isRumbleEnabled();
516
517 // Menu (hover) rumble: a short tick with an enforced silent gap afterward
518 static constexpr int kMenuRumbleOnFrames = 2; // ~33ms motor-on burst
519 static constexpr int kMenuRumbleGapFrames = 6; // ~100ms enforced silence after a tick
520
521 if (rumbleRequest[i]) {
522 rumbleRequest[i] = false;
523 if (menuRumbleFrames[i] == 0 && menuRumbleGapFrames[i] == 0) {
524 menuRumbleFrames[i] = kMenuRumbleOnFrames;
525 }
526 }
527
528 bool menuWantRumble = menuRumbleFrames[i] > 0;
529
530 if (menuRumbleFrames[i] > 0) {
531 menuRumbleFrames[i]--;
532 if (menuRumbleFrames[i] == 0) menuRumbleGapFrames[i] = kMenuRumbleGapFrames;
533 } else if (menuRumbleGapFrames[i] > 0) {
534 menuRumbleGapFrames[i]--;
535 }
536
537 bool wantRumble = menuWantRumble || continuousRumble[i];
538 bool motorOn = false;
539
540 // Hardware safety: never drive the motor for more than ~70 consecutive
541 // frames; force a short rest (5 frames) before it can run again.
542 if (silenceFrames[i] > 0) {
543 silenceFrames[i]--;
544 continuousRumbleCount[i] = 0;
545 } else {
546 if (wantRumble && allowRumble) {
547 continuousRumbleCount[i]++;
548 if (continuousRumbleCount[i] > 70) {
549 silenceFrames[i] = 5;
550 continuousRumbleCount[i] = 0;
551 } else {
552 motorOn = true;
553 }
554 } else {
555 continuousRumbleCount[i] = 0;
556 }
557 }
558
559 if (padData.hw_connected[INPUT_HW_WIIMOTE] || padData.hw_connected[INPUT_HW_WUPC]) {
560 WPADControlMotor((WPADChan)i, motorOn ? TRUE : FALSE);
561 }
562
563 if (i == 0) {
564 if (motorOn && padData.hw_connected[INPUT_HW_DRC]) {
565 if (wantRumble) {
566 VPADControlMotor(VPAD_CHAN_0, vpadRumblePattern, sizeof(vpadRumblePattern));
567 } else {
568 VPADControlMotor(VPAD_CHAN_0, vpadMenuRumblePattern, sizeof(vpadMenuRumblePattern));
569 }
570 } else {
571 VPADStopMotor(VPAD_CHAN_0);
572 }
573 }
574 }
575}
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