Parcourir la source

harmonyos: Port SDL3 to HarmonyOS/OpenHarmony.

This is my port of SDL3 to HarmonyOS/OpenHarmony. It has been tested on real
hardware from Huawei, and commercial games are being prepped to ship with it
already.

There is _heavy_ documentation in docs/README-harmonyos.md, including how to
set up a development environment and project, build, and debug, for the
completely uninitiated.

This work was sponsored by Outfit7: https://outfit7.com/

Fixes #9837.
Ryan C. Gordon il y a 3 mois
Parent
commit
a24429c192
100 fichiers modifiés avec 6441 ajouts et 51 suppressions
  1. 10 0
      .gitignore
  2. 167 2
      CMakeLists.txt
  3. 209 0
      build-scripts/harmonyos-tool.sh
  4. 188 0
      cmake/PreseedOpenHarmonyCache.cmake
  5. 1 1
      cmake/macros.cmake
  6. 5 0
      cmake/sdlcompilers.cmake
  7. 872 0
      docs/README-harmonyos.md
  8. 1 0
      docs/README-platforms.md
  9. 9 4
      include/SDL3/SDL_iostream.h
  10. 7 0
      include/SDL3/SDL_keyboard.h
  11. 13 0
      include/SDL3/SDL_main.h
  12. 7 0
      include/SDL3/SDL_main_impl.h
  13. 11 0
      include/SDL3/SDL_platform_defines.h
  14. 94 1
      include/SDL3/SDL_system.h
  15. 12 0
      include/SDL3/SDL_video.h
  16. 5 0
      include/build_config/SDL_build_config.h.cmake
  17. 12 0
      openharmony-project/.gitignore
  18. 11 0
      openharmony-project/AppScope/app.json5
  19. 8 0
      openharmony-project/AppScope/resources/base/element/string.json
  20. BIN
      openharmony-project/AppScope/resources/base/media/background.png
  21. BIN
      openharmony-project/AppScope/resources/base/media/foreground.png
  22. 7 0
      openharmony-project/AppScope/resources/base/media/layered_image.json
  23. 42 0
      openharmony-project/build-profile.json5
  24. 32 0
      openharmony-project/code-linter.json5
  25. 6 0
      openharmony-project/entry/.gitignore
  26. 45 0
      openharmony-project/entry/build-profile.json5
  27. 6 0
      openharmony-project/entry/hvigorfile.ts
  28. 23 0
      openharmony-project/entry/obfuscation-rules.txt
  29. 19 0
      openharmony-project/entry/oh-package-lock.json5
  30. 11 0
      openharmony-project/entry/oh-package.json5
  31. 18 0
      openharmony-project/entry/src/main/cpp/CMakeLists.txt
  32. 1 0
      openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts
  33. 7 0
      openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5
  34. 28 0
      openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets
  35. 16 0
      openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets
  36. 15 0
      openharmony-project/entry/src/main/ets/pages/Index.ets
  37. 67 0
      openharmony-project/entry/src/main/module.json5
  38. 8 0
      openharmony-project/entry/src/main/resources/base/element/color.json
  39. 8 0
      openharmony-project/entry/src/main/resources/base/element/float.json
  40. 33 0
      openharmony-project/entry/src/main/resources/base/element/string.json
  41. BIN
      openharmony-project/entry/src/main/resources/base/media/background.png
  42. BIN
      openharmony-project/entry/src/main/resources/base/media/foreground.png
  43. 7 0
      openharmony-project/entry/src/main/resources/base/media/layered_image.json
  44. BIN
      openharmony-project/entry/src/main/resources/base/media/startIcon.png
  45. 3 0
      openharmony-project/entry/src/main/resources/base/profile/backup_config.json
  46. 5 0
      openharmony-project/entry/src/main/resources/base/profile/main_pages.json
  47. 8 0
      openharmony-project/entry/src/main/resources/dark/element/color.json
  48. 23 0
      openharmony-project/hvigor/hvigor-config.json5
  49. 6 0
      openharmony-project/hvigorfile.ts
  50. 28 0
      openharmony-project/oh-package-lock.json5
  51. 10 0
      openharmony-project/oh-package.json5
  52. 7 0
      src/SDL.c
  53. 24 5
      src/SDL_log.c
  54. 3 0
      src/audio/SDL_audio.c
  55. 1 0
      src/audio/SDL_sysaudio.h
  56. 648 0
      src/audio/ohaudio/SDL_ohaudio.c
  57. 38 0
      src/audio/ohaudio/SDL_ohaudio.h
  58. 62 0
      src/audio/ohaudio/SDL_ohaudiofuncs.h
  59. 3 0
      src/camera/SDL_camera.c
  60. 1 0
      src/camera/SDL_syscamera.h
  61. 775 0
      src/camera/openharmony/SDL_camera_openharmony.c
  62. 22 0
      src/core/SDL_core_unsupported.c
  63. 8 0
      src/core/SDL_core_unsupported.h
  64. 4 0
      src/core/android/SDL_android.h
  65. 1207 0
      src/core/openharmony/SDL_openharmony.c
  66. 66 0
      src/core/openharmony/SDL_openharmony.h
  67. 1 1
      src/cpuinfo/SDL_cpuinfo.c
  68. 3 0
      src/dynapi/SDL_dynapi.exports
  69. 2 0
      src/dynapi/SDL_dynapi.h
  70. 3 0
      src/dynapi/SDL_dynapi.sym
  71. 3 0
      src/dynapi/SDL_dynapi_overrides.h
  72. 3 0
      src/dynapi/SDL_dynapi_procs.h
  73. 3 3
      src/events/SDL_keyboard.c
  74. 1 1
      src/filesystem/SDL_filesystem.c
  75. 78 0
      src/filesystem/openharmony/SDL_sysfilesystem.c
  76. 20 16
      src/filesystem/posix/SDL_sysfsops.c
  77. 36 2
      src/io/SDL_iostream.c
  78. 36 0
      src/locale/openharmony/SDL_syslocale.c
  79. 0 1
      src/locale/psp/SDL_syslocale.c
  80. 2 7
      src/main/SDL_main_callbacks.c
  81. 1 0
      src/main/generic/SDL_sysmain_callbacks.c
  82. 60 0
      src/main/openharmony/SDL_sysmain_callbacks.c
  83. 102 0
      src/main/openharmony/SDL_sysmain_runapp.c
  84. 5 0
      src/misc/android/SDL_sysurl.c
  85. 34 0
      src/misc/openharmony/SDL_sysurl.c
  86. 3 0
      src/power/SDL_power.c
  87. 1 0
      src/power/SDL_syspower.h
  88. 46 0
      src/power/openharmony/SDL_syspower.c
  89. 1 1
      src/render/SDL_render.c
  90. 10 6
      src/thread/pthread/SDL_systhread.c
  91. 4 0
      src/video/SDL_egl.c
  92. 1 0
      src/video/SDL_sysvideo.h
  93. 3 0
      src/video/SDL_video.c
  94. 3 0
      src/video/SDL_vulkan_internal.h
  95. 220 0
      src/video/openharmony/SDL_openharmonyclipboard.c
  96. 34 0
      src/video/openharmony/SDL_openharmonyclipboard.h
  97. 574 0
      src/video/openharmony/SDL_openharmonyevents.c
  98. 33 0
      src/video/openharmony/SDL_openharmonyevents.h
  99. 93 0
      src/video/openharmony/SDL_openharmonykeyboard.c
  100. 29 0
      src/video/openharmony/SDL_openharmonykeyboard.h

+ 10 - 0
.gitignore

@@ -99,6 +99,16 @@ out/
 android-project/local.properties
 android-project/local.properties
 android-project/.gradle/
 android-project/.gradle/
 
 
+# for OpenHarmony/HarmonyOS
+openharmony-project/entry/src/main/cpp/CMakeLists.txt
+openharmony-project/entry/src/main/cpp/*.c
+openharmony-project/entry/src/main/cpp/*.h
+openharmony-project/entry/src/main/cpp/SDL
+openharmony-project/entry/src/main/resources/rawfile
+openharmony-project/AppScope/app.json5
+openharmony-project/AppScope/resources/base/element/string.json
+openharmony-project/build-profile.json5
+
 test/checkkeys
 test/checkkeys
 test/checkkeysthreads
 test/checkkeysthreads
 test/gamepadmap
 test/gamepadmap

+ 167 - 2
CMakeLists.txt

@@ -78,6 +78,7 @@ include("${SDL3_SOURCE_DIR}/cmake/GetGitRevisionDescription.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/3rdparty.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/3rdparty.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedMSVCCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedMSVCCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedEmscriptenCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedEmscriptenCache.cmake")
+include("${SDL3_SOURCE_DIR}/cmake/PreseedOpenHarmonyCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedHaikuCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedHaikuCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedNokiaNGageCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedNokiaNGageCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedDOSCache.cmake")
 include("${SDL3_SOURCE_DIR}/cmake/PreseedDOSCache.cmake")
@@ -322,7 +323,7 @@ dep_option(SDL_SSE4_2              "Use SSE4.2 assembly routines" ON "SDL_ASSEMB
 dep_option(SDL_MMX                 "Use MMX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_X86 OR SDL_CPU_X64" OFF)
 dep_option(SDL_MMX                 "Use MMX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_X86 OR SDL_CPU_X64" OFF)
 dep_option(SDL_ALTIVEC             "Use Altivec assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_POWERPC32 OR SDL_CPU_POWERPC64" OFF)
 dep_option(SDL_ALTIVEC             "Use Altivec assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_POWERPC32 OR SDL_CPU_POWERPC64" OFF)
 dep_option(SDL_ARMNEON             "Use NEON assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM32 OR SDL_CPU_ARM64" OFF)
 dep_option(SDL_ARMNEON             "Use NEON assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM32 OR SDL_CPU_ARM64" OFF)
-dep_option(SDL_ARMSVE2             "Use SVE2 assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM64" OFF)
+dep_option(SDL_ARMSVE2             "Use SVE2 assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_ARM64 AND NOT OHOS" OFF)
 dep_option(SDL_LSX                 "Use LSX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF)
 dep_option(SDL_LSX                 "Use LSX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF)
 dep_option(SDL_LASX                "Use LASX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF)
 dep_option(SDL_LASX                "Use LASX assembly routines" ON "SDL_ASSEMBLY;SDL_CPU_LOONGARCH64" OFF)
 
 
@@ -385,7 +386,7 @@ dep_option(SDL_RENDER_D3D12        "Enable the Direct3D 12 render driver" ON "SD
 dep_option(SDL_RENDER_METAL        "Enable the Metal render driver" ON "SDL_RENDER;APPLE" OFF)
 dep_option(SDL_RENDER_METAL        "Enable the Metal render driver" ON "SDL_RENDER;APPLE" OFF)
 dep_option(SDL_RENDER_GPU          "Enable the SDL_GPU render driver" ON "SDL_RENDER;SDL_GPU" OFF)
 dep_option(SDL_RENDER_GPU          "Enable the SDL_GPU render driver" ON "SDL_RENDER;SDL_GPU" OFF)
 dep_option(SDL_VIVANTE             "Use Vivante EGL video driver" ON "${UNIX_SYS};SDL_CPU_ARM32" OFF)
 dep_option(SDL_VIVANTE             "Use Vivante EGL video driver" ON "${UNIX_SYS};SDL_CPU_ARM32" OFF)
-dep_option(SDL_VULKAN              "Enable Vulkan support" "${SDL_VULKAN_DEFAULT}" "SDL_VIDEO;ANDROID OR APPLE OR LINUX OR FREEBSD OR OPENBSD OR WINDOWS OR CYGWIN" OFF)
+dep_option(SDL_VULKAN              "Enable Vulkan support" "${SDL_VULKAN_DEFAULT}" "SDL_VIDEO;ANDROID OR APPLE OR LINUX OR FREEBSD OR OPENBSD OR WINDOWS OR CYGWIN OR OHOS" OFF)
 dep_option(SDL_RENDER_VULKAN       "Enable the Vulkan render driver" ON "SDL_RENDER;SDL_VULKAN" OFF)
 dep_option(SDL_RENDER_VULKAN       "Enable the Vulkan render driver" ON "SDL_RENDER;SDL_VULKAN" OFF)
 dep_option(SDL_METAL               "Enable Metal support" ON "APPLE" OFF)
 dep_option(SDL_METAL               "Enable Metal support" ON "APPLE" OFF)
 set_option(SDL_OPENVR              "Use OpenVR video driver" OFF)
 set_option(SDL_OPENVR              "Use OpenVR video driver" OFF)
@@ -1724,6 +1725,170 @@ if(ANDROID)
     target_link_options(SDL3-static INTERFACE "-Wl,-u,JNI_OnLoad")
     target_link_options(SDL3-static INTERFACE "-Wl,-u,JNI_OnLoad")
   endif()
   endif()
 
 
+elseif(OHOS)  # OpenHarmony/HarmonyOS
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/core/openharmony/*.c"
+    "${SDL3_SOURCE_DIR}/src/core/openharmony/*.h"
+  )
+
+  sdl_link_dependency(openharmonycore LIBS libace_napi.z.so libace_ndk.z.so deviceinfo_ndk.z rawfile.z hilog_ndk.z ohcommonevent)
+
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/main/openharmony/*.c"
+    "${SDL3_SOURCE_DIR}/src/main/openharmony/*.h"
+  )
+  set(HAVE_SDL_MAIN_CALLBACKS TRUE)
+
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/misc/openharmony/*.c"
+    "${SDL3_SOURCE_DIR}/src/misc/openharmony/*.h"
+  )
+  set(HAVE_SDL_MISC TRUE)
+
+  if(SDL_AUDIO)
+    set(SDL_AUDIO_DRIVER_OHAUDIO 1)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/audio/ohaudio/*.c"
+      "${SDL3_SOURCE_DIR}/src/audio/ohaudio/*.h"
+    )
+
+    set(HAVE_SDL_AUDIO TRUE)
+  endif()
+
+  set(SDL_FILESYSTEM_OPENHARMONY 1)
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/filesystem/openharmony/*.c"
+    "${SDL3_SOURCE_DIR}/src/filesystem/openharmony/*.h"
+  )
+  sdl_link_dependency(openharmonyfs LIBS ability_runtime)
+  set(HAVE_SDL_FILESYSTEM TRUE)
+
+  set(SDL_FSOPS_POSIX 1)
+  sdl_sources("${SDL3_SOURCE_DIR}/src/filesystem/posix/SDL_sysfsops.c")
+  set(HAVE_SDL_FSOPS TRUE)
+
+if (False)   # !!! FIXME
+  if(SDL_HAPTIC)
+    set(SDL_HAPTIC_OPENHARMONY 1)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/haptic/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/haptic/openharmony/*.h"
+    )
+    set(HAVE_SDL_HAPTIC TRUE)
+  endif()
+
+  # !!! FIXME: any chance HIDAPI would work here?
+  CheckHIDAPI()
+
+  if(SDL_JOYSTICK)
+    set(SDL_JOYSTICK_OPENHARMONY 1)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/joystick/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/joystick/openharmony/*.h"
+    )
+    set(HAVE_SDL_JOYSTICK TRUE)
+  endif()
+endif()
+
+  set(SDL_LOADSO_DLOPEN 1)
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/loadso/dlopen/*.c"
+    "${SDL3_SOURCE_DIR}/src/loadso/dlopen/*.h"
+  )
+  set(HAVE_SDL_LOADSO TRUE)
+
+  if(SDL_POWER)
+    set(SDL_POWER_OPENHARMONY 1)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/power/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/power/openharmony/*.h"
+    )
+    sdl_link_dependency(openharmonypower LIBS ohbattery_info)
+    set(HAVE_SDL_POWER TRUE)
+  endif()
+
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/locale/openharmony/*.c"
+    "${SDL3_SOURCE_DIR}/src/locale/openharmony/*.h"
+  )
+  set(HAVE_SDL_LOCALE TRUE)
+
+  set(SDL_TIME_UNIX 1)
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/time/unix/*.c"
+    "${SDL3_SOURCE_DIR}/src/time/unix/*.h"
+  )
+  set(HAVE_SDL_TIME TRUE)
+
+  set(SDL_TIMER_UNIX 1)
+  sdl_glob_sources(
+    "${SDL3_SOURCE_DIR}/src/timer/unix/*.c"
+    "${SDL3_SOURCE_DIR}/src/timer/unix/*.h"
+  )
+  set(HAVE_SDL_TIMERS TRUE)
+
+if(False)
+  if(SDL_SENSOR)
+    set(SDL_SENSOR_OPENHARMONY 1)
+    set(HAVE_SDL_SENSORS TRUE)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/sensor/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/sensor/openharmony/*.h"
+    )
+  endif()
+endif()
+
+  if(SDL_CAMERA)
+    set(SDL_CAMERA_DRIVER_OPENHARMONY 1)
+    set(HAVE_CAMERA TRUE)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/camera/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/camera/openharmony/*.h"
+    )
+    sdl_link_dependency(openharmonycamera LIBS ohcamera ohimage image_receiver native_image native_buffer)
+  endif()
+
+  if(SDL_VIDEO)
+    set(SDL_VIDEO_DRIVER_OPENHARMONY 1)
+    sdl_glob_sources(
+      "${SDL3_SOURCE_DIR}/src/video/openharmony/*.c"
+      "${SDL3_SOURCE_DIR}/src/video/openharmony/*.h"
+    )
+    set(HAVE_SDL_VIDEO TRUE)
+    sdl_link_dependency(openharmonyvideo LIBS native_display_manager pasteboard udmf)
+
+    # Core stuff
+    # find_library(ANDROID_DL_LIBRARY dl)
+    # FIXME failing dlopen https://github.com/android-ndk/ndk/issues/929
+# !!! FIXME
+#    sdl_link_dependency(android_video LIBS dl log android)
+    sdl_compile_definitions(PRIVATE "GL_GLEXT_PROTOTYPES")
+
+    #enable gles
+    if(SDL_OPENGLES)
+      set(SDL_VIDEO_OPENGL_EGL 1)
+      set(HAVE_OPENGLES TRUE)
+      set(SDL_VIDEO_OPENGL_ES2 1)
+      set(SDL_VIDEO_RENDER_OGL_ES2 1)
+      #sdl_link_dependency(opengles LIBS GLESv3)
+    endif()
+
+    if(SDL_VULKAN)
+      set(SDL_VIDEO_VULKAN 1)
+      set(HAVE_VULKAN TRUE)
+      if(SDL_RENDER_VULKAN)
+        set(SDL_VIDEO_RENDER_VULKAN 1)
+        set(HAVE_RENDER_VULKAN TRUE)
+      endif()
+    endif()
+  endif()
+
+  CheckPTHREAD()
+
+  if(SDL_CLOCK_GETTIME)
+    set(HAVE_CLOCK_GETTIME 1)
+  endif()
+
 elseif(EMSCRIPTEN)
 elseif(EMSCRIPTEN)
   # Hide noisy warnings that intend to aid mostly during initial stages of porting a new
   # Hide noisy warnings that intend to aid mostly during initial stages of porting a new
   # project. Uncomment at will for verbose cross-compiling -I/../ path info.
   # project. Uncomment at will for verbose cross-compiling -I/../ path info.

+ 209 - 0
build-scripts/harmonyos-tool.sh

@@ -0,0 +1,209 @@
+#!/usr/bin/bash
+
+HVIGORWPATH=`which hvigorw 2>/dev/null`
+if [ "x$HVIGORWPATH" == "x" ]; then
+    echo "Couldn't find hvigorw!"
+    echo "Please make sure the HarmonyOS command line tools are in your PATH." 1>&2
+    exit 1
+fi
+
+HDCPATH=`which hdc 2>/dev/null`
+if [ "x$HDCPATH" == "x" ]; then
+    echo "Couldn't find hdc!"
+    echo "Please make sure the HarmonyOS command line tools are in your PATH." 1>&2
+    exit 1
+fi
+
+CLTBINPATH=`dirname "$HVIGORWPATH"`
+CLTPATH=`dirname "$CLTBINPATH"`
+CLT=`realpath "$CLTPATH"`
+#echo "CLT is '$CLT'"
+
+need_usage=0
+
+APPJSON=""
+PROJDIR="$1"
+if [ "x$PROJDIR" == "x" ]; then
+    need_usage=1
+else
+    PROJDIR=`realpath "$PROJDIR"`
+    APPJSON="$PROJDIR/AppScope/app.json5"
+    if [ ! -f "$APPJSON" ]; then
+        echo "Can't find '$APPJSON'. Is project directory correct?" 1>&2
+        echo "" 1>&2
+        need_usage=1
+    fi
+fi
+
+do_clean=0
+do_build=0
+do_uninstall=0
+do_install=0
+do_kill=0
+do_launch=0
+do_log=0
+do_debug=0
+do_anything=0
+
+shift  # dump projectdir
+while [ $# -gt 0 ]; do
+    do_anything=1
+    case $1 in
+        --clean)
+            do_clean=1
+            shift
+            ;;
+        --build)
+            do_build=1
+            shift
+            ;;
+        --install)
+            do_install=1
+            shift
+            ;;
+        --uninstall)
+            do_uninstall=1
+            shift
+            ;;
+        --kill)
+            do_kill=1
+            shift
+            ;;
+        --launch)
+            do_launch=1
+            shift
+            ;;
+        --log)
+            do_log=1
+            shift
+            ;;
+        --debug)
+            do_launch=1
+            do_debug=1
+            shift
+            ;;
+        *)
+            echo "Unknown option '$1'" 1>&2
+            need_usage=1
+            shift
+            ;;
+    esac
+done
+
+if [ "$do_anything" == "0" ]; then
+    need_usage=1
+fi
+
+if [ "$need_usage" == "1" ]; then
+    echo "USAGE: $0 <project_dir> [--kill] [--uninstall] [--build] [--install] [--launch] [--debug] [--log]" 1>&2
+    echo "" 1>&2
+    exit 1
+fi
+
+if [ "$do_build" == "1" ]; then
+    if [ "$do_debug" == "1" ]; then
+        do_install=1
+    elif [ "$do_launch" == "1" ]; then
+        do_install=1
+    fi
+fi
+
+# Here we go!
+
+BUNDLE=`grep -F bundleName "$PROJDIR/AppScope/app.json5" |perl -w -p -e 's/\A\s*\"bundleName\"\s*\:\s*\"(.*?)\".*?\Z/$1/i;'`
+if [ "x$BUNDLE" == "x" ]; then
+    echo "Failed to determine bundle name! Aborting!" 1>&2
+    exit 1
+fi
+
+echo "BUNDLE is '$BUNDLE'"
+
+#set -x
+
+cd "$PROJDIR"
+
+if [ "$do_kill" == "1" ]; then
+    echo "KILLING..."
+    hdc shell aa force-stop $BUNDLE || exit 1
+fi
+
+if [ "$do_uninstall" == "1" ]; then
+    echo "UNINSTALLING..."
+    hdc uninstall $BUNDLE || exit 1
+fi
+
+if [ "$do_clean" == "1" ]; then
+    echo "CLEANING..."
+    hvigorw clean || exit 1
+fi
+
+if [ "$do_build" == "1" ]; then
+    echo "BUILDING..."
+    hvigorw assembleHap || exit 1
+fi
+
+if [ "$do_debug" == "1" ]; then
+    hdc shell mkdir -p data/local/tmp/debugserver/$BUNDLE
+    hdc file send "$CLT/sdk/default/hms/native/lldb/aarch64-linux-ohos/lldb-server" data/local/tmp/debugserver/$BUNDLE
+    hdc shell chmod 755 data/local/tmp/debugserver/$BUNDLE/lldb-server
+fi
+
+if [ "$do_install" == "1" ]; then
+    echo "INSTALLING..."
+    hdc install "$PROJDIR/entry/build/default/outputs/default/entry-default-signed.hap" || exit 1
+fi
+
+# Catch sigint, kill the backgrounded `hdc hilog` then exit.
+trap_sigint() {
+    if [ "$HILOGPID" != "" ]; then
+        kill -9 "$HILOGPID"
+    fi
+    wait "$HILOGPID" 2>/dev/null
+    echo ""
+    #echo "Killed $HILOGPID"
+    exit 0
+}
+
+HILOGPID=""
+if [ "$do_log" == "1" ]; then
+    trap trap_sigint INT
+
+    # This wild bash syntax puts `hdc` in the background, and pipes its output through `grep`, but `$!` will have hdc's process ID.
+    # The ridiculous perl regular expression basically filters out a ton of garbage from each line of the log. You can remove it entirely and just grep for the bundle name, though!
+    hdc hilog > >(grep -F --line-buffered "/$BUNDLE/" |perl -w -p -e 's/\A\d+\-\d+\s+(\d+\:\d+\:\d+)\.\d+\s+\d+\s+\d+\s+(.)\s+.*?\/.*?\/(.*?)\:\s+/$1 $2 $3: /;') & HILOGPID="$!"
+fi
+
+if [ "$do_launch" == "1" ]; then
+    echo "LAUNCHING..."
+    # !!! FIXME: maybe this makes the process wait on launch until debugger is attached?
+    #hdc shell aa appdebug -b $BUNDLE
+    hdc shell aa start -a EntryAbility -b $BUNDLE || exit 1
+fi
+
+if [ "$do_debug" == "1" ]; then
+    echo "DEBUGGING..."
+    hdc shell aa attach -b $BUNDLE
+    hdc shell aa process -a EntryAbility -b $BUNDLE -D "/data/local/tmp/debugserver/$BUNDLE/lldb-server platform --listen unix-abstract:///lldb-server/platform.sock"
+
+    PID=`hdc shell ps -w -o PID,ARGS |grep -F "$BUNDLE" |head -n 1 |awk '{print $1}'`
+    if [ "x$PID" == "x" ]; then
+        echo "Failed to determine PID! Aborting!" 1>&2
+        exit 1
+    fi
+
+    echo "PID is $PID"
+
+    trap - INT
+    "$CLT/sdk/default/openharmony/native/llvm/bin/lldb" \
+        -O "platform select remote-ohos" \
+        -O "platform connect unix-abstract-connect:///lldb-server/platform.sock" \
+        -O "settings append target.exec-search-paths \"$PROJDIR/entry/build/default/intermediates/cmake/default/obj/arm64-v8a\"" \
+        -O "attach $PID"
+    trap trap_sigint INT
+fi
+
+if [ "$HILOGPID" != "" ]; then
+    #echo "$0: WAITING ON $HILOGPID"
+    wait "$HILOGPID"
+fi
+

+ 188 - 0
cmake/PreseedOpenHarmonyCache.cmake

@@ -0,0 +1,188 @@
+if(OHOS)
+  function(SDL_Preseed_CMakeCache)
+    set(COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS    "1"   CACHE INTERNAL "Test COMPILER_SUPPORTS_FDIAGNOSTICS_COLOR_ALWAYS")
+    set(COMPILER_SUPPORTS_GCC_ATOMICS                  "1"    CACHE INTERNAL "Test COMPILER_SUPPORTS_GCC_ATOMICS")
+    set(COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET       "1"   CACHE INTERNAL "Test COMPILER_SUPPORTS_SYNC_LOCK_TEST_AND_SET")
+    set(HAVE_CLANG_COMMENT_BLOCK_COMMANDS              "1"    CACHE INTERNAL "Test HAVE_CLANG_COMMENT_BLOCK_COMMANDS")
+    set(HAVE_ALLOCA_H                                  "1"   CACHE INTERNAL "Have include alloca.h")
+    set(HAVE_LIBM                                      "1"   CACHE INTERNAL "Have library m")
+    set(HAVE_POSIX_SPAWN                               "1"   CACHE INTERNAL "Have symbol posix_spawn")
+    set(HAVE_FSEEKO                                    "1"   CACHE INTERNAL "Have symbol fseeko")
+    set(HAVE_OFF64_T                                   "1"   CACHE INTERNAL "Have symbol off64_t")
+    set(LIBC_HAS_ABS                                   "1"   CACHE INTERNAL "Have symbol abs")
+    set(LIBC_HAS_ACOS                                  "1"   CACHE INTERNAL "Have symbol acos")
+    set(LIBC_HAS_ACOSF                                 "1"   CACHE INTERNAL "Have symbol acosf")
+    set(LIBC_HAS_ASIN                                  "1"   CACHE INTERNAL "Have symbol asin")
+    set(LIBC_HAS_ASINF                                 "1"   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                                "1"   CACHE INTERNAL "Have symbol atan2f")
+    set(LIBC_HAS_ATANF                                 "1"   CACHE INTERNAL "Have symbol atanf")
+    set(LIBC_HAS_ATOF                                  "1"   CACHE INTERNAL "Have symbol atof")
+    set(LIBC_HAS_ATOI                                  "1"   CACHE INTERNAL "Have symbol atoi")
+    set(LIBC_HAS_BCOPY                                 "1"   CACHE INTERNAL "Have symbol bcopy")
+    set(LIBC_HAS_CALLOC                                "1"   CACHE INTERNAL "Have symbol calloc")
+    set(LIBC_HAS_CEIL                                  "1"   CACHE INTERNAL "Have symbol ceil")
+    set(LIBC_HAS_CEILF                                 "1"   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                                  "1"   CACHE INTERNAL "Have symbol cosf")
+    set(LIBC_HAS_EXP                                   "1"   CACHE INTERNAL "Have symbol exp")
+    set(LIBC_HAS_EXPF                                  "1"   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                                "1"   CACHE INTERNAL "Have symbol floorf")
+    set(LIBC_HAS_FMOD                                  "1"   CACHE INTERNAL "Have symbol fmod")
+    set(LIBC_HAS_FMODF                                 "1"   CACHE INTERNAL "Have symbol fmodf")
+    set(LIBC_HAS_FOPEN64                               "1"   CACHE INTERNAL "Have symbol fopen64")
+    set(LIBC_HAS_FREE                                  "1"   CACHE INTERNAL "Have symbol free")
+    set(LIBC_HAS_FSEEKO                                "1"   CACHE INTERNAL "Have symbol fseeko")
+    set(LIBC_HAS_FSEEKO64                              "1"   CACHE INTERNAL "Have symbol fseeko64 (broken in DJGPP)")
+    set(LIBC_HAS_GETENV                                "1"   CACHE INTERNAL "Have symbol getenv")
+    set(LIBC_HAS_ICONV_H                               "1"   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                                 ""    CACHE INTERNAL "Have include isinf(double)")
+    set(LIBC_ISINF_HANDLES_FLOAT                       ""    CACHE INTERNAL "Have include isinf(float)")
+    set(LIBC_HAS_ISINFF                                ""    CACHE INTERNAL "Have include isinff(float)")
+    set(LIBC_HAS_ISNAN                                 ""    CACHE INTERNAL "Have include isnan(double)")
+    set(LIBC_ISNAN_HANDLES_FLOAT                       ""    CACHE INTERNAL "Have include isnan(float)")
+    set(LIBC_HAS_ISNANF                                ""    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                                 "1"   CACHE INTERNAL "Have symbol log10")
+    set(LIBC_HAS_LOG10F                                "1"   CACHE INTERNAL "Have symbol log10f")
+    set(LIBC_HAS_LOGF                                  "1"   CACHE INTERNAL "Have symbol logf")
+    set(LIBC_HAS_LROUND                                "1"   CACHE INTERNAL "Have symbol lround")
+    set(LIBC_HAS_LROUNDF                               "1"   CACHE INTERNAL "Have symbol lroundf")
+    set(LIBC_HAS_MALLOC                                "1"   CACHE INTERNAL "Have symbol malloc")
+    set(LIBC_HAS_MALLOC_H                              "1"   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                                "1"   CACHE INTERNAL "Have symbol memcpy")
+    set(LIBC_HAS_MEMMOVE                               "1"   CACHE INTERNAL "Have symbol memmove")
+    set(LIBC_HAS_MEMORY_H                              "1"   CACHE INTERNAL "Have include memory.h")
+    set(LIBC_HAS_MEMSET                                "1"   CACHE INTERNAL "Have symbol memset")
+    set(LIBC_HAS_MODF                                  "1"   CACHE INTERNAL "Have symbol modf")
+    set(LIBC_HAS_MODFF                                 "1"   CACHE INTERNAL "Have symbol modff")
+    set(LIBC_HAS_POW                                   "1"   CACHE INTERNAL "Have symbol pow")
+    set(LIBC_HAS_POWF                                  "1"   CACHE INTERNAL "Have symbol powf")
+    set(LIBC_HAS_PUTENV                                "1"   CACHE INTERNAL "Have symbol putenv")
+    set(LIBC_HAS_REALLOC                               "1"   CACHE INTERNAL "Have symbol realloc")
+    set(LIBC_HAS_RINDEX                                ""    CACHE INTERNAL "Have symbol rindex")
+    set(LIBC_HAS_ROUND                                 "1"   CACHE INTERNAL "Have symbol round")
+    set(LIBC_HAS_ROUNDF                                "1"   CACHE INTERNAL "Have symbol roundf")
+    set(LIBC_HAS_SCALBN                                "1"   CACHE INTERNAL "Have symbol scalbn")
+    set(LIBC_HAS_SCALBNF                               "1"   CACHE INTERNAL "Have symbol scalbnf")
+    set(LIBC_HAS_SETENV                                "1"   CACHE INTERNAL "Have symbol setenv")
+    set(LIBC_HAS_SIGNAL_H                              "1"   CACHE INTERNAL "Have include signal.h")
+    set(LIBC_HAS_SIN                                   "1"   CACHE INTERNAL "Have symbol sin")
+    set(LIBC_HAS_SINF                                  "1"   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                                 "1"   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                            "1"   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                             "1"   CACHE INTERNAL "Have include strings.h")
+    set(LIBC_HAS_STRING_H                              "1"   CACHE INTERNAL "Have include string.h")
+    set(LIBC_HAS_STRLCAT                               "1"   CACHE INTERNAL "Have symbol strlcat")
+    set(LIBC_HAS_STRLCPY                               "1"   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                               "1"   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                                "1"   CACHE INTERNAL "Have symbol strtod")
+    set(LIBC_HAS_STRTOK_R                              "1"   CACHE INTERNAL "Have symbol strtok_r")
+    set(LIBC_HAS_STRTOL                                "1"   CACHE INTERNAL "Have symbol strtol")
+    set(LIBC_HAS_STRTOLL                               "1"   CACHE INTERNAL "Have symbol strtoll")
+    set(LIBC_HAS_STRTOUL                               "1"   CACHE INTERNAL "Have symbol strtoul")
+    set(LIBC_HAS_STRTOULL                              "1"   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                                  "1"   CACHE INTERNAL "Have symbol tanf")
+    set(LIBC_HAS_TIME_H                                "1"   CACHE INTERNAL "Have include time.h")
+    set(LIBC_HAS_TRUNC                                 "1"   CACHE INTERNAL "Have symbol trunc")
+    set(LIBC_HAS_TRUNCF                                "1"   CACHE INTERNAL "Have symbol truncf")
+    set(LIBC_HAS_UNSETENV                              "1"   CACHE INTERNAL "Have symbol unsetenv")
+    set(LIBC_HAS_VFORK                                 "1"   CACHE INTERNAL "Have symbol vfork")
+    set(LIBC_HAS_VSNPRINTF                             "1"   CACHE INTERNAL "Have symbol vsnprintf")
+    set(LIBC_HAS_VSSCANF                               "1"   CACHE INTERNAL "Have symbol vsscanf")
+    set(LIBC_HAS_WCHAR_H                               "1"   CACHE INTERNAL "Have include wchar.h")
+    set(LIBC_HAS_WCSCMP                                "1"   CACHE INTERNAL "Have symbol wcscmp")
+    set(LIBC_HAS_WCSDUP                                "1"   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                                "1"   CACHE INTERNAL "Have symbol wcslen")
+    set(LIBC_HAS_WCSNCMP                               "1"   CACHE INTERNAL "Have symbol wcsncmp")
+    set(LIBC_HAS_WCSNLEN                               "1"   CACHE INTERNAL "Have symbol wcsnlen")
+    set(LIBC_HAS_WCSSTR                                "1"   CACHE INTERNAL "Have symbol wcsstr")
+    set(LIBC_HAS_WCSTOL                                "1"   CACHE INTERNAL "Have symbol wcstol")
+    set(LIBC_HAS_WCSTOLL                               "1"   CACHE INTERNAL "Have symbol wcstoll")
+    set(LIBC_HAS_WCSTOUL                               "1"   CACHE INTERNAL "Have symbol wcstoul")
+    set(LIBC_HAS_WCSTOULL                              "1"   CACHE INTERNAL "Have symbol wcstoull")
+    set(LIBC_HAS__EXIT                                 "1"   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__ULTOA                                ""    CACHE INTERNAL "Have symbol _ultoa")
+    set(LIBC_HAS__WCSDUP                               "1"   CACHE INTERNAL "Have symbol _wcsdup")
+    set(LIBC_IS_GLIBC                                  ""    CACHE INTERNAL "Have symbol __GLIBC__")
+    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                     "1"   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                        "1"   CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION")
+    set(HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND        "1"   CACHE INTERNAL "Test HAVE_GCC_WDOCUMENTATION_UNKNOWN_COMMAND")
+    set(HAVE_GCC_COMMENT_BLOCK_COMMANDS                "1"   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                "1"   CACHE INTERNAL "Test HAVE_GCC_WUNUSED_LOCAL_TYPEDEFS")
+    set(HAVE_GCC_WIMPLICIT_FALLTHROUGH                 "1"   CACHE INTERNAL "Test HAVE_GCC_WIMPLICIT_FALLTHROUGH")
+    set(HAVE_GCC_FVISIBILITY                           ""    CACHE INTERNAL "Test HAVE_GCC_FVISIBILITY")
+    set(HAVE_ST_MTIM                                   "1"   CACHE INTERNAL "Test HAVE_ST_MTIM")
+    set(HAVE_LD_VERSION_SCRIPT                         "1"   CACHE INTERNAL "Test HAVE_LD_VERSION_SCRIPT")
+    set(HAVE_WL_VERSION_SCRIPT                         "1"   CACHE INTERNAL "Test HAVE_WL_VERSION_SCRIPT")
+    set(LINKER_SUPPORTS_VERSION_SCRIPT                 "1"   CACHE INTERNAL "Test LINKER_SUPPORTS_VERSION_SCRIPT")
+    set(LINKER_SUPPORTS_WL_NO_UNDEFINED                "1"   CACHE INTERNAL "Test LINKER_SUPPORTS_WL_NO_UNDEFINED")
+    set(ICONV_IN_LIBC                                  "1"   CACHE INTERNAL "Test ICONV_IN_LIBC")
+    set(ICONV_IN_LIBICONV                              ""    CACHE INTERNAL "Test ICONV_IN_LIBICONV")
+    set(HAVE_GETPAGESIZE                               "1"   CACHE INTERNAL "Have symbol getpagesize")
+    set(HAVE_SIGACTION                                 "1"   CACHE INTERNAL "Have symbol sigaction")
+    set(HAVE_SA_SIGACTION                              "1"   CACHE INTERNAL "Have symbol sa_sigaction")
+    set(HAVE_SETJMP                                    "1"   CACHE INTERNAL "Have symbol setjmp")
+    set(HAVE_NANOSLEEP                                 "1"   CACHE INTERNAL "Have symbol nanosleep")
+    set(HAVE_GMTIME_R                                  "1"   CACHE INTERNAL "Have symbol gmtime_r")
+    set(HAVE_LOCALTIME_R                               "1"   CACHE INTERNAL "Have symbol localtime_r")
+    set(HAVE_NL_LANGINFO                               "1"   CACHE INTERNAL "Have symbol nl_langinfo")
+    set(HAVE_SYSCONF                                   "1"   CACHE INTERNAL "Have symbol sysconf")
+    set(HAVE_SYSCTLBYNAME                              ""    CACHE INTERNAL "Have symbol sysctlbyname")
+    set(HAVE_GETAUXVAL                                 "1"   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                              "1"   CACHE INTERNAL "Have symbol memfd_create")
+    set(HAVE_POSIX_FALLOCATE                           "1"   CACHE INTERNAL "Have symbol posix_fallocate")
+    set(HAVE_DLOPEN_IN_LIBC                            "1"   CACHE INTERNAL "Have symbol dlopen")
+    set(HAVE_GETHOSTNAME                               "1"   CACHE INTERNAL "Have symbol gethostname")
+    set(HAVE_SIGTIMEDWAIT                              "1"   CACHE INTERNAL "Have symbol sigtimedwait")
+    set(HAVE_PPOLL                                     "1"   CACHE INTERNAL "Have symbol ppoll")
+    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                                 "1"   CACHE INTERNAL "Have symbol fdatasync")
+    set(HAVE_GETRESUID                                 "1"   CACHE INTERNAL "Have symbol getresuid")
+    set(HAVE_GETRESGID                                 "1"   CACHE INTERNAL "Have symbol getresgid")
+  endfunction()
+endif()

+ 1 - 1
cmake/macros.cmake

@@ -425,7 +425,7 @@ function(SDL_PrintSummary)
     message(STATUS "")
     message(STATUS "")
   endif()
   endif()
 
 
-  if(UNIX AND NOT (ANDROID OR APPLE OR EMSCRIPTEN OR HAIKU OR RISCOS OR DJGPP OR CYGWIN))
+  if(UNIX AND NOT (ANDROID OR APPLE OR EMSCRIPTEN OR HAIKU OR RISCOS OR DJGPP OR CYGWIN OR OHOS))
     if(NOT (HAVE_X11 OR HAVE_WAYLAND))
     if(NOT (HAVE_X11 OR HAVE_WAYLAND))
       if(NOT SDL_UNIX_CONSOLE_BUILD)
       if(NOT SDL_UNIX_CONSOLE_BUILD)
         message(FATAL_ERROR
         message(FATAL_ERROR

+ 5 - 0
cmake/sdlcompilers.cmake

@@ -184,6 +184,11 @@ function(SDL_AddCommonCompilerFlags TARGET)
   if(USE_TCC)
   if(USE_TCC)
       sdl_target_compile_option_all_languages(${TARGET} "-DSTBI_NO_SIMD")
       sdl_target_compile_option_all_languages(${TARGET} "-DSTBI_NO_SIMD")
   endif()
   endif()
+
+  if(OHOS)  # OpenHarmony/HarmonyOS
+      # Stop the `clang: warning: argument unused during compilation: '--gcc-toolchain=$X/sdk/default/openharmony/native/llvm' [-Wunused-command-line-argument]` on every file compiled. I think this is a toolchain file issue...?
+      sdl_target_compile_option_all_languages(${TARGET} "-Wno-unused-command-line-argument")
+  endif()
 endfunction()
 endfunction()
 
 
 function(check_x86_source_compiles BODY VAR)
 function(check_x86_source_compiles BODY VAR)

+ 872 - 0
docs/README-harmonyos.md

@@ -0,0 +1,872 @@
+# HarmonyOS
+
+(And also OpenHarmony.)
+
+## What is HarmonyOS?
+
+(This is my current understanding, please correct these docs if incorrect.)
+
+HarmonyOS is an operating system for tablets and phones. It is developed by
+[Huawei](https://www.huawei.com/en/). It is competitive with platforms like
+Android and iOS, capable of powering high-end phones. Unlike Google and Apple,
+Huawei is a Chinese company, and HarmonyOS is largely serving the Chinese
+market at this time.
+
+The system uses some custom pieces, but also relies on open source software
+from outside of the project, such as NodeJS, musl, etc.
+
+Much of the platform is open source, under the name "OpenHarmony," and Huawei's
+proprietary spin of OpenHarmony is called HarmonyOS. Huawei is the primary
+contributor to OpenHarmony.
+
+So it is probably best said: HarmonyOS is to Android what OpenHarmony is to
+AOSP. But, to be clear, OpenHarmony is not derived from Android.
+
+This document will generally refer to HarmonyOS, but from a development
+viewpoint, you are actually targeting OpenHarmony, even if you're building
+with intent to only target HarmonyOS on Huawei-produced phones.
+
+
+## Supported versions
+
+All testing to date has been done on a real phone, a Huawei Mate 80. SDL
+currently supports HarmonyOS 5.0.4 (API level 16) and later, although low-end
+testing hasn't been done so there might be little changes needed for extreme
+compatibility. Report bugs or send patches if this becomes a problem.
+
+
+## Some basic target platform truths
+
+More or less, an app on HarmonyOS is a NodeJS process, in the same way that
+an app on Android is a Java virtual machine process. While one generally
+writes apps in Java or Kotlin for Android, HarmonyOS uses a TypeScript
+variant called ArkTS. However, like SDL on Android, we will be focusing on
+native code in C or C++, so SDL makes moves similar to its Android strategy,
+migrating from ArkTS to native code as quickly as possible and handing control
+over to a (presumably C or C++) native code app, and making it so the app can
+entirely opt out of interacting with the ArkTS layer directly.
+
+The equivalent of the Android "JNI" layer is NodeJS's NAPI interfaces. A large
+portion of the system APIs are accessible directly from OS-provided native
+libraries, so there is a lot less reason to dip back down into the ArkTS layer
+for most tasks, whereas lots of things on Android need to talk to Java-only
+interfaces to accomplish common and important tasks, even in an
+otherwise-entirely native program.
+
+The C/C++ compiler is based on Clang (you might end up using a compiler called
+"BiSheng", which is Clang with some custom optimization techniques developed
+by Huawei, but the binary is still called "clang").
+
+The platform triplet is "aarch64-linux-ohos" (other CPU architectures are
+possible, but this is likely the most common. The
+devtools mention 32-bit ARM and x86-64, too). The CMake toolchain file sets
+the variable `OHOS` (OpenHarmony OS) to "OHOS", so `if(OHOS)` works. Further,
+`CMAKE_SYSTEM_NAME` is set to "OHOS". The C preprocessor defines `__OHOS__`.
+
+
+## Getting hardware to run HarmonyOS
+
+(This information is accurate as of September 2026, but may be out of date at
+any moment, so please check other sources before assuming this is still
+correct.)
+
+HarmonyOS phones work in America (at least on WiFi; presumably one could get a
+working (e)SIM card for phone calls and cell network access, but I haven't
+tried). It may be difficult to obtain a phone: I couldn't find one for
+purchase on Amazon or eBay from the United States. You might have an easier
+time in other countries. I assume it is super-easy in China. :)
+
+One can run OpenHarmony (not HarmonyOS) on a Raspberry Pi 4b, compliments of
+the Oniro project, which provides both source code and prebuilt images for
+download:
+
+https://docs.oniroproject.org/device-development/developer-boards/raspberry-pi-4model-b/
+
+This lets you run the open source variant of the OS on real hardware, but
+obviously a Raspberry Pi isn't a tablet or a phone, so the experience is
+somewhat tangential to the the most common configuration one might expect in
+the field, but it _does_ work.
+
+DevEco Studio offers phone, tablet, and smartwatch emulators for Huawei
+hardware, and these apparently run on Windows, macOS and Linux, but will not
+let you download them outside of mainland China (except for smartwatch
+emulators, for some reason). Comments on Huawei's forums suggest relaxing this
+restriction is planned, but no timeline has been announced.
+
+
+## Get HarmonyOS development tools
+
+You will need to download Huawei's developer tools. They offer a full IDE,
+called "DevEco Studio," but this is not strictly necessary for compiling SDL
+itself.
+
+There are also command line tools available for Windows, macOS, and Linux, and
+these _are_ sufficient to compile SDL. For building full applications, you
+likely need DevEco Studio to at least set up your project and manage signing
+keys, etc. Some things (but not all) are doable from the command line tools,
+and a handful of things are (currently) not.
+
+DevEco Studio, the GUI, is only available for Windows and macOS. It appears to
+be a fork of IntelliJ IDEA. It does not appear to be open source, afaict.
+
+To download the developer tools, you'll need to register an account at
+[the Huawei developer site](https://developer.huawei.com/en/). Then visit
+[the tool download page](https://developer.huawei.com/consumer/en/download/)
+For Linux, scroll down to "Command Line Tools." Download for your platform,
+which should be a simple .zip file, and unpack it. If you're on Windows or
+macOS, download and install DevEco Studio itself while you're here, too.
+
+
+## Don't fight against DevEco Studio
+
+I'm a Linux user, and I avoid programming IDEs in general, so my instinct is
+to just get everything done with my text editor and the command line tools,
+and this _feels_ possible, but I haven't quite figured it out. The system is
+friendly to things like CMake, clang, etc, and the documentation, even in
+English, is pretty thorough, but I still couldn't get there without using
+DevEco Studio a little.
+
+The system appears designed to at least hand off a working project from
+DevEco Studio to a Linux machine for building and deploying from the command
+line tools, for Continuous Integration if nothing else. Figuring out how to
+eliminate the Windows/Mac GUI dependency entirely is an ongoing and unresolved
+concern. I definitely needed it to manage signing keys. Once I got that far, I
+was able to copy the appropriate directories to a Linux system and use the
+`hvigorw` command line tool to build and sign an app, and `hdc` to install it
+on a real phone. Please see "The Helper Script" section, below, to automate
+a lot of the Linux command line pieces, too.
+
+
+## Install command line tools.
+
+(If you're entirely focused on DevEco Studio, you can probably skip this.)
+
+I unzipped the downloaded command line tools so they're sitting in
+`$HOME/HarmonyOS-SDK/command-line-tools`, but anywhere you want to put it is
+fine.
+
+When this document refers to this directory, it'll call it `$CLT` for short.
+
+Add some things to your PATH, so you can find `hdc` and `hvigorw`, etc.
+
+```bash
+export PATH=$PATH:$CLT/sdk/default/openharmony/toolchains:$CLT/bin
+```
+
+
+## Putting a HarmonyOS phone in developer mode.
+
+If you want to test and debug your SDL apps natively on a real phone directly,
+you have to put the device in Developer Mode. As the name implies, this is not
+necessary to install published apps, but only apps under development locally.
+Any retail phone running HarmonyOS should be able to do this. This is almost
+identical to how one would do this for local Android development.
+
+On the phone, go to Settings, Device Name (it's "Mate 80" on mine, in a box by
+itself near the top of the main settings page). This will bring up an "About"
+page. Scroll down to "Software version" and tap the version number seven times
+quickly. The phone will pop up a dialog asking if you want to enable developer
+mode and reboot. Say yes. If you've already done this, it will tell you so.
+
+After reboot, go into Settings, System. There will be a new "Developer options"
+section at the bottom. Turn on USB debugging. Plug in a USB cable to your
+computer, and choose to trust the connection (and choose "use USB to transfer
+files" if given the option, otherwise it will only allow charging over USB).
+
+At this point, you should be able to use "hdc" somewhat like you would use
+"adc" on an Android device:
+
+See if the phone is visible/accessible:
+
+```bash
+hdc list targets -v
+```
+
+You can also connect the phone over Wifi on the same LAN and not use USB. If
+you want to try, turn on "Wireless debugging" in Developer Options, note the
+IP address and port, and run:
+
+```bash
+hdc tconn 192.168.1.104:40317
+```
+
+Where the IP address and port match the phone.
+
+(DevEco Studio: add the device by IP address and port by going to "Tools" ->
+"IP Connection" on the menu bar. This appears to be IPv4 only at the moment,
+but it successfully connected to the phone. This was a good option, because
+for whatever reason the phone's USB connection would continually drop and
+reconnect on my VirtualBox Windows 10 VM. But YMMV!)
+
+
+## HarmonyOS App Development with SDL
+
+### Create a new project
+
+Copy the "openharmony-project" directory out of the root of SDL's source tree
+to where you want your project to live, and name it appropriately
+("mygame-openharmony" or whatever). Then, in that new directory:
+
+- in AppScope/app.json5, change at least "bundleName" and "vendor" to match
+  your project.
+- in AppScope/resources/base/element/string.json, change "app_name"'s value to
+  the name of your app.
+- in build-profile.json5, you need to add a signingConfig (this is probably
+  doable without the GUI, but I have not entirely solved how to do this outside
+  of DevEco Studio yet).
+
+Do not create a new project in DevEco Studio, since there are pieces of ArkTS
+scripting that need to be set up correctly for SDL, and that code is already
+present in openharmony-project. You _should_ be able to open the project in
+DevEco Studio once you've set it up.
+
+
+### Add source code to your new project.
+
+Native code building uses CMake, whether you use DevEco Studio or not. This
+makes integrating SDL into your project easy!
+
+Edit the text file entry/src/main/cpp/CMakeLists.txt inside your new project
+directory and change this line:
+
+```cmake
+add_library(main SHARED myapp.c)
+```
+
+"myapp.c" should be replaced with any source files that comprise your app.
+This is a standard CMake project file, so you can get as fancy as you want
+or just have a list of files separated by spaces.
+
+These files do not have to be in this directory; they can live outside of
+the project directory entirely, as long as you specify the correct directory
+to reach them.
+
+You must either copy the SDL source tree into entry/src/main/cpp, or make a
+symlink to it, or edit the `add_subdirectory("SDL")` line to point to it.
+This line makes SDL's source compile with the rest of the project. The
+directory you point to must be the root of an SDL clone, the one that holds
+CMakeLists.txt, src, and include.
+
+Your app _must_ compile to a shared library named "libmain.so" ... the
+"add_library" line does this. Do not build an executable! It must be a
+shared library, due to how HarmonyOS deals with apps. When you're done
+building, you should have a libmain.so and a libSDL3.so file.
+
+
+### Customize your project
+
+If you need some specific permissions (microphone/camera access, internet
+access, etc), you must list them in
+openharmony-project/entry/src/main/module.json5 ...some of these are
+automatically granted on installation, some will require the user to
+approve them on first use at runtime, but all of them must be listed. See
+the "requestPermissions" section of this file for examples. There are more
+permissions available than are listed there!
+
+You must set up some basic strings, before publishing your app, or you will
+either have an app icon with name "label" or fail certification because you
+didn't explain clearly why you need various permissions.
+
+These are in a JSON file in:
+
+openharmony-project/entry/src/main/resources/base/element/string.json
+
+You can do localizations of these strings by making a second copy of the
+file, where "base" is replaced with the proper localization ("en_GB" or
+whatever). See the documentation at:
+
+https://developer.huawei.com/consumer/en/doc/harmonyos-guides/resource-categories-and-access
+
+Static data files that ship with your app go in the directory:
+
+openharmony-project/entry/src/main/resources/rawfile/
+
+The file tree under this directory will be packaged up with the app and made
+available through SDL APIs (see "Filesystem" and "IOStream" sections, below).
+
+
+### Get a HarmonyOS Debug Certificate
+
+You need to sign apps to run them on a real device, even for local debugging
+and testing and not just deployment and release.
+
+For now, just open your project in DevEco Studio, then in the menu bar, go
+to "File", "Project Structure", "Signing Configs", "Automatically generate
+signature". Log in to your Huawei developer account if needed. Let it do the
+work. Later, I copied the project's directory and ".ohos" dir in my home
+directory to a Linux system, adjusted a few Windows absolute paths in
+build-profile.json5, and that was good enough.
+
+
+### Build your app from the command line
+
+Building your project from DevEco Studio is just a single click, but you
+can also build from the command line on macOS, Windows, or Linux.
+
+If you have everything (signing certs, the entire project structure, etc) in
+place, you can build an app from the command line. The HarmonyOS equivalent of
+Android's `gradlew` command is `hvigorw`. Go to the root folder of the project
+and run:
+
+```bash
+hvigorw assembleHap
+```
+
+If everything went well (AND IT OFTEN DOES NOT, READ THE OUTPUT!), you'll have
+a signed HarmonyOS app bundle (a ".hap" file) you can install to a device.
+
+There is a Bash shell script to automate this as much as possible: see the
+"The Helper Script" section, below.
+
+
+### Install on real hardware from the command line.
+
+Make sure the phone is connected (run `hdc list targets -v` to verify).
+
+```bash
+# (or whatever the directory and .hap file are named.)
+hdc install ./entry/build/default/outputs/default/entry-default-signed.hap
+```
+
+There is a Bash shell script to automate this as much as possible: see the
+"The Helper Script" section, below.
+
+
+### Debugging
+
+If you just want to do "printf debugging," then SDL_Log() will write to the
+system's "hilog" output, and you can use `hdc hilog |grep -F "APPBUNDLENAME"`
+to find your app's output in the absolute firehose of text that is produced
+through that interface.
+
+One can use a real GUI debugger through DevEco Studio without a lot of drama,
+and one can also use lldb from the command line with "remote debugging." The
+latter option takes a lot of manual setup, and additionally you have to be
+familiar with LLDB's text interface to find this useful, but it _can_ be done
+from Linux, where DevEco Studio isn't available (and also Windows and macOS,
+if all you have are the command line tools).
+
+There is a Bash shell script to automate this as much as possible: see the
+"The Helper Script" section, below.
+
+The instructions on setup from scratch, without the shell script, are here:
+
+https://developer.huawei.com/consumer/en/doc/harmonyos-guides/debug-lldb#remote-debugging
+
+The only thing that document fails to mention is that the PID number you need
+for LLDB's `attach` command can be obtained by running `hdc shell ps` and
+looking for your app's bundle ID.
+
+
+### The Helper Script
+
+There is a Bash shell script to simplify several tasks, if you're using the
+command line tools. This is not necessary from DevEco Studio. This has only
+been tested on Linux.
+
+Make sure your PATH is set up (see "Install Command Line Tools," above), and
+run this command:
+
+```bash
+SDL/build-scripts/harmonyos-tool.sh openharmony-project --build --debug
+```
+
+"openharmony-project" is literally the path to your project file. It can be a
+relative path, including "." if you are sitting in the directory.
+
+All the other command line arguments are optional, and can be used in any
+combination (for example, you can --launch without --build'ing, or --install
+and --debug in one run, etc.)
+
+If you specify --log, the script will fire up `hdc hilog` in the background
+and filter out everything but your app bundle's log calls, so you can watch
+the log while other tasks complete. It will start this once passed the
+--install stage (even if you didn't install), so you are watching it while
+launching/debugging begins. After all other tasks are done, harmonyos-tool.sh
+won't terminate until either the `hdc` process terminates or the user hits
+CTRL-C (rather, until SIGINT is delivered, which CTRL-C does). The script will
+kill the `hdc` process in that case before terminating itself.
+
+If you specify --clean, the script will clean up any build artifacts from
+previous compiles, etc. This will run before --build, so you can specify
+`--clean --build` for a complete rebuild.
+
+If you specify --build, the script will compile C/C++ code and assemble the
+.hap file to be run on a device or emulator.
+
+If you specify --install, the script will (re)install the .hap on a device
+(which will kill the process if it's currently running).
+
+If you specify --uninstall, the script will remove the existing app on a
+device (which will kill the process if it's currently running).
+
+If you specify --launch, the script will launch the app on a device, if it
+isn't already running.
+
+If you specify --debug, the script will prepare remote debugging via lldb,
+and attach to the running app.
+
+If you specify --kill, the script will force-stop any existing process.
+
+(--debug implies --launch, "--build --launch" and "--build --debug" imply
+--install.)
+
+Most of these tasks obviously need a phone that hdc can talk to.
+
+Note that debugging always attaches to a process, so if you need to debug
+something at startup, plan to add a sleep to the start of the program, as it
+can take several seconds for the attachment to complete. It isn't clear to me
+how to launch an app from the debugger so you can break at startup, but there
+is probably a way.
+
+Also note that this script is fragile in general; be gentle, report bugs, send
+patches.
+
+
+## SDL/HarmonyOS subsystem details
+
+Specifics of actually using SDL on HarmonyOS follow.
+
+
+### SDL platform defines
+
+SDL will define SDL_PLATFORM_OPENHARMONY and SDL_PLATFORM_UNIX. Do be cautioned
+that while HarmonyOS has some Linux kernel compatibility interfaces, it is
+_not_ comparable to a "normal" Linux system in almost any way, so
+SDL_PLATFORM_LINUX is not defined (SDL for Android, which literally uses the
+Linux kernel, follows this same convention inside SDL). HarmonyOS devices
+don't use the actual Linux kernel (they use a microkernel named "HongMeng"),
+but OpenHarmony devices that aren't specifically HarmonyOS _might_ use a Linux
+kernel. Don't make assumptions.
+
+
+### Dynamic API
+
+Presumably users cannot override the SDL build in their phone's apps, so the
+Dynamic API is disabled on this platform.
+
+
+### Log
+
+SDL_Log() and friends will push through hilog, so you can view the output
+from the `hdc hilog` command. Note that logs are preformatted strings by the
+time they hit hilog, so things like "%s" are already processed, and thus
+there is no `{public}` and `{private}` format modifiers available like there
+would be if calling into hilog's APIs directly.
+
+Output from `hdc hilog` for `SDL_Log("Hello world!");` will look something like:
+
+```
+06-29 22:22:02.953 25275 25275 I A00000/org.libsdl.loopwave/SDL/APP: Hello world!
+```
+
+This is kind of chatty, but not unlike Android's `adc logcat` output.
+
+These logs are viewable to end-users, even in release builds, if they have a
+phone in Developer Mode that can talk to `hdc`, so be careful what you log!
+
+Also plan to use "grep" to find relevant logs; `hdc hilog` is an endless
+waterfall of unrelated data that you _will_ get lost in immediately.
+
+
+### Main
+
+HarmonyOS has a fairly complicated startup sequence (described later in this
+document). Apps for this platform should use the "main callbacks" (with
+`#define SDL_USE_MAIN_CALLBACKS`) instead of an ANSI C style "main" entry
+point, as that fits the app design paradigm of OpenHarmony.
+
+However, if you build an app with a standard "main" function, SDL will notice
+this, spin a thread, and attempt to call that function from the new background
+thread, and call exit() when it returns. This _happens_ to work, at least for
+simple test cases that do rendering and touch input, but one uses this path at
+their own risk, as several important things might result in unexpected race
+conditions and unexpected behavior, possibly in a later version of the OS. It
+is strongly recommended that you migrate to the Main Callbacks!
+
+
+### Filesystem
+
+HarmonyOS uses '/' as a path separator, like Unix-style (etc) systems.
+
+SDL_GetBasePath() returns "assets://" unconditionally (see IOStream section,
+below). SDL_GetPrefPath() returns a path under the string returned from
+OH_AbilityRuntime_ApplicationContextGetFilesDir(), which in practice looks
+like "/data/storage/el2/base/files/APPNAME" ... this looks like an absolute
+path, but HarmonyOS maps this to an app-and-user-specific path behind the
+scenes. The prefpath is readable/writable with both SDL_IOStream and "normal"
+APIs like fopen().
+
+There is also SDL_GetOpenHarmonyInternalStoragePath(), which returns the base
+directory used for SDL_GetPrefPath() without extra subdirs appended to it or
+mkdir() calls issued, but generally SDL_GetPrefPath() is preferred as the
+more-portable option.
+
+
+### IOStream
+
+This works like Android: some files are accessible like normal filesystem
+things, and can be accessed with fopen(), open(), etc. Other things are
+installed with the application (the "HAP"), and they need special APIs to
+access, that sort of map to Android's AAssetManager, but a little more
+powerful (these are called "RawFile" APIs on HarmonyOS).
+
+Both types of files can be accessed as SDL_IOStreams through SDL_IOFromFile().
+SDL uses the same conventions as Android here: if the file path is a relative
+path or prefixed with "assets://", it'll use the RawFile APIs. Absolute paths
+will use "normal" filesystem APIs. SDL_GetBasePath() returns "assets://", so
+one can access files installed with the app with SDL_IOFromFile().
+
+"assets://" is entirely an SDL3 construct; it will not work with fopen(), and
+is not understood by the system itself.
+
+
+### Async I/O
+
+SDL uses the "generic" asyncio backend here (a thread pool that uses
+SDL_IOStreams). Not only is there no sort of io_uring-style interface (afaik),
+but this also means you can async-load stuff from "assets://" paths without
+problems.
+
+
+### Video
+
+One SDL_Window is allowed at a time and it takes the entire available display
+(like Android). Currently we only report a single display (the phone/tablet's
+screen).
+
+Setting an SDL_Window to be fullscreen means it will turn off the system
+status and navigation bars, allowing the window to use those extra pixels. A
+non-fullscreen window leaves those bars in place, letting users see the
+current time, battery/network/etc status, and other icon things.
+
+Touch events work, and multitouch is supported. Mouse events (from an external,
+physical mouse) are supported. Physical keyboard input works, and enabling
+text input will pop up a screen keyboard.
+
+Clipboard is supported for text. SDL_SetClipboardText() works as expected
+and other apps will see the text. However, an app needs the
+`ohos.permission.READ_PASTEBOARD` permission to obtain data from the system
+clipboard, and this is a _restricted_ permission: you must list it in
+modules.json5, prompt the user to approve it beforehand by calling
+SDL_RequestOpenHarmonyPermission(), and also the app developer must
+explicitly apply for permission from Huawei through AppGallery Connect!
+Consider if you actually need to read clipboard data into your app that badly.
+
+SDL_GetSystemTheme() and SDL_EVENT_SYSTEM_THEME_CHANGED both work.
+
+
+### Render/GPU
+
+OpenGL ES 3 is supported, as is Vulkan. Desktop OpenGL is supported by
+the platform, too, but for now support is disabled within SDL itself as likely
+superfluous (until it is not, open an issue if you need it). EGL is used to
+manage GL contexts internally, and a HarmonyOS-specific Vulkan extension is
+provided to get a surface.
+
+Like other platforms, we favor OpenGL ES 2 for the 2D renderer; Vulkan and "GPU"
+also works, but SDL's GLES is assumed to be more trusted at the current moment,
+and historically GLES has been more solid on mobile devices than Vulkan, but
+HarmonyOS-based phones might have significantly better Vulkan support than
+other comparable smartphones; this is yet to be determined.
+
+Since Vulkan works, the SDL3 GPU API is available and operational, in addition
+to the hardware-accelerated 2D Renderer API.
+
+
+### Audio
+
+HarmonyOS support OpenSL ES, but like on Android, it is deprecated. As our
+OpenSL ES backend is heavy with Android-specific code, it is not used on
+HarmonyOS. There is a new backend using HarmonyOS's OHAudio API. This is
+almost exactly like Android's AAudio API.
+
+Recording works, but you must request `ohos.permission.MICROPHONE` permission
+before it will work. You must list it in modules.json5, and prompt the user to
+approve it beforehand by calling SDL_RequestOpenHarmonyPermission(). Once
+permission is granted, recording audio devices can be opened as usual, and
+this permission remains for future runs, unless the user revokes in in the
+system preferences later. On HarmonyOS, if a user denies permission, they will
+never get the popup request dialog again, and must go and grant permission
+through the system preferences. Plan accordingly if you want your UI to
+explain this to users.
+
+Note that SDL for Android currently requests permission from the user on
+behalf of the app when opening a recording device, and blocks until the user
+approves or denies the request, but on HarmonyOS this is not done. This may
+change in the future, but for now the app should handle requesting permission
+before opening the device. If a future revision of SDL tries to manage the
+permission, it won't hurt anything if the app has already taken care of it.
+
+
+### Power
+
+Battery percent, and whether the device is plugged in, are reported.
+
+
+### Thread
+
+OpenHarmony uses pthreads, and are fully supported.
+
+
+### LoadSO
+
+HarmonyOS uses ELF shared libraries and has a POSIX-style dlopen() mechanism,
+so SDL_LoadObject() works as expected.
+
+
+### Tray
+
+The Tray API uses the "dummy" backend as this functionality doesn't make sense
+on a phone.
+
+
+### Time
+
+The Time and Timer subsystems use the usual Unix implementations and work fine.
+
+
+### Locale
+
+SDL_GetPreferredLocales() returns a single locale, as specified in System
+Preferences -> "System" -> "Language & Region". In my case, I have it set to
+English/United States, so SDL reports "en_US".
+
+SDL will send SDL_EVENT_LOCALE_CHANGED events if the user changes their locale
+in the system preferences while the app is running.
+
+
+### Camera
+
+Camera access works. You must request the `ohos.permission.CAMERA` permission
+in modules.json5. SDL will handle the UI to prompt the user for permission
+during SDL_OpenCamera() if necessary, so the app doesn't have to manage this
+further (unlike using a microphone in the audio API, SDL's camera API was
+built on the idea that the user's permission or refusal might be coming at a
+later time).
+
+
+### Misc
+
+SDL_OpenURL() works, and will launch the appropriate app ("ability") for
+whatever protocol is specified: "https://", etc, URLs will bring up a web
+browser, custom URL schemes can launch specific apps, etc.
+
+
+### STILL TODO
+
+- Message boxes
+- Assert
+- Dialog
+- Haptic
+- HIDAPI
+- Joystick
+- Process
+- Sensor
+- Storage
+- Pen (tablets work over USB-C on the phone, but this isn't wired up in SDL at all)
+- SDL_EVENT_DISPLAY_ORIENTATION (window resizes when phone orientation changes, this event doesn't fire yet, though)
+- Can external displays be used over USB-C => HDMI?  (yes, at least for mirroring)
+- SDL_HINT_ORIENTATION (for runtime control; but you _can_ explicitly set the "orientation" setting in module.json5 for now if you need a specific global, unchanging setting.)
+
+## Building SDL for HarmonyOS
+
+(Skip this if you are developing an SDL-based app and not SDL itself. There's
+a different section of this document for you, later.)
+
+SDL for HarmonyOS is built with CMake, like other platforms. Please note that
+you _must_ use the CMake that comes with HarmonyOS's development tools, or
+building will fail, as standard CMake does not recognize this platform at all
+currently.
+
+Generate a project file for building SDL. We use Ninja, because it's a fast
+command line tool that will maximize multi-CPU compiling, but it's probably
+possible to generate a real Visual Studio, Xcode, etc, project.
+
+```bash
+cd SDL
+mkdir buildbot-ohos   # or whatever you want to call the build directory.
+cd buildbot-ohos
+$CLT/sdk/default/openharmony/native/build-tools/cmake/bin/cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=$CLT/sdk/default/openharmony/native/build/cmake/ohos.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DOHOS_COMPATIBLE_SDK_VERSION=16.0.0 ..
+```
+
+(One can also add -DOHOS_ARCH=cputype, where "cputype" is "arm64-v8a" for ARM64, "armeabi-v7a" for ARM32, and "x86_64" Intel 64-bit. It defaults to ARM64 if unspecified.)
+
+
+If all went well, you should be able to build SDL for HarmonyOS now. In our
+case, build with Ninja:
+
+```bash
+ninja
+```
+
+Or use whatever instructs Xcode/Visual Studio/etc to build things.
+
+
+
+## Startup sequence
+
+(You can probably skip this section if you just want to target HarmonyOS with
+an SDL3-based app and not delve into the low-level details.)
+
+Starting an SDL app on HarmonyOS works differently than any other platform,
+but efforts have been made to _hide_ most of this from the C programmer, so
+this should mostly feel like any other SDL app they have built in C before.
+
+This is how startup works:
+
+(Exact paths might vary, and maybe this will simplify later.)
+
+- The actual application entry point is in ArkTS, in the source file
+  `$PROJECT/entry/src/main/ets/entryability/EntryAbility.ets`.
+- This file defines a class, EntryAbility, that extends UIAbility, and
+  also imports the native SDL library.
+- By now, libSDL3.so has been loaded. In a shared library constructor named
+  SDL_RegisterNativeInterfaces, it uses NAPI to register a native module. Once
+  this module initializes, we'll be able to call into SDL's C code from ArkTS
+  through interfaces we defined in SDL_Init_Native_Interfaces(). This is in
+  SDL/src/core/openharmony/SDL_openharmony.c. This will _also_ hook into
+  XComponent's API, to register some event callbacks.
+- EntryAbility has an overridden method named onCreate. When this
+  runs, the app ("Ability" in HarmonyOS terms) is starting up. This function
+  calls into the sdl module we registered to hand it a few objects (including
+  the EntryAbility instance, and some small things that are trivial to create
+  in ArkTS.
+- SDL, now in native code, will save off those objects as appropriate, and
+  also monkey-patch EntryAbility, so that it now has several overridden
+  methods that only exist in native code. This lets us deal with ArkTS-only
+  interfaces on EntryAbility that the system intends to interact with, without
+  providing any ArkTS scripting for them.
+- EntryAbility now has an overridden method named onWindowStageCreate. When this
+  runs, the system is ready for the app to display to the screen. This
+  method, in C, calls `windowStage.loadContent('pages/Index', ...)`.
+- This causes the app to load `$PROJECT/entry/src/main/ets/pages/Index.ets`.
+- Index.ets specifies a layout for the window/view/whatever. We simply have a
+  single row and column containing a single "XComponent" which is more or
+  less a UI widget backed by native code.
+- When the XComponent's OnSurfaceCreated callback fires (landing in
+  SDL_XComponent_OnSurfaceCreatedCallback()), we are then ready to hand control
+  to the actual C application. This starts in
+  SDL_OpenHarmonyMainSurfaceCreated(), which eventually lands in
+  RunAppOpenHarmonyMain().
+- Here we load libmain.so, which is where the app's actual C code lives.
+  If we see if libmain contains a symbol named "SDL_main", we spin a thread
+  and call into that symbol from the new thread as a standard ANSI C "main"
+  entry point, and exit() the process when it returns (which looks like a
+  crash on HarmonyOS, so please don't return.)
+- If there was no symbol called "SDL_main", we look for the Main Callbacks
+  symbols in libmain.so: SDL_AppInit, SDL_AppIterate, etc. If found, we will
+  call libmain.so's SDL_AppInit() and respond appropriately. If not found, we
+  panic and terminate the process, since we're out of options and something
+  was obviously built incorrectly.
+- Later, whenever the XComponent fires its OnFrame callback, we will call
+  SDL_OpenHarmonyOnFrameCallback(), which calls libmain.so's SDL_AppIterate(),
+  if we ended up using the Main Callbacks.
+
+
+
+## Incomplete notes
+
+Everything below here is incomplete, in-progress, and maybe incomprehensible.
+You can stop reading now, if you like.
+
+- A lot of how to get started with native code and rendering was gleaned from
+  https://gitee.com/harmonyos_samples/ndk-opengl and then trying to find
+  various symbols used in there in the HarmonyOS documentation.
+
+- https://github.com/openharmony/app_samples/ETSUI/XComponent, seems to do
+  a lot of the native startup without the windowStage nonsense. Not clear if
+  this still works, or is safe to do, but it _would_ simplify things a little.
+
+
+### Android mappings
+
+These are not direct mappings (the command lines are not identical, etc).
+
+- "adc" command line tool: "hdc"
+- "adc logcat": "hdc hilog"
+- "gradlew": "hvigorw"
+
+
+### Get a HarmonyOS Debug Certificate without DevEco Studio
+
+This is a giant dump of notes for doing this manually, which I didn't _quite_
+manage to get working. Huawei has docs on this, which I've sort of half limped
+through.
+
+For now, just create your project in DevEco Studio,
+then in the menu bar, got to "File" -> "Project Structure" ->
+"Signing Configs" -> "Automatically generate signature". Log in to your Huawei
+developer account if needed. Let it do the work. Later, I copied the project's
+directory and ".ohos" dir in my home directory to a Linux system, adjusted a
+few Windows absolute paths in build-profile.json5, and that was good enough.
+
+
+(Ignore these notes if you aren't improving them.)
+
+If you aren't using DevEco Studio to automatically generate signatures:
+
+You need a signing certificate before you can install apps on a real HarmonyOS
+device.
+
+You get a certificate by generating a Certificate Signing Request (a "CSR")
+and submitting it to Huawei.
+
+Right now this needs DevEco Studio to generate the CSR file, as a one-time
+step. Go into the IDE, click the hamburger menu icon on the top left of the
+window, choose the "Build" menu item, and then click "Generate Key and CSR".
+In the dialog that pops up, you'll first make a signing key in ".p12" format.
+Enter a filename and save path, choose a password for the key. Fill in the
+"Alias" which is any short, unique, possibly-ASCII, human readable string.
+Ignore the Advanced section.
+
+Click "Next". Now you'll generate the CSR file. The same .p12 file, password
+and alias you just generated will be prefilled in the new dialog. Just tell it
+where to save the new .csr file. Click "Finish" and you're done with DevEco
+Studio for this task.
+
+The Common Name ("CN=" field) on the Certificate Request is what DevEco Studio
+refers as an "alias." I put this in as "icculus.org" for mine, but it might
+just need to be unique. It's required, all the other fields are not (indeed,
+DevEco Studio does not ask for them by default and leaves them blank).
+
+*TODO IN THE FUTURE:* the CSR files are in a standard format, so OpenSSL's
+command line utility can _probably_ generate them without having to go through
+DevEco Studio, but anything OpenSSL is complicated and needs research.
+
+Dumping the .csr file that DevEco Studio generated (using the command line:
+`openssl req -in MyCSRFile.csr -noout -text`) reports the following, so there
+are probably OpenSSL incantations to generate your own .p12 and .csr file
+directly.
+
+```
+Certificate Request:
+    Data:
+        Version: 1 (0x0)
+        Subject: C=, ST=, L=, O=, OU=, CN=[the "Alias" that I entered is here]
+        Subject Public Key Info:
+            Public Key Algorithm: id-ecPublicKey
+                Public-Key: (256 bit)
+                pub:
+                    [hex values for public key are here]
+                ASN1 OID: prime256v1
+                NIST CURVE: P-256
+        Attributes:
+            Requested Extensions:
+                X509v3 Subject Key Identifier: 
+                    [hex values for key identifier are here]
+    Signature Algorithm: ecdsa-with-SHA256
+    Signature Value:
+        [hex values for signature are here]
+```
+
+Once you have a .csr file, go to Huawei's AppGallery Connect portal, and find
+the "Certificates, app IDs, and profiles" tab at the top. Click the "New
+certificate" button, and upload the .csr file you generated. Give it a name,
+set the "Certificate type" to "Debug certificate" and point to your .csr file.
+Click "Submit".
+
+If everything went okay, you'll have a certificate listed for download. This
+should be an immediate thing, it doesn't need approval on Huawei's side.
+Download the .cer file and save it for later.
+

+ 1 - 0
docs/README-platforms.md

@@ -9,6 +9,7 @@ SDL3 has been known to work on the following platforms at some point:
 - [Emscripten](README-emscripten.md) (Web browsers)
 - [Emscripten](README-emscripten.md) (Web browsers)
 - [FreeBSD](README-bsd.md)
 - [FreeBSD](README-bsd.md)
 - [Haiku OS](README-haiku.md)
 - [Haiku OS](README-haiku.md)
+- [HarmonyOS/OpenHarmony](README-harmonyos.md)
 - [iOS](README-ios.md)
 - [iOS](README-ios.md)
 - [Linux](README-linux.md)
 - [Linux](README-linux.md)
 - [macOS](README-macos.md) (10.14 and later)
 - [macOS](README-macos.md) (10.14 and later)

+ 9 - 4
include/SDL3/SDL_iostream.h

@@ -257,6 +257,10 @@ typedef struct SDL_IOStream SDL_IOStream;
  *   to an Android NDK `AAsset *`, that this SDL_IOStream is using to access
  *   to an Android NDK `AAsset *`, that this SDL_IOStream is using to access
  *   the filesystem. If SDL used some other method to access the filesystem,
  *   the filesystem. If SDL used some other method to access the filesystem,
  *   this property will not be set.
  *   this property will not be set.
+ * - `SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER`: a pointer, that can be
+ *   cast to an OpenHarmony/HarmonyOS `RawFile64 *`, that this SDL_IOStream is
+ *   using to access the filesystem. If SDL used some other method to access
+ *   the filesystem, this property will not be set.
  *
  *
  * \param file a UTF-8 string representing the filename to open.
  * \param file a UTF-8 string representing the filename to open.
  * \param mode an ASCII string representing the mode to be used for opening
  * \param mode an ASCII string representing the mode to be used for opening
@@ -277,10 +281,11 @@ typedef struct SDL_IOStream SDL_IOStream;
  */
  */
 extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, const char *mode);
 extern SDL_DECLSPEC SDL_IOStream * SDLCALL SDL_IOFromFile(const char *file, const char *mode);
 
 
-#define SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER    "SDL.iostream.windows.handle"
-#define SDL_PROP_IOSTREAM_STDIO_FILE_POINTER        "SDL.iostream.stdio.file"
-#define SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER    "SDL.iostream.file_descriptor"
-#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER    "SDL.iostream.android.aasset"
+#define SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER        "SDL.iostream.windows.handle"
+#define SDL_PROP_IOSTREAM_STDIO_FILE_POINTER            "SDL.iostream.stdio.file"
+#define SDL_PROP_IOSTREAM_FILE_DESCRIPTOR_NUMBER        "SDL.iostream.file_descriptor"
+#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER        "SDL.iostream.android.aasset"
+#define SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER "SDL.iostream.openharmony.rawfile64"
 
 
 /**
 /**
  * Use this function to prepare a read-write memory buffer for use with
  * Use this function to prepare a read-write memory buffer for use with

+ 7 - 0
include/SDL3/SDL_keyboard.h

@@ -464,6 +464,12 @@ typedef enum SDL_Capitalization
  *   use, overriding other properties. This is documented at
  *   use, overriding other properties. This is documented at
  *   https://developer.android.com/reference/android/text/InputType
  *   https://developer.android.com/reference/android/text/InputType
  *
  *
+ * On HarmonyOS/OpenHarmony you can directly specify the input type:
+ *
+ * - `SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER` - the text input type to
+ *   use, overriding other properties. This is documented at
+ *   https://developer.android.com/reference/android/text/InputType
+ *
  * \param window the window to enable text input.
  * \param window the window to enable text input.
  * \param props the properties to use.
  * \param props the properties to use.
  * \returns true on success or false on failure; call SDL_GetError() for more
  * \returns true on success or false on failure; call SDL_GetError() for more
@@ -489,6 +495,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_StartTextInputWithProperties(SDL_Window *wi
 #define SDL_PROP_TEXTINPUT_DEFAULT_TEXT_STRING          "SDL.textinput.default_text"
 #define SDL_PROP_TEXTINPUT_DEFAULT_TEXT_STRING          "SDL.textinput.default_text"
 #define SDL_PROP_TEXTINPUT_MAX_LENGTH_NUMBER            "SDL.textinput.max_length"
 #define SDL_PROP_TEXTINPUT_MAX_LENGTH_NUMBER            "SDL.textinput.max_length"
 #define SDL_PROP_TEXTINPUT_ANDROID_INPUTTYPE_NUMBER     "SDL.textinput.android.inputtype"
 #define SDL_PROP_TEXTINPUT_ANDROID_INPUTTYPE_NUMBER     "SDL.textinput.android.inputtype"
+#define SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER "SDL.textinput.openharmony.inputtype"
 
 
 /**
 /**
  * Check whether or not Unicode text input events are enabled for a window.
  * Check whether or not Unicode text input events are enabled for a window.

+ 13 - 0
include/SDL3/SDL_main.h

@@ -193,6 +193,19 @@
          */
          */
         #define SDL_MAIN_EXPORTED
         #define SDL_MAIN_EXPORTED
 
 
+    #elif defined(SDL_PLATFORM_OPENHARMONY)
+        /* On HarmonyOS/OpenHarmony SDL does a lot of work to gain control of the app. */
+        #define SDL_MAIN_NEEDED
+
+        /* As this is launched from ArkTS, the real entry point (main() function)
+           is outside of the the binary built from this code.
+           This define makes sure that, unlike on other platforms, SDL_main.h
+           and SDL_main_impl.h export an `SDL_main()` function (to be called
+           from ArkTS, sort of), but don't implement a native
+           `int main(int argc, char* argv[])` or similar.
+         */
+        #define SDL_MAIN_EXPORTED
+
     #elif defined(SDL_PLATFORM_EMSCRIPTEN)
     #elif defined(SDL_PLATFORM_EMSCRIPTEN)
         /* On Emscripten, SDL provides a main function that converts URL
         /* On Emscripten, SDL provides a main function that converts URL
            parameters that start with "SDL_" to environment variables, so
            parameters that start with "SDL_" to environment variables, so

+ 7 - 0
include/SDL3/SDL_main_impl.h

@@ -49,6 +49,13 @@
             /* currently there are no platforms that _need_ a magic entry point here
             /* currently there are no platforms that _need_ a magic entry point here
                for callbacks, but if one shows up, implement it here. */
                for callbacks, but if one shows up, implement it here. */
 
 
+        #elif defined(SDL_PLATFORM_OPENHARMONY)
+            /* HarmonyOS/OpenHarmony: we look up SDL_AppInit/SDL_AppIterate/etc in libmain.so and call them directly.
+               Since we don't need a `main` entry point, we don't generate an SDL_main implementation here _at all_. */
+            #ifndef SDL_MAIN_USE_CALLBACKS  /* !!! FIXME, remove this later? */
+            #error SDL3 on OpenHarmony currently requires the SDL Main Callbacks to operate.
+            #endif
+
         #else /* use a standard SDL_main, which the app SHOULD NOT ALSO SUPPLY. */
         #else /* use a standard SDL_main, which the app SHOULD NOT ALSO SUPPLY. */
 
 
             /* this define makes the normal SDL_main entry point stuff work...we just provide SDL_main() instead of the app. */
             /* this define makes the normal SDL_main entry point stuff work...we just provide SDL_main() instead of the app. */

+ 11 - 0
include/SDL3/SDL_platform_defines.h

@@ -112,6 +112,17 @@
 #undef SDL_PLATFORM_LINUX
 #undef SDL_PLATFORM_LINUX
 #endif
 #endif
 
 
+#if defined(__OHOS__)
+
+/**
+ * A preprocessor macro that is only defined if compiling for OpenHarmony/HarmonyOS.
+ *
+ * \since This macro is available since SDL 3.6.0.
+ */
+#define SDL_PLATFORM_OPENHARMONY 1
+#undef SDL_PLATFORM_LINUX
+#endif
+
 #if defined(__unix__) || defined(__unix) || defined(unix)
 #if defined(__unix__) || defined(__unix) || defined(unix)
 
 
 /**
 /**

+ 94 - 1
include/SDL3/SDL_system.h

@@ -522,7 +522,7 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetAndroidExternalStoragePath(void)
 extern SDL_DECLSPEC const char * SDLCALL SDL_GetAndroidCachePath(void);
 extern SDL_DECLSPEC const char * SDLCALL SDL_GetAndroidCachePath(void);
 
 
 /**
 /**
- * Callback that presents a response from a SDL_RequestAndroidPermission call.
+ * Callback that presents SDL_RequestAndroidPermission() results.
  *
  *
  * \param userdata an app-controlled pointer that is passed to the callback.
  * \param userdata an app-controlled pointer that is passed to the callback.
  * \param permission the Android-specific permission name that was requested.
  * \param permission the Android-specific permission name that was requested.
@@ -616,6 +616,99 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SendAndroidMessage(Uint32 command, int para
 
 
 #endif /* SDL_PLATFORM_ANDROID */
 #endif /* SDL_PLATFORM_ANDROID */
 
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+/**
+ * Query OpenHarmony API level of the current device.
+ *
+ * - API level 20: OpenHarmony 6.0.0
+ * - API level 18: OpenHarmony 5.1.0
+ * - API level 16: OpenHarmony 5.0.4
+ * - API level 15: OpenHarmony 5.0.3
+ * - API level 14: OpenHarmony 5.0.2
+ * - API level 13: OpenHarmony 5.0.1
+ * - API level 12: OpenHarmony 5.0.0
+ * - API level 11: OpenHarmony 4.1.0
+ * - API level 10: OpenHarmony 4.0.0
+ * - API level 9: OpenHarmony 3.2.0
+ *
+ * \returns the OpenHarmony API level.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.6.0.
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void);
+
+/**
+ * Get the path used for internal storage for this OpenHarmony application.
+ *
+ * This path is unique to your application and cannot be written to by other
+ * applications.
+ *
+ * Your internal storage path is typically:
+ * `/data/storage/el2/base/files`.
+ *
+ * \returns the path used for internal storage or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.6.0.
+ */
+extern SDL_DECLSPEC const char * SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void);
+
+/**
+ * Callback that presents SDL_RequestOpenHarmonyPermission() results.
+ *
+ * \param userdata an app-controlled pointer that is passed to the callback.
+ * \param permission the OpenHarmony-specific permission name that was requested.
+ * \param granted true if permission is granted, false if denied.
+ *
+ * \since This datatype is available since SDL 3.6.0.
+ *
+ * \sa SDL_RequestOpenHarmonyPermission
+ */
+typedef void (SDLCALL *SDL_RequestOpenHarmonyPermissionCallback)(void *userdata, const char *permission, bool granted);
+
+/**
+ * Request permissions at runtime, asynchronously.
+ *
+ * You do not need to call this for built-in functionality of SDL; recording
+ * from a microphone or reading images from a camera, using standard SDL APIs,
+ * will manage permission requests for you.
+ *
+ * This function never blocks. Instead, the app-supplied callback will be
+ * called when a decision has been made. This callback may happen on a
+ * different thread, and possibly much later, as it might wait on a user to
+ * respond to a system dialog. If permission has already been granted for a
+ * specific entitlement, the callback will still fire, probably on the current
+ * thread and before this function returns.
+ *
+ * If the request submission fails, this function returns false and the
+ * callback will NOT be called, but this should only happen in catastrophic
+ * conditions, like memory running out. Normally there will be a yes or no to
+ * the request through the callback.
+ *
+ * For the `permission` parameter, choose a value from here:
+ *
+ * https://developer.huawei.com/consumer/en/doc/harmonyos-guides/app-permissions
+ *
+ * Strings should be in the form of "ohos.permission.PERMISSION_NAME".
+ *
+ * \param permission the permission to request.
+ * \param cb the callback to trigger when the request has a response.
+ * \param userdata an app-controlled pointer that is passed to the callback.
+ * \returns true if the request was submitted, false if there was an error
+ *          submitting. The result of the request is only ever reported
+ *          through the callback, not this return value.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.6.0.
+ */
+extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata);
+
+#endif /* SDL_PLATFORM_OPENHARMONY */
+
+
 /**
 /**
  * Query if the current device is a phone.
  * Query if the current device is a phone.
  *
  *

+ 12 - 0
include/SDL3/SDL_video.h

@@ -1579,6 +1579,15 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window)
  * - `SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER`: the EGLSurface associated with
  * - `SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER`: the EGLSurface associated with
  *   the window
  *   the window
  *
  *
+ * On OpenHarmony/HarmonyOS:
+ *
+ * - `SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER`: the OH_NativeXComponent
+ *   associated with the window
+ * - `SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER`: the OHNativeWindow associated
+ *   with the window
+ * - `SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER`: the EGLSurface associated with
+ *   the window
+ *
  * On iOS:
  * On iOS:
  *
  *
  * - `SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER`: the `(__unsafe_unretained)`
  * - `SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER`: the `(__unsafe_unretained)`
@@ -1706,6 +1715,9 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetWindowProperties(SDL_Window
 #define SDL_PROP_WINDOW_HDR_HEADROOM_FLOAT                          "SDL.window.HDR_headroom"
 #define SDL_PROP_WINDOW_HDR_HEADROOM_FLOAT                          "SDL.window.HDR_headroom"
 #define SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER                      "SDL.window.android.window"
 #define SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER                      "SDL.window.android.window"
 #define SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER                     "SDL.window.android.surface"
 #define SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER                     "SDL.window.android.surface"
+#define SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER              "SDL.window.openharmony.xcomponent"
+#define SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER                  "SDL.window.openharmony.window"
+#define SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER                 "SDL.window.openharmony.surface"
 #define SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER                        "SDL.window.uikit.window"
 #define SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER                        "SDL.window.uikit.window"
 #define SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER                 "SDL.window.uikit.metal_view_tag"
 #define SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER                 "SDL.window.uikit.metal_view_tag"
 #define SDL_PROP_WINDOW_UIKIT_OPENGL_FRAMEBUFFER_NUMBER             "SDL.window.uikit.opengl.framebuffer"
 #define SDL_PROP_WINDOW_UIKIT_OPENGL_FRAMEBUFFER_NUMBER             "SDL.window.uikit.opengl.framebuffer"

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

@@ -297,6 +297,7 @@
 #cmakedefine SDL_AUDIO_DRIVER_NGAGE 1
 #cmakedefine SDL_AUDIO_DRIVER_NGAGE 1
 #cmakedefine SDL_AUDIO_DRIVER_QNX 1
 #cmakedefine SDL_AUDIO_DRIVER_QNX 1
 #cmakedefine SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER 1
 #cmakedefine SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER 1
+#cmakedefine SDL_AUDIO_DRIVER_OHAUDIO 1
 #cmakedefine SDL_AUDIO_DRIVER_PRIVATE 1
 #cmakedefine SDL_AUDIO_DRIVER_PRIVATE 1
 
 
 /* Enable various input drivers */
 /* Enable various input drivers */
@@ -404,6 +405,7 @@
 
 
 /* Enable various video drivers */
 /* Enable various video drivers */
 #cmakedefine SDL_VIDEO_DRIVER_ANDROID 1
 #cmakedefine SDL_VIDEO_DRIVER_ANDROID 1
+#cmakedefine SDL_VIDEO_DRIVER_OPENHARMONY 1
 #cmakedefine SDL_VIDEO_DRIVER_COCOA 1
 #cmakedefine SDL_VIDEO_DRIVER_COCOA 1
 #cmakedefine SDL_VIDEO_DRIVER_DUMMY 1
 #cmakedefine SDL_VIDEO_DRIVER_DUMMY 1
 #cmakedefine SDL_VIDEO_DRIVER_EMSCRIPTEN 1
 #cmakedefine SDL_VIDEO_DRIVER_EMSCRIPTEN 1
@@ -513,6 +515,7 @@
 #cmakedefine SDL_POWER_VITA 1
 #cmakedefine SDL_POWER_VITA 1
 #cmakedefine SDL_POWER_PSP 1
 #cmakedefine SDL_POWER_PSP 1
 #cmakedefine SDL_POWER_N3DS 1
 #cmakedefine SDL_POWER_N3DS 1
+#cmakedefine SDL_POWER_OPENHARMONY 1
 
 
 #cmakedefine SDL_POWER_PRIVATE 1
 #cmakedefine SDL_POWER_PRIVATE 1
 
 
@@ -530,6 +533,7 @@
 #cmakedefine SDL_FILESYSTEM_PS2 1
 #cmakedefine SDL_FILESYSTEM_PS2 1
 #cmakedefine SDL_FILESYSTEM_N3DS 1
 #cmakedefine SDL_FILESYSTEM_N3DS 1
 #cmakedefine SDL_FILESYSTEM_DOS 1
 #cmakedefine SDL_FILESYSTEM_DOS 1
+#cmakedefine SDL_FILESYSTEM_OPENHARMONY 1
 
 
 #cmakedefine SDL_FILESYSTEM_PRIVATE 1
 #cmakedefine SDL_FILESYSTEM_PRIVATE 1
 
 
@@ -551,6 +555,7 @@
 #cmakedefine SDL_CAMERA_DRIVER_V4L2 1
 #cmakedefine SDL_CAMERA_DRIVER_V4L2 1
 #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA 1
 #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA 1
 #cmakedefine SDL_CAMERA_DRIVER_ANDROID 1
 #cmakedefine SDL_CAMERA_DRIVER_ANDROID 1
+#cmakedefine SDL_CAMERA_DRIVER_OPENHARMONY 1
 #cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN 1
 #cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN 1
 #cmakedefine SDL_CAMERA_DRIVER_MEDIAFOUNDATION 1
 #cmakedefine SDL_CAMERA_DRIVER_MEDIAFOUNDATION 1
 #cmakedefine SDL_CAMERA_DRIVER_PIPEWIRE 1
 #cmakedefine SDL_CAMERA_DRIVER_PIPEWIRE 1

+ 12 - 0
openharmony-project/.gitignore

@@ -0,0 +1,12 @@
+/node_modules
+/oh_modules
+/local.properties
+/.idea
+**/build
+/.hvigor
+.cxx
+/.clangd
+/.clang-format
+/.clang-tidy
+**/.test
+/.appanalyzer

+ 11 - 0
openharmony-project/AppScope/app.json5

@@ -0,0 +1,11 @@
+{
+  "app": {
+    "bundleName": "com.example.app",
+    "vendor": "example",
+    "versionCode": 1000000,
+    "versionName": "1.0.0",
+    "buildVersion": "1",
+    "icon": "$media:layered_image",
+    "label": "$string:app_name"
+  }
+}

+ 8 - 0
openharmony-project/AppScope/resources/base/element/string.json

@@ -0,0 +1,8 @@
+{
+  "string": [
+    {
+      "name": "app_name",
+      "value": "app"
+    }
+  ]
+}

BIN
openharmony-project/AppScope/resources/base/media/background.png


BIN
openharmony-project/AppScope/resources/base/media/foreground.png


+ 7 - 0
openharmony-project/AppScope/resources/base/media/layered_image.json

@@ -0,0 +1,7 @@
+{
+  "layered-image":
+  {
+    "background" : "$media:background",
+    "foreground" : "$media:foreground"
+  }
+}

+ 42 - 0
openharmony-project/build-profile.json5

@@ -0,0 +1,42 @@
+{
+  "app": {
+    "products": [
+      {
+        "name": "default",
+        "signingConfig": "default",
+        "targetSdkVersion": "5.0.4(16)",
+        "compatibleSdkVersion": "5.0.4(16)",
+        "runtimeOS": "HarmonyOS",
+        "buildOption": {
+          "nativeCompiler": "BiSheng",
+          "strictMode": {
+            "caseSensitiveCheck": true,
+            "useNormalizedOHMUrl": true
+          }
+        }
+      }
+    ],
+    "buildModeSet": [
+      {
+        "name": "debug",
+      },
+      {
+        "name": "release"
+      }
+    ]
+  },
+  "modules": [
+    {
+      "name": "entry",
+      "srcPath": "./entry",
+      "targets": [
+        {
+          "name": "default",
+          "applyToProducts": [
+            "default"
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 32 - 0
openharmony-project/code-linter.json5

@@ -0,0 +1,32 @@
+{
+  "files": [
+    "**/*.ets"
+  ],
+  "ignore": [
+    "**/src/ohosTest/**/*",
+    "**/src/test/**/*",
+    "**/src/mock/**/*",
+    "**/node_modules/**/*",
+    "**/oh_modules/**/*",
+    "**/build/**/*",
+    "**/.preview/**/*"
+  ],
+  "ruleSet": [
+    "plugin:@performance/recommended",
+    "plugin:@typescript-eslint/recommended"
+  ],
+  "rules": {
+    "@security/no-unsafe-aes": "error",
+    "@security/no-unsafe-hash": "error",
+    "@security/no-unsafe-mac": "warn",
+    "@security/no-unsafe-dh": "error",
+    "@security/no-unsafe-dsa": "error",
+    "@security/no-unsafe-ecdsa": "error",
+    "@security/no-unsafe-rsa-encrypt": "error",
+    "@security/no-unsafe-rsa-sign": "error",
+    "@security/no-unsafe-rsa-key": "error",
+    "@security/no-unsafe-dsa-key": "error",
+    "@security/no-unsafe-dh-key": "error",
+    "@security/no-unsafe-3des": "error"
+  }
+}

+ 6 - 0
openharmony-project/entry/.gitignore

@@ -0,0 +1,6 @@
+/node_modules
+/oh_modules
+/.preview
+/build
+/.cxx
+/.test

+ 45 - 0
openharmony-project/entry/build-profile.json5

@@ -0,0 +1,45 @@
+{
+  "apiType": "stageMode",
+  "buildOption": {
+    "resOptions": {
+      "copyCodeResource": {
+        "enable": false
+      }
+    },
+    "externalNativeOptions": {
+      "path": "./src/main/cpp/CMakeLists.txt",
+      "arguments": "-DCMAKE_PLATFORM_NO_VERSIONED_SONAME=1",   // Otherwise, we load SDL3's data segment twice.  :(
+      "cppFlags": "",
+      "abiFilters": [
+        "arm64-v8a",
+        "x86_64"
+      ]
+    }
+  },
+  "buildOptionSet": [
+    {
+      "name": "release",
+      "arkOptions": {
+        "obfuscation": {
+          "ruleOptions": {
+            "enable": false,
+            "files": [
+              "./obfuscation-rules.txt"
+            ]
+          }
+        }
+      },
+      "nativeLib": {
+        "debugSymbol": {
+          "strip": true,
+          "exclude": []
+        }
+      }
+    },
+  ],
+  "targets": [
+    {
+      "name": "default"
+    },
+  ]
+}

+ 6 - 0
openharmony-project/entry/hvigorfile.ts

@@ -0,0 +1,6 @@
+import { hapTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+  system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+  plugins: []       /* Custom plugin to extend the functionality of Hvigor. */
+}

+ 23 - 0
openharmony-project/entry/obfuscation-rules.txt

@@ -0,0 +1,23 @@
+# Define project specific obfuscation rules here.
+# You can include the obfuscation configuration files in the current module's build-profile.json5.
+#
+# For more details, see
+#   https://developer.huawei.com/consumer/en/doc/harmonyos-guides/source-obfuscation
+
+# Obfuscation options:
+# -disable-obfuscation: disable all obfuscations
+# -enable-property-obfuscation: obfuscate the property names
+# -enable-toplevel-obfuscation: obfuscate the names in the global scope
+# -compact: remove unnecessary blank spaces and all line feeds
+# -remove-log: remove all console.* statements
+# -print-namecache: print the name cache that contains the mapping from the old names to new names
+# -apply-namecache: reuse the given cache file
+
+# Keep options:
+# -keep-property-name: specifies property names that you want to keep
+# -keep-global-name: specifies names that you want to keep in the global scope
+
+-enable-property-obfuscation
+-enable-toplevel-obfuscation
+-enable-filename-obfuscation
+-enable-export-obfuscation

+ 19 - 0
openharmony-project/entry/oh-package-lock.json5

@@ -0,0 +1,19 @@
+{
+  "meta": {
+    "stableOrder": true,
+    "enableUnifiedLockfile": false
+  },
+  "lockfileVersion": 3,
+  "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
+  "specifiers": {
+    "libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry"
+  },
+  "packages": {
+    "libentry.so@src/main/cpp/types/libentry": {
+      "name": "libentry.so",
+      "version": "1.0.0",
+      "resolved": "src/main/cpp/types/libentry",
+      "registryType": "local"
+    }
+  }
+}

+ 11 - 0
openharmony-project/entry/oh-package.json5

@@ -0,0 +1,11 @@
+{
+  "name": "entry",
+  "version": "1.0.0",
+  "description": "Please describe the basic information.",
+  "main": "",
+  "author": "",
+  "license": "",
+  "dependencies": {
+    //"libSDL3.so": "file:./src/main/cpp/types/libSDL3",
+  }
+}

+ 18 - 0
openharmony-project/entry/src/main/cpp/CMakeLists.txt

@@ -0,0 +1,18 @@
+# the minimum version of CMake.
+cmake_minimum_required(VERSION 3.5.0)
+project(loopwave)
+
+set(NATIVEAPP_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
+
+if(DEFINED PACKAGE_FIND_FILE)
+    include(${PACKAGE_FIND_FILE})
+endif()
+
+add_subdirectory("SDL")
+
+include_directories(${NATIVEAPP_ROOT_PATH}
+                    ${NATIVEAPP_ROOT_PATH}/include
+                    ${NATIVEAPP_ROOT_PATH}/SDL/include)
+
+add_library(main SHARED myapp.c)
+target_link_libraries(main PUBLIC SDL3::SDL3)

+ 1 - 0
openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts

@@ -0,0 +1 @@
+export const provideArkTSObjects: (ability: UIAbility, atManager: abilityAccessCtrl.AtManager, locale: intl.Locale) => void;

+ 7 - 0
openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5

@@ -0,0 +1,7 @@
+{
+  "name": "libSDL3.so",
+  "types": "./Index.d.ts",
+  "version": "1.0.0",
+  "description": "SDL3 interfaces callable from ArkTS."
+}
+

+ 28 - 0
openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets

@@ -0,0 +1,28 @@
+import { abilityAccessCtrl, AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { window } from '@kit.ArkUI';
+import { i18n } from '@kit.LocalizationKit';
+import { inputMethod } from '@kit.IMEKit';
+import pointer from "@ohos.multimodalInput.pointer"
+import sdl from 'libSDL3.so';
+
+const DOMAIN = 0x0000;
+
+export default class EntryAbility extends UIAbility {
+  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
+    hilog.info(DOMAIN, 'SDL/STARTUP', '%{public}s', 'Handing control from ArkTS to native SDL.');
+    try {
+        // This hands stuff to SDL's C code that it needs to deal with ArkTS interfaces. We are trying not to write scripting code, if we can
+        //  handle it awkwardly in C through NAPI calls, so we have a _tiny_ surface area of risk where the script and the native code can go
+        //  out of sync. This function call will add methods to `this`, such as onWindowStageCreate(), so we can drive everything from C and
+        //  have almost no ArkTS code at all.
+        // Which is to say: if this function call fails, your app won't actually start.
+        //
+        // ONLY EVER ADD PARAMETERS TO THIS FUNCTION CALL. THE C CODE WILL ABORT IF IT DOESN'T GET THE EXACT NUMBER IT EXPECTS.
+        sdl.provideArkTSObjects(this, abilityAccessCtrl.createAtManager(), i18n.System, pointer.setPointerVisibleSync, inputMethod.getController());
+    } catch (e) {
+        hilog.info(DOMAIN, 'SDL/STARTUP', '%{public}s', 'Exception thrown! App will not start.');
+    }
+  }
+}
+

+ 16 - 0
openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets

@@ -0,0 +1,16 @@
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
+
+const DOMAIN = 0x0000;
+
+export default class EntryBackupAbility extends BackupExtensionAbility {
+  async onBackup() {
+    hilog.info(DOMAIN, 'testTag', 'onBackup ok');
+    await Promise.resolve();
+  }
+
+  async onRestore(bundleVersion: BundleVersion) {
+    hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
+    await Promise.resolve();
+  }
+}

+ 15 - 0
openharmony-project/entry/src/main/ets/pages/Index.ets

@@ -0,0 +1,15 @@
+const DOMAIN = 0x0000;
+
+@Entry
+@Component
+struct Index {
+  build() {
+    Row() {
+      Column() {
+        XComponent({ id: 'SDL_XComponent', type: XComponentType.SURFACE, libraryname: 'SDL3' }).id('SDL_XComponent')
+      }
+      .width('100%')
+    }
+    .height('100%')
+  }
+}

+ 67 - 0
openharmony-project/entry/src/main/module.json5

@@ -0,0 +1,67 @@
+{
+  "module": {
+    "name": "entry",
+    "type": "entry",
+    "description": "$string:module_desc",
+    "mainElement": "EntryAbility",
+    // !!! FIXME: we can maybe put "car" and "wearable" in here, too, if SDL will run on them and we could test it at some point.
+    "deviceTypes": [
+      "phone",
+      "tablet",
+      "2in1",
+      "tv"
+    ],
+    "deliveryWithInstall": true,
+    "installationFree": false,
+    "pages": "$profile:main_pages",
+
+    // PERMISSIONS: Uncomment if your app needs any of these things. Please edit the "reason" text to more-clearly explain why you need access to the feature.
+    // Documentation on this section: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/declare-permissions
+    // Lists of specific permissions: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/app-permissions
+    // Note that READ_PASTEBOARD is a restricted permission, you need to take serious extra steps to use it. https://developer.huawei.com/consumer/en/doc/harmonyos-guides/restricted-permissions#ohospermissionread_pasteboard
+    "requestPermissions": [
+      //{ "name": "ohos.permission.MICROPHONE", "reason": "$string:perm_reason_MICROPHONE", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } },
+      //{ "name": "ohos.permission.CAMERA", "reason": "$string:perm_reason_CAMERA", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } },
+      //{ "name": "ohos.permission.INTERNET", "reason": "$string:perm_reason_INTERNET", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } },
+      //{ "name": "ohos.permission.READ_PASTEBOARD", "reason": "$string:perm_reason_READ_PASTEBOARD", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } },
+    ],
+
+    "abilities": [
+      {
+        "name": "EntryAbility",
+        "orientation": "auto_rotation_restricted",
+        "srcEntry": "./ets/entryability/EntryAbility.ets",
+        "description": "$string:EntryAbility_desc",
+        "icon": "$media:layered_image",
+        "label": "$string:EntryAbility_label",
+        "startWindowIcon": "$media:startIcon",
+        "startWindowBackground": "$color:start_window_background",
+        "exported": true,
+        "skills": [
+          {
+            "entities": [
+              "entity.system.home"
+            ],
+            "actions": [
+              "ohos.want.action.home"
+            ]
+          }
+        ]
+      }
+    ],
+    "extensionAbilities": [
+      {
+        "name": "EntryBackupAbility",
+        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
+        "type": "backup",
+        "exported": false,
+        "metadata": [
+          {
+            "name": "ohos.extension.backup",
+            "resource": "$profile:backup_config"
+          }
+        ],
+      }
+    ]
+  }
+}

+ 8 - 0
openharmony-project/entry/src/main/resources/base/element/color.json

@@ -0,0 +1,8 @@
+{
+  "color": [
+    {
+      "name": "start_window_background",
+      "value": "#FFFFFF"
+    }
+  ]
+}

+ 8 - 0
openharmony-project/entry/src/main/resources/base/element/float.json

@@ -0,0 +1,8 @@
+{
+  "float": [
+    {
+      "name": "page_text_font_size",
+      "value": "50fp"
+    }
+  ]
+}

+ 33 - 0
openharmony-project/entry/src/main/resources/base/element/string.json

@@ -0,0 +1,33 @@
+{
+  "string": [
+    {
+      "name": "module_desc",
+      "value": "SDL Application"
+    },
+    {
+      "name": "EntryAbility_desc",
+      "value": "SDL Application"
+    },
+    {
+      "name": "EntryAbility_label",
+      "value": "SDL App"
+    },
+    {
+      "name": "perm_reason_MICROPHONE",
+      "value": "Used to record audio"
+    },
+    {
+      "name": "perm_reason_CAMERA",
+      "value": "Used to record video"
+    },
+    {
+      "name": "perm_reason_INTERNET",
+      "value": "Used to download additional content"
+    },
+    {
+      "name": "perm_reason_READ_PASTEBOARD",
+      "value": "Used to read from the pasteboard"
+    }
+  ]
+}
+

BIN
openharmony-project/entry/src/main/resources/base/media/background.png


BIN
openharmony-project/entry/src/main/resources/base/media/foreground.png


+ 7 - 0
openharmony-project/entry/src/main/resources/base/media/layered_image.json

@@ -0,0 +1,7 @@
+{
+  "layered-image":
+  {
+    "background" : "$media:background",
+    "foreground" : "$media:foreground"
+  }
+}

BIN
openharmony-project/entry/src/main/resources/base/media/startIcon.png


+ 3 - 0
openharmony-project/entry/src/main/resources/base/profile/backup_config.json

@@ -0,0 +1,3 @@
+{
+  "allowToBackupRestore": true
+}

+ 5 - 0
openharmony-project/entry/src/main/resources/base/profile/main_pages.json

@@ -0,0 +1,5 @@
+{
+  "src": [
+    "pages/Index"
+  ]
+}

+ 8 - 0
openharmony-project/entry/src/main/resources/dark/element/color.json

@@ -0,0 +1,8 @@
+{
+  "color": [
+    {
+      "name": "start_window_background",
+      "value": "#000000"
+    }
+  ]
+}

+ 23 - 0
openharmony-project/hvigor/hvigor-config.json5

@@ -0,0 +1,23 @@
+{
+  "modelVersion": "6.1.1",
+  "dependencies": {
+  },
+  "execution": {
+    // "analyze": "normal",                     /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
+    // "daemon": true,                          /* Enable daemon compilation. Value: [ true | false ]. Default: true */
+    // "incremental": true,                     /* Enable incremental compilation. Value: [ true | false ]. Default: true */
+    // "parallel": true,                        /* Enable parallel compilation. Value: [ true | false ]. Default: true */
+    // "typeCheck": false,                      /* Enable typeCheck. Value: [ true | false ]. Default: false */
+    // "optimizationStrategy": "memory"         /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
+  },
+  "logging": {
+    // "level": "info"                          /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
+  },
+  "debugging": {
+    // "stacktrace": false                      /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
+  },
+  "nodeOptions": {
+    // "maxOldSpaceSize": 8192                  /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
+    // "exposeGC": true                         /* Enable to trigger garbage collection explicitly. Default: true*/
+  }
+}

+ 6 - 0
openharmony-project/hvigorfile.ts

@@ -0,0 +1,6 @@
+import { appTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+  system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+  plugins: []       /* Custom plugin to extend the functionality of Hvigor. */
+}

+ 28 - 0
openharmony-project/oh-package-lock.json5

@@ -0,0 +1,28 @@
+{
+  "meta": {
+    "stableOrder": true,
+    "enableUnifiedLockfile": false
+  },
+  "lockfileVersion": 3,
+  "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
+  "specifiers": {
+    "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0",
+    "@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25"
+  },
+  "packages": {
+    "@ohos/hamock@1.0.0": {
+      "name": "@ohos/hamock",
+      "version": "1.0.0",
+      "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==",
+      "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har",
+      "registryType": "ohpm"
+    },
+    "@ohos/hypium@1.0.25": {
+      "name": "@ohos/hypium",
+      "version": "1.0.25",
+      "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==",
+      "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har",
+      "registryType": "ohpm"
+    }
+  }
+}

+ 10 - 0
openharmony-project/oh-package.json5

@@ -0,0 +1,10 @@
+{
+  "modelVersion": "6.1.1",
+  "description": "Please describe the basic information.",
+  "dependencies": {
+  },
+  "devDependencies": {
+    "@ohos/hypium": "1.0.25",
+    "@ohos/hamock": "1.0.0"
+  }
+}

+ 7 - 0
src/SDL.c

@@ -61,6 +61,9 @@
 #ifdef SDL_PLATFORM_ANDROID
 #ifdef SDL_PLATFORM_ANDROID
 #include "core/android/SDL_android.h"
 #include "core/android/SDL_android.h"
 #endif
 #endif
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include "core/openharmony/SDL_openharmony.h"
+#endif
 
 
 #define SDL_ALL_SUBSYSTEM_FLAGS ~0U
 #define SDL_ALL_SUBSYSTEM_FLAGS ~0U
 
 
@@ -803,6 +806,8 @@ const char *SDL_GetPlatform(void)
     return "Nokia N-Gage";
     return "Nokia N-Gage";
 #elif defined(SDL_PLATFORM_OPENBSD)
 #elif defined(SDL_PLATFORM_OPENBSD)
     return "OpenBSD";
     return "OpenBSD";
+#elif defined(SDL_PLATFORM_OPENHARMONY)
+    return "OpenHarmony";
 #elif defined(SDL_PLATFORM_OS2)
 #elif defined(SDL_PLATFORM_OS2)
     return "OS/2";
     return "OS/2";
 #elif defined(SDL_PLATFORM_OSF)
 #elif defined(SDL_PLATFORM_OSF)
@@ -867,6 +872,8 @@ SDL_FormFactor SDL_GetDeviceFormFactor(void)
     return SDL_FORMFACTOR_PRIVATE;
     return SDL_FORMFACTOR_PRIVATE;
 #elif defined(SDL_PLATFORM_ANDROID)
 #elif defined(SDL_PLATFORM_ANDROID)
     return SDL_GetAndroidDeviceFormFactor();
     return SDL_GetAndroidDeviceFormFactor();
+#elif defined(SDL_PLATFORM_OPENHARMONY)
+    return SDL_GetOpenHarmonyDeviceFormFactor();
 #elif defined(SDL_PLATFORM_IOS)
 #elif defined(SDL_PLATFORM_IOS)
     extern SDL_FormFactor SDL_GetUIKitDeviceFormFactor(void);
     extern SDL_FormFactor SDL_GetUIKitDeviceFormFactor(void);
     return SDL_GetUIKitDeviceFormFactor();
     return SDL_GetUIKitDeviceFormFactor();

+ 24 - 5
src/SDL_log.c

@@ -40,6 +40,10 @@
 #include <android/log.h>
 #include <android/log.h>
 #endif
 #endif
 
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include <hilog/log.h>
+#endif
+
 #include "stdlib/SDL_vacopy.h"
 #include "stdlib/SDL_vacopy.h"
 
 
 // The size of the stack buffer to use for rendering log messages.
 // The size of the stack buffer to use for rendering log messages.
@@ -122,7 +126,19 @@ static int SDL_android_priority[] = {
     ANDROID_LOG_FATAL
     ANDROID_LOG_FATAL
 };
 };
 SDL_COMPILE_TIME_ASSERT(android_priority, SDL_arraysize(SDL_android_priority) == SDL_LOG_PRIORITY_COUNT);
 SDL_COMPILE_TIME_ASSERT(android_priority, SDL_arraysize(SDL_android_priority) == SDL_LOG_PRIORITY_COUNT);
-#endif // SDL_PLATFORM_ANDROID
+#elif defined(SDL_PLATFORM_OPENHARMONY)
+static int SDL_openharmony_priority[] = {
+    LOG_DEBUG,
+    LOG_DEBUG,
+    LOG_DEBUG,
+    LOG_DEBUG,
+    LOG_INFO,
+    LOG_WARN,
+    LOG_ERROR,
+    LOG_FATAL
+};
+SDL_COMPILE_TIME_ASSERT(openharmony_priority, SDL_arraysize(SDL_openharmony_priority) == SDL_LOG_PRIORITY_COUNT);
+#endif
 
 
 static void SDLCALL SDL_LoggingChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 static void SDLCALL SDL_LoggingChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
 {
@@ -583,7 +599,7 @@ void SDL_LogMessage(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_ST
     va_end(ap);
     va_end(ap);
 }
 }
 
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
 static const char *GetCategoryPrefix(int category)
 static const char *GetCategoryPrefix(int category)
 {
 {
     if (category < SDL_LOG_CATEGORY_RESERVED2) {
     if (category < SDL_LOG_CATEGORY_RESERVED2) {
@@ -594,7 +610,7 @@ static const char *GetCategoryPrefix(int category)
     }
     }
     return "CUSTOM";
     return "CUSTOM";
 }
 }
-#endif // SDL_PLATFORM_ANDROID
+#endif // SDL_PLATFORM_ANDROID || SDL_PLATFORM_OPENHARMONY
 
 
 void SDL_LogMessageV(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_STRING const char *fmt, va_list ap)
 void SDL_LogMessageV(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_STRING const char *fmt, va_list ap)
 {
 {
@@ -758,12 +774,15 @@ static void SDLCALL SDL_LogOutput(void *userdata, int category, SDL_LogPriority
         SDL_free(tstr);
         SDL_free(tstr);
         SDL_small_free(output, isstack);
         SDL_small_free(output, isstack);
     }
     }
-#elif defined(SDL_PLATFORM_ANDROID)
+#elif defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     {
     {
         char tag[32];
         char tag[32];
-
         SDL_snprintf(tag, SDL_arraysize(tag), "SDL/%s", GetCategoryPrefix(category));
         SDL_snprintf(tag, SDL_arraysize(tag), "SDL/%s", GetCategoryPrefix(category));
+        #if defined(SDL_PLATFORM_ANDROID)
         __android_log_write(SDL_android_priority[priority], tag, message);
         __android_log_write(SDL_android_priority[priority], tag, message);
+        #elif defined(SDL_PLATFORM_OPENHARMONY)
+        OH_LOG_Print(LOG_APP, SDL_openharmony_priority[priority], LOG_DOMAIN, tag, "%{public}s", message);
+        #endif
     }
     }
 #elif defined(SDL_PLATFORM_APPLE) && (defined(SDL_VIDEO_DRIVER_COCOA) || defined(SDL_VIDEO_DRIVER_UIKIT))
 #elif defined(SDL_PLATFORM_APPLE) && (defined(SDL_VIDEO_DRIVER_COCOA) || defined(SDL_VIDEO_DRIVER_UIKIT))
     /* Technically we don't need Cocoa/UIKit, but that's where this function is defined for now.
     /* Technically we don't need Cocoa/UIKit, but that's where this function is defined for now.

+ 3 - 0
src/audio/SDL_audio.c

@@ -95,6 +95,9 @@ static const AudioBootStrap *const bootstrap[] = {
 #ifdef SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER
 #ifdef SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER
     &DOSSOUNDBLASTER_bootstrap,
     &DOSSOUNDBLASTER_bootstrap,
 #endif
 #endif
+#ifdef SDL_AUDIO_DRIVER_OHAUDIO
+    &OHAUDIO_bootstrap,
+#endif
 #ifdef SDL_AUDIO_DRIVER_DISK
 #ifdef SDL_AUDIO_DRIVER_DISK
     &DISKAUDIO_bootstrap,
     &DISKAUDIO_bootstrap,
 #endif
 #endif

+ 1 - 0
src/audio/SDL_sysaudio.h

@@ -395,5 +395,6 @@ extern AudioBootStrap NGAGEAUDIO_bootstrap;
 extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap;
 extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap;
 extern AudioBootStrap QSAAUDIO_bootstrap;
 extern AudioBootStrap QSAAUDIO_bootstrap;
 extern AudioBootStrap DOSSOUNDBLASTER_bootstrap;
 extern AudioBootStrap DOSSOUNDBLASTER_bootstrap;
+extern AudioBootStrap OHAUDIO_bootstrap;
 
 
 #endif // SDL_sysaudio_h_
 #endif // SDL_sysaudio_h_

+ 648 - 0
src/audio/ohaudio/SDL_ohaudio.c

@@ -0,0 +1,648 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+// OpenHarmony OHAudio driver.
+// This code is based on the Android AAudio backend, since the two APIs are almost identical.
+
+#ifdef SDL_AUDIO_DRIVER_OHAUDIO
+
+#include "../SDL_sysaudio.h"
+#include "SDL_ohaudio.h"
+
+#include <ohaudio/native_audiorenderer.h>
+#include <ohaudio/native_audiostreambuilder.h>
+
+struct SDL_PrivateAudioData
+{
+    union {
+        OH_AudioRenderer *renderer;
+        OH_AudioCapturer *capturer;
+    } stream;
+
+    int num_buffers;
+    Uint8 *mixbuf;          // Raw mixing buffer
+    size_t mixbuf_bytes;    // num_buffers * device->buffer_size
+    size_t callback_bytes;
+    size_t processed_bytes;
+    SDL_Semaphore *semaphore;
+    SDL_AtomicInt error_callback_triggered;
+};
+
+// !!! FIXME: Remove this LOGI macro.
+// Debug
+#if 0
+#define LOGI(...) SDL_Log(__VA_ARGS__);
+#else
+#define LOGI(...)
+#endif
+
+#define LIB_OHAUDIO_SO "libohaudio.so"
+
+SDL_ELF_NOTE_DLOPEN(
+    "audio-ohaudio",
+    "Support for audio through OHAudio",
+    SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
+    LIB_OHAUDIO_SO
+)
+
+typedef struct OHAUDIO_Data
+{
+    SDL_SharedObject *handle;
+#define SDL_PROC(ret, func, params) ret (*func) params;
+#include "SDL_ohaudiofuncs.h"
+} OHAUDIO_Data;
+static OHAUDIO_Data ctx;
+
+static bool OHAUDIO_LoadFunctions(OHAUDIO_Data *data)
+{
+#define SDL_PROC(ret, func, params)                                                             \
+    do {                                                                                        \
+        data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func);                     \
+        if (!data->func) {                                                                      \
+            return SDL_SetError("Couldn't load OHAudio function %s: %s", #func, SDL_GetError()); \
+        }                                                                                       \
+    } while (0);
+
+#define SDL_PROC_OPTIONAL(ret, func, params)                                                          \
+    do {                                                                                              \
+        data->func = (ret (*) params)SDL_LoadFunction(data->handle, #func);  /* if it fails, okay. */ \
+    } while (0);
+#include "SDL_ohaudiofuncs.h"
+    return true;
+}
+
+
+static const char *AudioStreamResultToString(OH_AudioStream_Result res)
+{
+    switch (res) {
+        #define ERRCASE(nam) case AUDIOSTREAM_##nam: return #nam
+        ERRCASE(SUCCESS);
+        ERRCASE(ERROR_INVALID_PARAM);
+        ERRCASE(ERROR_ILLEGAL_STATE);
+        ERRCASE(ERROR_SYSTEM);
+        ERRCASE(ERROR_UNSUPPORTED_FORMAT);
+        #undef ERRCASE
+        default: break;
+    }
+    return "[unknown]";
+}
+
+static void ErrorCallback(void *userData, OH_AudioStream_Result error)
+{
+    SDL_AudioDevice *device = (SDL_AudioDevice *) userData;
+    LOGI("SDL ErrorCallback%s: %d - %s", device->recording ? "Capturer" : "Renderer", error, AudioStreamResultToString(error));
+    // You MUST NOT close the audio stream from this callback, so we cannot call SDL_AudioDeviceDisconnected here.
+    // Just flag the device so we can kill it in PlayDevice instead.
+    SDL_SetAtomicInt(&device->hidden->error_callback_triggered, (int) error);  // AUDIOSTREAM_SUCCESS is zero, so !triggered means no error.
+    SDL_SignalSemaphore(device->hidden->semaphore);  // in case we're blocking in WaitDevice.
+}
+
+static void ErrorCallbackCapturer(OH_AudioCapturer *capturer, void *userData, OH_AudioStream_Result error)
+{
+    ErrorCallback(userData, error);
+}
+
+static void ErrorCallbackRenderer(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error)
+{
+    ErrorCallback(userData, error);
+}
+
+static void ReadDataCallback(OH_AudioCapturer *capturer, void *userData, void *audioData, int32_t numBytes)
+{
+    SDL_AudioDevice *device = (SDL_AudioDevice *) userData;
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    const size_t framesize = SDL_AUDIO_FRAMESIZE(device->spec);
+    const size_t callback_bytes = (size_t) numBytes;
+    size_t old_buffer_index = hidden->callback_bytes / device->buffer_size;
+    const Uint8 *input = (const Uint8 *)audioData;
+    const size_t available_bytes = hidden->mixbuf_bytes - (hidden->callback_bytes - hidden->processed_bytes);
+    const size_t size = SDL_min(available_bytes, callback_bytes);
+    const size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes;
+    const size_t end = (offset + size) % hidden->mixbuf_bytes;
+
+    SDL_assert(size <= hidden->mixbuf_bytes);
+    (void) framesize;  // only used in macros that bubble out of release code, etc.
+
+    //LOGI("Recorded %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->callback_bytes / framesize, hidden->processed_bytes / framesize);
+
+    if (offset <= end) {
+        SDL_memcpy(&hidden->mixbuf[offset], input, size);
+    } else {
+        size_t partial = (hidden->mixbuf_bytes - offset);
+        SDL_memcpy(&hidden->mixbuf[offset], &input[0], partial);
+        SDL_memcpy(&hidden->mixbuf[0], &input[partial], end);
+    }
+
+    SDL_MemoryBarrierRelease();  // the other half of this is in OHAUDIO_RecordDevice().
+    hidden->callback_bytes += size;
+
+    if (size < callback_bytes) {
+        LOGI("Audio recording overflow, dropped %zu frames", (callback_bytes - size) / framesize);
+    }
+
+    // tell the audio thread to pull this data in.
+    const size_t new_buffer_index = hidden->callback_bytes / device->buffer_size;
+    while (old_buffer_index < new_buffer_index) {
+        // Trigger audio processing
+        SDL_SignalSemaphore(hidden->semaphore);
+        ++old_buffer_index;
+    }
+}
+
+static OH_AudioData_Callback_Result WriteDataCallback(OH_AudioRenderer *renderer, void *userData, void *audioData, int32_t numBytes)
+{
+    SDL_AudioDevice *device = (SDL_AudioDevice *) userData;
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    const size_t framesize = SDL_AUDIO_FRAMESIZE(device->spec);
+    const size_t callback_bytes = (size_t) numBytes;
+    size_t old_buffer_index = hidden->callback_bytes / device->buffer_size;
+    Uint8 *output = (Uint8 *)audioData;
+    const size_t available_bytes = (hidden->processed_bytes - hidden->callback_bytes);
+    const size_t size = SDL_min(available_bytes, callback_bytes);
+    const size_t offset = hidden->callback_bytes % hidden->mixbuf_bytes;
+    const size_t end = (offset + size) % hidden->mixbuf_bytes;
+
+    SDL_assert(size <= hidden->mixbuf_bytes);
+    (void) framesize;  // only used in macros that bubble out of release code, etc.
+
+    //LOGI("Playing %zu frames, %zu available, %zu max (%zu written, %zu read)", callback_bytes / framesize, available_bytes / framesize, hidden->mixbuf_bytes / framesize, hidden->processed_bytes / framesize, hidden->callback_bytes / framesize);
+
+    SDL_MemoryBarrierAcquire();  // the other half of this is in OHAUDIO_PlayDevice().
+    if (offset <= end) {
+        SDL_memcpy(output, &hidden->mixbuf[offset], size);
+    } else {
+        size_t partial = (hidden->mixbuf_bytes - offset);
+        SDL_memcpy(&output[0], &hidden->mixbuf[offset], partial);
+        SDL_memcpy(&output[partial], &hidden->mixbuf[0], end);
+    }
+    hidden->callback_bytes += size;
+
+    if (size < callback_bytes) {
+        LOGI("Audio playback underflow, missed %zu frames", (callback_bytes - size) / framesize);
+        SDL_memset(&output[size], device->silence_value, (callback_bytes - size));
+    }
+
+    // tell the audio thread to generate more data, so we're ready to memcpy it when this callback runs again.
+    const size_t new_buffer_index = hidden->callback_bytes / device->buffer_size;
+    while (old_buffer_index < new_buffer_index) {
+        // Trigger audio processing
+        SDL_SignalSemaphore(hidden->semaphore);
+        ++old_buffer_index;
+    }
+
+    return AUDIO_DATA_CALLBACK_RESULT_VALID;
+}
+
+static Uint8 *OHAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *bufsize)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes);
+    return &hidden->mixbuf[offset];
+}
+
+static bool OHAUDIO_WaitDevice(SDL_AudioDevice *device)
+{
+    while (!SDL_GetAtomicInt(&device->shutdown)) {
+        // this semaphore won't fire when the app is in the background (OHAUDIO_PauseDevices was called).
+        if (SDL_WaitSemaphoreTimeout(device->hidden->semaphore, 100)) {
+            return true;  // semaphore was signaled, let's go!
+        }
+        // Still waiting on the semaphore (or the system), check other things then wait again.
+    }
+    return true;
+}
+
+static bool BuildOHAudioStream(SDL_AudioDevice *device);
+
+static bool RecoverOHAudioDevice(SDL_AudioDevice *device)
+{
+    SDL_assert(!device->recording);  // !!! FIXME: we (currently) only do this for playback devices. This is true in AAudio, too.
+
+    // attempt to build a new stream, in case there's a new default device.
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    ctx.OH_AudioRenderer_Stop(hidden->stream.renderer);
+    ctx.OH_AudioRenderer_Release(hidden->stream.renderer);
+    hidden->stream.renderer = NULL;
+
+    SDL_aligned_free(hidden->mixbuf);
+    hidden->mixbuf = NULL;
+
+    SDL_DestroySemaphore(hidden->semaphore);
+    hidden->semaphore = NULL;
+
+    const int prev_sample_frames = device->sample_frames;
+    SDL_AudioSpec prevspec;
+    SDL_copyp(&prevspec, &device->spec);
+
+    if (!BuildOHAudioStream(device)) {
+        return false;  // oh well, we tried.
+    }
+
+    // we don't know the new device spec until we open the new device, so we saved off the old one and force it back
+    // so SDL_AudioDeviceFormatChanged can set up all the important state if necessary and then set it back to the new spec.
+    const int new_sample_frames = device->sample_frames;
+    SDL_AudioSpec newspec;
+    SDL_copyp(&newspec, &device->spec);
+
+    device->sample_frames = prev_sample_frames;
+    SDL_copyp(&device->spec, &prevspec);
+    if (!SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames)) {
+        return false;  // ugh
+    }
+    return true;
+}
+
+static bool OHAUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+
+    // WriteDataCallback picks up our work and unblocks OHAUDIO_WaitDevice. But make sure we didn't fail here.
+    const OH_AudioStream_Result err = (OH_AudioStream_Result) SDL_GetAtomicInt(&hidden->error_callback_triggered);
+    if (err) {
+        SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "ohaudio: Audio device triggered error %d (%s)", (int) err, AudioStreamResultToString(err));
+        if (!RecoverOHAudioDevice(device)) {
+            return false;  // oh well, we went down hard.
+        }
+    } else {
+        SDL_MemoryBarrierRelease();
+        hidden->processed_bytes += buflen;
+    }
+    return true;
+}
+
+static int OHAUDIO_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+
+    // ReadDataCallback picks up our work and unblocks OHAUDIO_WaitDevice. But make sure we didn't fail here.
+    if (SDL_GetAtomicInt(&hidden->error_callback_triggered)) {
+        SDL_SetAtomicInt(&hidden->error_callback_triggered, 0);
+        return -1;
+    }
+
+    SDL_assert(buflen == device->buffer_size);  // If this isn't true, we need to change semaphore trigger logic and account for wrapping copies here
+    size_t offset = (hidden->processed_bytes % hidden->mixbuf_bytes);
+    SDL_MemoryBarrierAcquire();
+    SDL_memcpy(buffer, &hidden->mixbuf[offset], buflen);
+    hidden->processed_bytes += buflen;
+    return buflen;
+}
+
+static void OHAUDIO_CloseDevice(SDL_AudioDevice *device)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+
+    if (hidden) {
+        if ((device->recording) && (hidden->stream.capturer)) {
+            ctx.OH_AudioCapturer_Stop(hidden->stream.capturer);
+            ctx.OH_AudioCapturer_Release(hidden->stream.capturer);
+        } else if ((!device->recording) && (hidden->stream.renderer)) {
+            ctx.OH_AudioRenderer_Stop(hidden->stream.renderer);
+            // !!! FIXME: do we have to wait for the state to change to make sure all buffered audio has played, or will close do this (or will the system do this after the close)?
+            // !!! FIXME: also, will this definitely wait for a running data callback to finish, and then stop the callback from firing again?
+            ctx.OH_AudioRenderer_Release(hidden->stream.renderer);
+        }
+
+        if (hidden->semaphore) {
+            SDL_DestroySemaphore(hidden->semaphore);
+        }
+
+        SDL_aligned_free(hidden->mixbuf);
+        SDL_free(hidden);
+        device->hidden = NULL;
+    }
+}
+
+static void SetOptionalStreamUsage(OH_AudioStreamBuilder *builder)
+{
+    const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE);
+    if (hint) {
+        OH_AudioStream_Usage usage = AUDIOSTREAM_USAGE_MOVIE;  // !!! FIXME: I don't know if this is a good default.
+        if ((SDL_strcasecmp(hint, "Communications") == 0) || (SDL_strcasecmp(hint, "GameChat") == 0)) {
+            usage = AUDIOSTREAM_USAGE_VOICE_COMMUNICATION;
+        } else if (SDL_strcasecmp(hint, "Game") == 0) {
+            usage = AUDIOSTREAM_USAGE_GAME;
+        }
+        ctx.OH_AudioStreamBuilder_SetRendererInfo(builder, usage);
+    }
+}
+
+static bool BuildOHAudioStream(SDL_AudioDevice *device)
+{
+    const char *streamtypestr = device->recording ? "Capturer" : "Renderer";
+
+    SDL_assert(SDL_GetOpenHarmonySDKVersion() >= 12);  // if not, we need to use a different, deprecated way to assign callbacks. It's doable, but messy if not required.
+
+    // !!! FIXME: make sure these all actually settle out into the say channel order SDL expects.
+    // !!! FIXME: 4POINT1 doesn't match SDL's layout in any case. If you can specify bits instead of prechosen enums, we want CH_LAYOUT_QUAD | CH_SET_LOW_FREQUENCY
+    static const OH_AudioChannelLayout sdl_layout_map[] = { CH_LAYOUT_MONO, CH_LAYOUT_STEREO, CH_LAYOUT_2POINT1, CH_LAYOUT_QUAD, CH_LAYOUT_4POINT1, CH_LAYOUT_5POINT1_BACK, CH_LAYOUT_6POINT1, CH_LAYOUT_7POINT1 };
+    SDL_assert(device->spec.channels <= SDL_arraysize(sdl_layout_map));  // update this if we add support for more channels later.
+    device->spec.channels = SDL_clamp(device->spec.channels, 1, SDL_arraysize(sdl_layout_map));
+
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    const bool recording = device->recording;
+    OH_AudioStream_Result res;
+
+    SDL_SetAtomicInt(&hidden->error_callback_triggered, 0);
+
+    OH_AudioStreamBuilder *builder = NULL;
+    res = ctx.OH_AudioStreamBuilder_Create(&builder, recording ? AUDIOSTREAM_TYPE_CAPTURER : AUDIOSTREAM_TYPE_RENDERER);
+    if (res != AUDIOSTREAM_SUCCESS) {
+        LOGI("SDL Failed OH_AudioStreamBuilder_Create %d", (int) res);
+        return SDL_SetError("SDL Failed OH_AudioStreamBuilder_Create %d", (int) res);
+    } else if (!builder) {
+        LOGI("SDL Failed OH_AudioStreamBuilder_Create - builder NULL");
+        return SDL_SetError("SDL Failed OH_AudioStreamBuilder_Create - builder NULL");
+    }
+
+    OH_AudioStream_SampleFormat format;
+    if ((device->spec.format == SDL_AUDIO_F32LE) && (SDL_GetOpenHarmonySDKVersion() >= 17)) {
+        format = AUDIOSTREAM_SAMPLE_F32LE;
+    } else if (device->spec.format == SDL_AUDIO_S32LE) {
+        format = AUDIOSTREAM_SAMPLE_S32LE;
+    } else if (device->spec.format == SDL_AUDIO_S16LE) {
+        format = AUDIOSTREAM_SAMPLE_S16LE;
+    } else if (device->spec.format == SDL_AUDIO_U8) {
+        format = AUDIOSTREAM_SAMPLE_U8;
+    } else {
+        format = AUDIOSTREAM_SAMPLE_S16LE;  // sint16 is a safe bet for everything else.
+    }
+    ctx.OH_AudioStreamBuilder_SetSampleFormat(builder, format);
+    ctx.OH_AudioStreamBuilder_SetSamplingRate(builder, device->spec.freq);
+    ctx.OH_AudioStreamBuilder_SetChannelCount(builder, device->spec.channels);
+
+#if 0  // !!! FIXME: maybe we don't mess with this. (this is from the AAudio backend.)
+    int32_t sample_frames;
+    if (SDL_GetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES)) {
+        sample_frames = device->sample_frames;
+    } else {
+        // Use 20 ms for the default audio buffer size
+        sample_frames = (device->spec.freq / 50);
+    }
+    ctx.AAudioStreamBuilder_setBufferCapacityInFrames(builder, 2 * sample_frames); // AAudio requires that the buffer capacity is at least
+    ctx.AAudioStreamBuilder_setFramesPerDataCallback(builder, sample_frames);      // twice the size of the data callback buffer size
+#endif
+
+    ctx.OH_AudioStreamBuilder_SetLatencyMode(builder, AUDIOSTREAM_LATENCY_MODE_FAST);
+    ctx.OH_AudioStreamBuilder_SetChannelLayout(builder, sdl_layout_map[device->spec.channels-1]);
+
+    LOGI("OHAudio Try to generate %s stream %d hz %s %u channels samples %u",
+         device->recording ? "recording" : "playback", device->spec.freq,
+         SDL_GetAudioFormatName(device->spec.format), device->spec.channels,
+         device->sample_frames);
+
+    if (device->recording) {
+        // try to use a microphone that is for recording external audio, otherwise you might end up with one used for telephone calls right when holding the device near your face.
+        ctx.OH_AudioStreamBuilder_SetCapturerInfo(builder, AUDIOSTREAM_SOURCE_TYPE_CAMCORDER);
+        ctx.OH_AudioStreamBuilder_SetCapturerReadDataCallback(builder, ReadDataCallback, device);
+        // !!! FIXME: we can't seem to bind a stream to a specific device, so I don't see a lot of value in knowing when routing changes.
+        //OH_AudioStream_Result OH_AudioStreamBuilder_SetCapturerDeviceChangeCallback(OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnDeviceChangeCallback  callback, void* userData);
+        ctx.OH_AudioStreamBuilder_SetCapturerErrorCallback(builder, ErrorCallbackCapturer, device);
+        res = ctx.OH_AudioStreamBuilder_GenerateCapturer(builder, &hidden->stream.capturer);
+    } else {
+        SetOptionalStreamUsage(builder);
+        ctx.OH_AudioStreamBuilder_SetRendererErrorCallback(builder, ErrorCallbackRenderer, device);
+        ctx.OH_AudioStreamBuilder_SetRendererWriteDataCallback(builder, WriteDataCallback, device);
+        // !!! FIXME: we can't seem to bind a stream to a specific device, so I don't see a lot of value in knowing when routing changes.
+        //OH_AudioStream_Result OH_AudioStreamBuilder_SetRendererOutputDeviceChangeCallback(OH_AudioStreamBuilder* builder, OH_AudioRenderer_OutputDeviceChangeCallback callback, void* userData);
+        res = ctx.OH_AudioStreamBuilder_GenerateRenderer(builder, &hidden->stream.renderer);
+    }
+
+    ctx.OH_AudioStreamBuilder_Destroy(builder);
+
+    if (res != AUDIOSTREAM_SUCCESS) {
+        LOGI("SDL Failed OH_AudioStreamBuilder_Generate%s %d %s", streamtypestr, res, AudioStreamResultToString(res));
+        return SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res));
+    }
+
+    // !!! FIXME: it's not clear to me if OHAudio streams will promise you the format you requested, converting if necessary, if Generate(Renderer|Capturer) succeeded.
+    // !!! FIXME:  AAudio goes and queries to see what it _actually_ got from the stream, so we'll do that too.
+
+    // I assume all of these succeed...why would it fail to tell you the sampling rate of the stream, etc?
+    int32_t sample_frames = 0, freq = 0, channels = 0;
+    if (device->recording) {
+        ctx.OH_AudioCapturer_GetFrameSizeInCallback(hidden->stream.capturer, &sample_frames);
+        ctx.OH_AudioCapturer_GetSamplingRate(hidden->stream.capturer, &freq);
+        ctx.OH_AudioCapturer_GetChannelCount(hidden->stream.capturer, &channels);
+        ctx.OH_AudioCapturer_GetSampleFormat(hidden->stream.capturer, &format);
+    } else {
+        ctx.OH_AudioRenderer_GetFrameSizeInCallback(hidden->stream.renderer, &sample_frames);
+        ctx.OH_AudioRenderer_GetSamplingRate(hidden->stream.renderer, &freq);
+        ctx.OH_AudioRenderer_GetChannelCount(hidden->stream.renderer, &channels);
+        ctx.OH_AudioRenderer_GetSampleFormat(hidden->stream.renderer, &format);
+    }
+
+    device->sample_frames = (int) sample_frames;  // OHAudio docs claim this is always consistent, never returns something like AAUDIO_UNSPECIFIED like AAudio.
+    device->spec.freq = (int) freq;
+    device->spec.channels = (int) channels;
+
+    if (format == AUDIOSTREAM_SAMPLE_U8) {
+        device->spec.format = SDL_AUDIO_U8;
+    } else if (format == AUDIOSTREAM_SAMPLE_S16LE) {
+        device->spec.format = SDL_AUDIO_S16LE;
+    } else if (format == AUDIOSTREAM_SAMPLE_S32LE) {
+        device->spec.format = SDL_AUDIO_S32LE;
+    } else if (format == AUDIOSTREAM_SAMPLE_F32LE) {
+        device->spec.format = SDL_AUDIO_F32LE;
+    } else {
+        return SDL_SetError("Got unexpected audio format %d from OH_Audio%s_GetSampleFormat", (int) format, streamtypestr);
+    }
+
+    SDL_UpdatedAudioDeviceFormat(device);
+
+    // Allocate a triple buffered mixing buffer
+    // Two buffers can be in the process of being filled while the third is being read
+    hidden->num_buffers = 3;
+    hidden->mixbuf_bytes = (hidden->num_buffers * device->buffer_size);
+    hidden->mixbuf = (Uint8 *)SDL_aligned_alloc(SDL_GetSIMDAlignment(), hidden->mixbuf_bytes);
+    if (!hidden->mixbuf) {
+        return false;
+    }
+    hidden->processed_bytes = 0;
+    hidden->callback_bytes = 0;
+
+    hidden->semaphore = SDL_CreateSemaphore(recording ? 0 : hidden->num_buffers - 1);
+    if (!hidden->semaphore) {
+        LOGI("SDL Failed SDL_CreateSemaphore %s recording:%d", SDL_GetError(), recording);
+        return false;
+    }
+
+    LOGI("OHAudio Actually generate %s stream %d hz %s %u channels samples %u, buffers %d",
+         device->recording ? "recording" : "playback",
+         device->spec.freq, SDL_GetAudioFormatName(device->spec.format),
+         device->spec.channels, device->sample_frames, hidden->num_buffers);
+
+    return true;
+}
+
+// !!! FIXME: make this non-blocking!
+#if 0
+static void SDLCALL RequestAndroidPermissionBlockingCallback(void *userdata, const char *permission, bool granted)
+{
+    SDL_SetAtomicInt((SDL_AtomicInt *) userdata, granted ? 1 : -1);
+}
+#endif
+
+static bool OHAUDIO_OpenDevice(SDL_AudioDevice *device)
+{
+#if 0  // !!! FIXME: look up how to do this in HarmonyOS.
+    if (device->recording) {
+        // !!! FIXME: make this non-blocking!
+        SDL_AtomicInt permission_response;
+        SDL_SetAtomicInt(&permission_response, 0);
+        if (!SDL_RequestAndroidPermission("android.permission.RECORD_AUDIO", RequestAndroidPermissionBlockingCallback, &permission_response)) {
+            return false;
+        }
+
+        while (SDL_GetAtomicInt(&permission_response) == 0) {
+            SDL_Delay(10);
+        }
+
+        if (SDL_GetAtomicInt(&permission_response) < 0) {
+            LOGI("This app doesn't have RECORD_AUDIO permission");
+            return SDL_SetError("This app doesn't have RECORD_AUDIO permission");
+        }
+    }
+#endif
+
+    device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden));
+    if (!device->hidden) {
+        return false;
+    }
+
+    return BuildOHAudioStream(device);
+}
+
+static bool PauseOneDevice(SDL_AudioDevice *device, void *userdata)
+{
+    struct SDL_PrivateAudioData *hidden = (struct SDL_PrivateAudioData *)device->hidden;
+    if (hidden) {
+        if (hidden->stream.renderer) {  // this is a union, so a valid capture stream will still have a non-NULL `renderer`.
+            OH_AudioStream_Result res;
+            if (device->recording) {
+                res = ctx.OH_AudioCapturer_Pause(hidden->stream.capturer);
+            } else {
+                res = ctx.OH_AudioRenderer_Pause(hidden->stream.renderer);
+            }
+
+            if (res != AUDIOSTREAM_SUCCESS) {
+                LOGI("SDL Failed OH_Audio%s_Pause %d", device->recording ? "Capturer" : "Renderer", res);
+                SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res));
+            }
+        }
+    }
+    return false;  // keep enumerating.
+}
+
+// Pause (block) all non already paused audio devices by taking their mixer lock
+void OHAUDIO_PauseDevices(void)
+{
+    if (ctx.handle) {  // OHAudio driver is used?
+        (void) SDL_FindPhysicalAudioDeviceByCallback(PauseOneDevice, NULL);
+    }
+}
+
+// Resume (unblock) all non already paused audio devices by releasing their mixer lock
+static bool ResumeOneDevice(SDL_AudioDevice *device, void *userdata)
+{
+    struct SDL_PrivateAudioData *hidden = device->hidden;
+    if (hidden) {
+        OH_AudioStream_Result res = AUDIOSTREAM_SUCCESS;
+        if (device->recording && hidden->stream.capturer) {
+            res = ctx.OH_AudioCapturer_Start(hidden->stream.capturer);
+        } else if (!device->recording && hidden->stream.renderer) {
+            res = ctx.OH_AudioRenderer_Start(hidden->stream.renderer);
+        }
+
+        if (res != AUDIOSTREAM_SUCCESS) {
+            LOGI("SDL Failed OH_Audio%s_Start %d", device->recording ? "Capturer" : "Renderer", res);
+            SDL_SetError("%s : %s", SDL_FUNCTION, AudioStreamResultToString(res));
+        }
+    }
+    return false;  // keep enumerating.
+}
+
+void OHAUDIO_ResumeDevices(void)
+{
+    if (ctx.handle) {  // AAUDIO driver is used?
+        (void) SDL_FindPhysicalAudioDeviceByCallback(ResumeOneDevice, NULL);
+    }
+}
+
+static void OHAUDIO_Deinitialize(void)
+{
+    if (ctx.handle) {
+        SDL_UnloadObject(ctx.handle);
+    }
+    SDL_zero(ctx);
+}
+
+
+static void OHAUDIO_ThreadInit(SDL_AudioDevice *device)
+{
+    SDL_SetCurrentThreadPriority(device->recording ? SDL_THREAD_PRIORITY_HIGH : SDL_THREAD_PRIORITY_TIME_CRITICAL);
+    // Start the audio stream here; OHAudio's docs say that starting and stopping might take significant time and shouldn't be done from the main thread.
+    if (device->recording) {
+        ctx.OH_AudioCapturer_Start(device->hidden->stream.capturer);
+    } else {
+        ctx.OH_AudioRenderer_Start(device->hidden->stream.renderer);
+    }
+}
+
+static bool OHAUDIO_Init(SDL_AudioDriverImpl *impl)
+{
+    SDL_zero(ctx);
+
+    ctx.handle = SDL_LoadObject(LIB_OHAUDIO_SO);
+    if (!ctx.handle) {
+        LOGI("SDL couldn't find " LIB_OHAUDIO_SO);
+        return false;
+    }
+
+    if (!OHAUDIO_LoadFunctions(&ctx)) {
+        SDL_UnloadObject(ctx.handle);
+        SDL_zero(ctx);
+        return false;
+    }
+
+    impl->ThreadInit = OHAUDIO_ThreadInit;
+    impl->Deinitialize = OHAUDIO_Deinitialize;
+    impl->OpenDevice = OHAUDIO_OpenDevice;
+    impl->CloseDevice = OHAUDIO_CloseDevice;
+    impl->WaitDevice = OHAUDIO_WaitDevice;
+    impl->PlayDevice = OHAUDIO_PlayDevice;
+    impl->GetDeviceBuf = OHAUDIO_GetDeviceBuf;
+    impl->WaitRecordingDevice = OHAUDIO_WaitDevice;
+    impl->RecordDevice = OHAUDIO_RecordDevice;
+    impl->HasRecordingSupport = true;
+    impl->OnlyHasDefaultPlaybackDevice = true;
+    impl->OnlyHasDefaultRecordingDevice = true;
+
+    LOGI("SDL OHAUDIO_Init OK");
+    return true;
+}
+
+AudioBootStrap OHAUDIO_bootstrap = {
+    "OHAudio", "OpenHarmony OHAudio audio driver", OHAUDIO_Init, false, false
+};
+
+#endif // SDL_AUDIO_DRIVER_OHAUDIO

+ 38 - 0
src/audio/ohaudio/SDL_ohaudio.h

@@ -0,0 +1,38 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifndef SDL_ohaudio_h_
+#define SDL_ohaudio_h_
+
+#ifdef SDL_AUDIO_DRIVER_OHAUDIO
+
+extern void OHAUDIO_ResumeDevices(void);
+extern void OHAUDIO_PauseDevices(void);
+
+#else
+
+#define OHAUDIO_ResumeDevices()
+#define OHAUDIO_PauseDevices()
+
+#endif
+
+#endif // SDL_ohaudio_h_

+ 62 - 0
src/audio/ohaudio/SDL_ohaudiofuncs.h

@@ -0,0 +1,62 @@
+/*
+  Simple DirectMedia Layer
+  Copyright , (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_PROC_OPTIONAL
+#define SDL_PROC_OPTIONAL(ret, func, params) SDL_PROC(ret, func, params)
+#endif
+
+#define SDL_PROC_UNUSED(ret, func, params)
+
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_Create, (OH_AudioStreamBuilder** builder, OH_AudioStream_Type type))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_Destroy, (OH_AudioStreamBuilder* builder))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetSamplingRate, (OH_AudioStreamBuilder* builder, int32_t rate))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetChannelCount, (OH_AudioStreamBuilder* builder, int32_t channelCount))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetSampleFormat, (OH_AudioStreamBuilder* builder, OH_AudioStream_SampleFormat format))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetLatencyMode, (OH_AudioStreamBuilder* builder, OH_AudioStream_LatencyMode latencyMode))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetChannelLayout, (OH_AudioStreamBuilder* builder, OH_AudioChannelLayout channelLayout))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererInfo, (OH_AudioStreamBuilder* builder, OH_AudioStream_Usage usage))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerInfo, (OH_AudioStreamBuilder* builder, OH_AudioStream_SourceType sourceType))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_GenerateRenderer, (OH_AudioStreamBuilder* builder, OH_AudioRenderer** audioRenderer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_GenerateCapturer, (OH_AudioStreamBuilder* builder, OH_AudioCapturer** audioCapturer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererWriteDataCallback, (OH_AudioStreamBuilder* builder, OH_AudioRenderer_OnWriteDataCallback callback, void* userData))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetRendererErrorCallback, (OH_AudioStreamBuilder* builder, OH_AudioRenderer_OnErrorCallback callback, void* userData))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerReadDataCallback, (OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnReadDataCallback callback, void* userData))
+SDL_PROC(OH_AudioStream_Result, OH_AudioStreamBuilder_SetCapturerErrorCallback, (OH_AudioStreamBuilder* builder, OH_AudioCapturer_OnErrorCallback callback, void* userData))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Release, (OH_AudioRenderer* renderer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Start, (OH_AudioRenderer* renderer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Pause, (OH_AudioRenderer* renderer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_Stop, (OH_AudioRenderer* renderer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetSamplingRate, (OH_AudioRenderer* renderer, int32_t* rate))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetChannelCount, (OH_AudioRenderer* renderer, int32_t* channelCount))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetSampleFormat, (OH_AudioRenderer* renderer, OH_AudioStream_SampleFormat* sampleFormat))
+SDL_PROC(OH_AudioStream_Result, OH_AudioRenderer_GetFrameSizeInCallback, (OH_AudioRenderer* renderer, int32_t* frameSize))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Release, (OH_AudioCapturer* capturer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Start, (OH_AudioCapturer* capturer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Pause, (OH_AudioCapturer* capturer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_Stop, (OH_AudioCapturer* capturer))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetSamplingRate, (OH_AudioCapturer* capturer, int32_t* rate))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetChannelCount, (OH_AudioCapturer* capturer, int32_t* channelCount))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetSampleFormat, (OH_AudioCapturer* capturer, OH_AudioStream_SampleFormat* sampleFormat))
+SDL_PROC(OH_AudioStream_Result, OH_AudioCapturer_GetFrameSizeInCallback, (OH_AudioCapturer* capturer, int32_t* frameSize))
+
+#undef SDL_PROC
+#undef SDL_PROC_UNUSED
+#undef SDL_PROC_OPTIONAL

+ 3 - 0
src/camera/SDL_camera.c

@@ -44,6 +44,9 @@ static const CameraBootStrap *const bootstrap[] = {
 #ifdef SDL_CAMERA_DRIVER_ANDROID
 #ifdef SDL_CAMERA_DRIVER_ANDROID
     &ANDROIDCAMERA_bootstrap,
     &ANDROIDCAMERA_bootstrap,
 #endif
 #endif
+#ifdef SDL_CAMERA_DRIVER_OPENHARMONY
+    &OPENHARMONYCAMERA_bootstrap,
+#endif
 #ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN
 #ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN
     &EMSCRIPTENCAMERA_bootstrap,
     &EMSCRIPTENCAMERA_bootstrap,
 #endif
 #endif

+ 1 - 0
src/camera/SDL_syscamera.h

@@ -222,6 +222,7 @@ extern CameraBootStrap PIPEWIRECAMERA_bootstrap;
 extern CameraBootStrap V4L2_bootstrap;
 extern CameraBootStrap V4L2_bootstrap;
 extern CameraBootStrap COREMEDIA_bootstrap;
 extern CameraBootStrap COREMEDIA_bootstrap;
 extern CameraBootStrap ANDROIDCAMERA_bootstrap;
 extern CameraBootStrap ANDROIDCAMERA_bootstrap;
+extern CameraBootStrap OPENHARMONYCAMERA_bootstrap;
 extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap;
 extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap;
 extern CameraBootStrap MEDIAFOUNDATION_bootstrap;
 extern CameraBootStrap MEDIAFOUNDATION_bootstrap;
 extern CameraBootStrap VITACAMERA_bootstrap;
 extern CameraBootStrap VITACAMERA_bootstrap;

+ 775 - 0
src/camera/openharmony/SDL_camera_openharmony.c

@@ -0,0 +1,775 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#include "../SDL_syscamera.h"
+#include "../SDL_camera_c.h"
+#include "../../video/SDL_pixels_c.h"
+#include "../../video/SDL_surface_c.h"
+#include "../../core/openharmony/SDL_openharmony.h"
+
+#ifdef SDL_CAMERA_DRIVER_OPENHARMONY
+
+/*
+ * module.json5:
+ *   { "name": "ohos.permission.CAMERA", "reason": "$string:perm_reason_CAMERA", "usedScene": { "abilities": [ "EntryAbility" ], "when": "inuse" } },
+ *
+ * Very likely SDL must be build with YUV support (done by default)
+ */
+
+// !!! FIXME: Using a "preview output" is the only way I could find to read pixels on the CPU. If I
+// !!! FIXME:  try to use a "video output" with an image receiver, it doesn't work: I get data from
+// !!! FIXME:  the camera and I can see motion in the output if I wave my hand in front of the camera,
+// !!! FIXME:  but it's _definitely_ not the NV21 format it claims to be, and the image is some weird
+// !!! FIXME:  scrambled thing split into rows.
+// !!! FIXME: While preview output is "fine," it doesn't give us access to various camera framerates
+// !!! FIXME:  (we currently lie and say 60fps), nor the maximum resolution of the camera.
+// !!! FIXME: I think VideoOutput wants the system to control the whole pipeline, from lens to encoder
+// !!! FIXME:  to disk, and preview output wants to add the GPU into the mix but will accept CPU access
+// !!! FIXME:  if you want to do things like Snapchat filters or whatever on the data. I don't know.
+
+// prevent unneeded rawfile headers (which contain C++ code) from getting included by other system headers.
+typedef struct RawFileDescriptor RawFileDescriptor;
+#define GLOBAL_RAW_FILE_H 1
+
+#include <multimedia/image_framework/image/image_native.h>
+#include <multimedia/image_framework/image/image_receiver_native.h>
+#include <ohcamera/camera.h>
+#include <ohcamera/camera_device.h>
+#include <ohcamera/camera_input.h>
+#include <ohcamera/capture_session.h>
+#include <ohcamera/video_output.h>
+#include <ohcamera/camera_manager.h>
+
+#include "../../core/openharmony/SDL_openharmony.h"
+
+struct SDL_PrivateCameraData
+{
+    Camera_Input *input;
+    OH_ImageReceiverOptions *options;
+    OH_ImageReceiverNative *image_receiver;
+    Camera_PreviewOutput *output;
+    //Camera_VideoOutput *output;
+    Camera_CaptureSession *session;
+    int rotation;  // degrees to rotate clockwise to get from camera's static orientation to device's native orientation. Apply this plus current phone rotation to get upright image!
+    SDL_CameraSpec requested_spec;
+};
+
+static bool SetErrorStr(const char *what, const char *errstr, const int rc)
+{
+    char errbuf[128];
+    if (!errstr) {
+        SDL_snprintf(errbuf, sizeof (errbuf), "Unknown error #%d", rc);
+        errstr = errbuf;
+    }
+    return SDL_SetError("%s: %s", what, errstr);
+}
+
+static const char *CameraErrorStr(const Camera_ErrorCode rc)
+{
+    switch (rc) {
+        case CAMERA_OK: return "no error";
+        case CAMERA_INVALID_ARGUMENT: return "invalid argument";
+        case CAMERA_OPERATION_NOT_ALLOWED: return "operation not allowed";
+        case CAMERA_SESSION_NOT_CONFIG: return "session not configured";
+        case CAMERA_SESSION_NOT_RUNNING: return "session not running";
+        case CAMERA_SESSION_CONFIG_LOCKED: return "session config locked";
+        case CAMERA_DEVICE_SETTING_LOCKED: return "device setting locked";
+        case CAMERA_CONFLICT_CAMERA: return "camera conflict";
+        case CAMERA_DEVICE_DISABLED: return "device disabled";
+        case CAMERA_DEVICE_PREEMPTED: return "device preempted";
+        case CAMERA_UNRESOLVED_CONFLICTS_WITH_CURRENT_CONFIGURATIONS: return "unresolved conflicts with current configuration";
+        case CAMERA_SERVICE_FATAL_ERROR: return "fatal error";
+        default: break;
+    }
+
+    return NULL;  // unknown error
+}
+
+static bool SetCameraError(const char *what, const Camera_ErrorCode rc)
+{
+    return SetErrorStr(what, CameraErrorStr(rc), (int) rc);
+}
+
+static const char *ImageErrorStr(const Image_ErrorCode rc)
+{
+    switch (rc) {
+        case IMAGE_SUCCESS: return "no error";
+        case IMAGE_BAD_PARAMETER: return "bad parameter";
+        case IMAGE_UNSUPPORTED_MIME_TYPE: return "unsupported mimetype";
+        case IMAGE_UNKNOWN_MIME_TYPE: return "unknown mimetype";
+        case IMAGE_TOO_LARGE: return "too large";
+        case IMAGE_GET_IMAGE_DATA_FAILED: return "get image data failed";
+        case IMAGE_DMA_NOT_EXIST: return "DMA doesn't exist";
+        case IMAGE_DMA_OPERATION_FAILED: return "DMA operation failed";
+        case IMAGE_UNSUPPORTED_OPERATION: return "unsupported operation";
+        case IMAGE_UNSUPPORTED_METADATA: return "unsupported metadata";
+        case IMAGE_UNSUPPORTED_CONVERSION: return "unsupported conversion";
+        case IMAGE_INVALID_REGION: return "invalid region";
+        case IMAGE_UNSUPPORTED_MEMORY_FORMAT: return "unsupported memory format";
+        case IMAGE_INVALID_PARAMETER: return "invalid parameter";
+        case IMAGE_UNSUPPORTED_DATA_FORMAT: return "unsupported data format";
+        case IMAGE_ALLOC_FAILED: return "allocation failed";
+        case IMAGE_COPY_FAILED: return "copy failed";
+        case IMAGE_LOCK_UNLOCK_FAILED: return "unlock failed";
+        case IMAGE_INIT_FAILED: return "init failed";
+        case IMAGE_CREATE_PIXELMAP_FAILED: return "create pixelmap failed";
+        case IMAGE_ALLOCATOR_MODE_UNSUPPORTED: return "allocator mode unsupported";
+        case IMAGE_UNKNOWN_ERROR: return "unknown error";
+        case IMAGE_BAD_SOURCE: return "bad source";
+        case IMAGE_SOURCE_UNSUPPORTED_MIME_TYPE: return "unsupported source mimetype";
+        case IMAGE_SOURCE_TOO_LARGE: return "source too large";
+        case IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE: return "unsupported source allocator type";
+        case IMAGE_SOURCE_UNSUPPORTED_METADATA: return "unsupported source metadata";
+        case IMAGE_SOURCE_UNSUPPORTED_OPTIONS: return "unsupported source options";
+        case IMAGE_SOURCE_INVALID_PARAMETER: return "invalid source parameter";
+        case IMAGE_DECODE_FAILED: return "decode failed";
+        case IMAGE_SOURCE_ALLOC_FAILED: return "source allocation failed";
+        case IMAGE_PACKER_INVALID_PARAMETER: return "invalid packer parameter";
+        case IMAGE_ENCODE_FAILED: return "encode failed";
+        case IMAGE_RECEIVER_INVALID_PARAMETER: return "invalid receiver parameter";
+        default: break;
+    }
+
+    return NULL;  // unknown error
+}
+
+static bool SetImageError(const char *what, const Image_ErrorCode rc)
+{
+    return SetErrorStr(what, ImageErrorStr(rc), (int) rc);
+}
+
+
+static Camera_Manager *camera_manager = NULL;
+
+static bool CreateCameraManager(void)
+{
+    SDL_assert(camera_manager == NULL);
+    const Camera_ErrorCode rc = OH_Camera_GetCameraManager(&camera_manager);
+    if (rc != CAMERA_OK) {
+        return SetCameraError("Error creating Camera_Manager", rc);
+    }
+    return true;
+}
+
+static void DestroyCameraManager(void)
+{
+    if (camera_manager) {
+        OH_Camera_DeleteCameraManager(camera_manager);
+        camera_manager = NULL;
+    }
+}
+
+static void ConvertOpenHarmonyFormatToSDL(Camera_Format fmt, SDL_PixelFormat *format, SDL_Colorspace *colorspace)
+{
+    switch (fmt) {
+        #define CASE(x, y, z)  case x: *format = y; *colorspace = z; return
+        CASE(CAMERA_FORMAT_YUV_420_SP, SDL_PIXELFORMAT_NV21, SDL_COLORSPACE_BT709_LIMITED);
+        CASE(CAMERA_FORMAT_JPEG, SDL_PIXELFORMAT_MJPG, SDL_COLORSPACE_SRGB);
+        CASE(CAMERA_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888, SDL_COLORSPACE_SRGB);
+        #undef CASE
+        default: break;
+    }
+
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: Unknown format Camera_Format '%d'", fmt);
+    #endif
+
+    *format = SDL_PIXELFORMAT_UNKNOWN;
+    *colorspace = SDL_COLORSPACE_UNKNOWN;
+}
+
+static Camera_Format ConvertSDLFormatToOpenHarmony(SDL_PixelFormat fmt)
+{
+    switch (fmt) {
+        #define CASE(x, y)  case y: return x
+        CASE(CAMERA_FORMAT_YUV_420_SP, SDL_PIXELFORMAT_NV21);
+        CASE(CAMERA_FORMAT_JPEG, SDL_PIXELFORMAT_MJPG);
+        CASE(CAMERA_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888);
+        #undef CASE
+        default: return 0;
+    }
+}
+
+static bool FindOpenHarmonyCameraByDevicePtr(SDL_Camera *device, void *userdata)
+{
+    const Camera_Device *ohdevice = (const Camera_Device *) userdata;
+    return (SDL_strcmp(ohdevice->cameraId, ((const Camera_Device *) device->handle)->cameraId) == 0);
+}
+
+static bool FindOpenHarmonyCameraByImageReceiver(SDL_Camera *device, void *userdata)
+{
+    OH_ImageReceiverNative *receiver = (OH_ImageReceiverNative *) userdata;
+    struct SDL_PrivateCameraData *hidden = device->hidden;
+    return (hidden && (hidden->image_receiver == receiver));
+}
+
+static bool FindOpenHarmonyCameraByInput(SDL_Camera *device, void *userdata)
+{
+    Camera_Input *input = (Camera_Input *) userdata;
+    struct SDL_PrivateCameraData *hidden = device->hidden;
+    return (hidden && (hidden->input == input));
+}
+
+static bool OPENHARMONYCAMERA_WaitDevice(SDL_Camera *device)
+{
+    return true;  // this isn't used atm, since we run our own thread via onImageAvailable callbacks.
+}
+
+static SDL_CameraFrameResult OPENHARMONYCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS, float *rotation)
+{
+    struct SDL_PrivateCameraData *hidden = device->hidden;
+    SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY;
+    Image_ErrorCode imgrc = IMAGE_SUCCESS;
+
+    OH_ImageNative *image = NULL;
+    if ((imgrc = OH_ImageReceiverNative_ReadNextImage(hidden->image_receiver, &image)) != IMAGE_SUCCESS) {
+        SetImageError("Failed to read next image", imgrc);
+        return SDL_CAMERA_FRAME_ERROR;
+    }
+
+    Image_Size size;
+    if ((imgrc = OH_ImageNative_GetImageSize(image, &size)) != IMAGE_SUCCESS) {
+        SetImageError("Failed to get image size", imgrc);
+        OH_ImageNative_Release(image);
+        return SDL_CAMERA_FRAME_ERROR;
+    }
+
+    int64_t ohtimestamp = 0;
+    if (OH_ImageNative_GetTimestamp(image, &ohtimestamp) == IMAGE_SUCCESS) {
+        *timestampNS = (Uint64) ohtimestamp;
+    } else {
+        *timestampNS = 0;
+    }
+
+    uint32_t components_buf[16];
+    uint32_t *components = components_buf;
+    size_t num_components = SDL_arraysize(components_buf);
+    if ((imgrc = OH_ImageNative_GetComponentTypes(image, &components, &num_components)) != IMAGE_SUCCESS) {
+        SetImageError("Failed to get image component types", imgrc);
+        OH_ImageNative_Release(image);
+        return SDL_CAMERA_FRAME_ERROR;
+    }
+    SDL_assert(num_components <= SDL_arraysize(components_buf));  // just in case.
+
+    int num_planes = (int) num_components;
+
+#if 0
+SDL_Log("CAMERA: %d planes:", (int) num_components);
+for (size_t i = 0; i < num_components; i++) { SDL_Log("CAMERA:  - %d: %d", (int) i, (int) components[i]); }
+
+    // !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame...
+    if ((num_planes == 3) && (device->spec.format == SDL_PIXELFORMAT_NV21)) {
+SDL_Log("THIS IS AN NV21 IMAGE, ALLEGEDLY.");
+        num_planes--;   // treat the interleaved planes as one.
+    }
+#endif
+
+    int32_t stride = 0;
+    // !!! FIXME: this works because of luck of how YUV420 and NV21 are layed out, but we _should_ check this for each component.
+    if ((imgrc = OH_ImageNative_GetRowStride(image, components[0], &stride)) != IMAGE_SUCCESS) {
+        SetImageError("Failed to get image row stride", imgrc);
+        OH_ImageNative_Release(image);
+        return SDL_CAMERA_FRAME_ERROR;
+    }
+
+    frame->pitch = (int) stride;
+
+    size_t buflen = 0;
+    for (int i = 0; (i < num_planes) && (i < 3); i++) {
+        size_t datalen = 0;
+        OH_ImageNative_GetBufferSize(image, components[i], &datalen);
+        buflen += datalen;
+    }
+
+    frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen);
+    if (frame->pixels == NULL) {
+        result = SDL_CAMERA_FRAME_ERROR;
+    } else {
+        Uint8 *dst = frame->pixels;
+
+        for (int i = 0; i < num_planes; i++) {
+            OH_NativeBuffer *imagebuf = NULL;
+            OH_ImageNative_GetByteBuffer(image, components[i], &imagebuf);
+
+            size_t datalen = 0;
+            OH_ImageNative_GetBufferSize(image, components[i], &datalen);
+
+            void *data = NULL;
+            if (OH_NativeBuffer_Map(imagebuf, &data) != 0) {
+                SDL_memset(dst, 0, datalen);  // oh well.
+            } else {
+                SDL_memcpy(dst, data, datalen);
+                OH_NativeBuffer_Unmap(imagebuf);
+            }
+
+            dst += datalen;
+        }
+    }
+
+    OH_ImageNative_Release(image);
+
+    int dev_rotation = 0;
+    switch (SDL_GetOpenHarmonyDeviceCurrentOrientation()) {
+        case SDL_ORIENTATION_PORTRAIT: dev_rotation = 0; break;
+        case SDL_ORIENTATION_LANDSCAPE: dev_rotation = 90; break;
+        case SDL_ORIENTATION_PORTRAIT_FLIPPED: dev_rotation = 180; break;
+        case SDL_ORIENTATION_LANDSCAPE_FLIPPED: dev_rotation = 270; break;
+        default: SDL_assert(!"Unexpected device rotation!"); dev_rotation = 0; break;
+    }
+
+    if (device->position == SDL_CAMERA_POSITION_BACK_FACING) {
+        dev_rotation = -dev_rotation;  // we want to subtract this value, instead of add, if back-facing.
+    }
+
+    *rotation = (float) (dev_rotation + device->hidden->rotation);   // current phone orientation, static camera orientation in relation to phone.
+
+    return result;
+}
+
+static void OPENHARMONYCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
+{
+    // !!! FIXME: this currently copies the data to the surface, but in theory we could just keep the OH_ImageNative until ReleaseFrame...
+    SDL_aligned_free(frame->pixels);
+}
+
+static void OnImageReceiverFrame(OH_ImageReceiverNative *receiver)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnImageReceiverFrame");
+    #endif
+    SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByImageReceiver, receiver);
+    if (device) {
+        SDL_CameraThreadIterate(device);
+    }
+}
+
+static void OnVideoOutputFrameStart(Camera_PreviewOutput *output)
+//static void OnVideoOutputFrameStart(Camera_VideoOutput *output)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnVideoOutputFrameStart");
+    #endif
+}
+
+static void OnVideoOutputFrameEnd(Camera_PreviewOutput *output, int32_t frame_count)
+//static void OnVideoOutputFrameEnd(Camera_VideoOutput *output, int32_t frame_count)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnVideoOutputFrameEnd");
+    #endif
+}
+
+static void OnVideoOutputError(Camera_PreviewOutput *output, Camera_ErrorCode err)
+//static void OnVideoOutputError(Camera_VideoOutput *output, Camera_ErrorCode err)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnVideoOutputError");
+    #endif
+}
+
+static void OnCameraInputError(const Camera_Input *input, Camera_ErrorCode err)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnCameraInputError");
+    #endif
+    SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByInput, (void *) input);
+    if (device) {
+        SDL_CameraDisconnected(device);
+    }
+}
+
+static void OPENHARMONYCAMERA_CloseDevice(SDL_Camera *device)
+{
+    if (device && device->hidden) {
+        struct SDL_PrivateCameraData *hidden = device->hidden;
+
+        if (hidden->session) {
+            OH_CaptureSession_Stop(hidden->session);
+            OH_CaptureSession_Release(hidden->session);
+        }
+
+        if (hidden->input) {
+            OH_CameraInput_Close(hidden->input);
+            OH_CameraInput_Release(hidden->input);
+        }
+
+        if (hidden->output) {
+            OH_PreviewOutput_Stop(hidden->output);
+            OH_PreviewOutput_Release(hidden->output);
+            //OH_VideoOutput_Stop(hidden->output);
+            //OH_VideoOutput_Release(hidden->output);
+        }
+
+        if (hidden->image_receiver) {
+            OH_ImageReceiverNative_Release(hidden->image_receiver);
+        }
+
+        if (hidden->options) {
+            OH_ImageReceiverOptions_Release(hidden->options);
+        }
+
+        device->hidden = NULL;
+
+        SDL_free(hidden);
+    }
+}
+
+// this is where the "opening" of the camera happens, after permission is granted.
+static bool PrepareCamera(SDL_Camera *device)
+{
+    struct SDL_PrivateCameraData *hidden = device->hidden;
+    SDL_assert(hidden != NULL);
+
+    // just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
+    const SDL_CameraSpec *spec = &hidden->requested_spec;
+
+    Camera_ErrorCode rc = CAMERA_OK;
+    Image_ErrorCode imgrc = IMAGE_SUCCESS;
+    Camera_Device *ohdevice = (Camera_Device *) device->handle;
+
+    const Camera_Profile video_profile = { ConvertSDLFormatToOpenHarmony(spec->format), { (uint32_t) spec->width, (uint32_t) spec->height } };
+    //const Camera_VideoProfile video_profile = { ConvertSDLFormatToOpenHarmony(spec->format), { (uint32_t) spec->width, (uint32_t) spec->height }, { spec->framerate_numerator, spec->framerate_numerator } };
+    const Image_Size imgsize = { (uint32_t) spec->width, (uint32_t) spec->height };
+    const int32_t imgcapacity = 8; // image buffer queue size. docs recommend this be 8: https://developer.huawei.com/consumer/en/doc/harmonyos-guides/native-camera-preview-imagereceiver
+    uint64_t receiver_surface_id = 0;
+    char receiver_surface_id_str[64];
+    uint32_t orientation = 0;
+
+    CameraInput_Callbacks camera_input_callbacks = { OnCameraInputError };
+    PreviewOutput_Callbacks video_output_callbacks = { OnVideoOutputFrameStart, OnVideoOutputFrameEnd, OnVideoOutputError };
+    //VideoOutput_Callbacks video_output_callbacks = { OnVideoOutputFrameStart, OnVideoOutputFrameEnd, OnVideoOutputError };
+
+    if ((imgrc = OH_ImageReceiverOptions_Create(&hidden->options)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to create image receiver options", imgrc);
+    } else if ((imgrc = OH_ImageReceiverOptions_SetSize(hidden->options, imgsize)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to set image receiver size option", imgrc);
+    } else if ((imgrc = OH_ImageReceiverOptions_SetCapacity(hidden->options, imgcapacity)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to set image receiver capacity option", imgrc);
+    } else if ((imgrc = OH_ImageReceiverNative_Create(hidden->options, &hidden->image_receiver)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to create native image receiver", imgrc);
+    } else if ((imgrc = OH_ImageReceiverNative_On(hidden->image_receiver, OnImageReceiverFrame)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to set native image receiver callback", imgrc);
+    } else if ((imgrc = OH_ImageReceiverNative_GetReceivingSurfaceId(hidden->image_receiver, &receiver_surface_id)) != IMAGE_SUCCESS) {
+        return SetImageError("Failed to create native image receiver", imgrc);
+    } else if (SDL_snprintf(receiver_surface_id_str, sizeof (receiver_surface_id_str), "%" SDL_PRIu64, receiver_surface_id) >= sizeof (receiver_surface_id_str)) {
+        return SDL_OutOfMemory();  // shouldn't happen.
+    } else if ((rc = OH_CameraDevice_GetCameraOrientation(ohdevice, &orientation)) != CAMERA_OK) {
+        return SetCameraError("Failed to determine camera orientation", rc);
+    } else if ((rc = OH_CameraManager_CreatePreviewOutput(camera_manager, &video_profile, receiver_surface_id_str, &hidden->output)) != CAMERA_OK) {
+    //} else if ((rc = OH_CameraManager_CreateVideoOutput(camera_manager, &video_profile, receiver_surface_id_str, &hidden->output)) != CAMERA_OK) {
+        return SetCameraError("Failed to create video output", rc);
+    } else if ((rc = OH_PreviewOutput_RegisterCallback(hidden->output, &video_output_callbacks)) != CAMERA_OK) {
+    //} else if ((rc = OH_VideoOutput_RegisterCallback(hidden->output, &video_output_callbacks)) != CAMERA_OK) {
+        return SetCameraError("Failed to set video output callbacks", rc);
+    } else if ((rc = OH_CameraManager_CreateCameraInput(camera_manager, ohdevice, &hidden->input)) != CAMERA_OK) {
+        return SetCameraError("Failed to create camera input", rc);
+    } else if ((rc = OH_CameraInput_RegisterCallback(hidden->input, &camera_input_callbacks)) != CAMERA_OK) {
+        return SetCameraError("Failed to register camera input callbacks", rc);
+    } else if ((rc = OH_CameraInput_Open(hidden->input)) != CAMERA_OK) {
+        return SetCameraError("Failed to open camera input", rc);
+    } else if ((rc = OH_CameraManager_CreateCaptureSession(camera_manager, &hidden->session)) != CAMERA_OK) {
+        return SetCameraError("Failed to create camera capture session", rc);
+    } else if ((rc = OH_CaptureSession_SetSessionMode(hidden->session, NORMAL_VIDEO)) != CAMERA_OK) {
+        return SetCameraError("Failed to set camera capture session mode", rc);
+    } else if ((rc = OH_CaptureSession_BeginConfig(hidden->session)) != CAMERA_OK) {
+        return SetCameraError("Failed to begin camera capture session config", rc);
+    // !!! FIXME: maybe? } else if ((rc = OH_CaptureSession_SetActiveColorSpace(hidden->session, colorspace)) != CAMERA_OK) {
+    // !!! FIXME: maybe?     return SetCameraError("Failed to set active colorspace in session config", rc);
+    } else if ((rc = OH_CaptureSession_AddInput(hidden->session, hidden->input)) != CAMERA_OK) {
+        return SetCameraError("Failed to add device input to camera capture session", rc);
+    } else if ((rc = OH_CaptureSession_AddPreviewOutput(hidden->session, hidden->output)) != CAMERA_OK) {
+    //} else if ((rc = OH_CaptureSession_AddVideoOutput(hidden->session, hidden->output)) != CAMERA_OK) {
+        return SetCameraError("Failed to add video output to camera capture session", rc);
+    } else if ((rc = OH_CaptureSession_CommitConfig(hidden->session)) != CAMERA_OK) {
+        return SetCameraError("Failed to commit camera capture session config", rc);
+    } else if ((rc = OH_CaptureSession_Start(hidden->session)) != CAMERA_OK) {
+        return SetCameraError("Failed to start camera capture session", rc);
+    //} else if ((rc = OH_VideoOutput_Start(hidden->output)) != CAMERA_OK) {
+    //    return SetCameraError("Failed to start video output", rc);
+    }
+
+    // we don't need this, it's only in `hidden` so it'll clean up if we fail halfway through this.
+    OH_ImageReceiverOptions_Release(hidden->options);
+    hidden->options = NULL;
+
+    hidden->rotation = (int) (orientation % 360);
+
+    return true;
+}
+
+static void SDLCALL CameraPermissionCallback(void *userdata, const char *permission, bool granted)
+{
+    SDL_Camera *device = (SDL_Camera *) userdata;
+    if (device->hidden != NULL) {   // if device was already closed, don't send an event.
+        if (!granted) {
+            SDL_CameraPermissionOutcome(device, false);  // sorry, permission denied.
+        } else if (!PrepareCamera(device)) {  // permission given? Actually open the camera now.
+            // uhoh, setup failed; since the app thinks we already "opened" the device, mark it as disconnected and don't report the permission.
+            SDL_CameraDisconnected(device);
+        } else {
+            // okay! We have permission to use the camera _and_ opening the hardware worked out, report that the camera is usable!
+            SDL_CameraPermissionOutcome(device, true);  // go go go!
+        }
+    }
+
+    UnrefPhysicalCamera(device);   // we ref'd this in OpenDevice, release the extra reference.
+}
+
+
+static bool OPENHARMONYCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
+{
+#if 0
+    // !!! FIXME: Android requires all concurrent cameras to be opened before assigning them all to a session.
+    // !!! FIXME: OpenHarmony, in API level 18, added OH_CameraInput_OpenConcurrentCameras() and some other support APIs,
+    // !!! FIXME: which is probably the way to handle this, but we're adopting SDL's current Android attitude for now: if it fails, okay.
+    if (CheckDevicePlaying()) {
+        return SDL_SetError("A camera is already playing");
+    }
+#endif
+
+    if (spec->framerate_denominator != 1) {
+        return SDL_SetError("Invalid framerate");  // we force these to fps/1
+    }
+
+    device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
+    if (device->hidden == NULL) {
+        return false;
+    }
+
+    RefPhysicalCamera(device);  // ref'd until permission callback fires.
+
+    // just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
+    SDL_copyp(&device->hidden->requested_spec, spec);
+    if (!SDL_RequestOpenHarmonyPermission("ohos.permission.CAMERA", CameraPermissionCallback, device)) {
+        UnrefPhysicalCamera(device);
+        return false;
+    }
+
+    return true;  // we don't open the camera until permission is granted, so always succeed for now.
+}
+
+static void OPENHARMONYCAMERA_FreeDeviceHandle(SDL_Camera *device)
+{
+    if (device) {
+        Camera_Device *ohdevice = (Camera_Device *) device->handle;
+        SDL_free(ohdevice->cameraId);
+        SDL_free(ohdevice);
+    }
+}
+
+static void GatherCameraSpecs(Camera_Device *ohdevice, CameraFormatAddData *add_data, char **fullname, SDL_CameraPosition *position)
+{
+    Camera_ErrorCode rc = CAMERA_OK;
+    SDL_zerop(add_data);
+
+    *fullname = NULL;  // !!! FIXME: there doesn't appear to be a "get device's name" API for obtaining something like "Logitech WebCam".
+
+    if (ohdevice->cameraPosition == CAMERA_POSITION_FRONT) {
+        *position = SDL_CAMERA_POSITION_FRONT_FACING;
+        if (!*fullname) {
+            *fullname = SDL_strdup("Front-facing camera");
+        }
+    } else if (ohdevice->cameraPosition == CAMERA_POSITION_BACK) {
+        *position = SDL_CAMERA_POSITION_BACK_FACING;
+        if (!*fullname) {
+            *fullname = SDL_strdup("Back-facing camera");
+        }
+    } else {
+        *position = SDL_CAMERA_POSITION_UNKNOWN;
+    }
+
+    if (!*fullname) {
+        *fullname = SDL_strdup("Generic camera");   // we tried.
+    }
+
+    Camera_OutputCapability *output_caps = NULL;
+    rc = OH_CameraManager_GetSupportedCameraOutputCapabilityWithSceneMode(camera_manager, ohdevice, NORMAL_VIDEO, &output_caps);
+    if ((rc != CAMERA_OK) || !output_caps) {
+        return;
+    }
+
+    const uint32_t total = output_caps->previewProfilesSize;
+    //const uint32_t total = output_caps->videoProfilesSize;
+    for (uint32_t i = 0; i < total; i++) {
+        const Camera_Profile *vidprof = output_caps->previewProfiles[i];
+        //const Camera_VideoProfile *vidprof = output_caps->videoProfiles[i];
+        SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN;
+        SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN;
+        const int w = (int) vidprof->size.width;
+        const int h = (int) vidprof->size.height;
+        if ((w <= 0) || (h <= 0)) {
+            continue;
+        } else {
+            ConvertOpenHarmonyFormatToSDL(vidprof->format, &device_format, &device_colorspace);
+            if (device_format == SDL_PIXELFORMAT_UNKNOWN) {
+                continue;
+            }
+        }
+
+        SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, 60, 1);
+        //SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, vidprof->range.min, 1);
+        //if (vidprof->range.min != vidprof->range.max) {
+        //    SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, vidprof->range.max, 1);
+        //}
+    }
+
+    OH_CameraManager_DeleteSupportedCameraOutputCapability(camera_manager, output_caps);
+}
+
+static void MaybeAddDevice(Camera_Device *ohdevice)
+{
+    const char *devid = ohdevice->cameraId;
+    Camera_ErrorCode rc;
+
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: MaybeAddDevice('%s')", devid);
+    #endif
+
+    if (SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByDevicePtr, ohdevice)) {
+        return;  // already have this one.
+    }
+
+    Camera_SceneMode *scene_modes = NULL;
+    uint32_t num_scene_modes = 0;
+    rc = OH_CameraManager_GetSupportedSceneModes(ohdevice, &scene_modes, &num_scene_modes);
+    if ((rc != CAMERA_OK) || !scene_modes) {
+        return;
+    }
+
+    bool okay = false;
+    for (uint32_t i = 0; i < num_scene_modes; i++) {
+        if (scene_modes[i] == NORMAL_VIDEO) {
+            okay = true;
+            break;
+        }
+    }
+
+    OH_CameraManager_DeleteSceneModes(camera_manager, scene_modes);
+
+    if (!okay) {
+        return;  // uh...weird camera...?
+    }
+
+    SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN;
+    char *fullname = NULL;
+    CameraFormatAddData add_data;
+    GatherCameraSpecs(ohdevice, &add_data, &fullname, &position);
+    if (add_data.num_specs > 0) {
+        SDL_Camera *device = NULL;
+        Camera_Device *devcpy = (Camera_Device *) SDL_malloc(sizeof (*devcpy));
+        if (devcpy) {
+            devcpy->cameraId = SDL_strdup(devid);
+            if (devcpy->cameraId) {
+                device = SDL_AddCamera(fullname, position, add_data.num_specs, add_data.specs, devcpy);
+            }
+        }
+
+        if (!device) {
+            if (devcpy) {
+                SDL_free(devcpy->cameraId);
+                SDL_free(devcpy);
+            }
+        }
+    }
+
+    SDL_free(fullname);
+    SDL_free(add_data.specs);
+}
+
+// note that camera "availability" covers both hotplugging and whether another
+//  has the device opened, but for something like OpenHarmony, it's probably fine
+//  to treat both unplugging and loss of access as disconnection events. When
+//  the other app closes the camera, we get an available event as if it was
+//  just plugged back in.
+
+static void OnCameraDeviceStatus(Camera_Manager *cameraManager, Camera_StatusInfo *status)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB OnCameraDeviceStatus('%s', %d)", status->camera->cameraId, (int) status->status);
+    #endif
+    SDL_assert(status != NULL);
+    SDL_assert(status->camera != NULL);
+    SDL_assert(status->camera->cameraId != NULL);
+    if (status->status == CAMERA_STATUS_AVAILABLE) {
+        MaybeAddDevice(status->camera);
+    } else if (status->status == CAMERA_STATUS_UNAVAILABLE) {
+        // !!! FIXME: this comment is from Android, is this true here, too?
+        // THIS CALLBACK FIRES WHEN YOU OPEN THE DEVICE YOURSELF.  :(
+        // Make sure we don't have the device opened, in which case a different callback will fire instead if actually lost.
+        SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindOpenHarmonyCameraByDevicePtr, status->camera);
+        if (device && !device->hidden) {
+            SDL_CameraDisconnected(device);
+        }
+    }
+}
+
+
+static CameraManager_Callbacks camera_availability_listener = { OnCameraDeviceStatus };
+
+static void OPENHARMONYCAMERA_DetectDevices(void)
+{
+    Camera_ErrorCode rc = CAMERA_OK;
+    Camera_Device *cameras = NULL;
+    uint32_t total = 0;
+    rc = OH_CameraManager_GetSupportedCameras(camera_manager, &cameras, &total);
+
+    if ((rc == CAMERA_OK) && (total > 0))  {  // if this fails, maybe the callback catches things later.
+        for (uint32_t i = 0; i < total; i++) {
+            MaybeAddDevice(&cameras[i]);
+        }
+        OH_CameraManager_DeleteSupportedCameras(camera_manager, cameras, total);
+    }
+
+    OH_CameraManager_RegisterCallback(camera_manager, &camera_availability_listener);
+}
+
+static void OPENHARMONYCAMERA_Deinitialize(void)
+{
+    OH_CameraManager_UnregisterCallback(camera_manager, &camera_availability_listener);
+    DestroyCameraManager();
+}
+
+static bool OPENHARMONYCAMERA_Init(SDL_CameraDriverImpl *impl)
+{
+    if (!CreateCameraManager()) {
+        return false;
+    }
+
+    impl->DetectDevices = OPENHARMONYCAMERA_DetectDevices;
+    impl->OpenDevice = OPENHARMONYCAMERA_OpenDevice;
+    impl->CloseDevice = OPENHARMONYCAMERA_CloseDevice;
+    impl->WaitDevice = OPENHARMONYCAMERA_WaitDevice;
+    impl->AcquireFrame = OPENHARMONYCAMERA_AcquireFrame;
+    impl->ReleaseFrame = OPENHARMONYCAMERA_ReleaseFrame;
+    impl->FreeDeviceHandle = OPENHARMONYCAMERA_FreeDeviceHandle;
+    impl->Deinitialize = OPENHARMONYCAMERA_Deinitialize;
+
+    impl->ProvidesOwnCallbackThread = true;
+
+    return true;
+}
+
+CameraBootStrap OPENHARMONYCAMERA_bootstrap = {
+    "ohcamera", "SDL OpenHarmony camera driver", OPENHARMONYCAMERA_Init, false
+};
+
+#endif

+ 22 - 0
src/core/SDL_core_unsupported.c

@@ -204,3 +204,25 @@ bool SDL_IsUbuntuTouch(void)
 }
 }
 
 
 #endif
 #endif
+
+#ifndef SDL_PLATFORM_OPENHARMONY
+const char *SDL_GetOpenHarmonyInternalStoragePath(void)
+{
+    SDL_Unsupported();
+    return NULL;
+}
+
+int SDL_GetOpenHarmonySDKVersion(void)
+{
+    return SDL_Unsupported();
+}
+
+bool SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata)
+{
+    (void)permission;
+    (void)cb;
+    (void)userdata;
+    return SDL_Unsupported();
+}
+#endif
+

+ 8 - 0
src/core/SDL_core_unsupported.h

@@ -69,3 +69,11 @@ extern SDL_DECLSPEC Sint32 SDLCALL JNI_OnLoad(JavaVM *vm, void *reserved);
 #if !defined(SDL_PLATFORM_LINUX)
 #if !defined(SDL_PLATFORM_LINUX)
 extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void);
 extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void);
 #endif /* !SDL_PLATFORM_LINUX */
 #endif /* !SDL_PLATFORM_LINUX */
+
+#if !defined(SDL_PLATFORM_OPENHARMONY)
+typedef void *SDL_RequestOpenHarmonyPermissionCallback;
+extern SDL_DECLSPEC const char * SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void);
+extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void);
+extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata);
+#endif
+

+ 4 - 0
src/core/android/SDL_android.h

@@ -113,6 +113,10 @@ bool Android_JNI_FileClose(void *userdata);
 bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata);
 bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata);
 bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info);
 bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info);
 
 
+#define SDL_PlatformEnumerateAssetDirectory Android_JNI_EnumerateAssetDirectory
+#define SDL_GetPlatformInternalStoragePath SDL_GetAndroidInternalStoragePath
+#define SDL_PlatformGetAssetPathInfo Android_JNI_GetAssetPathInfo
+
 // Environment support
 // Environment support
 void Android_JNI_GetManifestEnvironmentVariables(void);
 void Android_JNI_GetManifestEnvironmentVariables(void);
 int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode);
 int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode);

+ 1207 - 0
src/core/openharmony/SDL_openharmony.c

@@ -0,0 +1,1207 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_PLATFORM_OPENHARMONY
+
+#include "../../events/SDL_events_c.h"
+
+// !!! FIXME: work around some C++isms that leaked into OpenHarmony headers.
+// !!! FIXME: HACK to prevent <AbilityKit/ability_runtime/start_options.h> from including. It has '&' instead of '*' for some args, which suggests it's only been tested with C++.
+#define ABILITY_RUNTIME_START_OPTIONS_H
+typedef enum AbilityRuntime_StartOptions AbilityRuntime_StartOptions;
+
+
+// !!! FIXME: <rawfile/raw_file.h> has functions with C++ references. Prevent raw_file_manager.h from including it, and define the parts we need here.
+#define GLOBAL_RAW_FILE_H
+typedef struct RawFile RawFile;
+typedef struct RawFile64 RawFile64;
+typedef struct { int fd; long start; long length; } RawFileDescriptor;
+typedef struct { int fd; int64_t start; int64_t length; } RawFileDescriptor64;
+int64_t OH_ResourceManager_GetRawFileSize64(RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0)));
+int OH_ResourceManager_SeekRawFile64(const RawFile64 *rawFile, int64_t offset, int whence) __attribute__((__availability__(ohos, introduced=11.0.0)));
+int64_t OH_ResourceManager_ReadRawFile64(const RawFile64 *rawFile, void *buf, int64_t length) __attribute__((__availability__(ohos, introduced=11.0.0)));
+int64_t OH_ResourceManager_GetRawFileRemainingLength64(const RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0)));
+int64_t OH_ResourceManager_GetRawFileOffset64(const RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0)));
+void OH_ResourceManager_CloseRawFile64(RawFile64 *rawFile) __attribute__((__availability__(ohos, introduced=11.0.0)));
+bool OH_ResourceManager_GetRawFileDescriptor64(const RawFile64 *rawFile, RawFileDescriptor64 *descriptor) __attribute__((__availability__(ohos, introduced=11.0.0)));
+bool OH_ResourceManager_ReleaseRawFileDescriptor64(const RawFileDescriptor64 *descriptor) __attribute__((__availability__(ohos, introduced=11.0.0)));
+
+#include <window_manager/oh_display_manager.h>
+
+// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...?
+#define OH_XCOMPONENT_ID_LEN_MAX sdl_core_ohos_OH_XCOMPONENT_ID_LEN_MAX
+#define OH_MAX_TOUCH_POINTS_NUMBER sdl_core_ohos_OH_MAX_TOUCH_POINTS_NUMBER
+#include <ace/xcomponent/native_interface_xcomponent.h>
+
+#include <AbilityKit/ability_runtime/application_context.h>
+#include <BasicServicesKit/oh_commonevent.h>
+#include <BasicServicesKit/oh_commonevent_support.h>
+#include <deviceinfo.h>
+#include <rawfile/raw_file_manager.h>
+#include <hilog/log.h>
+#include <stdlib.h>
+
+// !!! FIXME: which of these headers do we actually need?
+#include <js_native_api.h>
+#include <js_native_api_types.h>
+#include <node_api.h>
+#include <node_api_types.h>
+#include <napi/native_api.h>
+
+#include "SDL_openharmony.h"
+#include "../../video/openharmony/SDL_openharmonyvideo.h"
+#include "../../video/openharmony/SDL_openharmonyevents.h"
+
+static CommonEvent_Subscriber *commonevent_subscriber = NULL;
+static napi_ref ability_object_ref = NULL;
+static napi_ref atmanager_ref = NULL;
+static napi_ref window_ref = NULL;
+static napi_ref ime_controller_ref = NULL;
+static napi_ref pointer_ref = NULL;
+static napi_ref on_insert_text_ref = NULL;
+static napi_ref on_delete_left_ref = NULL;
+static napi_ref i18nsystem_ref = NULL;
+static NativeResourceManager *native_resource_mgr = NULL;
+static napi_threadsafe_function syslocalechanged_threadsafefn = NULL;
+static napi_threadsafe_function req_permissions_threadsafefn = NULL;
+static napi_threadsafe_function open_url_threadsafefn = NULL;
+static napi_threadsafe_function change_sysbars_threadsafefn = NULL;
+static napi_threadsafe_function change_screensaver_threadsafefn = NULL;
+static napi_threadsafe_function show_screenkeyboard_threadsafefn = NULL;
+static napi_threadsafe_function hide_screenkeyboard_threadsafefn = NULL;
+static napi_threadsafe_function change_mouseptr_threadsafefn = NULL;
+static char *system_locale = NULL;
+static SDL_SystemTheme system_theme = SDL_SYSTEM_THEME_UNKNOWN;
+static SDL_DisplayOrientation device_orientation = SDL_ORIENTATION_PORTRAIT;
+
+
+// Some NAPI helper code...
+
+#define SDL_JS_ENTRY(expected_argc) \
+    size_t argc = expected_argc; \
+    napi_value argv[expected_argc]; \
+    napi_value self = NULL; \
+    napi_get_cb_info(env, info, &argc, argv, &self, NULL)
+
+#define SDL_JS_ENTRY_USERDATA(expected_argc, userdata_type) \
+    userdata_type *userdata = NULL; \
+    size_t argc = expected_argc; \
+    napi_value argv[expected_argc]; \
+    napi_value self = NULL; \
+    napi_get_cb_info(env, info, &argc, argv, &self, (void **) &userdata)
+
+#define CallNapiThreadsafeFunction napi_call_threadsafe_function
+#define SetNapiArrayElement napi_set_element
+#define SetNapiObjField napi_set_named_property
+
+static char *CreateSDLStringFromNAPIValue(napi_env env, napi_value val)
+{
+    char *retval = NULL;
+    size_t buflen = 0;
+    if (napi_get_value_string_utf8(env, val, NULL, 0, &buflen) != napi_ok) {
+        return NULL;
+    } else if ((retval = (char *) SDL_malloc(buflen + 1)) == NULL) {
+        return NULL;
+    } else if (napi_get_value_string_utf8(env, val, retval, buflen + 1, &buflen) != napi_ok) {
+        SDL_free(retval);
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CreateNapiString(napi_env env, const char *str)
+{
+    napi_value retval = NULL;
+    if (napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CreateNapiObject(napi_env env)
+{
+    napi_value retval = NULL;
+    if (napi_create_object(env, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CreateNapiArray(napi_env env, size_t len)
+{
+    napi_value retval = NULL;
+    if (napi_create_array_with_length(env, len, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CreateNapiFunction(napi_env env, const char *name, napi_callback cb, void *userdata)
+{
+    napi_value retval = NULL;
+    if (napi_create_function(env, name, name ? NAPI_AUTO_LENGTH : 0, cb, userdata, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value GetNapiArrayElement(napi_env env, napi_value arr, int idx)
+{
+    napi_value retval = NULL;
+    if (napi_get_element(env, arr, idx, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CreateNapiInt(napi_env env, int val)
+{
+    napi_value retval = NULL;
+    if (napi_create_int32(env, (int32_t) val, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static int GetNapiInt(napi_env env, napi_value val, int deflt)
+{
+    int32_t retval = (int32_t) deflt;
+    if (napi_get_value_int32(env, val, &retval) != napi_ok) {
+        return deflt;
+    }
+    return (int) retval;
+}
+
+static napi_value GetNapiBoolean(napi_env env, bool b)
+{
+    napi_value retval = NULL;
+    if (napi_get_boolean(env, b, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value GetNapiUndefined(napi_env env)
+{
+    napi_value retval = NULL;
+    napi_get_undefined(env, &retval);
+    return retval;
+}
+
+static napi_value GetNapiObjField(napi_env env, napi_value obj, const char *fieldname)
+{
+    napi_value retval = NULL;
+    if (napi_get_named_property(env, obj, fieldname, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value GetNapiRefValue(napi_env env, napi_ref ref)
+{
+    napi_value retval = NULL;
+    if (napi_get_reference_value(env, ref, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_threadsafe_function CreateNapiThreadsafeFunction(napi_env env, const char *name, napi_threadsafe_function_call_js fn)
+{
+    napi_threadsafe_function retval = NULL;
+    if (napi_create_threadsafe_function(env, NULL, NULL, CreateNapiString(env, name), 0, 1, NULL, NULL, NULL, fn, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+static napi_value CallNapiMethod(napi_env env, napi_value obj, const char *method, int num_args, napi_value *args)
+{
+    napi_value retval = NULL;
+    if (napi_call_function(env, obj, GetNapiObjField(env, obj, method), num_args, args, &retval) != napi_ok) {
+        return NULL;
+    }
+    return retval;
+}
+
+
+int SDL_GetOpenHarmonySDKVersion(void)
+{
+    static int sdk_version;
+    if (!sdk_version) {
+        sdk_version = OH_GetSdkApiVersion();
+    }
+    return sdk_version;
+}
+
+SDL_FormFactor SDL_GetOpenHarmonyDeviceFormFactor(void)
+{
+    static bool checked = false;
+    static SDL_FormFactor form_factor = SDL_FORMFACTOR_UNKNOWN;
+    if (!checked) {
+        checked = true;
+        const char *devtype = OH_GetDeviceType();
+        if (devtype) {
+            if ((SDL_strcmp(devtype, "phone") == 0) || (SDL_strcmp(devtype, "default") == 0)) {
+                form_factor = SDL_FORMFACTOR_PHONE;
+            } else if (SDL_strcmp(devtype, "wearable") == 0) {   // !!! FIXME: what is the difference between a "wearable" and "liteWearable"?
+                form_factor = SDL_FORMFACTOR_WATCH;
+            } else if (SDL_strcmp(devtype, "liteWearable") == 0) {
+                form_factor = SDL_FORMFACTOR_WATCH;
+            } else if (SDL_strcmp(devtype, "tablet") == 0) {
+                form_factor = SDL_FORMFACTOR_TABLET;
+            } else if (SDL_strcmp(devtype, "tv") == 0) {
+                form_factor = SDL_FORMFACTOR_TV;
+            } else if (SDL_strcmp(devtype, "car") == 0) {
+                form_factor = SDL_FORMFACTOR_CAR;
+            } else if (SDL_strcmp(devtype, "smartVision") == 0) {  // !!! FIXME: I assume this is either some sort of AI glasses or a VR headset...?
+                form_factor = SDL_FORMFACTOR_HEADSET;
+            } else {
+                // !!! FIXME: aren't there desktops that can run HarmonyOS? Or are there just tablets?
+            }
+        }
+    }
+    return form_factor;
+}
+
+void SDL_DebugLogOpenHarmonyInfo(void)
+{
+    static bool already_logged = false;
+    if (!already_logged) {
+        already_logged = true;
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, "SDL OpenHarmony system/device info:");
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Device type: %s", OH_GetDeviceType());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Manufacturer: %s", OH_GetManufacture());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Brand: %s", OH_GetBrand());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Market name: %s", OH_GetMarketName());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Product series: %s", OH_GetProductSeries());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Product model: %s", OH_GetProductModel());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Software model: %s", OH_GetSoftwareModel());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Hardware model: %s", OH_GetHardwareModel());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Bootloader version: %s", OH_GetBootloaderVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - ABI list: %s", OH_GetAbiList());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Security patch tag: %s", OH_GetSecurityPatchTag());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Display version: %s", OH_GetDisplayVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Incremental version: %s", OH_GetIncrementalVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - OS release type: %s", OH_GetOsReleaseType());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - OS full name: %s", OH_GetOSFullName());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - SDK API version: %d", SDL_GetOpenHarmonySDKVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - First API version: %d", OH_GetFirstApiVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Version ID: %s", OH_GetVersionId());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build type: %s", OH_GetBuildType());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build user: %s", OH_GetBuildUser());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build host: %s", OH_GetBuildHost());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build time: %s", OH_GetBuildTime());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Build root hash: %s", OH_GetBuildRootHash());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS name: %s", OH_GetDistributionOSName());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS version: %s", OH_GetDistributionOSVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS API version: %d", OH_GetDistributionOSApiVersion());
+        SDL_LogDebug(SDL_LOG_CATEGORY_SYSTEM, " - Distribution OS release type: %s", OH_GetDistributionOSReleaseType());
+    }
+}
+
+const char *SDL_GetOpenHarmonySystemLocale(void)
+{
+    return system_locale;
+}
+
+SDL_SystemTheme SDL_GetOpenHarmonySystemTheme(void)
+{
+    return system_theme;
+}
+
+SDL_DisplayOrientation SDL_GetOpenHarmonyDeviceCurrentOrientation(void)
+{
+    return device_orientation;
+}
+
+const char *SDL_GetOpenHarmonyInternalStoragePath(void)
+{
+    static char *files_path = NULL;
+    if (!files_path) {
+        char *path = NULL;
+        int32_t writelen = 0;
+        size_t slen = 128;
+        while (true) {
+            void *ptr = SDL_realloc(path, slen + 1);  // +1 to save space for null terminator.
+            if (!ptr) {
+                SDL_free(path);
+                return NULL;
+            }
+            path = (char *) ptr;
+
+            const AbilityRuntime_ErrorCode rc = OH_AbilityRuntime_ApplicationContextGetFilesDir(path, slen, &writelen);
+            if (rc == ABILITY_RUNTIME_ERROR_CODE_NO_ERROR) {
+                break;
+            } else if (rc != ABILITY_RUNTIME_ERROR_CODE_PARAM_INVALID) {
+                SDL_SetError("OH_AbilityRuntime_ApplicationContextGetFilesDir failed: %d", (int) rc);
+                SDL_free(path);
+                return NULL;
+            }
+            slen *= 2;  // try again with a bigger buffer.
+        }
+
+        files_path = (char *) SDL_realloc(path, SDL_strlen(path) + 1);  // shrink it down.
+        if (!files_path) {
+            files_path = path;  // oh well, _don't_ shrink it down...
+        }
+    }
+
+    return files_path;
+}
+
+
+// Filesystem stuff...
+
+bool SDL_OpenHarmonyRawFileOpen(void **puserdata, const char *fileName, const char *mode)
+{
+    if (SDL_strncmp(fileName, "assets://", 9) == 0) {
+        fileName += 9;
+    }
+
+    SDL_assert(native_resource_mgr != NULL);   // ArkTS should have sent us this at startup.
+    SDL_assert(puserdata != NULL);
+
+    if (mode && (SDL_strcmp(mode, "r") != 0) && (SDL_strcmp(mode, "rb") != 0)) {
+        return SDL_SetError("RawFile access is read-only");
+    }
+
+    RawFile64 *rf64 = OH_ResourceManager_OpenRawFile64(native_resource_mgr, fileName);
+    if (!rf64) {
+        return SDL_SetError("Failed to open RawFile");
+    }
+
+    *puserdata = rf64;
+    return true;
+}
+
+Sint64 SDL_OpenHarmonyRawFileSize(void *userdata)
+{
+    return (Sint64) OH_ResourceManager_GetRawFileSize64((RawFile64 *) userdata);
+}
+
+Sint64 SDL_OpenHarmonyRawFileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence)
+{
+    const int ohwhence = (int) whence;  // these values happen to match.
+    if (OH_ResourceManager_SeekRawFile64((const RawFile64 *) userdata, (int64_t) offset, ohwhence) < 0) {
+        SDL_SetError("RawFile seek failed");
+        return -1;
+    }
+    return (Sint64) OH_ResourceManager_GetRawFileOffset64((const RawFile64 *) userdata);
+}
+
+size_t SDL_OpenHarmonyRawFileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status)
+{
+    const size_t br = (size_t) OH_ResourceManager_ReadRawFile64((const RawFile64 *) userdata, buffer, (int64_t) size);  // this returns 0 on eof/error, not a negative, so just cast to size_t.
+    if (br < size) {
+        if (OH_ResourceManager_GetRawFileRemainingLength64((const RawFile64 *) userdata) == 0) {
+            *status = SDL_IO_STATUS_EOF;
+        } else {
+            *status = SDL_IO_STATUS_ERROR;
+            SDL_SetError("RawFile read failed");
+        }
+    }
+    return br;
+}
+
+bool SDL_OpenHarmonyRawFileClose(void *userdata)
+{
+    OH_ResourceManager_CloseRawFile64((RawFile64 *) userdata);
+    return true;
+}
+
+bool SDL_OpenHarmonyEnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata)
+{
+    const char *origpath = path;
+    if (SDL_strncmp(path, "assets://", 9) == 0) {
+        path += 9;
+    }
+
+    SDL_assert(native_resource_mgr != NULL);   // ArkTS should have sent us this at startup.
+    RawDir *rawdir = OH_ResourceManager_OpenRawDir(native_resource_mgr, path);
+    if (!rawdir) {
+        return SDL_SetError("RawDir open failed");
+    }
+
+    SDL_EnumerationResult result = SDL_ENUM_CONTINUE;
+    const int total = OH_ResourceManager_GetRawFileCount(rawdir);
+    for (int i = 0; (i < total) && (result == SDL_ENUM_CONTINUE); i++) {
+        const char *fname = OH_ResourceManager_GetRawFileName(rawdir, i);
+        result = cb(userdata, origpath, fname);
+    }
+
+    OH_ResourceManager_CloseRawDir(rawdir);
+
+    return (result != SDL_ENUM_FAILURE);
+}
+
+bool SDL_OpenHarmonyGetAssetPathInfo(const char *path, SDL_PathInfo *info)
+{
+    if (SDL_strncmp(path, "assets://", 9) == 0) {
+        path += 9;
+    }
+
+    SDL_assert(native_resource_mgr != NULL);   // ArkTS should have sent us this at startup.
+    SDL_zerop(info);
+    if (OH_ResourceManager_IsRawDir(native_resource_mgr, path)) {
+        info->type = SDL_PATHTYPE_DIRECTORY;
+    } else {
+        RawFile64 *rf64 = OH_ResourceManager_OpenRawFile64(native_resource_mgr, path);
+        if (!rf64) {
+            return SDL_SetError("No such file or directory");
+        }
+        info->type = SDL_PATHTYPE_FILE;
+        info->size = (Uint64) OH_ResourceManager_GetRawFileSize64(rf64);
+        OH_ResourceManager_CloseRawFile64(rf64);
+    }
+    return true;
+}
+
+
+// ArkTS/C bridging...
+
+typedef struct RequestPermissionData
+{
+    char *permission;
+    SDL_RequestOpenHarmonyPermissionCallback callback;
+    void *callback_userdata;
+} RequestPermissionData;
+
+// AsyncCallback when CallJSRequestPermissions() finishes its work.
+static napi_value SDL_JS_RequestPermissionResult(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY_USERDATA(2, RequestPermissionData);
+    napi_value result = GetNapiArrayElement(env, GetNapiObjField(env, argv[1], "authResults"), 0);
+    const bool granted = (GetNapiInt(env, result, -1) == 0);
+    userdata->callback(userdata->callback_userdata, userdata->permission, granted);
+
+    SDL_free(userdata->permission);
+    SDL_free(userdata);
+
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSRequestPermissions(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    RequestPermissionData *data = (RequestPermissionData *) userdata;
+    napi_value ability = GetNapiRefValue(env, ability_object_ref);
+    napi_value atmanager = GetNapiRefValue(env, atmanager_ref);
+    napi_value args[] = { GetNapiObjField(env, ability, "context"), CreateNapiArray(env, 1), CreateNapiFunction(env, NULL, SDL_JS_RequestPermissionResult, data) };
+    SetNapiArrayElement(env, args[1], 0, CreateNapiString(env, data->permission));
+    CallNapiMethod(env, atmanager, "requestPermissionsFromUser", SDL_arraysize(args), args);
+    // okay, assuming this worked out, we'll get a callback to SDL_JS_RequestPermissionResult() at some point in the future (if we haven't already).
+}
+
+bool SDL_RequestOpenHarmonyPermission(const char *permission, SDL_RequestOpenHarmonyPermissionCallback cb, void *userdata)
+{
+    RequestPermissionData *data = NULL;
+
+    if (!permission) {
+        return SDL_InvalidParamError("permission");
+    } else if (!cb) {
+        return SDL_InvalidParamError("cb");
+    } else if (!atmanager_ref) {
+        return SDL_SetError("atManager not initialized");
+    } else if ((data = (RequestPermissionData *) SDL_calloc(1, sizeof (*data))) == NULL) {
+        return false;
+    } else if ((data->permission = SDL_strdup(permission)) == NULL) {
+        SDL_free(data);
+        return false;
+    }
+
+    data->callback = cb;
+    data->callback_userdata = userdata;
+
+    return (CallNapiThreadsafeFunction(req_permissions_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok);
+}
+
+// AsyncCallback when CallJSOpenURL() finishes its work.
+static napi_value SDL_JS_OpenURLResult(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY_USERDATA(1, char);
+    SDL_free(userdata);
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSOpenURL(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    char *url = (char *) userdata;
+    napi_value ability = GetNapiRefValue(env, ability_object_ref);
+    napi_value abcontext = GetNapiObjField(env, ability, "context");
+    napi_value args[] = { CreateNapiString(env, url), CreateNapiFunction(env, NULL, SDL_JS_OpenURLResult, url) };
+    CallNapiMethod(env, abcontext, "openLink", SDL_arraysize(args), args);
+}
+
+bool SDL_OpenHarmonyOpenURL(const char *url)
+{
+    char *data = NULL;
+    if (!url) {
+        return SDL_InvalidParamError("url");
+    } else if (!ability_object_ref) {
+        return SDL_SetError("Ability not initialized");
+    } else if ((data = SDL_strdup(url)) == NULL) {
+        return false;
+    }
+    return (CallNapiThreadsafeFunction(open_url_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok);
+}
+
+
+// AsyncCallback when CallJSChangeSysBars() finishes its work.
+static napi_value SDL_JS_ChangeSysBarsResult(napi_env env, napi_callback_info info)
+{
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSChangeSysBars(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    const intptr_t flags = (const intptr_t) userdata;
+    const bool status_bar = (flags & (1 << 0)) != 0;
+    const bool navigation_bar = (flags & (1 << 1)) != 0;
+    int arrlen = 0;
+    if (status_bar) { arrlen++; }
+    if (navigation_bar) { arrlen++; }
+
+    napi_value arr = CreateNapiArray(env, arrlen);
+    arrlen = 0;
+    if (status_bar) {
+        SetNapiArrayElement(env, arr, arrlen++, CreateNapiString(env, "status"));
+    }
+    if (navigation_bar) {
+        SetNapiArrayElement(env, arr, arrlen++, CreateNapiString(env, "navigation"));
+    }
+
+    napi_value window = GetNapiRefValue(env, window_ref);
+    napi_value args[] = { arr, CreateNapiFunction(env, NULL, SDL_JS_ChangeSysBarsResult, NULL) };
+    CallNapiMethod(env, window, "setSystemBarEnable", SDL_arraysize(args), args);
+}
+
+bool SDL_OpenHarmonyToggleSystemBars(bool status_bar, bool navigation_bar)
+{
+    const intptr_t flags = (status_bar ? (1 << 0) : 0) | (navigation_bar ? (1 << 1) : 0);
+    if (!window_ref) {
+        return SDL_SetError("Window not initialized");
+    }
+    return (CallNapiThreadsafeFunction(change_sysbars_threadsafefn, (void *) flags, napi_tsfn_nonblocking) == napi_ok);
+}
+
+// AsyncCallback when CallJSChangeScreenSaver() finishes its work.
+static napi_value SDL_JS_ChangeScreenSaverResult(napi_env env, napi_callback_info info)
+{
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSChangeScreenSaver(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    napi_value window = GetNapiRefValue(env, window_ref);
+    napi_value args[] = { GetNapiBoolean(env, (userdata != NULL)), CreateNapiFunction(env, NULL, SDL_JS_ChangeScreenSaverResult, NULL) };
+    CallNapiMethod(env, window, "setWindowKeepScreenOn", SDL_arraysize(args), args);
+}
+
+bool SDL_OpenHarmonyChangeScreenSaver(bool enable)
+{
+    if (!window_ref) {
+        return SDL_SetError("Window not initialized");
+    }
+    return (CallNapiThreadsafeFunction(change_screensaver_threadsafefn, (void *) (size_t) (enable ? 0x1 : 0x0), napi_tsfn_nonblocking) == napi_ok);
+}
+
+// Called by the IME controller for insertText events.
+static napi_value SDL_JS_IME_Controller_OnInsertText(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY(1);
+    napi_value text = argv[0];
+    char *utf8 = CreateSDLStringFromNAPIValue(env, text);
+    if (utf8) {
+        SDL_SendKeyboardText(utf8);
+        SDL_free(utf8);
+    }
+    return GetNapiUndefined(env);
+}
+
+// Called by the IME controller for deleteLeft events.
+static napi_value SDL_JS_IME_Controller_OnDeleteLeft(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY(1);
+    napi_value number = argv[0];
+    const int total = GetNapiInt(env, number, 0);
+    for (int i = 0; i < total; i++) {
+        SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true);
+        SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false);
+    }
+    return GetNapiUndefined(env);
+}
+
+
+typedef struct ShowKeyboardData
+{
+    int input_type;
+    int cap_type;
+    SDL_Rect *text_input_rect;
+    SDL_Rect text_input_rect_data;
+} ShowKeyboardData;
+
+// AsyncCallback when CallJSShowScreenKeyboard() finishes its work.
+static napi_value SDL_JS_ShowScreenKeyboardResult(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY_USERDATA(0, ShowKeyboardData);
+    napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref);
+    napi_value ime_oninserttext_args[] = { CreateNapiString(env, "insertText"), GetNapiRefValue(env, on_insert_text_ref) };
+    CallNapiMethod(env, ime_controller, "on", SDL_arraysize(ime_oninserttext_args), ime_oninserttext_args);
+    napi_value ime_ondeleteleft_args[] = { CreateNapiString(env, "deleteLeft"), GetNapiRefValue(env, on_delete_left_ref) };
+    CallNapiMethod(env, ime_controller, "on", SDL_arraysize(ime_ondeleteleft_args), ime_ondeleteleft_args);
+    SDL_SendScreenKeyboardShown();
+    SDL_free(userdata);
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSShowScreenKeyboard(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    ShowKeyboardData *data = (ShowKeyboardData *) userdata;
+    napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref);
+
+    napi_value inputAttribute = CreateNapiObject(env);
+    SetNapiObjField(env, inputAttribute, "textInputType", CreateNapiInt(env, data->input_type));
+    SetNapiObjField(env, inputAttribute, "capitalizeMode", CreateNapiInt(env, data->cap_type));
+    SetNapiObjField(env, inputAttribute, "enterKeyType", CreateNapiInt(env, 0 /*inputMethod.enterKeyType.UNSPECIFIED*/));  // !!! FIXME
+
+    napi_value attach_params = CreateNapiObject(env);
+    SetNapiObjField(env, attach_params, "inputAttribute", inputAttribute);
+    
+    napi_value args[] = { GetNapiBoolean(env, true) /*showkeyboard*/, attach_params, CreateNapiFunction(env, NULL, SDL_JS_ShowScreenKeyboardResult, userdata) };
+    CallNapiMethod(env, ime_controller, "attach", SDL_arraysize(args), args);
+}
+
+bool SDL_OpenHarmonyShowScreenKeyboard(int input_type, int cap_type, const SDL_Rect *text_input_rect)
+{
+    if (!ime_controller_ref) {
+        return SDL_SetError("IME controller not initialized");
+    }
+
+    ShowKeyboardData *data = (ShowKeyboardData *) SDL_calloc(1, sizeof (*data));
+    if (!data) {
+        return false;
+    }
+    data->input_type = input_type;
+    data->cap_type = cap_type;
+    if (text_input_rect) {
+        SDL_copyp(&data->text_input_rect_data, text_input_rect);
+        data->text_input_rect = &data->text_input_rect_data;
+    }
+
+    return (CallNapiThreadsafeFunction(show_screenkeyboard_threadsafefn, data, napi_tsfn_nonblocking) == napi_ok);
+}
+
+// AsyncCallback when CallJSHideScreenKeyboard() finishes its work.
+static napi_value SDL_JS_HideScreenKeyboardResult(napi_env env, napi_callback_info info)
+{
+    napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref);
+    napi_value ime_oninserttext_args[] = { CreateNapiString(env, "insertText"), GetNapiRefValue(env, on_insert_text_ref) };
+    CallNapiMethod(env, ime_controller, "off", SDL_arraysize(ime_oninserttext_args), ime_oninserttext_args);
+    napi_value ime_ondeleteleft_args[] = { CreateNapiString(env, "deleteLeft"), GetNapiRefValue(env, on_delete_left_ref) };
+    CallNapiMethod(env, ime_controller, "off", SDL_arraysize(ime_ondeleteleft_args), ime_ondeleteleft_args);
+    SDL_SendScreenKeyboardHidden();
+    return GetNapiUndefined(env);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSHideScreenKeyboard(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    napi_value ime_controller = GetNapiRefValue(env, ime_controller_ref);
+    napi_value args[] = { CreateNapiFunction(env, NULL, SDL_JS_HideScreenKeyboardResult, userdata) };
+    CallNapiMethod(env, ime_controller, "hideTextInput", SDL_arraysize(args), args);
+}
+
+bool SDL_OpenHarmonyHideScreenKeyboard(void)
+{
+    if (!ime_controller_ref) {
+        return SDL_SetError("IME controller not initialized");
+    }
+    return (CallNapiThreadsafeFunction(hide_screenkeyboard_threadsafefn, NULL, napi_tsfn_nonblocking) == napi_ok);
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSChangeMousePointer(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    const bool enable = (userdata != NULL);
+    napi_value pointer = GetNapiRefValue(env, pointer_ref);
+    napi_value args[] = { GetNapiBoolean(env, enable) };
+//SDL_Log("%s MOUSE POINTER!", enable ? "SHOW" : "HIDE");
+    CallNapiMethod(env, pointer, "setPointerVisibleSync", SDL_arraysize(args), args);
+}
+
+static bool SDL_OpenHarmonyChangeMousePointer(bool enable)
+{
+    if (!pointer_ref) {
+        return SDL_SetError("Pointer namespace not initialized");
+    }
+    return (CallNapiThreadsafeFunction(change_mouseptr_threadsafefn, (void *) (size_t) (enable ? 0x1 : 0x0), napi_tsfn_nonblocking) == napi_ok);
+}
+
+bool SDL_OpenHarmonyShowMousePointer(void)
+{
+    return SDL_OpenHarmonyChangeMousePointer(true);
+}
+
+bool SDL_OpenHarmonyHideMousePointer(void)
+{
+    return SDL_OpenHarmonyChangeMousePointer(false);
+}
+
+
+// This _must_ be called from the Javascript thread, since it makes NAPI calls!
+static bool UpdateSystemLocale(napi_env env)  // true if known locale changed, false otherwise.
+{
+    if (!i18nsystem_ref) {
+        return false;
+    }
+
+    napi_value i18n = GetNapiRefValue(env, i18nsystem_ref);
+    if (!i18n) {
+        return false;  // uhoh.
+    }
+
+    napi_value language = CallNapiMethod(env, i18n, "getSystemLanguage", 0, NULL);
+    napi_value region = CallNapiMethod(env, i18n, "getSystemRegion", 0, NULL);
+
+    bool retval = false;
+    if (language && region) {
+        char *utf8lang = CreateSDLStringFromNAPIValue(env, language);
+        char *utf8region = CreateSDLStringFromNAPIValue(env, region);
+        if (utf8lang && utf8region) {
+            char *ptr = SDL_strchr(utf8lang, '-');  // if we get "en-Latn-US" or whatever, we just want "en".
+            if (ptr) {
+                *ptr = '\0';  // chop it off.
+            }
+            char *utf8locale = NULL;
+            if (SDL_asprintf(&utf8locale, "%s_%s", utf8lang, utf8region) > 0) {
+                retval = (!system_locale || (SDL_strcmp(system_locale, utf8locale) != 0));
+                if (!retval) {  // didn't change, free the new string.
+                    SDL_free(utf8locale);
+                } else {  // changed, swap them out, free the old string.
+                    char *tmp = system_locale;
+                    system_locale = utf8locale;
+                    SDL_free(tmp);
+                }
+            }
+        }
+        SDL_free(utf8region);
+        SDL_free(utf8lang);
+    }
+
+    return retval;
+}
+
+// this function is called from the main Javascript thread when it's convenient to fire it.
+static void CallJSSystemLocaleChanged(napi_env env, napi_value js_callback, void *context, void *userdata)
+{
+    if (UpdateSystemLocale(env)) {
+        SDL_SendLocaleChangedEvent();
+    }
+}
+
+
+// !!! FIXME: remove the `SDL_` prefix on these static functions.
+
+// Callbacks into our custom XComponent.
+static void SDL_XComponent_OnSurfaceCreatedCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_assert(native_resource_mgr != NULL);   // ArkTS should have sent us this at startup. Are you using our startup scripts?
+    extern void SDL_OpenHarmonyMainSurfaceCreated(void);  // this is in src/main/openharmony/SDL_sysmain_runapp.c
+    SDL_OpenHarmonyVideoSurfaceCreated(component, window);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+    SDL_OpenHarmonyMainSurfaceCreated();  // start the actual native code app if this is the first surface.
+}
+
+static void SDL_XComponent_OnFrameCallback(OH_NativeXComponent* component, uint64_t timestamp, uint64_t targetTimestamp)
+{
+    extern void SDL_OpenHarmonyOnFrameCallback(void);  // this is in src/main/openharmony/SDL_sysmain_callbacks.c
+    SDL_OpenHarmonyOnFrameCallback();  // This fires SDL_AppInterate.
+}
+
+static void SDL_XComponent_OnSurfaceChangedCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_OpenHarmonyVideoSurfaceChanged(component, window);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+}
+
+static void SDL_XComponent_OnSurfaceDestroyedCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_OpenHarmonyVideoSurfaceDestroyed(component, window);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+}
+
+static void SDL_XComponent_DispatchTouchEventCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_OpenHarmonyDispatchTouchEvent(component, window);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+}
+
+static void SDL_XComponent_DispatchMouseEventCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_OpenHarmonyDispatchMouseEvent(component, window);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+}
+
+static void SDL_XComponent_DispatchHoverEventCallback(OH_NativeXComponent* component, bool isHover)
+{
+    // !!! FIXME: use this?
+}
+
+static void SDL_XComponent_DispatchUIInputEventCallback(OH_NativeXComponent* component, ArkUI_UIInputEvent* event, ArkUI_UIInputEvent_Type type)
+{
+    SDL_OpenHarmonyDispatchUIInputEvent(component, event, type);  // this is in src/video/openharmony/SDL_openharmonyvideo.c
+}
+
+static void SDL_XComponent_DispatchKeyEventCallback(OH_NativeXComponent* component, void* window)
+{
+    SDL_OpenHarmonyDispatchKeyEvent(component, window);
+}
+
+static void OpenHarmonyCommonEventReceiver(const CommonEvent_RcvData *data)
+{
+    const char *evname = OH_CommonEvent_GetEventFromRcvData(data);
+    if (!evname) {
+        return;
+    } else if (SDL_strcmp(evname, COMMON_EVENT_LOCALE_CHANGED) == 0) {
+        // make sure we're in the javascript thread so we can call into the i18n system object via NAPI.
+        // this will update system_locale and then fire the SDL locale-changed event if appropriate.
+        CallNapiThreadsafeFunction(syslocalechanged_threadsafefn, NULL, napi_tsfn_nonblocking);
+    }
+}
+
+static void UpdateDeviceOrientation(void)
+{
+    NativeDisplayManager_Orientation orientation = DISPLAY_MANAGER_PORTRAIT;
+    OH_NativeDisplayManager_GetDefaultDisplayOrientation(&orientation);
+
+    switch (orientation) {
+        #define CHECKROT(ohenum, sdlenum) case DISPLAY_MANAGER_##ohenum: device_orientation = SDL_ORIENTATION_##sdlenum; break
+        CHECKROT(PORTRAIT, PORTRAIT);
+        CHECKROT(LANDSCAPE, LANDSCAPE);
+        CHECKROT(PORTRAIT_INVERTED, PORTRAIT_FLIPPED);
+        CHECKROT(LANDSCAPE_INVERTED, LANDSCAPE_FLIPPED);
+        CHECKROT(UNKNOWN, UNKNOWN);
+        #undef CHECKROT
+    }
+}
+
+// Called when phone/tablet rotates to a new orientation.
+static void OnDisplayChangeCallback(uint64_t displayId)
+{
+    uint64_t defdpyid = 0;
+    OH_NativeDisplayManager_GetDefaultDisplayId(&defdpyid);
+    if (displayId != defdpyid) {
+        return;  // we don't care if external displays change orientation.
+    }
+    UpdateDeviceOrientation();
+}
+
+
+// Called when windowStage.loadContent finishes.
+static napi_value SDL_JS_LoadContentResult(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY(1);
+    if (GetNapiInt(env, GetNapiObjField(env, argv[0], "code"), -1) != 0) {
+        OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed to load the content page! Aborting!");
+        exit(1);
+    }
+    //OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Succeeded in loading the content page. Startup may now continue.");
+
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onDestroy().
+static napi_value SDL_JS_UIAbility_OnDestroy(napi_env env, napi_callback_info info)
+{
+    SDL_FlushEvents(SDL_EVENT_FIRST, SDL_EVENT_LAST);
+    SDL_SendQuit();
+    SDL_OnApplicationWillTerminate();
+
+    if (commonevent_subscriber) {
+        OH_CommonEvent_UnSubscribe(commonevent_subscriber);
+        OH_CommonEvent_DestroySubscriber(commonevent_subscriber);
+        commonevent_subscriber = NULL;
+    }
+
+    SDL_free(system_locale);
+    system_locale = NULL;
+
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onForeground().
+// This is for older devices; on newer ones we use OnWillForeground and OnDidForeground instead.
+static napi_value SDL_JS_UIAbility_OnForeground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationWillEnterForeground();
+    SDL_OnApplicationDidEnterForeground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onWillForeground().
+// This is for newer devices; on older ones we use OnForeground instead.
+static napi_value SDL_JS_UIAbility_OnWillForeground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationWillEnterForeground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onDidForeground().
+// This is for newer devices; on older ones we use OnForeground instead.
+static napi_value SDL_JS_UIAbility_OnDidForeground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationDidEnterForeground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onBackground().
+// This is for older devices; on newer ones we use OnWillBackground and OnDidBackground instead.
+static napi_value SDL_JS_UIAbility_OnBackground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationWillEnterBackground();
+    SDL_OnApplicationDidEnterBackground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onWillBackground().
+// This is for newer devices; on older ones we use OnBackground instead.
+static napi_value SDL_JS_UIAbility_OnWillBackground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationWillEnterBackground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onDidBackground().
+// This is for newer devices; on older ones we use OnBackground instead.
+static napi_value SDL_JS_UIAbility_OnDidBackground(napi_env env, napi_callback_info info)
+{
+    SDL_OnApplicationDidEnterBackground();
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onWindowStageCreate().
+static napi_value SDL_JS_UIAbility_OnWindowStageCreate(napi_env env, napi_callback_info info)
+{
+    // grab the window object, load "pages/Index" to continue startup.
+    //OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "%{public}s", SDL_FUNCTION);
+    SDL_JS_ENTRY(1);
+    napi_value windowStage = argv[0];
+    napi_value window = CallNapiMethod(env, windowStage, "getMainWindowSync", 0, NULL);
+    napi_create_reference(env, window, 1, &window_ref);
+
+    napi_value args[] = { CreateNapiString(env, "pages/Index"), CreateNapiFunction(env, NULL, SDL_JS_LoadContentResult, NULL) };
+    CallNapiMethod(env, windowStage, "loadContent", SDL_arraysize(args), args);
+
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onWindowStageDestroy().
+static napi_value SDL_JS_UIAbility_OnWindowStageDestroy(napi_env env, napi_callback_info info)
+{
+    return GetNapiUndefined(env);
+}
+
+// Our native version of UIAbility.onMemoryLevel().
+static napi_value SDL_JS_UIAbility_OnMemoryLevel(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY(1);
+    if (GetNapiInt(env, argv[0], 0) == 2 /*MEMORY_LEVEL_CRITICAL*/) {
+        SDL_OnApplicationDidReceiveMemoryWarning();
+    }
+    return GetNapiUndefined(env);
+}
+
+static SDL_SystemTheme GetSDLSystemThemeFromAbilityConfig(napi_env env, napi_value config)
+{
+    switch (GetNapiInt(env, GetNapiObjField(env, config, "colorMode"), -1)) {
+        case 0: return SDL_SYSTEM_THEME_DARK;
+        case 1: return SDL_SYSTEM_THEME_LIGHT;
+        default: return SDL_SYSTEM_THEME_UNKNOWN;
+    }
+}
+
+// Our native version of UIAbility.onConfigurationUpdate().
+static napi_value SDL_JS_UIAbility_OnConfigurationUpdate(napi_env env, napi_callback_info info)
+{
+    SDL_JS_ENTRY(1);
+    system_theme = GetSDLSystemThemeFromAbilityConfig(env, argv[0]);
+    SDL_SetSystemTheme(system_theme);
+    return GetNapiUndefined(env);
+}
+
+static void TakeOverUIAbility(napi_env env, napi_value ability)
+{
+    const int apilevel = SDL_GetOpenHarmonySDKVersion();
+    napi_value prototype = GetNapiObjField(env, GetNapiObjField(env, ability, "constructor"), "prototype");
+
+    if (apilevel < 20) {
+        SetNapiObjField(env, prototype, "onForeground", CreateNapiFunction(env, "onForeground", SDL_JS_UIAbility_OnForeground, NULL));
+        SetNapiObjField(env, prototype, "onBackground", CreateNapiFunction(env, "onBackground", SDL_JS_UIAbility_OnBackground, NULL));
+    } else {
+        SetNapiObjField(env, prototype, "onWillForeground", CreateNapiFunction(env, "onWillForeground", SDL_JS_UIAbility_OnWillForeground, NULL));
+        SetNapiObjField(env, prototype, "onDidForeground", CreateNapiFunction(env, "onDidForeground", SDL_JS_UIAbility_OnDidForeground, NULL));
+        SetNapiObjField(env, prototype, "onWillBackground", CreateNapiFunction(env, "onWillBackground", SDL_JS_UIAbility_OnWillBackground, NULL));
+        SetNapiObjField(env, prototype, "onDidBackground", CreateNapiFunction(env, "onDidBackground", SDL_JS_UIAbility_OnDidBackground, NULL));
+    }
+
+    SetNapiObjField(env, prototype, "onDestroy", CreateNapiFunction(env, "onDestroy", SDL_JS_UIAbility_OnDestroy, NULL));
+    SetNapiObjField(env, prototype, "onWindowStageCreate", CreateNapiFunction(env, "onWindowStageCreate", SDL_JS_UIAbility_OnWindowStageCreate, NULL));
+    SetNapiObjField(env, prototype, "onWindowStageDestroy", CreateNapiFunction(env, "onWindowStageDestroy", SDL_JS_UIAbility_OnWindowStageDestroy, NULL));
+    SetNapiObjField(env, prototype, "onMemoryLevel", CreateNapiFunction(env, "onMemoryLevel", SDL_JS_UIAbility_OnMemoryLevel, NULL));
+    SetNapiObjField(env, prototype, "onConfigurationUpdate", CreateNapiFunction(env, "onConfigurationUpdate", SDL_JS_UIAbility_OnConfigurationUpdate, NULL));
+}
+
+// ArkTS calls this once near startup to pass us the Ability, so we can call back into Javascript as necessary.
+static napi_value SDL_JS_ProvideArkTSObjects(napi_env env, napi_callback_info info)
+{
+    SDL_assert(!native_resource_mgr);  // don't call this more than once!
+
+    // we don't bother cleaning up most things in this function, because they are intended to live as long as the process.
+    // !!! FIXME: but for completeness, maybe we should, in SDL_JS_UIAbility_OnDestroy().
+    #define expected_argc 5
+    SDL_JS_ENTRY(expected_argc);
+    if (argc != expected_argc) {
+        // if you hit this, we probably changed either this C code or the ArkTS code in EntryAbility.ets and you need to update one or both to get the back in sync.
+        OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "ProvideArkTSObjects: expected %{public}d objects, but got %{public}d! Script is out of sync? Aborting!", (int) expected_argc, (int) argc);
+        exit(1);
+    }
+    #undef expected_argc
+
+    napi_value ability = argv[0];
+    napi_value atmanager = argv[1];
+    napi_value i18nsystem = argv[2];
+    napi_value pointer = argv[3];
+    napi_value ime_controller = argv[4];
+    napi_create_reference(env, ability, 1, &ability_object_ref);
+    napi_create_reference(env, atmanager, 1, &atmanager_ref);
+    napi_create_reference(env, i18nsystem, 1, &i18nsystem_ref);
+    napi_create_reference(env, pointer, 1, &pointer_ref);
+    napi_create_reference(env, ime_controller, 1, &ime_controller_ref);
+
+    TakeOverUIAbility(env, ability);
+
+    napi_value context = GetNapiObjField(env, ability, "context");
+    napi_value resourceManager = GetNapiObjField(env, context, "resourceManager");
+    native_resource_mgr = OH_ResourceManager_InitNativeResourceManager(env, resourceManager);
+    napi_value config = GetNapiObjField(env, context, "config");
+
+    system_theme = GetSDLSystemThemeFromAbilityConfig(env, config);
+
+    napi_value on_insert_text = CreateNapiFunction(env, "onInsertText", SDL_JS_IME_Controller_OnInsertText, NULL);
+    napi_create_reference(env, on_insert_text, 1, &on_insert_text_ref);
+    napi_value on_delete_left = CreateNapiFunction(env, "onDeleteLeft", SDL_JS_IME_Controller_OnDeleteLeft, NULL);
+    napi_create_reference(env, on_delete_left, 1, &on_delete_left_ref);
+
+    UpdateSystemLocale(env);  // do this at startup, so we have it saved off while we know we're on the Javascript thread.
+
+    // the Video subsystem will also register one of these for tracking displays, but this is so we can track device orientation changes independently.
+    uint32_t display_change_listener_idx = 0;
+    OH_NativeDisplayManager_RegisterDisplayChangeListener(OnDisplayChangeCallback, &display_change_listener_idx);
+    UpdateDeviceOrientation();
+
+    // Set up some threadsafe functions, for calling back into ArkTS from the main thread, regardless of what thread native code is operating from.
+    syslocalechanged_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_SystemLocaleChanged", CallJSSystemLocaleChanged);
+    req_permissions_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_RequestOpenHarmonyPermission", CallJSRequestPermissions);
+    open_url_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyOpenURL", CallJSOpenURL);
+    change_sysbars_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeSystemBars", CallJSChangeSysBars);
+    change_screensaver_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeScreenSaver", CallJSChangeScreenSaver);
+    show_screenkeyboard_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyShowScreenKeyboard", CallJSShowScreenKeyboard);
+    hide_screenkeyboard_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyHideScreenKeyboard", CallJSHideScreenKeyboard);
+    change_mouseptr_threadsafefn = CreateNapiThreadsafeFunction(env, "SDL_OpenHarmonyChangeMousePointer", CallJSChangeMousePointer);
+
+    return GetNapiUndefined(env);
+}
+
+
+// This is called by SDL_RegisterNativeInterfaces when the library is loaded, which sets up the entry points where
+//  ArkTS code can call into our native code.
+static napi_value SDL_Init_Native_Interfaces(napi_env env, napi_value exports)
+{
+    // Functions that we want to be able to call from ArkTS go here.
+    // (declare them in C as `napi_value MyFunctionName(napi_env env, napi_callback_info info);`)
+    napi_property_descriptor desc[] = {
+        { "provideArkTSObjects", NULL, SDL_JS_ProvideArkTSObjects, NULL, NULL, NULL, napi_default, NULL },
+    };
+    napi_define_properties(env, exports, SDL_arraysize(desc), desc);
+
+    // Wire into our XComponent, so we can take control from C code. If any of this fails, I assume the app will either blow up or do nothing.
+    napi_value xcompobj = GetNapiObjField(env, exports, OH_NATIVE_XCOMPONENT_OBJ);
+    if (!xcompobj) {
+        return exports;
+    }
+
+    OH_NativeXComponent *nativeXComponent = NULL;
+    if (napi_unwrap(env, xcompobj, (void **) &nativeXComponent) != napi_ok) {
+        return exports;
+    }
+
+    static OH_NativeXComponent_Callback xcomp_callbacks = {  // this MUST be static! It keeps a pointer to this, and doesn't make a copy, afaict!
+        .OnSurfaceCreated = SDL_XComponent_OnSurfaceCreatedCallback,
+        .OnSurfaceChanged = SDL_XComponent_OnSurfaceChangedCallback,
+        .OnSurfaceDestroyed = SDL_XComponent_OnSurfaceDestroyedCallback,
+        .DispatchTouchEvent = SDL_XComponent_DispatchTouchEventCallback
+    };
+    OH_NativeXComponent_RegisterCallback(nativeXComponent, &xcomp_callbacks);
+    OH_NativeXComponent_RegisterOnFrameCallback(nativeXComponent, SDL_XComponent_OnFrameCallback);
+
+    static OH_NativeXComponent_MouseEvent_Callback xcomp_mouse_callbacks = {
+        .DispatchMouseEvent = SDL_XComponent_DispatchMouseEventCallback,
+        .DispatchHoverEvent = SDL_XComponent_DispatchHoverEventCallback
+    };
+    OH_NativeXComponent_RegisterMouseEventCallback(nativeXComponent, &xcomp_mouse_callbacks);
+
+    // only AXIS events supported here, at the moment, apparently, but most of the other things (mouse, touch, key) come through other supported callbacks.
+    // "Axis" in this case only means mousewheel, afaict.
+    OH_NativeXComponent_RegisterUIInputEventCallback(nativeXComponent, SDL_XComponent_DispatchUIInputEventCallback, ARKUI_UIINPUTEVENT_TYPE_AXIS);
+
+    OH_NativeXComponent_RegisterKeyEventCallback(nativeXComponent, SDL_XComponent_DispatchKeyEventCallback);
+
+    static const char * const common_events[] = { COMMON_EVENT_LOCALE_CHANGED };
+    CommonEvent_SubscribeInfo *subinfo = OH_CommonEvent_CreateSubscribeInfo((const char **) common_events, SDL_arraysize(common_events));
+    if (subinfo) {
+        commonevent_subscriber = OH_CommonEvent_CreateSubscriber(subinfo, OpenHarmonyCommonEventReceiver);
+        if (commonevent_subscriber) {
+            OH_CommonEvent_Subscribe(commonevent_subscriber);
+        }
+        OH_CommonEvent_DestroySubscribeInfo(subinfo);
+    }
+
+    return exports;
+}
+
+// This runs when libSDL3.so loads, and registers the NAPI module, so ArkTS can call into this to get going.
+void __attribute__((constructor)) SDL_RegisterNativeInterfaces(void)
+{
+    static napi_module sdl_napi_module = {
+        .nm_version = 1,
+        .nm_flags = 0,
+        .nm_filename = NULL,
+        .nm_register_func = SDL_Init_Native_Interfaces,
+        .nm_modname = "SDL3",
+        .nm_priv = ((void*)0),
+        .reserved = { 0 },
+    };
+    napi_module_register(&sdl_napi_module);
+}
+
+#endif // SDL_PLATFORM_OPENHARMONY
+

+ 66 - 0
src/core/openharmony/SDL_openharmony.h

@@ -0,0 +1,66 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifndef SDL_openharmony_h
+#define SDL_openharmony_h
+
+// Set up for C function definitions, even when using C++
+#ifdef __cplusplus
+/* *INDENT-OFF* */
+extern "C" {
+/* *INDENT-ON* */
+#endif
+
+void SDL_DebugLogOpenHarmonyInfo(void);
+const char *SDL_GetOpenHarmonySystemLocale(void);
+SDL_SystemTheme SDL_GetOpenHarmonySystemTheme(void);
+SDL_DisplayOrientation SDL_GetOpenHarmonyDeviceCurrentOrientation(void);
+SDL_FormFactor SDL_GetOpenHarmonyDeviceFormFactor(void);
+
+bool SDL_OpenHarmonyOpenURL(const char *url);
+bool SDL_OpenHarmonyToggleSystemBars(bool status_bar, bool navigation_bar);
+bool SDL_OpenHarmonyChangeScreenSaver(bool enable);
+bool SDL_OpenHarmonyShowScreenKeyboard(int input_type, int cap_type, const SDL_Rect *text_input_rect);
+bool SDL_OpenHarmonyHideScreenKeyboard(void);
+bool SDL_OpenHarmonyShowMousePointer(void);
+bool SDL_OpenHarmonyHideMousePointer(void);
+
+bool SDL_OpenHarmonyRawFileOpen(void **puserdata, const char *fileName, const char *mode);
+Sint64 SDL_OpenHarmonyRawFileSize(void *userdata);
+Sint64 SDL_OpenHarmonyRawFileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence);
+size_t SDL_OpenHarmonyRawFileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status);
+bool SDL_OpenHarmonyRawFileClose(void *userdata);
+bool SDL_OpenHarmonyEnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata);
+bool SDL_OpenHarmonyGetAssetPathInfo(const char *path, SDL_PathInfo *info);
+
+#define SDL_PlatformEnumerateAssetDirectory SDL_OpenHarmonyEnumerateAssetDirectory
+#define SDL_GetPlatformInternalStoragePath SDL_GetOpenHarmonyInternalStoragePath
+#define SDL_PlatformGetAssetPathInfo SDL_OpenHarmonyGetAssetPathInfo
+
+// Ends C function definitions when using C++
+#ifdef __cplusplus
+/* *INDENT-OFF* */
+}
+/* *INDENT-ON* */
+#endif
+
+#endif // SDL_openharmony_h

+ 1 - 1
src/cpuinfo/SDL_cpuinfo.c

@@ -493,7 +493,7 @@ static int CPU_haveNEON(void)
         return 0;
         return 0;
     }
     }
     return (hasneon & HWCAP_NEON) == HWCAP_NEON;
     return (hasneon & HWCAP_NEON) == HWCAP_NEON;
-#elif (defined(SDL_PLATFORM_LINUX) && defined(HAVE_GETAUXVAL)) || defined(SDL_PLATFORM_ANDROID)
+#elif (defined(SDL_PLATFORM_LINUX) && defined(HAVE_GETAUXVAL)) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     return (getauxval(AT_HWCAP) & HWCAP_NEON) == HWCAP_NEON;
     return (getauxval(AT_HWCAP) & HWCAP_NEON) == HWCAP_NEON;
 #elif defined(SDL_PLATFORM_LINUX)
 #elif defined(SDL_PLATFORM_LINUX)
     return readProcAuxvForNeon();
     return readProcAuxvForNeon();

+ 3 - 0
src/dynapi/SDL_dynapi.exports

@@ -1311,3 +1311,6 @@ _SDL_SetRenderViewportFloat
 _SDL_GetRenderViewportFloat
 _SDL_GetRenderViewportFloat
 _SDL_SetRenderClipRectFloat
 _SDL_SetRenderClipRectFloat
 _SDL_GetRenderClipRectFloat
 _SDL_GetRenderClipRectFloat
+_SDL_GetOpenHarmonySDKVersion
+_SDL_GetOpenHarmonyInternalStoragePath
+_SDL_RequestOpenHarmonyPermission

+ 2 - 0
src/dynapi/SDL_dynapi.h

@@ -49,6 +49,8 @@
 #define SDL_DYNAMIC_API 0
 #define SDL_DYNAMIC_API 0
 #elif defined(SDL_PLATFORM_ANDROID) // probably not useful on Android.
 #elif defined(SDL_PLATFORM_ANDROID) // probably not useful on Android.
 #define SDL_DYNAMIC_API 0
 #define SDL_DYNAMIC_API 0
+#elif defined(SDL_PLATFORM_OPENHARMONY) // probably not useful on OpenHarmony.
+#define SDL_DYNAMIC_API 0
 #elif defined(SDL_PLATFORM_EMSCRIPTEN) // probably not useful on Emscripten.
 #elif defined(SDL_PLATFORM_EMSCRIPTEN) // probably not useful on Emscripten.
 #define SDL_DYNAMIC_API 0
 #define SDL_DYNAMIC_API 0
 #elif defined(SDL_PLATFORM_PS2) && SDL_PLATFORM_PS2
 #elif defined(SDL_PLATFORM_PS2) && SDL_PLATFORM_PS2

+ 3 - 0
src/dynapi/SDL_dynapi.sym

@@ -1312,6 +1312,9 @@ SDL3_0.0.0 {
     SDL_GetRenderViewportFloat;
     SDL_GetRenderViewportFloat;
     SDL_SetRenderClipRectFloat;
     SDL_SetRenderClipRectFloat;
     SDL_GetRenderClipRectFloat;
     SDL_GetRenderClipRectFloat;
+    SDL_GetOpenHarmonySDKVersion;
+    SDL_GetOpenHarmonyInternalStoragePath;
+    SDL_RequestOpenHarmonyPermission;
     # extra symbols go here (don't modify this line)
     # extra symbols go here (don't modify this line)
   local: *;
   local: *;
 };
 };

+ 3 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -1338,3 +1338,6 @@
 #define SDL_GetRenderViewportFloat SDL_GetRenderViewportFloat_REAL
 #define SDL_GetRenderViewportFloat SDL_GetRenderViewportFloat_REAL
 #define SDL_SetRenderClipRectFloat SDL_SetRenderClipRectFloat_REAL
 #define SDL_SetRenderClipRectFloat SDL_SetRenderClipRectFloat_REAL
 #define SDL_GetRenderClipRectFloat SDL_GetRenderClipRectFloat_REAL
 #define SDL_GetRenderClipRectFloat SDL_GetRenderClipRectFloat_REAL
+#define SDL_GetOpenHarmonySDKVersion SDL_GetOpenHarmonySDKVersion_REAL
+#define SDL_GetOpenHarmonyInternalStoragePath SDL_GetOpenHarmonyInternalStoragePath_REAL
+#define SDL_RequestOpenHarmonyPermission SDL_RequestOpenHarmonyPermission_REAL

+ 3 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -1346,3 +1346,6 @@ SDL_DYNAPI_PROC(bool,SDL_SetRenderViewportFloat,(SDL_Renderer *a,const SDL_FRect
 SDL_DYNAPI_PROC(bool,SDL_GetRenderViewportFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_GetRenderViewportFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_SetRenderClipRectFloat,(SDL_Renderer *a,const SDL_FRect *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_SetRenderClipRectFloat,(SDL_Renderer *a,const SDL_FRect *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_GetRenderClipRectFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return)
 SDL_DYNAPI_PROC(bool,SDL_GetRenderClipRectFloat,(SDL_Renderer *a,SDL_FRect *b),(a,b),return)
+SDL_DYNAPI_PROC(int,SDL_GetOpenHarmonySDKVersion,(void),(),return)
+SDL_DYNAPI_PROC(const char*,SDL_GetOpenHarmonyInternalStoragePath,(void),(),return)
+SDL_DYNAPI_PROC(bool,SDL_RequestOpenHarmonyPermission,(const char *a,SDL_RequestOpenHarmonyPermissionCallback b,void *c),(a,b,c),return)

+ 3 - 3
src/events/SDL_keyboard.c

@@ -333,7 +333,7 @@ SDL_Window *SDL_GetKeyboardFocus(void)
 
 
 bool SDL_SetKeyboardFocus(SDL_Window *window)
 bool SDL_SetKeyboardFocus(SDL_Window *window)
 {
 {
-#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
+#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY)
     SDL_VideoDevice *video = SDL_GetVideoDevice();
     SDL_VideoDevice *video = SDL_GetVideoDevice();
 #endif
 #endif
     SDL_Keyboard *keyboard = &SDL_keyboard;
     SDL_Keyboard *keyboard = &SDL_keyboard;
@@ -354,7 +354,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window)
     if (keyboard->focus && keyboard->focus != window) {
     if (keyboard->focus && keyboard->focus != window) {
         SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_LOST, 0, 0);
         SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_LOST, 0, 0);
 
 
-#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
+#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY)
         // Ensures IME compositions are committed
         // Ensures IME compositions are committed
         if (SDL_TextInputActive(keyboard->focus)) {
         if (SDL_TextInputActive(keyboard->focus)) {
             if (video && video->StopTextInput) {
             if (video && video->StopTextInput) {
@@ -384,7 +384,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window)
     if (keyboard->focus) {
     if (keyboard->focus) {
         SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_GAINED, 0, 0);
         SDL_SendWindowEvent(keyboard->focus, SDL_EVENT_WINDOW_FOCUS_GAINED, 0, 0);
 
 
-#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID)
+#if !defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_ANDROID) && !defined(SDL_PLATFORM_OPENHARMONY)
         if (SDL_TextInputActive(keyboard->focus)) {
         if (SDL_TextInputActive(keyboard->focus)) {
             if (video && video->StartTextInput) {
             if (video && video->StartTextInput) {
                 video->StartTextInput(video, keyboard->focus, keyboard->focus->text_input_props);
                 video->StartTextInput(video, keyboard->focus, keyboard->focus->text_input_props);

+ 1 - 1
src/filesystem/SDL_filesystem.c

@@ -375,7 +375,7 @@ char **SDL_InternalGlobDirectory(const char *path, const char *pattern, SDL_Glob
     size_t pathlen = SDL_strlen(path);
     size_t pathlen = SDL_strlen(path);
 
 
     // if path ends with any slash, chop them off, so we don't confuse the pattern matcher later.
     // if path ends with any slash, chop them off, so we don't confuse the pattern matcher later.
-    #ifdef SDL_PLATFORM_ANDROID
+    #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     if (SDL_strcmp(path, "assets://") == 0) {  // don't chop '//' off this if we're looking for the root of the asset tree.
     if (SDL_strcmp(path, "assets://") == 0) {  // don't chop '//' off this if we're looking for the root of the asset tree.
         pathlen--;  // we'll add a 1 again later.
         pathlen--;  // we'll add a 1 again later.
     } else
     } else

+ 78 - 0
src/filesystem/openharmony/SDL_sysfilesystem.c

@@ -0,0 +1,78 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_FILESYSTEM_OPENHARMONY
+
+
+// !!! FIXME: HACK to prevent <AbilityKit/ability_runtime/start_options.h> from including. It has '&' instead of '*' for some args, which suggests it's only been tested with C++.
+#define ABILITY_RUNTIME_START_OPTIONS_H
+typedef enum AbilityRuntime_StartOptions AbilityRuntime_StartOptions;
+
+
+#include <sys/stat.h>
+#include <AbilityKit/ability_runtime/application_context.h>
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+// System dependent filesystem routines
+
+#include "../SDL_sysfilesystem.h"
+
+char *SDL_SYS_GetBasePath(void)
+{
+    return SDL_strdup("assets://");
+}
+
+char *SDL_SYS_GetExeName(void)
+{
+    char buffer[128];
+    int32_t writelen = 0;
+    const AbilityRuntime_ErrorCode rc = OH_AbilityRuntime_ApplicationContextGetBundleName(buffer, (int32_t) sizeof (buffer), &writelen);
+    if (rc != ABILITY_RUNTIME_ERROR_CODE_NO_ERROR) {
+        SDL_SetError("OH_AbilityRuntime_ApplicationContextGetBundleName failed: %d", (int) rc);
+        return NULL;
+    }
+    return SDL_strdup(buffer);
+}
+
+char *SDL_SYS_GetPrefPath(const char *org, const char *app)
+{
+    const char *prefdir = SDL_GetOpenHarmonyInternalStoragePath();
+    char *retval = NULL;
+    if (prefdir) {
+        if (SDL_asprintf(&retval, "%s/%s/", prefdir, app) < 0) {
+            retval = NULL;
+        }
+    }
+
+    mkdir(prefdir, 0755);
+    mkdir(retval, 0755);
+
+    return retval;
+}
+
+char *SDL_SYS_GetUserFolder(SDL_Folder folder)
+{
+    SDL_Unsupported();  // !!! FIXME: Can we support _any_ of this?
+    return NULL;
+}
+
+#endif // SDL_FILESYSTEM_OPENHARMONY

+ 20 - 16
src/filesystem/posix/SDL_sysfsops.c

@@ -39,24 +39,28 @@
 #include "../../core/android/SDL_android.h"
 #include "../../core/android/SDL_android.h"
 #endif
 #endif
 
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include "../../core/openharmony/SDL_openharmony.h"
+#endif
+
 
 
 bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata)
 bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata)
 {
 {
     char *apath = NULL;  // absolute path (for Android, iOS, etc). Overrides `path`.
     char *apath = NULL;  // absolute path (for Android, iOS, etc). Overrides `path`.
 
 
-#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_IOS)
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_OPENHARMONY)
     if (*path == '\0') {
     if (*path == '\0') {
         return SDL_SetError("No such file or directory");
         return SDL_SetError("No such file or directory");
     } else if (*path != '/') {
     } else if (*path != '/') {
-        #ifdef SDL_PLATFORM_ANDROID
+        #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
         if (SDL_strncmp(path, "assets://", 9) == 0) {
         if (SDL_strncmp(path, "assets://", 9) == 0) {
             char *pathwithsep = NULL;
             char *pathwithsep = NULL;
             SDL_asprintf(&pathwithsep, "%s%s", path, (path[SDL_strlen(path) - 1] != '/') ? "/" : "");
             SDL_asprintf(&pathwithsep, "%s%s", path, (path[SDL_strlen(path) - 1] != '/') ? "/" : "");
-            const bool retval = pathwithsep ? Android_JNI_EnumerateAssetDirectory(pathwithsep, cb, userdata) : false;
+            const bool retval = pathwithsep ? SDL_PlatformEnumerateAssetDirectory(pathwithsep, cb, userdata) : false;
             SDL_free(pathwithsep);
             SDL_free(pathwithsep);
             return retval;
             return retval;
         }
         }
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         #elif defined(SDL_PLATFORM_IOS)
         #elif defined(SDL_PLATFORM_IOS)
         char *base = SDL_GetPrefPath("", "");
         char *base = SDL_GetPrefPath("", "");
         if (!base) {
         if (!base) {
@@ -71,7 +75,7 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback
             return false;
             return false;
         }
         }
     }
     }
-#elif 0  // this is just for testing that `apath` works when you aren't on iOS or Android.
+#elif 0  // this is just for testing that `apath` works when you aren't on iOS or Android or HarmonyOS.
     if (*path != '/') {
     if (*path != '/') {
         char *c = SDL_SYS_GetCurrentDirectory();
         char *c = SDL_SYS_GetCurrentDirectory();
         SDL_asprintf(&apath, "%s%s", c, path);
         SDL_asprintf(&apath, "%s%s", c, path);
@@ -98,9 +102,9 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback
 
 
     DIR *dir = opendir(pathwithsep);
     DIR *dir = opendir(pathwithsep);
     if (!dir) {
     if (!dir) {
-#ifdef SDL_PLATFORM_ANDROID  // Maybe it's an asset... that didn't use an "assets://" URL?
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) // Maybe it's an asset... that didn't use an "assets://" URL?
         if (*pathwithsep != '/') {  // don't fall back to asset tree for absolute paths, in case opendir() failed for other reasons, like opendir("/") returning EACCES.
         if (*pathwithsep != '/') {  // don't fall back to asset tree for absolute paths, in case opendir() failed for other reasons, like opendir("/") returning EACCES.
-            const bool retval = Android_JNI_EnumerateAssetDirectory(pathwithsep + extralen, cb, userdata);
+            const bool retval = SDL_PlatformEnumerateAssetDirectory(pathwithsep + extralen, cb, userdata);
             SDL_free(pathwithsep);
             SDL_free(pathwithsep);
             return retval;
             return retval;
         }
         }
@@ -130,12 +134,12 @@ bool SDL_SYS_RemovePath(const char *path)
 {
 {
     int rc;
     int rc;
 
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     if (*path == '/') {
     if (*path == '/') {
         rc = remove(path);
         rc = remove(path);
     } else {
     } else {
         char *apath = NULL;
         char *apath = NULL;
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         if (!apath) {
         if (!apath) {
             return false;
             return false;
         }
         }
@@ -177,18 +181,18 @@ bool SDL_SYS_RenamePath(const char *oldpath, const char *newpath)
 {
 {
     int rc;
     int rc;
 
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     char *aoldpath = NULL;
     char *aoldpath = NULL;
     char *anewpath = NULL;
     char *anewpath = NULL;
     if (*oldpath != '/') {
     if (*oldpath != '/') {
-        SDL_asprintf(&aoldpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), oldpath);
+        SDL_asprintf(&aoldpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), oldpath);
         if (!aoldpath) {
         if (!aoldpath) {
             return false;
             return false;
         }
         }
         oldpath = aoldpath;
         oldpath = aoldpath;
     }
     }
     if (*newpath != '/') {
     if (*newpath != '/') {
-        SDL_asprintf(&anewpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), newpath);
+        SDL_asprintf(&anewpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), newpath);
         if (!anewpath) {
         if (!anewpath) {
             SDL_free(aoldpath);
             SDL_free(aoldpath);
             return false;
             return false;
@@ -349,16 +353,16 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
     struct stat statbuf;
     struct stat statbuf;
     int rc;
     int rc;
 
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     if (*path == '\0') {
     if (*path == '\0') {
         return SDL_SetError("No such file or directory");
         return SDL_SetError("No such file or directory");
     } else if (*path == '/') {
     } else if (*path == '/') {
         rc = stat(path, &statbuf);
         rc = stat(path, &statbuf);
     } else if (SDL_strncmp(path, "assets://", 9) == 0) {
     } else if (SDL_strncmp(path, "assets://", 9) == 0) {
-        return Android_JNI_GetAssetPathInfo(path, info);
+        return SDL_PlatformGetAssetPathInfo(path, info);
     } else {
     } else {
         char *apath = NULL;
         char *apath = NULL;
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         if (!apath) {
         if (!apath) {
             return false;
             return false;
         }
         }
@@ -366,7 +370,7 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
         SDL_free(apath);
         SDL_free(apath);
     }
     }
     if (rc < 0) {  // Maybe it's an asset... that didn't use an "assets://" URL?
     if (rc < 0) {  // Maybe it's an asset... that didn't use an "assets://" URL?
-        return Android_JNI_GetAssetPathInfo(path, info);
+        return SDL_PlatformGetAssetPathInfo(path, info);
     }
     }
 #elif defined(SDL_PLATFORM_IOS)
 #elif defined(SDL_PLATFORM_IOS)
     if (*path == '/') {
     if (*path == '/') {

+ 36 - 2
src/io/SDL_iostream.c

@@ -69,6 +69,11 @@ struct SDL_IOStream
 #include "../core/android/SDL_android.h"
 #include "../core/android/SDL_android.h"
 #endif
 #endif
 
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include <unistd.h>
+#include "../core/openharmony/SDL_openharmony.h"
+#endif
+
 #if defined(SDL_PLATFORM_WINDOWS) && !defined(SDL_PLATFORM_CYGWIN)
 #if defined(SDL_PLATFORM_WINDOWS) && !defined(SDL_PLATFORM_CYGWIN)
 
 
 typedef struct IOStreamWindowsData
 typedef struct IOStreamWindowsData
@@ -1024,7 +1029,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
         return NULL;
         return NULL;
     }
     }
 
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
 #ifdef HAVE_STDIO_H
 #ifdef HAVE_STDIO_H
     // Try to open the file on the filesystem first
     // Try to open the file on the filesystem first
     if (*file == '/') {
     if (*file == '/') {
@@ -1037,6 +1042,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
             }
             }
             return SDL_IOFromFP(fp, true);
             return SDL_IOFromFP(fp, true);
         }
         }
+    #ifdef SDL_PLATFORM_ANDROID
     } else if (SDL_strncmp(file, "content://", 10) == 0) {
     } else if (SDL_strncmp(file, "content://", 10) == 0) {
         // Try opening content:// URI
         // Try opening content:// URI
         int fd = Android_JNI_OpenFileDescriptor(file, mode);
         int fd = Android_JNI_OpenFileDescriptor(file, mode);
@@ -1053,10 +1059,11 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
         }
         }
 
 
         return SDL_IOFromFP(fp, true);
         return SDL_IOFromFP(fp, true);
+    #endif
     } else if (SDL_strncmp(file, "assets://", 9) != 0) {
     } else if (SDL_strncmp(file, "assets://", 9) != 0) {
         // Try opening it from internal storage if it's a relative path
         // Try opening it from internal storage if it's a relative path
         char *path = NULL;
         char *path = NULL;
-        SDL_asprintf(&path, "%s/%s", SDL_GetAndroidInternalStoragePath(), file);
+        SDL_asprintf(&path, "%s/%s", SDL_GetPlatformInternalStoragePath(), file);
         if (path) {
         if (path) {
             FILE *fp = fopen(path, mode);
             FILE *fp = fopen(path, mode);
             SDL_free(path);
             SDL_free(path);
@@ -1072,6 +1079,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
     }
     }
 #endif // HAVE_STDIO_H
 #endif // HAVE_STDIO_H
 
 
+    #ifdef SDL_PLATFORM_ANDROID
     // Try to open the file from the asset system?
     // Try to open the file from the asset system?
     void *iodata = NULL;
     void *iodata = NULL;
     if (!Android_JNI_FileOpen(&iodata, file, mode)) {
     if (!Android_JNI_FileOpen(&iodata, file, mode)) {
@@ -1093,6 +1101,32 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
         iostr->setioprops = android_setioprops;
         iostr->setioprops = android_setioprops;
     }
     }
 
 
+    #elif defined(SDL_PLATFORM_OPENHARMONY)
+    // Try to open the file from the asset system?
+    void *iodata = NULL;
+    if (!SDL_OpenHarmonyRawFileOpen(&iodata, file, mode)) {
+        return NULL;
+    }
+
+    SDL_IOStreamInterface iface;
+    SDL_INIT_INTERFACE(&iface);
+    iface.size = SDL_OpenHarmonyRawFileSize;
+    iface.seek = SDL_OpenHarmonyRawFileSeek;
+    iface.read = SDL_OpenHarmonyRawFileRead;
+    iface.write = NULL;  // no write access via RawFile.
+    iface.close = SDL_OpenHarmonyRawFileClose;
+
+    iostr = SDL_OpenIO(&iface, iodata);
+    if (!iostr) {
+        iface.close(iodata);
+    } else {
+        const SDL_PropertiesID props = SDL_GetIOProperties(iostr);
+        if (props) {
+            SDL_SetPointerProperty(props, SDL_PROP_IOSTREAM_OPENHARMONY_RAWFILE64_POINTER, iodata);
+        }
+    }
+    #endif
+
 #elif defined(SDL_PLATFORM_IOS)
 #elif defined(SDL_PLATFORM_IOS)
 
 
     // Try to open the file on the filesystem first
     // Try to open the file on the filesystem first

+ 36 - 0
src/locale/openharmony/SDL_syslocale.c

@@ -0,0 +1,36 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+#include "../SDL_syslocale.h"
+
+#include "../../core/openharmony/SDL_openharmony.h"
+
+bool SDL_SYS_GetPreferredLocales(char *buf, size_t buflen)
+{
+    const char *syslocale = SDL_GetOpenHarmonySystemLocale();
+    if (!syslocale) {
+        return SDL_SetError("Current locale couldn't be determined");
+    }
+    SDL_strlcpy(buf, syslocale, buflen);
+    return true;
+}
+

+ 0 - 1
src/locale/psp/SDL_syslocale.c

@@ -75,4 +75,3 @@ bool SDL_SYS_GetPreferredLocales(char *buf, size_t buflen)
     return true;
     return true;
 }
 }
 
 
-/* vi: set ts=4 sw=4 expandtab: */

+ 2 - 7
src/main/SDL_main_callbacks.c

@@ -88,10 +88,7 @@ static bool SDLCALL SDL_MainCallbackEventWatcher(void *userdata, SDL_Event *even
 
 
 bool SDL_HasMainCallbacks(void)
 bool SDL_HasMainCallbacks(void)
 {
 {
-    if (SDL_main_iteration_callback) {
-        return true;
-    }
-    return false;
+    return (SDL_main_iteration_callback != NULL);
 }
 }
 
 
 SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit)
 SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit)
@@ -107,9 +104,7 @@ SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func app
         if (!SDL_InitSubSystem(SDL_INIT_EVENTS)) {
         if (!SDL_InitSubSystem(SDL_INIT_EVENTS)) {
             SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE);
             SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE);
             return SDL_APP_FAILURE;
             return SDL_APP_FAILURE;
-        }
-
-        if (!SDL_AddEventWatch(SDL_MainCallbackEventWatcher, NULL)) {
+        } else if (!SDL_AddEventWatch(SDL_MainCallbackEventWatcher, NULL)) {
             SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE);
             SDL_SetAtomicInt(&apprc, SDL_APP_FAILURE);
             return SDL_APP_FAILURE;
             return SDL_APP_FAILURE;
         }
         }

+ 1 - 0
src/main/generic/SDL_sysmain_callbacks.c

@@ -23,6 +23,7 @@
 #include "../SDL_main_callbacks.h"
 #include "../SDL_main_callbacks.h"
 #include "../../video/SDL_sysvideo.h"
 #include "../../video/SDL_sysvideo.h"
 
 
+// !!! FIXME: does this ifndef need to be here?
 #ifndef SDL_PLATFORM_IOS
 #ifndef SDL_PLATFORM_IOS
 
 
 static Uint64 callback_rate_increment = 0;
 static Uint64 callback_rate_increment = 0;

+ 60 - 0
src/main/openharmony/SDL_sysmain_callbacks.c

@@ -0,0 +1,60 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifdef SDL_PLATFORM_OPENHARMONY
+
+#include "../SDL_main_callbacks.h"
+
+void SDL_OpenHarmonyOnFrameCallback(void)   // src/core/openharmony calls this when our XComponent is ready for a new frame, and we call SDL_AppIterate here
+{
+    if (SDL_HasMainCallbacks()) {  // ignore this if we're using SDL_main().
+        const SDL_AppResult rc = SDL_IterateMainCallbacks(true);
+        if (rc != SDL_APP_CONTINUE) {
+            SDL_Log("SDL_AppIterate said %s! Terminating!", (rc == SDL_APP_FAILURE) ? "SDL_APP_FAILURE" : "SDL_APP_SUCCESS");
+            SDL_QuitMainCallbacks(rc);
+            exit((rc == SDL_APP_FAILURE) ? 1 : 0);
+        }
+    }
+}
+
+// We hook into this deep into the actual OpenHarmony app lifecycle, see notes elsewhere. Once we return from this, we'll get OnFrame callbacks from our
+//  XComponent, which we'll use for SDL_AppIterate, etc.
+int SDL_EnterAppMainCallbacks(int argc, char *argv[], SDL_AppInit_func appinit, SDL_AppIterate_func appiter, SDL_AppEvent_func appevent, SDL_AppQuit_func appquit)
+{
+    const SDL_AppResult rc = SDL_InitMainCallbacks(argc, argv, appinit, appiter, appevent, appquit);
+    if (rc == SDL_APP_CONTINUE) {
+SDL_Log("SDL_AppInit said continue! We are live!");
+        return 0;  // this will fall all the way out of SDL_main (or whatever), where OpenHarmony will keep running the process, passing events through ArkTS, etc...
+    }
+
+SDL_Log("SDL_AppInit said %s! Terminating!", (rc == SDL_APP_FAILURE) ? "SDL_APP_FAILURE" : "SDL_APP_SUCCESS");
+
+    // appinit requested quit, just bounce out now.
+    SDL_QuitMainCallbacks(rc);
+    exit((rc == SDL_APP_FAILURE) ? 1 : 0);
+
+    return 1;  // just in case.
+}
+
+#endif
+

+ 102 - 0
src/main/openharmony/SDL_sysmain_runapp.c

@@ -0,0 +1,102 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifdef SDL_PLATFORM_OPENHARMONY
+
+#include <hilog/log.h>
+#include <dlfcn.h>
+#include <string.h>
+#include <errno.h>
+#include <pthread.h>
+
+#include "../SDL_main_callbacks.h"
+
+static int SDL_main_argc = 0;
+static char **SDL_main_argv = NULL;
+static void *ThreadEntry__SDL_main(void *userdata)
+{
+    SDL_main_func pMain = (SDL_main_func) userdata;
+    pthread_setname_np(pthread_self(), "SDL_main");
+    OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Calling SDL_main!");
+    const int rc = pMain(SDL_main_argc, SDL_main_argv);
+    OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "SDL_main returned %{public}d! Now terminating the process...", rc);
+    exit(rc);
+    return NULL;
+}
+
+// This is passed to SDL_RunApp. It decides whether libmain.so has the main callbacks or SDL_main,
+//  and either calls SDL_EnterAppMainCallbacks or spins a thread and attempts to work with SDL_main().
+// This will log something and attempt to terminate the process if things go wrong.
+static int RunAppOpenHarmonyMain(int argc, char **argv)
+{
+    // don't call SDL_Log() in here, we aren't necessarily set up yet! Use hilog directly!!
+
+    const char *sofile = "libmain.so";
+    void *lib = dlopen(sofile, RTLD_NOW | RTLD_LOCAL);
+    if (!lib) {
+        OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed loading %{public}s, can't start app! (%{public}s)", sofile, dlerror());
+        exit(1);
+    }
+
+    SDL_main_func pMain = (SDL_main_func) dlsym(lib, "SDL_main");
+    if (pMain) {
+        SDL_main_argc = argc;
+        SDL_main_argv = argv;
+        pthread_t thread;
+        const int rc = pthread_create(&thread, NULL, ThreadEntry__SDL_main, pMain);
+        if (rc != 0) {
+            OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Failed to create thread to run SDL_main from %{public}s, can't start app! (%{public}s)", sofile, strerror(rc));
+            exit(1);
+        }
+
+        pthread_detach(thread);  // we won't be waiting on this.
+        return 0;  // SDL_main is running in another thread, and this thread returns to the app's core loop thing.
+    }
+
+    #define FINDCBFN(nam) \
+        SDL_App##nam##_func pApp##nam = (SDL_App##nam##_func) dlsym(lib, "SDL_App" #nam); \
+        if (!pApp##nam) { \
+            OH_LOG_Print(LOG_APP, LOG_FATAL, LOG_DOMAIN, "SDL/STARTUP", "Couldn't find %{public}s in %{public}s, can't start app! (%{public}s)", "SDL_App" #nam, sofile, dlerror()); \
+            exit(1); \
+        }
+    FINDCBFN(Init);
+    FINDCBFN(Iterate);
+    FINDCBFN(Event);
+    FINDCBFN(Quit);
+    #undef FINDCBFN
+
+    // this returns after SDL_AppInit, and we'll deal with SDL_AppEvent/SDL_AppIterate in SDL_sysmain_callbacks.c, when we get OnFrame events from SDL's XComponent.
+    return SDL_EnterAppMainCallbacks(argc, argv, pAppInit, pAppIterate, pAppEvent, pAppQuit);
+}
+
+void SDL_OpenHarmonyMainSurfaceCreated(void)  // src/core/openharmony calls this when our XComponent is ready to go, and we use this to get the actual native app code booted.
+{
+    static bool booted = false;
+    if (!booted) {  // just in case we make more XComponents later...?
+        booted = true;
+        SDL_RunApp(0, NULL, RunAppOpenHarmonyMain, NULL);
+    }
+}
+
+#endif
+

+ 5 - 0
src/misc/android/SDL_sysurl.c

@@ -20,6 +20,8 @@
 */
 */
 #include "SDL_internal.h"
 #include "SDL_internal.h"
 
 
+#if defined(SDL_PLATFORM_ANDROID)
+
 #include "../SDL_sysurl.h"
 #include "../SDL_sysurl.h"
 #include "../../core/android/SDL_android.h"
 #include "../../core/android/SDL_android.h"
 
 
@@ -27,3 +29,6 @@ bool SDL_SYS_OpenURL(const char *url)
 {
 {
     return Android_JNI_OpenURL(url);
     return Android_JNI_OpenURL(url);
 }
 }
+
+#endif
+

+ 34 - 0
src/misc/openharmony/SDL_sysurl.c

@@ -0,0 +1,34 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#if defined(SDL_PLATFORM_OPENHARMONY)
+
+#include "../SDL_sysurl.h"
+#include "../../core/openharmony/SDL_openharmony.h"
+
+bool SDL_SYS_OpenURL(const char *url)
+{
+    return SDL_OpenHarmonyOpenURL(url);
+}
+
+#endif
+

+ 3 - 0
src/power/SDL_power.c

@@ -62,6 +62,9 @@ static SDL_GetPowerInfo_Impl implementations[] = {
 #ifdef SDL_POWER_ANDROID // handles Android.
 #ifdef SDL_POWER_ANDROID // handles Android.
     SDL_GetPowerInfo_Android,
     SDL_GetPowerInfo_Android,
 #endif
 #endif
+#ifdef SDL_POWER_OPENHARMONY  // handles HarmonyOS/OpenHarmony
+    SDL_GetPowerInfo_OpenHarmony,
+#endif
 #ifdef SDL_POWER_PSP // handles PSP.
 #ifdef SDL_POWER_PSP // handles PSP.
     SDL_GetPowerInfo_PSP,
     SDL_GetPowerInfo_PSP,
 #endif
 #endif

+ 1 - 0
src/power/SDL_syspower.h

@@ -35,6 +35,7 @@ bool SDL_GetPowerInfo_UIKit(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_MacOSX(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_MacOSX(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_Haiku(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_Haiku(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_Android(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_Android(SDL_PowerState *, int *, int *);
+bool SDL_GetPowerInfo_OpenHarmony(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_PSP(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_PSP(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_VITA(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_VITA(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_N3DS(SDL_PowerState *, int *, int *);
 bool SDL_GetPowerInfo_N3DS(SDL_PowerState *, int *, int *);

+ 46 - 0
src/power/openharmony/SDL_syspower.c

@@ -0,0 +1,46 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "SDL_internal.h"
+
+#ifndef SDL_POWER_DISABLED
+#ifdef SDL_POWER_OPENHARMONY
+
+#include <BasicServicesKit/ohbattery_info.h>
+
+bool SDL_GetPowerInfo_OpenHarmony(SDL_PowerState *state, int *seconds, int *percent)
+{
+    *seconds = -1;
+    *percent = (int) OH_BatteryInfo_GetCapacity();
+
+    const BatteryInfo_BatteryPluggedType pluggedtype = OH_BatteryInfo_GetPluggedType();
+    if (pluggedtype == PLUGGED_TYPE_NONE) {
+        *state = SDL_POWERSTATE_ON_BATTERY;
+    } else {
+        *state = (*percent < 100) ? SDL_POWERSTATE_CHARGING : SDL_POWERSTATE_CHARGED;
+    }
+
+    return true;
+}
+
+#endif // SDL_POWER_OPENHARMONY
+#endif // SDL_POWER_DISABLED
+

+ 1 - 1
src/render/SDL_render.c

@@ -40,7 +40,7 @@ SDL_AddEventWatch to catch SDL_EVENT_WILL_ENTER_BACKGROUND events and stopped
 drawing themselves. Other platforms still draw, as the compositor can use it,
 drawing themselves. Other platforms still draw, as the compositor can use it,
 and more importantly: drawing to render targets isn't lost. But I still think
 and more importantly: drawing to render targets isn't lost. But I still think
 this should probably be removed at some point in the future.  --ryan. */
 this should probably be removed at some point in the future.  --ryan. */
-#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) || defined(SDL_PLATFORM_ANDROID)
+#if defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
 #define DONT_DRAW_WHILE_HIDDEN 1
 #define DONT_DRAW_WHILE_HIDDEN 1
 #else
 #else
 #define DONT_DRAW_WHILE_HIDDEN 0
 #define DONT_DRAW_WHILE_HIDDEN 0

+ 10 - 6
src/thread/pthread/SDL_systhread.c

@@ -40,7 +40,7 @@
 #include "../../core/linux/SDL_dbus.h"
 #include "../../core/linux/SDL_dbus.h"
 #endif // SDL_PLATFORM_LINUX
 #endif // SDL_PLATFORM_LINUX
 
 
-#if (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN)
+#if (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY) || defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN)
 #include <dlfcn.h>
 #include <dlfcn.h>
 #ifndef RTLD_DEFAULT
 #ifndef RTLD_DEFAULT
 #define RTLD_DEFAULT NULL
 #define RTLD_DEFAULT NULL
@@ -77,7 +77,7 @@ static void *RunThread(void *data)
 #if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN)
 #if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)) && defined(HAVE_DLOPEN)
 static bool checked_setname = false;
 static bool checked_setname = false;
 static int (*ppthread_setname_np)(const char *) = NULL;
 static int (*ppthread_setname_np)(const char *) = NULL;
-#elif (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN)
+#elif (defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN)
 static bool checked_setname = false;
 static bool checked_setname = false;
 static int (*ppthread_setname_np)(pthread_t, const char *) = NULL;
 static int (*ppthread_setname_np)(pthread_t, const char *) = NULL;
 #endif
 #endif
@@ -88,12 +88,12 @@ bool SDL_SYS_CreateThread(SDL_Thread *thread,
     pthread_attr_t type;
     pthread_attr_t type;
 
 
 // do this here before any threads exist, so there's no race condition.
 // do this here before any threads exist, so there's no race condition.
-#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN)
+#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN)
     if (!checked_setname) {
     if (!checked_setname) {
         void *fn = dlsym(RTLD_DEFAULT, "pthread_setname_np");
         void *fn = dlsym(RTLD_DEFAULT, "pthread_setname_np");
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
         ppthread_setname_np = (int (*)(const char *))fn;
         ppthread_setname_np = (int (*)(const char *))fn;
-#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)
+#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
         ppthread_setname_np = (int (*)(pthread_t, const char *))fn;
         ppthread_setname_np = (int (*)(pthread_t, const char *))fn;
 #endif
 #endif
         checked_setname = true;
         checked_setname = true;
@@ -129,12 +129,12 @@ void SDL_SYS_SetupThread(const char *name)
 #endif
 #endif
 
 
     if (name) {
     if (name) {
-#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)) && defined(HAVE_DLOPEN)
+#if (defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)) && defined(HAVE_DLOPEN)
         SDL_assert(checked_setname);
         SDL_assert(checked_setname);
         if (ppthread_setname_np) {
         if (ppthread_setname_np) {
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
             ppthread_setname_np(name);
             ppthread_setname_np(name);
-#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID)
+#elif defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
             if (ppthread_setname_np(pthread_self(), name) == ERANGE) {
             if (ppthread_setname_np(pthread_self(), name) == ERANGE) {
                 char namebuf[16]; // Limited to 16 char
                 char namebuf[16]; // Limited to 16 char
                 SDL_strlcpy(namebuf, name, sizeof(namebuf));
                 SDL_strlcpy(namebuf, name, sizeof(namebuf));
@@ -171,6 +171,9 @@ void SDL_SYS_SetupThread(const char *name)
     pthread_sigmask(SIG_BLOCK, &mask, NULL);
     pthread_sigmask(SIG_BLOCK, &mask, NULL);
 #endif
 #endif
 
 
+// HarmonyOS defines this, but doesn't offer pthread_setcanceltype.
+// !!! FIXME: but it looks like we shouldn't be using this on any platform!  https://clang.llvm.org/extra/clang-tidy/checks/concurrency/thread-canceltype-asynchronous.html
+#ifndef SDL_PLATFORM_OPENHARMONY
 #ifdef PTHREAD_CANCEL_ASYNCHRONOUS
 #ifdef PTHREAD_CANCEL_ASYNCHRONOUS
     // Allow ourselves to be asynchronously cancelled
     // Allow ourselves to be asynchronously cancelled
     {
     {
@@ -178,6 +181,7 @@ void SDL_SYS_SetupThread(const char *name)
         pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldstate);
         pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldstate);
     }
     }
 #endif
 #endif
+#endif
 }
 }
 
 
 SDL_ThreadID SDL_GetCurrentThreadID(void)
 SDL_ThreadID SDL_GetCurrentThreadID(void)

+ 4 - 0
src/video/SDL_egl.c

@@ -86,6 +86,10 @@
 #define DEFAULT_OGL_ES_PVR "libGLES_CM.so"
 #define DEFAULT_OGL_ES_PVR "libGLES_CM.so"
 #define DEFAULT_OGL_ES     "libGLESv1_CM.so"
 #define DEFAULT_OGL_ES     "libGLESv1_CM.so"
 
 
+#elif defined(SDL_VIDEO_DRIVER_OPENHARMONY)
+#define DEFAULT_EGL        "libEGL.so"
+#define DEFAULT_OGL_ES2    "libGLESv3.so"
+
 #elif defined(SDL_VIDEO_DRIVER_WINDOWS)
 #elif defined(SDL_VIDEO_DRIVER_WINDOWS)
 // EGL AND OpenGL ES support via ANGLE
 // EGL AND OpenGL ES support via ANGLE
 #define DEFAULT_EGL        "libEGL.dll"
 #define DEFAULT_EGL        "libEGL.dll"

+ 1 - 0
src/video/SDL_sysvideo.h

@@ -529,6 +529,7 @@ extern VideoBootStrap WINDOWS_bootstrap;
 extern VideoBootStrap HAIKU_bootstrap;
 extern VideoBootStrap HAIKU_bootstrap;
 extern VideoBootStrap UIKIT_bootstrap;
 extern VideoBootStrap UIKIT_bootstrap;
 extern VideoBootStrap Android_bootstrap;
 extern VideoBootStrap Android_bootstrap;
+extern VideoBootStrap OPENHARMONY_bootstrap;
 extern VideoBootStrap PS2_bootstrap;
 extern VideoBootStrap PS2_bootstrap;
 extern VideoBootStrap PSP_bootstrap;
 extern VideoBootStrap PSP_bootstrap;
 extern VideoBootStrap VITA_bootstrap;
 extern VideoBootStrap VITA_bootstrap;

+ 3 - 0
src/video/SDL_video.c

@@ -110,6 +110,9 @@ static VideoBootStrap *bootstrap[] = {
 #ifdef SDL_VIDEO_DRIVER_ANDROID
 #ifdef SDL_VIDEO_DRIVER_ANDROID
     &Android_bootstrap,
     &Android_bootstrap,
 #endif
 #endif
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+    &OPENHARMONY_bootstrap,
+#endif
 #ifdef SDL_VIDEO_DRIVER_PS2
 #ifdef SDL_VIDEO_DRIVER_PS2
     &PS2_bootstrap,
     &PS2_bootstrap,
 #endif
 #endif

+ 3 - 0
src/video/SDL_vulkan_internal.h

@@ -27,6 +27,9 @@
 #ifdef SDL_VIDEO_DRIVER_ANDROID
 #ifdef SDL_VIDEO_DRIVER_ANDROID
 #define VK_USE_PLATFORM_ANDROID_KHR
 #define VK_USE_PLATFORM_ANDROID_KHR
 #endif
 #endif
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+#define VK_USE_PLATFORM_OHOS
+#endif
 #ifdef SDL_VIDEO_DRIVER_COCOA
 #ifdef SDL_VIDEO_DRIVER_COCOA
 #define VK_USE_PLATFORM_METAL_EXT
 #define VK_USE_PLATFORM_METAL_EXT
 #define VK_USE_PLATFORM_MACOS_MVK
 #define VK_USE_PLATFORM_MACOS_MVK

+ 220 - 0
src/video/openharmony/SDL_openharmonyclipboard.c

@@ -0,0 +1,220 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+
+#include <database/pasteboard/oh_pasteboard.h>
+#include <database/pasteboard/oh_pasteboard_err_code.h>
+#include <database/udmf/udmf.h>
+#include <database/udmf/uds.h>
+
+#include "SDL_openharmonyvideo.h"
+#include "SDL_openharmonyclipboard.h"
+#include "../../core/openharmony/SDL_openharmony.h"
+#include "../../events/SDL_events_c.h"
+
+static void PasteboardNotifyCallback(void *userdata, Pasteboard_NotifyType type)
+{
+    SDL_assert(userdata != NULL);
+    SDL_VideoDevice *_this = (SDL_VideoDevice *) userdata;
+    SDL_VideoData *data = _this->internal;
+
+    if (data->clipboard_set) {
+        data->clipboard_set = false;  // this was us, ignore it.
+        return;
+    }
+
+    OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard;
+    SDL_assert(pasteboard != NULL);
+
+    const char *mimetype = "text/plain";
+    if (OH_Pasteboard_HasType(pasteboard, mimetype)) {
+        const size_t slen = SDL_strlen(mimetype);
+        const size_t allocationsize = (slen + 1) + (sizeof (char *) * 2);
+        char **new_mime_types = SDL_AllocateTemporaryMemory(allocationsize);
+        if (new_mime_types) {
+            char *ptr = (char *)(new_mime_types + 2);
+            SDL_strlcpy(ptr, mimetype, slen + 1);
+            new_mime_types[0] = ptr;
+            new_mime_types[1] = NULL;
+            SDL_SendClipboardUpdate(false, new_mime_types, 1);
+        }
+    }
+}
+
+static void PasteboardFinalizeCallback(void *userdata)
+{
+    // do nothing.
+}
+
+void OPENHARMONY_InitClipboard(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->internal;
+
+    data->clipboard_set = false;
+
+    OH_Pasteboard *pasteboard = OH_Pasteboard_Create();
+    data->oh_pasteboard = pasteboard;
+
+    OH_PasteboardObserver *observer = OH_PasteboardObserver_Create();
+    data->oh_pasteboard_observer = observer;
+    if (observer) {
+        OH_PasteboardObserver_SetData(observer, _this, PasteboardNotifyCallback, PasteboardFinalizeCallback);
+    }
+
+    if (pasteboard && observer) {
+        OH_Pasteboard_Subscribe(pasteboard, NOTIFY_LOCAL_DATA_CHANGE, observer);
+    }
+}
+
+void OPENHARMONY_QuitClipboard(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->internal;
+
+    OH_PasteboardObserver *observer = (OH_PasteboardObserver *) data->oh_pasteboard_observer;
+    OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard;
+
+    if (observer && pasteboard) {
+        OH_Pasteboard_Unsubscribe(pasteboard, NOTIFY_LOCAL_DATA_CHANGE, observer);
+    }
+
+    if (observer) {
+        OH_PasteboardObserver_Destroy(observer);
+        data->oh_pasteboard_observer = NULL;
+    }
+
+    if (pasteboard) {
+        OH_Pasteboard_Destroy(pasteboard);
+        data->oh_pasteboard = NULL;
+    }
+}
+
+const char *const *OPENHARMONY_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types)
+{
+    static const char *const text_mime_types[] = { "text/plain" };
+    *num_mime_types = SDL_arraysize(text_mime_types);
+    return text_mime_types;
+}
+
+bool OPENHARMONY_SetClipboardText(SDL_VideoDevice *_this, const char *text)
+{
+    SDL_VideoData *data = _this->internal;
+    OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard;
+    if (!pasteboard) {
+        return SDL_SetError("Pasteboard not initialized");
+    }
+
+    OH_UdsPlainText *udspt = OH_UdsPlainText_Create();
+    if (!udspt) {
+        return SDL_SetError("Could not create OH_UdsPlainText instance");
+    }
+    OH_UdsPlainText_SetContent(udspt, text);
+
+    OH_UdmfRecord *udmfrec = OH_UdmfRecord_Create();
+    if (!udmfrec) {
+        OH_UdsPlainText_Destroy(udspt);
+        return SDL_SetError("Could not create OH_UdmfRecord instance");
+    }
+    OH_UdmfRecord_AddPlainText(udmfrec, udspt);
+
+    OH_UdmfData *udmfdata = OH_UdmfData_Create();
+    if (!udmfdata) {
+        OH_UdmfRecord_Destroy(udmfrec);
+        OH_UdsPlainText_Destroy(udspt);
+        return SDL_SetError("Could not create OH_UdmfData instance");
+    }
+    OH_UdmfData_AddRecord(udmfdata, udmfrec);
+
+    data->clipboard_set = true;
+    const PASTEBOARD_ErrCode rc = OH_Pasteboard_SetData(pasteboard, udmfdata);
+
+    OH_UdmfData_Destroy(udmfdata);
+    OH_UdmfRecord_Destroy(udmfrec);
+    OH_UdsPlainText_Destroy(udspt);
+
+    if (rc != ERR_OK) {
+        data->clipboard_set = true;
+        return SDL_SetError("OH_Pasteboard_SetData failed: %d", (int) rc);
+    }
+    return true;
+}
+
+char *OPENHARMONY_GetClipboardText(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->internal;
+    OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard;
+    if (!pasteboard) {
+        SDL_SetError("Pasteboard not initialized");
+        return NULL;
+    }
+
+    char *retval = NULL;
+    if (OH_Pasteboard_HasType(pasteboard, "text/plain")) {
+        int rc = (int) ERR_OK;
+        OH_UdmfData *udmfdata = OH_Pasteboard_GetData(pasteboard, &rc);
+        if (!udmfdata) {
+            SDL_SetError("Could not create OH_UdmfData instance %d", rc);
+            return NULL;
+        }
+
+        OH_UdmfRecord *udmfrec = OH_UdmfData_GetRecord(udmfdata, 0); // 0 == first record in udmfdata
+        if (!udmfrec) {
+            OH_UdmfData_Destroy(udmfdata);
+            SDL_SetError("Could not create OH_UdmfRecord instance");
+            return NULL;
+        }
+
+        OH_UdsPlainText *udspt = OH_UdsPlainText_Create();
+        if (!udspt) {
+            OH_UdmfRecord_Destroy(udmfrec);
+            OH_UdmfData_Destroy(udmfdata);
+            SDL_SetError("Could not create OH_UdsPlainText instance");
+            return NULL;
+        }
+
+        OH_UdmfRecord_GetPlainText(udmfrec, udspt);
+        const char *utf8 = OH_UdsPlainText_GetContent(udspt);
+        if (!utf8) {
+            SDL_SetError("Could not get plaintext content");
+        } else {
+            retval = SDL_strdup(utf8);
+        }
+
+        OH_UdsPlainText_Destroy(udspt);
+        OH_UdmfRecord_Destroy(udmfrec);
+        OH_UdmfData_Destroy(udmfdata);
+    }
+
+    return retval;
+}
+
+bool OPENHARMONY_HasClipboardText(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->internal;
+    OH_Pasteboard *pasteboard = (OH_Pasteboard *) data->oh_pasteboard;
+    if (!pasteboard) {
+        return SDL_SetError("Pasteboard not initialized");
+    }
+    return OH_Pasteboard_HasType(pasteboard, "text/plain");
+}
+
+#endif // SDL_VIDEO_DRIVER_OPENHARMONY

+ 34 - 0
src/video/openharmony/SDL_openharmonyclipboard.h

@@ -0,0 +1,34 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifndef SDL_openharmonyclipboard_h_
+#define SDL_openharmonyclipboard_h_
+
+extern void OPENHARMONY_InitClipboard(SDL_VideoDevice *_this);
+extern void OPENHARMONY_QuitClipboard(SDL_VideoDevice *_this);
+
+extern const char *const *OPENHARMONY_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types);
+extern bool OPENHARMONY_SetClipboardText(SDL_VideoDevice *_this, const char *text);
+extern char *OPENHARMONY_GetClipboardText(SDL_VideoDevice *_this);
+extern bool OPENHARMONY_HasClipboardText(SDL_VideoDevice *_this);
+
+#endif // SDL_openharmonyclipboard_h_

+ 574 - 0
src/video/openharmony/SDL_openharmonyevents.c

@@ -0,0 +1,574 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+
+// !!! FIXME: these are defined as "const uint32_t VARNAME = VALUE;" in native_interface_xcomponent.h, which becomes a global variable in _our_ C code! Maybe C++ handles this differently...?
+#define OH_XCOMPONENT_ID_LEN_MAX sdl_ohosevents_OH_XCOMPONENT_ID_LEN_MAX
+#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohosevents_OH_MAX_TOUCH_POINTS_NUMBER
+#include <ace/xcomponent/native_interface_xcomponent.h>
+
+#include "SDL_openharmonyevents.h"
+//#include "SDL_openharmonykeyboard.h"
+#include "SDL_openharmonywindow.h"
+#include "../SDL_sysvideo.h"
+#include "../../events/SDL_events_c.h"
+
+static SDL_Scancode MapOHKeycodeToSDLScancode(const OH_NativeXComponent_KeyCode ohcode)
+{
+    // !!! FIXME: this needs a serious looking-over.
+    switch (ohcode) {
+        #define KEYMAP(oh,sdl) case KEY_##oh: return SDL_SCANCODE_##sdl
+        KEYMAP(UNKNOWN, UNKNOWN);
+        //case KEY_FN = 0,
+        KEYMAP(HOME, AC_HOME);
+        KEYMAP(BACK, AC_BACK);
+        KEYMAP(MEDIA_PLAY_PAUSE, MEDIA_PLAY_PAUSE);
+        KEYMAP(MEDIA_STOP, MEDIA_STOP);
+        KEYMAP(MEDIA_NEXT, MEDIA_NEXT_TRACK);
+        KEYMAP(MEDIA_PREVIOUS, MEDIA_PREVIOUS_TRACK);
+        KEYMAP(MEDIA_REWIND, MEDIA_REWIND);
+        KEYMAP(MEDIA_FAST_FORWARD, MEDIA_FAST_FORWARD);
+        KEYMAP(VOLUME_UP, VOLUMEUP);
+        KEYMAP(VOLUME_DOWN, VOLUMEDOWN);
+        KEYMAP(POWER, POWER);
+        //KEYMAP(CAMERA, CAMERA);
+        KEYMAP(VOLUME_MUTE, MUTE);
+        KEYMAP(MUTE, MUTE);
+        //KEYMAP(BRIGHTNESS_UP = 40,
+        //KEYMAP(BRIGHTNESS_DOWN = 41,
+        KEYMAP(0, 0);
+        KEYMAP(1, 1);
+        KEYMAP(2, 2);
+        KEYMAP(3, 3);
+        KEYMAP(4, 4);
+        KEYMAP(5, 5);
+        KEYMAP(6, 6);
+        KEYMAP(7, 7);
+        KEYMAP(8, 8);
+        KEYMAP(9, 9);
+        //KEYMAP(STAR = 2010,
+        //KEYMAP(POUND = 2011,
+        KEYMAP(DPAD_UP, UP);
+        KEYMAP(DPAD_DOWN, DOWN);
+        KEYMAP(DPAD_LEFT, LEFT);
+        KEYMAP(DPAD_RIGHT, RIGHT);
+        //KEYMAP(DPAD_CENTER = 2016,
+        KEYMAP(A, A);
+        KEYMAP(B, B);
+        KEYMAP(C, C);
+        KEYMAP(D, D);
+        KEYMAP(E, E);
+        KEYMAP(F, F);
+        KEYMAP(G, G);
+        KEYMAP(H, H);
+        KEYMAP(I, I);
+        KEYMAP(J, J);
+        KEYMAP(K, K);
+        KEYMAP(L, L);
+        KEYMAP(M, M);
+        KEYMAP(N, N);
+        KEYMAP(O, O);
+        KEYMAP(P, P);
+        KEYMAP(Q, Q);
+        KEYMAP(R, R);
+        KEYMAP(S, S);
+        KEYMAP(T, T);
+        KEYMAP(U, U);
+        KEYMAP(V, V);
+        KEYMAP(W, W);
+        KEYMAP(X, X);
+        KEYMAP(Y, Y);
+        KEYMAP(Z, Z);
+        KEYMAP(COMMA, COMMA);
+        KEYMAP(PERIOD, PERIOD);
+        KEYMAP(ALT_LEFT, LALT);
+        KEYMAP(ALT_RIGHT, RALT);
+        KEYMAP(SHIFT_LEFT, LSHIFT);
+        KEYMAP(SHIFT_RIGHT, RSHIFT);
+        KEYMAP(TAB, TAB);
+        KEYMAP(SPACE, SPACE);
+        //KEYMAP(SYM = 2051,
+        //KEYMAP(EXPLORER = 2052,
+        //KEYMAP(ENVELOPE = 2053,
+        KEYMAP(ENTER, RETURN);
+        KEYMAP(DEL, DELETE);
+        KEYMAP(GRAVE, GRAVE);
+        KEYMAP(MINUS, MINUS);
+        KEYMAP(EQUALS, EQUALS);
+        KEYMAP(LEFT_BRACKET, LEFTBRACKET);
+        KEYMAP(RIGHT_BRACKET, RIGHTBRACKET);
+        KEYMAP(BACKSLASH, BACKSLASH);
+        KEYMAP(SEMICOLON, SEMICOLON);
+        KEYMAP(APOSTROPHE, APOSTROPHE);
+        KEYMAP(SLASH, SLASH);
+        KEYMAP(AT, KP_AT);
+        KEYMAP(PLUS, KP_PLUS);
+        KEYMAP(MENU, MENU);
+        KEYMAP(PAGE_UP, PAGEUP);
+        KEYMAP(PAGE_DOWN, PAGEDOWN);
+        KEYMAP(ESCAPE, ESCAPE);
+        //KEYMAP(FORWARD_DEL = 2071,
+        KEYMAP(CTRL_LEFT, LCTRL);
+        KEYMAP(CTRL_RIGHT, RCTRL);
+        KEYMAP(CAPS_LOCK, CAPSLOCK);
+        KEYMAP(SCROLL_LOCK, SCROLLLOCK);
+        KEYMAP(META_LEFT, LGUI);
+        KEYMAP(META_RIGHT, RGUI);
+        //KEYMAP(FUNCTION = 2078,
+        KEYMAP(SYSRQ, SYSREQ);
+        //KEYMAP(BREAK = 2080,
+        KEYMAP(MOVE_HOME, HOME);
+        KEYMAP(MOVE_END, END);
+        KEYMAP(INSERT, INSERT);
+        KEYMAP(FORWARD, AC_FORWARD);
+        KEYMAP(MEDIA_PLAY, MEDIA_PLAY);
+        KEYMAP(MEDIA_PAUSE, MEDIA_PAUSE);
+        //KEYMAP(MEDIA_CLOSE = 2087,
+        KEYMAP(MEDIA_EJECT, MEDIA_EJECT);
+        KEYMAP(MEDIA_RECORD, MEDIA_RECORD);
+        KEYMAP(F1, F1);
+        KEYMAP(F2, F2);
+        KEYMAP(F3, F3);
+        KEYMAP(F4, F4);
+        KEYMAP(F5, F5);
+        KEYMAP(F6, F6);
+        KEYMAP(F7, F7);
+        KEYMAP(F8, F8);
+        KEYMAP(F9, F9);
+        KEYMAP(F10, F10);
+        KEYMAP(F11, F11);
+        KEYMAP(F12, F12);
+        KEYMAP(NUM_LOCK, NUMLOCKCLEAR);
+        KEYMAP(NUMPAD_0, KP_0);
+        KEYMAP(NUMPAD_1, KP_1);
+        KEYMAP(NUMPAD_2, KP_2);
+        KEYMAP(NUMPAD_3, KP_3);
+        KEYMAP(NUMPAD_4, KP_4);
+        KEYMAP(NUMPAD_5, KP_5);
+        KEYMAP(NUMPAD_6, KP_6);
+        KEYMAP(NUMPAD_7, KP_7);
+        KEYMAP(NUMPAD_8, KP_8);
+        KEYMAP(NUMPAD_9, KP_9);
+        KEYMAP(NUMPAD_DIVIDE, KP_DIVIDE);
+        KEYMAP(NUMPAD_MULTIPLY, KP_MULTIPLY);
+        KEYMAP(NUMPAD_SUBTRACT, KP_MINUS);
+        KEYMAP(NUMPAD_ADD, KP_PLUS);
+        KEYMAP(NUMPAD_DOT, KP_PERIOD);
+        //KEYMAP(NUMPAD_COMMA = 2118,
+        KEYMAP(NUMPAD_ENTER, KP_ENTER);
+        //KEYMAP(NUMPAD_EQUALS = 2120,
+        KEYMAP(NUMPAD_LEFT_PAREN, KP_LEFTPAREN);
+        KEYMAP(NUMPAD_RIGHT_PAREN, KP_RIGHTPAREN);
+        //KEYMAP(VIRTUAL_MULTITASK = 2210,
+        KEYMAP(SLEEP, SLEEP);
+        KEYMAP(ZENKAKU_HANKAKU, LANG5);
+        //KEYMAP(102ND = 2602,
+        //KEYMAP(RO = 2603,
+        KEYMAP(KATAKANA, LANG3);
+        KEYMAP(HIRAGANA, LANG4);
+        //KEYMAP(HENKAN = 2606,
+        //KEYMAP(KATAKANA_HIRAGANA = 2607,
+        //KEYMAP(MUHENKAN = 2608,
+        //KEYMAP(LINEFEED = 2609,
+        //KEYMAP(MACRO = 2610,
+        KEYMAP(NUMPAD_PLUSMINUS, KP_PLUSMINUS);
+        //KEYMAP(SCALE = 2612,
+        //KEYMAP(HANGUEL = 2613,
+        //KEYMAP(HANJA = 2614,
+        //KEYMAP(YEN = 2615,
+        KEYMAP(STOP, STOP);
+        KEYMAP(AGAIN, AGAIN);
+        KEYMAP(PROPS, AC_PROPERTIES);  // I think....?
+        KEYMAP(UNDO, UNDO);
+        KEYMAP(COPY, COPY);
+        KEYMAP(OPEN, AC_OPEN);  /// I think...?
+        KEYMAP(PASTE, PASTE);
+        KEYMAP(FIND, FIND);
+        KEYMAP(CUT, CUT);
+        KEYMAP(HELP, HELP);
+        //KEYMAP(CALC = 2626,
+        //KEYMAP(FILE = 2627,
+        KEYMAP(BOOKMARKS, AC_BOOKMARKS);
+        KEYMAP(NEXT, MEDIA_NEXT_TRACK);
+        KEYMAP(PLAYPAUSE, MEDIA_PLAY_PAUSE);
+        KEYMAP(PREVIOUS, MEDIA_PREVIOUS_TRACK);
+        KEYMAP(STOPCD, MEDIA_STOP);
+        //KEYMAP(CONFIG = 2634,
+        KEYMAP(REFRESH, AC_REFRESH);
+        KEYMAP(EXIT, AC_EXIT);
+        //KEYMAP(EDIT = 2637,
+        //KEYMAP(SCROLLUP = 2638,
+        //KEYMAP(SCROLLDOWN = 2639,
+        KEYMAP(NEW, AC_NEW);
+        KEYMAP(REDO, AGAIN);
+        KEYMAP(CLOSE, AC_CLOSE);
+        KEYMAP(PLAY, MEDIA_PLAY);
+        //KEYMAP(BASSBOOST = 2644,
+        KEYMAP(PRINT, PRINTSCREEN);
+        //KEYMAP(CHAT = 2646,
+        //KEYMAP(FINANCE = 2647,
+        KEYMAP(CANCEL, CANCEL);
+        //KEYMAP(KBDILLUM_TOGGLE = 2649,
+        //KEYMAP(KBDILLUM_DOWN = 2650,
+        //KEYMAP(KBDILLUM_UP = 2651,
+        //KEYMAP(SEND = 2652,
+        //KEYMAP(REPLY = 2653,
+        //KEYMAP(FORWARDMAIL = 2654,
+        KEYMAP(SAVE, AC_SAVE);
+        //KEYMAP(DOCUMENTS = 2656,
+        //KEYMAP(VIDEO_NEXT = 2657,
+        //KEYMAP(VIDEO_PREV = 2658,
+        //KEYMAP(BRIGHTNESS_CYCLE = 2659,
+        //KEYMAP(BRIGHTNESS_ZERO = 2660,
+        //KEYMAP(DISPLAY_OFF = 2661,
+        //KEYMAP(BTN_MISC = 2662,
+        //KEYMAP(GOTO = 2663,
+        //KEYMAP(INFO = 2664,
+        //KEYMAP(PROGRAM = 2665,
+        //KEYMAP(PVR = 2666,
+        //KEYMAP(SUBTITLE = 2667,
+        //KEYMAP(FULL_SCREEN = 2668,
+        //KEYMAP(KEYBOARD = 2669,
+        //KEYMAP(ASPECT_RATIO = 2670,
+        //KEYMAP(PC = 2671,
+        //KEYMAP(TV = 2672,
+        //KEYMAP(TV2 = 2673,
+        //KEYMAP(VCR = 2674,
+        //KEYMAP(VCR2 = 2675,
+        //KEYMAP(SAT = 2676,
+        //KEYMAP(CD = 2677,
+        //KEYMAP(TAPE = 2678,
+        //KEYMAP(TUNER = 2679,
+        //KEYMAP(PLAYER = 2680,
+        //KEYMAP(DVD = 2681,
+        //KEYMAP(AUDIO = 2682,
+        //KEYMAP(VIDEO = 2683,
+        //KEYMAP(MEMO = 2684,
+        //KEYMAP(CALENDAR = 2685,
+        //KEYMAP(RED = 2686,
+        //KEYMAP(GREEN = 2687,
+        //KEYMAP(YELLOW = 2688,
+        //KEYMAP(BLUE = 2689,
+        //KEYMAP(CHANNELUP = 2690,
+        //KEYMAP(CHANNELDOWN = 2691,
+        //KEYMAP(LAST = 2692,
+        //KEYMAP(RESTART = 2693,
+        //KEYMAP(SLOW = 2694,
+        //KEYMAP(SHUFFLE = 2695,
+        //KEYMAP(VIDEOPHONE = 2696,
+        //KEYMAP(GAMES = 2697,
+        //KEYMAP(ZOOMIN = 2698,
+        //KEYMAP(ZOOMOUT = 2699,
+        //KEYMAP(ZOOMRESET = 2700,
+        //KEYMAP(WORDPROCESSOR = 2701,
+        //KEYMAP(EDITOR = 2702,
+        //KEYMAP(SPREADSHEET = 2703,
+        //KEYMAP(GRAPHICSEDITOR = 2704,
+        //KEYMAP(PRESENTATION = 2705,
+        //KEYMAP(DATABASE = 2706,
+        //KEYMAP(NEWS = 2707,
+        //KEYMAP(VOICEMAIL = 2708,
+        //KEYMAP(ADDRESSBOOK = 2709,
+        //KEYMAP(MESSENGER = 2710,
+        //KEYMAP(BRIGHTNESS_TOGGLE = 2711,
+        //KEYMAP(SPELLCHECK = 2712,
+        //KEYMAP(COFFEE = 2713,
+        //KEYMAP(MEDIA_REPEAT = 2714,
+        //KEYMAP(IMAGES = 2715,
+        //KEYMAP(BUTTONCONFIG = 2716,
+        //KEYMAP(TASKMANAGER = 2717,
+        //KEYMAP(JOURNAL = 2718,
+        //KEYMAP(CONTROLPANEL = 2719,
+        //KEYMAP(APPSELECT = 2720,
+        //KEYMAP(SCREENSAVER = 2721,
+        //KEYMAP(ASSISTANT = 2722,
+        //KEYMAP(KBD_LAYOUT_NEXT = 2723,
+        //KEYMAP(BRIGHTNESS_MIN = 2724,
+        //KEYMAP(BRIGHTNESS_MAX = 2725,
+        //KEYMAP(KBDINPUTASSIST_PREV = 2726,
+        //KEYMAP(KBDINPUTASSIST_NEXT = 2727,
+        //KEYMAP(KBDINPUTASSIST_PREVGROUP = 2728,
+        //KEYMAP(KBDINPUTASSIST_NEXTGROUP = 2729,
+        //KEYMAP(KBDINPUTASSIST_ACCEPT = 2730,
+        //KEYMAP(KBDINPUTASSIST_CANCEL = 2731,
+        //KEYMAP(FRONT = 2800,
+        //KEYMAP(SETUP = 2801,
+        //KEYMAP(WAKEUP = 2802,
+        //KEYMAP(SENDFILE = 2803,
+        //KEYMAP(DELETEFILE = 2804,
+        //KEYMAP(XFER = 2805,
+        //KEYMAP(PROG1 = 2806,
+        //KEYMAP(PROG2 = 2807,
+        //KEYMAP(MSDOS = 2808,
+        //KEYMAP(SCREENLOCK = 2809,
+        //KEYMAP(DIRECTION_ROTATE_DISPLAY = 2810,
+        //KEYMAP(CYCLEWINDOWS = 2811,
+        //KEYMAP(COMPUTER = 2812,
+        //KEYMAP(EJECTCLOSECD = 2813,
+        //KEYMAP(ISO = 2814,
+        //KEYMAP(MOVE = 2815,
+        KEYMAP(F13, F13);
+        KEYMAP(F14, F14);
+        KEYMAP(F15, F15);
+        KEYMAP(F16, F16);
+        KEYMAP(F17, F17);
+        KEYMAP(F18, F18);
+        KEYMAP(F19, F19);
+        KEYMAP(F20, F20);
+        KEYMAP(F21, F21);
+        KEYMAP(F22, F22);
+        KEYMAP(F23, F23);
+        KEYMAP(F24, F24);
+        //KEYMAP(PROG3 = 2828,
+        //KEYMAP(PROG4 = 2829,
+        //KEYMAP(DASHBOARD = 2830,
+        //KEYMAP(SUSPEND = 2831,
+        //KEYMAP(HP = 2832,
+        //KEYMAP(SOUND = 2833,
+        //KEYMAP(QUESTION = 2834,
+        //KEYMAP(CONNECT = 2836,
+        //KEYMAP(SPORT = 2837,
+        //KEYMAP(SHOP = 2838,
+        //KEYMAP(ALTERASE = 2839,
+        //KEYMAP(SWITCHVIDEOMODE = 2841,
+        //KEYMAP(BATTERY = 2842,
+        //KEYMAP(BLUETOOTH = 2843,
+        //KEYMAP(WLAN = 2844,
+        //KEYMAP(UWB = 2845,
+        //KEYMAP(WWAN_WIMAX = 2846,
+        //KEYMAP(RFKILL = 2847,
+        //KEYMAP(CHANNEL = 3001,
+        //KEYMAP(BTN_0 = 3100,
+        //KEYMAP(BTN_1 = 3101,
+        //KEYMAP(BTN_2 = 3102,
+        //KEYMAP(BTN_3 = 3103,
+        //KEYMAP(BTN_4 = 3104,
+        //KEYMAP(BTN_5 = 3105,
+        //KEYMAP(BTN_6 = 3106,
+        //KEYMAP(BTN_7 = 3107,
+        //KEYMAP(BTN_8 = 3108,
+        //KEYMAP(BTN_9 = 3109,
+        #undef KEYMAP
+        default: break;
+    }
+
+    return SDL_SCANCODE_UNKNOWN;
+}
+
+void SDL_OpenHarmonyDispatchTouchEvent(void *component, void *window)
+{
+    OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component;
+    OH_NativeXComponent_TouchEvent event;
+    if (OH_NativeXComponent_GetTouchEvent(xcomponent, window, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
+        return;  // oh well.
+    }
+
+#if 0
+    SDL_Log("TOUCH EVENT! id=%d screenX=%f screenY=%f x=%f y=%f type=%d size=%f force=%f devid=%lld timestamp=%lld numpoints=%d", (int) event.id, event.screenX, event.screenY, event.x, event.y, (int) event.type, (float) event.size, event.force, (long long) event.deviceId, (long long) event.timeStamp, (int) event.numPoints);
+    for (int i = 0; i < event.numPoints; i++) {
+        SDL_Log("TOUCH POINT #%d: id=%d screenX=%f screenY=%f x=%f y=%f type=%d size=%f force=%f timestamp=%lld ispressed=%s", i, (int) event.touchPoints[i].id, event.touchPoints[i].screenX, event.touchPoints[i].screenY, event.touchPoints[i].x, event.touchPoints[i].y, (int) event.touchPoints[i].type, (float) event.touchPoints[i].size, event.touchPoints[i].force, (long long) event.touchPoints[i].timeStamp, event.touchPoints[i].isPressed ? "true" : "false");
+    }
+#endif
+
+    SDL_EventType sdleventtype = SDL_EVENT_FIRST;
+    switch (event.type) {
+        #define EVTYPEMAP(ohevent, sdlevent) case OH_NATIVEXCOMPONENT_##ohevent: sdleventtype = SDL_EVENT_FINGER_##sdlevent; break
+        EVTYPEMAP(DOWN, DOWN);
+        EVTYPEMAP(UP, UP);
+        EVTYPEMAP(MOVE, MOTION);
+        EVTYPEMAP(CANCEL, CANCELED);
+        default: break;
+        #undef EVTYPEMAP
+    }
+
+    if (sdleventtype == SDL_EVENT_FIRST) {  // FIRST == unknown event type.
+        return;  // nothing to do.
+    }
+
+    int touchidx = 0;
+    while (touchidx < event.numPoints) {
+        if (event.touchPoints[touchidx].id == event.id) {
+            break;
+        }
+        touchidx++;
+    }
+
+    if (touchidx >= event.numPoints) {
+        return;  // uh...we don't have this touch...?
+    }
+
+    OH_NativeXComponent_TouchPointToolType tooltype;
+    if (OH_NativeXComponent_GetTouchPointToolType(xcomponent, touchidx, &tooltype) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
+        return;   // bad bad bad
+    }
+
+    const float normalized_x = OPENHARMONY_Window->w ? (event.x / ((float) OPENHARMONY_Window->w)) : 0.0f;
+    const float normalized_y = OPENHARMONY_Window->h ? (event.y / ((float) OPENHARMONY_Window->h)) : 0.0f;
+
+    if (tooltype == OH_NATIVEXCOMPONENT_TOOL_TYPE_FINGER) {
+        const SDL_TouchID touchid = (SDL_TouchID)(((uintptr_t)event.deviceId) + 1);
+        SDL_AddTouch(touchid, SDL_TOUCH_DEVICE_DIRECT, NULL);
+        if (sdleventtype == SDL_EVENT_FINGER_MOTION) {
+            SDL_SendTouchMotion((Uint64) event.timeStamp, touchid, (SDL_FingerID)(((uintptr_t)event.id)+1), OPENHARMONY_Window, normalized_x, normalized_y, event.force);
+        } else {
+            SDL_SendTouch((Uint64) event.timeStamp, touchid, (SDL_FingerID)(((uintptr_t)event.id)+1), OPENHARMONY_Window, sdleventtype, normalized_x, normalized_y, event.force);
+        }
+        return;
+    }
+
+#if 0  // !!! FIXME: see if Pen events are useful here.
+    switch (tooltype) {
+    /** Indicates invalid tool type. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_UNKNOWN = 0,
+    /** Indicates a finger. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_FINGER,
+    /** Indicates a stylus. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_PEN,
+    /** Indicates an eraser. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_RUBBER,
+    /** Indicates a brush. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_BRUSH,
+    /** Indicates a pencil. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_PENCIL,
+    /** Indicates a brush. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_AIRBRUSH,
+    /** Indicates a mouse. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_MOUSE,
+    /** Indicates a lens. */
+    OH_NATIVEXCOMPONENT_TOOL_TYPE_LENS,
+    }
+#endif
+}
+
+void SDL_OpenHarmonyDispatchMouseEvent(void *component, void *window)
+{
+    OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component;
+    OH_NativeXComponent_MouseEvent event;
+    if (OH_NativeXComponent_GetMouseEvent(xcomponent, window, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
+        return;  // oh well.
+    }
+
+#if 1
+    SDL_Log("MOUSE EVENT! screenX=%f screenY=%f x=%f y=%f timestamp=%lld action=%d button=0x%X", event.screenX, event.screenY, event.x, event.y, (long long) event.timestamp, (int) event.action, (unsigned int) event.button);
+#endif
+
+    const SDL_MouseID mouseid = SDL_DEFAULT_MOUSE_ID;  // !!! FIXME: should this be SDL_GLOBAL_MOUSE_ID or SDL_DEFAULT_MOUSE_ID?
+    bool down = false;
+    switch (event.action) {
+        case OH_NATIVEXCOMPONENT_MOUSE_PRESS:
+            down = true;
+            SDL_FALLTHROUGH;
+        case OH_NATIVEXCOMPONENT_MOUSE_RELEASE:
+            // these are bitmasks, so while I _assume_ you won't see more than one button per event, check them all separately, just in case.
+            #define CHECK_BUTTON(ohos, sdl) if (event.button & OH_NATIVEXCOMPONENT_##ohos##_BUTTON) { SDL_SendMouseButton((Uint64) event.timestamp, OPENHARMONY_Window, mouseid, SDL_BUTTON_##sdl, down); }
+            CHECK_BUTTON(LEFT, LEFT);
+            CHECK_BUTTON(RIGHT, RIGHT);
+            CHECK_BUTTON(MIDDLE, MIDDLE);
+            CHECK_BUTTON(BACK, X1);
+            CHECK_BUTTON(FORWARD, X2);
+            #undef CHECK_BUTTON
+            break;
+
+        case OH_NATIVEXCOMPONENT_MOUSE_MOVE:
+            SDL_SendMouseMotion((Uint64) event.timestamp, OPENHARMONY_Window, mouseid, false, event.x, event.y);
+            break;
+
+        default: break;  // nothing to do?
+    }
+}
+
+void SDL_OpenHarmonyDispatchUIInputEvent(void *vcomponent, void *vevent, int32_t vtype)
+{
+//    OH_NativeXComponent *component = (OH_NativeXComponent *) vcomponent;
+    ArkUI_UIInputEvent *event = (ArkUI_UIInputEvent *) vevent;
+    const ArkUI_UIInputEvent_Type type = (const ArkUI_UIInputEvent_Type) vtype;
+
+    if (type != ARKUI_UIINPUTEVENT_TYPE_AXIS) {
+        return;  // this is all we handle here for now. Maybe more in the future!
+    } else if (OH_ArkUI_UIInputEvent_GetSourceType(event) != UI_INPUT_EVENT_SOURCE_TYPE_MOUSE) {
+        return;  // skip two-finger scrolling (...for now...?)
+    }
+
+    const SDL_MouseID mouseid = SDL_DEFAULT_MOUSE_ID;  // !!! FIXME: should this be SDL_GLOBAL_MOUSE_ID or SDL_DEFAULT_MOUSE_ID?
+    const double vertical = OH_ArkUI_AxisEvent_GetVerticalAxisValue(event);
+    if (vertical != 0.0) {
+        // !!! FIXME: OpenHarmony has a system setting for "natural" scrolling we can use for SDL_MouseWheelDirection, but I
+        // !!! FIXME: don't know how to access it right now.
+        // a single "click" of the wheel on my mouse is 45 degrees (360 / 8 click positions), so let's assume that's normal (and what Windows would do with WHEEL_DELTA, which is a different value with the same concept).
+        SDL_SendMouseWheel((Uint64) OH_ArkUI_UIInputEvent_GetEventTime(event), OPENHARMONY_Window, mouseid, 0.0f, -(vertical / 45.0f), SDL_MOUSEWHEEL_NORMAL);
+    }
+
+    // !!! FIXME: this is in pixels, not degrees, and I'm not sure how to convert that yet.
+    // !!! FIXME: It's possible they don't support horizontal mouse wheels, and this is only for two-finger scrolling.
+    #if 0
+    const double horizontal = OH_ArkUI_AxisEvent_GetHorizontalAxisValue(event);
+    if (horizontal != 0.0) {
+    }
+    #endif
+}
+
+void SDL_OpenHarmonyDispatchKeyEvent(void *component, void *window)
+{
+    OH_NativeXComponent *xcomponent = (OH_NativeXComponent *) component;
+    OH_NativeXComponent_KeyEvent *event = NULL;
+    if (OH_NativeXComponent_GetKeyEvent(xcomponent, &event) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
+        return;  // oh well.
+    }
+
+    OH_NativeXComponent_KeyCode keycode = KEY_UNKNOWN;
+    OH_NativeXComponent_GetKeyEventCode(event, &keycode);
+    const SDL_Scancode scancode = MapOHKeycodeToSDLScancode(keycode);
+    if (scancode == SDL_SCANCODE_UNKNOWN) {
+        return;  // oh well.
+    }
+
+    OH_NativeXComponent_KeyAction action = OH_NATIVEXCOMPONENT_KEY_ACTION_UNKNOWN;
+    OH_NativeXComponent_GetKeyEventAction(event, &action);
+    bool down;
+    if (action == OH_NATIVEXCOMPONENT_KEY_ACTION_DOWN) {
+        down = true;
+    } else if (action == OH_NATIVEXCOMPONENT_KEY_ACTION_UP) {
+        down = false;
+    } else {
+        return;  // oh well.
+    }
+
+    int64_t timestamp = 0;
+    OH_NativeXComponent_GetKeyEventTimestamp(event, &timestamp);
+
+    SDL_SendKeyboardKey((Uint64) timestamp, SDL_DEFAULT_KEYBOARD_ID, keycode, scancode, down);
+}
+
+void OPENHARMONY_InitEvents(void)
+{
+}
+
+void OPENHARMONY_PumpEvents(SDL_VideoDevice *_this)
+{
+}
+
+void OPENHARMONY_QuitEvents(void)
+{
+}
+
+#endif // SDL_VIDEO_DRIVER_OPENHARMONY

+ 33 - 0
src/video/openharmony/SDL_openharmonyevents.h

@@ -0,0 +1,33 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#include "../SDL_sysvideo.h"
+
+// src/core/openharmony/SDL_openharmony.c calls these when it gets events from the system.
+extern void SDL_OpenHarmonyDispatchTouchEvent(void *component, void *window);
+extern void SDL_OpenHarmonyDispatchMouseEvent(void *component, void *window);
+extern void SDL_OpenHarmonyDispatchKeyEvent(void *component, void *window);
+extern void SDL_OpenHarmonyDispatchUIInputEvent(void *component, void *event, int32_t type);
+
+extern void OPENHARMONY_InitEvents(void);
+extern void OPENHARMONY_PumpEvents(SDL_VideoDevice *_this);
+extern void OPENHARMONY_QuitEvents(void);

+ 93 - 0
src/video/openharmony/SDL_openharmonykeyboard.c

@@ -0,0 +1,93 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+
+#include "../../events/SDL_events_c.h"
+
+#include "SDL_openharmonykeyboard.h"
+
+#include "../../core/openharmony/SDL_openharmony.h"
+
+bool OPENHARMONY_HasScreenKeyboardSupport(SDL_VideoDevice *_this)
+{
+    return true;
+}
+
+void OPENHARMONY_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
+{
+    int input_type = 0;
+    int cap_type = 0;
+    if (SDL_HasProperty(props, SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER)) {
+        input_type = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTINPUT_OPENHARMONY_INPUTTYPE_NUMBER, 0);
+    } else {
+        switch (SDL_GetTextInputType(props)) {
+            #define INPUTTYPE(sdl,oh) case SDL_TEXTINPUT_TYPE_##sdl: input_type = oh; break
+            INPUTTYPE(TEXT, 0);
+            INPUTTYPE(TEXT_NAME, 0);
+            INPUTTYPE(TEXT_EMAIL, 5);
+            INPUTTYPE(TEXT_USERNAME, 10);
+            INPUTTYPE(TEXT_PASSWORD_HIDDEN, 7);  // !!! FIXME: this is _also_ PASSWORD_VISIBLE, but I think this doesn't dictate rendering...?
+            INPUTTYPE(TEXT_PASSWORD_VISIBLE, 7);
+            INPUTTYPE(NUMBER_PASSWORD_HIDDEN, 8); // !!! FIXME: this is _also_ NUMBER_PASSWORD_VISIBLE, but I think this doesn't dictate rendering...?
+            INPUTTYPE(NUMBER_PASSWORD_VISIBLE, 8);
+            #undef INPUTTYPE
+            default: break;
+        }
+
+        switch (SDL_GetTextInputCapitalization(props)) {
+            #define CAPTYPE(sdl,oh) case SDL_CAPITALIZE_##sdl: cap_type = oh; break
+            CAPTYPE(NONE, 0);
+            CAPTYPE(LETTERS, 3);
+            CAPTYPE(WORDS, 2);
+            CAPTYPE(SENTENCES, 1);
+            #undef CAPTYPE
+            default: break;
+        }
+
+        #if 0  // !!! FIXME
+        if (SDL_GetTextInputAutocorrect(props)) {
+            input_type |= (TYPE_TEXT_FLAG_AUTO_CORRECT | TYPE_TEXT_FLAG_AUTO_COMPLETE);
+        }
+        #endif
+
+        if (SDL_GetTextInputMultiline(props)) {
+            input_type = 1;  // sorry, this only works with basic TEXT type.
+        }
+    }
+    SDL_OpenHarmonyShowScreenKeyboard(input_type, cap_type, &window->text_input_rect);
+}
+
+void OPENHARMONY_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
+{
+    SDL_OpenHarmonyHideScreenKeyboard();
+}
+
+void OPENHARMONY_RestoreScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
+{
+    if (_this->screen_keyboard_shown) {
+        OPENHARMONY_ShowScreenKeyboard(_this, window, window->text_input_props);
+    }
+}
+
+#endif // SDL_VIDEO_DRIVER_OPENHARMONY
+

+ 29 - 0
src/video/openharmony/SDL_openharmonykeyboard.h

@@ -0,0 +1,29 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#include "SDL_openharmonyvideo.h"
+
+extern bool OPENHARMONY_HasScreenKeyboardSupport(SDL_VideoDevice *_this);
+extern void OPENHARMONY_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
+extern void OPENHARMONY_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
+extern void OPENHARMONY_RestoreScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
+

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff