Sfoglia il codice sorgente

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 3 mesi fa
parent
commit
a24429c192
100 ha cambiato i file con 6441 aggiunte e 51 eliminazioni
  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/.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/checkkeysthreads
 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/PreseedMSVCCache.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/PreseedNokiaNGageCache.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_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_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_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_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_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_METAL               "Enable Metal support" ON "APPLE" 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")
   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)
   # 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.

+ 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 "")
   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 SDL_UNIX_CONSOLE_BUILD)
         message(FATAL_ERROR

+ 5 - 0
cmake/sdlcompilers.cmake

@@ -184,6 +184,11 @@ function(SDL_AddCommonCompilerFlags TARGET)
   if(USE_TCC)
       sdl_target_compile_option_all_languages(${TARGET} "-DSTBI_NO_SIMD")
   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()
 
 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)
 - [FreeBSD](README-bsd.md)
 - [Haiku OS](README-haiku.md)
+- [HarmonyOS/OpenHarmony](README-harmonyos.md)
 - [iOS](README-ios.md)
 - [Linux](README-linux.md)
 - [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
  *   the filesystem. If SDL used some other method to access the filesystem,
  *   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 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);
 
-#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

+ 7 - 0
include/SDL3/SDL_keyboard.h

@@ -464,6 +464,12 @@ typedef enum SDL_Capitalization
  *   use, overriding other properties. This is documented at
  *   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 props the properties to use.
  * \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_MAX_LENGTH_NUMBER            "SDL.textinput.max_length"
 #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.

+ 13 - 0
include/SDL3/SDL_main.h

@@ -193,6 +193,19 @@
          */
         #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)
         /* On Emscripten, SDL provides a main function that converts URL
            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
                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. */
 
             /* 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
 #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)
 
 /**

+ 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);
 
 /**
- * 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 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 */
 
+#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.
  *

+ 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
  *   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:
  *
  * - `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_ANDROID_WINDOW_POINTER                      "SDL.window.android.window"
 #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_METAL_VIEW_TAG_NUMBER                 "SDL.window.uikit.metal_view_tag"
 #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_QNX 1
 #cmakedefine SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER 1
+#cmakedefine SDL_AUDIO_DRIVER_OHAUDIO 1
 #cmakedefine SDL_AUDIO_DRIVER_PRIVATE 1
 
 /* Enable various input drivers */
@@ -404,6 +405,7 @@
 
 /* Enable various video drivers */
 #cmakedefine SDL_VIDEO_DRIVER_ANDROID 1
+#cmakedefine SDL_VIDEO_DRIVER_OPENHARMONY 1
 #cmakedefine SDL_VIDEO_DRIVER_COCOA 1
 #cmakedefine SDL_VIDEO_DRIVER_DUMMY 1
 #cmakedefine SDL_VIDEO_DRIVER_EMSCRIPTEN 1
@@ -513,6 +515,7 @@
 #cmakedefine SDL_POWER_VITA 1
 #cmakedefine SDL_POWER_PSP 1
 #cmakedefine SDL_POWER_N3DS 1
+#cmakedefine SDL_POWER_OPENHARMONY 1
 
 #cmakedefine SDL_POWER_PRIVATE 1
 
@@ -530,6 +533,7 @@
 #cmakedefine SDL_FILESYSTEM_PS2 1
 #cmakedefine SDL_FILESYSTEM_N3DS 1
 #cmakedefine SDL_FILESYSTEM_DOS 1
+#cmakedefine SDL_FILESYSTEM_OPENHARMONY 1
 
 #cmakedefine SDL_FILESYSTEM_PRIVATE 1
 
@@ -551,6 +555,7 @@
 #cmakedefine SDL_CAMERA_DRIVER_V4L2 1
 #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA 1
 #cmakedefine SDL_CAMERA_DRIVER_ANDROID 1
+#cmakedefine SDL_CAMERA_DRIVER_OPENHARMONY 1
 #cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN 1
 #cmakedefine SDL_CAMERA_DRIVER_MEDIAFOUNDATION 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
 #include "core/android/SDL_android.h"
 #endif
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include "core/openharmony/SDL_openharmony.h"
+#endif
 
 #define SDL_ALL_SUBSYSTEM_FLAGS ~0U
 
@@ -803,6 +806,8 @@ const char *SDL_GetPlatform(void)
     return "Nokia N-Gage";
 #elif defined(SDL_PLATFORM_OPENBSD)
     return "OpenBSD";
+#elif defined(SDL_PLATFORM_OPENHARMONY)
+    return "OpenHarmony";
 #elif defined(SDL_PLATFORM_OS2)
     return "OS/2";
 #elif defined(SDL_PLATFORM_OSF)
@@ -867,6 +872,8 @@ SDL_FormFactor SDL_GetDeviceFormFactor(void)
     return SDL_FORMFACTOR_PRIVATE;
 #elif defined(SDL_PLATFORM_ANDROID)
     return SDL_GetAndroidDeviceFormFactor();
+#elif defined(SDL_PLATFORM_OPENHARMONY)
+    return SDL_GetOpenHarmonyDeviceFormFactor();
 #elif defined(SDL_PLATFORM_IOS)
     extern SDL_FormFactor SDL_GetUIKitDeviceFormFactor(void);
     return SDL_GetUIKitDeviceFormFactor();

+ 24 - 5
src/SDL_log.c

@@ -40,6 +40,10 @@
 #include <android/log.h>
 #endif
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include <hilog/log.h>
+#endif
+
 #include "stdlib/SDL_vacopy.h"
 
 // The size of the stack buffer to use for rendering log messages.
@@ -122,7 +126,19 @@ static int SDL_android_priority[] = {
     ANDROID_LOG_FATAL
 };
 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)
 {
@@ -583,7 +599,7 @@ void SDL_LogMessage(int category, SDL_LogPriority priority, SDL_PRINTF_FORMAT_ST
     va_end(ap);
 }
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
 static const char *GetCategoryPrefix(int category)
 {
     if (category < SDL_LOG_CATEGORY_RESERVED2) {
@@ -594,7 +610,7 @@ static const char *GetCategoryPrefix(int category)
     }
     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)
 {
@@ -758,12 +774,15 @@ static void SDLCALL SDL_LogOutput(void *userdata, int category, SDL_LogPriority
         SDL_free(tstr);
         SDL_small_free(output, isstack);
     }
-#elif defined(SDL_PLATFORM_ANDROID)
+#elif defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     {
         char tag[32];
-
         SDL_snprintf(tag, SDL_arraysize(tag), "SDL/%s", GetCategoryPrefix(category));
+        #if defined(SDL_PLATFORM_ANDROID)
         __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))
     /* 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
     &DOSSOUNDBLASTER_bootstrap,
 #endif
+#ifdef SDL_AUDIO_DRIVER_OHAUDIO
+    &OHAUDIO_bootstrap,
+#endif
 #ifdef SDL_AUDIO_DRIVER_DISK
     &DISKAUDIO_bootstrap,
 #endif

+ 1 - 0
src/audio/SDL_sysaudio.h

@@ -395,5 +395,6 @@ extern AudioBootStrap NGAGEAUDIO_bootstrap;
 extern AudioBootStrap EMSCRIPTENAUDIO_bootstrap;
 extern AudioBootStrap QSAAUDIO_bootstrap;
 extern AudioBootStrap DOSSOUNDBLASTER_bootstrap;
+extern AudioBootStrap OHAUDIO_bootstrap;
 
 #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
     &ANDROIDCAMERA_bootstrap,
 #endif
+#ifdef SDL_CAMERA_DRIVER_OPENHARMONY
+    &OPENHARMONYCAMERA_bootstrap,
+#endif
 #ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN
     &EMSCRIPTENCAMERA_bootstrap,
 #endif

+ 1 - 0
src/camera/SDL_syscamera.h

@@ -222,6 +222,7 @@ extern CameraBootStrap PIPEWIRECAMERA_bootstrap;
 extern CameraBootStrap V4L2_bootstrap;
 extern CameraBootStrap COREMEDIA_bootstrap;
 extern CameraBootStrap ANDROIDCAMERA_bootstrap;
+extern CameraBootStrap OPENHARMONYCAMERA_bootstrap;
 extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap;
 extern CameraBootStrap MEDIAFOUNDATION_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
+
+#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)
 extern SDL_DECLSPEC bool SDLCALL SDL_IsUbuntuTouch(void);
 #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_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
 void Android_JNI_GetManifestEnvironmentVariables(void);
 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 (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;
 #elif defined(SDL_PLATFORM_LINUX)
     return readProcAuxvForNeon();

+ 3 - 0
src/dynapi/SDL_dynapi.exports

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

+ 2 - 0
src/dynapi/SDL_dynapi.h

@@ -49,6 +49,8 @@
 #define SDL_DYNAMIC_API 0
 #elif defined(SDL_PLATFORM_ANDROID) // probably not useful on Android.
 #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.
 #define SDL_DYNAMIC_API 0
 #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_SetRenderClipRectFloat;
     SDL_GetRenderClipRectFloat;
+    SDL_GetOpenHarmonySDKVersion;
+    SDL_GetOpenHarmonyInternalStoragePath;
+    SDL_RequestOpenHarmonyPermission;
     # extra symbols go here (don't modify this line)
   local: *;
 };

+ 3 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -1338,3 +1338,6 @@
 #define SDL_GetRenderViewportFloat SDL_GetRenderViewportFloat_REAL
 #define SDL_SetRenderClipRectFloat SDL_SetRenderClipRectFloat_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_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(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)
 {
-#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();
 #endif
     SDL_Keyboard *keyboard = &SDL_keyboard;
@@ -354,7 +354,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window)
     if (keyboard->focus && keyboard->focus != window) {
         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
         if (SDL_TextInputActive(keyboard->focus)) {
             if (video && video->StopTextInput) {
@@ -384,7 +384,7 @@ bool SDL_SetKeyboardFocus(SDL_Window *window)
     if (keyboard->focus) {
         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 (video && video->StartTextInput) {
                 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);
 
     // 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.
         pathlen--;  // we'll add a 1 again later.
     } 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"
 #endif
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include "../../core/openharmony/SDL_openharmony.h"
+#endif
+
 
 bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata)
 {
     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') {
         return SDL_SetError("No such file or directory");
     } else if (*path != '/') {
-        #ifdef SDL_PLATFORM_ANDROID
+        #if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
         if (SDL_strncmp(path, "assets://", 9) == 0) {
             char *pathwithsep = NULL;
             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);
             return retval;
         }
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         #elif defined(SDL_PLATFORM_IOS)
         char *base = SDL_GetPrefPath("", "");
         if (!base) {
@@ -71,7 +75,7 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback
             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 != '/') {
         char *c = SDL_SYS_GetCurrentDirectory();
         SDL_asprintf(&apath, "%s%s", c, path);
@@ -98,9 +102,9 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback
 
     DIR *dir = opendir(pathwithsep);
     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.
-            const bool retval = Android_JNI_EnumerateAssetDirectory(pathwithsep + extralen, cb, userdata);
+            const bool retval = SDL_PlatformEnumerateAssetDirectory(pathwithsep + extralen, cb, userdata);
             SDL_free(pathwithsep);
             return retval;
         }
@@ -130,12 +134,12 @@ bool SDL_SYS_RemovePath(const char *path)
 {
     int rc;
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     if (*path == '/') {
         rc = remove(path);
     } else {
         char *apath = NULL;
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         if (!apath) {
             return false;
         }
@@ -177,18 +181,18 @@ bool SDL_SYS_RenamePath(const char *oldpath, const char *newpath)
 {
     int rc;
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     char *aoldpath = NULL;
     char *anewpath = NULL;
     if (*oldpath != '/') {
-        SDL_asprintf(&aoldpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), oldpath);
+        SDL_asprintf(&aoldpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), oldpath);
         if (!aoldpath) {
             return false;
         }
         oldpath = aoldpath;
     }
     if (*newpath != '/') {
-        SDL_asprintf(&anewpath, "%s/%s", SDL_GetAndroidInternalStoragePath(), newpath);
+        SDL_asprintf(&anewpath, "%s/%s", SDL_GetPlatformInternalStoragePath(), newpath);
         if (!anewpath) {
             SDL_free(aoldpath);
             return false;
@@ -349,16 +353,16 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
     struct stat statbuf;
     int rc;
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
     if (*path == '\0') {
         return SDL_SetError("No such file or directory");
     } else if (*path == '/') {
         rc = stat(path, &statbuf);
     } else if (SDL_strncmp(path, "assets://", 9) == 0) {
-        return Android_JNI_GetAssetPathInfo(path, info);
+        return SDL_PlatformGetAssetPathInfo(path, info);
     } else {
         char *apath = NULL;
-        SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
+        SDL_asprintf(&apath, "%s/%s", SDL_GetPlatformInternalStoragePath(), path);
         if (!apath) {
             return false;
         }
@@ -366,7 +370,7 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
         SDL_free(apath);
     }
     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)
     if (*path == '/') {

+ 36 - 2
src/io/SDL_iostream.c

@@ -69,6 +69,11 @@ struct SDL_IOStream
 #include "../core/android/SDL_android.h"
 #endif
 
+#ifdef SDL_PLATFORM_OPENHARMONY
+#include <unistd.h>
+#include "../core/openharmony/SDL_openharmony.h"
+#endif
+
 #if defined(SDL_PLATFORM_WINDOWS) && !defined(SDL_PLATFORM_CYGWIN)
 
 typedef struct IOStreamWindowsData
@@ -1024,7 +1029,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
         return NULL;
     }
 
-#ifdef SDL_PLATFORM_ANDROID
+#if defined(SDL_PLATFORM_ANDROID) || defined(SDL_PLATFORM_OPENHARMONY)
 #ifdef HAVE_STDIO_H
     // Try to open the file on the filesystem first
     if (*file == '/') {
@@ -1037,6 +1042,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
             }
             return SDL_IOFromFP(fp, true);
         }
+    #ifdef SDL_PLATFORM_ANDROID
     } else if (SDL_strncmp(file, "content://", 10) == 0) {
         // Try opening content:// URI
         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);
+    #endif
     } else if (SDL_strncmp(file, "assets://", 9) != 0) {
         // Try opening it from internal storage if it's a relative path
         char *path = NULL;
-        SDL_asprintf(&path, "%s/%s", SDL_GetAndroidInternalStoragePath(), file);
+        SDL_asprintf(&path, "%s/%s", SDL_GetPlatformInternalStoragePath(), file);
         if (path) {
             FILE *fp = fopen(path, mode);
             SDL_free(path);
@@ -1072,6 +1079,7 @@ SDL_IOStream *SDL_IOFromFile(const char *file, const char *mode)
     }
 #endif // HAVE_STDIO_H
 
+    #ifdef SDL_PLATFORM_ANDROID
     // Try to open the file from the asset system?
     void *iodata = NULL;
     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;
     }
 
+    #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)
 
     // 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;
 }
 
-/* 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)
 {
-    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)
@@ -107,9 +104,7 @@ SDL_AppResult SDL_InitMainCallbacks(int argc, char *argv[], SDL_AppInit_func app
         if (!SDL_InitSubSystem(SDL_INIT_EVENTS)) {
             SDL_SetAtomicInt(&apprc, 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);
             return SDL_APP_FAILURE;
         }

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

@@ -23,6 +23,7 @@
 #include "../SDL_main_callbacks.h"
 #include "../../video/SDL_sysvideo.h"
 
+// !!! FIXME: does this ifndef need to be here?
 #ifndef SDL_PLATFORM_IOS
 
 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"
 
+#if defined(SDL_PLATFORM_ANDROID)
+
 #include "../SDL_sysurl.h"
 #include "../../core/android/SDL_android.h"
 
@@ -27,3 +29,6 @@ bool SDL_SYS_OpenURL(const char *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.
     SDL_GetPowerInfo_Android,
 #endif
+#ifdef SDL_POWER_OPENHARMONY  // handles HarmonyOS/OpenHarmony
+    SDL_GetPowerInfo_OpenHarmony,
+#endif
 #ifdef SDL_POWER_PSP // handles PSP.
     SDL_GetPowerInfo_PSP,
 #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_Haiku(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_VITA(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,
 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. */
-#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
 #else
 #define DONT_DRAW_WHILE_HIDDEN 0

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

@@ -40,7 +40,7 @@
 #include "../../core/linux/SDL_dbus.h"
 #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>
 #ifndef RTLD_DEFAULT
 #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)
 static bool checked_setname = false;
 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 int (*ppthread_setname_np)(pthread_t, const char *) = NULL;
 #endif
@@ -88,12 +88,12 @@ bool SDL_SYS_CreateThread(SDL_Thread *thread,
     pthread_attr_t type;
 
 // 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) {
         void *fn = dlsym(RTLD_DEFAULT, "pthread_setname_np");
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
         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;
 #endif
         checked_setname = true;
@@ -129,12 +129,12 @@ void SDL_SYS_SetupThread(const char *name)
 #endif
 
     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);
         if (ppthread_setname_np) {
 #if defined(SDL_PLATFORM_MACOS) || defined(SDL_PLATFORM_IOS)
             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) {
                 char namebuf[16]; // Limited to 16 char
                 SDL_strlcpy(namebuf, name, sizeof(namebuf));
@@ -171,6 +171,9 @@ void SDL_SYS_SetupThread(const char *name)
     pthread_sigmask(SIG_BLOCK, &mask, NULL);
 #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
     // Allow ourselves to be asynchronously cancelled
     {
@@ -178,6 +181,7 @@ void SDL_SYS_SetupThread(const char *name)
         pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldstate);
     }
 #endif
+#endif
 }
 
 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     "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)
 // EGL AND OpenGL ES support via ANGLE
 #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 UIKIT_bootstrap;
 extern VideoBootStrap Android_bootstrap;
+extern VideoBootStrap OPENHARMONY_bootstrap;
 extern VideoBootStrap PS2_bootstrap;
 extern VideoBootStrap PSP_bootstrap;
 extern VideoBootStrap VITA_bootstrap;

+ 3 - 0
src/video/SDL_video.c

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

+ 3 - 0
src/video/SDL_vulkan_internal.h

@@ -27,6 +27,9 @@
 #ifdef SDL_VIDEO_DRIVER_ANDROID
 #define VK_USE_PLATFORM_ANDROID_KHR
 #endif
+#ifdef SDL_VIDEO_DRIVER_OPENHARMONY
+#define VK_USE_PLATFORM_OHOS
+#endif
 #ifdef SDL_VIDEO_DRIVER_COCOA
 #define VK_USE_PLATFORM_METAL_EXT
 #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);
+

Some files were not shown because too many files changed in this diff