Parcourir la source

Restore support for the Nokia N-Gage (#12148)

Michael Fitzmayer il y a 2 mois
Parent
commit
7ae64592c9
51 fichiers modifiés avec 4183 ajouts et 43 suppressions
  1. 102 0
      .github/actions/setup-ngage-sdk/action.yml
  2. 19 2
      .github/workflows/create-test-plan.py
  3. 5 0
      .github/workflows/generic.yml
  4. 98 4
      CMakeLists.txt
  5. 189 0
      cmake/PreseedNokiaNGageCache.cmake
  6. 2 0
      cmake/sdlplatform.cmake
  7. 8 6
      cmake/test/CMakeLists.txt
  8. 24 11
      cmake/test/main_gui.c
  9. 17 2
      cmake/test/sdltest.c
  10. 62 3
      docs/README-ngage.md
  11. 4 0
      examples/CMakeLists.txt
  12. 1 1
      include/SDL3/SDL_assert.h
  13. 1 1
      include/SDL3/SDL_begin_code.h
  14. 11 1
      include/SDL3/SDL_platform_defines.h
  15. 4 0
      include/build_config/SDL_build_config.h.cmake
  16. 2 0
      src/SDL.c
  17. 2 0
      src/SDL_error.c
  18. 24 0
      src/SDL_log.c
  19. 3 0
      src/audio/SDL_audio.c
  20. 1 0
      src/audio/SDL_sysaudio.h
  21. 103 0
      src/audio/ngage/SDL_ngageaudio.c
  22. 368 0
      src/audio/ngage/SDL_ngageaudio.cpp
  23. 44 0
      src/audio/ngage/SDL_ngageaudio.h
  24. 98 0
      src/audio/ngage/SDL_ngageaudio.hpp
  25. 77 0
      src/core/ngage/SDL_ngage.cpp
  26. 36 0
      src/core/ngage/SDL_ngage.h
  27. 8 0
      src/cpuinfo/SDL_cpuinfo.c
  28. 2 0
      src/dynapi/SDL_dynapi.h
  29. 67 0
      src/filesystem/ngage/SDL_sysfilesystem.c
  30. 68 0
      src/filesystem/ngage/SDL_sysfilesystem.cpp
  31. 307 0
      src/locale/ngage/SDL_syslocale.cpp
  32. 31 0
      src/main/ngage/SDL_sysmain_callbacks.c
  33. 199 0
      src/main/ngage/SDL_sysmain_main.cpp
  34. 46 0
      src/main/ngage/SDL_sysmain_main.hpp
  35. 3 0
      src/render/SDL_render.c
  36. 1 0
      src/render/SDL_sysrender.h
  37. 544 0
      src/render/ngage/SDL_render_ngage.c
  38. 744 0
      src/render/ngage/SDL_render_ngage.cpp
  39. 105 0
      src/render/ngage/SDL_render_ngage_c.h
  40. 91 0
      src/render/ngage/SDL_render_ngage_c.hpp
  41. 152 0
      src/render/ngage/SDL_render_ops.cpp
  42. 32 0
      src/render/ngage/SDL_render_ops.hpp
  43. 2 0
      src/stdlib/SDL_string.c
  44. 2 1
      src/stdlib/SDL_vacopy.h
  45. 184 0
      src/time/ngage/SDL_systime.cpp
  46. 47 0
      src/timer/ngage/SDL_systimer.cpp
  47. 1 0
      src/video/SDL_sysvideo.h
  48. 3 0
      src/video/SDL_video.c
  49. 175 0
      src/video/ngage/SDL_ngagevideo.c
  50. 39 0
      src/video/ngage/SDL_ngagevideo.h
  51. 25 11
      test/CMakeLists.txt

+ 102 - 0
.github/actions/setup-ngage-sdk/action.yml

@@ -0,0 +1,102 @@
+name: 'Setup Nonka N-Gage SDK'
+description: 'Download and setup Nokia N-Gage SDK'
+inputs:
+  path:
+    description: 'Installation path'
+    default: 'default'
+runs:
+  using: 'composite'
+  steps:
+    - uses: actions/setup-python@v5
+      with:
+        python-version: '3.x'
+    - name: 'Verify platform'
+      id: calc
+      shell: sh
+      run: |
+        case "${{ runner.os }}-${{ runner.arch }}" in
+          "Windows-X86" | "Windows-X64")
+            echo "ok!"
+            echo "cache-key=ngage-sdk-windows" >> ${GITHUB_OUTPUT}
+            default_install_path="C:/ngagesdk"
+            ;;
+          *)
+            echo "Unsupported ${{ runner.os }}-${{ runner.arch }}"
+            exit 1;
+            ;;
+        esac
+        install_path="${{ inputs.path }}"
+        if [ "x$install_path" = "xdefault" ]; then
+          install_path="$default_install_path"
+        fi
+        echo "install-path=$install_path" >> ${GITHUB_OUTPUT}
+
+        toolchain_repo="https://github.com/ngagesdk/ngage-toolchain"
+        toolchain_branch="main"
+        echo "toolchain-repo=${toolchain_repo}"     >> ${GITHUB_OUTPUT}
+        echo "toolchain-branch=${toolchain_branch}" >> ${GITHUB_OUTPUT}
+
+        sdk_repo="https://github.com/ngagesdk/sdk"
+        sdk_branch="main"
+        echo "sdk-repo=${sdk_repo}"       >> ${GITHUB_OUTPUT}
+        echo "sdk-branch=${sdk_branch}"   >> ${GITHUB_OUTPUT}
+
+        tools_repo="https://github.com/ngagesdk/tools"
+        tools_branch="main"
+        echo "tools-repo=${tools_repo}"       >> ${GITHUB_OUTPUT}
+        echo "tools-branch=${tools_branch}"   >> ${GITHUB_OUTPUT}
+
+        extras_repo="https://github.com/ngagesdk/extras"
+        extras_branch="main"
+        echo "extras-repo=${extras_repo}"       >> ${GITHUB_OUTPUT}
+        echo "extras-branch=${extras_branch}"   >> ${GITHUB_OUTPUT}
+#    - name: 'Restore cached ${{ steps.calc.outputs.archive }}'
+#      id: cache-restore
+#      uses: actions/cache/restore@v4
+#      with:
+#        path: '${{ runner.temp }}'
+#        key: ${{ steps.calc.outputs.cache-key }}
+    - name: 'Download N-Gage SDK'
+#      if: ${{ !steps.cache-restore.outputs.cache-hit || steps.cache-restore.outputs.cache-hit == 'false' }}
+      shell: pwsh
+      run: |
+
+        Invoke-WebRequest "${{ steps.calc.outputs.toolchain-repo }}/archive/refs/heads/${{ steps.calc.outputs.toolchain-branch }}.zip"  -OutFile "${{ runner.temp }}/ngage-toolchain.zip"
+        Invoke-WebRequest "${{ steps.calc.outputs.sdk-repo }}/archive/refs/heads/${{ steps.calc.outputs.sdk-branch }}.zip"              -OutFile "${{ runner.temp }}/sdk.zip"
+        Invoke-WebRequest "${{ steps.calc.outputs.tools-repo }}/archive/refs/heads/${{ steps.calc.outputs.tools-branch }}.zip"          -OutFile "${{ runner.temp }}/tools.zip"
+        Invoke-WebRequest "${{ steps.calc.outputs.extras-repo }}/archive/refs/heads/${{ steps.calc.outputs.extras-branch }}.zip"        -OutFile "${{ runner.temp }}/extras.zip"
+
+#    - name: 'Cache ${{ steps.calc.outputs.archive }}'
+#      if: ${{ !steps.cache-restore.outputs.cache-hit || steps.cache-restore.outputs.cache-hit == 'false' }}
+#      uses: actions/cache/save@v4
+#      with:
+#        path: |
+#          ${{ runner.temp }}/apps.zip
+#          ${{ runner.temp }}/sdk.zip
+#          ${{ runner.temp }}/tools.zip
+#        key: ${{ steps.calc.outputs.cache-key }}
+    - name: 'Extract N-Gage SDK'
+      shell: pwsh
+      run: |
+        New-Item -ItemType Directory -Path "${{ steps.calc.outputs.install-path }}" -Force
+
+        New-Item -ItemType Directory -Path "${{ runner.temp }}/ngage-toolchain-temp" -Force 
+        7z "-o${{ runner.temp }}/ngage-toolchain-temp"      x "${{ runner.temp }}/ngage-toolchain.zip"
+        Move-Item -Path "${{ runner.temp }}/ngage-toolchain-temp/ngage-toolchain-${{ steps.calc.outputs.toolchain-branch }}/*" -Destination "${{ steps.calc.outputs.install-path }}"
+
+        7z "-o${{ steps.calc.outputs.install-path }}/sdk"   x "${{ runner.temp }}/sdk.zip"
+        Move-Item -Path "${{ steps.calc.outputs.install-path }}/sdk/sdk-${{ steps.calc.outputs.sdk-branch }}"       -Destination "${{ steps.calc.outputs.install-path }}/sdk/sdk"
+
+        7z "-o${{ steps.calc.outputs.install-path }}/sdk"   x "${{ runner.temp }}/tools.zip"
+        Move-Item -Path "${{ steps.calc.outputs.install-path }}/sdk/tools-${{ steps.calc.outputs.tools-branch }}"   -Destination "${{ steps.calc.outputs.install-path }}/sdk/tools"
+
+        7z "-o${{ steps.calc.outputs.install-path }}/sdk"   x "${{ runner.temp }}/extras.zip"
+        Move-Item -Path "${{ steps.calc.outputs.install-path }}/sdk/extras-${{ steps.calc.outputs.extras-branch }}" -Destination "${{ steps.calc.outputs.install-path }}/sdk/extras"
+    - name: 'Set output variables'
+      id: final
+      shell: sh
+      run: |
+        echo "${{ steps.calc.outputs.install-path }}/sdk/sdk/6.1/Shared/EPOC32/gcc/bin" >> $GITHUB_PATH
+        echo "${{ steps.calc.outputs.install-path }}/sdk/sdk/6.1/Shared/EPOC32/ngagesdk/bin" >> $GITHUB_PATH
+        echo "NGAGESDK=${{ steps.calc.outputs.install-path }}" >> $GITHUB_ENV
+        echo "CMAKE_TOOLCHAIN_FILE=${{ steps.calc.outputs.install-path }}/cmake/ngage-toolchain.cmake" >> $GITHUB_ENV

+ 19 - 2
.github/workflows/create-test-plan.py

@@ -54,6 +54,7 @@ class SdlPlatform(Enum):
     Riscos = "riscos"
     FreeBSD = "freebsd"
     NetBSD = "netbsd"
+    NGage = "ngage"
 
 
 class Msys2Platform(Enum):
@@ -139,11 +140,12 @@ JOB_SPECS = {
     "riscos": JobSpec(name="RISC OS",                                       os=JobOs.UbuntuLatest,      platform=SdlPlatform.Riscos,      artifact="SDL-riscos",             container="riscosdotinfo/riscos-gccsdk-4.7:latest", ),
     "netbsd": JobSpec(name="NetBSD",                                        os=JobOs.UbuntuLatest,      platform=SdlPlatform.NetBSD,      artifact="SDL-netbsd-x64", ),
     "freebsd": JobSpec(name="FreeBSD",                                      os=JobOs.UbuntuLatest,      platform=SdlPlatform.FreeBSD,     artifact="SDL-freebsd-x64", ),
+    "ngage": JobSpec(name="N-Gage",                                         os=JobOs.WindowsLatest,     platform=SdlPlatform.NGage,       artifact="SDL-ngage", ),
 }
 
 
 class StaticLibType(Enum):
-    MSVC = "SDL3-static.lib"
+    STATIC_LIB = "SDL3-static.lib"
     A = "libSDL3.a"
 
 
@@ -223,6 +225,7 @@ class JobDetails:
     check_sources: bool = False
     setup_python: bool = False
     pypi_packages: list[str] = dataclasses.field(default_factory=list)
+    setup_gage_sdk_path: str = ""
 
     def to_workflow(self, enable_artifacts: bool) -> dict[str, str|bool]:
         data = {
@@ -290,6 +293,7 @@ class JobDetails:
             "check-sources": self.check_sources,
             "setup-python": self.setup_python,
             "pypi-packages": my_shlex_join(self.pypi_packages),
+            "setup-ngage-sdk-path": self.setup_gage_sdk_path,
         }
         return {k: v for k, v in data.items() if v != ""}
 
@@ -365,7 +369,7 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool) -> JobDeta
             job.msvc_project_flags.append("-p:TreatWarningsAsError=true")
             job.test_pkg_config = False
             job.shared_lib = SharedLibType.WIN32
-            job.static_lib = StaticLibType.MSVC
+            job.static_lib = StaticLibType.STATIC_LIB
             job.cmake_arguments.extend((
                 "-DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=ProgramDatabase",
                 "-DCMAKE_EXE_LINKER_FLAGS=-DEBUG",
@@ -740,6 +744,19 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool) -> JobDeta
                     job.cpactions_arch = "x86-64"
                     job.cpactions_setup_cmd = "export PATH=\"/usr/pkg/sbin:/usr/pkg/bin:/sbin:$PATH\"; export PKG_CONFIG_PATH=\"/usr/pkg/lib/pkgconfig\";export PKG_PATH=\"https://cdn.netBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f \"1 2\" -d.)/All/\";echo \"PKG_PATH=$PKG_PATH\";echo \"uname -a -> \"$(uname -a)\"\";sudo -E sysctl -w security.pax.aslr.enabled=0;sudo -E sysctl -w security.pax.aslr.global=0;sudo -E pkgin clean;sudo -E pkgin update"
                     job.cpactions_install_cmd = "sudo -E pkgin -y install cmake dbus pkgconf ninja-build pulseaudio libxkbcommon wayland wayland-protocols libinotify libusb1"
+        case SdlPlatform.NGage:
+            build_parallel = False
+            job.cmake_build_type = "Release"
+            job.setup_ninja = True
+            job.static_lib = StaticLibType.STATIC_LIB
+            job.shared_lib = None
+            job.clang_tidy = False
+            job.werror = False  # FIXME: enable SDL_WERROR
+            job.shared = False
+            job.run_tests = False
+            job.setup_gage_sdk_path = "C:/ngagesdk"
+            job.cmake_toolchain_file = "C:/ngagesdk/cmake/ngage-toolchain.cmake"
+            job.test_pkg_config = False
         case _:
             raise ValueError(f"Unsupported platform={spec.platform}")
 

+ 5 - 0
.github/workflows/generic.yml

@@ -93,6 +93,11 @@ jobs:
         with:
           arch: ${{ matrix.platform.msvc-vcvars-arch }}
           sdk: ${{ matrix.platform.msvc-vcvars-sdk }}
+      - name: 'Set up Nokia N-Gage SDK'
+        uses: ./.github/actions/setup-ngage-sdk
+        if: ${{ matrix.platform.setup-ngage-sdk-path != '' }}
+        with:
+          path: '${{ matrix.platform.setup-ngage-sdk-path }}'
       - name: 'Set up Windows GDK Desktop'
         uses: ./.github/actions/setup-gdk-desktop
         if: ${{ matrix.platform.setup-gdk-folder != '' }}

+ 98 - 4
CMakeLists.txt

@@ -76,6 +76,7 @@ include("${SDL3_SOURCE_DIR}/cmake/GetGitRevisionDescription.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/3rdparty.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedMSVCCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedEmscriptenCache.cmake")
+include("${SDL3_SOURCE_DIR}/cmake/PreseedNokiaNGageCache.cmake")
 
 SDL_DetectCompiler()
 SDL_DetectTargetCPUArchitectures(SDL_CPUS)
@@ -155,7 +156,7 @@ endif()
 # The hidraw support doesn't catch Xbox, PS4 and Nintendo controllers,
 #  so we'll just use libusb when it's available. libusb does not support iOS,
 #  so we default to yes on iOS.
-if(IOS OR TVOS OR VISIONOS OR WATCHOS OR ANDROID)
+if(IOS OR TVOS OR VISIONOS OR WATCHOS OR ANDROID OR NGAGE)
   set(SDL_HIDAPI_LIBUSB_AVAILABLE FALSE)
 else()
   set(SDL_HIDAPI_LIBUSB_AVAILABLE TRUE)
@@ -219,7 +220,7 @@ if(EMSCRIPTEN)
   set(SDL_SHARED_AVAILABLE OFF)
 endif()
 
-if(VITA OR PSP OR PS2 OR N3DS OR RISCOS)
+if(VITA OR PSP OR PS2 OR N3DS OR RISCOS OR NGAGE)
   set(SDL_SHARED_AVAILABLE OFF)
 endif()
 
@@ -414,6 +415,24 @@ if(VITA)
   set_option(VIDEO_VITA_PVR  "Build with PSVita PVR gles/gles2 support" OFF)
 endif()
 
+if (NGAGE)
+  set(SDL_GPU              OFF)
+  set(SDL_CAMERA           OFF)
+  set(SDL_JOYSTICK         OFF)
+  set(SDL_HAPTIC           OFF)
+  set(SDL_HIDAPI           OFF)
+  set(SDL_POWER            OFF)
+  set(SDL_SENSOR           OFF)
+  set(SDL_DIALOG           OFF)
+  set(SDL_DISKAUDIO        OFF)
+  set(SDL_DUMMYAUDIO       OFF)
+  set(SDL_DUMMYCAMERA      OFF)
+  set(SDL_DUMMYVIDEO       OFF)
+  set(SDL_OFFSCREEN        OFF)
+  set(SDL_RENDER_GPU       OFF)
+  set(SDL_VIRTUAL_JOYSTICK OFF)
+endif()
+
 if(NOT (SDL_SHARED OR SDL_STATIC))
   message(FATAL_ERROR "SDL_SHARED and SDL_STATIC cannot both be disabled")
 endif()
@@ -2931,6 +2950,81 @@ elseif(N3DS)
   set(HAVE_SDL_LOCALE TRUE)
 
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/io/n3ds/*.c")
+
+elseif(NGAGE)
+
+  enable_language(CXX)
+
+  set(SDL_MAIN_USE_CALLBACKS 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/main/ngage/*.c")
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/main/ngage/*.cpp")
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/core/ngage/*.cpp")
+  set(HAVE_SDL_MAIN_CALLBACKS TRUE)
+
+  if(SDL_AUDIO)
+    set(SDL_AUDIO_DRIVER_NGAGE 1)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/audio/ngage/*.c")
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/audio/ngage/*.cpp")
+    set(HAVE_SDL_AUDIO TRUE)
+  endif()
+
+  set(SDL_FILESYSTEM_NGAGE 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/ngage/*.c")
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/ngage/*.cpp")
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/posix/*.c")
+  set(HAVE_SDL_FILESYSTEM TRUE)
+
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/locale/ngage/*.cpp")
+
+  if(SDL_RENDER)
+    set(SDL_VIDEO_RENDER_NGAGE 1)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/render/ngage/*.c")
+  endif()
+
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/time/ngage/*.cpp")
+  set(SDL_TIME_NGAGE 1)
+
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/render/ngage/*.cpp")
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/time/unix/*.c")
+
+  set(SDL_TIMER_NGAGE 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/timer/ngage/*.cpp")
+
+  set(SDL_FSOPS_POSIX        1)
+
+  set(SDL_VIDEO_DRIVER_NGAGE 1)
+  sdl_glob_sources("${SDL3_SOURCE_DIR}/src/video/ngage/*.c")
+  set(HAVE_SDL_TIMERS TRUE)
+
+  set_option(SDL_LEAN_AND_MEAN "Enable lean and mean" ON)
+  if(SDL_LEAN_AND_MEAN)
+    sdl_compile_definitions(
+      PRIVATE
+      SDL_LEAN_AND_MEAN
+    )
+  endif()
+
+  sdl_link_dependency(ngage
+    LINK_OPTIONS "SHELL:-s MAIN_COMPAT=0"
+    PKG_CONFIG_LINK_OPTIONS "-s;MAIN_COMPAT=0"
+    LIBS
+      NRenderer
+      3dtypes
+      cone
+      libgcc
+      libgcc_ngage
+      mediaclientaudiostream
+      charconv
+      bitgdi
+      euser
+      estlib
+      ws32
+      hal
+      fbscli
+      efsrv
+      scdv
+      gdi
+  )
 endif()
 
 sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/SDL_dialog.c)
@@ -3111,8 +3205,8 @@ endif()
 
 # We always need to have threads and timers around
 if(NOT HAVE_SDL_THREADS)
-  # The emscripten platform has been carefully vetted to work without threads
-  if(EMSCRIPTEN)
+  # The Emscripten and N-Gage platform has been carefully vetted to work without threads
+  if(EMSCRIPTEN OR NGAGE)
     set(SDL_THREADS_DISABLED 1)
     sdl_glob_sources("${SDL3_SOURCE_DIR}/src/thread/generic/*.c")
   else()

+ 189 - 0
cmake/PreseedNokiaNGageCache.cmake

@@ -0,0 +1,189 @@
+if(NGAGESDK)
+  function(SDL_Preseed_CMakeCache)
+    set(COMPILER_SUPPORTS_ARMNEON                        ""    CACHE INTERNAL "Test COMPILER_SUPPORTS_ARMNEON")
+    set(COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS      ""    CACHE INTERNAL "Test COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS")
+    set(COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET         ""    CACHE INTERNAL "Test COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET")
+    set(HAVE_CLANG_COMMENT_BLOCK_COMMANDS                ""    CACHE INTERNAL "Test HAVE_CLANG_COMMENT_BLOCK_COMMANDS")
+    set(HAVE_ALLOCA_H                                    ""    CACHE INTERNAL "Have include alloca.h")
+    set(HAVE_LIBM                                        "1"   CACHE INTERNAL "Have library m")
+    set(HAVE_POSIX_SPAWN                                 ""    CACHE INTERNAL "Have symbol posix_spawn")
+    set(HAVE_MALLOC                                      "1"   CACHE INTERNAL "Have include malloc.h")
+    set(LIBC_HAS_ABS                                     "1"   CACHE INTERNAL "Have symbol abs")
+    set(LIBC_HAS_ACOS                                    "1"   CACHE INTERNAL "Have symbol acos")
+    set(LIBC_HAS_ACOSF                                   ""    CACHE INTERNAL "Have symbol acosf")
+    set(LIBC_HAS_ASIN                                    "1"   CACHE INTERNAL "Have symbol asin")
+    set(LIBC_HAS_ASINF                                   ""    CACHE INTERNAL "Have symbol asinf")
+    set(LIBC_HAS_ATAN                                    "1"   CACHE INTERNAL "Have symbol atan")
+    set(LIBC_HAS_ATAN2                                   "1"   CACHE INTERNAL "Have symbol atan2")
+    set(LIBC_HAS_ATAN2F                                  ""    CACHE INTERNAL "Have symbol atan2f")
+    set(LIBC_HAS_ATANF                                   ""    CACHE INTERNAL "Have symbol atanf")
+    set(LIBC_HAS_ATOF                                    ""    CACHE INTERNAL "Have symbol atof")
+    set(LIBC_HAS_ATOI                                    ""    CACHE INTERNAL "Have symbol atoi")
+    set(LIBC_HAS_BCOPY                                   "1"   CACHE INTERNAL "Have symbol bcopy")
+    set(LIBC_HAS_CALLOC                                  ""    CACHE INTERNAL "Have symbol calloc")
+    set(LIBC_HAS_CEIL                                    "1"   CACHE INTERNAL "Have symbol ceil")
+    set(LIBC_HAS_CEILF                                   ""    CACHE INTERNAL "Have symbol ceilf")
+    set(LIBC_HAS_COPYSIGN                                "1"   CACHE INTERNAL "Have symbol copysign")
+    set(LIBC_HAS_COPYSIGNF                               "1"   CACHE INTERNAL "Have symbol copysignf")
+    set(LIBC_HAS_COS                                     "1"   CACHE INTERNAL "Have symbol cos")
+    set(LIBC_HAS_COSF                                    ""    CACHE INTERNAL "Have symbol cosf")
+    set(LIBC_HAS_EXP                                     "1"   CACHE INTERNAL "Have symbol exp")
+    set(LIBC_HAS_EXPF                                    ""    CACHE INTERNAL "Have symbol expf")
+    set(LIBC_HAS_FABS                                    "1"   CACHE INTERNAL "Have symbol fabs")
+    set(LIBC_HAS_FABSF                                   "1"   CACHE INTERNAL "Have symbol fabsf")
+    set(LIBC_HAS_FLOAT_H                                 "1"   CACHE INTERNAL "Have include float.h")
+    set(LIBC_HAS_FLOOR                                   "1"   CACHE INTERNAL "Have symbol floor")
+    set(LIBC_HAS_FLOORF                                  ""    CACHE INTERNAL "Have symbol floorf")
+    set(LIBC_HAS_FMOD                                    ""    CACHE INTERNAL "Have symbol fmod")
+    set(LIBC_HAS_FMODF                                   ""    CACHE INTERNAL "Have symbol fmodf")
+    set(LIBC_HAS_FOPEN64                                 ""    CACHE INTERNAL "Have symbol fopen64")
+    set(LIBC_HAS_FREE                                    "1"   CACHE INTERNAL "Have symbol free")
+    set(LIBC_HAS_FSEEKO                                  ""    CACHE INTERNAL "Have symbol fseeko")
+    set(LIBC_HAS_FSEEKO64                                ""    CACHE INTERNAL "Have symbol fseeko64")
+    set(LIBC_HAS_GETENV                                  ""    CACHE INTERNAL "Have symbol getenv")
+    set(LIBC_HAS_ICONV_H                                 ""    CACHE INTERNAL "Have include iconv.h")
+    set(LIBC_HAS_INDEX                                   "1"   CACHE INTERNAL "Have symbol index")
+    set(LIBC_HAS_INTTYPES_H                              "1"   CACHE INTERNAL "Have include inttypes.h")
+    set(LIBC_HAS_ISINF                                   "1"   CACHE INTERNAL "Have include isinf(double)")
+    set(LIBC_ISINF_HANDLES_FLOAT                         "1"   CACHE INTERNAL "Have include isinf(float)")
+    set(LIBC_HAS_ISINFF                                  "1"   CACHE INTERNAL "Have include isinff(float)")
+    set(LIBC_HAS_ISNAN                                   "1"   CACHE INTERNAL "Have include isnan(double)")
+    set(LIBC_ISNAN_HANDLES_FLOAT                         "1"   CACHE INTERNAL "Have include isnan(float)")
+    set(LIBC_HAS_ISNANF                                  "1"   CACHE INTERNAL "Have include isnanf(float)")
+    set(LIBC_HAS_ITOA                                    ""    CACHE INTERNAL "Have symbol itoa")
+    set(LIBC_HAS_LIMITS_H                                "1"   CACHE INTERNAL "Have include limits.h")
+    set(LIBC_HAS_LOG                                     "1"   CACHE INTERNAL "Have symbol log")
+    set(LIBC_HAS_LOG10                                   ""    CACHE INTERNAL "Have symbol log10")
+    set(LIBC_HAS_LOG10F                                  ""    CACHE INTERNAL "Have symbol log10f")
+    set(LIBC_HAS_LOGF                                    ""    CACHE INTERNAL "Have symbol logf")
+    set(LIBC_HAS_LROUND                                  ""    CACHE INTERNAL "Have symbol lround")
+    set(LIBC_HAS_LROUNDF                                 ""    CACHE INTERNAL "Have symbol lroundf")
+    set(LIBC_HAS_MALLOC                                  "1"   CACHE INTERNAL "Have symbol malloc")
+    set(LIBC_HAS_MALLOC_H                                ""    CACHE INTERNAL "Have include malloc.h")
+    set(LIBC_HAS_MATH_H                                  "1"   CACHE INTERNAL "Have include math.h")
+    set(LIBC_HAS_MEMCMP                                  "1"   CACHE INTERNAL "Have symbol memcmp")
+    set(LIBC_HAS_MEMCPY                                  ""    CACHE INTERNAL "Have symbol memcpy")
+    set(LIBC_HAS_MEMMOVE                                 ""    CACHE INTERNAL "Have symbol memmove")
+    set(LIBC_HAS_MEMORY_H                                ""    CACHE INTERNAL "Have include memory.h")
+    set(LIBC_HAS_MEMSET                                  ""    CACHE INTERNAL "Have symbol memset")
+    set(LIBC_HAS_MODF                                    "1"   CACHE INTERNAL "Have symbol modf")
+    set(LIBC_HAS_MODFF                                   ""    CACHE INTERNAL "Have symbol modff")
+    set(LIBC_HAS_POW                                     "1"   CACHE INTERNAL "Have symbol pow")
+    set(LIBC_HAS_POWF                                    ""    CACHE INTERNAL "Have symbol powf")
+    set(LIBC_HAS_PUTENV                                  ""    CACHE INTERNAL "Have symbol putenv")
+    set(LIBC_HAS_REALLOC                                 ""    CACHE INTERNAL "Have symbol realloc")
+    set(LIBC_HAS_RINDEX                                  "1"   CACHE INTERNAL "Have symbol rindex")
+    set(LIBC_HAS_ROUND                                   ""    CACHE INTERNAL "Have symbol round")
+    set(LIBC_HAS_ROUNDF                                  ""    CACHE INTERNAL "Have symbol roundf")
+    set(LIBC_HAS_SCALBN                                  "1"   CACHE INTERNAL "Have symbol scalbn")
+    set(LIBC_HAS_SCALBNF                                 ""    CACHE INTERNAL "Have symbol scalbnf")
+    set(LIBC_HAS_SETENV                                  ""    CACHE INTERNAL "Have symbol setenv")
+    set(LIBC_HAS_SIGNAL_H                                ""    CACHE INTERNAL "Have include signal.h")
+    set(LIBC_HAS_SIN                                     "1"   CACHE INTERNAL "Have symbol sin")
+    set(LIBC_HAS_SINF                                    ""    CACHE INTERNAL "Have symbol sinf")
+    set(LIBC_HAS_SQR                                     ""    CACHE INTERNAL "Have symbol sqr")
+    set(LIBC_HAS_SQRT                                    "1"   CACHE INTERNAL "Have symbol sqrt")
+    set(LIBC_HAS_SQRTF                                   ""    CACHE INTERNAL "Have symbol sqrtf")
+    set(LIBC_HAS_SSCANF                                  "1"   CACHE INTERNAL "Have symbol sscanf")
+    set(LIBC_HAS_STDARG_H                                "1"   CACHE INTERNAL "Have include stdarg.h")
+    set(LIBC_HAS_STDBOOL_H                               "1"   CACHE INTERNAL "Have include stdbool.h")
+    set(LIBC_HAS_STDDEF_H                                "1"   CACHE INTERNAL "Have include stddef.h")
+    set(LIBC_HAS_STDINT_H                                "1"   CACHE INTERNAL "Have include stdint.h")
+    set(LIBC_HAS_STDIO_H                                 "1"   CACHE INTERNAL "Have include stdio.h")
+    set(LIBC_HAS_STDLIB_H                                "1"   CACHE INTERNAL "Have include stdlib.h")
+    set(LIBC_HAS_STRCASESTR                              ""    CACHE INTERNAL "Have symbol strcasestr")
+    set(LIBC_HAS_STRCHR                                  "1"   CACHE INTERNAL "Have symbol strchr")
+    set(LIBC_HAS_STRCMP                                  "1"   CACHE INTERNAL "Have symbol strcmp")
+    set(LIBC_HAS_STRINGS_H                               ""    CACHE INTERNAL "Have include strings.h")
+    set(LIBC_HAS_STRING_H                                "1"   CACHE INTERNAL "Have include string.h")
+    set(LIBC_HAS_STRLCAT                                 ""    CACHE INTERNAL "Have symbol strlcat")
+    set(LIBC_HAS_STRLCPY                                 ""    CACHE INTERNAL "Have symbol strlcpy")
+    set(LIBC_HAS_STRLEN                                  "1"   CACHE INTERNAL "Have symbol strlen")
+    set(LIBC_HAS_STRNCMP                                 "1"   CACHE INTERNAL "Have symbol strncmp")
+    set(LIBC_HAS_STRNLEN                                 ""    CACHE INTERNAL "Have symbol strnlen")
+    set(LIBC_HAS_STRNSTR                                 ""    CACHE INTERNAL "Have symbol strnstr")
+    set(LIBC_HAS_STRPBRK                                 "1"   CACHE INTERNAL "Have symbol strpbrk")
+    set(LIBC_HAS_STRRCHR                                 "1"   CACHE INTERNAL "Have symbol strrchr")
+    set(LIBC_HAS_STRSTR                                  "1"   CACHE INTERNAL "Have symbol strstr")
+    set(LIBC_HAS_STRTOD                                  ""    CACHE INTERNAL "Have symbol strtod")
+    set(LIBC_HAS_STRTOK_R                                ""    CACHE INTERNAL "Have symbol strtok_r")
+    set(LIBC_HAS_STRTOL                                  ""    CACHE INTERNAL "Have symbol strtol")
+    set(LIBC_HAS_STRTOLL                                 ""    CACHE INTERNAL "Have symbol strtoll")
+    set(LIBC_HAS_STRTOUL                                 ""    CACHE INTERNAL "Have symbol strtoul")
+    set(LIBC_HAS_STRTOULL                                ""    CACHE INTERNAL "Have symbol strtoull")
+    set(LIBC_HAS_SYS_TYPES_H                             "1"   CACHE INTERNAL "Have include sys/types.h")
+    set(LIBC_HAS_TAN                                     "1"   CACHE INTERNAL "Have symbol tan")
+    set(LIBC_HAS_TANF                                    ""    CACHE INTERNAL "Have symbol tanf")
+    set(LIBC_HAS_TIME_H                                  "1"   CACHE INTERNAL "Have include time.h")
+    set(LIBC_HAS_TRUNC                                   ""    CACHE INTERNAL "Have symbol trunc")
+    set(LIBC_HAS_TRUNCF                                  ""    CACHE INTERNAL "Have symbol truncf")
+    set(LIBC_HAS_UNSETENV                                ""    CACHE INTERNAL "Have symbol unsetenv")
+    set(LIBC_HAS_VSNPRINTF                               ""    CACHE INTERNAL "Have symbol vsnprintf")
+    set(LIBC_HAS_VSSCANF                                 ""    CACHE INTERNAL "Have symbol vsscanf")
+    set(LIBC_HAS_WCHAR_H                                 "1"   CACHE INTERNAL "Have include wchar.h")
+    set(LIBC_HAS_WCSCMP                                  ""    CACHE INTERNAL "Have symbol wcscmp")
+    set(LIBC_HAS_WCSDUP                                  ""    CACHE INTERNAL "Have symbol wcsdup")
+    set(LIBC_HAS_WCSLCAT                                 ""    CACHE INTERNAL "Have symbol wcslcat")
+    set(LIBC_HAS_WCSLCPY                                 ""    CACHE INTERNAL "Have symbol wcslcpy")
+    set(LIBC_HAS_WCSLEN                                  ""    CACHE INTERNAL "Have symbol wcslen")
+    set(LIBC_HAS_WCSNCMP                                 ""    CACHE INTERNAL "Have symbol wcsncmp")
+    set(LIBC_HAS_WCSNLEN                                 ""    CACHE INTERNAL "Have symbol wcsnlen")
+    set(LIBC_HAS_WCSSTR                                  ""    CACHE INTERNAL "Have symbol wcsstr")
+    set(LIBC_HAS_WCSTOL                                  ""    CACHE INTERNAL "Have symbol wcstol")
+    set(LIBC_HAS__EXIT                                   ""    CACHE INTERNAL "Have symbol _Exit")
+    set(LIBC_HAS__I64TOA                                 ""    CACHE INTERNAL "Have symbol _i64toa")
+    set(LIBC_HAS__LTOA                                   ""    CACHE INTERNAL "Have symbol _ltoa")
+    set(LIBC_HAS__STRREV                                 ""    CACHE INTERNAL "Have symbol _strrev")
+    set(LIBC_HAS__UI64TOA                                ""    CACHE INTERNAL "Have symbol _ui64toa")
+    set(LIBC_HAS__UITOA                                  ""    CACHE INTERNAL "Have symbol _uitoa")
+    set(LIBC_HAS__ULTOA                                  ""    CACHE INTERNAL "Have symbol _ultoa")
+    set(LIBC_HAS__WCSDUP                                 ""    CACHE INTERNAL "Have symbol _wcsdup")
+    set(LIBC_IS_GLIBC                                    ""    CACHE INTERNAL "Have symbol __GLIBC__")
+    set(_ALLOCA_IN_MALLOC_H                              ""    CACHE INTERNAL "Have symbol _alloca")
+    set(HAVE_GCC_WALL                                    "1"   CACHE INTERNAL "Test HAVE_GCC_WALL")
+    set(HAVE_GCC_WUNDEF                                  "1"   CACHE INTERNAL "Test HAVE_GCC_WUNDEF")
+    set(HAVE_GCC_WFLOAT_CONVERSION                       ""    CACHE INTERNAL "Test HAVE_GCC_WFLOAT_CONVERSION")
+    set(HAVE_GCC_NO_STRICT_ALIASING                      "1"   CACHE INTERNAL "Test HAVE_GCC_NO_STRICT_ALIASING")
+    set(HAVE_GCC_WDOCUMENTATION                          ""    CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION")
+    set(HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND          ""    CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND")
+    set(HAVE_GCC_COMMENT_BLOCK_COMMANDS                  ""    CACHE INTERNAL "Test HAVE_GCC_COMMENT_BLOCK_COMMANDS")
+    set(HAVE_GCC_WSHADOW                                 "1"   CACHE INTERNAL "Test HAVE_GCC_WSHADOW")
+    set(HAVE_GCC_WUNUSED_LOCAL_TYPEDEFS                  ""    CACHE INTERNAL "Test HAVE_GCC_WUNUSED_LOCAL_TYPEDEFS")
+    set(HAVE_GCC_WIMPLICIT_FALLTHROUGH                   ""    CACHE INTERNAL "Test HAVE_GCC_WIMPLICIT_FALLTHROUGH")
+    set(HAVE_GCC_FVISIBILITY                             ""    CACHE INTERNAL "Test HAVE_GCC_FVISIBILITY")
+    set(HAVE_ST_MTIM                                     ""    CACHE INTERNAL "Test HAVE_ST_MTIM")
+    #set(HAVE_O_CLOEXEC                                   ""    CACHE INTERNAL "Test HAVE_O_CLOEXEC")
+    #set(COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR             ""    CACHE INTERNAL "Test COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR")
+    set(COMPILER_SUPPORTS_GCC_ATOMICS                    ""    CACHE INTERNAL "Test COMPILER_SUPPORTS_GCC_ATOMICS")
+    set(LINKER_SUPPORTS_VERSION_SCRIPT                   ""    CACHE INTERNAL "Test LINKER_SUPPORTS_VERSION_SCRIPT")
+    set(LINKER_SUPPORTS_WL_NO_UNDEFINED                  ""    CACHE INTERNAL "Test LINKER_SUPPORTS_WL_NO_UNDEFINED")
+    set(ICONV_IN_LIBC                                    ""    CACHE INTERNAL "Test ICONV_IN_LIBC")
+    set(ICONV_IN_LIBICONV                                ""    CACHE INTERNAL "Test ICONV_IN_LIBICONV")
+    #set(LIBC_HAS_WORKING_LIBUNWIND                       ""    CACHE INTERNAL "Test LIBC_HAS_WORKING_LIBUNWIND")
+    #set(LIBUNWIND_HAS_WORKINGLIBUNWIND                   ""    CACHE INTERNAL "Test LIBUNWIND_HAS_WORKINGLIBUNWIND")
+    set(HAVE_GETPAGESIZE                                 ""    CACHE INTERNAL "Have symbol getpagesize")
+    set(HAVE_SIGACTION                                   ""    CACHE INTERNAL "Have symbol sigaction")
+    set(HAVE_SA_SIGACTION                                ""    CACHE INTERNAL "Have symbol sa_sigaction")
+    set(HAVE_SETJMP                                      ""    CACHE INTERNAL "Have symbol setjmp")
+    set(HAVE_NANOSLEEP                                   ""    CACHE INTERNAL "Have symbol nanosleep")
+    set(HAVE_GMTIME_R                                    ""    CACHE INTERNAL "Have symbol gmtime_r")
+    set(HAVE_LOCALTIME_R                                 ""    CACHE INTERNAL "Have symbol localtime_r")
+    set(HAVE_NL_LANGINFO                                 ""    CACHE INTERNAL "Have symbol nl_langinfo")
+    set(HAVE_SYSCONF                                     ""    CACHE INTERNAL "Have symbol sysconf")
+    set(HAVE_SYSCTLBYNAME                                ""    CACHE INTERNAL "Have symbol sysctlbyname")
+    set(HAVE_GETAUXVAL                                   ""    CACHE INTERNAL "Have symbol getauxval")
+    set(HAVE_ELF_AUX_INFO                                ""    CACHE INTERNAL "Have symbol elf_aux_info")
+    set(HAVE_POLL                                        ""    CACHE INTERNAL "Have symbol poll")
+    set(HAVE_MEMFD_CREATE                                ""    CACHE INTERNAL "Have symbol memfd_create")
+    set(HAVE_POSIX_FALLOCATE                             ""    CACHE INTERNAL "Have symbol posix_fallocate")
+    set(HAVE_DLOPEN_IN_LIBC                              ""    CACHE INTERNAL "Have symbol dlopen")
+
+    set(HAVE_GETHOSTNAME                                 ""    CACHE INTERNAL "Have symbol gethostname")
+    set(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR           ""    CACHE INTERNAL "Have symbol addchdir")
+    set(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP        ""    CACHE INTERNAL "Have symbol addchdir_np")
+    set(HAVE_FDATASYNC                                   ""    CACHE INTERNAL "Have symbol fdatasync")
+
+    set(HAVE_SDL_FSOPS                                   "1"   CACHE INTERNAL "Enable SDL_FSOPS")
+    set(HAVE_SDL_LOCALE                                  "1"   CACHE INTERNAL "Enable SDL_LOCALE")
+  endfunction()
+endif()

+ 2 - 0
cmake/sdlplatform.cmake

@@ -22,6 +22,8 @@ function(SDL_DetectCMakePlatform)
     set(sdl_cmake_platform Haiku)
   elseif(NINTENDO_3DS)
     set(sdl_cmake_platform n3ds)
+  elseif(NGAGESDK)
+    set(sdl_cmake_platform ngage)
   elseif(PS2)
     set(sdl_cmake_platform ps2)
   elseif(VITA)

+ 8 - 6
cmake/test/CMakeLists.txt

@@ -96,12 +96,14 @@ if(TEST_STATIC)
     add_executable(gui-static WIN32 main_gui.c)
     target_link_libraries(gui-static PRIVATE SDL3::SDL3-static)
 
-    # Assume SDL library has been built with `set(CMAKE_POSITION_INDEPENDENT_CODE ON)`
-    add_library(sharedlib-static SHARED main_lib.c)
-    target_link_libraries(sharedlib-static PRIVATE SDL3::SDL3-static)
-    generate_export_header(sharedlib-static EXPORT_MACRO_NAME MYLIBRARY_EXPORT)
-    target_compile_definitions(sharedlib-static PRIVATE "EXPORT_HEADER=\"${CMAKE_CURRENT_BINARY_DIR}/sharedlib-static_export.h\"")
-    set_target_properties(sharedlib-static PROPERTIES C_VISIBILITY_PRESET "hidden")
+    if(TEST_SHARED)
+        # Assume SDL library has been built with `set(CMAKE_POSITION_INDEPENDENT_CODE ON)`
+        add_library(sharedlib-static SHARED main_lib.c)
+        target_link_libraries(sharedlib-static PRIVATE SDL3::SDL3-static)
+        generate_export_header(sharedlib-static EXPORT_MACRO_NAME MYLIBRARY_EXPORT)
+        target_compile_definitions(sharedlib-static PRIVATE "EXPORT_HEADER=\"${CMAKE_CURRENT_BINARY_DIR}/sharedlib-static_export.h\"")
+        set_target_properties(sharedlib-static PROPERTIES C_VISIBILITY_PRESET "hidden")
+    endif()
 
     if(TEST_TEST)
         add_executable(sdltest-static sdltest.c)

+ 24 - 11
cmake/test/main_gui.c

@@ -1,24 +1,37 @@
-#include <SDL3/SDL.h>
+#define SDL_MAIN_USE_CALLBACKS
 #include <SDL3/SDL_main.h>
+#include <SDL3/SDL.h>
+
+static SDL_Window *window;
 
-int main(int argc, char *argv[])
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
+{
+    return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppIterate(void *appstate)
 {
-    SDL_Window *window = NULL;
     SDL_Surface *screenSurface = NULL;
+    screenSurface = SDL_GetWindowSurface(window);
+    SDL_FillSurfaceRect(screenSurface, NULL, SDL_MapSurfaceRGB(screenSurface, 0xff, 0xff, 0xff));
+    SDL_UpdateWindowSurface(window);
+    return SDL_APP_CONTINUE;
+}
+
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
+{
     if (!SDL_Init(SDL_INIT_VIDEO)) {
         SDL_Log("Could not initialize SDL: %s", SDL_GetError());
-        return 1;
+        return SDL_APP_FAILURE;
     }
     window = SDL_CreateWindow("Hello SDL", 640, 480, 0);
     if (!window) {
         SDL_Log("could not create window: %s", SDL_GetError());
-        return 1;
+        return SDL_APP_FAILURE;
     }
-    screenSurface = SDL_GetWindowSurface(window);
-    SDL_FillSurfaceRect(screenSurface, NULL, SDL_MapSurfaceRGB(screenSurface, 0xff, 0xff, 0xff));
-    SDL_UpdateWindowSurface(window);
-    SDL_Delay(100);
+    return SDL_APP_CONTINUE;
+}
+
+void SDL_AppQuit(void *appstate, SDL_AppResult result) {
     SDL_DestroyWindow(window);
-    SDL_Quit();
-    return 0;
 }

+ 17 - 2
cmake/test/sdltest.c

@@ -1,9 +1,24 @@
+#define SDL_MAIN_USE_CALLBACKS
 #include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
 #include <SDL3/SDL_test.h>
 
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
+{
+    return SDL_APP_SUCCESS;
+}
+
+SDL_AppResult SDL_AppIterate(void *appstate)
+{
+    return SDL_APP_SUCCESS;
+}
 
-int main(int argc, char *argv[]) {
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
+{
     SDLTest_CommonState state;
     SDLTest_CommonDefaultArgs(&state, argc, argv);
-    return 0;
+    return SDL_APP_SUCCESS;
+}
+
+void SDL_AppQuit(void *appstate, SDL_AppResult result) {
 }

+ 62 - 3
docs/README-ngage.md

@@ -1,5 +1,64 @@
-Support for the Nokia N-Gage has been removed from SDL3 (but will make a
-comeback when newer compilers are available for the platform).
+# Nokia N-Gage
 
-SDL2 still supports this platform.
+SDL port for the Nokia N-Gage
+[Homebrew toolchain](https://github.com/ngagesdk/ngage-toolchain)
+contributed by:
 
+- [Michael Fitzmayer](https://github.com/mupfdev)
+
+- [Anonymous Maarten](https://github.com/madebr)
+
+Many thanks to:
+
+- icculus and slouken for always making room for us — even when we show up in 2025
+ still waving the N-Gage flag.
+
+- The Nokia N-Gage [Discord community](https://discord.gg/dbUzqJ26vs)
+ who keeps the platform alive.
+
+- The staff and supporters of the
+ [Suomen pelimuseo](https://www.vapriikki.fi/nayttelyt/fantastinen-floppi/), and
+ to Heikki Jungmann, for their ongoing love and dedication for the Nokia N-Gage — you
+ guys are awesome!
+ 
+## History
+
+When SDL support was discontinued due to the lack of C99 support at the time,
+this version was rebuilt from the ground up after resolving the compiler issues.
+
+In contrast to the earlier SDL2 port, this version features a dedicated rendering
+backend and a functional, albeit limited, audio interface.  Support for the
+software renderer has been removed.
+
+The outcome is a significantly leaner and more efficient SDL port, which we hope
+will breathe new life into this beloved yet obscure platform.
+
+## To the Stubborn Legends of the DC Scene  
+
+This port is lovingly dedicated to the ever-nostalgic Dreamcast homebrew scene —
+because if we managed to pull this off for the N-Gage (yes, the N-Gage), surely
+you guys can stop clinging to SDL2 like it's a rare Shenmue prototype and finally
+make the leap to SDL3.  It’s 2025, not 1999 — and let’s be honest, you’re rocking
+a state-of-the-art C23 compiler.  The irony writes itself.
+
+## Existing Issues and Limitations
+
+- For now, the new
+ [SDL3 main callbacks](https://wiki.libsdl.org/SDL3/README/main-functions#how-to-use-main-callbacks-in-sdl3)
+ are not optional and must be used. This is important as the callbacks
+ are optional on other platforms.
+
+- If the application is put in the background while sound is playing,
+ some of the audio is looped until the app is back in focus.
+
+- It is recommended initialising SDLs audio sub-system even when it
+ is not required. The backend is started at a higher level.  Initialising
+ SDLs audio sub-system ensures that the backend is properly deinitialised.
+
+- Because the audio sample rate can change during phone calls, the sample
+ rate is currently fixed at 8kHz to ensure stable behavior.  Although
+ dynamically adjusting the sample rate is theoretically possible, the
+ current implementation doesn't support it yet.  This limitation is
+ expected to be resolved in a future update.
+
+- Dependency tracking is currently non-functional.

+ 4 - 0
examples/CMakeLists.txt

@@ -111,6 +111,10 @@ macro(add_sdl_example_executable TARGET)
     elseif(EMSCRIPTEN)
         set_property(TARGET ${TARGET} PROPERTY SUFFIX ".html")
         target_link_options(${TARGET} PRIVATE -sALLOW_MEMORY_GROWTH=1)
+    elseif(NGAGE)
+        string(MD5 TARGET_MD5 "${TARGET}")
+        string(SUBSTRING "${TARGET_MD5}" 0 8 TARGET_MD5_8)
+        target_link_options(${TARGET} PRIVATE "SHELL:-s UID3=0x${TARGET_MD5_8}")
     endif()
 
     if(OPENGL_FOUND)

+ 1 - 1
include/SDL3/SDL_assert.h

@@ -132,7 +132,7 @@ extern "C" {
     #define SDL_TriggerBreakpoint() __debugbreak()
 #elif defined(_MSC_VER) && defined(_M_IX86)
     #define SDL_TriggerBreakpoint() { _asm { int 0x03 }  }
-#elif defined(ANDROID)
+#elif defined(ANDROID) || defined(__SYMBIAN32__)
     #include <assert.h>
     #define SDL_TriggerBreakpoint() assert(0)
 #elif SDL_HAS_BUILTIN(__builtin_debugtrap)

+ 1 - 1
include/SDL3/SDL_begin_code.h

@@ -389,7 +389,7 @@
 #endif /* SDL_FORCE_INLINE not defined */
 
 #ifndef SDL_NORETURN
-#ifdef __GNUC__
+#if defined(__GNUC__)
 #define SDL_NORETURN __attribute__((noreturn))
 #elif defined(_MSC_VER)
 #define SDL_NORETURN __declspec(noreturn)

+ 11 - 1
include/SDL3/SDL_platform_defines.h

@@ -317,7 +317,7 @@
 #define SDL_PLATFORM_CYGWIN 1
 #endif
 
-#if defined(_WIN32) || defined(SDL_PLATFORM_CYGWIN)
+#if (defined(_WIN32) || defined(SDL_PLATFORM_CYGWIN)) && !defined(__NGAGE__)
 
 /**
  * A preprocessor macro that is only defined if compiling for Windows.
@@ -473,4 +473,14 @@
 #define SDL_PLATFORM_3DS 1
 #endif
 
+#ifdef __NGAGE__
+
+/**
+ * A preprocessor macro that is only defined if compiling for the Nokia N-Gage.
+ *
+ * \since This macro is available since SDL 3.4.0.
+ */
+#define SDL_PLATFORM_NGAGE 1
+#endif
+
 #endif /* SDL_platform_defines_h_ */

+ 4 - 0
include/build_config/SDL_build_config.h.cmake

@@ -277,6 +277,7 @@
 #cmakedefine SDL_AUDIO_DRIVER_PSP 1
 #cmakedefine SDL_AUDIO_DRIVER_PS2 1
 #cmakedefine SDL_AUDIO_DRIVER_N3DS 1
+#cmakedefine SDL_AUDIO_DRIVER_NGAGE 1
 #cmakedefine SDL_AUDIO_DRIVER_QNX 1
 
 #cmakedefine SDL_AUDIO_DRIVER_PRIVATE 1
@@ -365,6 +366,7 @@
 #cmakedefine SDL_TIME_PSP 1
 #cmakedefine SDL_TIME_PS2 1
 #cmakedefine SDL_TIME_N3DS 1
+#cmakedefine SDL_TIME_NGAGE 1
 
 /* Enable various timer systems */
 #cmakedefine SDL_TIMER_HAIKU 1
@@ -387,6 +389,7 @@
 #cmakedefine SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC @SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC@
 #cmakedefine SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC_GBM @SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC_GBM@
 #cmakedefine SDL_VIDEO_DRIVER_N3DS 1
+#cmakedefine SDL_VIDEO_DRIVER_NGAGE 1
 #cmakedefine SDL_VIDEO_DRIVER_OFFSCREEN 1
 #cmakedefine SDL_VIDEO_DRIVER_PS2 1
 #cmakedefine SDL_VIDEO_DRIVER_PSP 1
@@ -438,6 +441,7 @@
 #cmakedefine SDL_VIDEO_RENDER_VULKAN 1
 #cmakedefine SDL_VIDEO_RENDER_OGL 1
 #cmakedefine SDL_VIDEO_RENDER_OGL_ES2 1
+#cmakedefine SDL_VIDEO_RENDER_NGAGE 1
 #cmakedefine SDL_VIDEO_RENDER_PS2 1
 #cmakedefine SDL_VIDEO_RENDER_PSP 1
 #cmakedefine SDL_VIDEO_RENDER_VITA_GXM 1

+ 2 - 0
src/SDL.c

@@ -728,6 +728,8 @@ const char *SDL_GetPlatform(void)
     return "macOS";
 #elif defined(SDL_PLATFORM_NETBSD)
     return "NetBSD";
+#elif defined(SDL_PLATFORM_NGAGE)
+    return "Nokia N-Gage";
 #elif defined(SDL_PLATFORM_OPENBSD)
     return "OpenBSD";
 #elif defined(SDL_PLATFORM_OS2)

+ 2 - 0
src/SDL_error.c

@@ -20,6 +20,8 @@
 */
 #include "SDL_internal.h"
 
+#include "stdlib/SDL_vacopy.h"
+
 // Simple error handling in SDL
 
 #include "SDL_error_c.h"

+ 24 - 0
src/SDL_log.c

@@ -587,6 +587,25 @@ void SDL_LogMessageV(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_S
         return;
     }
 
+#if defined(SDL_PLATFORM_NGAGE)
+    extern void NGAGE_vnprintf(char *buf, size_t size, const char *fmt, va_list ap);
+    char buf[1024];
+    NGAGE_vnprintf(buf, sizeof(buf), fmt, ap);
+
+#ifdef ENABLE_FILE_LOG
+    FILE* file;
+    file = fopen("E:/SDL_Log.txt", "a");
+    if (file)
+    {
+        vfprintf(file, fmt, ap);
+        fprintf(file, "\n");
+        (void)fclose(file);
+    }
+#endif
+
+    return;
+#endif
+
     // Render into stack buffer
     va_copy(aq, ap);
     len = SDL_vsnprintf(stack_buf, sizeof(stack_buf), fmt, aq);
@@ -767,9 +786,14 @@ static void SDLCALL SDL_LogOutput(void *userdata, int category, SDL_LogPriority
             (void)fclose(pFile);
         }
     }
+#elif defined(SDL_PLATFORM_NGAGE)
+    {
+        /* Nothing to do here. */
+    }
 #endif
 #if defined(HAVE_STDIO_H) && \
     !(defined(SDL_PLATFORM_APPLE) && (defined(SDL_VIDEO_DRIVER_COCOA) || defined(SDL_VIDEO_DRIVER_UIKIT))) && \
+    !(defined(SDL_PLATFORM_NGAGE)) && \
     !(defined(SDL_PLATFORM_WIN32))
     (void)fprintf(stderr, "%s%s\n", GetLogPriorityPrefix(priority), message);
 #endif

+ 3 - 0
src/audio/SDL_audio.c

@@ -77,6 +77,9 @@ static const AudioBootStrap *const bootstrap[] = {
 #ifdef SDL_AUDIO_DRIVER_N3DS
     &N3DSAUDIO_bootstrap,
 #endif
+#ifdef SDL_AUDIO_DRIVER_NGAGE
+    &NGAGEAUDIO_bootstrap,
+#endif
 #ifdef SDL_AUDIO_DRIVER_EMSCRIPTEN
     &EMSCRIPTENAUDIO_bootstrap,
 #endif

+ 1 - 0
src/audio/SDL_sysaudio.h

@@ -393,6 +393,7 @@ extern AudioBootStrap PS2AUDIO_bootstrap;
 extern AudioBootStrap PSPAUDIO_bootstrap;
 extern AudioBootStrap VITAAUD_bootstrap;
 extern AudioBootStrap N3DSAUDIO_bootstrap;
+extern AudioBootStrap NGAGEAUDIO_bootstrap;
 extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap;
 extern AudioBootStrap QSAAUDIO_bootstrap;
 

+ 103 - 0
src/audio/ngage/SDL_ngageaudio.c

@@ -0,0 +1,103 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_NGAGE
+
+#include "../SDL_sysaudio.h"
+#include "SDL_ngageaudio.h"
+
+static SDL_AudioDevice *devptr = NULL;
+
+SDL_AudioDevice *NGAGE_GetAudioDeviceAddr()
+{
+    return devptr;
+}
+
+static bool NGAGEAUDIO_OpenDevice(SDL_AudioDevice *device)
+{
+    SDL_PrivateAudioData *phdata = SDL_calloc(1, sizeof(SDL_PrivateAudioData));
+    if (!phdata) {
+        SDL_OutOfMemory();
+        return false;
+    }
+    device->hidden = phdata;
+
+    phdata->buffer = SDL_calloc(1, device->buffer_size);
+    if (!phdata->buffer) {
+        SDL_OutOfMemory();
+        SDL_free(phdata);
+        return false;
+    }
+    devptr = device;
+
+    // Since the phone can change the sample rate during a phone call,
+    // we set the sample rate to 8KHz to be safe.  Even though it
+    // might be possible to adjust the sample rate dynamically, it's
+    // not supported by the current implementation.
+
+    device->spec.format = SDL_AUDIO_S16LE;
+    device->spec.channels = 1;
+    device->spec.freq = 8000;
+
+    SDL_UpdatedAudioDeviceFormat(device);
+
+    return true;
+}
+
+static Uint8 *NGAGEAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
+{
+    SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
+    if (!phdata) {
+        *buffer_size = 0;
+        return 0;
+    }
+
+    *buffer_size = device->buffer_size;
+    return phdata->buffer;
+}
+
+static void NGAGEAUDIO_CloseDevice(SDL_AudioDevice *device)
+{
+    if (device->hidden) {
+        SDL_free(device->hidden->buffer);
+        SDL_free(device->hidden);
+    }
+
+    return;
+}
+
+static bool NGAGEAUDIO_Init(SDL_AudioDriverImpl *impl)
+{
+    impl->OpenDevice = NGAGEAUDIO_OpenDevice;
+    impl->GetDeviceBuf = NGAGEAUDIO_GetDeviceBuf;
+    impl->CloseDevice = NGAGEAUDIO_CloseDevice;
+
+    impl->ProvidesOwnCallbackThread = true;
+    impl->OnlyHasDefaultPlaybackDevice = true;
+
+    return true;
+}
+
+AudioBootStrap NGAGEAUDIO_bootstrap = { "N-Gage", "N-Gage audio driver", NGAGEAUDIO_Init, false };
+
+#endif // SDL_AUDIO_DRIVER_NGAGE

+ 368 - 0
src/audio/ngage/SDL_ngageaudio.cpp

@@ -0,0 +1,368 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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.
+*/
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "SDL_ngageaudio.h"
+#include "../SDL_sysaudio.h"
+#include "SDL_internal.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef SDL_AUDIO_DRIVER_NGAGE
+
+#include "SDL_ngageaudio.hpp"
+
+CAudio::CAudio() : CActive(EPriorityStandard), iBufDes(NULL, 0) {}
+
+CAudio *CAudio::NewL(TInt aLatency)
+{
+    CAudio *self = new (ELeave) CAudio();
+    CleanupStack::PushL(self);
+    self->ConstructL(aLatency);
+    CleanupStack::Pop(self);
+    return self;
+}
+
+void CAudio::ConstructL(TInt aLatency)
+{
+    CActiveScheduler::Add(this);
+    User::LeaveIfError(iTimer.CreateLocal());
+    iTimerCreated = ETrue;
+
+    iStream = CMdaAudioOutputStream::NewL(*this);
+    if (!iStream) {
+        SDL_Log("Error: Failed to create audio stream");
+        User::Leave(KErrNoMemory);
+    }
+
+    iLatency = aLatency;
+    iLatencySamples = aLatency * 8; // 8kHz.
+
+    // Determine minimum and maximum number of samples to write with one
+    // WriteL request.
+    iMinWrite = iLatencySamples / 8;
+    iMaxWrite = iLatencySamples / 2;
+
+    // Set defaults.
+    iState = EStateNone;
+    iTimerCreated = EFalse;
+    iTimerActive = EFalse;
+}
+
+CAudio::~CAudio()
+{
+    if (iStream) {
+        iStream->Stop();
+
+        while (iState != EStateDone) {
+            User::After(100000); // 100ms.
+        }
+
+        delete iStream;
+    }
+}
+
+void CAudio::Start()
+{
+    if (iStream) {
+        // Set to 8kHz mono audio.
+        iStreamSettings.iChannels = TMdaAudioDataSettings::EChannelsMono;
+        iStreamSettings.iSampleRate = TMdaAudioDataSettings::ESampleRate8000Hz;
+        iStream->Open(&iStreamSettings);
+        iState = EStateOpening;
+    } else {
+        SDL_Log("Error: Failed to open audio stream");
+    }
+}
+
+// Feeds more processed data to the audio stream.
+void CAudio::Feed()
+{
+    // If a WriteL is already in progress, or we aren't even playing;
+    // do nothing!
+    if ((iState != EStateWriting) && (iState != EStatePlaying)) {
+        return;
+    }
+
+    // Figure out the number of samples that really have been played
+    // through the output.
+    TTimeIntervalMicroSeconds pos = iStream->Position();
+
+    TInt played = 8 * (pos.Int64() / TInt64(1000)).GetTInt(); // 8kHz.
+
+    played += iBaseSamplesPlayed;
+
+    // Determine the difference between the number of samples written to
+    // CMdaAudioOutputStream and the number of samples it has played.
+    // The difference is the amount of data in the buffers.
+    if (played < 0) {
+        played = 0;
+    }
+
+    TInt buffered = iSamplesWritten - played;
+    if (buffered < 0) {
+        buffered = 0;
+    }
+
+    if (iState == EStateWriting) {
+        return;
+    }
+
+    // The trick for low latency: Do not let the buffers fill up beyond the
+    // latency desired! We write as many samples as the difference between
+    // the latency target (in samples) and the amount of data buffered.
+    TInt samplesToWrite = iLatencySamples - buffered;
+
+    // Do not write very small blocks. This should improve efficiency, since
+    // writes to the streaming API are likely to be expensive.
+    if (samplesToWrite < iMinWrite) {
+        // Not enough data to write, set up a timer to fire after a while.
+        // Try againwhen it expired.
+        if (iTimerActive) {
+            return;
+        }
+        iTimerActive = ETrue;
+        SetActive();
+        iTimer.After(iStatus, (1000 * iLatency) / 8);
+        return;
+    }
+
+    // Do not write more than the set number of samples at once.
+    int numSamples = samplesToWrite;
+    if (numSamples > iMaxWrite) {
+        numSamples = iMaxWrite;
+    }
+
+    SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
+    if (device) {
+        SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
+
+        iBufDes.Set(phdata->buffer, 2 * numSamples, 2 * numSamples);
+        iStream->WriteL(iBufDes);
+        iState = EStateWriting;
+
+        // Keep track of the number of samples written (for latency calculations).
+        iSamplesWritten += numSamples;
+    } else {
+        // Output device not ready yet. Let's go for another round.
+        if (iTimerActive) {
+            return;
+        }
+        iTimerActive = ETrue;
+        SetActive();
+        iTimer.After(iStatus, (1000 * iLatency) / 8);
+    }
+}
+
+void CAudio::RunL()
+{
+    iTimerActive = EFalse;
+    Feed();
+}
+
+void CAudio::DoCancel()
+{
+    iTimerActive = EFalse;
+    iTimer.Cancel();
+}
+
+void CAudio::StartThread()
+{
+    TInt heapMinSize = 8192;        // 8 KB initial heap size.
+    TInt heapMaxSize = 1024 * 1024; // 1 MB maximum heap size.
+
+    TInt err = iProcess.Create(_L("ProcessThread"), ProcessThreadCB, KDefaultStackSize * 2, heapMinSize, heapMaxSize, this);
+    if (err == KErrNone) {
+        iProcess.SetPriority(EPriorityLess);
+        iProcess.Resume();
+    } else {
+        SDL_Log("Error: Failed to create audio processing thread: %d", err);
+    }
+}
+
+void CAudio::StopThread()
+{
+    if (iStreamStarted) {
+        iProcess.Kill(KErrNone);
+        iProcess.Close();
+        iStreamStarted = EFalse;
+    }
+}
+
+TInt CAudio::ProcessThreadCB(TAny *aPtr)
+{
+    CAudio *self = static_cast<CAudio *>(aPtr);
+    SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
+
+    while (self->iStreamStarted) {
+        if (device) {
+            SDL_PlaybackAudioThreadIterate(device);
+        } else {
+            device = NGAGE_GetAudioDeviceAddr();
+        }
+        User::After(100000); // 100ms.
+    }
+    return KErrNone;
+}
+
+void CAudio::MaoscOpenComplete(TInt aError)
+{
+    if (aError == KErrNone) {
+        iStream->SetVolume(1);
+        iStreamStarted = ETrue;
+        StartThread();
+
+    } else {
+        SDL_Log("Error: Failed to open audio stream: %d", aError);
+    }
+}
+
+void CAudio::MaoscBufferCopied(TInt aError, const TDesC8 & /*aBuffer*/)
+{
+    if (aError == KErrNone) {
+        iState = EStatePlaying;
+        Feed();
+    } else if (aError == KErrAbort) {
+        // The stream has been stopped.
+        iState = EStateDone;
+    } else {
+        SDL_Log("Error: Failed to copy audio buffer: %d", aError);
+    }
+}
+
+void CAudio::MaoscPlayComplete(TInt aError)
+{
+    // If we finish due to an underflow, we'll need to restart playback.
+    // Normally KErrUnderlow is raised   at stream end, but in our case the API
+    // should never see the stream end -- we are continuously feeding it more
+    // data!  Many underflow errors mean that the latency target is too low.
+    if (aError == KErrUnderflow) {
+        // The number of samples played gets resetted to zero when we restart
+        // playback after underflow.
+        iBaseSamplesPlayed = iSamplesWritten;
+
+        iStream->Stop();
+        Cancel();
+
+        iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz, TMdaAudioDataSettings::EChannelsMono);
+
+        iState = EStatePlaying;
+        Feed();
+        return;
+
+    } else if (aError != KErrNone) {
+        // Handle error.
+    }
+
+    // We shouldn't get here.
+    SDL_Log("%s: %d", __FUNCTION__, aError);
+}
+
+static TBool gAudioRunning;
+
+TBool AudioIsReady()
+{
+    return gAudioRunning;
+}
+
+TInt AudioThreadCB(TAny *aParams)
+{
+    CTrapCleanup *cleanup = CTrapCleanup::New();
+    if (!cleanup) {
+        return KErrNoMemory;
+    }
+
+    CActiveScheduler *scheduler = new CActiveScheduler();
+    if (!scheduler) {
+        delete cleanup;
+        return KErrNoMemory;
+    }
+
+    CActiveScheduler::Install(scheduler);
+
+    TRAPD(err,
+          {
+              TInt latency = *(TInt *)aParams;
+              CAudio *audio = CAudio::NewL(latency);
+              CleanupStack::PushL(audio);
+
+              gAudioRunning = ETrue;
+              audio->Start();
+              TBool once = EFalse;
+
+              while (gAudioRunning) {
+                  // Allow active scheduler to process any events.
+                  TInt error;
+                  CActiveScheduler::RunIfReady(error, CActive::EPriorityIdle);
+
+                  if (!once) {
+                      SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
+                      if (device) {
+                          // Stream ready; start feeding audio data.
+                          // After feeding it once, the callbacks will take over.
+                          audio->iState = CAudio::EStatePlaying;
+                          audio->Feed();
+                          once = ETrue;
+                      }
+                  }
+
+                  User::After(100000); // 100ms.
+              }
+
+              CleanupStack::PopAndDestroy(audio);
+          });
+
+    delete scheduler;
+    delete cleanup;
+    return err;
+}
+
+RThread audioThread;
+
+void InitAudio(TInt *aLatency)
+{
+    _LIT(KAudioThreadName, "AudioThread");
+
+    TInt err = audioThread.Create(KAudioThreadName, AudioThreadCB, KDefaultStackSize, 0, aLatency);
+    if (err != KErrNone) {
+        User::Leave(err);
+    }
+
+    audioThread.Resume();
+}
+
+void DeinitAudio()
+{
+    gAudioRunning = EFalse;
+
+    TRequestStatus status;
+    audioThread.Logon(status);
+    User::WaitForRequest(status);
+
+    audioThread.Close();
+}
+
+#endif // SDL_AUDIO_DRIVER_NGAGE

+ 44 - 0
src/audio/ngage/SDL_ngageaudio.h

@@ -0,0 +1,44 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#ifndef SDL_ngageaudio_h
+#define SDL_ngageaudio_h
+
+typedef struct SDL_PrivateAudioData
+{
+    Uint8 *buffer;
+
+} SDL_PrivateAudioData;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../SDL_sysaudio.h"
+
+SDL_AudioDevice *NGAGE_GetAudioDeviceAddr();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // SDL_ngageaudio_h

+ 98 - 0
src/audio/ngage/SDL_ngageaudio.hpp

@@ -0,0 +1,98 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_ngageaudio_hpp
+#define SDL_ngageaudio_hpp
+
+#include <e32base.h>
+#include <e32std.h>
+#include <mda/common/audio.h>
+#include <mdaaudiooutputstream.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../SDL_sysaudio.h"
+#include "SDL_ngageaudio.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+TBool AudioIsReady();
+void InitAudio(TInt *aLatency);
+void DeinitAudio();
+
+class CAudio : public CActive, public MMdaAudioOutputStreamCallback
+{
+  public:
+    static CAudio *NewL(TInt aLatency);
+    ~CAudio();
+
+    void ConstructL(TInt aLatency);
+    void Start();
+    void Feed();
+
+    void RunL();
+    void DoCancel();
+
+    static TInt ProcessThreadCB(TAny * /*aPtr*/);
+
+    // From MMdaAudioOutputStreamCallback
+    void MaoscOpenComplete(TInt aError);
+    void MaoscBufferCopied(TInt aError, const TDesC8 &aBuffer);
+    void MaoscPlayComplete(TInt aError);
+
+    enum
+    {
+        EStateNone = 0,
+        EStateOpening,
+        EStatePlaying,
+        EStateWriting,
+        EStateDone
+    } iState;
+
+  private:
+    CAudio();
+    void StartThread();
+    void StopThread();
+
+    CMdaAudioOutputStream *iStream;
+    TMdaAudioDataSettings iStreamSettings;
+    TBool iStreamStarted;
+
+    TPtr8 iBufDes;           // Descriptor for the buffer.
+    TInt iLatency;           // Latency target in ms
+    TInt iLatencySamples;    // Latency target in samples.
+    TInt iMinWrite;          // Min number of samples to write per turn.
+    TInt iMaxWrite;          // Max number of samples to write per turn.
+    TInt iBaseSamplesPlayed; // amples played before last restart.
+    TInt iSamplesWritten;    // Number of samples written so far.
+
+    RTimer iTimer;
+    TBool iTimerCreated;
+    TBool iTimerActive;
+
+    RThread iProcess;
+};
+
+#endif // SDL_ngageaudio_hpp

+ 77 - 0
src/core/ngage/SDL_ngage.cpp

@@ -0,0 +1,77 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#include <e32std.h>
+#include <e32svr.h>
+#include <hal.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+bool NGAGE_IsClassicModel()
+{
+    int phone_id;
+    HAL::Get(HALData::EMachineUid, phone_id);
+
+    return (0x101f8c19 == phone_id);
+}
+
+void NGAGE_printf(const char *fmt, ...)
+{
+    char buffer[512] = { 0 };
+
+    va_list ap;
+    va_start(ap, fmt);
+    vsprintf(buffer, fmt, ap);
+    va_end(ap);
+
+    TBuf<512> buf;
+    buf.Copy(TPtrC8((TText8 *)buffer));
+
+    RDebug::Print(_L("%S"), &buf);
+}
+
+void NGAGE_vnprintf(char *buf, size_t size, const char *fmt, va_list ap)
+{
+    char buffer[512] = { 0 };
+
+    vsprintf(buffer, fmt, ap);
+
+    TBuf<512> tbuf;
+    tbuf.Copy(TPtrC8((TText8 *)buffer));
+
+    RDebug::Print(_L("%S"), &tbuf);
+
+    strncpy(buf, buffer, size - 1);
+    buf[size - 1] = '\0';
+}
+
+TInt NGAGE_GetFreeHeapMemory()
+{
+    TInt free = 0;
+    return User::Available(free);
+}
+
+#ifdef __cplusplus
+}
+#endif

+ 36 - 0
src/core/ngage/SDL_ngage.h

@@ -0,0 +1,36 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#ifndef SDL_ngage_h
+#define SDL_ngage_h
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+bool NGAGE_IsClassicModel();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDL_ngage_h */

+ 8 - 0
src/cpuinfo/SDL_cpuinfo.c

@@ -421,6 +421,12 @@ static int CPU_haveARMSIMD(void)
     return regs.r[0];
 }
 
+#elif defined(SDL_PLATFORM_NGAGE)
+static int CPU_haveARMSIMD(void)
+{
+    // The RM920T is based on the ARMv4T architecture and doesn't have SIMD.
+    return 0;
+}
 #else
 static int CPU_haveARMSIMD(void)
 {
@@ -468,6 +474,8 @@ static int CPU_haveNEON(void)
     return 1;
 #elif defined(SDL_PLATFORM_3DS)
     return 0;
+#elif defined(SDL_PLATFORM_NGAGE)
+    return 0; // The ARM920T is based on the ARMv4T architecture and doesn't have NEON.
 #elif defined(SDL_PLATFORM_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.

+ 2 - 0
src/dynapi/SDL_dynapi.h

@@ -63,6 +63,8 @@
 #define SDL_DYNAMIC_API 0 // vitasdk doesn't support dynamic linking
 #elif defined(SDL_PLATFORM_3DS)
 #define SDL_DYNAMIC_API 0 // devkitARM doesn't support dynamic linking
+#elif defined(SDL_PLATFORM_NGAGE)
+#define SDL_DYNAMIC_API 0
 #elif defined(DYNAPI_NEEDS_DLOPEN) && !defined(HAVE_DLOPEN)
 #define SDL_DYNAMIC_API 0 // we need dlopen(), but don't have it....
 #endif

+ 67 - 0
src/filesystem/ngage/SDL_sysfilesystem.c

@@ -0,0 +1,67 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <[email protected]>
+
+  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"
+
+extern void NGAGE_GetAppPath(char* path);
+
+char *SDL_SYS_GetBasePath(void)
+{
+    char app_path[512];
+    NGAGE_GetAppPath(app_path);
+    char *base_path = SDL_strdup(app_path);
+    return base_path;
+}
+
+char *SDL_SYS_GetPrefPath(const char *org, const char *app)
+{
+    char *pref_path;
+    if (SDL_asprintf(&pref_path, "C:/System/Apps/%s/%s/", org, app) < 0)
+        return NULL;
+    else
+        return pref_path;
+}
+
+char *SDL_SYS_GetUserFolder(SDL_Folder folder)
+{
+    const char *folder_path = NULL;
+    switch (folder)
+    {
+        case SDL_FOLDER_HOME:
+            folder_path = "C:/";
+            break;
+        case SDL_FOLDER_PICTURES:
+            folder_path = "C:/Nokia/Pictures/";
+            break;
+        case SDL_FOLDER_SAVEDGAMES:
+            folder_path = "C:/";
+            break;
+        case SDL_FOLDER_SCREENSHOTS:
+            folder_path = "C:/Nokia/Pictures/";
+            break;
+        case SDL_FOLDER_VIDEOS:
+            folder_path = "C:/Nokia/Videos/";
+            break;
+        default:
+            folder_path = "C:/Nokia/Others/";
+            break;
+    }
+    return SDL_strdup(folder_path);
+}

+ 68 - 0
src/filesystem/ngage/SDL_sysfilesystem.cpp

@@ -0,0 +1,68 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <[email protected]>
+
+  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.
+*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "SDL_internal.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#include <e32base.h>
+#include <e32std.h>
+#include <f32file.h>
+#include <utf.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void NGAGE_GetAppPath(char* path)
+{
+    TBuf<512> aPath;
+
+    TFileName fullExePath = RProcess().FileName();
+
+    TParsePtrC parser(fullExePath);
+    aPath.Copy(parser.DriveAndPath());
+
+    TBuf8<512> utf8Path; // Temporary buffer for UTF-8 data.
+    CnvUtfConverter::ConvertFromUnicodeToUtf8(utf8Path, aPath);
+
+    // Copy UTF-8 data to the provided char* buffer.
+    strncpy(path, (const char*)utf8Path.Ptr(), utf8Path.Length());
+    path[utf8Path.Length()] = '\0';
+
+    // Replace backslashes with forward slashes.
+    for (int i = 0; i < utf8Path.Length(); i++)
+    {
+        if (path[i] == '\\')
+        {
+            path[i] = '/';
+        }
+    }
+}
+
+#ifdef __cplusplus
+}
+#endif

+ 307 - 0
src/locale/ngage/SDL_syslocale.cpp

@@ -0,0 +1,307 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_syslocale.h"
+#include "SDL_internal.h"
+
+#include <bautils.h>
+#include <e32base.h>
+#include <e32cons.h>
+#include <e32std.h>
+
+bool SDL_SYS_GetPreferredLocales(char *buf, size_t buflen)
+{
+    TLanguage language = User::Language();
+    const char *locale;
+
+    switch (language) {
+    case ELangFrench:
+    case ELangSwissFrench:
+        locale = "fr_CH";
+        break;
+    case ELangBelgianFrench:
+        locale = "fr_BE";
+        break;
+    case ELangInternationalFrench:
+        locale = "fr_FR";
+        break;
+    case ELangGerman:
+    case ELangSwissGerman:
+    case ELangAustrian:
+        locale = "de_DE";
+        break;
+    case ELangSpanish:
+    case ELangInternationalSpanish:
+    case ELangLatinAmericanSpanish:
+        locale = "es_ES";
+        break;
+    case ELangItalian:
+    case ELangSwissItalian:
+        locale = "it_IT";
+        break;
+    case ELangSwedish:
+    case ELangFinlandSwedish:
+        locale = "sv_SE";
+        break;
+    case ELangDanish:
+        locale = "da_DK";
+        break;
+    case ELangNorwegian:
+    case ELangNorwegianNynorsk:
+        locale = "no_NO";
+        break;
+    case ELangFinnish:
+        locale = "fi_FI";
+        break;
+    case ELangPortuguese:
+        locale = "pt_PT";
+        break;
+    case ELangBrazilianPortuguese:
+        locale = "pt_BR";
+        break;
+    case ELangTurkish:
+    case ELangCyprusTurkish:
+        locale = "tr_TR";
+        break;
+    case ELangIcelandic:
+        locale = "is_IS";
+        break;
+    case ELangRussian:
+        locale = "ru_RU";
+        break;
+    case ELangHungarian:
+        locale = "hu_HU";
+        break;
+    case ELangDutch:
+        locale = "nl_NL";
+        break;
+    case ELangBelgianFlemish:
+        locale = "nl_BE";
+        break;
+    case ELangAustralian:
+    case ELangNewZealand:
+        locale = "en_AU";
+        break;
+    case ELangCzech:
+        locale = "cs_CZ";
+        break;
+    case ELangSlovak:
+        locale = "sk_SK";
+        break;
+    case ELangPolish:
+        locale = "pl_PL";
+        break;
+    case ELangSlovenian:
+        locale = "sl_SI";
+        break;
+    case ELangTaiwanChinese:
+        locale = "zh_TW";
+        break;
+    case ELangHongKongChinese:
+        locale = "zh_HK";
+        break;
+    case ELangPrcChinese:
+        locale = "zh_CN";
+        break;
+    case ELangJapanese:
+        locale = "ja_JP";
+        break;
+    case ELangThai:
+        locale = "th_TH";
+        break;
+    case ELangAfrikaans:
+        locale = "af_ZA";
+        break;
+    case ELangAlbanian:
+        locale = "sq_AL";
+        break;
+    case ELangAmharic:
+        locale = "am_ET";
+        break;
+    case ELangArabic:
+        locale = "ar_SA";
+        break;
+    case ELangArmenian:
+        locale = "hy_AM";
+        break;
+    case ELangAzerbaijani:
+        locale = "az_AZ";
+        break;
+    case ELangBelarussian:
+        locale = "be_BY";
+        break;
+    case ELangBengali:
+        locale = "bn_IN";
+        break;
+    case ELangBulgarian:
+        locale = "bg_BG";
+        break;
+    case ELangBurmese:
+        locale = "my_MM";
+        break;
+    case ELangCatalan:
+        locale = "ca_ES";
+        break;
+    case ELangCroatian:
+        locale = "hr_HR";
+        break;
+    case ELangEstonian:
+        locale = "et_EE";
+        break;
+    case ELangFarsi:
+        locale = "fa_IR";
+        break;
+    case ELangCanadianFrench:
+        locale = "fr_CA";
+        break;
+    case ELangScotsGaelic:
+        locale = "gd_GB";
+        break;
+    case ELangGeorgian:
+        locale = "ka_GE";
+        break;
+    case ELangGreek:
+    case ELangCyprusGreek:
+        locale = "el_GR";
+        break;
+    case ELangGujarati:
+        locale = "gu_IN";
+        break;
+    case ELangHebrew:
+        locale = "he_IL";
+        break;
+    case ELangHindi:
+        locale = "hi_IN";
+        break;
+    case ELangIndonesian:
+        locale = "id_ID";
+        break;
+    case ELangIrish:
+        locale = "ga_IE";
+        break;
+    case ELangKannada:
+        locale = "kn_IN";
+        break;
+    case ELangKazakh:
+        locale = "kk_KZ";
+        break;
+    case ELangKhmer:
+        locale = "km_KH";
+        break;
+    case ELangKorean:
+        locale = "ko_KR";
+        break;
+    case ELangLao:
+        locale = "lo_LA";
+        break;
+    case ELangLatvian:
+        locale = "lv_LV";
+        break;
+    case ELangLithuanian:
+        locale = "lt_LT";
+        break;
+    case ELangMacedonian:
+        locale = "mk_MK";
+        break;
+    case ELangMalay:
+        locale = "ms_MY";
+        break;
+    case ELangMalayalam:
+        locale = "ml_IN";
+        break;
+    case ELangMarathi:
+        locale = "mr_IN";
+        break;
+    case ELangMoldavian:
+        locale = "ro_MD";
+        break;
+    case ELangMongolian:
+        locale = "mn_MN";
+        break;
+    case ELangPunjabi:
+        locale = "pa_IN";
+        break;
+    case ELangRomanian:
+        locale = "ro_RO";
+        break;
+    case ELangSerbian:
+        locale = "sr_RS";
+        break;
+    case ELangSinhalese:
+        locale = "si_LK";
+        break;
+    case ELangSomali:
+        locale = "so_SO";
+        break;
+    case ELangSwahili:
+        locale = "sw_KE";
+        break;
+    case ELangTajik:
+        locale = "tg_TJ";
+        break;
+    case ELangTamil:
+        locale = "ta_IN";
+        break;
+    case ELangTelugu:
+        locale = "te_IN";
+        break;
+    case ELangTibetan:
+        locale = "bo_CN";
+        break;
+    case ELangTigrinya:
+        locale = "ti_ET";
+        break;
+    case ELangTurkmen:
+        locale = "tk_TM";
+        break;
+    case ELangUkrainian:
+        locale = "uk_UA";
+        break;
+    case ELangUrdu:
+        locale = "ur_PK";
+        break;
+    case ELangUzbek:
+        locale = "uz_UZ";
+        break;
+    case ELangVietnamese:
+        locale = "vi_VN";
+        break;
+    case ELangWelsh:
+        locale = "cy_GB";
+        break;
+    case ELangZulu:
+        locale = "zu_ZA";
+        break;
+    case ELangEnglish:
+        locale = "en_GB";
+        break;
+    case ELangAmerican:
+    case ELangCanadianEnglish:
+    case ELangInternationalEnglish:
+    case ELangSouthAfricanEnglish:
+    default:
+        locale = "en_US";
+        break;
+    }
+
+    SDL_strlcpy(buf, locale, buflen);
+
+    return true;
+}

+ 31 - 0
src/main/ngage/SDL_sysmain_callbacks.c

@@ -0,0 +1,31 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_PLATFORM_NGAGE
+
+int SDL_EnterAppMainCallbacks(int argc, char* argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit)
+{
+    // Intentionally does nothing; Callbacks are called using the RunL() method.
+    return 0;
+}
+
+#endif // SDL_PLATFORM_NGAGE

+ 199 - 0
src/main/ngage/SDL_sysmain_main.cpp

@@ -0,0 +1,199 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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.
+*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "SDL_internal.h"
+
+extern SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]);
+extern SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event);
+extern SDL_AppResult SDL_AppIterate(void* appstate);
+extern void SDL_AppQuit(void* appstate, SDL_AppResult result);
+
+#ifdef __cplusplus
+}
+#endif
+
+#include <e32std.h>
+#include <estlib.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "SDL_sysmain_main.hpp"
+#include "../../audio/ngage/SDL_ngageaudio.hpp"
+#include "../../render/ngage/SDL_render_ngage_c.hpp"
+
+CRenderer *gRenderer = 0;
+
+GLDEF_C TInt E32Main()
+{
+    // Get args and environment.
+    int argc = 1;
+    char* argv[] = { "game", NULL };
+    char** envp = NULL;
+
+    // Create lvalue variables for __crt0 arguments.
+    char** argv_lvalue = argv;
+    char** envp_lvalue = envp;
+
+    CTrapCleanup* cleanup = CTrapCleanup::New();
+    if (!cleanup)
+    {
+        return KErrNoMemory;
+    }
+
+    TRAPD(err,
+    {
+        CActiveScheduler* scheduler = new (ELeave) CActiveScheduler();
+        CleanupStack::PushL(scheduler);
+        CActiveScheduler::Install(scheduler);
+
+        TInt posixErr = SpawnPosixServerThread();
+        if (posixErr != KErrNone)
+        {
+            SDL_Log("Error: Failed to spawn POSIX server thread: %d", posixErr);
+            User::Leave(posixErr);
+        }
+
+        __crt0(argc, argv_lvalue, envp_lvalue);
+
+        // Increase heap size.
+        RHeap* newHeap = User::ChunkHeap(NULL, 7500000, 7500000, KMinHeapGrowBy);
+        if (!newHeap)
+        {
+            SDL_Log("Error: Failed to create new heap");
+            User::Leave(KErrNoMemory);
+        }
+        CleanupStack::PushL(newHeap);
+
+        RHeap* oldHeap = User::SwitchHeap(newHeap);
+
+        TInt targetLatency = 225;
+        InitAudio(&targetLatency);
+
+        // Wait until audio is ready.
+        while (!AudioIsReady())
+        {
+            User::After(100000); // 100ms.
+        }
+
+        // Create and start the rendering backend.
+        gRenderer = CRenderer::NewL();
+        CleanupStack::PushL(gRenderer);
+
+        // Create and start the SDL main runner.
+        CSDLmain* mainApp = CSDLmain::NewL();
+        CleanupStack::PushL(mainApp);
+        mainApp->Start();
+
+        // Start the active scheduler to handle events.
+        CActiveScheduler::Start();
+
+        CleanupStack::PopAndDestroy(gRenderer);
+        CleanupStack::PopAndDestroy(mainApp);
+
+        User::SwitchHeap(oldHeap);
+
+        CleanupStack::PopAndDestroy(newHeap);
+        CleanupStack::PopAndDestroy(scheduler);
+    });
+
+    if (err != KErrNone)
+    {
+        SDL_Log("Error: %d", err);
+    }
+
+    return err;
+}
+
+CSDLmain* CSDLmain::NewL()
+{
+    CSDLmain* self = new (ELeave) CSDLmain();
+    CleanupStack::PushL(self);
+    self->ConstructL();
+    CleanupStack::Pop(self);
+    return self;
+}
+
+CSDLmain::CSDLmain() : CActive(EPriorityLow) {}
+
+void CSDLmain::ConstructL()
+{
+    CActiveScheduler::Add(this);
+}
+
+CSDLmain::~CSDLmain()
+{
+    Cancel();
+}
+
+void CSDLmain::Start()
+{
+    SetActive();
+    TRequestStatus* status = &iStatus;
+    User::RequestComplete(status, KErrNone);
+}
+
+void CSDLmain::DoCancel() {}
+
+static bool callbacks_initialized = false;
+
+void CSDLmain::RunL()
+{
+    if (callbacks_initialized)
+    {
+        SDL_Event event;
+
+        iResult = SDL_AppIterate(NULL);
+        if (iResult != SDL_APP_CONTINUE)
+        {
+            DeinitAudio();
+            SDL_AppQuit(NULL, iResult);
+            SDL_Quit();
+            CActiveScheduler::Stop();
+            return;
+        }
+
+        SDL_PumpEvents();
+        if (SDL_PollEvent(&event))
+        {
+            iResult = SDL_AppEvent(NULL, &event);
+            if (iResult != SDL_APP_CONTINUE)
+            {
+                DeinitAudio();
+                SDL_AppQuit(NULL, iResult);
+                SDL_Quit();
+                CActiveScheduler::Stop();
+                return;
+            }
+        }
+
+        Start();
+    }
+    else
+    {
+        SDL_SetMainReady();
+        SDL_AppInit(NULL, 0, NULL);
+        callbacks_initialized = true;
+        Start();
+    }
+}

+ 46 - 0
src/main/ngage/SDL_sysmain_main.hpp

@@ -0,0 +1,46 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#ifndef SDL_sysmain_main_hpp_
+#define SDL_sysmain_main_hpp_
+
+#include <e32std.h>
+
+class CSDLmain : public CActive
+{
+public:
+    static CSDLmain* NewL();
+    ~CSDLmain();
+
+    void Start();
+
+protected:
+    void DoCancel() ;
+    void RunL();
+
+private:
+    CSDLmain();
+    void ConstructL();
+    SDL_AppResult iResult;
+};
+
+#endif // SDL_sysmain_main_hpp_

+ 3 - 0
src/render/SDL_render.c

@@ -120,6 +120,9 @@ static const SDL_RenderDriver *render_drivers[] = {
 #ifdef SDL_VIDEO_RENDER_METAL
     &METAL_RenderDriver,
 #endif
+#ifdef SDL_VIDEO_RENDER_NGAGE
+    &NGAGE_RenderDriver,
+#endif
 #ifdef SDL_VIDEO_RENDER_OGL
     &GL_RenderDriver,
 #endif

+ 1 - 0
src/render/SDL_sysrender.h

@@ -357,6 +357,7 @@ extern SDL_RenderDriver D3D12_RenderDriver;
 extern SDL_RenderDriver GL_RenderDriver;
 extern SDL_RenderDriver GLES2_RenderDriver;
 extern SDL_RenderDriver METAL_RenderDriver;
+extern SDL_RenderDriver NGAGE_RenderDriver;
 extern SDL_RenderDriver VULKAN_RenderDriver;
 extern SDL_RenderDriver PS2_RenderDriver;
 extern SDL_RenderDriver PSP_RenderDriver;

+ 544 - 0
src/render/ngage/SDL_render_ngage.c

@@ -0,0 +1,544 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_VIDEO_RENDER_NGAGE
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+#ifndef Int2Fix
+#define Int2Fix(i) ((i) << 16)
+#endif
+
+#ifndef Fix2Int
+#define Fix2Int(i) ((((unsigned int)(i) > 0xFFFF0000) ? 0 : ((i) >> 16)))
+#endif
+
+#ifndef Fix2Real
+#define Fix2Real(i) ((i) / 65536.0)
+#endif
+
+#ifndef Real2Fix
+#define Real2Fix(i) ((int)((i) * 65536.0))
+#endif
+
+#include "../SDL_sysrender.h"
+#include "SDL_render_ngage_c.h"
+
+static void NGAGE_WindowEvent(SDL_Renderer *renderer, const SDL_WindowEvent *event);
+static bool NGAGE_GetOutputSize(SDL_Renderer *renderer, int *w, int *h);
+static bool NGAGE_SupportsBlendMode(SDL_Renderer *renderer, SDL_BlendMode blendMode);
+static bool NGAGE_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL_PropertiesID create_props);
+static bool NGAGE_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd);
+static bool NGAGE_QueueSetDrawColor(SDL_Renderer *renderer, SDL_RenderCommand *cmd);
+static bool NGAGE_QueueDrawVertices(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points, int count);
+static bool NGAGE_QueueFillRects(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FRect *rects, int count);
+static bool NGAGE_QueueCopy(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const SDL_FRect *srcrect, const SDL_FRect *dstrect);
+static bool NGAGE_QueueCopyEx(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const SDL_FRect *srcquad, const SDL_FRect *dstrect, const double angle, const SDL_FPoint *center, const SDL_FlipMode flip, float scale_x, float scale_y);
+static bool NGAGE_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const float *xy, int xy_stride, const SDL_FColor *color, int color_stride, const float *uv, int uv_stride, int num_vertices, const void *indices, int num_indices, int size_indices, float scale_x, float scale_y);
+
+static void NGAGE_InvalidateCachedState(SDL_Renderer *renderer);
+static bool NGAGE_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd, void *vertices, size_t vertsize);
+static bool NGAGE_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *rect, const void *pixels, int pitch);
+
+static bool NGAGE_LockTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *rect, void **pixels, int *pitch);
+static void NGAGE_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture);
+static void NGAGE_SetTextureScaleMode(SDL_Renderer *renderer, SDL_Texture *texture, SDL_ScaleMode scaleMode);
+static bool NGAGE_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture);
+static SDL_Surface *NGAGE_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect);
+static bool NGAGE_RenderPresent(SDL_Renderer *renderer);
+static void NGAGE_DestroyTexture(SDL_Renderer *renderer, SDL_Texture *texture);
+
+static void NGAGE_DestroyRenderer(SDL_Renderer *renderer);
+
+static bool NGAGE_SetVSync(SDL_Renderer *renderer, int vsync);
+
+static bool NGAGE_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL_PropertiesID create_props)
+{
+    SDL_SetupRendererColorspace(renderer, create_props);
+
+    if (renderer->output_colorspace != SDL_COLORSPACE_RGB_DEFAULT) {
+        return SDL_SetError("Unsupported output colorspace");
+    }
+
+    NGAGE_RendererData *phdata = SDL_calloc(1, sizeof(NGAGE_RendererData));
+    if (!phdata) {
+        SDL_OutOfMemory();
+        return false;
+    }
+
+    renderer->WindowEvent = NGAGE_WindowEvent;
+    renderer->GetOutputSize = NGAGE_GetOutputSize;
+    renderer->SupportsBlendMode = NGAGE_SupportsBlendMode;
+    renderer->CreateTexture = NGAGE_CreateTexture;
+    renderer->QueueSetViewport = NGAGE_QueueSetViewport;
+    renderer->QueueSetDrawColor = NGAGE_QueueSetDrawColor;
+    renderer->QueueDrawPoints = NGAGE_QueueDrawVertices;
+    renderer->QueueDrawLines = NGAGE_QueueDrawVertices;
+    renderer->QueueFillRects = NGAGE_QueueFillRects;
+    renderer->QueueCopy = NGAGE_QueueCopy;
+    renderer->QueueCopyEx = NGAGE_QueueCopyEx;
+    renderer->QueueGeometry = NGAGE_QueueGeometry;
+
+    renderer->InvalidateCachedState = NGAGE_InvalidateCachedState;
+    renderer->RunCommandQueue = NGAGE_RunCommandQueue;
+    renderer->UpdateTexture = NGAGE_UpdateTexture;
+    renderer->LockTexture = NGAGE_LockTexture;
+    renderer->UnlockTexture = NGAGE_UnlockTexture;
+    // renderer->SetTextureScaleMode = NGAGE_SetTextureScaleMode;
+    renderer->SetRenderTarget = NGAGE_SetRenderTarget;
+    renderer->RenderReadPixels = NGAGE_RenderReadPixels;
+    renderer->RenderPresent = NGAGE_RenderPresent;
+    renderer->DestroyTexture = NGAGE_DestroyTexture;
+
+    renderer->DestroyRenderer = NGAGE_DestroyRenderer;
+
+    renderer->SetVSync = NGAGE_SetVSync;
+
+    renderer->name = NGAGE_RenderDriver.name;
+    renderer->window = window;
+    renderer->internal = phdata;
+
+    SDL_AddSupportedTextureFormat(renderer, SDL_PIXELFORMAT_ARGB4444);
+    SDL_SetNumberProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_MAX_TEXTURE_SIZE_NUMBER, 256);
+    SDL_SetHintWithPriority(SDL_HINT_RENDER_LINE_METHOD, "2", SDL_HINT_OVERRIDE);
+
+    return true;
+}
+
+SDL_RenderDriver NGAGE_RenderDriver = {
+    NGAGE_CreateRenderer,
+    "N-Gage"
+};
+
+static void NGAGE_WindowEvent(SDL_Renderer *renderer, const SDL_WindowEvent *event)
+{
+    return;
+}
+
+static bool NGAGE_GetOutputSize(SDL_Renderer *renderer, int *w, int *h)
+{
+    return true;
+}
+
+static bool NGAGE_SupportsBlendMode(SDL_Renderer *renderer, SDL_BlendMode blendMode)
+{
+    switch (blendMode) {
+    case SDL_BLENDMODE_NONE:
+    case SDL_BLENDMODE_MOD:
+        return true;
+    default:
+        return false;
+    }
+}
+
+static bool NGAGE_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL_PropertiesID create_props)
+{
+    NGAGE_TextureData *data = (NGAGE_TextureData *)SDL_calloc(1, sizeof(*data));
+    if (!data) {
+        return false;
+    }
+
+    if (!NGAGE_CreateTextureData(data, texture->w, texture->h)) {
+        SDL_free(data);
+        return false;
+    }
+
+    SDL_Surface *surface = SDL_CreateSurface(texture->w, texture->h, texture->format);
+    if (!surface) {
+        SDL_free(data);
+        return false;
+    }
+
+    data->surface = surface;
+    texture->internal = data;
+
+    return true;
+}
+
+static bool NGAGE_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+{
+    if (!cmd->data.viewport.rect.w && !cmd->data.viewport.rect.h) {
+        SDL_Rect viewport = { 0, 0, NGAGE_SCREEN_WIDTH, NGAGE_SCREEN_HEIGHT };
+        SDL_SetRenderViewport(renderer, &viewport);
+    }
+
+    return true;
+}
+
+static bool NGAGE_QueueSetDrawColor(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+{
+    return true;
+}
+
+static bool NGAGE_QueueDrawVertices(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points, int count)
+{
+    NGAGE_Vertex *verts = (NGAGE_Vertex *)SDL_AllocateRenderVertices(renderer, count * sizeof(NGAGE_Vertex), 0, &cmd->data.draw.first);
+    if (!verts) {
+        return false;
+    }
+
+    cmd->data.draw.count = count;
+
+    for (int i = 0; i < count; i++, points++) {
+        int fixed_x = Real2Fix(points->x);
+        int fixed_y = Real2Fix(points->y);
+
+        verts[i].x = Fix2Int(fixed_x);
+        verts[i].y = Fix2Int(fixed_y);
+
+        Uint32 color = NGAGE_ConvertColor(cmd->data.draw.color.r, cmd->data.draw.color.g, cmd->data.draw.color.b, cmd->data.draw.color.a, cmd->data.draw.color_scale);
+
+        verts[i].color.a = (Uint8)(color >> 24);
+        verts[i].color.b = (Uint8)(color >> 16);
+        verts[i].color.g = (Uint8)(color >> 8);
+        verts[i].color.r = (Uint8)color;
+    }
+
+    return true;
+}
+
+static bool NGAGE_QueueFillRects(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FRect *rects, int count)
+{
+    NGAGE_Vertex *verts = (NGAGE_Vertex *)SDL_AllocateRenderVertices(renderer, count * 2 * sizeof(NGAGE_Vertex), 0, &cmd->data.draw.first);
+    if (!verts) {
+        return false;
+    }
+
+    cmd->data.draw.count = count;
+
+    for (int i = 0; i < count; i++, rects++) {
+        verts[i * 2].x = Real2Fix(rects->x);
+        verts[i * 2].y = Real2Fix(rects->y);
+        verts[i * 2 + 1].x = Real2Fix(rects->w);
+        verts[i * 2 + 1].y = Real2Fix(rects->h);
+
+        verts[i * 2].x = Fix2Int(verts[i * 2].x);
+        verts[i * 2].y = Fix2Int(verts[i * 2].y);
+        verts[i * 2 + 1].x = Fix2Int(verts[i * 2 + 1].x);
+        verts[i * 2 + 1].y = Fix2Int(verts[i * 2 + 1].y);
+
+        Uint32 color = NGAGE_ConvertColor(cmd->data.draw.color.r, cmd->data.draw.color.g, cmd->data.draw.color.b, cmd->data.draw.color.a, cmd->data.draw.color_scale);
+
+        verts[i * 2].color.a = (Uint8)(color >> 24);
+        verts[i * 2].color.b = (Uint8)(color >> 16);
+        verts[i * 2].color.g = (Uint8)(color >> 8);
+        verts[i * 2].color.r = (Uint8)color;
+    }
+
+    return true;
+}
+
+static bool NGAGE_QueueCopy(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const SDL_FRect *srcrect, const SDL_FRect *dstrect)
+{
+    SDL_Rect *verts = (SDL_Rect *)SDL_AllocateRenderVertices(renderer, 2 * sizeof(SDL_Rect), 0, &cmd->data.draw.first);
+
+    if (!verts) {
+        return false;
+    }
+
+    cmd->data.draw.count = 1;
+
+    verts->x = (int)srcrect->x;
+    verts->y = (int)srcrect->y;
+    verts->w = (int)srcrect->w;
+    verts->h = (int)srcrect->h;
+
+    verts++;
+
+    verts->x = (int)dstrect->x;
+    verts->y = (int)dstrect->y;
+    verts->w = (int)dstrect->w;
+    verts->h = (int)dstrect->h;
+
+    return true;
+}
+
+static bool NGAGE_QueueCopyEx(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const SDL_FRect *srcquad, const SDL_FRect *dstrect, const double angle, const SDL_FPoint *center, const SDL_FlipMode flip, float scale_x, float scale_y)
+{
+    NGAGE_CopyExData *verts = (NGAGE_CopyExData *)SDL_AllocateRenderVertices(renderer, sizeof(NGAGE_CopyExData), 0, &cmd->data.draw.first);
+
+    if (!verts) {
+        return false;
+    }
+
+    cmd->data.draw.count = 1;
+
+    verts->srcrect.x = (int)srcquad->x;
+    verts->srcrect.y = (int)srcquad->y;
+    verts->srcrect.w = (int)srcquad->w;
+    verts->srcrect.h = (int)srcquad->h;
+    verts->dstrect.x = (int)dstrect->x;
+    verts->dstrect.y = (int)dstrect->y;
+    verts->dstrect.w = (int)dstrect->w;
+    verts->dstrect.h = (int)dstrect->h;
+
+    verts->angle = Real2Fix(angle);
+    verts->center.x = Real2Fix(center->x);
+    verts->center.y = Real2Fix(center->y);
+    verts->scale_x = Real2Fix(scale_x);
+    verts->scale_y = Real2Fix(scale_y);
+
+    verts->flip = flip;
+
+    return true;
+}
+
+static bool NGAGE_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, SDL_Texture *texture, const float *xy, int xy_stride, const SDL_FColor *color, int color_stride, const float *uv, int uv_stride, int num_vertices, const void *indices, int num_indices, int size_indices, float scale_x, float scale_y)
+{
+    return true;
+}
+
+static void NGAGE_InvalidateCachedState(SDL_Renderer *renderer)
+{
+    return;
+}
+
+static bool NGAGE_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd, void *vertices, size_t vertsize)
+{
+    NGAGE_RendererData *phdata = (NGAGE_RendererData *)renderer->internal;
+    if (!phdata) {
+        return false;
+    }
+    phdata->viewport = 0;
+
+    while (cmd) {
+        switch (cmd->command) {
+        case SDL_RENDERCMD_NO_OP:
+            break;
+        case SDL_RENDERCMD_SETVIEWPORT:
+            phdata->viewport = &cmd->data.viewport.rect;
+            break;
+
+        case SDL_RENDERCMD_SETCLIPRECT:
+        {
+            const SDL_Rect *rect = &cmd->data.cliprect.rect;
+
+            if (cmd->data.cliprect.enabled) {
+                NGAGE_SetClipRect(rect);
+            }
+
+            break;
+        }
+
+        case SDL_RENDERCMD_SETDRAWCOLOR:
+        {
+            break;
+        }
+
+        case SDL_RENDERCMD_CLEAR:
+        {
+            Uint32 color = NGAGE_ConvertColor(cmd->data.color.color.r, cmd->data.color.color.g, cmd->data.color.color.b, cmd->data.color.color.a, cmd->data.color.color_scale);
+
+            NGAGE_Clear(color);
+            break;
+        }
+
+        case SDL_RENDERCMD_DRAW_POINTS:
+        {
+            NGAGE_Vertex *verts = (NGAGE_Vertex *)(((Uint8 *)vertices) + cmd->data.draw.first);
+            const int count = cmd->data.draw.count;
+
+            // Apply viewport.
+            if (phdata->viewport && (phdata->viewport->x || phdata->viewport->y)) {
+                for (int i = 0; i < count; i++) {
+                    verts[i].x += phdata->viewport->x;
+                    verts[i].y += phdata->viewport->y;
+                }
+            }
+
+            NGAGE_DrawPoints(verts, count);
+            break;
+        }
+        case SDL_RENDERCMD_DRAW_LINES:
+        {
+            NGAGE_Vertex *verts = (NGAGE_Vertex *)(((Uint8 *)vertices) + cmd->data.draw.first);
+            const int count = cmd->data.draw.count;
+
+            // Apply viewport.
+            if (phdata->viewport && (phdata->viewport->x || phdata->viewport->y)) {
+                for (int i = 0; i < count; i++) {
+                    verts[i].x += phdata->viewport->x;
+                    verts[i].y += phdata->viewport->y;
+                }
+            }
+
+            NGAGE_DrawLines(verts, count);
+            break;
+        }
+
+        case SDL_RENDERCMD_FILL_RECTS:
+        {
+            NGAGE_Vertex *verts = (NGAGE_Vertex *)(((Uint8 *)vertices) + cmd->data.draw.first);
+            const int count = cmd->data.draw.count;
+
+            // Apply viewport.
+            if (phdata->viewport && (phdata->viewport->x || phdata->viewport->y)) {
+                for (int i = 0; i < count; i++) {
+                    verts[i].x += phdata->viewport->x;
+                    verts[i].y += phdata->viewport->y;
+                }
+            }
+
+            NGAGE_FillRects(verts, count);
+            break;
+        }
+
+        case SDL_RENDERCMD_COPY:
+        {
+            SDL_Rect *verts = (SDL_Rect *)(((Uint8 *)vertices) + cmd->data.draw.first);
+            SDL_Rect *srcrect = verts;
+            SDL_Rect *dstrect = verts + 1;
+            SDL_Texture *texture = cmd->data.draw.texture;
+
+            // Apply viewport.
+            if (phdata->viewport && (phdata->viewport->x || phdata->viewport->y)) {
+                dstrect->x += phdata->viewport->x;
+                dstrect->y += phdata->viewport->y;
+            }
+
+            NGAGE_Copy(renderer, texture, srcrect, dstrect);
+            break;
+        }
+
+        case SDL_RENDERCMD_COPY_EX:
+        {
+            NGAGE_CopyExData *copydata = (NGAGE_CopyExData *)(((Uint8 *)vertices) + cmd->data.draw.first);
+            SDL_Texture *texture = cmd->data.draw.texture;
+
+            // Apply viewport.
+            if (phdata->viewport && (phdata->viewport->x || phdata->viewport->y)) {
+                copydata->dstrect.x += phdata->viewport->x;
+                copydata->dstrect.y += phdata->viewport->y;
+            }
+
+            NGAGE_CopyEx(renderer, texture, copydata);
+            break;
+        }
+
+        case SDL_RENDERCMD_GEOMETRY:
+        {
+            break;
+        }
+        }
+        cmd = cmd->next;
+    }
+
+    return true;
+}
+
+static bool NGAGE_UpdateTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *rect, const void *pixels, int pitch)
+{
+    NGAGE_TextureData *phdata = (NGAGE_TextureData *)texture->internal;
+
+    SDL_Surface *surface = phdata->surface;
+    Uint8 *src, *dst;
+    int row;
+    size_t length;
+
+    if (SDL_MUSTLOCK(surface)) {
+        if (!SDL_LockSurface(surface)) {
+            return false;
+        }
+    }
+    src = (Uint8 *)pixels;
+    dst = (Uint8 *)surface->pixels +
+          rect->y * surface->pitch +
+          rect->x * surface->fmt->bytes_per_pixel;
+
+    length = (size_t)rect->w * surface->fmt->bytes_per_pixel;
+    for (row = 0; row < rect->h; ++row) {
+        SDL_memcpy(dst, src, length);
+        src += pitch;
+        dst += surface->pitch;
+    }
+    if (SDL_MUSTLOCK(surface)) {
+        SDL_UnlockSurface(surface);
+    }
+
+    return true;
+}
+
+static bool NGAGE_LockTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *rect, void **pixels, int *pitch)
+{
+    NGAGE_TextureData *phdata = (NGAGE_TextureData *)texture->internal;
+    SDL_Surface *surface = phdata->surface;
+
+    *pixels =
+        (void *)((Uint8 *)surface->pixels + rect->y * surface->pitch +
+                 rect->x * surface->fmt->bytes_per_pixel);
+    *pitch = surface->pitch;
+    return true;
+}
+
+static void NGAGE_UnlockTexture(SDL_Renderer *renderer, SDL_Texture *texture)
+{
+}
+
+static void NGAGE_SetTextureScaleMode(SDL_Renderer *renderer, SDL_Texture *texture, SDL_ScaleMode scaleMode)
+{
+}
+
+static bool NGAGE_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture)
+{
+    return true;
+}
+
+static SDL_Surface *NGAGE_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect)
+{
+    return (SDL_Surface *)0;
+}
+
+static bool NGAGE_RenderPresent(SDL_Renderer *renderer)
+{
+    NGAGE_Flip();
+
+    return true;
+}
+
+static void NGAGE_DestroyTexture(SDL_Renderer *renderer, SDL_Texture *texture)
+{
+    NGAGE_TextureData *data = (NGAGE_TextureData *)texture->internal;
+    if (data) {
+        SDL_DestroySurface(data->surface);
+        NGAGE_DestroyTextureData(data);
+        SDL_free(data);
+        texture->internal = 0;
+    }
+}
+
+static void NGAGE_DestroyRenderer(SDL_Renderer *renderer)
+{
+    NGAGE_RendererData *phdata = (NGAGE_RendererData *)renderer->internal;
+    if (phdata) {
+        SDL_free(phdata);
+        renderer->internal = 0;
+    }
+}
+
+static bool NGAGE_SetVSync(SDL_Renderer *renderer, int vsync)
+{
+    return true;
+}
+
+#endif // SDL_VIDEO_RENDER_NGAGE

+ 744 - 0
src/render/ngage/SDL_render_ngage.cpp

@@ -0,0 +1,744 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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.
+*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../../events/SDL_keyboard_c.h"
+#include "../SDL_sysrender.h"
+#include "SDL_internal.h"
+#include "SDL_render_ngage_c.h"
+
+#ifdef __cplusplus
+}
+#endif
+
+#ifdef SDL_VIDEO_RENDER_NGAGE
+
+#include "SDL_render_ngage_c.hpp"
+#include "SDL_render_ops.hpp"
+
+const TUint32 WindowClientHandle = 0x571D0A;
+
+extern CRenderer *gRenderer;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void NGAGE_Clear(const Uint32 color)
+{
+    gRenderer->Clear(color);
+}
+
+bool NGAGE_Copy(SDL_Renderer *renderer, SDL_Texture *texture, SDL_Rect *srcrect, SDL_Rect *dstrect)
+{
+    return gRenderer->Copy(renderer, texture, srcrect, dstrect);
+}
+
+bool NGAGE_CopyEx(SDL_Renderer *renderer, SDL_Texture *texture, NGAGE_CopyExData *copydata)
+{
+    return gRenderer->CopyEx(renderer, texture, copydata);
+}
+
+bool NGAGE_CreateTextureData(NGAGE_TextureData *data, const int width, const int height)
+{
+    return gRenderer->CreateTextureData(data, width, height);
+}
+
+void NGAGE_DestroyTextureData(NGAGE_TextureData *data)
+{
+    if (data) {
+        delete data->bitmap;
+        data->bitmap = NULL;
+    }
+}
+
+void NGAGE_DrawLines(NGAGE_Vertex *verts, const int count)
+{
+    gRenderer->DrawLines(verts, count);
+}
+
+void NGAGE_DrawPoints(NGAGE_Vertex *verts, const int count)
+{
+    gRenderer->DrawPoints(verts, count);
+}
+
+void NGAGE_FillRects(NGAGE_Vertex *verts, const int count)
+{
+    gRenderer->FillRects(verts, count);
+}
+
+void NGAGE_Flip()
+{
+    gRenderer->Flip();
+}
+
+void NGAGE_SetClipRect(const SDL_Rect *rect)
+{
+    gRenderer->SetClipRect(rect->x, rect->y, rect->w, rect->h);
+}
+
+void NGAGE_SetDrawColor(const Uint32 color)
+{
+    if (gRenderer) {
+        gRenderer->SetDrawColor(color);
+    }
+}
+
+void NGAGE_PumpEventsInternal()
+{
+    gRenderer->PumpEvents();
+}
+
+void NGAGE_SuspendScreenSaverInternal(bool suspend)
+{
+    gRenderer->SuspendScreenSaver(suspend);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+CRenderer *CRenderer::NewL()
+{
+    CRenderer *self = new (ELeave) CRenderer();
+    CleanupStack::PushL(self);
+    self->ConstructL();
+    CleanupStack::Pop(self);
+    return self;
+}
+
+CRenderer::CRenderer() : iRenderer(0), iDirectScreen(0), iScreenGc(0), iWsSession(), iWsWindowGroup(), iWsWindowGroupID(0), iWsWindow(), iWsScreen(0), iWsEventStatus(), iWsEvent(), iShowFPS(EFalse), iFPS(0), iFont(0) {}
+
+CRenderer::~CRenderer()
+{
+    delete iRenderer;
+    iRenderer = 0;
+}
+
+void CRenderer::ConstructL()
+{
+    TInt error = KErrNone;
+
+    error = iWsSession.Connect();
+    if (error != KErrNone) {
+        SDL_Log("Failed to connect to window server: %d", error);
+        User::Leave(error);
+    }
+
+    iWsScreen = new (ELeave) CWsScreenDevice(iWsSession);
+    error = iWsScreen->Construct();
+    if (error != KErrNone) {
+        SDL_Log("Failed to construct screen device: %d", error);
+        User::Leave(error);
+    }
+
+    iWsWindowGroup = RWindowGroup(iWsSession);
+    error = iWsWindowGroup.Construct(WindowClientHandle);
+    if (error != KErrNone) {
+        SDL_Log("Failed to construct window group: %d", error);
+        User::Leave(error);
+    }
+    iWsWindowGroup.SetOrdinalPosition(0);
+
+    RProcess thisProcess;
+    TParse exeName;
+    exeName.Set(thisProcess.FileName(), NULL, NULL);
+    TBuf<32> winGroupName;
+    winGroupName.Append(0);
+    winGroupName.Append(0);
+    winGroupName.Append(0); // UID
+    winGroupName.Append(0);
+    winGroupName.Append(exeName.Name()); // Caption
+    winGroupName.Append(0);
+    winGroupName.Append(0); // DOC name
+    iWsWindowGroup.SetName(winGroupName);
+
+    iWsWindow = RWindow(iWsSession);
+    error = iWsWindow.Construct(iWsWindowGroup, WindowClientHandle - 1);
+    if (error != KErrNone) {
+        SDL_Log("Failed to construct window: %d", error);
+        User::Leave(error);
+    }
+    iWsWindow.SetBackgroundColor(KRgbWhite);
+    iWsWindow.SetRequiredDisplayMode(EColor4K);
+    iWsWindow.Activate();
+    iWsWindow.SetSize(iWsScreen->SizeInPixels());
+    iWsWindow.SetVisible(ETrue);
+
+    iWsWindowGroupID = iWsWindowGroup.Identifier();
+
+    TRAPD(errc, iRenderer = iRenderer->NewL());
+    if (errc != KErrNone) {
+        SDL_Log("Failed to create renderer: %d", errc);
+        return;
+    }
+
+    iDirectScreen = CDirectScreenAccess::NewL(
+        iWsSession,
+        *(iWsScreen),
+        iWsWindow, *this);
+
+    // Select font.
+    TFontSpec fontSpec(_L("LatinBold12"), 12);
+    TInt errd = iWsScreen->GetNearestFontInTwips((CFont *&)iFont, fontSpec);
+    if (errd != KErrNone) {
+        SDL_Log("Failed to get font: %d", errd);
+        return;
+    }
+
+    // Activate events.
+    iWsEventStatus = KRequestPending;
+    iWsSession.EventReady(&iWsEventStatus);
+
+    DisableKeyBlocking();
+
+    iIsFocused = ETrue;
+    iShowFPS = EFalse;
+    iSuspendScreenSaver = EFalse;
+
+    if (!iDirectScreen->IsActive()) {
+        TRAPD(err, iDirectScreen->StartL());
+        if (KErrNone != err) {
+            return;
+        }
+        iDirectScreen->ScreenDevice()->SetAutoUpdate(ETrue);
+    }
+}
+
+void CRenderer::Restart(RDirectScreenAccess::TTerminationReasons aReason)
+{
+    if (!iDirectScreen->IsActive()) {
+        TRAPD(err, iDirectScreen->StartL());
+        if (KErrNone != err) {
+            return;
+        }
+        iDirectScreen->ScreenDevice()->SetAutoUpdate(ETrue);
+    }
+}
+
+void CRenderer::AbortNow(RDirectScreenAccess::TTerminationReasons aReason)
+{
+    if (iDirectScreen->IsActive()) {
+        iDirectScreen->Cancel();
+    }
+}
+
+void CRenderer::Clear(TUint32 iColor)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        iRenderer->Gc()->SetBrushColor(iColor);
+        iRenderer->Gc()->Clear();
+    }
+}
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+Uint32 NGAGE_ConvertColor(float r, float g, float b, float a, float color_scale)
+{
+    TFixed ff = 255 << 16; // 255.f
+
+    TFixed scalef = Real2Fix(color_scale);
+    TFixed rf = Real2Fix(r);
+    TFixed gf = Real2Fix(g);
+    TFixed bf = Real2Fix(b);
+    TFixed af = Real2Fix(a);
+
+    rf = FixMul(rf, scalef);
+    gf = FixMul(gf, scalef);
+    bf = FixMul(bf, scalef);
+
+    rf = SDL_clamp(rf, 0, ff);
+    gf = SDL_clamp(gf, 0, ff);
+    bf = SDL_clamp(bf, 0, ff);
+    af = SDL_clamp(af, 0, ff);
+
+    rf = FixMul(rf, ff) >> 16;
+    gf = FixMul(gf, ff) >> 16;
+    bf = FixMul(bf, ff) >> 16;
+    af = FixMul(af, ff) >> 16;
+
+    return (af << 24) | (bf << 16) | (gf << 8) | rf;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *srcrect, const SDL_Rect *dstrect)
+{
+    if (!texture) {
+        return false;
+    }
+
+    NGAGE_TextureData *phdata = (NGAGE_TextureData *)texture->internal;
+    if (!phdata) {
+        return false;
+    }
+
+    SDL_FColor *c = &texture->color;
+    int w = phdata->surface->w;
+    int h = phdata->surface->h;
+    int pitch = phdata->surface->pitch;
+    void *source = phdata->surface->pixels;
+    void *dest;
+
+    if (!source) {
+        return false;
+    }
+
+    void *pixel_buffer_a = SDL_calloc(1, pitch * h);
+    if (!pixel_buffer_a) {
+        return false;
+    }
+    dest = pixel_buffer_a;
+
+    void *pixel_buffer_b = SDL_calloc(1, pitch * h);
+    if (!pixel_buffer_b) {
+        SDL_free(pixel_buffer_a);
+        return false;
+    }
+
+    if (c->a != 1.f || c->r != 1.f || c->g != 1.f || c->b != 1.f) {
+        ApplyColorMod(dest, source, pitch, w, h, texture->color);
+
+        source = dest;
+    }
+
+    float sx;
+    float sy;
+    SDL_GetRenderScale(renderer, &sx, &sy);
+
+    if (sx != 1.f || sy != 1.f) {
+        TFixed scale_x = Real2Fix(sx);
+        TFixed scale_y = Real2Fix(sy);
+        TFixed center_x = Int2Fix(w / 2);
+        TFixed center_y = Int2Fix(h / 2);
+
+        dest == pixel_buffer_a ? dest = pixel_buffer_b : dest = pixel_buffer_a;
+
+        ApplyScale(dest, source, pitch, w, h, center_x, center_y, scale_x, scale_y);
+
+        source = dest;
+    }
+
+    Mem::Copy(phdata->bitmap->DataAddress(), source, pitch * h);
+    SDL_free(pixel_buffer_a);
+    SDL_free(pixel_buffer_b);
+
+    if (phdata->bitmap) {
+        TRect aSource(TPoint(srcrect->x, srcrect->y), TSize(srcrect->w, srcrect->h));
+        TPoint aDest(dstrect->x, dstrect->y);
+        iRenderer->Gc()->BitBlt(aDest, phdata->bitmap, aSource);
+    }
+
+    return true;
+}
+
+bool CRenderer::CopyEx(SDL_Renderer *renderer, SDL_Texture *texture, const NGAGE_CopyExData *copydata)
+{
+    NGAGE_TextureData *phdata = (NGAGE_TextureData *)texture->internal;
+    if (!phdata) {
+        return false;
+    }
+
+    SDL_FColor *c = &texture->color;
+    int w = phdata->surface->w;
+    int h = phdata->surface->h;
+    int pitch = phdata->surface->pitch;
+    void *source = phdata->surface->pixels;
+    void *dest;
+
+    if (!source) {
+        return false;
+    }
+
+    void *pixel_buffer_a = SDL_calloc(1, pitch * h);
+    if (!pixel_buffer_a) {
+        return false;
+    }
+    dest = pixel_buffer_a;
+
+    void *pixel_buffer_b = SDL_calloc(1, pitch * h);
+    if (!pixel_buffer_a) {
+        SDL_free(pixel_buffer_a);
+        return false;
+    }
+
+    if (copydata->flip) {
+        ApplyFlip(dest, source, pitch, w, h, copydata->flip);
+        source = dest;
+    }
+
+    if (copydata->scale_x != 1.f || copydata->scale_y != 1.f) {
+        dest == pixel_buffer_a ? dest = pixel_buffer_b : dest = pixel_buffer_a;
+        ApplyScale(dest, source, pitch, w, h, copydata->center.x, copydata->center.y, copydata->scale_x, copydata->scale_y);
+        source = dest;
+    }
+
+    if (copydata->angle) {
+        dest == pixel_buffer_a ? dest = pixel_buffer_b : dest = pixel_buffer_a;
+        ApplyRotation(dest, source, pitch, w, h, copydata->center.x, copydata->center.y, copydata->angle);
+        source = dest;
+    }
+
+    if (c->a != 1.f || c->r != 1.f || c->g != 1.f || c->b != 1.f) {
+        dest == pixel_buffer_a ? dest = pixel_buffer_b : dest = pixel_buffer_a;
+        ApplyColorMod(dest, source, pitch, w, h, texture->color);
+        source = dest;
+    }
+
+    Mem::Copy(phdata->bitmap->DataAddress(), source, pitch * h);
+    SDL_free(pixel_buffer_a);
+    SDL_free(pixel_buffer_b);
+
+    if (phdata->bitmap) {
+        TRect aSource(TPoint(copydata->srcrect.x, copydata->srcrect.y), TSize(copydata->srcrect.w, copydata->srcrect.h));
+        TPoint aDest(copydata->dstrect.x, copydata->dstrect.y);
+        iRenderer->Gc()->BitBlt(aDest, phdata->bitmap, aSource);
+    }
+
+    return true;
+}
+
+bool CRenderer::CreateTextureData(NGAGE_TextureData *aTextureData, const TInt aWidth, const TInt aHeight)
+{
+    if (!aTextureData) {
+        return false;
+    }
+
+    aTextureData->bitmap = new CFbsBitmap();
+    if (!aTextureData->bitmap) {
+        return false;
+    }
+
+    TInt error = aTextureData->bitmap->Create(TSize(aWidth, aHeight), EColor4K);
+    if (error != KErrNone) {
+        delete aTextureData->bitmap;
+        aTextureData->bitmap = NULL;
+        return false;
+    }
+
+    return true;
+}
+
+void CRenderer::DrawLines(NGAGE_Vertex *aVerts, const TInt aCount)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        TPoint *aPoints = new TPoint[aCount];
+
+        for (TInt i = 0; i < aCount; i++) {
+            aPoints[i] = TPoint(aVerts[i].x, aVerts[i].y);
+        }
+
+        TUint32 aColor = (((TUint8)aVerts->color.a << 24) |
+                          ((TUint8)aVerts->color.b << 16) |
+                          ((TUint8)aVerts->color.g << 8) |
+                          (TUint8)aVerts->color.r);
+
+        iRenderer->Gc()->SetPenColor(aColor);
+        iRenderer->Gc()->DrawPolyLineNoEndPoint(aPoints, aCount);
+
+        delete[] aPoints;
+    }
+}
+
+void CRenderer::DrawPoints(NGAGE_Vertex *aVerts, const TInt aCount)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        for (TInt i = 0; i < aCount; i++, aVerts++) {
+            TUint32 aColor = (((TUint8)aVerts->color.a << 24) |
+                              ((TUint8)aVerts->color.b << 16) |
+                              ((TUint8)aVerts->color.g << 8) |
+                              (TUint8)aVerts->color.r);
+
+            iRenderer->Gc()->SetPenColor(aColor);
+            iRenderer->Gc()->Plot(TPoint(aVerts->x, aVerts->y));
+        }
+    }
+}
+
+void CRenderer::FillRects(NGAGE_Vertex *aVerts, const TInt aCount)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        for (TInt i = 0; i < aCount; i++, aVerts++) {
+            TPoint pos(aVerts[i].x, aVerts[i].y);
+            TSize size(
+                aVerts[i + 1].x,
+                aVerts[i + 1].y);
+            TRect rect(pos, size);
+
+            TUint32 aColor = (((TUint8)aVerts->color.a << 24) |
+                              ((TUint8)aVerts->color.b << 16) |
+                              ((TUint8)aVerts->color.g << 8) |
+                              (TUint8)aVerts->color.r);
+
+            iRenderer->Gc()->SetPenColor(aColor);
+            iRenderer->Gc()->SetBrushColor(aColor);
+            iRenderer->Gc()->DrawRect(rect);
+        }
+    }
+}
+
+void CRenderer::Flip()
+{
+    if (!iRenderer) {
+        SDL_Log("iRenderer is NULL.");
+        return;
+    }
+
+    if (!iIsFocused) {
+        return;
+    }
+
+    iRenderer->Gc()->UseFont(iFont);
+
+    if (iShowFPS && iRenderer->Gc()) {
+        UpdateFPS();
+
+        TBuf<64> info;
+
+        iRenderer->Gc()->SetPenStyle(CGraphicsContext::ESolidPen);
+        iRenderer->Gc()->SetBrushStyle(CGraphicsContext::ENullBrush);
+        iRenderer->Gc()->SetPenColor(KRgbCyan);
+
+        TRect aTextRect(TPoint(3, 203 - iFont->HeightInPixels()), TSize(45, iFont->HeightInPixels() + 2));
+        iRenderer->Gc()->SetBrushStyle(CGraphicsContext::ESolidBrush);
+        iRenderer->Gc()->SetBrushColor(KRgbBlack);
+        iRenderer->Gc()->DrawRect(aTextRect);
+
+        // Draw messages.
+        info.Format(_L("FPS: %d"), iFPS);
+        iRenderer->Gc()->DrawText(info, TPoint(5, 203));
+    } else {
+        // This is a workaround that helps regulating the FPS.
+        iRenderer->Gc()->DrawText(_L(""), TPoint(0, 0));
+    }
+    iRenderer->Gc()->DiscardFont();
+    iRenderer->Flip(iDirectScreen);
+
+    // Keep the backlight on.
+    if (iSuspendScreenSaver) {
+        User::ResetInactivityTime();
+    }
+    // Suspend the current thread for a short while.
+    // Give some time to other threads and active objects.
+    User::After(0);
+}
+
+void CRenderer::SetDrawColor(TUint32 iColor)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        iRenderer->Gc()->SetPenColor(iColor);
+        iRenderer->Gc()->SetBrushColor(iColor);
+        iRenderer->Gc()->SetBrushStyle(CGraphicsContext::ESolidBrush);
+
+        TRAPD(err, iRenderer->SetCurrentColor(iColor));
+        if (err != KErrNone) {
+            return;
+        }
+    }
+}
+
+void CRenderer::SetClipRect(TInt aX, TInt aY, TInt aWidth, TInt aHeight)
+{
+    if (iRenderer && iRenderer->Gc()) {
+        TRect viewportRect(aX, aY, aX + aWidth, aY + aHeight);
+        iRenderer->Gc()->SetClippingRect(viewportRect);
+    }
+}
+
+void CRenderer::UpdateFPS()
+{
+    static TTime lastTime;
+    static TInt frameCount = 0;
+    TTime currentTime;
+    const TUint KOneSecond = 1000000; // 1s in ms.
+
+    currentTime.HomeTime();
+    ++frameCount;
+
+    TTimeIntervalMicroSeconds timeDiff = currentTime.MicroSecondsFrom(lastTime);
+
+    if (timeDiff.Int64() >= KOneSecond) {
+        // Calculate FPS.
+        iFPS = frameCount;
+
+        // Reset frame count and last time.
+        frameCount = 0;
+        lastTime = currentTime;
+    }
+}
+
+void CRenderer::SuspendScreenSaver(TBool aSuspend)
+{
+    iSuspendScreenSaver = aSuspend;
+}
+
+static SDL_Scancode ConvertScancode(int key)
+{
+    SDL_Keycode keycode;
+
+    switch (key) {
+    case EStdKeyBackspace: // Clear key
+        keycode = SDLK_BACKSPACE;
+        break;
+    case 0x31: // 1
+        keycode = SDLK_1;
+        break;
+    case 0x32: // 2
+        keycode = SDLK_2;
+        break;
+    case 0x33: // 3
+        keycode = SDLK_3;
+        break;
+    case 0x34: // 4
+        keycode = SDLK_4;
+        break;
+    case 0x35: // 5
+        keycode = SDLK_5;
+        break;
+    case 0x36: // 6
+        keycode = SDLK_6;
+        break;
+    case 0x37: // 7
+        keycode = SDLK_7;
+        break;
+    case 0x38: // 8
+        keycode = SDLK_8;
+        break;
+    case 0x39: // 9
+        keycode = SDLK_9;
+        break;
+    case 0x30: // 0
+        keycode = SDLK_0;
+        break;
+    case 0x2a: // Asterisk
+        keycode = SDLK_ASTERISK;
+        break;
+    case EStdKeyHash: // Hash
+        keycode = SDLK_HASH;
+        break;
+    case EStdKeyDevice0: // Left softkey
+        keycode = SDLK_SOFTLEFT;
+        break;
+    case EStdKeyDevice1: // Right softkey
+        keycode = SDLK_SOFTRIGHT;
+        break;
+    case EStdKeyApplication0: // Call softkey
+        keycode = SDLK_CALL;
+        break;
+    case EStdKeyApplication1: // End call softkey
+        keycode = SDLK_ENDCALL;
+        break;
+    case EStdKeyDevice3: // Middle softkey
+        keycode = SDLK_SELECT;
+        break;
+    case EStdKeyUpArrow: // Up arrow
+        keycode = SDLK_UP;
+        break;
+    case EStdKeyDownArrow: // Down arrow
+        keycode = SDLK_DOWN;
+        break;
+    case EStdKeyLeftArrow: // Left arrow
+        keycode = SDLK_LEFT;
+        break;
+    case EStdKeyRightArrow: // Right arrow
+        keycode = SDLK_RIGHT;
+        break;
+    default:
+        keycode = SDLK_UNKNOWN;
+        break;
+    }
+
+    return SDL_GetScancodeFromKey(keycode, NULL);
+}
+
+void CRenderer::HandleEvent(const TWsEvent &aWsEvent)
+{
+    Uint64 timestamp;
+
+    switch (aWsEvent.Type()) {
+    case EEventKeyDown: /* Key events */
+        timestamp = SDL_GetPerformanceCounter();
+        SDL_SendKeyboardKey(timestamp, 1, aWsEvent.Key()->iCode, ConvertScancode(aWsEvent.Key()->iScanCode), true);
+
+        if (aWsEvent.Key()->iScanCode == EStdKeyHash) {
+            if (iShowFPS) {
+                iShowFPS = EFalse;
+            } else {
+                iShowFPS = ETrue;
+            }
+        }
+
+        break;
+    case EEventKeyUp: /* Key events */
+        timestamp = SDL_GetPerformanceCounter();
+        SDL_SendKeyboardKey(timestamp, 1, aWsEvent.Key()->iCode, ConvertScancode(aWsEvent.Key()->iScanCode), false);
+
+    case EEventFocusGained:
+        DisableKeyBlocking();
+        if (!iDirectScreen->IsActive()) {
+            TRAPD(err, iDirectScreen->StartL());
+            if (KErrNone != err) {
+                return;
+            }
+            iDirectScreen->ScreenDevice()->SetAutoUpdate(ETrue);
+            iIsFocused = ETrue;
+        }
+        Flip();
+        break;
+    case EEventFocusLost:
+    {
+        if (iDirectScreen->IsActive()) {
+            iDirectScreen->Cancel();
+        }
+
+        iIsFocused = EFalse;
+        break;
+    }
+    default:
+        break;
+    }
+}
+
+void CRenderer::DisableKeyBlocking()
+{
+    TRawEvent aEvent;
+
+    aEvent.Set((TRawEvent::TType) /*EDisableKeyBlock*/ 51);
+    iWsSession.SimulateRawEvent(aEvent);
+}
+
+void CRenderer::PumpEvents()
+{
+    while (iWsEventStatus != KRequestPending) {
+        iWsSession.GetEvent(iWsEvent);
+        HandleEvent(iWsEvent);
+        iWsEventStatus = KRequestPending;
+        iWsSession.EventReady(&iWsEventStatus);
+    }
+}
+
+#endif // SDL_VIDEO_RENDER_NGAGE

+ 105 - 0
src/render/ngage/SDL_render_ngage_c.h

@@ -0,0 +1,105 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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 ngage_video_render_ngage_c_h
+#define ngage_video_render_ngage_c_h
+
+#define NGAGE_SCREEN_WIDTH  176
+#define NGAGE_SCREEN_HEIGHT 208
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "../SDL_sysrender.h"
+
+typedef struct NGAGE_RendererData
+{
+    SDL_Rect *viewport;
+
+} NGAGE_RendererData;
+
+typedef struct NGAGE_Vertex
+{
+    int x;
+    int y;
+
+    struct
+    {
+        Uint8 a;
+        Uint8 r;
+        Uint8 g;
+        Uint8 b;
+
+    } color;
+
+} NGAGE_Vertex;
+
+typedef struct CFbsBitmap CFbsBitmap;
+
+typedef struct NGAGE_TextureData
+{
+    CFbsBitmap *bitmap;
+    SDL_Surface *surface;
+
+} NGAGE_TextureData;
+
+typedef struct NGAGE_CopyExData
+{
+    SDL_Rect srcrect;
+    SDL_Rect dstrect;
+
+    int angle;
+
+    struct
+    {
+        int x;
+        int y;
+
+    } center;
+
+    SDL_FlipMode flip;
+
+    int scale_x;
+    int scale_y;
+
+} NGAGE_CopyExData;
+
+void NGAGE_Clear(const Uint32 color);
+Uint32 NGAGE_ConvertColor(float r, float g, float b, float a, float color_scale);
+bool NGAGE_Copy(SDL_Renderer *renderer, SDL_Texture *texture, SDL_Rect *srcrect, SDL_Rect *dstrect);
+bool NGAGE_CopyEx(SDL_Renderer *renderer, SDL_Texture *texture, NGAGE_CopyExData *copydata);
+bool NGAGE_CreateTextureData(NGAGE_TextureData *data, const int width, const int height);
+void NGAGE_DestroyTextureData(NGAGE_TextureData *data);
+void NGAGE_DrawLines(NGAGE_Vertex *verts, const int count);
+void NGAGE_DrawPoints(NGAGE_Vertex *verts, const int count);
+void NGAGE_FillRects(NGAGE_Vertex *verts, const int count);
+void NGAGE_Flip(void);
+void NGAGE_SetClipRect(const SDL_Rect *rect);
+void NGAGE_SetDrawColor(const Uint32 color);
+void NGAGE_PumpEventsInternal(void);
+void NGAGE_SuspendScreenSaverInternal(bool suspend);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ngage_video_render_ngage_c_h

+ 91 - 0
src/render/ngage/SDL_render_ngage_c.hpp

@@ -0,0 +1,91 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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 ngage_video_render_ngage_c_hpp
+#define ngage_video_render_ngage_c_hpp
+
+#include "SDL_render_ngage_c.h"
+#include <NRenderer.h>
+#include <e32std.h>
+#include <w32std.h>
+
+class CRenderer : public MDirectScreenAccess
+{
+  public:
+    static CRenderer *NewL();
+    virtual ~CRenderer();
+
+    // Rendering functions.
+    void Clear(TUint32 iColor);
+    bool Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rect *srcrect, const SDL_Rect *dstrect);
+    bool CopyEx(SDL_Renderer *renderer, SDL_Texture *texture, const NGAGE_CopyExData *copydata);
+    bool CreateTextureData(NGAGE_TextureData *aTextureData, const TInt aWidth, const TInt aHeight);
+    void DrawLines(NGAGE_Vertex *aVerts, const TInt aCount);
+    void DrawPoints(NGAGE_Vertex *aVerts, const TInt aCount);
+    void FillRects(NGAGE_Vertex *aVerts, const TInt aCount);
+    void Flip();
+    void SetDrawColor(TUint32 iColor);
+    void SetClipRect(TInt aX, TInt aY, TInt aWidth, TInt aHeight);
+    void UpdateFPS();
+    void SuspendScreenSaver(TBool aSuspend);
+
+    // Event handling.
+    void DisableKeyBlocking();
+    void HandleEvent(const TWsEvent &aWsEvent);
+    void PumpEvents();
+
+  private:
+    CRenderer();
+    void ConstructL(void);
+
+    // BackBuffer.
+    CNRenderer *iRenderer;
+
+    // Direct screen access.
+    CDirectScreenAccess *iDirectScreen;
+    CFbsBitGc *iScreenGc;
+    TBool iIsFocused;
+
+    // Window server session.
+    RWsSession iWsSession;
+    RWindowGroup iWsWindowGroup;
+    TInt iWsWindowGroupID;
+    RWindow iWsWindow;
+    CWsScreenDevice *iWsScreen;
+
+    // Event handling.
+    TRequestStatus iWsEventStatus;
+    TWsEvent iWsEvent;
+
+    // MDirectScreenAccess functions.
+    void Restart(RDirectScreenAccess::TTerminationReasons aReason);
+    void AbortNow(RDirectScreenAccess::TTerminationReasons aReason);
+
+    // Frame per second.
+    TBool iShowFPS;
+    TUint iFPS;
+    const CFont *iFont;
+
+    // Screen saver.
+    TBool iSuspendScreenSaver;
+};
+
+#endif // ngage_video_render_ngage_c_hpp

+ 152 - 0
src/render/ngage/SDL_render_ops.cpp

@@ -0,0 +1,152 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#include <3dtypes.h>
+#include "SDL_render_ops.hpp"
+
+void ApplyColorMod(void *dest, void *source, int pitch, int width, int height, SDL_FColor color)
+{
+    TUint16 *src_pixels = static_cast<TUint16 *>(source);
+    TUint16 *dst_pixels = static_cast<TUint16 *>(dest);
+
+    TFixed rf = Real2Fix(color.r);
+    TFixed gf = Real2Fix(color.g);
+    TFixed bf = Real2Fix(color.b);
+
+    for (int y = 0; y < height; ++y)
+    {
+        for (int x = 0; x < width; ++x)
+        {
+            TUint16 pixel = src_pixels[y * pitch / 2 + x];
+            TUint8 r = (pixel & 0xF800) >> 8;
+            TUint8 g = (pixel & 0x07E0) >> 3;
+            TUint8 b = (pixel & 0x001F) << 3;
+            r = FixMul(r, rf);
+            g = FixMul(g, gf);
+            b = FixMul(b, bf);
+            dst_pixels[y * pitch / 2 + x] = (r << 8) | (g << 3) | (b >> 3);
+        }
+    }
+}
+
+void ApplyFlip(void* dest, void* source, int pitch, int width, int height, SDL_FlipMode flip)
+{
+    TUint16* src_pixels = static_cast<TUint16*>(source);
+    TUint16* dst_pixels = static_cast<TUint16*>(dest);
+
+    for (int y = 0; y < height; ++y)
+    {
+        for (int x = 0; x < width; ++x)
+        {
+            int src_x = x;
+            int src_y = y;
+
+            if (flip & SDL_FLIP_HORIZONTAL)
+            {
+                src_x = width - 1 - x;
+            }
+
+            if (flip & SDL_FLIP_VERTICAL)
+            {
+                src_y = height - 1 - y;
+            }
+
+            dst_pixels[y * pitch / 2 + x] = src_pixels[src_y * pitch / 2 + src_x];
+        }
+    }
+}
+
+void ApplyRotation(void* dest, void* source, int pitch, int width, int height, TFixed center_x, TFixed center_y, TFixed angle)
+{
+    TUint16* src_pixels = static_cast<TUint16*>(source);
+    TUint16* dst_pixels = static_cast<TUint16*>(dest);
+
+    TFixed cos_angle = 0;
+    TFixed sin_angle = 0;
+
+    if (angle != 0)
+    {
+        FixSinCos(angle, sin_angle, cos_angle);
+    }
+
+    for (int y = 0; y < height; ++y)
+    {
+        for (int x = 0; x < width; ++x)
+        {
+            // Translate point to origin.
+            TFixed translated_x = Int2Fix(x) - center_x;
+            TFixed translated_y = Int2Fix(y) - center_y;
+
+            // Rotate point (clockwise).
+            TFixed rotated_x = FixMul(translated_x, cos_angle) + FixMul(translated_y, sin_angle);
+            TFixed rotated_y = FixMul(translated_y, cos_angle) - FixMul(translated_x, sin_angle);
+
+            // Translate point back.
+            int final_x = Fix2Int(rotated_x + center_x);
+            int final_y = Fix2Int(rotated_y + center_y);
+
+            // Check bounds.
+            if (final_x >= 0 && final_x < width && final_y >= 0 && final_y < height)
+            {
+                dst_pixels[y * pitch / 2 + x] = src_pixels[final_y * pitch / 2 + final_x];
+            }
+            else
+            {
+                dst_pixels[y * pitch / 2 + x] = 0;
+            }
+        }
+    }
+}
+
+void ApplyScale(void* dest, void* source, int pitch, int width, int height, TFixed center_x, TFixed center_y, TFixed scale_x, TFixed scale_y)
+{
+    TUint16* src_pixels = static_cast<TUint16*>(source);
+    TUint16* dst_pixels = static_cast<TUint16*>(dest);
+
+    for (int y = 0; y < height; ++y)
+    {
+        for (int x = 0; x < width; ++x)
+        {
+            // Translate point to origin.
+            TFixed translated_x = Int2Fix(x) - center_x;
+            TFixed translated_y = Int2Fix(y) - center_y;
+
+            // Scale point.
+            TFixed scaled_x = FixDiv(translated_x, scale_x);
+            TFixed scaled_y = FixDiv(translated_y, scale_y);
+
+            // Translate point back.
+            int final_x = Fix2Int(scaled_x + center_x);
+            int final_y = Fix2Int(scaled_y + center_y);
+
+            // Check bounds.
+            if (final_x >= 0 && final_x < width && final_y >= 0 && final_y < height)
+            {
+                dst_pixels[y * pitch / 2 + x] = src_pixels[final_y * pitch / 2 + final_x];
+            }
+            else
+            {
+                dst_pixels[y * pitch / 2 + x] = 0;
+            }
+        }
+    }
+}

+ 32 - 0
src/render/ngage/SDL_render_ops.hpp

@@ -0,0 +1,32 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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 ngage_video_render_ops_hpp
+#define ngage_video_render_ops_hpp
+
+#include <3dtypes.h>
+
+void ApplyColorMod(void* dest, void* source, int pitch, int width, int height, SDL_FColor color);
+void ApplyFlip(void* dest, void* source, int pitch, int width, int height, SDL_FlipMode flip);
+void ApplyRotation(void* dest, void* source, int pitch, int width, int height, TFixed center_x, TFixed center_y, TFixed angle);
+void ApplyScale(void* dest, void* source, int pitch, int width, int height, TFixed center_x, TFixed center_y, TFixed scale_x, TFixed scale_y);
+
+#endif // ngage_video_render_ops_hpp

+ 2 - 0
src/stdlib/SDL_string.c

@@ -34,6 +34,8 @@
 
 #if defined(__SIZEOF_WCHAR_T__)
 #define SDL_SIZEOF_WCHAR_T __SIZEOF_WCHAR_T__
+#elif defined(SDL_PLATFORM_NGAGE)
+#define SDL_SIZEOF_WCHAR_T 2
 #elif defined(SDL_PLATFORM_WINDOWS)
 #define SDL_SIZEOF_WCHAR_T 2
 #else  // assume everything else is UTF-32 (add more tests if compiler-assert fails below!)

+ 2 - 1
src/stdlib/SDL_vacopy.h

@@ -20,11 +20,12 @@
 */
 
 // Do our best to make sure va_copy is working
-#if defined(_MSC_VER) && _MSC_VER <= 1800
+#if (defined(_MSC_VER) && _MSC_VER <= 1800) || defined(__SYMBIAN32__)
 // Visual Studio 2013 tries to link with _vacopy in the C runtime. Newer versions do an inline assignment
 #undef va_copy
 #define va_copy(dst, src) dst = src
 
 #elif defined(__GNUC__) && (__GNUC__ < 3)
 #define va_copy(dst, src) __va_copy(dst, src)
+
 #endif

+ 184 - 0
src/time/ngage/SDL_systime.cpp

@@ -0,0 +1,184 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <[email protected]>
+
+  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_TIME_NGAGE
+
+#include <bautils.h>
+#include <e32base.h>
+#include <e32cons.h>
+#include <e32std.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+static TTime UnixEpoch();
+
+void SDL_GetSystemTimeLocalePreferences(SDL_DateFormat *df, SDL_TimeFormat *tf)
+{
+    TLanguage language = User::Language();
+
+    switch (language) {
+    case ELangFrench:
+    case ELangSwissFrench:
+    case ELangBelgianFrench:
+    case ELangInternationalFrench:
+    case ELangGerman:
+    case ELangSwissGerman:
+    case ELangAustrian:
+    case ELangSpanish:
+    case ELangInternationalSpanish:
+    case ELangLatinAmericanSpanish:
+    case ELangItalian:
+    case ELangSwissItalian:
+    case ELangSwedish:
+    case ELangFinlandSwedish:
+    case ELangDanish:
+    case ELangNorwegian:
+    case ELangNorwegianNynorsk:
+    case ELangFinnish:
+    case ELangPortuguese:
+    case ELangBrazilianPortuguese:
+    case ELangTurkish:
+    case ELangCyprusTurkish:
+    case ELangIcelandic:
+    case ELangRussian:
+    case ELangHungarian:
+    case ELangDutch:
+    case ELangBelgianFlemish:
+    case ELangCzech:
+    case ELangSlovak:
+    case ELangPolish:
+    case ELangSlovenian:
+    case ELangTaiwanChinese:
+    case ELangHongKongChinese:
+    case ELangPrcChinese:
+    case ELangJapanese:
+    case ELangThai:
+    case ELangAfrikaans:
+    case ELangAlbanian:
+    case ELangAmharic:
+    case ELangArabic:
+    case ELangArmenian:
+    case ELangAzerbaijani:
+    case ELangBelarussian:
+    case ELangBengali:
+    case ELangBulgarian:
+    case ELangBurmese:
+    case ELangCatalan:
+    case ELangCroatian:
+    case ELangEstonian:
+    case ELangFarsi:
+    case ELangScotsGaelic:
+    case ELangGeorgian:
+    case ELangGreek:
+    case ELangCyprusGreek:
+    case ELangGujarati:
+    case ELangHebrew:
+    case ELangHindi:
+    case ELangIndonesian:
+    case ELangIrish:
+    case ELangKannada:
+    case ELangKazakh:
+    case ELangKhmer:
+    case ELangKorean:
+    case ELangLao:
+    case ELangLatvian:
+    case ELangLithuanian:
+    case ELangMacedonian:
+    case ELangMalay:
+    case ELangMalayalam:
+    case ELangMarathi:
+    case ELangMoldavian:
+    case ELangMongolian:
+    case ELangPunjabi:
+    case ELangRomanian:
+    case ELangSerbian:
+    case ELangSinhalese:
+    case ELangSomali:
+    case ELangSwahili:
+    case ELangTajik:
+    case ELangTamil:
+    case ELangTelugu:
+    case ELangTibetan:
+    case ELangTigrinya:
+    case ELangTurkmen:
+    case ELangUkrainian:
+    case ELangUrdu:
+    case ELangUzbek:
+    case ELangVietnamese:
+    case ELangWelsh:
+    case ELangZulu:
+        *df = SDL_DATE_FORMAT_DDMMYYYY;
+        *tf = SDL_TIME_FORMAT_24HR;
+        break;
+    case ELangAmerican:
+    case ELangCanadianEnglish:
+    case ELangInternationalEnglish:
+    case ELangSouthAfricanEnglish:
+    case ELangAustralian:
+    case ELangNewZealand:
+    case ELangCanadianFrench:
+        *df = SDL_DATE_FORMAT_MMDDYYYY;
+        *tf = SDL_TIME_FORMAT_12HR;
+        break;
+    case ELangEnglish:
+    case ELangOther:
+    default:
+        *df = SDL_DATE_FORMAT_DDMMYYYY;
+        *tf = SDL_TIME_FORMAT_24HR;
+        break;
+    }
+}
+
+bool SDL_GetCurrentTime(SDL_Time *ticks)
+{
+    if (!ticks) {
+        return SDL_InvalidParamError("ticks");
+    }
+
+    TTime now;
+    now.UniversalTime();
+
+    TTimeIntervalMicroSeconds interval = now.MicroSecondsFrom(UnixEpoch());
+    TInt64 interval_ns = interval.Int64() * 1000;
+    Uint32 ns_low = interval_ns.Low();
+    Uint32 ns_high = interval_ns.High();
+
+    *ticks = ((Uint64)ns_high << 32) | ns_low;
+
+    return true;
+}
+
+static TTime UnixEpoch()
+{
+    _LIT(KUnixEpoch, "19700101:000000.000000");
+    TTime epochTime;
+    epochTime.Set(KUnixEpoch);
+    return epochTime;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // SDL_TIME_NGAGE

+ 47 - 0
src/timer/ngage/SDL_systimer.cpp

@@ -0,0 +1,47 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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"
+
+#include <e32hal.h>
+#include <e32std.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+Uint64 SDL_GetPerformanceCounter(void)
+{
+    return (Uint64)User::TickCount();
+}
+
+Uint64 SDL_GetPerformanceFrequency(void)
+{
+    return (Uint64)1000000u;
+}
+
+void SDL_SYS_DelayNS(Uint64 ns)
+{
+    User::After(SDL_NS_TO_US(ns));
+}
+
+#ifdef __cplusplus
+}
+#endif

+ 1 - 0
src/video/SDL_sysvideo.h

@@ -530,6 +530,7 @@ extern VideoBootStrap PSP_bootstrap;
 extern VideoBootStrap VITA_bootstrap;
 extern VideoBootStrap RISCOS_bootstrap;
 extern VideoBootStrap N3DS_bootstrap;
+extern VideoBootStrap NGAGE_bootstrap;
 extern VideoBootStrap RPI_bootstrap;
 extern VideoBootStrap KMSDRM_bootstrap;
 extern VideoBootStrap DUMMY_bootstrap;

+ 3 - 0
src/video/SDL_video.c

@@ -119,6 +119,9 @@ static VideoBootStrap *bootstrap[] = {
 #ifdef SDL_VIDEO_DRIVER_N3DS
     &N3DS_bootstrap,
 #endif
+#ifdef SDL_VIDEO_DRIVER_NGAGE
+    &NGAGE_bootstrap,
+#endif
 #ifdef SDL_VIDEO_DRIVER_KMSDRM
     &KMSDRM_bootstrap,
 #endif

+ 175 - 0
src/video/ngage/SDL_ngagevideo.c

@@ -0,0 +1,175 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_sysvideo.h"
+
+#ifdef SDL_VIDEO_DRIVER_NGAGE
+
+#include "SDL_ngagevideo.h"
+
+#define NGAGE_VIDEO_DRIVER_NAME "N-Gage"
+
+static void NGAGE_DeleteDevice(SDL_VideoDevice *device);
+static bool NGAGE_VideoInit(SDL_VideoDevice *device);
+static void NGAGE_VideoQuit(SDL_VideoDevice *device);
+
+static bool NGAGE_GetDisplayBounds(SDL_VideoDevice *device, SDL_VideoDisplay *display, SDL_Rect *rect);
+static bool NGAGE_GetDisplayModes(SDL_VideoDevice *device, SDL_VideoDisplay *display);
+
+static void NGAGE_PumpEvents(SDL_VideoDevice *device);
+
+static bool NGAGE_SuspendScreenSaver(SDL_VideoDevice *device);
+
+static SDL_VideoDevice *NGAGE_CreateDevice(void)
+{
+    SDL_VideoDevice *device;
+    SDL_VideoData *phdata;
+
+    // Initialize all variables that we clean on shutdown.
+    device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice));
+    if (!device) {
+        SDL_OutOfMemory();
+        return (SDL_VideoDevice *)0;
+    }
+
+    // Initialize internal N-Gage specific data.
+    phdata = (SDL_VideoData *)SDL_calloc(1, sizeof(SDL_VideoData));
+    if (!phdata) {
+        SDL_OutOfMemory();
+        SDL_free(device);
+        return (SDL_VideoDevice *)0;
+    }
+
+    device->internal = phdata;
+
+    device->name = "Nokia N-Gage";
+
+    device->VideoInit = NGAGE_VideoInit;
+    device->VideoQuit = NGAGE_VideoQuit;
+
+    device->GetDisplayBounds = NGAGE_GetDisplayBounds;
+    device->GetDisplayModes = NGAGE_GetDisplayModes;
+
+    device->PumpEvents = NGAGE_PumpEvents;
+
+    device->SuspendScreenSaver = NGAGE_SuspendScreenSaver;
+
+    device->free = NGAGE_DeleteDevice;
+
+    device->device_caps = VIDEO_DEVICE_CAPS_FULLSCREEN_ONLY;
+
+    return device;
+}
+
+VideoBootStrap NGAGE_bootstrap = {
+    NGAGE_VIDEO_DRIVER_NAME,
+    "N-Gage Video Driver",
+    NGAGE_CreateDevice,
+    0
+};
+
+static void NGAGE_DeleteDevice(SDL_VideoDevice *device)
+{
+    SDL_free(device->internal);
+    SDL_free(device);
+}
+
+static bool NGAGE_VideoInit(SDL_VideoDevice *device)
+{
+    SDL_VideoData *phdata = (SDL_VideoData *)device->internal;
+
+    if (!phdata) {
+        return false;
+    }
+
+    SDL_zero(phdata->mode);
+    SDL_zero(phdata->display);
+
+    phdata->mode.w = 176;
+    phdata->mode.h = 208;
+    phdata->mode.refresh_rate = 60.0f;
+    phdata->mode.format = SDL_PIXELFORMAT_ARGB4444;
+
+    phdata->display.name = "N-Gage";
+    phdata->display.desktop_mode = phdata->mode;
+
+    if (SDL_AddVideoDisplay(&phdata->display, false) == 0) {
+        return false;
+    }
+
+    return true;
+}
+
+static void NGAGE_VideoQuit(SDL_VideoDevice *device)
+{
+    SDL_VideoData *phdata = (SDL_VideoData *)device->internal;
+
+    if (phdata) {
+        SDL_zero(phdata->mode);
+        SDL_zero(phdata->display);
+    }
+}
+
+static bool NGAGE_GetDisplayBounds(SDL_VideoDevice *device, SDL_VideoDisplay *display, SDL_Rect *rect)
+{
+    if (!display) {
+        return false;
+    }
+
+    rect->x = 0;
+    rect->y = 0;
+    rect->w = display->current_mode->w;
+    rect->h = display->current_mode->h;
+
+    return true;
+}
+
+static bool NGAGE_GetDisplayModes(SDL_VideoDevice *device, SDL_VideoDisplay *display)
+{
+    SDL_VideoData *phdata = (SDL_VideoData *)device->internal;
+    SDL_DisplayMode mode;
+
+    SDL_zero(mode);
+    mode.w = phdata->mode.w;
+    mode.h = phdata->mode.h;
+    mode.refresh_rate = phdata->mode.refresh_rate;
+    mode.format = phdata->mode.format;
+
+    if (!SDL_AddFullscreenDisplayMode(display, &mode)) {
+        return false;
+    }
+
+    return true;
+}
+
+#include "../../render/ngage/SDL_render_ngage_c.h"
+
+static void NGAGE_PumpEvents(SDL_VideoDevice *device)
+{
+    NGAGE_PumpEventsInternal();
+}
+
+static bool NGAGE_SuspendScreenSaver(SDL_VideoDevice *device)
+{
+    NGAGE_SuspendScreenSaverInternal(device->suspend_screensaver);
+    return true;
+}
+
+#endif // SDL_VIDEO_DRIVER_NGAGE

+ 39 - 0
src/video/ngage/SDL_ngagevideo.h

@@ -0,0 +1,39 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <[email protected]>
+
+  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_VIDEO_DRIVER_NGAGE
+
+#include "../SDL_sysvideo.h"
+
+#ifndef _SDL_ngagevideo_h
+#define _SDL_ngagevideo_h
+
+typedef struct SDL_VideoData
+{
+    SDL_DisplayMode mode;
+    SDL_VideoDisplay display;
+
+} SDL_VideoData;
+
+#endif // _SDL_ngagevideo_h
+
+#endif // SDL_VIDEO_DRIVER_NGAGE

+ 25 - 11
test/CMakeLists.txt

@@ -32,8 +32,6 @@ if(NOT (MSVC AND SDL_CPU_ARM64))
     find_package(OpenGL)
 endif()
 
-set(SDL_TEST_EXECUTABLES)
-
 add_library(sdltests_utils OBJECT
     testutils.c
 )
@@ -101,7 +99,7 @@ define_property(TARGET PROPERTY SDL_NONINTERACTIVE BRIEF_DOCS "If true, target i
 define_property(TARGET PROPERTY SDL_NONINTERACTIVE_ARGUMENTS BRIEF_DOCS "Argument(s) to run executable in non-interactive mode." FULL_DOCS "Argument(s) to run executable in non-interactive mode.")
 define_property(TARGET PROPERTY SDL_NONINTERACTIVE_TIMEOUT BRIEF_DOCS "Timeout for noninteractive executable." FULL_DOCS "Timeout for noninteractive executable.")
 
-macro(add_sdl_test_executable TARGET)
+function(add_sdl_test_executable TARGET)
     cmake_parse_arguments(AST "BUILD_DEPENDENT;NONINTERACTIVE;NEEDS_RESOURCES;TESTUTILS;THREADS;NO_C90;MAIN_CALLBACKS;NOTRACKMEM" "" "DEPENDS;DISABLE_THREADS_ARGS;NONINTERACTIVE_TIMEOUT;NONINTERACTIVE_ARGS;INSTALLED_ARGS;SOURCES" ${ARGN})
     if(AST_UNPARSED_ARGUMENTS)
         message(FATAL_ERROR "Unknown argument(s): ${AST_UNPARSED_ARGUMENTS}")
@@ -133,7 +131,8 @@ macro(add_sdl_test_executable TARGET)
         add_dependencies(${TARGET} ${AST_DEPENDS})
     endif()
 
-    list(APPEND SDL_TEST_EXECUTABLES ${TARGET})
+    set_propertY(TARGET ${TARGET} PROPERTY SDL_INSTALL "1")
+    set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY SDL_TEST_EXECUTABLES "${TARGET}")
     set_property(TARGET ${TARGET} PROPERTY SDL_NOTRACKMEM ${AST_NOTRACKMEM})
     if(AST_NONINTERACTIVE)
         set_property(TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE 1)
@@ -192,6 +191,14 @@ macro(add_sdl_test_executable TARGET)
         target_link_options(${TARGET} PRIVATE "SHELL:--pre-js ${CMAKE_CURRENT_SOURCE_DIR}/emscripten/pre.js")
         target_link_options(${TARGET} PRIVATE "-sEXIT_RUNTIME=1")
         set_property(TARGET ${TARGET} APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/emscripten/pre.js")
+    elseif(NGAGE)
+        string(MD5 TARGET_MD5 "${TARGET}")
+        string(SUBSTRING "${TARGET_MD5}" 0 8 TARGET_MD5_8)
+        target_link_options(${TARGET} PRIVATE "SHELL:-s UID3=0x${TARGET_MD5_8}")
+        if(NOT AST_MAIN_CALLBACKS)
+            set_property(TARGET ${TARGET} PROPERTY "EXCLUDE_FROM_ALL" "1")
+            set_propertY(TARGET ${TARGET} PROPERTY SDL_INSTALL "0")
+        endif()
     endif()
 
     if(OPENGL_FOUND)
@@ -200,10 +207,10 @@ macro(add_sdl_test_executable TARGET)
 
     # FIXME: only add "${SDL3_BINARY_DIR}/include-config-$<LOWER_CASE:$<CONFIG>>" + include paths of external dependencies
     target_include_directories(${TARGET} PRIVATE "$<TARGET_PROPERTY:SDL3::${sdl_name_component},INCLUDE_DIRECTORIES>")
-endmacro()
+endfunction()
 
-check_include_file(signal.h HAVE_SIGNAL_H)
-if(HAVE_SIGNAL_H)
+check_include_file(signal.h LIBC_HAS_SIGNAL_H)
+if(LIBC_HAS_SIGNAL_H)
     add_definitions(-DHAVE_SIGNAL_H)
 endif()
 
@@ -425,6 +432,8 @@ add_sdl_test_executable(testprocess
 add_sdl_test_executable(childprocess SOURCES childprocess.c)
 add_dependencies(testprocess childprocess)
 
+get_property(SDL_TEST_EXECUTABLES DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY SDL_TEST_EXECUTABLES)
+
 if (HAVE_WAYLAND)
     # Set the GENERATED property on the protocol file, since it is first created at build time
     set_property(SOURCE ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c PROPERTY GENERATED 1)
@@ -654,10 +663,15 @@ if(SDL_INSTALL_TESTS)
             DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/SDL3
         )
     else()
-        install(
-            TARGETS ${SDL_TEST_EXECUTABLES}
-            DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/SDL3
-        )
+        foreach(test IN LISTS SDL_TEST_EXECUTABLES)
+            get_property(install_target TARGET ${test} PROPERTY "SDL_INSTALL")
+            if(install_target)
+                install(
+                    TARGETS ${test}
+                    DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/installed-tests/SDL3
+                )
+            endif()
+        endforeach()
     endif()
     if(MSVC)
         foreach(test IN LISTS SDL_TEST_EXECUTABLES)