SDL: N3DS port (squashed)

From 655275378d22acbef4685d8992a5c7b370bab4fa Mon Sep 17 00:00:00 2001
From: Pierre Wendling <[EMAIL REDACTED]>
Date: Tue, 30 Mar 2021 04:32:39 -0400
Subject: [PATCH] N3DS port (squashed)

A dedicated renderer using Citro3D would likely allow for better
much better graphical performances.
---
 .github/workflows/n3ds.yml              |  40 +++
 CMakeLists.txt                          |  74 ++++-
 docs/README-n3ds.md                     |  27 ++
 include/SDL_config.h.cmake              |   7 +
 include/SDL_main.h                      |   9 +
 include/SDL_platform.h                  |   5 +
 include/SDL_stdinc.h                    |   2 +-
 include/SDL_video.h                     |   1 +
 src/SDL.c                               |   2 +
 src/SDL_log.c                           |   7 +
 src/audio/SDL_audio.c                   |   3 +
 src/audio/SDL_sysaudio.h                |   1 +
 src/audio/n3ds/SDL_n3dsaudio.c          | 363 +++++++++++++++++++++++
 src/audio/n3ds/SDL_n3dsaudio.h          |  50 ++++
 src/cpuinfo/SDL_cpuinfo.c               |   2 +
 src/dynapi/SDL_dynapi.h                 |   2 +
 src/file/SDL_rwops.c                    |   6 +
 src/file/n3ds/SDL_rwopsromfs.c          |  56 ++++
 src/file/n3ds/SDL_rwopsromfs.h          |  30 ++
 src/filesystem/n3ds/SDL_sysfilesystem.c |  86 ++++++
 src/joystick/SDL_gamecontrollerdb.h     |   3 +
 src/joystick/SDL_joystick.c             |   3 +
 src/joystick/SDL_sysjoystick.h          |   1 +
 src/joystick/n3ds/SDL_sysjoystick.c     | 367 ++++++++++++++++++++++++
 src/libm/math_private.h                 |   2 +-
 src/locale/n3ds/SDL_syslocale.c         |  59 ++++
 src/main/n3ds/SDL_n3ds_main.c           |  82 ++++++
 src/power/SDL_power.c                   |   3 +
 src/power/SDL_syspower.h                |   1 +
 src/power/n3ds/SDL_syspower.c           | 111 +++++++
 src/thread/SDL_thread_c.h               |   2 +
 src/thread/n3ds/SDL_syscond.c           | 133 +++++++++
 src/thread/n3ds/SDL_sysmutex.c          |  93 ++++++
 src/thread/n3ds/SDL_sysmutex_c.h        |  37 +++
 src/thread/n3ds/SDL_syssem.c            | 134 +++++++++
 src/thread/n3ds/SDL_systhread.c         | 148 ++++++++++
 src/thread/n3ds/SDL_systhread_c.h       |  32 +++
 src/timer/n3ds/SDL_systimer.c           |  81 ++++++
 src/video/SDL_sysvideo.h                |   1 +
 src/video/SDL_video.c                   |   5 +-
 src/video/n3ds/SDL_n3dsevents.c         |  45 +++
 src/video/n3ds/SDL_n3dsevents_c.h       |  31 ++
 src/video/n3ds/SDL_n3dsframebuffer.c    | 148 ++++++++++
 src/video/n3ds/SDL_n3dsframebuffer_c.h  |  33 +++
 src/video/n3ds/SDL_n3dsswkb.c           |  76 +++++
 src/video/n3ds/SDL_n3dsswkb.h           |  38 +++
 src/video/n3ds/SDL_n3dsvideo.c          | 163 +++++++++++
 src/video/n3ds/SDL_n3dsvideo.h          |  38 +++
 test/CMakeLists.txt                     |  25 ++
 test/n3ds/logo48x48.png                 | Bin 0 -> 3069 bytes
 50 files changed, 2663 insertions(+), 5 deletions(-)
 create mode 100644 .github/workflows/n3ds.yml
 create mode 100644 docs/README-n3ds.md
 create mode 100644 src/audio/n3ds/SDL_n3dsaudio.c
 create mode 100644 src/audio/n3ds/SDL_n3dsaudio.h
 create mode 100644 src/file/n3ds/SDL_rwopsromfs.c
 create mode 100644 src/file/n3ds/SDL_rwopsromfs.h
 create mode 100644 src/filesystem/n3ds/SDL_sysfilesystem.c
 create mode 100644 src/joystick/n3ds/SDL_sysjoystick.c
 create mode 100644 src/locale/n3ds/SDL_syslocale.c
 create mode 100644 src/main/n3ds/SDL_n3ds_main.c
 create mode 100644 src/power/n3ds/SDL_syspower.c
 create mode 100644 src/thread/n3ds/SDL_syscond.c
 create mode 100644 src/thread/n3ds/SDL_sysmutex.c
 create mode 100644 src/thread/n3ds/SDL_sysmutex_c.h
 create mode 100644 src/thread/n3ds/SDL_syssem.c
 create mode 100644 src/thread/n3ds/SDL_systhread.c
 create mode 100644 src/thread/n3ds/SDL_systhread_c.h
 create mode 100644 src/timer/n3ds/SDL_systimer.c
 create mode 100644 src/video/n3ds/SDL_n3dsevents.c
 create mode 100644 src/video/n3ds/SDL_n3dsevents_c.h
 create mode 100644 src/video/n3ds/SDL_n3dsframebuffer.c
 create mode 100644 src/video/n3ds/SDL_n3dsframebuffer_c.h
 create mode 100644 src/video/n3ds/SDL_n3dsswkb.c
 create mode 100644 src/video/n3ds/SDL_n3dsswkb.h
 create mode 100644 src/video/n3ds/SDL_n3dsvideo.c
 create mode 100644 src/video/n3ds/SDL_n3dsvideo.h
 create mode 100644 test/n3ds/logo48x48.png

diff --git a/.github/workflows/n3ds.yml b/.github/workflows/n3ds.yml
new file mode 100644
index 000000000000..af985e4e368f
--- /dev/null
+++ b/.github/workflows/n3ds.yml
@@ -0,0 +1,40 @@
+name: Build (Nintendo 3DS)
+
+on: [push, pull_request]
+
+jobs:
+  n3ds:
+    runs-on: ubuntu-latest
+    container:
+      image: devkitpro/devkitarm:latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Install build requirements
+        run: |
+          apt update
+          apt install ninja-build
+      - name: Configure CMake
+        run: |
+          cmake -S . -B build -G Ninja \
+            -DCMAKE_TOOLCHAIN_FILE=${DEVKITPRO}/cmake/3DS.cmake \
+            -DSDL_TESTS=ON \
+            -DSDL_INSTALL_TESTS=ON \
+            -DCMAKE_BUILD_TYPE=Release \
+            -DCMAKE_INSTALL_PREFIX=prefix
+      - name: Build
+        run: cmake --build build --verbose
+      - name: Install CMake
+        run: |
+          echo "SDL2_DIR=$(pwd)/prefix" >> $GITHUB_ENV
+          cmake --install build/
+          ( cd prefix; find ) | LC_ALL=C sort -u
+      - name: Verify CMake configuration files
+        run: |
+          cmake -S cmake/test -B cmake_config_build -G Ninja \
+            -DCMAKE_TOOLCHAIN_FILE=${DEVKITPRO}/cmake/3DS.cmake \
+            -DTEST_SHARED=FALSE \
+            -DCMAKE_PREFIX_PATH=${{ env.SDL2_DIR }} \
+            -DCMAKE_BUILD_TYPE=Release
+          cmake --build cmake_config_build --verbose
+      # Not running test_pkgconfig.sh and test_sdlconfig.sh
+      # as invoking the compiler manually is not supported
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 15a6cbf288bc..48cbb6174f41 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -196,6 +196,8 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "BeOS.*")
   message_error("BeOS support has been removed as of SDL 2.0.2.")
 elseif(CMAKE_SYSTEM_NAME MATCHES "Haiku.*")
   set(HAIKU TRUE)
+elseif(NINTENDO_3DS)
+  set(N3DS TRUE)
 endif()
 
 # Don't mistake osx for unix
@@ -277,7 +279,7 @@ if(APPLE OR ARCH_64 OR MSVC_CLANG)
     set(OPT_DEF_SSEMATH ON)
   endif()
 endif()
-if(UNIX OR MINGW OR MSYS OR (USE_CLANG AND NOT WINDOWS) OR VITA OR PSP OR PS2)
+if(UNIX OR MINGW OR MSYS OR (USE_CLANG AND NOT WINDOWS) OR VITA OR PSP OR PS2 OR N3DS)
   set(OPT_DEF_LIBC ON)
 endif()
 
@@ -381,7 +383,7 @@ if(EMSCRIPTEN)
   set(SDL_TEST_ENABLED_BY_DEFAULT OFF)
 endif()
 
-if(VITA OR PSP OR PS2)
+if(VITA OR PSP OR PS2 OR N3DS)
   set(SDL_SHARED_ENABLED_BY_DEFAULT OFF)
   set(SDL_LOADSO_ENABLED_BY_DEFAULT OFF)
 endif()
@@ -2734,6 +2736,74 @@ elseif(OS2)
   if(SDL_HIDAPI)
     CheckHIDAPI()
   endif()
+
+elseif(N3DS)
+  file(GLOB N3DS_MAIN_SOURCES ${SDL2_SOURCE_DIR}/src/main/n3ds/*.c)
+  set(SDLMAIN_SOURCES ${SDLMAIN_SOURCES} ${N3DS_MAIN_SOURCES})
+
+  if(SDL_AUDIO)
+    set(SDL_AUDIO_DRIVER_N3DS 1)
+    file(GLOB N3DS_AUDIO_SOURCES ${SDL2_SOURCE_DIR}/src/audio/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_AUDIO_SOURCES})
+    set(HAVE_SDL_AUDIO TRUE)
+  endif()
+
+  if(SDL_FILESYSTEM)
+    set(SDL_FILESYSTEM_N3DS 1)
+    file(GLOB N3DS_FILESYSTEM_SOURCES ${SDL2_SOURCE_DIR}/src/filesystem/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_FILESYSTEM_SOURCES})
+    set(HAVE_SDL_FILESYSTEM TRUE)
+  endif()
+
+  if(SDL_JOYSTICK)
+    set(SDL_JOYSTICK_N3DS 1)
+    file(GLOB N3DS_JOYSTICK_SOURCES ${SDL2_SOURCE_DIR}/src/joystick/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_JOYSTICK_SOURCES})
+    set(HAVE_SDL_JOYSTICK TRUE)
+  endif()
+
+  if(SDL_POWER)
+    set(SDL_POWER_N3DS 1)
+    file(GLOB N3DS_POWER_SOURCES ${SDL2_SOURCE_DIR}/src/power/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_POWER_SOURCES})
+    set(HAVE_SDL_POWER TRUE)
+  endif()
+
+  if(SDL_THREADS)
+    set(SDL_THREAD_N3DS 1)
+    file(GLOB N3DS_THREAD_SOURCES ${SDL2_SOURCE_DIR}/src/thread/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_THREAD_SOURCES} ${SDL2_SOURCE_DIR}/src/thread/generic/SDL_systls.c)
+    set(HAVE_SDL_THREADS TRUE)
+  endif()
+
+  if(SDL_TIMERS)
+    set(SDL_TIMER_N3DS 1)
+    file(GLOB TIMER_SOURCES ${SDL2_SOURCE_DIR}/src/timer/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${TIMER_SOURCES})
+    set(HAVE_SDL_TIMERS TRUE)
+  endif()
+
+  if(SDL_VIDEO)
+    set(SDL_VIDEO_DRIVER_N3DS 1)
+    file(GLOB N3DS_VIDEO_SOURCES ${SDL2_SOURCE_DIR}/src/video/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_VIDEO_SOURCES})
+    set(HAVE_SDL_VIDEO TRUE)
+  endif()
+
+  if(SDL_LOCALE)
+    file(GLOB N3DS_LOCALE_SOURCES ${SDL2_SOURCE_DIR}/src/locale/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_LOCALE_SOURCES})
+    set(HAVE_SDL_LOCALE TRUE)
+  endif()
+
+  # Requires the n3ds file implementation
+  if(SDL_FILE)
+    file(GLOB N3DS_FILE_SOURCES ${SDL2_SOURCE_DIR}/src/file/n3ds/*.c)
+    list(APPEND SOURCE_FILES ${N3DS_FILE_SOURCES})
+    set(HAVE_SDL_FILE TRUE)
+  else()
+    message_error("SDL_FILE must be enabled to build on N3DS")
+  endif()
 endif()
 
 if(HAVE_VULKAN AND NOT SDL_LOADSO)
diff --git a/docs/README-n3ds.md b/docs/README-n3ds.md
new file mode 100644
index 000000000000..761c76dd6944
--- /dev/null
+++ b/docs/README-n3ds.md
@@ -0,0 +1,27 @@
+# Nintendo 3DS
+
+SDL port for the Nintendo 3DS [Homebrew toolchain](https://devkitpro.org/) contributed by:
+
+-   [Pierre Wendling](https://github.com/FtZPetruska)
+
+Credits to:
+
+-   The awesome people who ported SDL to other homebrew platforms.
+-   The Devkitpro team for making all the tools necessary to achieve this.
+
+## Building
+
+To build for the Nintendo 3DS, make sure you have devkitARM and cmake installed and run:
+
+```bash
+cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/3DS.cmake" -DCMAKE_BUILD_TYPE=Release
+cmake --build build
+cmake --install build
+```
+
+## Notes
+
+-   Currently only software rendering is supported.
+-   Window are created on the top screen by default, use the `SDL_WINDOW_N3DS_BOTTOM` flag to put them on the bottom screen.
+-   SDL2main should be used to ensure all the necessary services are initialised.
+-   By default, the extra L2 cache and higher clock speeds of the New 2/3DS lineup are enabled. If you wish to turn it off, [use the PTMSYSM service](https://libctru.devkitpro.org/ptmsysm_8h.html#ae3a437bfd0de05fbc5ba9a460d148430) to turn it off in your program.
diff --git a/include/SDL_config.h.cmake b/include/SDL_config.h.cmake
index 15c5c9e1d93f..6dc89a4a37dc 100644
--- a/include/SDL_config.h.cmake
+++ b/include/SDL_config.h.cmake
@@ -325,6 +325,7 @@
 #cmakedefine SDL_AUDIO_DRIVER_VITA @SDL_AUDIO_DRIVER_VITA@
 #cmakedefine SDL_AUDIO_DRIVER_PSP @SDL_AUDIO_DRIVER_PSP@
 #cmakedefine SDL_AUDIO_DRIVER_PS2 @SDL_AUDIO_DRIVER_PS2@
+#cmakedefine SDL_AUDIO_DRIVER_N3DS @SDL_AUDIO_DRIVER_N3DS@
 
 /* Enable various input drivers */
 #cmakedefine SDL_INPUT_LINUXEV @SDL_INPUT_LINUXEV@
@@ -349,6 +350,7 @@
 #cmakedefine SDL_JOYSTICK_VITA @SDL_JOYSTICK_VITA@
 #cmakedefine SDL_JOYSTICK_PSP @SDL_JOYSTICK_PSP@
 #cmakedefine SDL_JOYSTICK_PS2 @SDL_JOYSTICK_PS2@
+#cmakedefine SDL_JOYSTICK_N3DS @SDL_JOYSTICK_N3DS@
 #cmakedefine SDL_HAPTIC_DUMMY @SDL_HAPTIC_DUMMY@
 #cmakedefine SDL_HAPTIC_LINUX @SDL_HAPTIC_LINUX@
 #cmakedefine SDL_HAPTIC_IOKIT @SDL_HAPTIC_IOKIT@
@@ -381,6 +383,7 @@
 #cmakedefine SDL_THREAD_VITA @SDL_THREAD_VITA@
 #cmakedefine SDL_THREAD_PSP @SDL_THREAD_PSP@
 #cmakedefine SDL_THREAD_PS2 @SDL_THREAD_PS2@
+#cmakedefine SDL_THREAD_N3DS @SDL_THREAD_N3DS@
 
 /* Enable various timer systems */
 #cmakedefine SDL_TIMER_HAIKU @SDL_TIMER_HAIKU@
@@ -391,6 +394,7 @@
 #cmakedefine SDL_TIMER_VITA @SDL_TIMER_VITA@
 #cmakedefine SDL_TIMER_PSP @SDL_TIMER_PSP@
 #cmakedefine SDL_TIMER_PS2 @SDL_TIMER_PS2@
+#cmakedefine SDL_TIMER_N3DS @SDL_TIMER_N3DS@
 
 /* Enable various video drivers */
 #cmakedefine SDL_VIDEO_DRIVER_ANDROID @SDL_VIDEO_DRIVER_ANDROID@
@@ -444,6 +448,7 @@
 #cmakedefine SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS @SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS@
 #cmakedefine SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM @SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM@
 #cmakedefine SDL_VIDEO_DRIVER_VITA @SDL_VIDEO_DRIVER_VITA@
+#cmakedefine SDL_VIDEO_DRIVER_N3DS @SDL_VIDEO_DRIVER_N3DS@
 
 #cmakedefine SDL_VIDEO_RENDER_D3D @SDL_VIDEO_RENDER_D3D@
 #cmakedefine SDL_VIDEO_RENDER_D3D11 @SDL_VIDEO_RENDER_D3D11@
@@ -487,6 +492,7 @@
 #cmakedefine SDL_POWER_HARDWIRED @SDL_POWER_HARDWIRED@
 #cmakedefine SDL_POWER_VITA @SDL_POWER_VITA@
 #cmakedefine SDL_POWER_PSP @SDL_POWER_PSP@
+#cmakedefine SDL_POWER_N3DS @SDL_POWER_N3DS@
 
 /* Enable system filesystem support */
 #cmakedefine SDL_FILESYSTEM_ANDROID @SDL_FILESYSTEM_ANDROID@
@@ -501,6 +507,7 @@
 #cmakedefine SDL_FILESYSTEM_VITA @SDL_FILESYSTEM_VITA@
 #cmakedefine SDL_FILESYSTEM_PSP @SDL_FILESYSTEM_PSP@
 #cmakedefine SDL_FILESYSTEM_PS2 @SDL_FILESYSTEM_PS2@
+#cmakedefine SDL_FILESYSTEM_N3DS @SDL_FILESYSTEM_N3DS@
 
 /* Enable misc subsystem */
 #cmakedefine SDL_MISC_DUMMY @SDL_MISC_DUMMY@
diff --git a/include/SDL_main.h b/include/SDL_main.h
index 8b267082f23f..113d11de06a5 100644
--- a/include/SDL_main.h
+++ b/include/SDL_main.h
@@ -108,6 +108,15 @@
    void reset_IOP(); \
    void reset_IOP() {}
 
+#elif defined(__3DS__)
+/*
+  On N3DS, SDL provides a main function that sets up the screens
+  and storage.
+
+  If you provide this yourself, you may define SDL_MAIN_HANDLED
+*/
+#define SDL_MAIN_AVAILABLE
+
 #endif
 #endif /* SDL_MAIN_HANDLED */
 
diff --git a/include/SDL_platform.h b/include/SDL_platform.h
index f1f6f8b0666e..a8e3ac225ec7 100644
--- a/include/SDL_platform.h
+++ b/include/SDL_platform.h
@@ -221,6 +221,11 @@
 #define __VITA__ 1
 #endif
 
+#if defined(__3DS__)
+#undef __3DS__
+#define __3DS__ 1
+#endif
+
 #include "begin_code.h"
 /* Set up for C function definitions, even when using C++ */
 #ifdef __cplusplus
diff --git a/include/SDL_stdinc.h b/include/SDL_stdinc.h
index 5f79c95b3124..c60d6eebb28d 100644
--- a/include/SDL_stdinc.h
+++ b/include/SDL_stdinc.h
@@ -410,7 +410,7 @@ SDL_COMPILE_TIME_ASSERT(sint64, sizeof(Sint64) == 8);
 
 /** \cond */
 #ifndef DOXYGEN_SHOULD_IGNORE_THIS
-#if !defined(__ANDROID__) && !defined(__VITA__)
+#if !defined(__ANDROID__) && !defined(__VITA__) && !defined(__3DS__)
    /* TODO: include/SDL_stdinc.h:174: error: size of array 'SDL_dummy_enum' is negative */
 typedef enum
 {
diff --git a/include/SDL_video.h b/include/SDL_video.h
index d9dce43a96f4..60afda22a787 100644
--- a/include/SDL_video.h
+++ b/include/SDL_video.h
@@ -126,6 +126,7 @@ typedef enum
     SDL_WINDOW_KEYBOARD_GRABBED = 0x00100000,   /**< window has grabbed keyboard input */
     SDL_WINDOW_VULKAN           = 0x10000000,   /**< window usable for Vulkan surface */
     SDL_WINDOW_METAL            = 0x20000000,   /**< window usable for Metal view */
+    SDL_WINDOW_N3DS_BOTTOM      = 0x40000000,   /**< window should be on the bottom screen (N3DS only) */
 
     SDL_WINDOW_INPUT_GRABBED = SDL_WINDOW_MOUSE_GRABBED /**< equivalent to SDL_WINDOW_MOUSE_GRABBED for compatibility */
 } SDL_WindowFlags;
diff --git a/src/SDL.c b/src/SDL.c
index 0e1c32a92c6a..93f7a7f6de17 100644
--- a/src/SDL.c
+++ b/src/SDL.c
@@ -609,6 +609,8 @@ SDL_GetPlatform(void)
     return "PlayStation Vita";
 #elif __NGAGE__
     return "Nokia N-Gage";
+#elif __3DS__
+    return "Nintendo 3DS";
 #else
     return "Unknown (see SDL_platform.h)";
 #endif
diff --git a/src/SDL_log.c b/src/SDL_log.c
index 34c6fe8ae075..66191d2b413b 100644
--- a/src/SDL_log.c
+++ b/src/SDL_log.c
@@ -485,6 +485,13 @@ SDL_LogOutput(void *userdata, int category, SDL_LogPriority priority,
         fprintf(pFile, "%s: %s\n", SDL_priority_prefixes[priority], message);
         fclose (pFile);
     }
+#elif defined(__3DS__)
+    {
+        FILE*        pFile;
+        pFile = fopen ("/SDL_Log.txt", "a");
+        fprintf(pFile, "%s: %s\n", SDL_priority_prefixes[priority], message);
+        fclose (pFile);
+    }
 #endif
 #if HAVE_STDIO_H && \
     !(defined(__APPLE__) && (defined(SDL_VIDEO_DRIVER_COCOA) || defined(SDL_VIDEO_DRIVER_UIKIT)))
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
index e33e41dbc33e..11700d497d41 100644
--- a/src/audio/SDL_audio.c
+++ b/src/audio/SDL_audio.c
@@ -105,6 +105,9 @@ static const AudioBootStrap *const bootstrap[] = {
 #if SDL_AUDIO_DRIVER_VITA
     &VITAAUD_bootstrap,
 #endif
+#if SDL_AUDIO_DRIVER_N3DS
+    &N3DSAUDIO_bootstrap,
+#endif
 #if SDL_AUDIO_DRIVER_EMSCRIPTEN
     &EMSCRIPTENAUDIO_bootstrap,
 #endif
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
index 6afaae195c6c..a911de041e22 100644
--- a/src/audio/SDL_sysaudio.h
+++ b/src/audio/SDL_sysaudio.h
@@ -209,6 +209,7 @@ extern AudioBootStrap ANDROIDAUDIO_bootstrap;
 extern AudioBootStrap PS2AUDIO_bootstrap;
 extern AudioBootStrap PSPAUDIO_bootstrap;
 extern AudioBootStrap VITAAUD_bootstrap;
+extern AudioBootStrap N3DSAUDIO_bootstrap;
 extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap;
 extern AudioBootStrap OS2AUDIO_bootstrap;
 
diff --git a/src/audio/n3ds/SDL_n3dsaudio.c b/src/audio/n3ds/SDL_n3dsaudio.c
new file mode 100644
index 000000000000..484bec8ae9e2
--- /dev/null
+++ b/src/audio/n3ds/SDL_n3dsaudio.c
@@ -0,0 +1,363 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#ifdef SDL_AUDIO_DRIVER_N3DS
+
+#include "SDL_audio.h"
+
+/* N3DS Audio driver */
+
+#include "../SDL_sysaudio.h"
+#include "SDL_n3dsaudio.h"
+#include "SDL_timer.h"
+
+#define N3DSAUDIO_DRIVER_NAME "n3ds"
+
+static dspHookCookie dsp_hook;
+static SDL_AudioDevice *audio_device;
+
+static void FreePrivateData(_THIS);
+static int FindAudioFormat(_THIS);
+
+static SDL_INLINE void
+contextLock(_THIS)
+{
+    LightLock_Lock(&this->hidden->lock);
+}
+
+static SDL_INLINE void
+contextUnlock(_THIS)
+{
+    LightLock_Unlock(&this->hidden->lock);
+}
+
+static void
+N3DSAUD_LockAudio(_THIS)
+{
+    contextLock(this);
+}
+
+static void
+N3DSAUD_UnlockAudio(_THIS)
+{
+    contextUnlock(this);
+}
+
+static void
+N3DSAUD_DspHook(DSP_HookType hook)
+{
+    if (hook == DSPHOOK_ONCANCEL) {
+        contextLock(audio_device);
+        audio_device->hidden->isCancelled = SDL_TRUE;
+        SDL_AtomicSet(&audio_device->enabled, SDL_FALSE);
+        CondVar_Broadcast(&audio_device->hidden->cv);
+        contextUnlock(audio_device);
+    }
+}
+
+static void
+AudioFrameFinished(void *device)
+{
+    bool shouldBroadcast = false;
+    unsigned i;
+    SDL_AudioDevice *this = (SDL_AudioDevice *) device;
+
+    contextLock(this);
+
+    for (i = 0; i < NUM_BUFFERS; i++) {
+        if (this->hidden->waveBuf[i].status == NDSP_WBUF_DONE) {
+            this->hidden->waveBuf[i].status = NDSP_WBUF_FREE;
+            shouldBroadcast = SDL_TRUE;
+        }
+    }
+
+    if (shouldBroadcast) {
+        CondVar_Broadcast(&this->hidden->cv);
+    }
+
+    contextUnlock(this);
+}
+
+static int
+N3DSAUDIO_OpenDevice(_THIS, const char *devname)
+{
+    Result ndsp_init_res;
+    Uint8 *data_vaddr;
+    float mix[12];
+    this->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof *this->hidden);
+
+    if (this->hidden == NULL) {
+        return SDL_OutOfMemory();
+    }
+
+    /* Initialise the DSP service */
+    ndsp_init_res = ndspInit();
+    if (R_FAILED(ndsp_init_res)) {
+        if ((R_SUMMARY(ndsp_init_res) == RS_NOTFOUND) && (R_MODULE(ndsp_init_res) == RM_DSP)) {
+            SDL_SetError("DSP init failed: dspfirm.cdc missing!");
+        } else {
+            SDL_SetError("DSP init failed. Error code: 0x%lX", ndsp_init_res);
+        }
+        return -1;
+    }
+
+    /* Initialise internal state */
+    LightLock_Init(&this->hidden->lock);
+    CondVar_Init(&this->hidden->cv);
+
+    if (this->spec.channels > 2) {
+        this->spec.channels = 2;
+    }
+
+    /* Should not happen but better be safe. */
+    if (FindAudioFormat(this) < 0) {
+        return SDL_SetError("No supported audio format found.");
+    }
+
+    /* Update the fragment size as size in bytes */
+    SDL_CalculateAudioSpec(&this->spec);
+
+    /* Allocate mixing buffer */
+    if (this->spec.size >= SDL_MAX_UINT32 / 2) {
+        return SDL_SetError("Mixing buffer is too large.");
+    }
+
+    this->hidden->mixlen = this->spec.size;
+    this->hidden->mixbuf = (Uint8 *) SDL_malloc(this->spec.size);
+    if (this->hidden->mixbuf == NULL) {
+        return SDL_OutOfMemory();
+    }
+
+    SDL_memset(this->hidden->mixbuf, this->spec.silence, this->spec.size);
+
+    data_vaddr = (Uint8 *) linearAlloc(this->hidden->mixlen * NUM_BUFFERS);
+    if (data_vaddr == NULL) {
+        return SDL_OutOfMemory();
+    }
+
+    SDL_memset(data_vaddr, 0, this->hidden->mixlen * NUM_BUFFERS);
+    DSP_FlushDataCache(data_vaddr, this->hidden->mixlen * NUM_BUFFERS);
+
+    this->hidden->nextbuf = 0;
+    this->hidden->channels = this->spec.channels;
+    this->hidden->samplerate = this->spec.freq;
+
+    ndspChnReset(0);
+
+    ndspChnSetInterp(0, NDSP_INTERP_LINEAR);
+    ndspChnSetRate(0, this->spec.freq);
+    ndspChnSetFormat(0, this->hidden->format);
+
+    SDL_memset(mix, 0, sizeof(mix));
+    mix[0] = 1.0;
+    mix[1] = 1.0;
+    ndspChnSetMix(0, mix);
+
+    SDL_memset(this->hidden->waveBuf, 0, sizeof(ndspWaveBuf) * NUM_BUFFERS);
+
+    for (unsigned i = 0; i < NUM_BUFFERS; i++) {
+        this->hidden->waveBuf[i].data_vaddr = data_vaddr;
+        this->hidden->waveBuf[i].nsamples = this->hidden->mixlen / this->hidden->bytePerSample;
+        data_vaddr += this->hidden->mixlen;
+    }
+
+    /* Setup callback */
+    audio_device = this;
+    ndspSetCallback(AudioFrameFinished, this);
+    dspHook(&dsp_hook, N3DSAUD_DspHook);
+
+    return 0;
+}
+
+static int
+N3DSAUDIO_CaptureFromDevice(_THIS, void *buffer, int buflen)
+{
+    /* Delay to make this sort of simulate real audio input. */
+    SDL_Delay((this->spec.samples * 1000) / this->spec.freq);
+
+    /* always return a full buffer of silence. */
+    SDL_memset(buffer, this->spec.silence, buflen);
+    return buflen;
+}
+
+static void
+N3DSAUDIO_PlayDevice(_THIS)
+{
+    size_t nextbuf;
+    size_t sampleLen;
+    contextLock(this);
+
+    nextbuf = this->hidden->nextbuf;
+    sampleLen = this->hidden->mixlen;
+
+    if (this->hidden->isCancelled ||
+        this->hidden->waveBuf[nextbuf].status != NDSP_WBUF_FREE) {
+        contextUnlock(this);
+        return;
+    }
+
+    this->hidden->nextbuf = (nextbuf + 1) % NUM_BUFFERS;
+
+    contextUnlock(this);
+
+    memcpy((void *) this->hidden->waveBuf[nextbuf].data_vaddr,
+           this->hidden->mixbuf, sampleLen);
+    DSP_FlushDataCache(this->hidden->waveBuf[nextbuf].data_vaddr, sampleLen);
+
+    ndspChnWaveBufAdd(0, &this->hidden->waveBuf[nextbuf]);
+}
+
+static void
+N3DSAUDIO_WaitDevice(_THIS)
+{
+    contextLock(this);
+    while (!this->hidden->isCancelled &&
+           this->hidden->waveBuf[this->hidden->nextbuf].status != NDSP_WBUF_FREE) {
+        CondVar_Wait(&this->hidden->cv, &this->hidden->lock);
+    }
+    contextUnlock(this);
+}
+
+static Uint8 *
+N3DSAUDIO_GetDeviceBuf(_THIS)
+{
+    return this->hidden->mixbuf;
+}
+
+static void
+N3DSAUDIO_CloseDevice(_THIS)
+{
+    contextLock(this);
+
+    dspUnhook(&dsp_hook);
+    ndspSetCallback(NULL, NULL);
+
+    if (!this->hidden->isCancelled) {
+        ndspChnReset(0);
+        memset(this->hidden->waveBuf, 0, sizeof(ndspWaveBuf) * NUM_BUFFERS);
+        CondVar_Broadcast(&this->hidden->cv);
+    }
+
+    contextUnlock(this);
+
+    ndspExit();
+
+    FreePrivateData(this);
+}
+
+static void
+N3DSAUDIO_ThreadInit(_THIS)
+{
+    s32 current_priority;
+    svcGetThreadPriority(&current_priority, CUR_THREAD_HANDLE);
+    current_priority--;
+    /* 0x18 is reserved for video, 0x30 is the default for main thread */
+    current_priority = SDL_clamp(current_priority, 0x19, 0x2F);
+    svcSetThreadPriority(CUR_THREAD_HANDLE, current_priority);
+}
+
+static SDL_bool
+N3DSAUDIO_Init(SDL_AudioDriverImpl *impl)
+{
+    /* Set the function pointers */
+    impl->OpenDevice = N3DSAUDIO_OpenDevice;
+    impl->PlayDevice = N3DSAUDIO_PlayDevice;
+    impl->WaitDevice = N3DSAUDIO_WaitDevice;
+    impl->GetDeviceBuf = N3DSAUDIO_GetDeviceBuf;
+    impl->CloseDevice = N3DSAUDIO_CloseDevice;
+    impl->ThreadInit = N3DSAUDIO_ThreadInit;
+    impl->LockDevice = N3DSAUD_LockAudio;
+    impl->UnlockDevice = N3DSAUD_UnlockAudio;
+    impl->OnlyHasDefaultOutputDevice = SDL_TRUE;
+
+    /* Should be possible, but micInit would fail */
+    impl->HasCaptureSupport = SDL_FALSE;
+    impl->CaptureFromDevice = N3DSAUDIO_CaptureFromDevice;
+
+    return SDL_TRUE; /* this audio target is available. */
+}
+
+AudioBootStrap N3DSAUDIO_bootstrap = {
+    N3DSAUDIO_DRIVER_NAME,
+    "SDL N3DS audio driver",
+    N3DSAUDIO_Init,
+    0
+};
+
+/**
+ * Cleans up all allocated memory, safe to call with null pointers
+ */
+static void
+FreePrivateData(_THIS)
+{
+    if (!this->hidden) {
+        return;
+    }
+
+    if (this->hidden->waveBuf[0].data_vaddr) {
+        linearFree((void *) this->hidden->waveBuf[0].data_vaddr);
+    }
+
+    if (this->hidden->mixbuf) {
+        SDL_free(this->hidden->mixbuf);
+        this->hidden->mixbuf = NULL;
+    }
+
+    SDL_free(this->hidden);
+    this->hidden = NULL;
+}
+
+static int
+FindAudioFormat(_THIS)
+{
+    SDL_bool found_valid_format = SDL_FALSE;
+    Uint16 test_format = SDL_FirstAudioFormat(this->spec.format);
+
+    while (!found_valid_format && test_format) {
+        this->spec.format = test_format;
+        switch (test_format) {
+        case AUDIO_S8:
+            /* Signed 8-bit audio supported */
+            this->hidden->format = (this->spec.channels == 2) ? NDSP_FORMAT_STEREO_PCM8 : NDSP_FORMAT_MONO_PCM8;
+            this->hidden->isSigned = 1;
+            this->hidden->bytePerSample = this->spec.channels;
+            found_valid_format = SDL_TRUE;
+            break;
+        case AUDIO_S16:
+            /* Signed 16-bit audio supported */
+            this->hidden->format = (this->spec.channels == 2) ? NDSP_FORMAT_STEREO_PCM16 : NDSP_FORMAT_MONO_PCM16;
+            this->hidden->isSigned = 1;
+            this->hidden->bytePerSample = this->spec.channels * 2;
+            found_valid_format = SDL_TRUE;
+            break;
+        default:
+            test_format = SDL_NextAudioFormat();
+            break;
+        }
+    }
+
+    return found_valid_format ? 0 : -1;
+}
+
+#endif /* SDL_AUDIO_DRIVER_N3DS */
+
+/* vi: set sts=4 ts=4 sw=4 expandtab: */
diff --git a/src/audio/n3ds/SDL_n3dsaudio.h b/src/audio/n3ds/SDL_n3dsaudio.h
new file mode 100644
index 000000000000..d01f17f539a6
--- /dev/null
+++ b/src/audio/n3ds/SDL_n3dsaudio.h
@@ -0,0 +1,50 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef _SDL_n3dsaudio_h_
+#define _SDL_n3dsaudio_h_
+
+#include <3ds.h>
+
+/* Hidden "this" pointer for the audio functions */
+#define _THIS SDL_AudioDevice *this
+
+#define NUM_BUFFERS 2 /* -- Don't lower this! */
+
+struct SDL_PrivateAudioData
+{
+    /* Speaker data */
+    Uint8 *mixbuf;
+    Uint32 mixlen;
+    Uint32 format;
+    Uint32 samplerate;
+    Uint32 channels;
+    Uint8 bytePerSample;
+    Uint32 isSigned;
+    Uint32 nextbuf;
+    ndspWaveBuf waveBuf[NUM_BUFFERS];
+    LightLock lock;
+    CondVar cv;
+    SDL_bool isCancelled;
+};
+
+#endif /* _SDL_n3dsaudio_h_ */
+/* vi: set sts=4 ts=4 sw=4 expandtab: */
diff --git a/src/cpuinfo/SDL_cpuinfo.c b/src/cpuinfo/SDL_cpuinfo.c
index 8221f7796713..6ba5c85eb478 100644
--- a/src/cpuinfo/SDL_cpuinfo.c
+++ b/src/cpuinfo/SDL_cpuinfo.c
@@ -468,6 +468,8 @@ CPU_haveNEON(void)
     return 1;  /* ARMv8 always has non-optional NEON support. */
 #elif __VITA__
     return 1;
+#elif __3DS__
+    return 1;
 #elif defined(__APPLE__) && defined(__ARM_ARCH) && (__ARM_ARCH >= 7)
     /* (note that sysctlbyname("hw.optional.neon") doesn't work!) */
     return 1;  /* all Apple ARMv7 chips and later have NEON. */
diff --git a/src/dynapi/SDL_dynapi.h b/src/dynapi/SDL_dynapi.h
index dc53e58b2887..e3268697cd55 100644
--- a/src/dynapi/SDL_dynapi.h
+++ b/src/dynapi/SDL_dynapi.h
@@ -63,6 +63,8 @@
 #define SDL_DYNAMIC_API 0  /* vitasdk doesn't support dynamic linking */
 #elif defined(__NGAGE__)
 #define SDL_DYNAMIC_API 0  /* The N-Gage doesn't support dynamic linking either */
+#elif defined(__3DS__)
+#define SDL_DYNAMIC_API 0  /* devkitARM doesn't support dynamic linking */
 #elif defined(DYNAPI_NEEDS_DLOPEN) && !defined(HAVE_DLOPEN)
 #define SDL_DYNAMIC_API 0  /* we need dlopen(), but don't have it.... */
 #endif
diff --git a/src/file/SDL_rwops.c b/src/file/SDL_rwops.c
index a93c2fb3ce00..ead504c10a78 100644
--- a/src/file/SDL_rwops.c
+++ b/src/file/SDL_rwops.c
@@ -53,6 +53,10 @@
 #include "cocoa/SDL_rwopsbundlesupport.h"
 #endif /* __APPLE__ */
 
+#ifdef __3DS__
+#include "n3ds/SDL_rwopsromfs.h"
+#endif /* __3DS__ */
+
 #ifdef __ANDROID__
 #include "../core/android/SDL_android.h"
 #include "SDL_system.h"
@@ -601,6 +605,8 @@ SDL_RWFromFile(const char *file, const char *mode)
         #elif __WINRT__
         FILE *fp = NULL;
         fopen_s(&fp, file, mode);
+        #elif __3DS__
+        FILE *fp = N3DS_FileOpen(file, mode);
         #else
         FILE *fp = fopen(file, mode);
         #endif
diff --git a/src/file/n3ds/SDL_rwopsromfs.c b/src/file/n3ds/SDL_rwopsromfs.c
new file mode 100644
index 000000000000..fe92c3d3be76
--- /dev/null
+++ b/src/file/n3ds/SDL_rwopsromfs.c
@@ -0,0 +1,56 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2022 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to a

(Patch may be truncated, please check the link at the top of this post.)