SDL: n3ds: Treat the d-pad as a hat switch at the SDL_Joystick level.

From fb3afd33e49c443cd8c5fd7e8ed0a20a07a6f8ac Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 22 Jun 2026 09:27:23 -0400
Subject: [PATCH] n3ds: Treat the d-pad as a hat switch at the SDL_Joystick
 level.

Reference Issue #14830.
---
 src/joystick/SDL_gamepad_db.h       |   2 +-
 src/joystick/n3ds/SDL_sysjoystick.c | 117 +++++++++++++++++++---------
 2 files changed, 80 insertions(+), 39 deletions(-)

diff --git a/src/joystick/SDL_gamepad_db.h b/src/joystick/SDL_gamepad_db.h
index 1a834455ead67..c184eefe237a3 100644
--- a/src/joystick/SDL_gamepad_db.h
+++ b/src/joystick/SDL_gamepad_db.h
@@ -877,7 +877,7 @@ static const char *s_GamepadMappings[] = {
     "0000000050535669746120436f6e7400,PSVita Controller,crc:d598,a:b2,b:b1,back:b10,dpdown:b6,dpleft:b7,dpright:b9,dpup:b8,leftshoulder:b4,leftstick:b14,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b15,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,",
 #endif
 #ifdef SDL_JOYSTICK_N3DS
-    "000000004e696e74656e646f20334400,Nintendo 3DS,crc:3210,a:b1,b:b0,back:b2,dpdown:b7,dpleft:b5,dpright:b4,dpup:b6,leftshoulder:b9,lefttrigger:b14,leftx:a0,lefty:a1,rightshoulder:b8,righttrigger:b15,rightx:a2,righty:a3,start:b3,x:b11,y:b10,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
+    "000000004e696e74656e646f20334400,Nintendo 3DS,crc:3210,a:b1,b:b0,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b11,rightx:a2,righty:a3,start:b3,x:b7,y:b6,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,",
 #endif
     NULL
 };
diff --git a/src/joystick/n3ds/SDL_sysjoystick.c b/src/joystick/n3ds/SDL_sysjoystick.c
index fda19dd2179d5..54bffa5c77f48 100644
--- a/src/joystick/n3ds/SDL_sysjoystick.c
+++ b/src/joystick/n3ds/SDL_sysjoystick.c
@@ -28,7 +28,8 @@
 
 #include "../SDL_sysjoystick.h"
 
-#define NB_BUTTONS 23
+#define NB_BUTTONS 23   // physical buttons on the device (although we treat four of these, the dpad, as a hat switch).
+#define N3DS_HAT_MASK 0xF0   // 0xF0==d-pad bits, which we handle elsewhere, so mask them out here.
 
 /*
   N3DS sticks values are roughly within +/-160
@@ -54,8 +55,9 @@ static inline int Correct_Axis_Y(int Y) {
     return Correct_Axis_X(-Y);
 }
 
-static void UpdateN3DSPressedButtons(Uint64 timestamp, SDL_Joystick *joystick);
-static void UpdateN3DSReleasedButtons(Uint64 timestamp, SDL_Joystick *joystick);
+static void UpdateN3DSPressedButtons(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_state, u32 current_state);
+static void UpdateN3DSReleasedButtons(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_state, u32 current_state);
+static void UpdateN3DSHat(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_down_state, u32 current_down_state, u32 previous_up_state, u32 current_up_state);
 static void UpdateN3DSCircle(Uint64 timestamp, SDL_Joystick *joystick);
 static void UpdateN3DSCStick(Uint64 timestamp, SDL_Joystick *joystick);
 
@@ -89,9 +91,9 @@ static SDL_JoystickID N3DS_JoystickGetDeviceInstanceID(int device_index)
 
 static bool N3DS_JoystickOpen(SDL_Joystick *joystick, int device_index)
 {
-    joystick->nbuttons = NB_BUTTONS;
+    joystick->nbuttons = NB_BUTTONS - 4;  // -4 for the dpad (which we treat as a hat).
     joystick->naxes = 4;
-    joystick->nhats = 0;
+    joystick->nhats = 1;  // treat the dpad as a hat.
 
     return true;
 }
@@ -103,46 +105,85 @@ static bool N3DS_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)
 
 static void N3DS_JoystickUpdate(SDL_Joystick *joystick)
 {
-    Uint64 timestamp = SDL_GetTicksNS();
-
-    UpdateN3DSPressedButtons(timestamp, joystick);
-    UpdateN3DSReleasedButtons(timestamp, joystick);
+    static u32 previous_down_state = 0;
+    static u32 previous_up_state = 0;
+    const Uint64 timestamp = SDL_GetTicksNS();
+    const u32 current_down_state = hidKeysDown();
+    const u32 current_up_state = hidKeysUp();
+
+    UpdateN3DSPressedButtons(timestamp, joystick, previous_down_state, current_down_state);
+    UpdateN3DSReleasedButtons(timestamp, joystick, previous_up_state, current_up_state);
+    UpdateN3DSHat(timestamp, joystick, previous_down_state, current_down_state, previous_up_state, current_up_state);
     UpdateN3DSCircle(timestamp, joystick);
     UpdateN3DSCStick(timestamp, joystick);
+
+    previous_down_state = current_down_state;
+    previous_up_state = current_up_state;
 }
 
-static void UpdateN3DSPressedButtons(Uint64 timestamp, SDL_Joystick *joystick)
+static void UpdateN3DSPressedButtons(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_state, u32 current_state)
 {
-    static u32 previous_state = 0;
-    u32 updated_down;
-    u32 current_state = hidKeysDown();
-    updated_down = previous_state ^ current_state;
+    const u32 updated_down = (previous_state ^ current_state) & ~N3DS_HAT_MASK;
     if (updated_down) {
-        for (Uint8 i = 0; i < joystick->nbuttons; i++) {
-            if (current_state & BIT(i) & updated_down) {
-                SDL_SendJoystickButton(timestamp, joystick, i, true);
+        Uint8 buttonidx = 0;
+        Uint8 i = 0;
+        while (i < joystick->nbuttons) {
+            if ((buttonidx < 4) || (buttonidx > 7)) {  // skip dpad (we treat it as a hat).
+                if (current_state & BIT(buttonidx) & updated_down) {
+                    SDL_SendJoystickButton(timestamp, joystick, i, true);
+                }
+                i++;
             }
+            buttonidx++;
         }
     }
-    previous_state = current_state;
 }
 
-static void UpdateN3DSReleasedButtons(Uint64 timestamp, SDL_Joystick *joystick)
+static void UpdateN3DSReleasedButtons(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_state, u32 current_state)
 {
-    static u32 previous_state = 0;
-    u32 updated_up;
-    u32 current_state = hidKeysUp();
-    updated_up = previous_state ^ current_state;
+    const u32 updated_up = (previous_state ^ current_state) & ~N3DS_HAT_MASK;
     if (updated_up) {
-        for (Uint8 i = 0; i < joystick->nbuttons; i++) {
-            if (current_state & BIT(i) & updated_up) {
-                SDL_SendJoystickButton(timestamp, joystick, i, false);
-            }
+        Uint8 buttonidx = 0;
+        Uint8 i = 0;
+        while (i < joystick->nbuttons) {
+            if ((buttonidx < 4) || (buttonidx > 7)) {  // skip dpad (we treat it as a hat).
+                if (current_state & BIT(buttonidx) & updated_up) {
+                    SDL_SendJoystickButton(timestamp, joystick, i, false);
+                }
+                i++;
+             }
+            buttonidx++;
         }
     }
-    previous_state = current_state;
 }
 
+static void UpdateN3DSHat(Uint64 timestamp, SDL_Joystick *joystick, u32 previous_down_state, u32 current_down_state, u32 previous_up_state, u32 current_up_state)
+{
+    // The 3DS dpad looks like 4 buttons at this level, but we treat it as a hat switch, so apps that are talking to SDL_Joystick can hope to do basic directional things without a configuration step.
+    // (but they should _really_ be using the gamepad API.)
+    const u32 updated_hat = (((previous_up_state ^ current_up_state) | (previous_down_state ^ current_down_state)) & N3DS_HAT_MASK);  // did bits 4 through 7 change?
+    if (updated_hat) {
+        Uint8 hat = SDL_HAT_CENTERED;
+
+        #define HATSTATE(n3dsbit, sdlenum) if (current_down_state & BIT(n3dsbit)) { hat |= SDL_HAT_##sdlenum; }
+        HATSTATE(4, RIGHT);
+        HATSTATE(5, LEFT);
+        HATSTATE(6, UP);
+        HATSTATE(7, DOWN);
+        #undef HATSTATE
+
+        // this is a physical d-pad on the device, so it probably _can't_ send opposing buttons at the same time, but just in case, cancel them out.
+        if ((hat & (SDL_HAT_UP|SDL_HAT_DOWN)) == (SDL_HAT_UP|SDL_HAT_DOWN)) {
+            hat &= ~(SDL_HAT_UP|SDL_HAT_DOWN);
+        }
+        if ((hat & (SDL_HAT_LEFT|SDL_HAT_RIGHT)) == (SDL_HAT_LEFT|SDL_HAT_RIGHT)) {
+            hat &= ~(SDL_HAT_LEFT|SDL_HAT_RIGHT);
+        }
+        SDL_SendJoystickHat(timestamp, joystick, 0, hat);
+    }
+}
+
+
 static void UpdateN3DSCircle(Uint64 timestamp, SDL_Joystick *joystick)
 {
     static circlePosition previous_state = { 0, 0 };
@@ -194,19 +235,19 @@ static bool N3DS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping
     *out = (SDL_GamepadMapping){
         .a = { EMappingKind_Button, 0 },
         .b = { EMappingKind_Button, 1 },
-        .x = { EMappingKind_Button, 10 },
-        .y = { EMappingKind_Button, 11 },
+        .x = { EMappingKind_Button, 6 },
+        .y = { EMappingKind_Button, 7 },
         .back = { EMappingKind_Button, 2 },
         .guide = { EMappingKind_None, 255 },
         .start = { EMappingKind_Button, 3 },
         .leftstick = { EMappingKind_None, 255 },
         .rightstick = { EMappingKind_None, 255 },
-        .leftshoulder = { EMappingKind_Button, 9 },
-        .rightshoulder = { EMappingKind_Button, 8 },
-        .dpup = { EMappingKind_Button, 6 },
-        .dpdown = { EMappingKind_Button, 7 },
-        .dpleft = { EMappingKind_Button, 5 },
-        .dpright = { EMappingKind_Button, 4 },
+        .leftshoulder = { EMappingKind_Button, 5 },
+        .rightshoulder = { EMappingKind_Button, 4 },
+        .dpup = { EMappingKind_Hat, SDL_HAT_UP },
+        .dpdown = { EMappingKind_Hat, SDL_HAT_DOWN },
+        .dpleft = { EMappingKind_Hat, SDL_HAT_LEFT },
+        .dpright = { EMappingKind_Hat, SDL_HAT_RIGHT },
         .misc1 = { EMappingKind_None, 255 },
         .right_paddle1 = { EMappingKind_None, 255 },
         .left_paddle1 = { EMappingKind_None, 255 },
@@ -216,8 +257,8 @@ static bool N3DS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping
         .lefty = { EMappingKind_Axis, 1 },
         .rightx = { EMappingKind_Axis, 2 },
         .righty = { EMappingKind_Axis, 3 },
-        .lefttrigger = { EMappingKind_Button, 14 },
-        .righttrigger = { EMappingKind_Button, 15 },
+        .lefttrigger = { EMappingKind_Button, 10 },
+        .righttrigger = { EMappingKind_Button, 11 },
     };
     return true;
 }