diff --git a/.gitignore b/.gitignore index 6ae2fd4100..b419000db2 100644 --- a/.gitignore +++ b/.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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cabec1374..d0095516e1 100644 --- a/CMakeLists.txt +++ b/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. diff --git a/build-scripts/harmonyos-tool.sh b/build-scripts/harmonyos-tool.sh new file mode 100755 index 0000000000..684b7241db --- /dev/null +++ b/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 [--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 + diff --git a/cmake/PreseedOpenHarmonyCache.cmake b/cmake/PreseedOpenHarmonyCache.cmake new file mode 100644 index 0000000000..b78c24aeb9 --- /dev/null +++ b/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() diff --git a/cmake/macros.cmake b/cmake/macros.cmake index e0df1669c2..6862dd7790 100644 --- a/cmake/macros.cmake +++ b/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 diff --git a/cmake/sdlcompilers.cmake b/cmake/sdlcompilers.cmake index 87b00bbd70..932f161d6c 100644 --- a/cmake/sdlcompilers.cmake +++ b/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) diff --git a/docs/README-harmonyos.md b/docs/README-harmonyos.md new file mode 100644 index 0000000000..d7b6f7a384 --- /dev/null +++ b/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. + diff --git a/docs/README-platforms.md b/docs/README-platforms.md index 4c305190af..4f9a7d6502 100644 --- a/docs/README-platforms.md +++ b/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) diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h index f369fde17f..3edde85a49 100644 --- a/include/SDL3/SDL_iostream.h +++ b/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 diff --git a/include/SDL3/SDL_keyboard.h b/include/SDL3/SDL_keyboard.h index da0a4185b0..ba31c92cad 100644 --- a/include/SDL3/SDL_keyboard.h +++ b/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. diff --git a/include/SDL3/SDL_main.h b/include/SDL3/SDL_main.h index 64daa42c39..baccece0e8 100644 --- a/include/SDL3/SDL_main.h +++ b/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 diff --git a/include/SDL3/SDL_main_impl.h b/include/SDL3/SDL_main_impl.h index bf5f583639..f5307ceb34 100644 --- a/include/SDL3/SDL_main_impl.h +++ b/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. */ diff --git a/include/SDL3/SDL_platform_defines.h b/include/SDL3/SDL_platform_defines.h index 4d13740073..db3d28af8c 100644 --- a/include/SDL3/SDL_platform_defines.h +++ b/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) /** diff --git a/include/SDL3/SDL_system.h b/include/SDL3/SDL_system.h index fb58b71c42..6cdc10e1a0 100644 --- a/include/SDL3/SDL_system.h +++ b/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. * diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index b85d0275d7..83318e3764 100644 --- a/include/SDL3/SDL_video.h +++ b/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" diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index 7c2f1162be..bd342ceb67 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/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 diff --git a/openharmony-project/.gitignore b/openharmony-project/.gitignore new file mode 100644 index 0000000000..d2ff20141c --- /dev/null +++ b/openharmony-project/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/openharmony-project/AppScope/app.json5 b/openharmony-project/AppScope/app.json5 new file mode 100644 index 0000000000..6435fba585 --- /dev/null +++ b/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" + } +} diff --git a/openharmony-project/AppScope/resources/base/element/string.json b/openharmony-project/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000..198f1e7b39 --- /dev/null +++ b/openharmony-project/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "app" + } + ] +} diff --git a/openharmony-project/AppScope/resources/base/media/background.png b/openharmony-project/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000..923f2b3f27 Binary files /dev/null and b/openharmony-project/AppScope/resources/base/media/background.png differ diff --git a/openharmony-project/AppScope/resources/base/media/foreground.png b/openharmony-project/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000000..eb9427585b Binary files /dev/null and b/openharmony-project/AppScope/resources/base/media/foreground.png differ diff --git a/openharmony-project/AppScope/resources/base/media/layered_image.json b/openharmony-project/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000..fb49920440 --- /dev/null +++ b/openharmony-project/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/openharmony-project/build-profile.json5 b/openharmony-project/build-profile.json5 new file mode 100644 index 0000000000..f5b4a7bf01 --- /dev/null +++ b/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" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/openharmony-project/code-linter.json5 b/openharmony-project/code-linter.json5 new file mode 100644 index 0000000000..073990fa45 --- /dev/null +++ b/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" + } +} \ No newline at end of file diff --git a/openharmony-project/entry/.gitignore b/openharmony-project/entry/.gitignore new file mode 100644 index 0000000000..e2713a2779 --- /dev/null +++ b/openharmony-project/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/openharmony-project/entry/build-profile.json5 b/openharmony-project/entry/build-profile.json5 new file mode 100644 index 0000000000..18b39a1080 --- /dev/null +++ b/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" + }, + ] +} \ No newline at end of file diff --git a/openharmony-project/entry/hvigorfile.ts b/openharmony-project/entry/hvigorfile.ts new file mode 100644 index 0000000000..b0e3a1ab98 --- /dev/null +++ b/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. */ +} \ No newline at end of file diff --git a/openharmony-project/entry/obfuscation-rules.txt b/openharmony-project/entry/obfuscation-rules.txt new file mode 100644 index 0000000000..b78c7692ce --- /dev/null +++ b/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 \ No newline at end of file diff --git a/openharmony-project/entry/oh-package-lock.json5 b/openharmony-project/entry/oh-package-lock.json5 new file mode 100644 index 0000000000..7c36300c32 --- /dev/null +++ b/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" + } + } +} \ No newline at end of file diff --git a/openharmony-project/entry/oh-package.json5 b/openharmony-project/entry/oh-package.json5 new file mode 100644 index 0000000000..a423c238ae --- /dev/null +++ b/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", + } +} diff --git a/openharmony-project/entry/src/main/cpp/CMakeLists.txt b/openharmony-project/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..297e41036c --- /dev/null +++ b/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) diff --git a/openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts b/openharmony-project/entry/src/main/cpp/types/libSDL3/Index.d.ts new file mode 100644 index 0000000000..7589256fa7 --- /dev/null +++ b/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; diff --git a/openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 b/openharmony-project/entry/src/main/cpp/types/libSDL3/oh-package.json5 new file mode 100644 index 0000000000..aa0d1e5908 --- /dev/null +++ b/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." +} + diff --git a/openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets b/openharmony-project/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000..5c65a1e74a --- /dev/null +++ b/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.'); + } + } +} + diff --git a/openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/openharmony-project/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000..8e4de99282 --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/ets/pages/Index.ets b/openharmony-project/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000..af12664496 --- /dev/null +++ b/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%') + } +} diff --git a/openharmony-project/entry/src/main/module.json5 b/openharmony-project/entry/src/main/module.json5 new file mode 100644 index 0000000000..ebc08bdcc7 --- /dev/null +++ b/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" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/element/color.json b/openharmony-project/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000..3c712962da --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/element/float.json b/openharmony-project/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000..33ea22304f --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/openharmony-project/entry/src/main/resources/base/element/string.json b/openharmony-project/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000..6e1a147355 --- /dev/null +++ b/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" + } + ] +} + diff --git a/openharmony-project/entry/src/main/resources/base/media/background.png b/openharmony-project/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000..923f2b3f27 Binary files /dev/null and b/openharmony-project/entry/src/main/resources/base/media/background.png differ diff --git a/openharmony-project/entry/src/main/resources/base/media/foreground.png b/openharmony-project/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000..97014d3e10 Binary files /dev/null and b/openharmony-project/entry/src/main/resources/base/media/foreground.png differ diff --git a/openharmony-project/entry/src/main/resources/base/media/layered_image.json b/openharmony-project/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000..fb49920440 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/media/startIcon.png b/openharmony-project/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000..205ad8b5a8 Binary files /dev/null and b/openharmony-project/entry/src/main/resources/base/media/startIcon.png differ diff --git a/openharmony-project/entry/src/main/resources/base/profile/backup_config.json b/openharmony-project/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000..78f40ae7c4 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/openharmony-project/entry/src/main/resources/base/profile/main_pages.json b/openharmony-project/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000..1898d94f58 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/openharmony-project/entry/src/main/resources/dark/element/color.json b/openharmony-project/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000..79b11c2747 --- /dev/null +++ b/openharmony-project/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/openharmony-project/hvigor/hvigor-config.json5 b/openharmony-project/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000..4481d3121f --- /dev/null +++ b/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*/ + } +} diff --git a/openharmony-project/hvigorfile.ts b/openharmony-project/hvigorfile.ts new file mode 100644 index 0000000000..47113e2e36 --- /dev/null +++ b/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. */ +} \ No newline at end of file diff --git a/openharmony-project/oh-package-lock.json5 b/openharmony-project/oh-package-lock.json5 new file mode 100644 index 0000000000..c5a91ec3c9 --- /dev/null +++ b/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" + } + } +} \ No newline at end of file diff --git a/openharmony-project/oh-package.json5 b/openharmony-project/oh-package.json5 new file mode 100644 index 0000000000..01177cc612 --- /dev/null +++ b/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" + } +} diff --git a/src/SDL.c b/src/SDL.c index 0201ad3e17..72c4d77b5f 100644 --- a/src/SDL.c +++ b/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(); diff --git a/src/SDL_log.c b/src/SDL_log.c index 6295fc68ef..fdae94525a 100644 --- a/src/SDL_log.c +++ b/src/SDL_log.c @@ -40,6 +40,10 @@ #include #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include +#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. diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index 43a71e6532..57121a77da 100644 --- a/src/audio/SDL_audio.c +++ b/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 diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h index df22b84cbc..76b8443f28 100644 --- a/src/audio/SDL_sysaudio.h +++ b/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_ diff --git a/src/audio/ohaudio/SDL_ohaudio.c b/src/audio/ohaudio/SDL_ohaudio.c new file mode 100644 index 0000000000..1bfcdd59df --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudio.c @@ -0,0 +1,648 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 +#include + +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 diff --git a/src/audio/ohaudio/SDL_ohaudio.h b/src/audio/ohaudio/SDL_ohaudio.h new file mode 100644 index 0000000000..8f8dcaf51c --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudio.h @@ -0,0 +1,38 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_ diff --git a/src/audio/ohaudio/SDL_ohaudiofuncs.h b/src/audio/ohaudio/SDL_ohaudiofuncs.h new file mode 100644 index 0000000000..b09af8ab69 --- /dev/null +++ b/src/audio/ohaudio/SDL_ohaudiofuncs.h @@ -0,0 +1,62 @@ +/* + Simple DirectMedia Layer + Copyright , (C) 1997-2026 Sam Lantinga + + 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 diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index 02b8f530a0..a0cd468fe2 100644 --- a/src/camera/SDL_camera.c +++ b/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 diff --git a/src/camera/SDL_syscamera.h b/src/camera/SDL_syscamera.h index 40c4ae9fe8..4b4c5be45c 100644 --- a/src/camera/SDL_syscamera.h +++ b/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; diff --git a/src/camera/openharmony/SDL_camera_openharmony.c b/src/camera/openharmony/SDL_camera_openharmony.c new file mode 100644 index 0000000000..12fe3cdd5b --- /dev/null +++ b/src/camera/openharmony/SDL_camera_openharmony.c @@ -0,0 +1,775 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/src/core/SDL_core_unsupported.c b/src/core/SDL_core_unsupported.c index 7f9dbbe0dc..6a8fc6f309 100644 --- a/src/core/SDL_core_unsupported.c +++ b/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 + diff --git a/src/core/SDL_core_unsupported.h b/src/core/SDL_core_unsupported.h index d100836019..730503a5f0 100644 --- a/src/core/SDL_core_unsupported.h +++ b/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 + diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index 01074612e9..bb5f06eac1 100644 --- a/src/core/android/SDL_android.h +++ b/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); diff --git a/src/core/openharmony/SDL_openharmony.c b/src/core/openharmony/SDL_openharmony.c new file mode 100644 index 0000000000..a8b6872c45 --- /dev/null +++ b/src/core/openharmony/SDL_openharmony.c @@ -0,0 +1,1207 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 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: 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 + +// !!! 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 + +#include +#include +#include +#include +#include +#include +#include + +// !!! FIXME: which of these headers do we actually need? +#include +#include +#include +#include +#include + +#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 + diff --git a/src/core/openharmony/SDL_openharmony.h b/src/core/openharmony/SDL_openharmony.h new file mode 100644 index 0000000000..a3624f7310 --- /dev/null +++ b/src/core/openharmony/SDL_openharmony.h @@ -0,0 +1,66 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 diff --git a/src/cpuinfo/SDL_cpuinfo.c b/src/cpuinfo/SDL_cpuinfo.c index d58c150317..d343592848 100644 --- a/src/cpuinfo/SDL_cpuinfo.c +++ b/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(); diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports index 5d5df5d17c..3ff5e7c637 100644 --- a/src/dynapi/SDL_dynapi.exports +++ b/src/dynapi/SDL_dynapi.exports @@ -1311,3 +1311,6 @@ _SDL_SetRenderViewportFloat _SDL_GetRenderViewportFloat _SDL_SetRenderClipRectFloat _SDL_GetRenderClipRectFloat +_SDL_GetOpenHarmonySDKVersion +_SDL_GetOpenHarmonyInternalStoragePath +_SDL_RequestOpenHarmonyPermission diff --git a/src/dynapi/SDL_dynapi.h b/src/dynapi/SDL_dynapi.h index 2713e3f464..04c310ce29 100644 --- a/src/dynapi/SDL_dynapi.h +++ b/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 diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 63864e372b..f9b5a748cb 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/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: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index d8deffc20e..f6ce69d83d 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/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 diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index cd2fc200a2..2eeb1f3879 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/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) diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c index 53508fd53f..796ca3dbde 100644 --- a/src/events/SDL_keyboard.c +++ b/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); diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c index bde66cdae5..3c065acadd 100644 --- a/src/filesystem/SDL_filesystem.c +++ b/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 diff --git a/src/filesystem/openharmony/SDL_sysfilesystem.c b/src/filesystem/openharmony/SDL_sysfilesystem.c new file mode 100644 index 0000000000..5c657a7757 --- /dev/null +++ b/src/filesystem/openharmony/SDL_sysfilesystem.c @@ -0,0 +1,78 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 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 +#include + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// 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 diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c index 41dbf63344..b473dfbfc7 100644 --- a/src/filesystem/posix/SDL_sysfsops.c +++ b/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 == '/') { diff --git a/src/io/SDL_iostream.c b/src/io/SDL_iostream.c index 60caf33166..3382201b04 100644 --- a/src/io/SDL_iostream.c +++ b/src/io/SDL_iostream.c @@ -69,6 +69,11 @@ struct SDL_IOStream #include "../core/android/SDL_android.h" #endif +#ifdef SDL_PLATFORM_OPENHARMONY +#include +#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 diff --git a/src/locale/openharmony/SDL_syslocale.c b/src/locale/openharmony/SDL_syslocale.c new file mode 100644 index 0000000000..c7a9edb543 --- /dev/null +++ b/src/locale/openharmony/SDL_syslocale.c @@ -0,0 +1,36 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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; +} + diff --git a/src/locale/psp/SDL_syslocale.c b/src/locale/psp/SDL_syslocale.c index b6ec4f70dd..3a3975be27 100644 --- a/src/locale/psp/SDL_syslocale.c +++ b/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: */ diff --git a/src/main/SDL_main_callbacks.c b/src/main/SDL_main_callbacks.c index 757baef0ca..3b4a206831 100644 --- a/src/main/SDL_main_callbacks.c +++ b/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; } diff --git a/src/main/generic/SDL_sysmain_callbacks.c b/src/main/generic/SDL_sysmain_callbacks.c index 425e79077c..3ac645411d 100644 --- a/src/main/generic/SDL_sysmain_callbacks.c +++ b/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; diff --git a/src/main/openharmony/SDL_sysmain_callbacks.c b/src/main/openharmony/SDL_sysmain_callbacks.c new file mode 100644 index 0000000000..520ab5143f --- /dev/null +++ b/src/main/openharmony/SDL_sysmain_callbacks.c @@ -0,0 +1,60 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + diff --git a/src/main/openharmony/SDL_sysmain_runapp.c b/src/main/openharmony/SDL_sysmain_runapp.c new file mode 100644 index 0000000000..7feca8b80d --- /dev/null +++ b/src/main/openharmony/SDL_sysmain_runapp.c @@ -0,0 +1,102 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 +#include +#include +#include +#include + +#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 + diff --git a/src/misc/android/SDL_sysurl.c b/src/misc/android/SDL_sysurl.c index 8ed17a4d42..c3dcdc28bb 100644 --- a/src/misc/android/SDL_sysurl.c +++ b/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 + diff --git a/src/misc/openharmony/SDL_sysurl.c b/src/misc/openharmony/SDL_sysurl.c new file mode 100644 index 0000000000..ae3d2f3f7a --- /dev/null +++ b/src/misc/openharmony/SDL_sysurl.c @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + diff --git a/src/power/SDL_power.c b/src/power/SDL_power.c index 8308890934..b950236440 100644 --- a/src/power/SDL_power.c +++ b/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 diff --git a/src/power/SDL_syspower.h b/src/power/SDL_syspower.h index 6ef3e26b2c..ac54b7dc02 100644 --- a/src/power/SDL_syspower.h +++ b/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 *); diff --git a/src/power/openharmony/SDL_syspower.c b/src/power/openharmony/SDL_syspower.c new file mode 100644 index 0000000000..02caf37768 --- /dev/null +++ b/src/power/openharmony/SDL_syspower.c @@ -0,0 +1,46 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + +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 + diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index 51516b6ef6..302abe8945 100644 --- a/src/render/SDL_render.c +++ b/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 diff --git a/src/thread/pthread/SDL_systhread.c b/src/thread/pthread/SDL_systhread.c index eb13d02490..13b3ad0e0a 100644 --- a/src/thread/pthread/SDL_systhread.c +++ b/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 #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) diff --git a/src/video/SDL_egl.c b/src/video/SDL_egl.c index d5713ac55a..686931fe79 100644 --- a/src/video/SDL_egl.c +++ b/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" diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index 76d924c25d..8b71f803fa 100644 --- a/src/video/SDL_sysvideo.h +++ b/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; diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 4c8611828d..890d3637a0 100644 --- a/src/video/SDL_video.c +++ b/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 diff --git a/src/video/SDL_vulkan_internal.h b/src/video/SDL_vulkan_internal.h index 4a828b90df..e60f3aae69 100644 --- a/src/video/SDL_vulkan_internal.h +++ b/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 diff --git a/src/video/openharmony/SDL_openharmonyclipboard.c b/src/video/openharmony/SDL_openharmonyclipboard.c new file mode 100644 index 0000000000..5927d187e3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyclipboard.c @@ -0,0 +1,220 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 +#include +#include +#include + +#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 diff --git a/src/video/openharmony/SDL_openharmonyclipboard.h b/src/video/openharmony/SDL_openharmonyclipboard.h new file mode 100644 index 0000000000..00d9fefeee --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyclipboard.h @@ -0,0 +1,34 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_ diff --git a/src/video/openharmony/SDL_openharmonyevents.c b/src/video/openharmony/SDL_openharmonyevents.c new file mode 100644 index 0000000000..311a516b07 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyevents.c @@ -0,0 +1,574 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + +#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, ×tamp); + + 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 diff --git a/src/video/openharmony/SDL_openharmonyevents.h b/src/video/openharmony/SDL_openharmonyevents.h new file mode 100644 index 0000000000..ee0f9d1bd3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyevents.h @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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); diff --git a/src/video/openharmony/SDL_openharmonykeyboard.c b/src/video/openharmony/SDL_openharmonykeyboard.c new file mode 100644 index 0000000000..c26a9f130d --- /dev/null +++ b/src/video/openharmony/SDL_openharmonykeyboard.c @@ -0,0 +1,93 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + diff --git a/src/video/openharmony/SDL_openharmonykeyboard.h b/src/video/openharmony/SDL_openharmonykeyboard.h new file mode 100644 index 0000000000..3de748653a --- /dev/null +++ b/src/video/openharmony/SDL_openharmonykeyboard.h @@ -0,0 +1,29 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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); + diff --git a/src/video/openharmony/SDL_openharmonymouse.c b/src/video/openharmony/SDL_openharmonymouse.c new file mode 100644 index 0000000000..178d257409 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonymouse.c @@ -0,0 +1,76 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 "SDL_openharmonymouse.h" + +#include "../../events/SDL_mouse_c.h" + +#include "../../core/openharmony/SDL_openharmony.h" + +static bool OPENHARMONY_ShowCursor(SDL_Cursor *cursor) +{ +return false; +#if 0 + if (!cursor) { + cursor = OPENHARMONY_CreateEmptyCursor(); + } + if (cursor) { + SDL_CursorData *data = cursor->internal; + if (data->custom_cursor) { + if (!OPENHARMONY_JNI_SetCustomCursor(data->custom_cursor)) { + return SDL_Unsupported(); + } + } else { + if (!OPENHARMONY_JNI_SetSystemCursor(data->system_cursor)) { + return SDL_Unsupported(); + } + } + return true; + } else { + // SDL error set inside OPENHARMONY_CreateEmptyCursor() + return false; + } +#endif +} + +void OPENHARMONY_InitMouse(void) +{ + SDL_Mouse *mouse = SDL_GetMouse(); +//!!! FIXME: mouse->CreateCursor = OPENHARMONY_CreateCursor; +//!!! FIXME: mouse->CreateSystemCursor = OPENHARMONY_CreateSystemCursor; + mouse->ShowCursor = OPENHARMONY_ShowCursor; +//!!! FIXME: mouse->FreeCursor = OPENHARMONY_FreeCursor; +//!!! FIXME: mouse->SetRelativeMouseMode = OPENHARMONY_SetRelativeMouseMode; + +//!!! FIXME: SDL_SetDefaultCursor(OPENHARMONY_CreateDefaultCursor()); +} + +void OPENHARMONY_QuitMouse(void) +{ +//!!! FIXME: OPENHARMONY_DestroyEmptyCursor(); +} + +#endif + diff --git a/src/video/openharmony/SDL_openharmonymouse.h b/src/video/openharmony/SDL_openharmonymouse.h new file mode 100644 index 0000000000..f00266ffd2 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonymouse.h @@ -0,0 +1,30 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_openharmonymouse_h_ +#define SDL_openharmonymouse_h_ + +#include "SDL_openharmonyvideo.h" + +extern void OPENHARMONY_InitMouse(void); +extern void OPENHARMONY_QuitMouse(void); + +#endif // SDL_openharmonymouse_h_ diff --git a/src/video/openharmony/SDL_openharmonyopengl.c b/src/video/openharmony/SDL_openharmonyopengl.c new file mode 100644 index 0000000000..278c694232 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyopengl.c @@ -0,0 +1,79 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_VIDEO_DRIVER_OPENHARMONY) && defined(SDL_VIDEO_OPENGL_EGL) + +// OpenHarmony/HarmonyOS SDL video driver implementation + +#include "../SDL_egl_c.h" +#include "SDL_openharmonywindow.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonyopengl.h" +#include "../../core/openharmony/SDL_openharmony.h" + +bool OPENHARMONY_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context) +{ + if (window && context) { + return SDL_EGL_MakeCurrent(_this, window->internal->egl_surface, context); + } else { + return SDL_EGL_MakeCurrent(_this, NULL, NULL); + } +} + +SDL_GLContext OPENHARMONY_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_GLContext result; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //if (!OPENHARMONY_WaitActiveAndLockActivity()) { + // return NULL; + //} + + result = SDL_EGL_CreateContext(_this, window->internal->egl_surface); + + //OPENHARMONY_UnlockActivityMutex(); + + return result; +} + +bool OPENHARMONY_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + bool result; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //OPENHARMONY_LockActivityMutex(); + + result = SDL_EGL_SwapBuffers(_this, window->internal->egl_surface); + + //OPENHARMONY_UnlockActivityMutex(); + + return result; +} + +bool OPENHARMONY_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + return SDL_EGL_LoadLibrary(_this, path, EGL_DEFAULT_DISPLAY); +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyopengl.h b/src/video/openharmony/SDL_openharmonyopengl.h new file mode 100644 index 0000000000..8bcea96693 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyopengl.h @@ -0,0 +1,31 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_openharmonyopengl_h_ +#define SDL_openharmonyopengl_h_ + +extern SDL_GLContext OPENHARMONY_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window); +extern bool OPENHARMONY_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context); +extern bool OPENHARMONY_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool OPENHARMONY_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path); + +#endif // SDL_openharmonyopengl_h_ diff --git a/src/video/openharmony/SDL_openharmonyvideo.c b/src/video/openharmony/SDL_openharmonyvideo.c new file mode 100644 index 0000000000..63ab3db5d9 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvideo.c @@ -0,0 +1,280 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 + +// HarmonyOS/OpenHarmony SDL video driver implementation + +#include + +// !!! 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_ohosvideo_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohosvideo_OH_MAX_TOUCH_POINTS_NUMBER +#include + +// !!! FIXME: move stuff in from Android code + +#include "../SDL_sysvideo.h" +#include "../SDL_pixels_c.h" +#include "../../events/SDL_events_c.h" +#include "../../events/SDL_windowevents_c.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyopengl.h" +#include "SDL_openharmonyclipboard.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonykeyboard.h" +#include "SDL_openharmonymouse.h" +//#include "SDL_openharmonytouch.h" +#include "SDL_openharmonywindow.h" +#include "SDL_openharmonyvulkan.h" +//#include "SDL_openharmonymessagebox.h" + +#define OPENHARMONY_VID_DRIVER_NAME "openharmony" + +#include "../SDL_egl_c.h" +#define OPENHARMONY_GLES_GetProcAddress SDL_EGL_GetProcAddressInternal +#define OPENHARMONY_GLES_UnloadLibrary SDL_EGL_UnloadLibrary +#define OPENHARMONY_GLES_SetSwapInterval SDL_EGL_SetSwapInterval +#define OPENHARMONY_GLES_GetSwapInterval SDL_EGL_GetSwapInterval +#define OPENHARMONY_GLES_DestroyContext SDL_EGL_DestroyContext + + +static OH_NativeXComponent *native_xcomponent = NULL; +static void *native_window = NULL; + +void SDL_OpenHarmonyGetNativeWindowPointers(void **xcomponent, void **window) +{ + *xcomponent = (void *) native_xcomponent; + *window = (void *) native_window; +} + +void SDL_OpenHarmonyVideoSurfaceDestroyed(void *component, void *window) +{ + SDL_assert(native_xcomponent == ((OH_NativeXComponent *) component)); // right now we assume one surface, one window. + native_xcomponent = NULL; + native_window = NULL; +} + +void SDL_OpenHarmonyVideoSurfaceChanged(void *component, void *window) +{ + SDL_assert(native_xcomponent == ((OH_NativeXComponent *) component)); // right now we assume one surface, one window. + uint64_t w, h; + OH_NativeXComponent_GetXComponentSize(native_xcomponent, native_window, &w, &h); + SDL_SendWindowEvent(OPENHARMONY_Window, SDL_EVENT_WINDOW_RESIZED, (int) w, (int) h); +} + +void SDL_OpenHarmonyVideoSurfaceCreated(void *component, void *window) +{ + SDL_assert(native_xcomponent == NULL); // right now we assume one surface, one window. + native_xcomponent = (OH_NativeXComponent *) component; + native_window = window; +} + +static bool OPENHARMONY_SuspendScreenSaver(SDL_VideoDevice *_this) +{ + return SDL_OpenHarmonyChangeScreenSaver(!_this->suspend_screensaver); +} + +static bool OPENHARMONY_VideoInit(SDL_VideoDevice *_this) +{ + SDL_VideoData *videodata = _this->internal; + + videodata->isPaused = false; + videodata->isPausing = false; + + // !!! FIXME: eventually we'll want to enumerate displays, since you can probably plug in something + // !!! FIXME: through a USB-C to HDMI adapter, or maybe there will be separate "outer" screens vs + // !!! FIXME: an internal foldable one, but for now let's just get _something_ on _any_ display. + uint64_t dispid64 = 0; + if (OH_NativeDisplayManager_GetDefaultDisplayId(&dispid64) != DISPLAY_MANAGER_OK) { + return SDL_SetError("Couldn't get default display id"); + } + const uint32_t dispid = (uint32_t) dispid64; + + NativeDisplayManager_DisplayInfo *dispinfo = NULL; + if (OH_NativeDisplayManager_CreateDisplayById(dispid, &dispinfo) != DISPLAY_MANAGER_OK) { // (should really be called "CreateDisplayInfo", not "CreateDisplay") + return SDL_SetError("Couldn't get default display info"); + } + + SDL_DisplayMode mode; + SDL_zero(mode); + mode.format = SDL_PIXELFORMAT_BGRA8888; // !!! FIXME + mode.w = (int) dispinfo->physicalWidth; + mode.h = (int) dispinfo->physicalHeight; + mode.refresh_rate = (float) dispinfo->refreshRate; + mode.pixel_density = 1.0f; + + const NativeDisplayManager_Rotation rotation = dispinfo->rotation; + const NativeDisplayManager_Orientation orientation = dispinfo->orientation; + + OH_NativeDisplayManager_DestroyDisplay(dispinfo); + + const SDL_DisplayID displayID = SDL_AddBasicVideoDisplay(&mode); + if (displayID == 0) { + return false; + } + + SDL_VideoDisplay *display = SDL_GetVideoDisplay(displayID); + + switch (orientation) { + case DISPLAY_MANAGER_PORTRAIT: display->natural_orientation = SDL_ORIENTATION_PORTRAIT; break; + case DISPLAY_MANAGER_LANDSCAPE: display->natural_orientation = SDL_ORIENTATION_LANDSCAPE; break; + case DISPLAY_MANAGER_PORTRAIT_INVERTED: display->natural_orientation = SDL_ORIENTATION_PORTRAIT_FLIPPED; break; + case DISPLAY_MANAGER_LANDSCAPE_INVERTED: display->natural_orientation = SDL_ORIENTATION_LANDSCAPE_FLIPPED; break; + default: display->natural_orientation = SDL_ORIENTATION_UNKNOWN; break; + } + + // !!! FIXME: this is probably wrong, I think on OpenHarmony phones/tablets, these are setting both orientation and rotation, and this works out because most people are launching apps while holding the phone in portrait mode (0 rotation). + // !!! FIXME: if I'm right, we should decide if this is a phone/tablet screen and just set the natural orientation to portrait and then use this code to calculate current orientation. + if (rotation == DISPLAY_MANAGER_ROTATION_90) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_PORTRAIT, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_LANDSCAPE_FLIPPED, SDL_ORIENTATION_LANDSCAPE }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else if (rotation == DISPLAY_MANAGER_ROTATION_180) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_LANDSCAPE_FLIPPED, SDL_ORIENTATION_LANDSCAPE, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_PORTRAIT }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else if (rotation == DISPLAY_MANAGER_ROTATION_270) { // rotations are clockwise on OpenHarmony. + static const SDL_DisplayOrientation rotated[5] = { SDL_ORIENTATION_UNKNOWN, SDL_ORIENTATION_PORTRAIT_FLIPPED, SDL_ORIENTATION_PORTRAIT, SDL_ORIENTATION_LANDSCAPE, SDL_ORIENTATION_LANDSCAPE_FLIPPED }; + SDL_assert(((int) display->natural_orientation) < SDL_arraysize(rotated)); + display->current_orientation = rotated[(int) display->natural_orientation]; + } else { + display->current_orientation = display->natural_orientation; + } + + display->content_scale = mode.pixel_density; + + // !!! FIXME: look at SDL_OnApplicationDidChangeStatusBarOrientation() and do something similar. + + +// !!! FIXME +#if 0 + OPENHARMONY_InitTouch(); + OPENHARMONY_InitMouse(); +#endif + OPENHARMONY_InitClipboard(_this); + + // We're done! + return true; +} + +void OPENHARMONY_VideoQuit(SDL_VideoDevice *_this) +{ + OPENHARMONY_QuitClipboard(_this); + +// !!! FIXME +#if 0 + OPENHARMONY_QuitMouse(); + OPENHARMONY_QuitTouch(); +#endif +} + +static void OPENHARMONY_DeleteDevice(SDL_VideoDevice *device) +{ + SDL_free(device->internal); + SDL_free(device); +} + +static SDL_VideoDevice *OPENHARMONY_CreateDevice(void) +{ + SDL_VideoDevice *device; + SDL_VideoData *data; + + // Initialize all variables that we clean on shutdown + device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); + if (!device) { + return NULL; + } + + data = (SDL_VideoData *)SDL_calloc(1, sizeof(SDL_VideoData)); + if (!data) { + SDL_free(device); + return NULL; + } + + device->internal = data; + device->system_theme = SDL_GetOpenHarmonySystemTheme(); + + // Set the function pointers + device->VideoInit = OPENHARMONY_VideoInit; + device->VideoQuit = OPENHARMONY_VideoQuit; + + device->CreateSDLWindow = OPENHARMONY_CreateWindow; + device->SetWindowTitle = OPENHARMONY_SetWindowTitle; + device->SetWindowFullscreen = OPENHARMONY_SetWindowFullscreen; + device->MinimizeWindow = OPENHARMONY_MinimizeWindow; + device->SetWindowResizable = OPENHARMONY_SetWindowResizable; + device->DestroyWindow = OPENHARMONY_DestroyWindow; + + device->free = OPENHARMONY_DeleteDevice; + + // GL pointers +#ifdef SDL_VIDEO_OPENGL_EGL + device->GL_LoadLibrary = OPENHARMONY_GLES_LoadLibrary; + device->GL_GetProcAddress = OPENHARMONY_GLES_GetProcAddress; + device->GL_UnloadLibrary = OPENHARMONY_GLES_UnloadLibrary; + device->GL_CreateContext = OPENHARMONY_GLES_CreateContext; + device->GL_MakeCurrent = OPENHARMONY_GLES_MakeCurrent; + device->GL_SetSwapInterval = OPENHARMONY_GLES_SetSwapInterval; + device->GL_GetSwapInterval = OPENHARMONY_GLES_GetSwapInterval; + device->GL_SwapWindow = OPENHARMONY_GLES_SwapWindow; + device->GL_DestroyContext = OPENHARMONY_GLES_DestroyContext; +#endif + +#ifdef SDL_VIDEO_VULKAN + device->Vulkan_LoadLibrary = OPENHARMONY_Vulkan_LoadLibrary; + device->Vulkan_UnloadLibrary = OPENHARMONY_Vulkan_UnloadLibrary; + device->Vulkan_GetInstanceExtensions = OPENHARMONY_Vulkan_GetInstanceExtensions; + device->Vulkan_CreateSurface = OPENHARMONY_Vulkan_CreateSurface; + device->Vulkan_DestroySurface = OPENHARMONY_Vulkan_DestroySurface; +#endif + + // Screensaver + device->SuspendScreenSaver = OPENHARMONY_SuspendScreenSaver; + + device->PumpEvents = OPENHARMONY_PumpEvents; + + // Screen keyboard + device->HasScreenKeyboardSupport = OPENHARMONY_HasScreenKeyboardSupport; + device->ShowScreenKeyboard = OPENHARMONY_ShowScreenKeyboard; + device->HideScreenKeyboard = OPENHARMONY_HideScreenKeyboard; + + // Clipboard + device->GetTextMimeTypes = OPENHARMONY_GetTextMimeTypes; + device->SetClipboardText = OPENHARMONY_SetClipboardText; + device->GetClipboardText = OPENHARMONY_GetClipboardText; + device->HasClipboardText = OPENHARMONY_HasClipboardText; + + device->device_caps = VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + + return device; +} + +VideoBootStrap OPENHARMONY_bootstrap = { + OPENHARMONY_VID_DRIVER_NAME, "SDL OpenHarmony/HarmonyOS video driver", + OPENHARMONY_CreateDevice, + /*!!! FIXME OPENHARMONY_ShowMessageBox*/ NULL, + false +}; + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonyvideo.h b/src/video/openharmony/SDL_openharmonyvideo.h new file mode 100644 index 0000000000..28f0d5901e --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvideo.h @@ -0,0 +1,47 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_openharmonyvideo_h_ +#define SDL_openharmonyvideo_h_ + +#include "../SDL_sysvideo.h" + +// these are fired from XComponent callbacks registered in src/core/openharmony/SDL_openharmony.c +extern void SDL_OpenHarmonyVideoSurfaceDestroyed(void *component, void *window); +extern void SDL_OpenHarmonyVideoSurfaceChanged(void *component, void *window); +extern void SDL_OpenHarmonyVideoSurfaceCreated(void *component, void *window); + +// Get the app's current native window. +extern void SDL_OpenHarmonyGetNativeWindowPointers(void **xcomponent, void **window); + +struct SDL_VideoData +{ + void *oh_pasteboard; + void *oh_pasteboard_observer; + bool clipboard_set; + +// !!! FIXME: Android stuff. + int isPaused; + int isPausing; +}; + +#endif // SDL_openharmonyvideo_h_ diff --git a/src/video/openharmony/SDL_openharmonyvulkan.c b/src/video/openharmony/SDL_openharmonyvulkan.c new file mode 100644 index 0000000000..3cafbd73e3 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvulkan.c @@ -0,0 +1,174 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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. +*/ + +/* + * Copied from the Android implementation, which was done by Mark Callow, + * www.edgewise-consulting.com, and originally based on Jacob Lifshay's SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_OPENHARMONY) + +#include "../SDL_vulkan_internal.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonywindow.h" + +#include "SDL_openharmonyvulkan.h" + + +bool OPENHARMONY_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + VkExtensionProperties *extensions = NULL; + Uint32 extensionCount = 0; + bool hasSurfaceExtension = false; + bool hasOHOSSurfaceExtension = false; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL; + + if (_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan already loaded"); + } + + // Load the Vulkan loader library + if (!path) { + path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY); + if (!path) { + path = "libvulkan.so"; + } + } + + SDL_SharedObject *lib = SDL_LoadObject(path); + _this->vulkan_config.loader_handle = lib; + if (!lib) { + return false; + } + + SDL_strlcpy(_this->vulkan_config.loader_path, path, sizeof (_this->vulkan_config.loader_path)); + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr) SDL_LoadFunction(lib, "vkGetInstanceProcAddr"); + if (!vkGetInstanceProcAddr) { + goto fail; + } + + _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr; + _this->vulkan_config.vkEnumerateInstanceExtensionProperties = (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)(VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"); + if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) { + goto fail; + } + + extensions = SDL_Vulkan_CreateInstanceExtensionsList((PFN_vkEnumerateInstanceExtensionProperties) _this->vulkan_config.vkEnumerateInstanceExtensionProperties, &extensionCount); + if (!extensions) { + goto fail; + } + + for (Uint32 i = 0; i < extensionCount; i++) { + if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasSurfaceExtension = true; + } else if (SDL_strcmp(VK_OHOS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasOHOSSurfaceExtension = true; + } + } + + SDL_free(extensions); + + if (!hasSurfaceExtension) { + SDL_SetError("Installed Vulkan doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } else if (!hasOHOSSurfaceExtension) { + SDL_SetError("Installed Vulkan doesn't implement the " VK_OHOS_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } + + return true; + +fail: + SDL_UnloadObject(_this->vulkan_config.loader_handle); + _this->vulkan_config.loader_handle = NULL; + return false; +} + +void OPENHARMONY_Vulkan_UnloadLibrary(SDL_VideoDevice *_this) +{ + if (_this->vulkan_config.loader_handle) { + SDL_UnloadObject(_this->vulkan_config.loader_handle); + _this->vulkan_config.loader_handle = NULL; + } +} + +char const * const *OPENHARMONY_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count) +{ + static const char *const extensionsForOpenHarmony[] = { + VK_KHR_SURFACE_EXTENSION_NAME, VK_OHOS_SURFACE_EXTENSION_NAME + }; + if (count) { + *count = SDL_arraysize(extensionsForOpenHarmony); + } + return extensionsForOpenHarmony; +} + +bool OPENHARMONY_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface) +{ + SDL_WindowData *windowData = window->internal; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr; + PFN_vkCreateSurfaceOHOS vkCreateSurfaceOHOS = (PFN_vkCreateSurfaceOHOS ) vkGetInstanceProcAddr(instance, "vkCreateSurfaceOHOS"); + VkSurfaceCreateInfoOHOS createInfo; + VkResult result; + + if (!_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan is not loaded"); + } + + if (!vkCreateSurfaceOHOS) { + return SDL_SetError(VK_OHOS_SURFACE_EXTENSION_NAME " extension is not enabled in the Vulkan instance."); + } + + SDL_zero(createInfo); + createInfo.sType = VK_STRUCTURE_TYPE_SURFACE_CREATE_INFO_OHOS; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.window = (OHNativeWindow *) windowData->native_window; + result = vkCreateSurfaceOHOS(instance, &createInfo, allocator, surface); + if (result != VK_SUCCESS) { + if (result == VK_ERROR_NATIVE_WINDOW_IN_USE_KHR) { + return SDL_SetError("vkCreateSurfaceOHOS failed: %s, was the window created with SDL_WINDOW_VULKAN?", SDL_Vulkan_GetResultString(result)); + } else { + return SDL_SetError("vkCreateSurfaceOHOS failed: %s", SDL_Vulkan_GetResultString(result)); + } + } + return true; +} + +void OPENHARMONY_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator) +{ + if (_this->vulkan_config.loader_handle) { + SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator); + } +} + +#endif + diff --git a/src/video/openharmony/SDL_openharmonyvulkan.h b/src/video/openharmony/SDL_openharmonyvulkan.h new file mode 100644 index 0000000000..1643fd76b6 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonyvulkan.h @@ -0,0 +1,52 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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. +*/ + +/* + * Copied from the Android implementation, which was done by Mark Callow, + * www.edgewise-consulting.com, and originally based on Jacob Lifshay's SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#ifndef SDL_openharmonyvulkan_h_ +#define SDL_openharmonyvulkan_h_ + +#include "../SDL_vulkan_internal.h" +#include "../SDL_sysvideo.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_OPENHARMONY) + +extern bool OPENHARMONY_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern void OPENHARMONY_Vulkan_UnloadLibrary(SDL_VideoDevice *_this); +extern char const * const *OPENHARMONY_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count); +extern bool OPENHARMONY_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface); +extern void OPENHARMONY_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator); + +#endif + +#endif // SDL_openharmonyvulkan_h_ diff --git a/src/video/openharmony/SDL_openharmonywindow.c b/src/video/openharmony/SDL_openharmonywindow.c new file mode 100644 index 0000000000..f462d9d74b --- /dev/null +++ b/src/video/openharmony/SDL_openharmonywindow.c @@ -0,0 +1,187 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_ohoswindow_OH_XCOMPONENT_ID_LEN_MAX +#define OH_MAX_TOUCH_POINTS_NUMBER sdl_ohoswindow_OH_MAX_TOUCH_POINTS_NUMBER +#include + +#include "../SDL_sysvideo.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_windowevents_c.h" +#include "../../core/openharmony/SDL_openharmony.h" + +#include "SDL_openharmonyvideo.h" +#include "SDL_openharmonyevents.h" +#include "SDL_openharmonywindow.h" + + +// Currently only one window +SDL_Window *OPENHARMONY_Window = NULL; + +bool OPENHARMONY_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) +{ + SDL_WindowData *data; + bool result = true; + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //if (!OPENHARMONY_WaitActiveAndLockActivity()) { + // return false; + //} + + if (OPENHARMONY_Window) { + result = SDL_SetError("SDL on OpenHarmony (currently) only supports one window"); + goto endfunction; + } + + void *voidxcomp, *native_window; + SDL_OpenHarmonyGetNativeWindowPointers(&voidxcomp, &native_window); + OH_NativeXComponent *native_xcomponent = (OH_NativeXComponent *) voidxcomp; + if (!native_xcomponent || !native_window) { + result = SDL_SetError("Don't have Native XComponent or Native Window"); + goto endfunction; + } + +#if 0 // !!! FIXME + // Set orientation + Android_JNI_SetOrientation(window->w, window->h, window->flags & SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS)); +#endif + + uint64_t w, h; + OH_NativeXComponent_GetXComponentSize(native_xcomponent, native_window, &w, &h); + + // Adjust the window data to match the screen + window->x = 0; + window->y = 0; + window->w = (int) w; + window->h = (int) h; + + // One window, it always has focus + SDL_SetMouseFocus(window); + SDL_SetKeyboardFocus(window); + + data = (SDL_WindowData *)SDL_calloc(1, sizeof(*data)); + if (!data) { + result = false; + goto endfunction; + } + + data->native_xcomponent = native_xcomponent; + data->native_window = native_window; + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_XCOMPONENT_POINTER, data->native_xcomponent); + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_WINDOW_POINTER, data->native_window); + + /* Do not create EGLSurface for Vulkan window since it will then make the window + incompatible with vkCreateAndroidSurfaceKHR */ +#ifdef SDL_VIDEO_OPENGL_EGL + if (window->flags & SDL_WINDOW_OPENGL) { + data->egl_surface = SDL_EGL_CreateSurface(_this, window, (NativeWindowType)data->native_window); + if (data->egl_surface == EGL_NO_SURFACE) { + SDL_free(data); + result = false; + goto endfunction; + } + } + SDL_SetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_OPENHARMONY_SURFACE_POINTER, data->egl_surface); +#endif + +#if 0 // !!! FIXME + SDL_SetWindowSafeAreaInsets(window, Android_SafeInsetLeft, Android_SafeInsetRight, Android_SafeInsetTop, Android_SafeInsetBottom); +#endif + + window->internal = data; + OPENHARMONY_Window = window; + +endfunction: + + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //Android_UnlockActivityMutex(); + + return result; +} + +void OPENHARMONY_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) +{ +#if 0 // !!! FIXME + Android_JNI_SetActivityTitle(window->title); +#endif +} + +SDL_FullscreenResult OPENHARMONY_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) +{ + if (window == OPENHARMONY_Window) { + const bool show_bars = (fullscreen == SDL_FULLSCREEN_OP_LEAVE); + if (!SDL_OpenHarmonyToggleSystemBars(show_bars, show_bars)) { + return SDL_FULLSCREEN_FAILED; + } + // !!! FIXME: make sure this causes an XComponent resize event, that we can pick up later. + return SDL_FULLSCREEN_PENDING; + } + return SDL_FULLSCREEN_FAILED; +} + +void OPENHARMONY_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ +#if 0 // !!! FIXME + Android_JNI_MinimizeWindow(); +#endif +} + +void OPENHARMONY_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable) +{ +#if 0 // !!! FIXME + // Set orientation + Android_JNI_SetOrientation(window->w, window->h, window->flags & SDL_WINDOW_RESIZABLE, SDL_GetHint(SDL_HINT_ORIENTATIONS)); +#endif +} + +void OPENHARMONY_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + // !!! FIXME: Android has locking here; maybe it isn't necessary for OpenHarmony? What happens if we're in a background thread? + //Android_LockActivityMutex(); + + if (window == OPENHARMONY_Window) { + OPENHARMONY_Window = NULL; + + if (window->internal) { + SDL_WindowData *data = window->internal; + +#ifdef SDL_VIDEO_OPENGL_EGL + if (data->egl_surface != EGL_NO_SURFACE) { + SDL_EGL_DestroySurface(_this, data->egl_surface); + } +#endif + + // the XComponent and native window live past the SDL_Window; it can be reused for future windows. + + SDL_free(window->internal); + window->internal = NULL; + } + } + + //Android_UnlockActivityMutex(); +} + +#endif // SDL_VIDEO_DRIVER_OPENHARMONY diff --git a/src/video/openharmony/SDL_openharmonywindow.h b/src/video/openharmony/SDL_openharmonywindow.h new file mode 100644 index 0000000000..07515eb428 --- /dev/null +++ b/src/video/openharmony/SDL_openharmonywindow.h @@ -0,0 +1,51 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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_openharmonywindow_h_ +#define SDL_openharmonywindow_h_ + +#include "../../core/openharmony/SDL_openharmony.h" +#include "../SDL_egl_c.h" + +extern bool OPENHARMONY_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); +extern void OPENHARMONY_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_FullscreenResult OPENHARMONY_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen); +extern void OPENHARMONY_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void OPENHARMONY_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable); + +extern void OPENHARMONY_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_Window *OPENHARMONY_Window; + +struct SDL_WindowData +{ +#ifdef SDL_VIDEO_OPENGL_EGL + EGLSurface egl_surface; + EGLContext egl_context; // We use this to preserve the context when losing focus + int has_swap_interval; // Save/Restore the swap interval / vsync + int swap_interval; +#endif + bool backup_done; + void *native_xcomponent; + void *native_window; +}; + +#endif // SDL_openharmonywindow_h_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9e79d58d52..8400152dc2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -167,7 +167,7 @@ function(add_sdl_test_executable TARGET) if(AST_NEEDS_RESOURCES) list(APPEND EXTRA_SOURCES ${RESOURCE_FILES}) endif() - if(ANDROID) + if(ANDROID OR OHOS) add_library(${TARGET} SHARED ${AST_SOURCES} ${EXTRA_SOURCES}) else() add_executable(${TARGET} ${AST_SOURCES} ${EXTRA_SOURCES}) @@ -489,7 +489,7 @@ if(EMSCRIPTEN) endif() add_sdl_test_executable(testbounds NONINTERACTIVE SOURCES testbounds.c NAME83 bounds) add_sdl_test_executable(testcustomcursor SOURCES testcustomcursor.c NAME83 custcurs) -add_sdl_test_executable(testvulkan SOURCES testvulkan.c NAME83 vulkan) +add_sdl_test_executable(testvulkan MAIN_CALLBACKS SOURCES testvulkan.c NAME83 vulkan) add_sdl_test_executable(testoffscreen SOURCES testoffscreen.c NAME83 offscrn) add_sdl_test_executable(testpopup SOURCES testpopup.c NAME83 popup) add_sdl_test_executable(testdialog SOURCES testdialog.c NAME83 dialog) diff --git a/test/testclipboard.c b/test/testclipboard.c index 8fa8756c9a..dc8e71135e 100644 --- a/test/testclipboard.c +++ b/test/testclipboard.c @@ -62,6 +62,10 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { switch (event->type) { + case SDL_EVENT_MOUSE_BUTTON_DOWN: + SDL_SetClipboardData(ClipboardDataCallback, NULL, NULL, mime_types, SDL_arraysize(mime_types)); + break; + case SDL_EVENT_KEY_DOWN: if (event->key.key == SDLK_ESCAPE) { return SDL_APP_SUCCESS; @@ -223,7 +227,7 @@ SDL_AppResult SDL_AppIterate(void *appstate) SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); float x = 4.0f; float y = 4.0f; - SDL_RenderDebugText(renderer, x, y, "Press Ctrl+C to copy content to the clipboard (Alt+C to clear)"); + SDL_RenderDebugText(renderer, x, y, "Press Ctrl+C (or tap/click) to copy content to the clipboard (Alt+C to clear)"); y += SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * 2; SDL_RenderDebugText(renderer, x, y, "Press Ctrl+P to set the primary selection text (Alt+P to clear)"); y += SDL_DEBUG_TEXT_FONT_CHARACTER_SIZE * 2; diff --git a/test/testsymbols.c b/test/testsymbols.c index 3b5591e6ac..165c4facea 100644 --- a/test/testsymbols.c +++ b/test/testsymbols.c @@ -34,6 +34,12 @@ extern SDL_DECLSPEC void SDLCALL SDL_SendAndroidMessage(void); extern SDL_DECLSPEC void SDLCALL SDL_ShowAndroidToast(void); #endif +#if !defined(SDL_PLATFORM_OPENHARMONY) +extern SDL_DECLSPEC int SDLCALL SDL_GetOpenHarmonySDKVersion(void); +extern SDL_DECLSPEC void SDLCALL SDL_GetOpenHarmonyInternalStoragePath(void); +extern SDL_DECLSPEC bool SDLCALL SDL_RequestOpenHarmonyPermission(void); +#endif + #if !defined(SDL_PLATFORM_GDK) extern SDL_DECLSPEC void SDLCALL SDL_GDKResumeGPU(void); extern SDL_DECLSPEC void SDLCALL SDL_GDKSuspendGPU(void); diff --git a/test/testurl.c b/test/testurl.c index a6fd9c5ec4..ef61eb2f62 100644 --- a/test/testurl.c +++ b/test/testurl.c @@ -68,7 +68,9 @@ int main(int argc, char **argv) while (!quit) { while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + tryOpenURL(url); + } else if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.key == SDLK_SPACE) { tryOpenURL(url); } else if (event.key.key == SDLK_ESCAPE) { diff --git a/test/testvulkan.c b/test/testvulkan.c index f924ef55d6..f57f194437 100644 --- a/test/testvulkan.c +++ b/test/testvulkan.c @@ -9,19 +9,26 @@ including commercial applications, and to alter it and redistribute it freely. */ -#include -#include +#define SDL_MAIN_USE_CALLBACKS 1 +#include #include +#include + +#include #if defined(SDL_PLATFORM_ANDROID) && defined(__ARM_EABI__) && !defined(__ARM_ARCH_7A__) -int main(int argc, char *argv[]) +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "No Vulkan support on this system"); - return 1; + return SDL_APP_FAILURE; } +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { return SDL_APP_FAILURE; } +SDL_AppResult SDL_AppIterate(void *appstate) { return SDL_APP_FAILURE; } +void SDL_AppQuit(void *appstate, SDL_AppResult result) {} + #else #define VK_NO_PROTOTYPES @@ -1144,19 +1151,18 @@ static bool render(void) return true; } -int main(int argc, char **argv) +static Uint32 frames; +static Uint64 then; + +SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) { - int done; const SDL_DisplayMode *mode; - SDL_Event event; - Uint64 then, now; - Uint32 frames; int dw, dh; /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); if (!state) { - return 1; + return SDL_APP_FAILURE; } /* Set Vulkan parameters */ @@ -1165,7 +1171,7 @@ int main(int argc, char **argv) if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) { SDLTest_CommonQuit(state); - return 1; + return SDL_APP_FAILURE; } mode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay()); @@ -1183,40 +1189,52 @@ int main(int argc, char **argv) /* Main render loop */ frames = 0; then = SDL_GetTicks(); - done = 0; - while (!done) { - /* Check for events */ - frames++; - while (SDL_PollEvent(&event)) { - /* Need to destroy the swapchain before the window created - * by SDL. - */ - if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { - destroySwapchainAndSwapchainSpecificStuff(true); - } - SDLTest_CommonEvent(state, &event, &done); - } - if (!done) { - int i; - for (i = 0; i < state->num_windows; ++i) { - if (state->windows[i]) { - vulkanContext = &vulkanContexts[i]; - render(); - } - } + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) +{ + int done = 0; + + /* Need to destroy the swapchain before the window created + * by SDL. + */ + if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + destroySwapchainAndSwapchainSpecificStuff(true); + } + + SDLTest_CommonEvent(state, event, &done); + return done ? SDL_APP_SUCCESS : SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppIterate(void *appstate) +{ + int i; + + frames++; + for (i = 0; i < state->num_windows; ++i) { + if (state->windows[i]) { + vulkanContext = &vulkanContexts[i]; + render(); } } + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void *appstate, SDL_AppResult result) +{ /* Print out some timing information */ - now = SDL_GetTicks(); - if (now > then) { - SDL_Log("%2.2f frames per second", ((double)frames * 1000) / (now - then)); + if (result != SDL_APP_FAILURE) { + const Uint64 now = SDL_GetTicks(); + if (now > then) { + SDL_Log("%2.2f frames per second", ((double)frames * 1000) / (now - then)); + } } shutdownVulkan(true); SDLTest_CommonQuit(state); - return 0; } #endif