diff --git a/.appveyor.yml b/.appveyor.yml index 933f517..db3c3a0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,6 +5,7 @@ image: branches: only: - master + - develop - /^v\d.*$/ - /^deploy-test(-.*)?$/ @@ -36,14 +37,14 @@ for: install: - git submodule update --init --recursive - sudo pip3 install protobuf - - brew install qt opus openssl@1.1 nasm sdl2 protobuf + - HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf - scripts/build-ffmpeg.sh build_script: - - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt" + - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt@5" - scripts/build-common.sh - cp -a build/gui/chiaki.app Chiaki.app - - /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg + - /usr/local/opt/qt@5/bin/macdeployqt Chiaki.app -dmg artifacts: - path: Chiaki.dmg diff --git a/.builds/android.yml b/.builds/android.yml index 7b64ab4..20cd88b 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -22,7 +22,7 @@ tasks: sudo docker run \ -v /home/build:/home/build \ -u $(id -u):$(id -g) \ - thestr4ng3r/android:f064ea6 \ + thestr4ng3r/android:90d826e \ /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab diff --git a/.builds/common.yml b/.builds/common.yml index 46b754f..b692b08 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -9,38 +9,47 @@ packages: - ninja - protoc - py3-protobuf + - py3-setuptools - opus-dev - qt5-qtbase-dev - qt5-qtsvg-dev - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - sdl2-static # this is gone on alpine edge so might be necessary to remove later - - docker + - podman - fuse + - udev + - argp-standalone artifacts: - chiaki.nro - Chiaki.AppImage tasks: - - start_docker: | - sudo service docker start - sudo chmod +s /usr/bin/docker # Yes, I know what I am doing - sudo service fuse start # Fuse for AppImages + - setup_podman: | + sudo rc-service udev start + sudo rc-service cgroups start + sudo rc-service fuse start # Fuse for AppImages + echo build:100000:65536 | sudo tee /etc/subuid + echo build:100000:65536 | sudo tee /etc/subgid + # https://www.kernel.org/doc/Documentation/networking/tuntap.txt + # for slirp4netns + sudo mkdir -p /dev/net + sudo mknod /dev/net/tun c 10 200 + sudo chmod 0666 /dev/net/tun - local_build_and_test: | cd chiaki - cmake -Bbuild -GNinja + cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON ninja -C build build/test/chiaki-unit - appimage: | cd chiaki - scripts/run-docker-build-appimage.sh + scripts/run-podman-build-appimage.sh cp appimage/Chiaki.AppImage ../Chiaki.AppImage - switch: | cd chiaki - scripts/switch/run-docker-build-chiaki.sh + scripts/switch/run-podman-build-chiaki.sh cp build_switch/switch/chiaki.nro ../chiaki.nro - bullseye: | cd chiaki - scripts/run-docker-build-bullseye.sh + scripts/run-podman-build-bullseye.sh diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index caa9016..72109b5 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,5 +1,5 @@ -image: freebsd/latest +image: freebsd/14.x sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,7 +7,8 @@ sources: packages: - cmake - protobuf - - py37-protobuf + - py311-setuptools # should not be needed with nanopb >= 0.4.9 + - py311-protobuf - opus - qt5-core - qt5-qmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index b8785ef..e1595ac 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/6.7 +image: openbsd/latest sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,6 +7,7 @@ sources: packages: - cmake - protobuf + - py3-setuptools # should not be needed with nanopb >= 0.4.9 - py3-protobuf - opus - qtbase diff --git a/.gitignore b/.gitignore index 0b9eccd..f30ef42 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ compile_commands.json chiaki.conf /appimage .cache/ +/*.app +/*.dmg diff --git a/.gitmodules b/.gitmodules index ac87125..250f8fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/google/oboe [submodule "switch/borealis"] path = switch/borealis - url = https://github.com/natinusala/borealis.git + url = https://git.sr.ht/~thestr4ng3r/borealis diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bddb7f..64caaee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ endif() tri_option(CHIAKI_ENABLE_FFMPEG_DECODER "Enable FFMPEG video decoder" ${CHIAKI_FFMPEG_DEFAULT}) tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) +option(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT "Fetch Mbed TLS instead of using system-provided libs" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) @@ -31,7 +32,7 @@ tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of s tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 2) -set(CHIAKI_VERSION_MINOR 0) +set(CHIAKI_VERSION_MINOR 2) set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) @@ -89,6 +90,20 @@ endif() if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + set(FETCHCONTENT_QUIET CACHE BOOL FALSE) + include(FetchContent) + set(ENABLE_TESTING CACHE INTERNAL OFF) + set(ENABLE_PROGRAMS CACHE INTERNAL OFF) + set(USE_SHARED_MBEDTLS_LIBRARY CACHE INTERNAL OFF) + FetchContent_Declare( + mbedtls + GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git + GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0 + GIT_PROGRESS TRUE + ) + FetchContent_MakeAvailable(mbedtls) + endif() endif() if(CHIAKI_ENABLE_FFMPEG_DECODER) @@ -135,21 +150,30 @@ if(CHIAKI_ENABLE_CLI) add_subdirectory(cli) endif() +if(CHIAKI_ENABLE_GUI AND CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) + find_package(SDL2 MODULE REQUIRED) +endif() + if(CHIAKI_ENABLE_SETSU) - find_package(Udev QUIET) - find_package(Evdev QUIET) - if(Udev_FOUND AND Evdev_FOUND) - set(CHIAKI_ENABLE_SETSU ON) - else() - if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) - message(FATAL_ERROR " -CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. -Keep in mind that setsu is only supported on Linux!") - endif() + if(CHIAKI_ENABLE_SETSU STREQUAL AUTO AND SDL2_FOUND AND (SDL2_VERSION_MINOR GREATER 0 OR SDL2_VERSION_PATCH GREATER_EQUAL 14)) + message(STATUS "SDL version ${SDL2_VERSION} is >= 2.0.14, disabling Setsu") set(CHIAKI_ENABLE_SETSU OFF) - endif() - if(CHIAKI_ENABLE_SETSU) - add_subdirectory(setsu) + else() + find_package(Udev QUIET) + find_package(Evdev QUIET) + if(Udev_FOUND AND Evdev_FOUND) + set(CHIAKI_ENABLE_SETSU ON) + else() + if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) + message(FATAL_ERROR " + CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. + Keep in mind that setsu is only supported on Linux!") + endif() + set(CHIAKI_ENABLE_SETSU OFF) + endif() + if(CHIAKI_ENABLE_SETSU) + add_subdirectory(setsu) + endif() endif() endif() @@ -160,7 +184,6 @@ else() endif() if(CHIAKI_ENABLE_GUI) - #add_subdirectory(setsu) add_subdirectory(gui) endif() diff --git a/README.md b/README.md index b84b73d..1a94b66 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,35 @@ [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/c81ogebvsmo43dd3?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play -for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. +for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. + +## Project Status and Contributing + +As all relevant features are implemented, this project is considered to be finished and in maintenance mode only. +No major updates are planned and contributions are only accepted in special cases such as security issues. +The objective is to keep a stable base and not break existing support for less mainstream platforms such as BSDs. + +**For a more active, fast moving and community-oriented project, refer +to [chiaki-ng](https://streetpea.github.io/chiaki-ng/) ("next generation"). +If you would like to contribute, this will likely also be the best place to do so.** ![Screenshot](assets/screenshot.png) -## Features - -Everything necessary for a full streaming session, including the initial -registration and wakeup of the console, is supported. -The following features however are yet to be implemented: -* Rumble -* Accelerometer/Gyroscope - ## Installing -You can either download a pre-built release (easier) or build Chiaki from source. +You can either download a pre-built release or build Chiaki from source. ### Downloading a Release -Builds are provided for Linux, Android, macOS and Windows. +Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows. You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x .AppImage`) and run it. -* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. +* **Android**: Install from [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. -* **Switch**: Follow README specific [instructions](./switch/README.md) +* **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card. ### Building from Source @@ -47,8 +49,8 @@ cmake .. make ``` -For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). -in +For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md) or [switch/](./switch/README.md) for Nintendo Switch. + ## Usage If your Console is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. @@ -71,13 +73,6 @@ Settings -> Remote Play -> Add Device, or on a PS5: Settings -> System -> Remote You can now double-click your Console in Chiaki's main window to start Remote Play. -## Joining the Community or Getting Help - -There are official groups for Chiaki on Telegram and IRC. They are bridged so you can join whichever you like: - -- **Telegram:** https://t.me/chiakitg -- **IRC:** #chiaki on irc.freenode.net - ## Acknowledgements This project has only been made possible because of the following Open Source projects: diff --git a/android/app/build.gradle b/android/app/build.gradle index ee70e84..21a89f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' def rootCMakeLists = "../../CMakeLists.txt" @@ -18,13 +18,12 @@ def chiakiVersion = "$chiakiVersionMajor.$chiakiVersionMinor.$chiakiVersionPatch println("Determined Chiaki Version: $chiakiVersion") android { - compileSdkVersion 30 - buildToolsVersion "30.0.2" + compileSdkVersion 33 defaultConfig { applicationId "com.metallic.chiaki" minSdkVersion 21 - targetSdkVersion 30 - versionCode 8 + targetSdkVersion 33 + versionCode 12 versionName chiakiVersion externalNativeBuild { cmake { @@ -33,11 +32,16 @@ android { "-DCHIAKI_ENABLE_GUI=OFF", "-DCHIAKI_ENABLE_SETSU=OFF", "-DCHIAKI_ENABLE_ANDROID=ON", + "-DCHIAKI_ENABLE_FFMPEG_DECODER=OFF", + "-DCHIAKI_ENABLE_PI_DECODER=OFF", "-DCHIAKI_LIB_ENABLE_OPUS=OFF", "-DCHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT=ON" } } } + buildFeatures { + viewBinding true + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +51,7 @@ android { } externalNativeBuild { cmake { - version "3.10.2+" + version "3.22.1" path rootCMakeLists } } @@ -61,6 +65,7 @@ android { } } + Properties properties = new Properties() def propertiesFile = file("../local.properties") if (propertiesFile.exists()) { @@ -86,31 +91,26 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } -androidExtensions { - // for @Parcelize - experimental = true -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.0' - implementation 'com.google.android.material:material:1.1.0-beta02' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.preference:preference:1.2.0' + implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.2.0' - implementation "io.reactivex.rxjava2:rxjava:2.2.12" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.5.1' + implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - def room_version = "2.2.4" + def room_version = "2.5.0" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-rxjava2:$room_version" - implementation "com.squareup.moshi:moshi:1.9.2" - kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2" + implementation "com.squareup.moshi:moshi:1.14.0" + kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 14b0466..a0d28cb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:exported="true"> diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 35543ad..9b14b60 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -110,9 +110,9 @@ JNIEXPORT jstring JNICALL JNI_FCN(quitReasonToString)(JNIEnv *env, jobject obj, return E->NewStringUTF(env, chiaki_quit_reason_string((ChiakiQuitReason)value)); } -JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsStopped)(JNIEnv *env, jobject obj, jint value) +JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsError)(JNIEnv *env, jobject obj, jint value) { - return value == CHIAKI_QUIT_REASON_STOPPED; + return chiaki_quit_reason_is_error(value); } JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset, jobject codec) @@ -133,6 +133,7 @@ typedef struct android_chiaki_session_t jmethodID java_session_event_connected_meth; jmethodID java_session_event_login_pin_request_meth; jmethodID java_session_event_quit_meth; + jmethodID java_session_event_rumble_meth; jfieldID java_controller_state_buttons; jfieldID java_controller_state_l2_state; jfieldID java_controller_state_r2_state; @@ -140,6 +141,20 @@ typedef struct android_chiaki_session_t jfieldID java_controller_state_left_y; jfieldID java_controller_state_right_x; jfieldID java_controller_state_right_y; + jfieldID java_controller_state_touches; + jfieldID java_controller_state_gyro_x; + jfieldID java_controller_state_gyro_y; + jfieldID java_controller_state_gyro_z; + jfieldID java_controller_state_accel_x; + jfieldID java_controller_state_accel_y; + jfieldID java_controller_state_accel_z; + jfieldID java_controller_state_orient_x; + jfieldID java_controller_state_orient_y; + jfieldID java_controller_state_orient_z; + jfieldID java_controller_state_orient_w; + jfieldID java_controller_touch_x; + jfieldID java_controller_touch_y; + jfieldID java_controller_touch_id; AndroidChiakiVideoDecoder video_decoder; AndroidChiakiAudioDecoder audio_decoder; @@ -178,6 +193,14 @@ static void android_chiaki_event_cb(ChiakiEvent *event, void *user) free(reason_str); break; } + case CHIAKI_EVENT_RUMBLE: + E->CallVoidMethod(env, session->java_session, + session->java_session_event_rumble_meth, + (jint)event->rumble.left, + (jint)event->rumble.right); + break; + default: + break; } (*global_vm)->DetachCurrentThread(global_vm); @@ -296,6 +319,7 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_session_event_connected_meth = E->GetMethodID(env, session->java_session_class, "eventConnected", "()V"); session->java_session_event_login_pin_request_meth = E->GetMethodID(env, session->java_session_class, "eventLoginPinRequest", "(Z)V"); session->java_session_event_quit_meth = E->GetMethodID(env, session->java_session_class, "eventQuit", "(ILjava/lang/String;)V"); + session->java_session_event_rumble_meth = E->GetMethodID(env, session->java_session_class, "eventRumble", "(II)V"); jclass controller_state_class = E->FindClass(env, BASE_PACKAGE"/ControllerState"); session->java_controller_state_buttons = E->GetFieldID(env, controller_state_class, "buttons", "I"); @@ -305,6 +329,22 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_controller_state_left_y = E->GetFieldID(env, controller_state_class, "leftY", "S"); session->java_controller_state_right_x = E->GetFieldID(env, controller_state_class, "rightX", "S"); session->java_controller_state_right_y = E->GetFieldID(env, controller_state_class, "rightY", "S"); + session->java_controller_state_touches = E->GetFieldID(env, controller_state_class, "touches", "[L"BASE_PACKAGE"/ControllerTouch;"); + session->java_controller_state_gyro_x = E->GetFieldID(env, controller_state_class, "gyroX", "F"); + session->java_controller_state_gyro_y = E->GetFieldID(env, controller_state_class, "gyroY", "F"); + session->java_controller_state_gyro_z = E->GetFieldID(env, controller_state_class, "gyroZ", "F"); + session->java_controller_state_accel_x = E->GetFieldID(env, controller_state_class, "accelX", "F"); + session->java_controller_state_accel_y = E->GetFieldID(env, controller_state_class, "accelY", "F"); + session->java_controller_state_accel_z = E->GetFieldID(env, controller_state_class, "accelZ", "F"); + session->java_controller_state_orient_x = E->GetFieldID(env, controller_state_class, "orientX", "F"); + session->java_controller_state_orient_y = E->GetFieldID(env, controller_state_class, "orientY", "F"); + session->java_controller_state_orient_z = E->GetFieldID(env, controller_state_class, "orientZ", "F"); + session->java_controller_state_orient_w = E->GetFieldID(env, controller_state_class, "orientW", "F"); + + jclass controller_touch_class = E->FindClass(env, BASE_PACKAGE"/ControllerTouch"); + session->java_controller_touch_x = E->GetFieldID(env, controller_touch_class, "x", "S"); + session->java_controller_touch_y = E->GetFieldID(env, controller_touch_class, "y", "S"); + session->java_controller_touch_id = E->GetFieldID(env, controller_touch_class, "id", "B"); chiaki_session_set_event_cb(&session->session, android_chiaki_event_cb, session); chiaki_session_set_video_sample_cb(&session->session, android_chiaki_video_decoder_video_sample, &session->video_decoder); @@ -373,7 +413,8 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetSurface)(JNIEnv *env, jobject obj, jlon JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject obj, jlong ptr, jobject controller_state_java) { AndroidChiakiSession *session = (AndroidChiakiSession *)ptr; - ChiakiControllerState controller_state = { 0 }; + ChiakiControllerState controller_state; + chiaki_controller_state_set_idle(&controller_state); controller_state.buttons = (uint32_t)E->GetIntField(env, controller_state_java, session->java_controller_state_buttons); controller_state.l2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_l2_state); controller_state.r2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_r2_state); @@ -381,6 +422,34 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject o controller_state.left_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_left_y); controller_state.right_x = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_x); controller_state.right_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_y); + jobjectArray touch_array = E->GetObjectField(env, controller_state_java, session->java_controller_state_touches); + size_t touch_array_len = (size_t)E->GetArrayLength(env, touch_array); + for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + { + if(i < touch_array_len) + { + jobject touch = E->GetObjectArrayElement(env, touch_array, i); + controller_state.touches[i].x = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_x); + controller_state.touches[i].y = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_y); + controller_state.touches[i].id = (int8_t)E->GetByteField(env, touch, session->java_controller_touch_id); + } + else + { + controller_state.touches[i].x = 0; + controller_state.touches[i].y = 0; + controller_state.touches[i].id = -1; + } + } + controller_state.gyro_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_x); + controller_state.gyro_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_y); + controller_state.gyro_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_z); + controller_state.accel_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_x); + controller_state.accel_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_y); + controller_state.accel_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_z); + controller_state.orient_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_x); + controller_state.orient_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_y); + controller_state.orient_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_z); + controller_state.orient_w = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_w); chiaki_session_set_controller_state(&session->session, &controller_state); } diff --git a/android/app/src/main/cpp/oboe b/android/app/src/main/cpp/oboe index 0ab5b12..8740d0f 160000 --- a/android/app/src/main/cpp/oboe +++ b/android/app/src/main/cpp/oboe @@ -1 +1 @@ -Subproject commit 0ab5b12a5bc3630a3d6c83b20eed2a669ebf7a24 +Subproject commit 8740d0fc321a55489dbbf6067298201b7d2e106d diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index d57623d..1146ad8 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -26,29 +26,34 @@ ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *dec return chiaki_mutex_init(&decoder->codec_mutex, false); } +static void kill_decoder(AndroidChiakiVideoDecoder *decoder) +{ + chiaki_mutex_lock(&decoder->codec_mutex); + decoder->shutdown_output = true; + ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000); + if(codec_buf_index >= 0) + { + CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); + AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + chiaki_thread_join(&decoder->output_thread, NULL); + } + else + { + CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + } + AMediaCodec_delete(decoder->codec); + decoder->codec = NULL; + decoder->shutdown_output = false; +} + void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder) { if(decoder->codec) - { - chiaki_mutex_lock(&decoder->codec_mutex); - decoder->shutdown_output = true; - ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1); - if(codec_buf_index >= 0) - { - CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); - AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - chiaki_thread_join(&decoder->output_thread, NULL); - } - else - { - CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - } - AMediaCodec_delete(decoder->codec); - } + kill_decoder(decoder); chiaki_mutex_fini(&decoder->codec_mutex); } @@ -56,6 +61,16 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder { chiaki_mutex_lock(&decoder->codec_mutex); + if(!surface) + { + if(decoder->codec) + { + kill_decoder(decoder); + CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed"); + } + return; + } + if(decoder->codec) { #if __ANDROID_API__ >= 23 diff --git a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt index 4d4738c..8398f62 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt @@ -23,9 +23,11 @@ val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE registered_host RENAME ps4_mac TO server_mac") - database.execSQL("ALTER TABLE registered_host RENAME ps4_nickname TO server_nickname") database.execSQL("ALTER TABLE registered_host ADD target INTEGER NOT NULL DEFAULT 1000") + database.execSQL("CREATE TABLE `new_registered_host` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `target` INTEGER NOT NULL, `ap_ssid` TEXT, `ap_bssid` TEXT, `ap_key` TEXT, `ap_name` TEXT, `server_mac` INTEGER NOT NULL, `server_nickname` TEXT, `rp_regist_key` BLOB NOT NULL, `rp_key_type` INTEGER NOT NULL, `rp_key` BLOB NOT NULL)"); + database.execSQL("INSERT INTO `new_registered_host` SELECT `id`, `target`, `ap_ssid`, `ap_bssid`, `ap_key`, `ap_name`, `ps4_mac`, `ps4_nickname`, `rp_regist_key`, `rp_key_type`, `rp_key` FROM `registered_host`") + database.execSQL("DROP TABLE registered_host") + database.execSQL("ALTER TABLE new_registered_host RENAME TO registered_host") } } diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt index 5825000..7065610 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ForeignKey.SET_NULL +import androidx.room.ForeignKey.Companion.SET_NULL import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Single diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index c9691b5..3c4f2f9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -68,11 +68,26 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(onScreenControlsEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(onScreenControlsEnabledKey, value).apply() } - val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_key) + val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_enabled_key) var touchpadOnlyEnabled get() = sharedPreferences.getBoolean(touchpadOnlyEnabledKey, false) set(value) { sharedPreferences.edit().putBoolean(touchpadOnlyEnabledKey, value).apply() } + val rumbleEnabledKey get() = resources.getString(R.string.preferences_rumble_enabled_key) + var rumbleEnabled + get() = sharedPreferences.getBoolean(rumbleEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() } + + val motionEnabledKey get() = resources.getString(R.string.preferences_motion_enabled_key) + var motionEnabled + get() = sharedPreferences.getBoolean(motionEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() } + + val buttonHapticEnabledKey get() = resources.getString(R.string.preferences_button_haptic_enabled_key) + var buttonHapticEnabled + get() = sharedPreferences.getBoolean(buttonHapticEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(buttonHapticEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index b96d88d..d920a8c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ColumnInfo.BLOB +import androidx.room.ColumnInfo.Companion.BLOB import com.metallic.chiaki.lib.RegistHost import com.metallic.chiaki.lib.Target import io.reactivex.Completable diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index 8334dc5..74257ac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -25,6 +25,8 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import okio.Buffer import okio.Okio +import okio.buffer +import okio.source import java.io.File import java.io.IOException @@ -164,7 +166,7 @@ fun importSettingsFromUri(activity: Activity, uri: Uri, disposable: CompositeDis try { val inputStream = activity.contentResolver.openInputStream(uri) ?: throw IOException() - val buffer = Okio.buffer(Okio.source(inputStream)) + val buffer = inputStream.source().buffer() val reader = JsonReader.of(buffer) val adapter = moshi().serializedSettingsAdapter() diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index d638cb2..bb0b0a6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -3,8 +3,7 @@ package com.metallic.chiaki.lib import android.os.Parcelable import android.util.Log import android.view.Surface -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import java.lang.Exception import java.net.InetSocketAddress import kotlin.math.abs @@ -84,14 +83,14 @@ private class ChiakiNative } @JvmStatic external fun errorCodeToString(value: Int): String @JvmStatic external fun quitReasonToString(value: Int): String - @JvmStatic external fun quitReasonIsStopped(value: Int): Boolean + @JvmStatic external fun quitReasonIsError(value: Int): Boolean @JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int, codec: Codec): ConnectVideoProfile @JvmStatic external fun sessionCreate(result: CreateResult, connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean, javaSession: Session) @JvmStatic external fun sessionFree(ptr: Long) @JvmStatic external fun sessionStart(ptr: Long): Int @JvmStatic external fun sessionStop(ptr: Long): Int @JvmStatic external fun sessionJoin(ptr: Long): Int - @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface) + @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?) @JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState) @JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String) @JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService) @@ -150,6 +149,14 @@ class ChiakiLog(val levelMask: Int, val callback: (level: Int, text: String) -> private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a else b +private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX + +data class ControllerTouch( + var x: UShort = 0U, + var y: UShort = 0U, + var id: Byte = -1 // -1 = up +) + data class ControllerState constructor( var buttons: UInt = 0U, var l2State: UByte = 0U, @@ -157,26 +164,40 @@ data class ControllerState constructor( var leftX: Short = 0, var leftY: Short = 0, var rightX: Short = 0, - var rightY: Short = 0 + var rightY: Short = 0, + private var touchIdNext: UByte = 0U, + var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), + var gyroX: Float = 0.0f, + var gyroY: Float = 0.0f, + var gyroZ: Float = 0.0f, + var accelX: Float = 0.0f, + var accelY: Float = 1.0f, + var accelZ: Float = 0.0f, + var orientX: Float = 0.0f, + var orientY: Float = 0.0f, + var orientZ: Float = 0.0f, + var orientW: Float = 1.0f ){ companion object { val BUTTON_CROSS = (1 shl 0).toUInt() val BUTTON_MOON = (1 shl 1).toUInt() - val BUTTON_BOX = (1 shl 2).toUInt() - val BUTTON_PYRAMID = (1 shl 3).toUInt() + val BUTTON_BOX = (1 shl 2).toUInt() + val BUTTON_PYRAMID = (1 shl 3).toUInt() val BUTTON_DPAD_LEFT = (1 shl 4).toUInt() val BUTTON_DPAD_RIGHT = (1 shl 5).toUInt() - val BUTTON_DPAD_UP = (1 shl 6).toUInt() + val BUTTON_DPAD_UP = (1 shl 6).toUInt() val BUTTON_DPAD_DOWN = (1 shl 7).toUInt() - val BUTTON_L1 = (1 shl 8).toUInt() - val BUTTON_R1 = (1 shl 9).toUInt() + val BUTTON_L1 = (1 shl 8).toUInt() + val BUTTON_R1 = (1 shl 9).toUInt() val BUTTON_L3 = (1 shl 10).toUInt() val BUTTON_R3 = (1 shl 11).toUInt() - val BUTTON_OPTIONS = (1 shl 12).toUInt() + val BUTTON_OPTIONS = (1 shl 12).toUInt() val BUTTON_SHARE = (1 shl 13).toUInt() - val BUTTON_TOUCHPAD = (1 shl 14).toUInt() + val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() + val TOUCHPAD_WIDTH: UShort = 1920U + val TOUCHPAD_HEIGHT: UShort = 942U } infix fun or(o: ControllerState) = ControllerState( @@ -186,24 +207,116 @@ data class ControllerState constructor( leftX = maxAbs(leftX, o.leftX), leftY = maxAbs(leftY, o.leftY), rightX = maxAbs(rightX, o.rightX), - rightY = maxAbs(rightY, o.rightY) + rightY = maxAbs(rightY, o.rightY), + touches = touches.zip(o.touches) { a, b -> if(a.id >= 0) a else b }.toTypedArray(), + gyroX = gyroX, + gyroY = gyroY, + gyroZ = gyroZ, + accelX = accelX, + accelY = accelY, + accelZ = accelZ, + orientX = orientX, + orientY = orientY, + orientZ = orientZ, + orientW = orientW ) + + override fun equals(other: Any?): Boolean + { + if(this === other) return true + if(javaClass != other?.javaClass) return false + + other as ControllerState + + if(buttons != other.buttons) return false + if(l2State != other.l2State) return false + if(r2State != other.r2State) return false + if(leftX != other.leftX) return false + if(leftY != other.leftY) return false + if(rightX != other.rightX) return false + if(rightY != other.rightY) return false + if(touchIdNext != other.touchIdNext) return false + if(!touches.contentEquals(other.touches)) return false + if(gyroX != other.gyroX) return false + if(gyroY != other.gyroY) return false + if(gyroZ != other.gyroZ) return false + if(accelX != other.accelX) return false + if(accelY != other.accelY) return false + if(accelZ != other.accelZ) return false + if(orientX != other.orientX) return false + if(orientY != other.orientY) return false + if(orientZ != other.orientZ) return false + if(orientW != other.orientW) return false + + return true + } + + override fun hashCode(): Int + { + var result = buttons.hashCode() + result = 31 * result + l2State.hashCode() + result = 31 * result + r2State.hashCode() + result = 31 * result + leftX + result = 31 * result + leftY + result = 31 * result + rightX + result = 31 * result + rightY + result = 31 * result + touchIdNext.hashCode() + result = 31 * result + touches.contentHashCode() + result = 31 * result + gyroX.hashCode() + result = 31 * result + gyroY.hashCode() + result = 31 * result + gyroZ.hashCode() + result = 31 * result + accelX.hashCode() + result = 31 * result + accelY.hashCode() + result = 31 * result + accelZ.hashCode() + result = 31 * result + orientX.hashCode() + result = 31 * result + orientY.hashCode() + result = 31 * result + orientZ.hashCode() + result = 31 * result + orientW.hashCode() + return result + } + + fun startTouch(x: UShort, y: UShort): UByte? = + touches + .find { it.id < 0 } + ?.also { + it.id = touchIdNext.toByte() + it.x = x + it.y = y + touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() + }?.id?.toUByte() + + fun stopTouch(id: UByte) + { + touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + it.id = -1 + } + } + + fun setTouchPos(id: UByte, x: UShort, y: UShort): Boolean + = touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + val r = it.x != x || it.y != y + it.x = x + it.y = y + r + } ?: false } class QuitReason(val value: Int) { override fun toString() = ChiakiNative.quitReasonToString(value) - /** - * whether the reason is CHIAKI_QUIT_REASON_STOPPED - */ - val isStopped = ChiakiNative.quitReasonIsStopped(value) + val isError = ChiakiNative.quitReasonIsError(value) } sealed class Event object ConnectedEvent: Event() data class LoginPinRequestEvent(val pinIncorrect: Boolean): Event() data class QuitEvent(val reason: QuitReason, val reasonString: String?): Event() +data class RumbleEvent(val left: UByte, val right: UByte): Event() class CreateError(val errorCode: ErrorCode): Exception("Failed to create a native object: $errorCode") @@ -259,7 +372,12 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean) event(QuitEvent(QuitReason(reasonValue), reasonString)) } - fun setSurface(surface: Surface) + private fun eventRumble(left: Int, right: Int) + { + event(RumbleEvent(left.toUByte(), right.toUByte())) + } + + fun setSurface(surface: Surface?) { ChiakiNative.sessionSetSurface(nativePtr, surface) } diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index 0bd844e..465a2a7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -3,6 +3,7 @@ package com.metallic.chiaki.main import android.util.Log +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils @@ -16,8 +17,8 @@ import com.metallic.chiaki.common.DiscoveredDisplayHost import com.metallic.chiaki.common.DisplayHost import com.metallic.chiaki.common.ManualDisplayHost import com.metallic.chiaki.common.ext.inflate +import com.metallic.chiaki.databinding.ItemDisplayHostBinding import com.metallic.chiaki.lib.DiscoveryHost -import kotlinx.android.synthetic.main.item_display_host.view.* class DisplayHostDiffCallback(val old: List, val new: List): DiffUtil.Callback() { @@ -42,10 +43,10 @@ class DisplayHostRecyclerViewAdapter( diff.dispatchUpdatesTo(this) } - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemDisplayHostBinding): RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) - = ViewHolder(parent.inflate(R.layout.item_display_host)) + = ViewHolder(ItemDisplayHostBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = hosts.count() @@ -53,7 +54,7 @@ class DisplayHostRecyclerViewAdapter( { val context = holder.itemView.context val host = hosts[position] - holder.itemView.also { + holder.binding.also { it.nameTextView.text = host.name it.hostTextView.text = context.getString(R.string.display_host_host, host.host) val id = host.id @@ -87,7 +88,7 @@ class DisplayHostRecyclerViewAdapter( else -> R.drawable.ic_console } ) - it.setOnClickListener { clickCallback(host) } + it.root.setOnClickListener { clickCallback(host) } val canWakeup = host.registeredHost != null val canEditDelete = host is ManualDisplayHost diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 4fd8178..5bf1f49 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -17,52 +17,54 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.* import com.metallic.chiaki.common.ext.putRevealExtra import com.metallic.chiaki.common.ext.viewModelFactory +import com.metallic.chiaki.databinding.ActivityMainBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.DiscoveryHost import com.metallic.chiaki.manualconsole.EditManualConsoleActivity import com.metallic.chiaki.regist.RegistActivity import com.metallic.chiaki.settings.SettingsActivity import com.metallic.chiaki.stream.StreamActivity -import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel + private lateinit var binding: ActivityMainBinding private var discoveryMenuItem: MenuItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) - floatingActionButton.setOnClickListener { - expandFloatingActionButton(!floatingActionButton.isExpanded) + binding.floatingActionButton.setOnClickListener { + expandFloatingActionButton(!binding.floatingActionButton.isExpanded) } - floatingActionButtonDialBackground.setOnClickListener { + binding.floatingActionButtonDialBackground.setOnClickListener { expandFloatingActionButton(false) } - addManualButton.setOnClickListener { addManualConsole() } - addManualLabelButton.setOnClickListener { addManualConsole() } + binding.addManualButton.setOnClickListener { addManualConsole() } + binding.addManualLabelButton.setOnClickListener { addManualConsole() } - registerButton.setOnClickListener { showRegistration() } - registerLabelButton.setOnClickListener { showRegistration() } + binding.registerButton.setOnClickListener { showRegistration() } + binding.registerLabelButton.setOnClickListener { showRegistration() } viewModel = ViewModelProvider(this, viewModelFactory { MainViewModel(getDatabase(this), Preferences(this)) }) .get(MainViewModel::class.java) val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered, this::wakeupHost, this::editHost, this::deleteHost) - hostsRecyclerView.adapter = recyclerViewAdapter - hostsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.hostsRecyclerView.adapter = recyclerViewAdapter + binding.hostsRecyclerView.layoutManager = LinearLayoutManager(this) viewModel.displayHosts.observe(this, Observer { - val top = hostsRecyclerView.computeVerticalScrollOffset() == 0 + val top = binding.hostsRecyclerView.computeVerticalScrollOffset() == 0 recyclerViewAdapter.hosts = it if(top) - hostsRecyclerView.scrollToPosition(0) + binding.hostsRecyclerView.scrollToPosition(0) updateEmptyInfo() }) @@ -76,19 +78,19 @@ class MainActivity : AppCompatActivity() { if(viewModel.displayHosts.value?.isEmpty() ?: true) { - emptyInfoLayout.visibility = View.VISIBLE + binding.emptyInfoLayout.visibility = View.VISIBLE val discoveryActive = viewModel.discoveryActive.value ?: false - emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) - emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) + binding.emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) + binding.emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) } else - emptyInfoLayout.visibility = View.GONE + binding.emptyInfoLayout.visibility = View.GONE } private fun expandFloatingActionButton(expand: Boolean) { - floatingActionButton.isExpanded = expand - floatingActionButton.isActivated = floatingActionButton.isExpanded + binding.floatingActionButton.isExpanded = expand + binding.floatingActionButton.isActivated = binding.floatingActionButton.isExpanded } override fun onStart() @@ -105,7 +107,7 @@ class MainActivity : AppCompatActivity() override fun onBackPressed() { - if(floatingActionButton.isExpanded) + if(binding.floatingActionButton.isExpanded) { expandFloatingActionButton(false) return @@ -151,7 +153,7 @@ class MainActivity : AppCompatActivity() private fun addManualConsole() { Intent(this, EditManualConsoleActivity::class.java).also { - it.putRevealExtra(addManualButton, rootLayout) + it.putRevealExtra(binding.addManualButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } @@ -159,7 +161,7 @@ class MainActivity : AppCompatActivity() private fun showRegistration() { Intent(this, RegistActivity::class.java).also { - it.putRevealExtra(registerButton, rootLayout) + it.putRevealExtra(binding.registerButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index 49e5f4d..bb64b87 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -16,10 +16,10 @@ import com.metallic.chiaki.common.RegisteredHost import com.metallic.chiaki.common.ext.RevealActivity import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityEditManualBinding import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import kotlinx.android.synthetic.main.activity_edit_manual.* class EditManualConsoleActivity: AppCompatActivity(), RevealActivity { @@ -28,18 +28,20 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity const val EXTRA_MANUAL_HOST_ID = "manual_host_id" } - override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - override val revealWindow: Window get() = window - private lateinit var viewModel: EditManualConsoleViewModel + private lateinit var binding: ActivityEditManualBinding + + override val revealIntent: Intent get() = intent + override val revealRootLayout: View get() = binding.rootLayout + override val revealWindow: Window get() = window private val disposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_edit_manual) + binding = ActivityEditManualBinding.inflate(layoutInflater) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this, viewModelFactory { @@ -52,17 +54,17 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity .get(EditManualConsoleViewModel::class.java) viewModel.existingHost?.observe(this, Observer { - hostEditText.setText(it.host) + binding.hostEditText.setText(it.host) }) viewModel.selectedRegisteredHost.observe(this, Observer { - registeredHostTextView.setText(titleForRegisteredHost(it)) + binding.registeredHostTextView.setText(titleForRegisteredHost(it)) }) viewModel.registeredHosts.observe(this, Observer { hosts -> - registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, + binding.registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, hosts.map { titleForRegisteredHost(it) })) - registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { + binding.registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { if(position >= hosts.size) @@ -73,8 +75,7 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity } }) - - saveButton.setOnClickListener { saveHost() } + binding.saveButton.setOnClickListener { saveHost() } } private fun titleForRegisteredHost(registeredHost: RegisteredHost?) = @@ -85,14 +86,14 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity private fun saveHost() { - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() if(host.isEmpty()) { - hostEditText.error = getString(R.string.entered_host_invalid) + binding.hostEditText.error = getString(R.string.entered_host_invalid) return } - saveButton.isEnabled = false + binding.saveButton.isEnabled = false viewModel.saveHost(host) .observeOn(AndroidSchedulers.mainThread()) .subscribe { diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index fb3ddaa..9190f9c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.metallic.chiaki.R import com.metallic.chiaki.common.ext.RevealActivity +import com.metallic.chiaki.databinding.ActivityRegistBinding import com.metallic.chiaki.lib.RegistInfo import com.metallic.chiaki.lib.Target -import kotlinx.android.synthetic.main.activity_regist.* import java.lang.IllegalArgumentException class RegistActivity: AppCompatActivity(), RevealActivity @@ -30,33 +30,35 @@ class RegistActivity: AppCompatActivity(), RevealActivity private const val REQUEST_REGIST = 1 } + private lateinit var viewModel: RegistViewModel + private lateinit var binding: ActivityRegistBinding + override val revealWindow: Window get() = window override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - - private lateinit var viewModel: RegistViewModel + override val revealRootLayout: View get() = binding.rootLayout override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_regist) + binding = ActivityRegistBinding.inflate(layoutInflater) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this).get(RegistViewModel::class.java) - hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") - broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) + binding.hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") + binding.broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) - registButton.setOnClickListener { doRegist() } + binding.registButton.setOnClickListener { doRegist() } - ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { + binding.ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { RegistViewModel.ConsoleVersion.PS5 -> R.id.ps5RadioButton RegistViewModel.ConsoleVersion.PS4_GE_8 -> R.id.ps4VersionGE8RadioButton RegistViewModel.ConsoleVersion.PS4_GE_7 -> R.id.ps4VersionGE7RadioButton RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.id.ps4VersionLT7RadioButton }) - ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> + binding.ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> viewModel.ps4Version.value = when(checkedId) { R.id.ps5RadioButton -> RegistViewModel.ConsoleVersion.PS5 @@ -68,14 +70,14 @@ class RegistActivity: AppCompatActivity(), RevealActivity } viewModel.ps4Version.observe(this, Observer { - psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE - psnIdTextInputLayout.hint = getString(when(it!!) + binding.psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE + binding.psnIdTextInputLayout.hint = getString(when(it!!) { RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id else -> R.string.hint_regist_psn_account_id }) - pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) - pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) + binding.pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) + binding.pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) }) } @@ -83,11 +85,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity { val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5 - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() val hostValid = host.isNotEmpty() - val broadcast = broadcastCheckBox.isChecked + val broadcast = binding.broadcastCheckBox.isChecked - val psnId = psnIdEditText.text.toString().trim() + val psnId = binding.psnIdEditText.text.toString().trim() val psnOnlineId: String? = if(ps4Version == RegistViewModel.ConsoleVersion.PS4_LT_7) psnId else null val psnAccountId: ByteArray? = if(ps4Version != RegistViewModel.ConsoleVersion.PS4_LT_7) @@ -101,11 +103,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity } - val pin = pinEditText.text.toString() + val pin = binding.pinEditText.text.toString() val pinValid = pin.length == PIN_LENGTH - hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null - psnIdEditText.error = + binding.hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null + binding.psnIdEditText.error = if(!psnIdValid) getString(when(ps4Version) { @@ -114,7 +116,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity }) else null - pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null + binding.pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null if(!hostValid || !psnIdValid || !pinValid) return diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index 870b67e..16f545a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -16,8 +16,8 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.MacAddress import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityRegistExecuteBinding import com.metallic.chiaki.lib.RegistInfo -import kotlinx.android.synthetic.main.activity_regist_execute.* import kotlin.math.max class RegistExecuteActivity: AppCompatActivity() @@ -31,55 +31,57 @@ class RegistExecuteActivity: AppCompatActivity() } private lateinit var viewModel: RegistExecuteViewModel + private lateinit var binding: ActivityRegistExecuteBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_regist_execute) + binding = ActivityRegistExecuteBinding.inflate(layoutInflater) + setContentView(binding.root) viewModel = ViewModelProvider(this, viewModelFactory { RegistExecuteViewModel(getDatabase(this)) }) .get(RegistExecuteViewModel::class.java) - logTextView.setHorizontallyScrolling(true) - logTextView.movementMethod = ScrollingMovementMethod() + binding.logTextView.setHorizontallyScrolling(true) + binding.logTextView.movementMethod = ScrollingMovementMethod() viewModel.logText.observe(this, Observer { - val textLayout = logTextView.layout ?: return@Observer + val textLayout = binding.logTextView.layout ?: return@Observer val lineCount = textLayout.lineCount if(lineCount < 1) return@Observer - logTextView.text = it - val scrollY = textLayout.getLineBottom(lineCount - 1) - logTextView.height + logTextView.paddingTop + logTextView.paddingBottom - logTextView.scrollTo(0, max(scrollY, 0)) + binding.logTextView.text = it + val scrollY = textLayout.getLineBottom(lineCount - 1) - binding.logTextView.height + binding.logTextView.paddingTop + binding.logTextView.paddingBottom + binding.logTextView.scrollTo(0, max(scrollY, 0)) }) viewModel.state.observe(this, Observer { - progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE when(it) { RegistExecuteViewModel.State.FAILED -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_failed) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_failed) setResult(RESULT_FAILED) } RegistExecuteViewModel.State.SUCCESSFUL, RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_success) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_success) setResult(RESULT_OK) if(it == RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE) showDuplicateDialog() } RegistExecuteViewModel.State.STOPPED -> { - infoTextView.visibility = View.GONE + binding.infoTextView.visibility = View.GONE setResult(Activity.RESULT_CANCELED) } - else -> infoTextView.visibility = View.GONE + else -> binding.infoTextView.visibility = View.GONE } }) - shareLogButton.setOnClickListener { + binding.shareLogButton.setOnClickListener { val log = viewModel.logText.value ?: "" Intent(Intent.ACTION_SEND).also { it.type = "text/plain" diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index e96b7b5..822fe76 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -1,19 +1,37 @@ package com.metallic.chiaki.session -import android.util.Log -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent +import android.content.Context +import android.hardware.* +import android.view.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.ControllerState -class StreamInput(val preferences: Preferences) +class StreamInput(val context: Context, val preferences: Preferences) { var controllerStateChangedCallback: ((ControllerState) -> Unit)? = null val controllerState: ControllerState get() { - val controllerState = keyControllerState or motionControllerState + val controllerState = sensorControllerState or keyControllerState or motionControllerState + + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + @Suppress("DEPRECATION") + when(windowManager.defaultDisplay.rotation) + { + Surface.ROTATION_90 -> { + controllerState.accelX *= -1.0f + controllerState.accelZ *= -1.0f + controllerState.gyroX *= -1.0f + controllerState.gyroZ *= -1.0f + controllerState.orientX *= -1.0f + controllerState.orientZ *= -1.0f + } + else -> {} + } // prioritize motion controller's l2 and r2 over key // (some controllers send only key, others both but key earlier than full press) @@ -25,6 +43,7 @@ class StreamInput(val preferences: Preferences) return controllerState or touchControllerState } + private val sensorControllerState = ControllerState() // from Motion Sensors private val keyControllerState = ControllerState() // from KeyEvents private val motionControllerState = ControllerState() // from MotionEvents var touchControllerState = ControllerState() @@ -36,6 +55,66 @@ class StreamInput(val preferences: Preferences) private val swapCrossMoon = preferences.swapCrossMoon + private val sensorEventListener = object: SensorEventListener { + override fun onSensorChanged(event: SensorEvent) + { + when(event.sensor.type) + { + Sensor.TYPE_ACCELEROMETER -> { + sensorControllerState.accelX = event.values[1] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelY = event.values[2] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelZ = event.values[0] / SensorManager.GRAVITY_EARTH + } + Sensor.TYPE_GYROSCOPE -> { + sensorControllerState.gyroX = event.values[1] + sensorControllerState.gyroY = event.values[2] + sensorControllerState.gyroZ = event.values[0] + } + Sensor.TYPE_ROTATION_VECTOR -> { + val q = floatArrayOf(0f, 0f, 0f, 0f) + SensorManager.getQuaternionFromVector(q, event.values) + sensorControllerState.orientX = q[2] + sensorControllerState.orientY = q[3] + sensorControllerState.orientZ = q[1] + sensorControllerState.orientW = q[0] + } + else -> return + } + controllerStateUpdated() + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + } + + private val motionLifecycleObserver = object: LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() + { + val samplingPeriodUs = 4000 + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + listOfNotNull( + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), + sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), + sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + ).forEach { + sensorManager.registerListener(sensorEventListener, it, samplingPeriodUs) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() + { + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + sensorManager.unregisterListener(sensorEventListener) + } + } + + fun observe(lifecycleOwner: LifecycleOwner) + { + if(preferences.motionEnabled) + lifecycleOwner.lifecycle.addObserver(motionLifecycleObserver) + } + private fun controllerStateUpdated() { controllerStateChangedCallback?.let { it(controllerState) } @@ -98,7 +177,7 @@ class StreamInput(val preferences: Preferences) { if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK) return false - fun Float.signedAxis() = (this * Short.MAX_VALUE).toShort() + fun Float.signedAxis() = (this * Short.MAX_VALUE).toInt().toShort() fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte() motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis() motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis() diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index 059840c..dd32a40 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -25,8 +25,11 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va private val _state = MutableLiveData(StreamStateIdle) val state: LiveData get() = _state + private val _rumbleState = MutableLiveData(RumbleEvent(0U, 0U)) + val rumbleState: LiveData get() = _rumbleState - var surfaceTexture: SurfaceTexture? = null + private var surfaceTexture: SurfaceTexture? = null + private var surface: Surface? = null init { @@ -59,9 +62,9 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va _state.value = StreamStateConnecting session.eventCallback = this::eventCallback session.start() - val surfaceTexture = surfaceTexture - if(surfaceTexture != null) - session.setSurface(Surface(surfaceTexture)) + val surface = surface + if(surface != null) + session.setSurface(surface) this.session = session } catch(e: CreateError) @@ -86,9 +89,30 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va event.pinIncorrect ) ) + is RumbleEvent -> _rumbleState.postValue(event) } } + fun attachToSurfaceView(surfaceView: SurfaceView) + { + surfaceView.holder.addCallback(object: SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) + { + val surface = holder.surface + this@StreamSession.surface = surface + session?.setSurface(surface) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) + { + this@StreamSession.surface = null + session?.setSurface(null) + } + }) + } + fun attachToTextureView(textureView: TextureView) { textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener { @@ -97,6 +121,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va if(surfaceTexture != null) return surfaceTexture = surface + this@StreamSession.surface = Surface(surfaceTexture) session?.setSurface(Surface(surface)) } diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt index 3a87e1b..1cf224f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt @@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.metallic.chiaki.R -import kotlinx.android.synthetic.main.activity_settings.* +import com.metallic.chiaki.databinding.ActivitySettingsBinding interface TitleFragment { @@ -18,20 +18,23 @@ interface TitleFragment class SettingsActivity: AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + private lateinit var binding: ActivitySettingsBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) val rootFragment = SettingsFragment() replaceFragment(rootFragment, false) supportFragmentManager.addOnBackStackChangedListener { val titleFragment = supportFragmentManager.findFragmentById(R.id.settingsFragment) as? TitleFragment ?: return@addOnBackStackChangedListener - titleTextView.text = titleFragment.getTitle(resources) + binding.titleTextView.text = titleFragment.getTitle(resources) } - titleTextView.text = rootFragment.getTitle(resources) + binding.titleTextView.text = rootFragment.getTitle(resources) } override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) = when(pref.fragment) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index f38f619..f574d09 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -16,7 +16,6 @@ import com.metallic.chiaki.common.exportAndShareAllSettings import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase import com.metallic.chiaki.common.importSettingsFromUri -import com.metallic.chiaki.lib.Codec import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo @@ -26,6 +25,9 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose preferences.swapCrossMoonKey -> preferences.swapCrossMoon + preferences.rumbleEnabledKey -> preferences.rumbleEnabled + preferences.motionEnabledKey -> preferences.motionEnabled + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled else -> defValue } @@ -35,6 +37,9 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose = value preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value + preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value + preferences.motionEnabledKey -> preferences.motionEnabled = value + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled = value } } diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt index 46c8904..6acf547 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt @@ -2,13 +2,13 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.metallic.chiaki.R import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_log_file.view.* +import com.metallic.chiaki.databinding.ItemLogFileBinding import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -20,7 +20,7 @@ class SettingsLogsAdapter: RecyclerView.Adapter( private val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) private val timeFormat = SimpleDateFormat("HH:mm:ss:SSS", Locale.getDefault()) - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemLogFileBinding): RecyclerView.ViewHolder(binding.root) var logFiles: List = listOf() set(value) @@ -29,16 +29,16 @@ class SettingsLogsAdapter: RecyclerView.Adapter( notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent.inflate(R.layout.item_log_file)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ViewHolder(ItemLogFileBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = logFiles.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val view = holder.itemView val logFile = logFiles[position] - view.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" - view.summaryTextView.text = logFile.filename - view.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } + holder.binding.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" + holder.binding.summaryTextView.text = logFile.filename + holder.binding.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt index 07e1e3f..0397172 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt @@ -21,29 +21,35 @@ import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.fileProviderAuthority -import kotlinx.android.synthetic.main.fragment_settings_logs.* +import com.metallic.chiaki.databinding.FragmentSettingsLogsBinding class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment { private lateinit var viewModel: SettingsLogsViewModel + private var _binding: FragmentSettingsLogsBinding? = null + private val binding get() = _binding!! + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.fragment_settings_logs, container, false) + FragmentSettingsLogsBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val context = context!! + val context = requireContext() viewModel = ViewModelProvider(this, viewModelFactory { SettingsLogsViewModel(LogManager(context)) }) .get(SettingsLogsViewModel::class.java) val adapter = SettingsLogsAdapter() - logsRecyclerView.layoutManager = LinearLayoutManager(context) - logsRecyclerView.adapter = adapter + binding.logsRecyclerView.layoutManager = LinearLayoutManager(context) + binding.logsRecyclerView.adapter = adapter adapter.shareCallback = this::shareLogFile viewModel.sessionLogs.observe(viewLifecycleOwner, Observer { adapter.logFiles = it - emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE + binding.emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE }) val itemTouchSwipeCallback = object : ItemTouchSwipeCallback(context) @@ -55,7 +61,7 @@ class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment viewModel.deleteLog(file) } } - ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(logsRecyclerView) + ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(binding.logsRecyclerView) } override fun getTitle(resources: Resources): String = resources.getString(R.string.preferences_logs_title) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 57a06ce..509d594 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -2,17 +2,15 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.metallic.chiaki.R import com.metallic.chiaki.common.RegisteredHost -import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_registered_host.view.* +import com.metallic.chiaki.databinding.ItemRegisteredHostBinding class SettingsRegisteredHostsAdapter: RecyclerView.Adapter() { - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemRegisteredHostBinding): RecyclerView.ViewHolder(binding.root) var hosts: List = listOf() set(value) @@ -21,15 +19,15 @@ class SettingsRegisteredHostsAdapter: RecyclerView.Adapter + if(aspectDeformation > 0) + width = (height * aspectRatio).toInt() + else + height = (width / aspectRatio).toInt() + TransformMode.FIT -> + if(aspectDeformation > 0) + height = (width / aspectRatio).toInt() + else + width = (height * aspectRatio).toInt() + TransformMode.STRETCH -> {} + } + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + } +} diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 67d425b..bd77e0c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -6,12 +6,8 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.AlertDialog import android.graphics.Matrix -import android.os.Bundle -import android.os.Handler -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.TextureView -import android.view.View +import android.os.* +import android.view.* import android.widget.EditText import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone @@ -20,15 +16,18 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.metallic.chiaki.R -import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.common.ext.viewModelFactory +import com.metallic.chiaki.databinding.ActivityStreamBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.session.* -import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment +import com.metallic.chiaki.touchcontrols.DefaultTouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment -import kotlinx.android.synthetic.main.activity_stream.* +import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import kotlin.math.min private sealed class DialogContents private object StreamQuitDialog: DialogContents() @@ -44,6 +43,8 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } private lateinit var viewModel: StreamViewModel + private lateinit var binding: ActivityStreamBinding + private val uiVisibilityHandler = Handler() override fun onCreate(savedInstanceState: Bundle?) @@ -58,64 +59,76 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } viewModel = ViewModelProvider(this, viewModelFactory { - StreamViewModel(Preferences(this), LogManager(this), connectInfo) + StreamViewModel(application, connectInfo) })[StreamViewModel::class.java] - setContentView(R.layout.activity_stream) + viewModel.input.observe(this) + + binding = ActivityStreamBinding.inflate(layoutInflater) + setContentView(binding.root) window.decorView.setOnSystemUiVisibilityChangeListener(this) viewModel.onScreenControlsEnabled.observe(this, Observer { - if(onScreenControlsSwitch.isChecked != it) - onScreenControlsSwitch.isChecked = it - if(onScreenControlsSwitch.isChecked) - touchpadOnlySwitch.isChecked = false + if(binding.onScreenControlsSwitch.isChecked != it) + binding.onScreenControlsSwitch.isChecked = it + if(binding.onScreenControlsSwitch.isChecked) + binding.touchpadOnlySwitch.isChecked = false }) - onScreenControlsSwitch.setOnCheckedChangeListener { _, isChecked -> + binding.onScreenControlsSwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setOnScreenControlsEnabled(isChecked) showOverlay() } viewModel.touchpadOnlyEnabled.observe(this, Observer { - if(touchpadOnlySwitch.isChecked != it) - touchpadOnlySwitch.isChecked = it - if(touchpadOnlySwitch.isChecked) - onScreenControlsSwitch.isChecked = false + if(binding.touchpadOnlySwitch.isChecked != it) + binding.touchpadOnlySwitch.isChecked = it + if(binding.touchpadOnlySwitch.isChecked) + binding.onScreenControlsSwitch.isChecked = false }) - touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> + binding.touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setTouchpadOnlyEnabled(isChecked) showOverlay() } - displayModeToggle.addOnButtonCheckedListener { _, checkedId, _ -> - // following 'if' is a workaround until selectionRequired for MaterialButtonToggleGroup - // comes out of alpha. - // See https://stackoverflow.com/questions/56164004/required-single-selection-on-materialbuttontogglegroup - if (displayModeToggle.checkedButtonId == -1) - displayModeToggle.check(checkedId) - - adjustTextureViewAspect() + binding.displayModeToggle.addOnButtonCheckedListener { _, _, _ -> + adjustStreamViewAspect() showOverlay() } - viewModel.session.attachToTextureView(textureView) + //viewModel.session.attachToTextureView(textureView) + viewModel.session.attachToSurfaceView(binding.surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) - textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - adjustTextureViewAspect() + adjustStreamViewAspect() + + if(Preferences(this).rumbleEnabled) + { + val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator + viewModel.session.rumbleState.observe(this, Observer { + val amplitude = min(255, (it.left.toInt() + it.right.toInt()) / 2) + vibrator.cancel() + if(amplitude == 0) + return@Observer + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude)) + else + vibrator.vibrate(1000) + }) } } + private val controlsDisposable = CompositeDisposable() + override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) if(fragment is TouchControlsFragment) { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } + fragment.controllerState + .subscribe { viewModel.input.touchControllerState = it } + .addTo(controlsDisposable) fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled - } - if(fragment is TouchpadOnlyFragment) - { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } - fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled + if(fragment is TouchpadOnlyFragment) + fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled } } @@ -132,6 +145,12 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe viewModel.session.pause() } + override fun onDestroy() + { + super.onDestroy() + controlsDisposable.dispose() + } + private fun reconnect() { viewModel.session.shutdown() @@ -150,14 +169,14 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun showOverlay() { - overlay.isVisible = true - overlay.animate() + binding.overlay.isVisible = true + binding.overlay.animate() .alpha(1.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { - overlay.alpha = 1.0f + binding.overlay.alpha = 1.0f } }) uiVisibilityHandler.removeCallbacks(hideSystemUIRunnable) @@ -166,13 +185,13 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun hideOverlay() { - overlay.animate() + binding.overlay.animate() .alpha(0.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { - overlay.isGone = true + binding.overlay.isGone = true } }) } @@ -205,34 +224,39 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun stateChanged(state: StreamState) { - progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE when(state) { is StreamStateQuit -> { - if(!state.reason.isStopped && dialogContents != StreamQuitDialog) + if(dialogContents != StreamQuitDialog) { - dialog?.dismiss() - val reasonStr = state.reasonString - val dialog = MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) - + (if(reasonStr != null) "\n$reasonStr" else "")) - .setPositiveButton(R.string.action_reconnect) { _, _ -> - dialog = null - reconnect() - } - .setOnCancelListener { - dialog = null - finish() - } - .setNegativeButton(R.string.action_quit_session) { _, _ -> - dialog = null - finish() - } - .create() - dialogContents = StreamQuitDialog - dialog.show() + if(state.reason.isError) + { + dialog?.dismiss() + val reasonStr = state.reasonString + val dialog = MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) + + (if(reasonStr != null) "\n$reasonStr" else "")) + .setPositiveButton(R.string.action_reconnect) { _, _ -> + dialog = null + reconnect() + } + .setOnCancelListener { + dialog = null + finish() + } + .setNegativeButton(R.string.action_quit_session) { _, _ -> + dialog = null + finish() + } + .create() + dialogContents = StreamQuitDialog + dialog.show() + } + else + finish() } } @@ -287,77 +311,89 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe dialog.show() } } + + else -> {} } } - private fun adjustTextureViewAspect() + private fun adjustTextureViewAspect(textureView: TextureView) { - val displayInfo = DisplayInfo(viewModel.session.connectInfo.videoProfile, textureView) - val resolution = displayInfo.computeResolutionFor(displayModeToggle.checkedButtonId) - + val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView) + val resolution = trans.resolutionFor(TransformMode.fromButton(binding.displayModeToggle.checkedButtonId)) Matrix().also { textureView.getTransform(it) - it.setScale(resolution.width / displayInfo.viewWidth, resolution.height / displayInfo.viewHeight) - it.postTranslate((displayInfo.viewWidth - resolution.width) * 0.5f, (displayInfo.viewHeight - resolution.height) * 0.5f) + it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight) + it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f) textureView.setTransform(it) } } + private fun adjustSurfaceViewAspect() + { + val videoProfile = viewModel.session.connectInfo.videoProfile + binding.aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() + binding.aspectRatioLayout.mode = TransformMode.fromButton(binding.displayModeToggle.checkedButtonId) + } + + private fun adjustStreamViewAspect() = adjustSurfaceViewAspect() + override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) - } - -class DisplayInfo constructor(val videoProfile: ConnectVideoProfile, val textureView: TextureView) +enum class TransformMode { - val contentWidth : Float get() = videoProfile.width.toFloat() - val contentHeight : Float get() = videoProfile.height.toFloat() + FIT, + STRETCH, + ZOOM; + + companion object + { + fun fromButton(displayModeButtonId: Int) + = when (displayModeButtonId) + { + R.id.display_mode_stretch_button -> STRETCH + R.id.display_mode_zoom_button -> ZOOM + else -> FIT + } + } +} + +class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView) +{ + private val contentWidth : Float get() = videoProfile.width.toFloat() + private val contentHeight : Float get() = videoProfile.height.toFloat() val viewWidth : Float get() = textureView.width.toFloat() val viewHeight : Float get() = textureView.height.toFloat() - val contentAspect : Float get() = contentHeight / contentWidth + private val contentAspect : Float get() = contentHeight / contentWidth - fun computeResolutionFor(displayModeButtonId: Int) : Resolution - { - when (displayModeButtonId) + fun resolutionFor(mode: TransformMode): Resolution + = when(mode) { - R.id.display_mode_stretch_button -> return computeStrechedResolution() - R.id.display_mode_zoom_button -> return computeZoomedResolution() - else -> return computeNormalResolution() + TransformMode.STRETCH -> strechedResolution + TransformMode.ZOOM -> zoomedResolution + TransformMode.FIT -> normalResolution } - } - private fun computeStrechedResolution(): Resolution - { - return Resolution(viewWidth, viewHeight) - } + private val strechedResolution get() = Resolution(viewWidth, viewHeight) - private fun computeZoomedResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) + private val zoomedResolution get() = + if(viewHeight > viewWidth * contentAspect) { val zoomFactor = viewHeight / contentHeight - return Resolution(contentWidth * zoomFactor, viewHeight) + Resolution(contentWidth * zoomFactor, viewHeight) } else { val zoomFactor = viewWidth / contentWidth - return Resolution(viewWidth, contentHeight * zoomFactor) + Resolution(viewWidth, contentHeight * zoomFactor) } - } - private fun computeNormalResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) - { - return Resolution(viewWidth, viewWidth * contentAspect) - } + private val normalResolution get() = + if(viewHeight > viewWidth * contentAspect) + Resolution(viewWidth, viewWidth * contentAspect) else - { - return Resolution(viewHeight / contentAspect, viewHeight) - } - } - + Resolution(viewHeight / contentAspect, viewHeight) } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt index ebaaf80..df69378 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt @@ -2,19 +2,22 @@ package com.metallic.chiaki.stream -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import android.app.Application +import android.content.Context +import androidx.lifecycle.* import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.session.StreamSession import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.* import com.metallic.chiaki.session.StreamInput -class StreamViewModel(val preferences: Preferences, val logManager: LogManager, val connectInfo: ConnectInfo): ViewModel() +class StreamViewModel(val application: Application, val connectInfo: ConnectInfo): ViewModel() { + val preferences = Preferences(application) + val logManager = LogManager(application) + private var _session: StreamSession? = null - val input = StreamInput(preferences) + val input = StreamInput(application, preferences) val session = StreamSession(connectInfo, logManager, preferences.logVerbose, input) private var _onScreenControlsEnabled = MutableLiveData(preferences.onScreenControlsEnabled) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt new file mode 100644 index 0000000..909eab4 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt @@ -0,0 +1,29 @@ +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.appcompat.app.AppCompatActivity +import com.metallic.chiaki.common.Preferences + +class ButtonHaptics(val context: Context) +{ + private val enabled = Preferences(context).buttonHapticEnabled + + fun trigger(harder: Boolean = false) + { + if(!enabled) + return + val vibrator = context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate( + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + VibrationEffect.createPredefined(if(harder) VibrationEffect.EFFECT_CLICK else VibrationEffect.EFFECT_TICK) + else + VibrationEffect.createOneShot(10, if(harder) 200 else 100) + ) + else + vibrator.vibrate(10) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index b2eba6b..66a9afe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -6,14 +6,19 @@ import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import androidx.core.view.children import com.metallic.chiaki.R class ButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + var buttonPressed = false private set(value) { @@ -21,6 +26,8 @@ class ButtonView @JvmOverloads constructor( field = value if(diff) { + if(value) + haptics.trigger() invalidate() buttonPressedCallback?.let { it(field) } } @@ -46,16 +53,39 @@ class ButtonView @JvmOverloads constructor( { super.onDraw(canvas) val drawable = if(buttonPressed) drawablePressed else drawableIdle - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) drawable?.draw(canvas) } + /** + * If this button overlaps with others in the same layout, + * let the one whose center is closest to the touch handle it. + */ + private fun bestFittingTouchView(x: Float, y: Float): View + { + val loc = locationOnScreen + Vector(x, y) + return (parent as? ViewGroup)?.children?.filter { + it is ButtonView + }?.filter { + val pos = it.locationOnScreen + loc.x >= pos.x && loc.x < pos.x + it.width && loc.y >= pos.y && loc.y < pos.y + it.height + }?.sortedBy { + (loc - (it.locationOnScreen + Vector(it.width.toFloat(), it.height.toFloat()) * 0.5f)).lengthSq + }?.firstOrNull() ?: this + } + override fun onTouchEvent(event: MotionEvent): Boolean { - when(event.action) + when(event.actionMasked) { - MotionEvent.ACTION_DOWN -> buttonPressed = true - MotionEvent.ACTION_UP -> buttonPressed = false + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if(bestFittingTouchView(event.getX(event.actionIndex), event.getY(event.actionIndex)) != this) + return false + buttonPressed = true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + buttonPressed = false + } } return true } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 9386e8f..9c359d8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -16,6 +16,8 @@ class DPadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + enum class Direction { LEFT, RIGHT, @@ -71,7 +73,7 @@ class DPadView @JvmOverloads constructor( else drawable = dpadIdleDrawable - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) //drawable?.alpha = 127 drawable?.draw(canvas) } @@ -113,6 +115,8 @@ class DPadView @JvmOverloads constructor( if(state != newState) { + if(newState != null) + haptics.trigger() state = newState invalidate() stateChangeCallback?.let { it(state) } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 9c4ca51..b1ecfbe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -3,74 +3,97 @@ package com.metallic.chiaki.touchcontrols import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R +import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* +import io.reactivex.Observable +import io.reactivex.rxkotlin.Observables.combineLatest +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject -class TouchControlsFragment : Fragment() +abstract class TouchControlsFragment : Fragment() { - private var controllerState = ControllerState() - private set(value) + protected var ownControllerState = ControllerState() + set(value) { val diff = field != value field = value if(diff) - controllerStateCallback?.let { it(value) } + ownControllerStateSubject.onNext(ownControllerState) } - var controllerStateCallback: ((ControllerState) -> Unit)? = null - var onScreenControlsEnabled: LiveData? = null + protected val ownControllerStateSubject: Subject + = BehaviorSubject.create().also { it.onNext(ownControllerState) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_controls, container, false) + // to delay attaching to the touchpadView until it's available + protected val controllerStateProxy: Subject> + = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } + val controllerState: Observable get() = + controllerStateProxy.flatMap { it } + + var onScreenControlsEnabled: LiveData? = null +} + +class DefaultTouchControlsFragment : TouchControlsFragment() +{ + private var _binding: FragmentControlsBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentControlsBinding.inflate(inflater, container, false).let { + _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - dpadView.stateChangeCallback = this::dpadStateChanged - crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) - moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) - pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) - boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) - l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) - r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) - optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) - shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) - psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) + binding.dpadView.stateChangeCallback = this::dpadStateChanged + binding.crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) + binding.moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) + binding.pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) + binding.boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) + binding.l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) + binding.r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) + binding.l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) + binding.r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) + binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) + binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) + binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } - r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } + binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } } + binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } } val quantizeStick = { f: Float -> - (Short.MAX_VALUE * f).toShort() + (Short.MAX_VALUE * f).toInt().toShort() } - leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.leftAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { leftX = quantizeStick(it.x) leftY = quantizeStick(it.y) }} - rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.rightAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { rightX = quantizeStick(it.x) rightY = quantizeStick(it.y) }} - onScreenControlsEnabled?.observe(this, Observer { + onScreenControlsEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } private fun dpadStateChanged(direction: DPadView.Direction?) { - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = ((buttons and ControllerState.BUTTON_DPAD_LEFT.inv() and ControllerState.BUTTON_DPAD_RIGHT.inv() @@ -92,7 +115,7 @@ class TouchControlsFragment : Fragment() } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt index 01d6869..a1ead71 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -26,7 +26,7 @@ class TouchTracker if(pointerId == null) { pointerId = event.getPointerId(event.actionIndex) - currentPosition = Vector(event.x, event.y) + currentPosition = Vector(event.getX(event.actionIndex), event.getY(event.actionIndex)) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 610c099..8910ef7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -6,49 +6,42 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R -import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* +import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding +import io.reactivex.rxkotlin.Observables.combineLatest -class TouchpadOnlyFragment : Fragment() +class TouchpadOnlyFragment : TouchControlsFragment() { - private var controllerState = ControllerState() - private set(value) - { - val diff = field != value - field = value - if(diff) - controllerStateCallback?.let { it(value) } - } - - var controllerStateCallback: ((ControllerState) -> Unit)? = null var touchpadOnlyEnabled: LiveData? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_touchpad_only, container, false) + private var _binding: FragmentTouchpadOnlyBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let { + _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - - touchpadOnlyEnabled?.observe(this, Observer { + touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask else buttons and buttonMask.inv() - } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt new file mode 100644 index 0000000..a7e7b85 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.metallic.chiaki.R +import com.metallic.chiaki.lib.ControllerState +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import kotlin.math.max + +class TouchpadView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) +{ + companion object + { + private const val BUTTON_PRESS_MAX_MOVE_DIST_DP = 32.0f + private const val SHORT_BUTTON_PRESS_DURATION_MS = 200L + private const val BUTTON_HOLD_DELAY_MS = 500L + } + + private val haptics = ButtonHaptics(context) + + private val drawableIdle: Drawable? + private val drawablePressed: Drawable? + + private val state: ControllerState = ControllerState() + + inner class Touch( + val stateId: UByte, + private val startX: Float, + private val startY: Float) + { + var lifted = false // will be true but touch still in list when only relevant for short touch + private var maxDist: Float = 0.0f + val moveInsignificant: Boolean get() = maxDist < BUTTON_PRESS_MAX_MOVE_DIST_DP + + fun onMove(x: Float, y: Float) + { + val d = (Vector(x, y) - Vector(startX, startY)).length / resources.displayMetrics.density + maxDist = max(d, maxDist) + } + + val startButtonHoldRunnable = Runnable { + if(!moveInsignificant || buttonHeld) + return@Runnable + haptics.trigger(true) + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + buttonHeld = true + triggerStateChanged() + } + } + private val pointerTouches = mutableMapOf() + + private val stateSubject: Subject + = BehaviorSubject.create().also { it.onNext(state) } + val controllerState: Observable get() = stateSubject + + private var shortPressingTouches = listOf() + private val shortButtonPressLiftRunnable = Runnable { + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + shortPressingTouches.forEach { + state.stopTouch(it.stateId) + } + shortPressingTouches = listOf() + triggerStateChanged() + } + + private var buttonHeld = false + + init + { + context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { + drawableIdle = getDrawable(R.styleable.TouchpadView_drawableIdle) + drawablePressed = getDrawable(R.styleable.TouchpadView_drawablePressed) + recycle() + } + isClickable = true + } + + override fun onDraw(canvas: Canvas) + { + super.onDraw(canvas) + if(pointerTouches.values.find { !it.lifted } == null) + return + val drawable = if(state.buttons and ControllerState.BUTTON_TOUCHPAD != 0U) drawablePressed else drawableIdle + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) + drawable?.draw(canvas) + } + + private fun touchX(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_WIDTH - 1u).toUShort(), + (ControllerState.TOUCHPAD_WIDTH.toFloat() * event.getX(index) / width.toFloat()).toUInt().toUShort())) + + private fun touchY(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_HEIGHT - 1u).toUShort(), + (ControllerState.TOUCHPAD_HEIGHT.toFloat() * event.getY(index) / height.toFloat()).toUInt().toUShort())) + + override fun onTouchEvent(event: MotionEvent): Boolean + { + when(event.actionMasked) + { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + haptics.trigger() + val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex)) + pointerTouches[event.getPointerId(event.actionIndex)] = touch + if(!buttonHeld) + postDelayed(touch.startButtonHoldRunnable, BUTTON_HOLD_DELAY_MS) + triggerStateChanged() + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + pointerTouches.remove(event.getPointerId(event.actionIndex))?.let { + removeCallbacks(it.startButtonHoldRunnable) + when + { + buttonHeld -> + { + buttonHeld = false + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + state.stopTouch(it.stateId) + } + it.moveInsignificant -> triggerShortButtonPress(it) + else -> state.stopTouch(it.stateId) + } + triggerStateChanged() + } + } + MotionEvent.ACTION_MOVE -> { + val changed = pointerTouches.entries.fold(false) { acc, it -> + val index = event.findPointerIndex(it.key) + if(index < 0) + acc + else + { + it.value.onMove(event.getX(event.actionIndex), event.getY(event.actionIndex)) + acc || state.setTouchPos(it.value.stateId, touchX(event, index), touchY(event, index)) + } + } + if(changed) + triggerStateChanged() + } + } + return true + } + + private fun triggerShortButtonPress(touch: Touch) + { + shortPressingTouches = shortPressingTouches + listOf(touch) + removeCallbacks(shortButtonPressLiftRunnable) + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + postDelayed(shortButtonPressLiftRunnable, SHORT_BUTTON_PRESS_DURATION_MS) + } + + private fun triggerStateChanged() + { + invalidate() + stateSubject.onNext(state) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt index 36216e8..208c41a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -2,6 +2,7 @@ package com.metallic.chiaki.touchcontrols +import android.view.View import kotlin.math.sqrt data class Vector(val x: Float, val y: Float) @@ -18,4 +19,10 @@ data class Vector(val x: Float, val y: Float) val lengthSq get() = x*x + y*y val length get() = sqrt(lengthSq) val normalized get() = this / length +} + +val View.locationOnScreen: Vector get() { + val v = intArrayOf(0, 0) + this.getLocationOnScreen(v) + return Vector(v[0].toFloat(), v[1].toFloat()) } \ No newline at end of file diff --git a/android/app/src/main/res/drawable/control_button_l1.xml b/android/app/src/main/res/drawable/control_button_l1.xml index a709f25..17f9594 100644 --- a/android/app/src/main/res/drawable/control_button_l1.xml +++ b/android/app/src/main/res/drawable/control_button_l1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l1_pressed.xml b/android/app/src/main/res/drawable/control_button_l1_pressed.xml index b8bcce0..0267190 100644 --- a/android/app/src/main/res/drawable/control_button_l1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2.xml b/android/app/src/main/res/drawable/control_button_l2.xml index 6867aa0..8e75f40 100644 --- a/android/app/src/main/res/drawable/control_button_l2.xml +++ b/android/app/src/main/res/drawable/control_button_l2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2_pressed.xml b/android/app/src/main/res/drawable/control_button_l2_pressed.xml index e4df2de..b120891 100644 --- a/android/app/src/main/res/drawable/control_button_l2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l3.xml b/android/app/src/main/res/drawable/control_button_l3.xml new file mode 100644 index 0000000..34c3382 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_l3_pressed.xml b/android/app/src/main/res/drawable/control_button_l3_pressed.xml new file mode 100644 index 0000000..43f239a --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r1.xml b/android/app/src/main/res/drawable/control_button_r1.xml index a0b3262..d614419 100644 --- a/android/app/src/main/res/drawable/control_button_r1.xml +++ b/android/app/src/main/res/drawable/control_button_r1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r1_pressed.xml b/android/app/src/main/res/drawable/control_button_r1_pressed.xml index 5cadcb6..8d51983 100644 --- a/android/app/src/main/res/drawable/control_button_r1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2.xml b/android/app/src/main/res/drawable/control_button_r2.xml index c6a625d..dd16994 100644 --- a/android/app/src/main/res/drawable/control_button_r2.xml +++ b/android/app/src/main/res/drawable/control_button_r2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2_pressed.xml b/android/app/src/main/res/drawable/control_button_r2_pressed.xml index 260e60b..7cc84b1 100644 --- a/android/app/src/main/res/drawable/control_button_r2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r3.xml b/android/app/src/main/res/drawable/control_button_r3.xml new file mode 100644 index 0000000..0f2bcfd --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r3_pressed.xml b/android/app/src/main/res/drawable/control_button_r3_pressed.xml new file mode 100644 index 0000000..08635e8 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_touchpad.xml b/android/app/src/main/res/drawable/control_button_touchpad.xml deleted file mode 100644 index 40c0465..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml deleted file mode 100644 index 8255860..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_touchpad.xml b/android/app/src/main/res/drawable/control_touchpad.xml new file mode 100644 index 0000000..509b8a3 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_touchpad_pressed.xml new file mode 100644 index 0000000..b4f3f16 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_button_haptic.xml b/android/app/src/main/res/drawable/ic_button_haptic.xml new file mode 100644 index 0000000..fea4c99 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_button_haptic.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_motion.xml b/android/app/src/main/res/drawable/ic_motion.xml new file mode 100644 index 0000000..c5a4ea1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_motion.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_rumble.xml b/android/app/src/main/res/drawable/ic_rumble.xml new file mode 100644 index 0000000..4ecdb61 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_rumble.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 255ccde..6a7f9a3 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -1,16 +1,24 @@ - - + android:layout_height="match_parent" + android:layout_gravity="center"> + + @@ -34,7 +42,6 @@ android:id="@+id/overlay" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_gravity="bottom" android:fitsSystemWindows="true"> + android:clipChildren="false" + tools:ignore="RtlHardcoded,RtlSymmetry"> + + + app:layout_constraintBottom_toBottomOf="parent" /> + + + + + - - + app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent"/> - - + android:layout_marginLeft="80dp" + android:layout_marginRight="80dp"/> \ No newline at end of file diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml index b0a1170..04f738a 100644 --- a/android/app/src/main/res/values/attrs.xml +++ b/android/app/src/main/res/values/attrs.xml @@ -1,8 +1,11 @@ + + + - - + + @@ -11,4 +14,9 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml index 543f0f4..9ee2e9b 100644 --- a/android/app/src/main/res/values/dimens.xml +++ b/android/app/src/main/res/values/dimens.xml @@ -1,6 +1,10 @@ - 48dp + 88dp + 176dp + 24dp + 16dp + 64dp 48dp 32dp 48dp diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0f54522..254042b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -88,6 +88,12 @@ H265 (PS5 only) Swap Cross/Moon and Box/Pyramid Buttons Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) + Rumble + Use phone vibration motor for rumble + Motion + Use device\'s motion sensors for controller motion + Touch Haptics + Use phone vibration motor for short haptic feedback on button touches Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -101,7 +107,10 @@ discovery_enabled on_screen_controls_enabled - touchpad_only_enabled + touchpad_only_enabled + rumble_enabled + motion_enabled + button_haptic_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index 0416f6e..49989db 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -19,6 +19,24 @@ app:summary="@string/preferences_swap_cross_moon_summary" app:icon="@drawable/ic_gamepad" /> + + + + + + + + + diff --git a/assets/chiaki_macos_simple.svg b/assets/chiaki_macos_simple.svg new file mode 100644 index 0000000..7f1359d --- /dev/null +++ b/assets/chiaki_macos_simple.svg @@ -0,0 +1,102 @@ + + + + diff --git a/assets/controls/l1.svg b/assets/controls/l1.svg index ce67e89..9d3c3f9 100644 --- a/assets/controls/l1.svg +++ b/assets/controls/l1.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="l1.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/l2.svg b/assets/controls/l2.svg index 01e886f..4d561e0 100644 --- a/assets/controls/l2.svg +++ b/assets/controls/l2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="l2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/l3.svg b/assets/controls/l3.svg new file mode 100644 index 0000000..464e440 --- /dev/null +++ b/assets/controls/l3.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/l3_raw.svg b/assets/controls/l3_raw.svg new file mode 100644 index 0000000..f04135f --- /dev/null +++ b/assets/controls/l3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + L3 + + diff --git a/assets/controls/lr12.svg b/assets/controls/lr12.svg new file mode 100644 index 0000000..874475f --- /dev/null +++ b/assets/controls/lr12.svg @@ -0,0 +1,124 @@ + + + + + + + + image/svg+xml + + + + + + + + L1 + + + + R1 + + + + L2 + + + + R2 + + diff --git a/assets/controls/r1.svg b/assets/controls/r1.svg index ef51bc2..70f881c 100644 --- a/assets/controls/r1.svg +++ b/assets/controls/r1.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r1.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/r2.svg b/assets/controls/r2.svg index 350ffaa..c32639d 100644 --- a/assets/controls/r2.svg +++ b/assets/controls/r2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,13 @@ - diff --git a/assets/controls/r3.svg b/assets/controls/r3.svg new file mode 100644 index 0000000..b5f1fa7 --- /dev/null +++ b/assets/controls/r3.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/r3_raw.svg b/assets/controls/r3_raw.svg new file mode 100644 index 0000000..5f47c8e --- /dev/null +++ b/assets/controls/r3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + R3 + + diff --git a/assets/controls/touchpad_surface.svg b/assets/controls/touchpad_surface.svg new file mode 100644 index 0000000..7476128 --- /dev/null +++ b/assets/controls/touchpad_surface.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 83c37a9..83de1f9 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -15,3 +15,4 @@ endif() add_executable(chiaki-cli src/main.c) target_link_libraries(chiaki-cli chiaki-cli-lib) +install(TARGETS chiaki-cli) diff --git a/cli/src/discover.c b/cli/src/discover.c index 503b53a..cf9319f 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -142,13 +142,19 @@ CHIAKI_EXPORT int chiaki_cli_cmd_discover(ChiakiLog *log, int argc, char *argv[] return 1; } - ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); // TODO: IPv6, PS5, should probably use the service - ChiakiDiscoveryPacket packet; memset(&packet, 0, sizeof(packet)); packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; - - chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS4: %s", chiaki_error_string(err)); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS5); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS5: %s", chiaki_error_string(err)); while(1) sleep(1); // TODO: wtf diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index cf5ab45..cea2792 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -11,10 +11,14 @@ static char doc[] = "Send a PS4 wakeup packet."; #define ARG_KEY_HOST 'h' #define ARG_KEY_REGISTKEY 'r' +#define ARG_KEY_PS4 '4' +#define ARG_KEY_PS5 '5' static struct argp_option options[] = { { "host", ARG_KEY_HOST, "Host", 0, "Host to send wakeup packet to", 0 }, - { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "PS4 registration key", 0 }, + { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "Remote Play registration key (plaintext)", 0 }, + { "ps4", ARG_KEY_PS4, NULL, 0, "PlayStation 4", 0 }, + { "ps5", ARG_KEY_PS5, NULL, 0, "PlayStation 5 (default)", 0 }, { 0 } }; @@ -22,6 +26,7 @@ typedef struct arguments { const char *host; const char *registkey; + bool ps5; } Arguments; static int parse_opt(int key, char *arg, struct argp_state *state) @@ -39,6 +44,12 @@ static int parse_opt(int key, char *arg, struct argp_state *state) case ARGP_KEY_ARG: argp_usage(state); break; + case ARG_KEY_PS4: + arguments->ps5 = false; + break; + case ARG_KEY_PS5: + arguments->ps5 = true; + break; default: return ARGP_ERR_UNKNOWN; } @@ -51,6 +62,7 @@ static struct argp argp = { options, parse_opt, 0, doc, 0, 0, 0 }; CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) { Arguments arguments = { 0 }; + arguments.ps5 = true; error_t argp_r = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, &arguments); if(argp_r != 0) return 1; @@ -73,5 +85,5 @@ CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) uint64_t credential = (uint64_t)strtoull(arguments.registkey, NULL, 16); - return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, false); + return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, arguments.ps5); } diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index 32e8ac0..bdb66aa 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -49,7 +49,11 @@ function (_ffmpeg_find component headername) # Try pkg-config first if(PKG_CONFIG_FOUND) - pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules(FFMPEG_${component} lib${component}) + else() + pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + endif() if(FFMPEG_${component}_FOUND) if((TARGET PkgConfig::FFMPEG_${component}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) if(APPLE) @@ -69,6 +73,9 @@ function (_ffmpeg_find component headername) add_library(FFMPEG::${component} ALIAS PkgConfig::FFMPEG_${component}) else() add_library("FFMPEG::${component}" INTERFACE IMPORTED) + if(CMAKE_VERSION VERSION_LESS "3.6") + link_directories("${FFMPEG_${component}_LIBRARY_DIRS}") + endif() set_target_properties("FFMPEG::${component}" PROPERTIES INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIRS}" @@ -224,10 +231,14 @@ foreach (_ffmpeg_component IN LISTS FFMPEG_FIND_COMPONENTS) list(APPEND _ffmpeg_required_vars "FFMPEG_${_ffmpeg_component}_LIBRARIES") else() - set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS - "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") - set(FFMPEG_${_ffmpeg_component}_LIBRARIES - "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + if(NOT FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS) + set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS + "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") + endif() + if(NOT FFMPEG_${_ffmpeg_component}_LIBRARIES) + set(FFMPEG_${_ffmpeg_component}_LIBRARIES + "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + endif() list(APPEND FFMPEG_INCLUDE_DIRS "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS}") list(APPEND FFMPEG_LIBRARIES diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake index a0b9ebd..03717b0 100644 --- a/cmake/FindSDL2.cmake +++ b/cmake/FindSDL2.cmake @@ -1,6 +1,26 @@ find_package(SDL2 NO_MODULE QUIET) +# Adapted from libsdl-org/SDL_ttf: https://github.com/libsdl-org/SDL_ttf/blob/main/cmake/FindPrivateSDL2.cmake#L19-L31 +# Copyright (C) 1997-2022 Sam Lantinga +# Licensed under the zlib license (https://github.com/libsdl-org/SDL_ttf/blob/main/LICENSE.txt) +set(SDL2_VERSION_MAJOR) +set(SDL2_VERSION_MINOR) +set(SDL2_VERSION_PATCH) +set(SDL2_VERSION) +if(SDL2_INCLUDE_DIR) + file(READ "${SDL2_INCLUDE_DIR}/SDL_version.h" _sdl_version_h) + string(REGEX MATCH "#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)" _sdl2_major_re "${_sdl_version_h}") + set(SDL2_VERSION_MAJOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)" _sdl2_minor_re "${_sdl_version_h}") + set(SDL2_VERSION_MINOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)" _sdl2_patch_re "${_sdl_version_h}") + set(SDL2_VERSION_PATCH "${CMAKE_MATCH_1}") + if(_sdl2_major_re AND _sdl2_minor_re AND _sdl2_patch_re) + set(SDL2_VERSION "${SDL2_VERSION_MAJOR}.${SDL2_VERSION_MINOR}.${SDL2_VERSION_PATCH}") + endif() +endif() + if(SDL2_FOUND AND (NOT TARGET SDL2::SDL2)) add_library(SDL2::SDL2 UNKNOWN IMPORTED GLOBAL) if(NOT SDL2_LIBDIR) diff --git a/cmake/OpenSSLExternalProject.cmake b/cmake/OpenSSLExternalProject.cmake index 0538d48..1e1370d 100644 --- a/cmake/OpenSSLExternalProject.cmake +++ b/cmake/OpenSSLExternalProject.cmake @@ -33,8 +33,8 @@ endif() find_program(MAKE_EXE NAMES gmake make) ExternalProject_Add(OpenSSL-ExternalProject - URL https://www.openssl.org/source/openssl-1.1.1d.tar.gz - URL_HASH SHA256=1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 + URL https://www.openssl.org/source/openssl-1.1.1s.tar.gz + URL_HASH SHA256=c5ac01e760ee6ff0dab61d6b2bbd30146724d063eb322180c6f18a6f74e4b6aa INSTALL_DIR "${OPENSSL_INSTALL_DIR}" CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${OPENSSL_BUILD_ENV} "/Configure" "--prefix=" no-shared ${OPENSSL_CONFIG_EXTRA_ARGS} "${OPENSSL_OS_COMPILER}" diff --git a/cmake/switch.cmake b/cmake/switch.cmake index 6046d67..7e600be 100644 --- a/cmake/switch.cmake +++ b/cmake/switch.cmake @@ -1,34 +1,15 @@ # Find DEVKITPRO set(DEVKITPRO "$ENV{DEVKITPRO}" CACHE PATH "Path to DevKitPro") -set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}" CACHE PATH "Path to portlibs inside DevKitPro") -if(NOT DEVKITPRO OR NOT PORTLIBS_PREFIX) - message(FATAL_ERROR "Please set DEVKITPRO & PORTLIBS_PREFIX env before calling cmake. https://devkitpro.org/wiki/Getting_Started") +if(NOT DEVKITPRO) + message(FATAL_ERROR "Please set DEVKITPRO env before calling cmake. https://devkitpro.org/wiki/Getting_Started") endif() # include devkitpro toolchain -include("${DEVKITPRO}/switch.cmake") +include("${DEVKITPRO}/cmake/Switch.cmake") set(NSWITCH TRUE) -# Enable gcc -g, to use -# /opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C -# set(CMAKE_BUILD_TYPE Debug) -# set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Shared libs not available" ) - -# FIXME rework this file to use the toolchain only -# https://github.com/diasurgical/devilutionX/pull/764 -set(ARCH "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -ftls-model=local-exec") -# set(CMAKE_C_FLAGS "-O2 -ffunction-sections ${ARCH}") -set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") -# workaroud force -fPIE to avoid -# aarch64-none-elf/bin/ld: read-only segment has dynamic relocations -set(CMAKE_EXE_LINKER_FLAGS "-specs=${DEVKITPRO}/libnx/switch.specs ${ARCH} -fPIE -Wl,-Map,Output.map") - -# add portlibs to the list of include dir -include_directories("${PORTLIBS_PREFIX}/include") - # troubleshoot message(STATUS "CMAKE_FIND_ROOT_PATH = ${CMAKE_FIND_ROOT_PATH}") message(STATUS "PKG_CONFIG_EXECUTABLE = ${PKG_CONFIG_EXECUTABLE}") @@ -79,4 +60,3 @@ function(add_nro_target output_name target title author version icon romfs) endfunction() set(CMAKE_USE_SYSTEM_ENVIRONMENT_PATH OFF) -set(CMAKE_PREFIX_PATH "/") diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 5f44908..443150f 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -6,9 +6,6 @@ find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Concurrent Multimedia Open if(APPLE) find_package(Qt5 REQUIRED COMPONENTS MacExtras) endif() -if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - find_package(SDL2 MODULE REQUIRED) -endif() if(WIN32) add_definitions(-DWIN32_LEAN_AND_MEAN) @@ -70,12 +67,12 @@ if(CHIAKI_ENABLE_CLI) endif() target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg) +target_link_libraries(chiaki SDL2::SDL2) if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS) endif() if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - target_link_libraries(chiaki SDL2::SDL2) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) endif() if(CHIAKI_ENABLE_SETSU) diff --git a/gui/chiaki.icns b/gui/chiaki.icns index 13613ff..e51fa65 100644 Binary files a/gui/chiaki.icns and b/gui/chiaki.icns differ diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index 6f234e9..19bd287 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -3,6 +3,8 @@ #ifndef CHIAKI_AVOPENGLWIDGET_H #define CHIAKI_AVOPENGLWIDGET_H +#include "transformmode.h" + #include #include @@ -74,21 +76,24 @@ class AVOpenGLWidget: public QOpenGLWidget public: static QSurfaceFormat CreateSurfaceFormat(); - explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr); + explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr, TransformMode transform_mode = TransformMode::Fit); ~AVOpenGLWidget() override; void SwapFrames(); AVOpenGLFrame *GetBackgroundFrame() { return &frames[1 - frame_fg]; } + void SetTransformMode(TransformMode mode) { transform_mode = mode; } + TransformMode GetTransformMode() const { return transform_mode; } + protected: + TransformMode transform_mode; void mouseMoveEvent(QMouseEvent *event) override; void initializeGL() override; void paintGL() override; - private slots: - void ResetMouseTimeout(); public slots: + void ResetMouseTimeout(); void HideMouse(); }; diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 6ec6efb..eee0120 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -12,8 +12,12 @@ #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER #include +#include #endif +#define PS_TOUCHPAD_MAX_X 1920 +#define PS_TOUCHPAD_MAX_Y 1079 + class Controller; class ControllerManager : public QObject @@ -33,7 +37,9 @@ class ControllerManager : public QObject private slots: void UpdateAvailableControllers(); void HandleEvents(); - void ControllerEvent(int device_id); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void ControllerEvent(SDL_Event evt); +#endif public: static ControllerManager *GetInstance(); @@ -57,12 +63,24 @@ class Controller : public QObject private: Controller(int device_id, ControllerManager *manager); - void UpdateState(); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void UpdateState(SDL_Event event); + bool HandleButtonEvent(SDL_ControllerButtonEvent event); + bool HandleAxisEvent(SDL_ControllerAxisEvent event); +#if SDL_VERSION_ATLEAST(2, 0, 14) + bool HandleSensorEvent(SDL_ControllerSensorEvent event); + bool HandleTouchpadEvent(SDL_ControllerTouchpadEvent event); +#endif +#endif ControllerManager *manager; int id; + ChiakiOrientationTracker orientation_tracker; + ChiakiControllerState state; + bool is_dualsense; #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + QMap, uint8_t> touch_ids; SDL_GameController *controller; #endif @@ -73,9 +91,44 @@ class Controller : public QObject int GetDeviceID(); QString GetName(); ChiakiControllerState GetState(); + void SetRumble(uint8_t left, uint8_t right); + void SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right); + bool IsDualSense(); signals: void StateChanged(); }; +/* PS5 trigger effect documentation: + https://controllers.fandom.com/wiki/Sony_DualSense#FFB_Trigger_Modes + + Taken from SDL2, licensed under the zlib license, + Copyright (C) 1997-2022 Sam Lantinga + https://github.com/libsdl-org/SDL/blob/release-2.24.1/test/testgamecontroller.c#L263-L289 +*/ +typedef struct +{ + Uint8 ucEnableBits1; /* 0 */ + Uint8 ucEnableBits2; /* 1 */ + Uint8 ucRumbleRight; /* 2 */ + Uint8 ucRumbleLeft; /* 3 */ + Uint8 ucHeadphoneVolume; /* 4 */ + Uint8 ucSpeakerVolume; /* 5 */ + Uint8 ucMicrophoneVolume; /* 6 */ + Uint8 ucAudioEnableBits; /* 7 */ + Uint8 ucMicLightMode; /* 8 */ + Uint8 ucAudioMuteBits; /* 9 */ + Uint8 rgucRightTriggerEffect[11]; /* 10 */ + Uint8 rgucLeftTriggerEffect[11]; /* 21 */ + Uint8 rgucUnknown1[6]; /* 32 */ + Uint8 ucLedFlags; /* 38 */ + Uint8 rgucUnknown2[2]; /* 39 */ + Uint8 ucLedAnim; /* 41 */ + Uint8 ucLedBrightness; /* 42 */ + Uint8 ucPadLights; /* 43 */ + Uint8 ucLedRed; /* 44 */ + Uint8 ucLedGreen; /* 45 */ + Uint8 ucLedBlue; /* 46 */ +} DS5EffectsState_t; + #endif // CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index f1190de..4112c19 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -55,6 +55,7 @@ class MainWindow : public QMainWindow void UpdateDiscoveryEnabled(); void ShowSettings(); + void Quit(); void UpdateDisplayServers(); void UpdateServerWidgets(); diff --git a/gui/include/settings.h b/gui/include/settings.h index c2320b8..00f38ab 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -63,6 +63,9 @@ class Settings : public QObject void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); } uint32_t GetLogLevelMask(); + bool GetDualSenseEnabled() const { return settings.value("settings/dualsense_enabled", false).toBool(); } + void SetDualSenseEnabled(bool enabled) { settings.setValue("settings/dualsense_enabled", enabled); } + ChiakiVideoResolutionPreset GetResolution() const; void SetResolution(ChiakiVideoResolutionPreset resolution); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index d564bef..00af648 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -20,6 +20,7 @@ class SettingsDialog : public QDialog QCheckBox *log_verbose_check_box; QComboBox *disconnect_action_combo_box; + QCheckBox *dualsense_check_box; QComboBox *resolution_combo_box; QComboBox *fps_combo_box; @@ -37,6 +38,7 @@ class SettingsDialog : public QDialog private slots: void LogVerboseChanged(); + void DualSenseChanged(); void DisconnectActionSelected(); void ResolutionSelected(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index c98758e..4b0ba01 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -13,12 +13,14 @@ #if CHIAKI_GUI_ENABLE_SETSU #include +#include #endif #include "exception.h" #include "sessionlog.h" #include "controllermanager.h" #include "settings.h" +#include "transformmode.h" #include #include @@ -52,9 +54,18 @@ struct StreamSessionConnectInfo ChiakiConnectVideoProfile video_profile; unsigned int audio_buffer_size; bool fullscreen; + TransformMode transform_mode; bool enable_keyboard; + bool enable_dualsense; - StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); + StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode); }; class StreamSession : public QObject @@ -74,6 +85,9 @@ class StreamSession : public QObject Setsu *setsu; QMap, uint8_t> setsu_ids; ChiakiControllerState setsu_state; + SetsuDevice *setsu_motion_device; + ChiakiOrientationTracker orient_tracker; + bool orient_dirty; #endif ChiakiControllerState keyboard_state; @@ -88,17 +102,23 @@ class StreamSession : public QObject unsigned int audio_buffer_size; QAudioOutput *audio_output; QIODevice *audio_io; + SDL_AudioDeviceID haptics_output; + uint8_t *haptics_resampler_buf; QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); - void Event(ChiakiEvent *event); + void PushHapticsFrame(uint8_t *buf, size_t buf_size); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); #endif private slots: void InitAudio(unsigned int channels, unsigned int rate); + void InitHaptics(); + void Event(ChiakiEvent *event); + void DisconnectHaptics(); + void ConnectHaptics(); public: explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); @@ -120,7 +140,7 @@ class StreamSession : public QObject #endif void HandleKeyboardEvent(QKeyEvent *event); - void HandleMouseEvent(QMouseEvent *event); + bool HandleMouseEvent(QMouseEvent *event); signals: void FfmpegFrameAvailable(); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 5c526b0..f3bd8bb 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -22,10 +22,14 @@ class StreamWindow: public QMainWindow const StreamSessionConnectInfo connect_info; StreamSession *session; + QAction *fullscreen_action; + QAction *stretch_action; + QAction *zoom_action; AVOpenGLWidget *av_widget; void Init(); void UpdateVideoTransform(); + void UpdateTransformModeActions(); protected: void keyPressEvent(QKeyEvent *event) override; @@ -42,6 +46,9 @@ class StreamWindow: public QMainWindow void SessionQuit(ChiakiQuitReason reason, const QString &reason_str); void LoginPINRequested(bool incorrect); void ToggleFullscreen(); + void ToggleStretch(); + void ToggleZoom(); + void Quit(); }; #endif // CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/include/transformmode.h b/gui/include/transformmode.h new file mode 100644 index 0000000..5c01d87 --- /dev/null +++ b/gui/include/transformmode.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_TRANSFORMMODE_H +#define CHIAKI_TRANSFORMMODE_H + +enum class TransformMode { + Fit, + Zoom, + Stretch +}; + +#endif diff --git a/gui/res/chiaki_macos.svg b/gui/res/chiaki_macos.svg new file mode 100644 index 0000000..7f1359d --- /dev/null +++ b/gui/res/chiaki_macos.svg @@ -0,0 +1,102 @@ + + + + diff --git a/gui/res/resources.qrc b/gui/res/resources.qrc index 86efda9..b89bee7 100644 --- a/gui/res/resources.qrc +++ b/gui/res/resources.qrc @@ -5,6 +5,7 @@ discover-24px.svg discover-off-24px.svg chiaki.svg + chiaki_macos.svg console-ps4.svg console-ps5.svg diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 3d1d8eb..bfd184a 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -109,7 +109,6 @@ static const float vert_pos[] = { 1.0f, 1.0f }; - QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() { QSurfaceFormat format; @@ -123,13 +122,13 @@ QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() return format; } -AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) +AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent, TransformMode transform_mode) : QOpenGLWidget(parent), - session(session) + session(session), transform_mode(transform_mode) { enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; - for(auto &cc: conversion_configs) + for(auto &cc : conversion_configs) { if(pixel_format == cc.pixel_format) { @@ -382,10 +381,10 @@ void AVOpenGLWidget::paintGL() vp_width = widget_width; vp_height = widget_height; } - else + else if(transform_mode == TransformMode::Fit) { float aspect = (float)frame->width / (float)frame->height; - if(aspect < (float)widget_width / (float)widget_height) + if(widget_height && aspect < (float)widget_width / (float)widget_height) { vp_height = widget_height; vp_width = (GLsizei)(vp_height * aspect); @@ -396,6 +395,34 @@ void AVOpenGLWidget::paintGL() vp_height = (GLsizei)(vp_width / aspect); } } + else if(transform_mode == TransformMode::Zoom) + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_width = widget_width; + vp_height = (GLsizei)(vp_width / aspect); + } + else + { + vp_height = widget_height; + vp_width = (GLsizei)(vp_height * aspect); + } + } + else // transform_mode == TransformMode::Stretch + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_height = widget_height; + vp_width = widget_width; + } + else + { + vp_width = widget_width; + vp_height = widget_height; + } + } f->glViewport((widget_width - vp_width) / 2, (widget_height - vp_height) / 2, vp_width, vp_height); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 2f9a2aa..8cc501f 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -68,6 +68,11 @@ static QSet chiaki_motion_controller_guids({ "030000008f0e00001431000000000000", }); +static QSet> chiaki_dualsense_controller_ids({ + // in format (vendor id, product id) + QPair(0x054c, 0x0ce6), // DualSense controller + QPair(0x054c, 0x0df2), // DualSense Edge controller +}); static ControllerManager *instance = nullptr; @@ -85,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent) { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER SDL_SetMainReady(); +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); +#endif if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { const char *err = SDL_GetError(); @@ -157,22 +171,51 @@ void ControllerManager::HandleEvents() break; case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONDOWN: - ControllerEvent(event.cbutton.which); - break; case SDL_CONTROLLERAXISMOTION: - ControllerEvent(event.caxis.which); +#if not defined(CHIAKI_ENABLE_SETSU) and SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: +#endif + ControllerEvent(event); break; } } #endif } -void ControllerManager::ControllerEvent(int device_id) +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void ControllerManager::ControllerEvent(SDL_Event event) { + int device_id; + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + device_id = event.cbutton.which; + break; + case SDL_CONTROLLERAXISMOTION: + device_id = event.caxis.which; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + device_id = event.csensor.which; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + device_id = event.ctouchpad.which; + break; +#endif + default: + return; + } if(!open_controllers.contains(device_id)) return; - open_controllers[device_id]->UpdateState(); + open_controllers[device_id]->UpdateState(event); } +#endif QSet ControllerManager::GetAvailableControllers() { @@ -197,10 +240,13 @@ void ControllerManager::ControllerClosed(Controller *controller) open_controllers.remove(controller->GetDeviceID()); } -Controller::Controller(int device_id, ControllerManager *manager) : QObject(manager) +Controller::Controller(int device_id, ControllerManager *manager) + : QObject(manager), is_dualsense(false) { this->id = device_id; this->manager = manager; + chiaki_orientation_tracker_init(&this->orientation_tracker); + chiaki_controller_state_set_idle(&this->state); #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER controller = nullptr; @@ -209,6 +255,14 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana if(SDL_JoystickGetDeviceInstanceID(i) == device_id) { controller = SDL_GameControllerOpen(i); +#if SDL_VERSION_ATLEAST(2, 0, 14) + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); +#endif + auto controller_id = QPair(SDL_GameControllerGetVendor(controller), SDL_GameControllerGetProduct(controller)); + is_dualsense = chiaki_dualsense_controller_ids.contains(controller_id); break; } } @@ -219,16 +273,197 @@ Controller::~Controller() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(controller) + { + // Clear trigger effects, SDL doesn't do it automatically + const uint8_t clear_effect[10] = { 0 }; + this->SetTriggerEffects(0x05, clear_effect, 0x05, clear_effect); SDL_GameControllerClose(controller); + } #endif manager->ControllerClosed(this); } -void Controller::UpdateState() +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void Controller::UpdateState(SDL_Event event) { + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if(!HandleButtonEvent(event.cbutton)) + return; + break; + case SDL_CONTROLLERAXISMOTION: + if(!HandleAxisEvent(event.caxis)) + return; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + if(!HandleSensorEvent(event.csensor)) + return; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + if(!HandleTouchpadEvent(event.ctouchpad)) + return; + break; +#endif + default: + return; + + } emit StateChanged(); } +inline bool Controller::HandleButtonEvent(SDL_ControllerButtonEvent event) { + ChiakiControllerButton ps_btn; + switch(event.button) + { + case SDL_CONTROLLER_BUTTON_A: + ps_btn = CHIAKI_CONTROLLER_BUTTON_CROSS; + break; + case SDL_CONTROLLER_BUTTON_B: + ps_btn = CHIAKI_CONTROLLER_BUTTON_MOON; + break; + case SDL_CONTROLLER_BUTTON_X: + ps_btn = CHIAKI_CONTROLLER_BUTTON_BOX; + break; + case SDL_CONTROLLER_BUTTON_Y: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PYRAMID; + break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_UP; + break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; + break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L1; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R1; + break; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L3; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R3; + break; + case SDL_CONTROLLER_BUTTON_START: + ps_btn = CHIAKI_CONTROLLER_BUTTON_OPTIONS; + break; + case SDL_CONTROLLER_BUTTON_BACK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_SHARE; + break; + case SDL_CONTROLLER_BUTTON_GUIDE: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PS; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLER_BUTTON_TOUCHPAD: + ps_btn = CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; + break; +#endif + default: + return false; + } + if(event.type == SDL_CONTROLLERBUTTONDOWN) + state.buttons |= ps_btn; + else + state.buttons &= ~ps_btn; + return true; +} + +inline bool Controller::HandleAxisEvent(SDL_ControllerAxisEvent event) { + switch(event.axis) + { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + state.l2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + state.r2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_LEFTX: + state.left_x = event.value; + break; + case SDL_CONTROLLER_AXIS_LEFTY: + state.left_y = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTX: + state.right_x = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTY: + state.right_y = event.value; + break; + default: + return false; + } + return true; +} + +#if SDL_VERSION_ATLEAST(2, 0, 14) +inline bool Controller::HandleSensorEvent(SDL_ControllerSensorEvent event) +{ + switch(event.sensor) + { + case SDL_SENSOR_ACCEL: + state.accel_x = event.data[0] / SDL_STANDARD_GRAVITY; + state.accel_y = event.data[1] / SDL_STANDARD_GRAVITY; + state.accel_z = event.data[2] / SDL_STANDARD_GRAVITY; + break; + case SDL_SENSOR_GYRO: + state.gyro_x = event.data[0]; + state.gyro_y = event.data[1]; + state.gyro_z = event.data[2]; + break; + default: + return false; + } + chiaki_orientation_tracker_update( + &orientation_tracker, state.gyro_x, state.gyro_y, state.gyro_z, + state.accel_x, state.accel_y, state.accel_z, event.timestamp * 1000); + chiaki_orientation_tracker_apply_to_controller_state(&orientation_tracker, &state); + return true; +} + +inline bool Controller::HandleTouchpadEvent(SDL_ControllerTouchpadEvent event) +{ + auto key = qMakePair(event.touchpad, event.finger); + bool exists = touch_ids.contains(key); + uint8_t chiaki_id; + switch(event.type) + { + case SDL_CONTROLLERTOUCHPADDOWN: + if(touch_ids.size() >= CHIAKI_CONTROLLER_TOUCHES_MAX) + return false; + chiaki_id = chiaki_controller_state_start_touch(&state, event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + touch_ids.insert(key, chiaki_id); + break; + case SDL_CONTROLLERTOUCHPADMOTION: + if(!exists) + return false; + chiaki_controller_state_set_touch_pos(&state, touch_ids[key], event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + break; + case SDL_CONTROLLERTOUCHPADUP: + if(!exists) + return false; + chiaki_controller_state_stop_touch(&state, touch_ids[key]); + touch_ids.remove(key); + break; + default: + return false; + } + return true; +} +#endif +#endif + bool Controller::IsConnected() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER @@ -252,7 +487,11 @@ QString Controller::GetName() #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(!controller) return QString(); - return SDL_GameControllerName(controller); + SDL_Joystick *js = SDL_GameControllerGetJoystick(controller); + SDL_JoystickGUID guid = SDL_JoystickGetGUID(js); + char guid_str[256]; + SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); + return QString("%1 (%2)").arg(SDL_JoystickName(js), guid_str); #else return QString(); #endif @@ -260,34 +499,39 @@ QString Controller::GetName() ChiakiControllerState Controller::GetState() { - ChiakiControllerState state; - chiaki_controller_state_set_idle(&state); -#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - if(!controller) - return state; - - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A) ? CHIAKI_CONTROLLER_BUTTON_CROSS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B) ? CHIAKI_CONTROLLER_BUTTON_MOON : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X) ? CHIAKI_CONTROLLER_BUTTON_BOX : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_Y) ? CHIAKI_CONTROLLER_BUTTON_PYRAMID : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP) ? CHIAKI_CONTROLLER_BUTTON_DPAD_UP : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN) ? CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_L1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_R1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSTICK) ? CHIAKI_CONTROLLER_BUTTON_L3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSTICK) ? CHIAKI_CONTROLLER_BUTTON_R3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START) ? CHIAKI_CONTROLLER_BUTTON_OPTIONS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK) ? CHIAKI_CONTROLLER_BUTTON_SHARE : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_GUIDE) ? CHIAKI_CONTROLLER_BUTTON_PS : 0; - state.l2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7); - state.r2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7); - state.left_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); - state.left_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); - state.right_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); - state.right_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); - -#endif return state; } + +void Controller::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return; + SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000); +#endif +} + +void Controller::SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right) +{ +#if defined(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) && SDL_VERSION_ATLEAST(2, 0, 16) + if(!is_dualsense || !controller) + return; + DS5EffectsState_t state; + SDL_zero(state); + state.ucEnableBits1 |= (0x04 /* left trigger */ | 0x08 /* right trigger */); + state.rgucLeftTriggerEffect[0] = type_left; + SDL_memcpy(state.rgucLeftTriggerEffect + 1, data_left, 10); + state.rgucRightTriggerEffect[0] = type_right; + SDL_memcpy(state.rgucRightTriggerEffect + 1, data_right, 10); + SDL_GameControllerSendEffect(controller, &state, sizeof(state)); +#endif +} + +bool Controller::IsDualSense() +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return false; + return is_dualsense; +#endif +} diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 56a85f2..5f2aad7 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -71,7 +71,11 @@ int real_main(int argc, char *argv[]) QApplication app(argc, argv); +#ifdef Q_OS_MACOS + QApplication::setWindowIcon(QIcon(":/icons/chiaki_macos.svg")); +#else QApplication::setWindowIcon(QIcon(":/icons/chiaki.svg")); +#endif QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); @@ -99,9 +103,15 @@ int real_main(int argc, char *argv[]) QCommandLineOption morning_option("morning", "", "morning"); parser.addOption(morning_option); - QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen (only for use with stream command)"); + QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen mode [maintains aspect ratio, adds black bars to fill unsused parts of screen if applicable] (only for use with stream command)"); parser.addOption(fullscreen_option); + QCommandLineOption zoom_option("zoom", "Start window in fullscreen zoomed in to fit screen [maintains aspect ratio, cutting off edges of image to fill screen] (only for use with stream command)"); + parser.addOption(zoom_option); + + QCommandLineOption stretch_option("stretch", "Start window in fullscreen stretched to fit screen [distorts aspect ratio to fill screen] (only for use with stream command)"); + parser.addOption(stretch_option); + parser.process(app); QStringList args = parser.positionalArguments(); @@ -111,7 +121,7 @@ int real_main(int argc, char *argv[]) if(args[0] == "list") { for(const auto &host : settings.GetRegisteredHosts()) - printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); + printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); return 0; } if(args[0] == "stream") @@ -123,25 +133,34 @@ int real_main(int argc, char *argv[]) QString host = args[args.size()-1]; QByteArray morning; QByteArray regist_key; + ChiakiTarget target = CHIAKI_TARGET_PS4_10; if(parser.value(regist_key_option).isEmpty() && parser.value(morning_option).isEmpty()) { if(args.length() < 3) parser.showHelp(1); + + bool found = false; for(const auto &temphost : settings.GetRegisteredHosts()) { if(temphost.GetServerNickname() == args[1]) { + found = true; morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); + target = temphost.GetTarget(); break; } + } + if(!found) + { printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); return 1; } } else { + // TODO: explicit option for target regist_key = parser.value(regist_key_option).toUtf8(); if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) { @@ -161,8 +180,21 @@ int real_main(int argc, char *argv[]) return 1; } } - // TODO: target here - StreamSessionConnectInfo connect_info(&settings, CHIAKI_TARGET_PS4_10, host, regist_key, morning, parser.isSet(fullscreen_option)); + if ((parser.isSet(stretch_option) && (parser.isSet(zoom_option) || parser.isSet(fullscreen_option))) || (parser.isSet(zoom_option) && parser.isSet(fullscreen_option))) + { + printf("Must choose between fullscreen, zoom or stretch option."); + return 1; + } + + StreamSessionConnectInfo connect_info( + &settings, + target, + host, + regist_key, + morning, + parser.isSet(fullscreen_option), + parser.isSet(zoom_option) ? TransformMode::Zoom : parser.isSet(stretch_option) ? TransformMode::Stretch : TransformMode::Fit); + return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 296626d..0912b2e 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -146,6 +146,11 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) AddToolBarAction(settings_action); connect(settings_action, &QAction::triggered, this, &MainWindow::ShowSettings); + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &MainWindow::Quit); + auto scroll_area = new QScrollArea(this); scroll_area->setWidgetResizable(true); scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -167,7 +172,7 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) grid_widget->setContentsMargins(0, 0, 0, 0); resize(800, 600); - + connect(&discovery_manager, &DiscoveryManager::HostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::RegisteredHostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::ManualHostsUpdated, this, &MainWindow::UpdateDisplayServers); @@ -249,7 +254,14 @@ void MainWindow::ServerItemWidgetTriggered() } QString host = server.GetHostAddr(); - StreamSessionConnectInfo info(settings, server.registered_host.GetTarget(), host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); + StreamSessionConnectInfo info( + settings, + server.registered_host.GetTarget(), + host, + server.registered_host.GetRPRegistKey(), + server.registered_host.GetRPKey(), + false, + TransformMode::Fit); new StreamWindow(info); } else @@ -298,6 +310,11 @@ void MainWindow::ShowSettings() dialog.exec(); } +void MainWindow::Quit() +{ + qApp->exit(); +} + void MainWindow::UpdateDisplayServers() { display_servers.clear(); @@ -330,7 +347,7 @@ void MainWindow::UpdateDisplayServers() display_servers.append(server); } - + UpdateServerWidgets(); } diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 17f7d83..6d296d1 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -88,10 +88,10 @@ uint32_t Settings::GetLogLevelMask() } static const QMap resolutions = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p" } }; static const ChiakiVideoResolutionPreset resolution_default = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -136,8 +136,8 @@ void Settings::SetBitrate(unsigned int bitrate) } static const QMap codecs = { - { CHIAKI_CODEC_H264, "h264"}, - { CHIAKI_CODEC_H265, "h265"} + { CHIAKI_CODEC_H264, "h264" }, + { CHIAKI_CODEC_H265, "h265" } }; static const ChiakiCodec codec_default = CHIAKI_CODEC_H265; diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index b8bd412..1770932 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -69,6 +69,11 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa log_verbose_check_box->setChecked(settings->GetLogVerbose()); connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged); + dualsense_check_box = new QCheckBox(this); + general_layout->addRow(tr("Extended DualSense Support:\nEnable haptics and adaptive triggers\nfor attached DualSense controllers.\nThis is currently experimental."), dualsense_check_box); + dualsense_check_box->setChecked(settings->GetDualSenseEnabled()); + connect(dualsense_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::DualSenseChanged); + auto log_directory_label = new QLineEdit(GetLogBaseDir(), this); log_directory_label->setReadOnly(true); general_layout->addRow(tr("Log Directory:"), log_directory_label); @@ -89,7 +94,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa connect(disconnect_action_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(DisconnectActionSelected())); general_layout->addRow(tr("Action on Disconnect:"), disconnect_action_combo_box); - + audio_device_combo_box = new QComboBox(this); audio_device_combo_box->addItem(tr("Auto")); auto current_audio_device = settings->GetAudioOutDevice(); @@ -112,7 +117,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa auto available_devices = audio_devices_future_watcher->result(); while(audio_device_combo_box->count() > 1) // remove all but "Auto" audio_device_combo_box->removeItem(1); - for (QAudioDeviceInfo di : available_devices) + for(QAudioDeviceInfo di : available_devices) audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); @@ -136,10 +141,10 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa resolution_combo_box = new QComboBox(this); static const QList> resolution_strings = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)" } }; auto current_res = settings->GetResolution(); for(const auto &p : resolution_strings) @@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged() settings->SetLogVerbose(log_verbose_check_box->isChecked()); } +void SettingsDialog::DualSenseChanged() +{ + settings->SetDualSenseEnabled(dualsense_check_box->isChecked()); +} + void SettingsDialog::FPSSelected() { settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt()); diff --git a/gui/src/settingskeycapturedialog.cpp b/gui/src/settingskeycapturedialog.cpp index 3339813..a631444 100644 --- a/gui/src/settingskeycapturedialog.cpp +++ b/gui/src/settingskeycapturedialog.cpp @@ -7,7 +7,7 @@ #include #include -SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) +SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget *parent) { setWindowTitle(tr("Key Capture")); @@ -23,7 +23,7 @@ SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) connect(button, &QPushButton::clicked, this, &QDialog::accept); } -void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent* event) +void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent *event) { KeyCaptured(Qt::Key(event->key())); accept(); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index fedae10..42c9959 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,7 +14,20 @@ #define SETSU_UPDATE_INTERVAL_MS 4 -StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) +#ifdef Q_OS_LINUX +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "DualSense" +#else +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "Wireless Controller" +#endif + +StreamSessionConnectInfo::StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode) : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); @@ -30,11 +43,14 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTar this->morning = morning; audio_buffer_size = settings->GetAudioBufferSize(); this->fullscreen = fullscreen; + this->transform_mode = transform_mode; this->enable_keyboard = false; // TODO: from settings + this->enable_dualsense = settings->GetDualSenseEnabled(); } static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); #if CHIAKI_GUI_ENABLE_SETSU static void SessionSetsuCb(SetsuEvent *event, void *user); @@ -49,9 +65,12 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje pi_decoder(nullptr), #endif audio_output(nullptr), - audio_io(nullptr) + audio_io(nullptr), + haptics_output(0), + haptics_resampler_buf(nullptr) { connected = false; + ChiakiErrorCode err; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi) @@ -66,7 +85,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje ffmpeg_decoder = new ChiakiFfmpegDecoder; ChiakiLogSniffer sniffer; chiaki_log_sniffer_init(&sniffer, CHIAKI_LOG_ALL, GetChiakiLog()); - ChiakiErrorCode err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, + err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, chiaki_log_sniffer_get_log(&sniffer), chiaki_target_is_ps5(connect_info.target) ? connect_info.video_profile.codec : CHIAKI_CODEC_H264, connect_info.hw_decoder.isEmpty() ? NULL : connect_info.hw_decoder.toUtf8().constData(), @@ -101,11 +120,21 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje QByteArray host_str = connect_info.host.toUtf8(); - ChiakiConnectInfo chiaki_connect_info; + ChiakiConnectInfo chiaki_connect_info = {}; chiaki_connect_info.ps5 = chiaki_target_is_ps5(connect_info.target); chiaki_connect_info.host = host_str.constData(); chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; + chiaki_connect_info.enable_keyboard = false; + chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense; + +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) + { + CHIAKI_LOGW(GetChiakiLog(), "A codec other than H264 was requested for Pi Decoder. Falling back to it."); + chiaki_connect_info.video_profile.codec = CHIAKI_CODEC_H264; + } +#endif if(connect_info.regist_key.size() != sizeof(chiaki_connect_info.regist_key)) throw ChiakiException("RegistKey invalid"); @@ -126,6 +155,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink); chiaki_session_set_audio_sink(&session, &audio_sink); + if(connect_info.enable_dualsense) + { + ChiakiAudioSink haptics_sink; + haptics_sink.user = this; + haptics_sink.frame_cb = HapticsFrameCb; + chiaki_session_set_haptics_sink(&session, &haptics_sink); + } + #if CHIAKI_LIB_ENABLE_PI_DECODER if(pi_decoder) chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder); @@ -144,16 +181,29 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #endif #if CHIAKI_GUI_ENABLE_SETSU + setsu_motion_device = nullptr; chiaki_controller_state_set_idle(&setsu_state); + orient_dirty = true; + chiaki_orientation_tracker_init(&orient_tracker); setsu = setsu_new(); auto timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [this]{ setsu_poll(setsu, SessionSetsuCb, this); + if(orient_dirty) + { + chiaki_orientation_tracker_apply_to_controller_state(&orient_tracker, &setsu_state); + SendFeedbackState(); + orient_dirty = false; + } }); timer->start(SETSU_UPDATE_INTERVAL_MS); #endif key_map = connect_info.key_map; + if(connect_info.enable_dualsense) + { + InitHaptics(); + } UpdateGamepads(); } @@ -181,6 +231,16 @@ StreamSession::~StreamSession() chiaki_ffmpeg_decoder_fini(ffmpeg_decoder); delete ffmpeg_decoder; } + if(haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + haptics_output = 0; + } + if(haptics_resampler_buf) + { + free(haptics_resampler_buf); + haptics_resampler_buf = nullptr; + } } void StreamSession::Start() @@ -209,13 +269,16 @@ void StreamSession::SetLoginPIN(const QString &pin) chiaki_session_set_login_pin(&session, (const uint8_t *)data.constData(), data.size()); } -void StreamSession::HandleMouseEvent(QMouseEvent *event) +bool StreamSession::HandleMouseEvent(QMouseEvent *event) { + if(event->button() != Qt::MouseButton::LeftButton) + return false; if(event->type() == QEvent::MouseButtonPress) keyboard_state.buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; else keyboard_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; SendFeedbackState(); + return true; } void StreamSession::HandleKeyboardEvent(QKeyEvent *event) @@ -282,6 +345,8 @@ void StreamSession::UpdateGamepads() { CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID()); controllers.remove(controller_id); + if(controller->IsDualSense()) + DisconnectHaptics(); delete controller; } } @@ -300,6 +365,11 @@ void StreamSession::UpdateGamepads() CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData()); connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState); controllers[controller_id] = controller; + if(controller->IsDualSense()) + { + // Connect haptics audio device with a delay to give the sound system time to set up + QTimer::singleShot(1000, this, &StreamSession::ConnectHaptics); + } } } @@ -312,17 +382,16 @@ void StreamSession::SendFeedbackState() ChiakiControllerState state; chiaki_controller_state_set_idle(&state); -#if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +#if CHIAKI_GUI_ENABLE_SETSU + // setsu is the one that potentially has gyro/accel/orient so copy that directly first + state = setsu_state; +#endif + for(auto controller : controllers) { auto controller_state = controller->GetState(); chiaki_controller_state_or(&state, &state, &controller_state); } -#endif - -#if CHIAKI_GUI_ENABLE_SETSU - chiaki_controller_state_or(&state, &state, &setsu_state); -#endif chiaki_controller_state_or(&state, &state, &keyboard_state); chiaki_session_set_controller_state(&session, &state); @@ -359,6 +428,82 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate) channels, rate, audio_output->bufferSize()); } +void StreamSession::InitHaptics() +{ + haptics_output = 0; + haptics_resampler_buf = nullptr; +#ifdef Q_OS_LINUX + // Haptics work most reliably with Pipewire, so try to use that if available + SDL_SetHint("SDL_AUDIODRIVER", "pipewire"); +#endif + + if(SDL_Init(SDL_INIT_AUDIO) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not initialize SDL Audio for haptics output: %s", SDL_GetError()); + return; + } + +#ifdef Q_OS_LINUX + if(!strstr(SDL_GetCurrentAudioDriver(), "pipewire")) + { + CHIAKI_LOGW( + log.GetChiakiLog(), + "Haptics output is not using Pipewire, this may not work reliably. (was: '%s')", + SDL_GetCurrentAudioDriver()); + } +#endif + + SDL_AudioCVT cvt; + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = 240; // 10 16bit stereo samples + haptics_resampler_buf = (uint8_t*) calloc(cvt.len * cvt.len_mult, sizeof(uint8_t)); +} + +void StreamSession::DisconnectHaptics() +{ + if(this->haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + this->haptics_output = 0; + } +} + +void StreamSession::ConnectHaptics() +{ + if(this->haptics_output > 0) + { + CHIAKI_LOGW(this->log.GetChiakiLog(), "Haptics already connected to an attached DualSense controller, ignoring additional controllers."); + return; + } + + SDL_AudioSpec want, have; + SDL_zero(want); + want.freq = 48000; + want.format = AUDIO_S16LSB; + want.channels = 4; + want.samples = 480; // 10ms buffer + want.callback = NULL; + + const char *device_name = nullptr; + for(int i=0; i < SDL_GetNumAudioDevices(0); i++) + { + device_name = SDL_GetAudioDeviceName(i, 0); + if(!device_name || !strstr(device_name, DUALSENSE_AUDIO_DEVICE_NEEDLE)) + continue; + haptics_output = SDL_OpenAudioDevice(device_name, 0, &want, &have, 0); + if(haptics_output == 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not open SDL Audio Device %s for haptics output: %s", device_name, SDL_GetError()); + continue; + } + SDL_PauseAudioDevice(haptics_output, 0); + CHIAKI_LOGI(log.GetChiakiLog(), "Haptics Audio Device '%s' opened with %d channels @ %d Hz, buffer size %u (driver=%s)", device_name, have.channels, have.freq, have.size, SDL_GetCurrentAudioDriver()); + return; + } + CHIAKI_LOGW(log.GetChiakiLog(), "DualSense features were enabled and a DualSense is connected, but could not find the DualSense audio device!"); + return; +} + void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) { if(!audio_io) @@ -366,6 +511,35 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) audio_io->write((const char *)buf, static_cast(samples_count * 2 * 2)); } +void StreamSession::PushHapticsFrame(uint8_t *buf, size_t buf_size) +{ + if(haptics_output == 0) + return; + SDL_AudioCVT cvt; + // Haptics samples are coming in at 3KHZ, but the DualSense expects 48KHZ + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = buf_size * 2; + cvt.buf = haptics_resampler_buf; + // Remix to 4 channels + for (int i=0; i < buf_size; i+=4) + { + SDL_memset(haptics_resampler_buf + i * 2, 0, 4); + SDL_memcpy(haptics_resampler_buf + (i * 2) + 4, buf + i, 4); + } + // Resample to 48kHZ + if(SDL_ConvertAudio(&cvt) != 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to resample haptics audio: %s", SDL_GetError()); + return; + } + + if(SDL_QueueAudio(haptics_output, cvt.buf, cvt.len_cvt) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to submit haptics audio to device: %s", SDL_GetError()); + return; + } +} + void StreamSession::Event(ChiakiEvent *event) { switch(event->type) @@ -380,6 +554,30 @@ void StreamSession::Event(ChiakiEvent *event) case CHIAKI_EVENT_LOGIN_PIN_REQUEST: emit LoginPINRequested(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: { + uint8_t left = event->rumble.left; + uint8_t right = event->rumble.right; + QMetaObject::invokeMethod(this, [this, left, right]() { + for(auto controller : controllers) + controller->SetRumble(left, right); + }); + break; + } + case CHIAKI_EVENT_TRIGGER_EFFECTS: { + uint8_t type_left = event->trigger_effects.type_left; + uint8_t data_left[10]; + memcpy(data_left, event->trigger_effects.left, 10); + uint8_t data_right[10]; + memcpy(data_right, event->trigger_effects.right, 10); + uint8_t type_right = event->trigger_effects.type_right; + QMetaObject::invokeMethod(this, [this, type_left, data_left, type_right, data_right]() { + for(auto controller : controllers) + controller->SetTriggerEffects(type_left, data_left, type_right, data_right); + }); + break; + } + default: + break; } } @@ -391,27 +589,64 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path); + switch(event->dev_type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + // connect all the touchpads! + if(setsu_connect(setsu, event->path, event->dev_type)) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Touchpad Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Touchpad Device %s", event->path); + break; + case SETSU_DEVICE_TYPE_MOTION: + // connect only one motion since multiple make no sense + if(setsu_motion_device) + { + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s detected there is already one connected", + event->path); + break; + } + setsu_motion_device = setsu_connect(setsu, event->path, event->dev_type); + if(setsu_motion_device) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Motion Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Motion Device %s", event->path); + break; + } break; case SETSU_EVENT_DEVICE_REMOVED: - for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + switch(event->dev_type) { - if(it.key().first == event->path) - { - chiaki_controller_state_stop_touch(&setsu_state, it.value()); - setsu_ids.erase(it++); - } - else - it++; + case SETSU_DEVICE_TYPE_TOUCHPAD: + CHIAKI_LOGI(GetChiakiLog(), "Setsu Touchpad Device %s disconnected", event->path); + for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + { + if(it.key().first == event->path) + { + chiaki_controller_state_stop_touch(&setsu_state, it.value()); + setsu_ids.erase(it++); + } + else + it++; + } + SendFeedbackState(); + break; + case SETSU_DEVICE_TYPE_MOTION: + if(!setsu_motion_device || strcmp(setsu_device_get_path(setsu_motion_device), event->path)) + break; + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s disconnected", event->path); + setsu_motion_device = nullptr; + chiaki_orientation_tracker_init(&orient_tracker); + orient_dirty = true; + break; } - SendFeedbackState(); break; case SETSU_EVENT_TOUCH_DOWN: break; case SETSU_EVENT_TOUCH_UP: for(auto it=setsu_ids.begin(); it!=setsu_ids.end(); it++) { - if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->tracking_id) + if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->touch.tracking_id) { chiaki_controller_state_stop_touch(&setsu_state, it.value()); setsu_ids.erase(it); @@ -421,18 +656,18 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) SendFeedbackState(); break; case SETSU_EVENT_TOUCH_POSITION: { - QPair k = { setsu_device_get_path(event->dev), event->tracking_id }; + QPair k = { setsu_device_get_path(event->dev), event->touch.tracking_id }; auto it = setsu_ids.find(k); if(it == setsu_ids.end()) { - int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->x, event->y); + int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->touch.x, event->touch.y); if(cid >= 0) setsu_ids[k] = (uint8_t)cid; else break; } else - chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->x, event->y); + chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->touch.x, event->touch.y); SendFeedbackState(); break; } @@ -442,6 +677,13 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) case SETSU_EVENT_BUTTON_UP: setsu_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; break; + case SETSU_EVENT_MOTION: + chiaki_orientation_tracker_update(&orient_tracker, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.timestamp); + orient_dirty = true; + break; } } #endif @@ -460,6 +702,7 @@ class StreamSessionPrivate } static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); } + static void PushHapticsFrame(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushHapticsFrame(buf, buf_size); } static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); } #if CHIAKI_GUI_ENABLE_SETSU static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); } @@ -479,6 +722,12 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user) StreamSessionPrivate::PushAudioFrame(session, buf, samples_count); } +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user) +{ + auto session = reinterpret_cast(user); + StreamSessionPrivate::PushHapticsFrame(session, buf, buf_size); +} + static void EventCb(ChiakiEvent *event, void *user) { auto session = reinterpret_cast(user); diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index feff758..a2337d7 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget *parent) : QMainWindow(parent), @@ -23,8 +24,6 @@ StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget try { - if(connect_info.fullscreen) - showFullScreen(); Init(); } catch(const Exception &e) @@ -40,6 +39,8 @@ StreamWindow::~StreamWindow() delete av_widget; } +#include + void StreamWindow::Init() { session = new StreamSession(connect_info, this); @@ -47,10 +48,36 @@ void StreamWindow::Init() connect(session, &StreamSession::SessionQuit, this, &StreamWindow::SessionQuit); connect(session, &StreamSession::LoginPINRequested, this, &StreamWindow::LoginPINRequested); + const QKeySequence fullscreen_shortcut = Qt::Key_F11; + const QKeySequence stretch_shortcut = Qt::CTRL + Qt::Key_S; + const QKeySequence zoom_shortcut = Qt::CTRL + Qt::Key_Z; + + fullscreen_action = new QAction(tr("Fullscreen"), this); + fullscreen_action->setCheckable(true); + fullscreen_action->setShortcut(fullscreen_shortcut); + addAction(fullscreen_action); + connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + if(session->GetFfmpegDecoder()) { - av_widget = new AVOpenGLWidget(session, this); + av_widget = new AVOpenGLWidget(session, this, connect_info.transform_mode); setCentralWidget(av_widget); + + av_widget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(av_widget, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) { + av_widget->ResetMouseTimeout(); + + QMenu menu(av_widget); + menu.addAction(fullscreen_action); + menu.addSeparator(); + menu.addAction(stretch_action); + menu.addAction(zoom_action); + releaseKeyboard(); + connect(&menu, &QMenu::aboutToHide, this, [this] { + grabKeyboard(); + }); + menu.exec(av_widget->mapToGlobal(pos)); + }); } else { @@ -63,13 +90,34 @@ void StreamWindow::Init() session->Start(); - auto fullscreen_action = new QAction(tr("Fullscreen"), this); - fullscreen_action->setShortcut(Qt::Key_F11); - addAction(fullscreen_action); - connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + stretch_action = new QAction(tr("Stretch"), this); + stretch_action->setCheckable(true); + stretch_action->setShortcut(stretch_shortcut); + addAction(stretch_action); + connect(stretch_action, &QAction::triggered, this, &StreamWindow::ToggleStretch); + + zoom_action = new QAction(tr("Zoom"), this); + zoom_action->setCheckable(true); + zoom_action->setShortcut(zoom_shortcut); + addAction(zoom_action); + connect(zoom_action, &QAction::triggered, this, &StreamWindow::ToggleZoom); + + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &StreamWindow::Quit); resize(connect_info.video_profile.width, connect_info.video_profile.height); - show(); + + if(connect_info.fullscreen) + { + showFullScreen(); + fullscreen_action->setChecked(true); + } + else + show(); + + UpdateTransformModeActions(); } void StreamWindow::keyPressEvent(QKeyEvent *event) @@ -84,22 +132,32 @@ void StreamWindow::keyReleaseEvent(QKeyEvent *event) session->HandleKeyboardEvent(event); } +void StreamWindow::Quit() +{ + close(); +} + void StreamWindow::mousePressEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mousePressEvent(event); } void StreamWindow::mouseReleaseEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mouseReleaseEvent(event); } void StreamWindow::mouseDoubleClickEvent(QMouseEvent *event) { - ToggleFullscreen(); - + if(event->button() == Qt::MouseButton::LeftButton) + { + ToggleFullscreen(); + return; + } QMainWindow::mouseDoubleClickEvent(event); } @@ -143,7 +201,7 @@ void StreamWindow::closeEvent(QCloseEvent *event) void StreamWindow::SessionQuit(ChiakiQuitReason reason, const QString &reason_str) { - if(reason != CHIAKI_QUIT_REASON_STOPPED) + if(chiaki_quit_reason_is_error(reason)) { QString m = tr("Chiaki Session has quit") + ":\n" + chiaki_quit_reason_string(reason); if(!reason_str.isEmpty()) @@ -175,15 +233,48 @@ void StreamWindow::LoginPINRequested(bool incorrect) void StreamWindow::ToggleFullscreen() { if(isFullScreen()) + { showNormal(); + fullscreen_action->setChecked(false); + } else { showFullScreen(); if(av_widget) av_widget->HideMouse(); + fullscreen_action->setChecked(true); } } +void StreamWindow::UpdateTransformModeActions() +{ + TransformMode tm = av_widget ? av_widget->GetTransformMode() : TransformMode::Fit; + stretch_action->setChecked(tm == TransformMode::Stretch); + zoom_action->setChecked(tm == TransformMode::Zoom); +} + +void StreamWindow::ToggleStretch() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Stretch + ? TransformMode::Fit + : TransformMode::Stretch); + UpdateTransformModeActions(); +} + +void StreamWindow::ToggleZoom() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Zoom + ? TransformMode::Fit + : TransformMode::Zoom); + UpdateTransformModeActions(); +} + void StreamWindow::resizeEvent(QResizeEvent *event) { UpdateVideoTransform(); diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e2a79d3..cbfd6b0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -35,7 +35,8 @@ set(HEADER_FILES include/chiaki/time.h include/chiaki/fec.h include/chiaki/regist.h - include/chiaki/opusdecoder.h) + include/chiaki/opusdecoder.h + include/chiaki/orientation.h) set(SOURCE_FILES src/common.c @@ -71,9 +72,10 @@ set(SOURCE_FILES src/controller.c src/takionsendbuffer.c src/time.c - src/fec + src/fec.c src/regist.c - src/opusdecoder.c) + src/opusdecoder.c + src/orientation.c) if(CHIAKI_ENABLE_FFMPEG_DECODER) list(APPEND HEADER_FILES include/chiaki/ffmpegdecoder.h) @@ -117,11 +119,15 @@ find_package(Threads REQUIRED) target_link_libraries(chiaki-lib Threads::Threads) if(CHIAKI_LIB_ENABLE_MBEDTLS) - # provided by mbedtls-static (mbedtls-devel) - find_library(MBEDTLS mbedtls) - find_library(MBEDX509 mbedx509) - find_library(MBEDCRYPTO mbedcrypto) - target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + target_link_libraries(chiaki-lib mbedtls mbedx509 mbedcrypto) + else() + # provided by mbedtls-static (mbedtls-devel) + find_library(MBEDTLS mbedtls) + find_library(MBEDX509 mbedx509) + find_library(MBEDCRYPTO mbedcrypto) + target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + endif() elseif(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT) target_link_libraries(chiaki-lib OpenSSL_Crypto) else() diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index e8f2e7c..24e86a9 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -66,6 +66,10 @@ typedef struct chiaki_controller_state_t uint8_t touch_id_next; ChiakiControllerTouch touches[CHIAKI_CONTROLLER_TOUCHES_MAX]; + + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; } ChiakiControllerState; CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state); @@ -79,27 +83,12 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y); -static inline bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) -{ - if(!(a->buttons == b->buttons - && a->l2_state == b->l2_state - && a->r2_state == b->r2_state - && a->left_x == b->left_x - && a->left_y == b->left_y - && a->right_x == b->right_x - && a->right_y == b->right_y)) - return false; - - for(size_t i=0; itouches[i].id != b->touches[i].id) - return false; - if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) - return false; - } - return true; -} +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b); +/** + * Union of two controller states. + * Ignores gyro, accel and orient because it makes no sense there. + */ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b); #ifdef __cplusplus diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index c9881b5..f26200b 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -25,6 +25,8 @@ extern "C" { #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4 "00020020" #define CHIAKI_DISCOVERY_PORT_PS5 9302 #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5 "00030010" +#define CHIAKI_DISCOVERY_PORT_LOCAL_MIN 9303 +#define CHIAKI_DISCOVERY_PORT_LOCAL_MAX 9319 typedef enum chiaki_discovery_cmd_t { diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 264af5c..be66384 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -15,6 +15,9 @@ extern "C" { typedef struct chiaki_feedback_state_t { + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; int16_t left_x; int16_t left_y; int16_t right_x; diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h new file mode 100644 index 0000000..6dde62b --- /dev/null +++ b/lib/include/chiaki/orientation.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_ORIENTATION_H +#define CHIAKI_ORIENTATION_H + +#include "common.h" +#include "controller.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Quaternion orientation from accelerometer and gyroscope + * using Madgwick's algorithm. + * See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + */ +typedef struct chiaki_orientation_t +{ + float x, y, z, w; +} ChiakiOrientation; + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec); + +/** + * Extension of ChiakiOrientation, also tracking an absolute timestamp and the current gyro/accel state + */ +typedef struct chiaki_orientation_tracker_t +{ + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + ChiakiOrientation orient; + uint32_t timestamp; + uint64_t sample_index; +} ChiakiOrientationTracker; + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us); +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_ORIENTATION_H diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index db7bf02..5c355ad 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -78,6 +78,7 @@ typedef struct chiaki_connect_info_t ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; // Downgrade video_profile if server does not seem to support it. bool enable_keyboard; + bool enable_dualsense; } ChiakiConnectInfo; @@ -93,11 +94,17 @@ typedef enum { CHIAKI_QUIT_REASON_CTRL_CONNECT_FAILED, CHIAKI_QUIT_REASON_CTRL_CONNECTION_REFUSED, CHIAKI_QUIT_REASON_STREAM_CONNECTION_UNKNOWN, - CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED, + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN, // like REMOTE_DISCONNECTED, but because the server shut down } ChiakiQuitReason; CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason); +static inline bool chiaki_quit_reason_is_error(ChiakiQuitReason reason) +{ + return reason != CHIAKI_QUIT_REASON_STOPPED && reason != CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; +} + typedef struct chiaki_quit_event_t { ChiakiQuitReason reason; @@ -114,6 +121,20 @@ typedef struct chiaki_audio_stream_info_event_t ChiakiAudioHeader audio_header; } ChiakiAudioStreamInfoEvent; +typedef struct chiaki_rumble_event_t +{ + uint8_t unknown; + uint8_t left; // low-frequency + uint8_t right; // high-frequency +} ChiakiRumbleEvent; + +typedef struct chiaki_trigger_effects_event_t +{ + uint8_t type_left; + uint8_t type_right; + uint8_t left[10]; + uint8_t right[10]; +} ChiakiTriggerEffectsEvent; typedef enum { CHIAKI_EVENT_CONNECTED, @@ -121,7 +142,9 @@ typedef enum { CHIAKI_EVENT_KEYBOARD_OPEN, CHIAKI_EVENT_KEYBOARD_TEXT_CHANGE, CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, + CHIAKI_EVENT_RUMBLE, CHIAKI_EVENT_QUIT, + CHIAKI_EVENT_TRIGGER_EFFECTS, } ChiakiEventType; typedef struct chiaki_event_t @@ -131,6 +154,8 @@ typedef struct chiaki_event_t { ChiakiQuitEvent quit; ChiakiKeyboardEvent keyboard; + ChiakiRumbleEvent rumble; + ChiakiTriggerEffectsEvent trigger_effects; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect @@ -155,13 +180,14 @@ typedef struct chiaki_session_t bool ps5; struct addrinfo *host_addrinfos; struct addrinfo *host_addrinfo_selected; - char hostname[128]; + char hostname[256]; char regist_key[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t morning[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t did[CHIAKI_RP_DID_SIZE]; ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; bool enable_keyboard; + bool enable_dualsense; } connect_info; ChiakiTarget target; @@ -183,6 +209,7 @@ typedef struct chiaki_session_t ChiakiVideoSampleCallback video_sample_cb; void *video_sample_cb_user; ChiakiAudioSink audio_sink; + ChiakiAudioSink haptics_sink; ChiakiThread session_thread; @@ -238,6 +265,14 @@ static inline void chiaki_session_set_audio_sink(ChiakiSession *session, ChiakiA session->audio_sink = *sink; } +/** + * @param sink contents are copied + */ +static inline void chiaki_session_set_haptics_sink(ChiakiSession *session, ChiakiAudioSink *sink) +{ + session->haptics_sink = *sink; +} + #ifdef __cplusplus } #endif diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index ba1817a..90578c6 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -32,6 +32,7 @@ typedef struct chiaki_stream_connection_t ChiakiPacketStats packet_stats; ChiakiAudioReceiver *audio_receiver; ChiakiVideoReceiver *video_receiver; + ChiakiAudioReceiver *haptics_receiver; ChiakiFeedbackSender feedback_sender; /** diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 3f7e96f..15d62e5 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -26,7 +26,9 @@ extern "C" { typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, - CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 + CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS = 11, } ChiakiTakionMessageDataType; typedef struct chiaki_takion_av_packet_t @@ -35,6 +37,7 @@ typedef struct chiaki_takion_av_packet_t ChiakiSeqNum16 frame_index; bool uses_nalu_info_structs; bool is_video; + bool is_haptics; ChiakiSeqNum16 unit_index; uint16_t units_in_frame_total; // source + units_in_frame_fec uint16_t units_in_frame_fec; @@ -45,8 +48,6 @@ typedef struct chiaki_takion_av_packet_t uint64_t key_pos; - uint8_t byte_before_audio_data; - uint8_t *data; // not owned size_t data_size; } ChiakiTakionAVPacket; @@ -105,6 +106,7 @@ typedef struct chiaki_takion_connect_info_t ChiakiTakionCallback cb; void *cb_user; bool enable_crypt; + bool enable_dualsense; uint8_t protocol_version; } ChiakiTakionConnectInfo; @@ -161,6 +163,8 @@ typedef struct chiaki_takion_t ChiakiTakionAVPacketParse av_packet_parse; ChiakiKeyState key_state; + + bool enable_dualsense; } ChiakiTakion; diff --git a/lib/protobuf/CMakeLists.txt b/lib/protobuf/CMakeLists.txt index 6efa342..43fde68 100644 --- a/lib/protobuf/CMakeLists.txt +++ b/lib/protobuf/CMakeLists.txt @@ -11,8 +11,16 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" set(SOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.c") set(HEADER_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.h") +if(UNIX AND IS_ABSOLUTE "${PROTOC}") + # make sure protoc is in PATH when invoking the generator below, which needs it. + get_filename_component(PROTOC_PATH "${PROTOC}" DIRECTORY) + set(GEN_PREFIX "${CMAKE_COMMAND}" -E env "PATH=${PROTOC_PATH}:$ENV{PATH}") +else() + set(GEN_PREFIX "") +endif() + add_custom_command(OUTPUT ${SOURCE_FILES} ${HEADER_FILES} - COMMAND "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" + COMMAND ${GEN_PREFIX} "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" MAIN_DEPENDENCY "${CMAKE_CURRENT_BINARY_DIR}/takion.pb") set(CHIAKI_LIB_PROTO_SOURCE_FILES "${SOURCE_FILES}" PARENT_SCOPE) diff --git a/lib/protobuf/takion.proto b/lib/protobuf/takion.proto index 748d70a..ceef0f1 100644 --- a/lib/protobuf/takion.proto +++ b/lib/protobuf/takion.proto @@ -312,7 +312,8 @@ message ControllerConnectionPayload { VITA = 3; XINPUT = 4; MOBILE = 5; - BOND = 6; + DUALSENSE = 6; + VR2SENSE = 7; } } diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 5808e4c..744d807 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -5,7 +5,7 @@ #include -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size); +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session, ChiakiPacketStats *packet_stats) { @@ -23,7 +23,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *au return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receiver) { #ifdef CHIAKI_LIB_ENABLE_OPUS @@ -32,7 +31,6 @@ CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receive chiaki_mutex_fini(&audio_receiver->mutex); } - CHIAKI_EXPORT void chiaki_audio_receiver_stream_info(ChiakiAudioReceiver *audio_receiver, ChiakiAudioHeader *audio_header) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -79,15 +77,15 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re if(packet->data_size != (size_t)unit_size * (size_t)packet->units_in_frame_total) { CHIAKI_LOGE(audio_receiver->log, "Audio AV Packet size mismatch %#llx vs %#llx", - (unsigned long long)packet->data_size, - (unsigned long long)(unit_size * packet->units_in_frame_total)); + (unsigned long long)packet->data_size, + (unsigned long long)(unit_size * packet->units_in_frame_total)); return; } if(packet->frame_index > (1 << 15)) audio_receiver->frame_index_startup = false; - for(size_t i=0; iframe_index - fec_units_count + fec_index; } - chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->data + unit_size * i, unit_size); + chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->is_haptics, packet->data + unit_size * i, unit_size); } if(audio_receiver->packet_stats) chiaki_packet_stats_push_seq(audio_receiver->packet_stats, packet->frame_index); } -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size) +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -119,7 +117,9 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, Chi goto beach; audio_receiver->frame_index_prev = frame_index; - if(audio_receiver->session->audio_sink.frame_cb) + if(is_haptics && audio_receiver->session->haptics_sink.frame_cb) + audio_receiver->session->haptics_sink.frame_cb(buf, buf_size, audio_receiver->session->haptics_sink.user); + else if(!is_haptics && audio_receiver->session->audio_sink.frame_cb) audio_receiver->session->audio_sink.frame_cb(buf, buf_size, audio_receiver->session->audio_sink.user); beach: diff --git a/lib/src/base64.c b/lib/src/base64.c index fbdf635..9d0fecd 100644 --- a/lib/src/base64.c +++ b/lib/src/base64.c @@ -7,7 +7,6 @@ // Implementations taken from // https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64 - CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_size, char *out, size_t out_size) { const char base64chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -23,11 +22,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // these three 8-bit (ASCII) characters become one 24-bit number n = ((uint32_t)in[x]) << 16; - if((x+1) < in_size) - n += ((uint32_t)in[x+1]) << 8; + if((x + 1) < in_size) + n += ((uint32_t)in[x + 1]) << 8; - if((x+2) < in_size) - n += in[x+2]; + if((x + 2) < in_size) + n += in[x + 2]; // this 24-bit number gets separated into four 6-bit numbers n0 = (uint8_t)(n >> 18) & 63; @@ -46,7 +45,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have only two bytes available, then their encoding is // spread out over three chars - if((x+1) < in_size) + if((x + 1) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -55,7 +54,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have all three bytes available, then their encoding is spread // out over four characters - if((x+2) < in_size) + if((x + 2) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -65,9 +64,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // create and add padding that is required if we did not have a multiple of 3 // number of characters available - if (pad_count > 0) + if(pad_count > 0) { - for (; pad_count < 3; pad_count++) + for(; pad_count < 3; pad_count++) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -80,25 +79,22 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ return CHIAKI_ERR_SUCCESS; } - - - #define WHITESPACE 64 -#define EQUALS 65 -#define INVALID 66 +#define EQUALS 65 +#define INVALID 66 static const unsigned char d[] = { - 66,66,66,66,66,66,66,66,66,66,64,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,62,66,66,66,63,52,53, - 54,55,56,57,58,59,60,61,66,66,66,65,66,66,66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,66,66,66,66,66,66,26,27,28, - 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66 + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 62, 66, 66, 66, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 66, 66, 66, 65, 66, 66, 66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 66, 66, 66, 66, 66, 66, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66 }; CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_size, uint8_t *out, size_t *out_size) @@ -108,17 +104,17 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz uint32_t buf = 0; size_t len = 0; - while (in < end) + while(in < end) { unsigned char c = d[(size_t)(*in++)]; switch(c) { case WHITESPACE: - continue; // skip whitespace + continue; // skip whitespace case INVALID: - return CHIAKI_ERR_INVALID_DATA; // invalid input - case EQUALS: // pad character, end of data + return CHIAKI_ERR_INVALID_DATA; // invalid input + case EQUALS: // pad character, end of data in = end; continue; default: @@ -132,7 +128,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz *(out++) = (unsigned char)((buf >> 16) & 0xff); *(out++) = (unsigned char)((buf >> 8) & 0xff); *(out++) = (unsigned char)(buf & 0xff); - buf = 0; iter = 0; + buf = 0; + iter = 0; } } } diff --git a/lib/src/common.c b/lib/src/common.c index 0dd2006..adb0fc6 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -1,14 +1,14 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include -#include #include +#include #include +#include #include #include -#include #ifdef _WIN32 #include @@ -98,7 +98,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init() WORD wsa_version = MAKEWORD(2, 2); WSADATA wsa_data; int err = WSAStartup(wsa_version, &wsa_data); - if (err != 0) + if(err != 0) return CHIAKI_ERR_NETWORK; } #endif diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index e478410..8fb912b 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -25,7 +25,7 @@ static void *congestion_control_thread_func(void *user) packet.received = (uint16_t)received; packet.lost = (uint16_t)lost; CHIAKI_LOGV(control->takion->log, "Sending Congestion Control Packet, received: %u, lost: %u", - (unsigned int)packet.received, (unsigned int)packet.lost); + (unsigned int)packet.received, (unsigned int)packet.lost); chiaki_takion_send_congestion(control->takion, &packet); } diff --git a/lib/src/controller.c b/lib/src/controller.c index b347bb7..744a83f 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -14,17 +14,25 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->right_x = 0; state->right_y = 0; state->touch_id_next = 0; - for(size_t i=0; itouches[i].id = -1; state->touches[i].x = 0; state->touches[i].y = 0; } + state->gyro_x = state->gyro_y = state->gyro_z = 0.0f; + state->accel_x = 0.0f; + state->accel_y = 1.0f; + state->accel_z = 0.0f; + state->orient_x = 0.0f; + state->orient_y = 0.0f; + state->orient_z = 0.0f; + state->orient_w = 1.0f; } CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) { - for(size_t i=0; itouches[i].id < 0) { @@ -40,7 +48,7 @@ CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState * CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *state, uint8_t id) { - for(size_t i=0; itouches[i].id == id) { @@ -53,7 +61,7 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y) { id &= TOUCH_ID_MASK; - for(size_t i=0; itouches[i].id == id) { @@ -64,8 +72,43 @@ CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState * } } -#define MAX(a, b) ((a) > (b) ? (a) : (b)) -#define ABS(a) ((a) > 0 ? (a) : -(a)) +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) +{ + if(!(a->buttons == b->buttons + && a->l2_state == b->l2_state + && a->r2_state == b->r2_state + && a->left_x == b->left_x + && a->left_y == b->left_y + && a->right_x == b->right_x + && a->right_y == b->right_y)) + return false; + + for(size_t i=0; itouches[i].id != b->touches[i].id) + return false; + if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) + return false; + } + +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + + return true; +} + +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b) @@ -78,8 +121,21 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->right_x = MAX_ABS(a->right_x, b->right_x); out->right_y = MAX_ABS(a->right_y, b->right_y); + #define ORF(n, idle_val) if(a->n == idle_val) out->n = b->n; else out->n = a->n + ORF(accel_x, 0.0f); + ORF(accel_y, 1.0f); + ORF(accel_z, 0.0f); + ORF(gyro_x, 0.0f); + ORF(gyro_y, 0.0f); + ORF(gyro_z, 0.0f); + ORF(orient_x, 0.0f); + ORF(orient_y, 0.0f); + ORF(orient_z, 0.0f); + ORF(orient_w, 1.0f); + #undef ORF + out->touch_id_next = 0; - for(size_t i=0; itouches[i].id >= 0 ? &a->touches[i] : (b->touches[i].id >= 0 ? &b->touches[i] : NULL); if(!touch) diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index f92d12b..29a39b2 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -488,20 +488,27 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) { - if(!ctrl->session->connect_info.enable_keyboard) - return; - // TODO: Last byte of pre_enable request is random (?) - // TODO: Signature ?! - uint8_t enable = 1; - uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; - uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - ctrl_message_send(ctrl, 0xD, signature, 0x10); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); - ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); + if(ctrl->session->connect_info.enable_dualsense) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling DualSense features"); + const uint8_t enable[3] = { 0x00, 0x40, 0x00 }; + ctrl_message_send(ctrl, 0x13, enable, 3); + } + if(ctrl->session->connect_info.enable_keyboard) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling Keyboard"); + // TODO: Last byte of pre_enable request is random (?) + // TODO: Signature ?! + uint8_t enable = 1; + uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; + uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ctrl_message_send(ctrl, 0xD, signature, 0x10); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + } } - static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { if(ctrl->session->ctrl_session_id_received) @@ -652,7 +659,7 @@ static void ctrl_message_received_keyboard_close(ChiakiCtrl *ctrl, uint8_t *payl chiaki_session_send_event(ctrl->session, &keyboard_event); } -static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t* payload, size_t payload_size) +static void ctrl_message_received_keyboard_text_change(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { assert(payload_size >= sizeof(CtrlKeyboardTextResponseMessage)); @@ -676,7 +683,8 @@ static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t free(buffer); } -typedef struct ctrl_response_t { +typedef struct ctrl_response_t +{ bool server_type_valid; uint8_t rp_server_type[0x10]; bool success; @@ -742,7 +750,6 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(session->log, "Failed to set ctrl socket to non-blocking: %s", chiaki_error_string(err)); - chiaki_mutex_unlock(&ctrl->notif_mutex); err = chiaki_stop_pipe_connect(&ctrl->notif_pipe, sock, sa, addr->ai_addrlen); chiaki_mutex_lock(&ctrl->notif_mutex); diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 0658c87..f207a54 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -152,27 +152,48 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_init(ChiakiDiscovery *discovery, return CHIAKI_ERR_NETWORK; } - memset(&discovery->local_addr, 0, sizeof(discovery->local_addr)); - discovery->local_addr.sa_family = family; - if(family == AF_INET6) + // First try CHIAKI_DISCOVERY_PORT_LOCAL_MIN..local_addr, 0, sizeof(discovery->local_addr)); + discovery->local_addr.sa_family = family; + if(family == AF_INET6) + { #ifndef __SWITCH__ - struct in6_addr anyaddr = IN6ADDR_ANY_INIT; + struct in6_addr anyaddr = IN6ADDR_ANY_INIT; #endif - struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; #ifndef __SWITCH__ - addr->sin6_addr = anyaddr; + addr->sin6_addr = anyaddr; #endif - addr->sin6_port = htons(0); - } - else // AF_INET - { - struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; - addr->sin_addr.s_addr = htonl(INADDR_ANY); - addr->sin_port = htons(0); + addr->sin6_port = htons(port); + } + else // AF_INET + { + struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; + addr->sin_addr.s_addr = htonl(INADDR_ANY); + addr->sin_port = htons(port); + } + + r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); + if(r >= 0 || !port) + break; + if(port == CHIAKI_DISCOVERY_PORT_LOCAL_MAX) + { + port = 0; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying random", + (unsigned int)port); + } + else + { + port++; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying one higher", + (unsigned int)port); + } } - int r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); if(r < 0) { CHIAKI_LOGE(discovery->log, "Discovery failed to bind"); diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 09f31ef..f163092 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -238,7 +238,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(service->hosts_count == service->options.hosts_max) { CHIAKI_LOGE(service->log, "Discovery Service received new host, but no space available"); - goto r2con; + goto rzcon; } CHIAKI_LOGI(service->log, "Discovery Service detected new host with id %s", host->host_id); @@ -279,7 +279,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(change) discovery_service_report_state(service); -r2con: +rzcon: chiaki_mutex_unlock(&service->state_mutex); } diff --git a/lib/src/ecdh.c b/lib/src/ecdh.c index 1a48b54..3277b60 100644 --- a/lib/src/ecdh.c +++ b/lib/src/ecdh.c @@ -79,7 +79,6 @@ CHIAKI_EXPORT void chiaki_ecdh_fini(ChiakiECDH *ecdh) #endif } - CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const uint8_t *private_key, size_t private_key_size, const uint8_t *public_key, size_t public_key_size) { #ifdef CHIAKI_LIB_ENABLE_MBEDTLS @@ -89,24 +88,19 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const // public int r = 0; - r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, - public_key, public_key_size); - if(r != 0 ){ + r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, public_key, public_key_size); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // secret r = mbedtls_mpi_read_binary(&ecdh->ctx.d, private_key, private_key_size); - if(r != 0 ){ + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // regen key - r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, - &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); - if(r != 0 ){ + r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } return CHIAKI_ERR_SUCCESS; #else diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d5a46bc..6db8364 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL +#define _USE_MATH_DEFINES + #include #include @@ -9,26 +11,65 @@ #include #endif #include +#include + +#define GYRO_MIN -30.0f +#define GYRO_MAX 30.0f +#define ACCEL_MIN -5.0f +#define ACCEL_MAX 5.0f + +static uint32_t compress_quat(float *q) +{ + // very similar idea as https://github.com/jpreiss/quatcompress + size_t largest_i = 0; + for(size_t i = 1; i < 4; i++) + { + if(fabs(q[i]) > fabs(q[largest_i])) + largest_i = i; + } + uint32_t r = (q[largest_i] < 0.0 ? 1 : 0) | (largest_i << 1); + for(size_t i = 0; i < 3; i++) + { + size_t qi = i < largest_i ? i : i + 1; + float v = q[qi]; + if(v < -M_SQRT1_2) + v = -M_SQRT1_2; + if(v > M_SQRT1_2) + v = M_SQRT1_2; + v += M_SQRT1_2; + v *= (float)0x1ff / (2.0f * M_SQRT1_2); + r |= (uint32_t)v << (3 + i * 9); + } + return r; +} CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackState *state) { - buf[0x0] = 0xa0; // TODO - buf[0x1] = 0xff; // TODO - buf[0x2] = 0x7f; // TODO - buf[0x3] = 0xff; // TODO - buf[0x4] = 0x7f; // TODO - buf[0x5] = 0xff; // TODO - buf[0x6] = 0x7f; // TODO - buf[0x7] = 0xff; // TODO - buf[0x8] = 0x7f; // TODO - buf[0x9] = 0x99; // TODO - buf[0xa] = 0x99; // TODO - buf[0xb] = 0xff; // TODO - buf[0xc] = 0x7f; // TODO - buf[0xd] = 0xfe; // TODO - buf[0xe] = 0xf7; // TODO - buf[0xf] = 0xef; // TODO - buf[0x10] = 0x1f; // TODO + buf[0x0] = 0xa0; + uint16_t v = (uint16_t)(0xffff * ((float)state->gyro_x - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x1] = v; + buf[0x2] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_y - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x3] = v; + buf[0x4] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_z - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x5] = v; + buf[0x6] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_x - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x7] = v; + buf[0x8] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_y - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x9] = v; + buf[0xa] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_z - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0xb] = v; + buf[0xc] = v >> 8; + float q[4] = { state->orient_x, state->orient_y, state->orient_z, state->orient_w }; + uint32_t qc = compress_quat(q); + buf[0xd] = qc; + buf[0xe] = qc >> 0x8; + buf[0xf] = qc >> 0x10; + buf[0x10] = qc >> 0x18; *((chiaki_unaligned_uint16_t *)(buf + 0x11)) = htons((uint16_t)state->left_x); *((chiaki_unaligned_uint16_t *)(buf + 0x13)) = htons((uint16_t)state->left_y); *((chiaki_unaligned_uint16_t *)(buf + 0x15)) = htons((uint16_t)state->right_x); @@ -38,9 +79,13 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) { chiaki_feedback_state_format_v9(buf, state); - buf[0x10] = 0x0; + buf[0x19] = 0x0; buf[0x1a] = 0x0; - buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense + + // 1 is classic DualShock, 0 is DualSense, but using 0 requires setting [0x19] and [0x1a] to + // values taken from raw HID, which is generally not available. But setting 1 for both seems + // to always work fine. + buf[0x1b] = 0x1; } CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index dc67bf6..cafffff 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -83,10 +83,24 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_sender_set_controller_state(Chiaki static bool controller_state_equals_for_feedback_state(ChiakiControllerState *a, ChiakiControllerState *b) { - return a->left_x == b->left_x + if(!(a->left_x == b->left_x && a->left_y == b->left_y && a->right_x == b->right_x - && a->right_y == b->right_y; + && a->right_y == b->right_y)) + return false; +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + return true; } static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) @@ -96,6 +110,18 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) state.left_y = feedback_sender->controller_state.left_y; state.right_x = feedback_sender->controller_state.right_x; state.right_y = feedback_sender->controller_state.right_y; + state.gyro_x = feedback_sender->controller_state.gyro_x; + state.gyro_y = feedback_sender->controller_state.gyro_y; + state.gyro_z = feedback_sender->controller_state.gyro_z; + state.accel_x = feedback_sender->controller_state.accel_x; + state.accel_y = feedback_sender->controller_state.accel_y; + state.accel_z = feedback_sender->controller_state.accel_z; + + state.orient_x = feedback_sender->controller_state.orient_x; + state.orient_y = feedback_sender->controller_state.orient_y; + state.orient_z = feedback_sender->controller_state.orient_z; + state.orient_w = feedback_sender->controller_state.orient_w; + ChiakiErrorCode err = chiaki_takion_send_feedback_state(feedback_sender->takion, feedback_sender->state_seq_num++, &state); if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(feedback_sender->log, "FeedbackSender failed to send Feedback State"); diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index 058f6ac..bcee36b 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -48,6 +48,8 @@ CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_proce frame_processor->buf_stride_per_unit = 0; frame_processor->units_source_expected = 0; frame_processor->units_fec_expected = 0; + frame_processor->units_source_received = 0; + frame_processor->units_fec_received = 0; frame_processor->unit_slots = NULL; frame_processor->unit_slots_size = 0; frame_processor->flushed = true; @@ -150,7 +152,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGE(frame_processor->log, "Packet's unit index is too high"); return CHIAKI_ERR_INVALID_DATA; } - + if(!packet->data_size) { CHIAKI_LOGW(frame_processor->log, "Unit is empty"); @@ -162,7 +164,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGW(frame_processor->log, "Unit is bigger than pre-calculated size!"); return CHIAKI_ERR_INVALID_DATA; } - + ChiakiFrameUnit *unit = frame_processor->unit_slots + packet->unit_index; if(unit->data_size) { diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index c99c5c2..e8dcb12 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -19,10 +19,8 @@ #include "utils.h" - #define KEY_BUF_CHUNK_SIZE 0x1000 - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret); static void *gkcrypt_thread_func(void *user); @@ -110,7 +108,6 @@ CHIAKI_EXPORT void chiaki_gkcrypt_fini(ChiakiGKCrypt *gkcrypt) } } - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret) { uint8_t data[3 + CHIAKI_HANDSHAKE_KEY_SIZE + 2]; @@ -123,37 +120,41 @@ static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, uint8_t hmac[CHIAKI_GKCRYPT_BLOCK_SIZE*2]; size_t hmac_size = sizeof(hmac); - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS +#ifdef CHIAKI_LIB_ENABLE_MBEDTLS mbedtls_md_context_t ctx; mbedtls_md_init(&ctx); - if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256) , 1) != 0){ + if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0){ + if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0){ + if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_finish(&ctx, hmac) != 0){ + if(mbedtls_md_hmac_finish(&ctx, hmac) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } mbedtls_md_free(&ctx); - #else +#else if(!HMAC(EVP_sha256(), ecdh_secret, CHIAKI_ECDH_SECRET_SIZE, data, sizeof(data), hmac, (unsigned int *)&hmac_size)) return CHIAKI_ERR_UNKNOWN; - #endif +#endif assert(hmac_size == sizeof(hmac)); memcpy(gkcrypt->key_base, hmac, CHIAKI_GKCRYPT_BLOCK_SIZE); @@ -220,7 +221,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry mbedtls_aes_context ctx; mbedtls_aes_init(&ctx); - if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0){ + if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0) + { mbedtls_aes_free(&ctx); return CHIAKI_ERR_UNKNOWN; } @@ -248,9 +250,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry counter_add(cur, gkcrypt->iv, counter_offset++); #ifdef CHIAKI_LIB_ENABLE_MBEDTLS - for(int i=0; i #endif - CHIAKI_EXPORT void chiaki_http_header_free(ChiakiHttpHeader *header) { while(header) @@ -147,7 +146,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_recv_http_header(int sock, char *buf, size_ return err; } - int received = (int)recv(sock, buf, (int)buf_size, 0); + int received; + do + { + received = (int)recv(sock, buf, (int)buf_size, 0); +#if _WIN32 + } while(false); +#else + } while(received < 0 && errno == EINTR); +#endif if(received <= 0) return received == 0 ? CHIAKI_ERR_DISCONNECTED : CHIAKI_ERR_NETWORK; diff --git a/lib/src/orientation.c b/lib/src/orientation.c new file mode 100644 index 0000000..7f07b09 --- /dev/null +++ b/lib/src/orientation.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include +#include + +#define SIN_1_4_PI 0.7071067811865475 +#define SIN_NEG_1_4_PI -0.7071067811865475 +#define COS_1_4_PI 0.7071067811865476 +#define COS_NEG_1_4_PI 0.7071067811865476 + +#define WARMUP_SAMPLES_COUNT 30 +#define BETA_WARMUP 20.0f +#define BETA_DEFAULT 0.05f + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) +{ + // 90 deg rotation around x for Madgwick + orient->x = SIN_1_4_PI; + orient->y = 0.0f; + orient->z = 0.0f; + orient->w = COS_1_4_PI; +} + +static float inv_sqrt(float x); + +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec) +{ + float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; + // Madgwick's IMU algorithm. + // See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + float recip_norm; + float s0, s1, s2, s3; + float q_dot1, q_dot2, q_dot3, q_dot4; + float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3; + + // Rate of change of quaternion from gyroscope + q_dot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); + q_dot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); + q_dot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); + q_dot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) + if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) + { + // Normalise accelerometer measurement + recip_norm = inv_sqrt(ax * ax + ay * ay + az * az); + ax *= recip_norm; + ay *= recip_norm; + az *= recip_norm; + + // Auxiliary variables to avoid repeated arithmetic + _2q0 = 2.0f * q0; + _2q1 = 2.0f * q1; + _2q2 = 2.0f * q2; + _2q3 = 2.0f * q3; + _4q0 = 4.0f * q0; + _4q1 = 4.0f * q1; + _4q2 = 4.0f * q2; + _8q1 = 8.0f * q1; + _8q2 = 8.0f * q2; + q0q0 = q0 * q0; + q1q1 = q1 * q1; + q2q2 = q2 * q2; + q3q3 = q3 * q3; + + // Gradient decent algorithm corrective step + s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay; + s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; + s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; + s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; + recip_norm = s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3; // normalise step magnitude + // avoid NaN when the orientation is already perfect or inverse to perfect + if(recip_norm > 0.000001f) + { + recip_norm = inv_sqrt(recip_norm); + s0 *= recip_norm; + s1 *= recip_norm; + s2 *= recip_norm; + s3 *= recip_norm; + + // Apply feedback step + q_dot1 -= beta * s0; + q_dot2 -= beta * s1; + q_dot3 -= beta * s2; + q_dot4 -= beta * s3; + } + } + + // Integrate rate of change of quaternion to yield quaternion + q0 += q_dot1 * time_step_sec; + q1 += q_dot2 * time_step_sec; + q2 += q_dot3 * time_step_sec; + q3 += q_dot4 * time_step_sec; + + // Normalise quaternion + recip_norm = inv_sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); + q0 *= recip_norm; + q1 *= recip_norm; + q2 *= recip_norm; + q3 *= recip_norm; + + orient->x = q1; + orient->y = q2; + orient->z = q3; + orient->w = q0; +} + +static float inv_sqrt(float x) +{ +#if 1 + return 1.0f / sqrt(x); +#else + // Fast inverse square-root + // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root + float halfx = 0.5f * x; + float y = x; + long i = *(long*)&y; + i = 0x5f3759df - (i>>1); + y = *(float*)&i; + y = y * (1.5f - (halfx * y * y)); + return y; +#endif +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) +{ + tracker->accel_x = 0.0f; + tracker->accel_y = 1.0f; + tracker->accel_z = 0.0f; + tracker->gyro_x = tracker->gyro_y = tracker->gyro_z = 0.0f; + chiaki_orientation_init(&tracker->orient); + tracker->timestamp = 0; + tracker->sample_index = 0; +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us) +{ + tracker->gyro_x = gx; + tracker->gyro_y = gy; + tracker->gyro_z = gz; + tracker->accel_x = ax; + tracker->accel_y = ay; + tracker->accel_z = az; + tracker->sample_index++; + if(tracker->sample_index <= 1) + { + tracker->timestamp = timestamp_us; + return; + } + uint64_t delta_us = timestamp_us; + if(delta_us < tracker->timestamp) + delta_us += (1ULL << 32); + delta_us -= tracker->timestamp; + tracker->timestamp = timestamp_us; + chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, + tracker->sample_index < WARMUP_SAMPLES_COUNT ? BETA_WARMUP : BETA_DEFAULT, + (float)delta_us / 1000000.0f); +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state) +{ + state->gyro_x = tracker->gyro_x; + state->gyro_y = tracker->gyro_y; + state->gyro_z = tracker->gyro_z; + state->accel_x = tracker->accel_x; + state->accel_y = tracker->accel_y; + state->accel_z = tracker->accel_z; + // -90 deg rotation around x from Madgwick + state->orient_w = COS_NEG_1_4_PI * tracker->orient.w - SIN_NEG_1_4_PI * tracker->orient.x; + state->orient_x = COS_NEG_1_4_PI * tracker->orient.x + SIN_NEG_1_4_PI * tracker->orient.w; + state->orient_y = COS_NEG_1_4_PI * tracker->orient.y - SIN_NEG_1_4_PI * tracker->orient.z; + state->orient_z = COS_NEG_1_4_PI * tracker->orient.z + SIN_NEG_1_4_PI * tracker->orient.y; +} diff --git a/lib/src/packetstats.c b/lib/src/packetstats.c index c04adc8..c7da9d6 100644 --- a/lib/src/packetstats.c +++ b/lib/src/packetstats.c @@ -74,4 +74,3 @@ CHIAKI_EXPORT void chiaki_packet_stats_get(ChiakiPacketStats *stats, bool reset, reset_stats(stats); chiaki_mutex_unlock(&stats->mutex); } - diff --git a/lib/src/pidecoder.c b/lib/src/pidecoder.c index 72a22f5..ae99fbd 100644 --- a/lib/src/pidecoder.c +++ b/lib/src/pidecoder.c @@ -8,7 +8,6 @@ #include #include -#define MAX_DECODE_UNIT_SIZE 262144 CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, ChiakiLog *log) { @@ -108,29 +107,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, C return CHIAKI_ERR_UNKNOWN; } - OMX_PARAM_PORTDEFINITIONTYPE port; - - memset(&port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); - port.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); - port.nVersion.nVersion = OMX_VERSION; - port.nPortIndex = 130; - if(OMX_GetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "Failed to get decoder port definition\n"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - - // Increase the buffer size to fit the largest possible frame - port.nBufferSize = MAX_DECODE_UNIT_SIZE; - - if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for port"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - if(ilclient_enable_port_buffers(decoder->video_decode, 130, NULL, NULL, NULL) != 0) { CHIAKI_LOGE(decoder->log, "ilclient_enable_port_buffers failed"); @@ -180,50 +156,55 @@ CHIAKI_EXPORT void chiaki_pi_decoder_fini(ChiakiPiDecoder *decoder) static bool push_buffer(ChiakiPiDecoder *decoder, uint8_t *buf, size_t buf_size) { - OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); - if(!omx_buf) + while(buf_size) { - CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); - return false; - } - - if(omx_buf->nAllocLen < buf_size) - { - CHIAKI_LOGE(decoder->log, "Buffer from omx is too small for frame"); - return false; - } - - omx_buf->nFilledLen = 0; - omx_buf->nOffset = 0; - omx_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; - if(decoder->first_packet) - { - omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; - decoder->first_packet = false; - } - - memcpy(omx_buf->pBuffer + omx_buf->nFilledLen, buf, buf_size); - omx_buf->nFilledLen += buf_size; - - if(!decoder->port_settings_changed - && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) - || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) - { - decoder->port_settings_changed = true; - - if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); + if(!omx_buf) { - CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); return false; } - ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); - } + size_t push_size = buf_size; + if(push_size > omx_buf->nAllocLen) + { + CHIAKI_LOGW(decoder->log, "OMX Buffer too small, fragmenting to multiple buffers."); + push_size = omx_buf->nAllocLen; + } + memcpy(omx_buf->pBuffer, buf, push_size); + buf_size -= push_size; + buf += push_size; + omx_buf->nFilledLen = push_size; + omx_buf->nOffset = 0; + omx_buf->nFlags = 0; + if(!buf_size) + omx_buf->nFlags |= OMX_BUFFERFLAG_ENDOFFRAME; + if(decoder->first_packet) + { + omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; + decoder->first_packet = false; + } - if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); - return false; + if(!decoder->port_settings_changed + && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) + || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) + { + decoder->port_settings_changed = true; + + if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + return false; + } + + ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); + } + + if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); + return false; + } } return true; } diff --git a/lib/src/random.c b/lib/src/random.c index 974db60..158ce4d 100644 --- a/lib/src/random.c +++ b/lib/src/random.c @@ -28,10 +28,12 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_random_bytes_crypt(uint8_t *buf, size_t buf mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_set_prediction_resistance(&ctr_drbg, MBEDTLS_CTR_DRBG_PR_OFF); - if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) "RANDOM_GEN", 10 ) != 0 ){ + if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)"RANDOM_GEN", 10) != 0) + { return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0){ + if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0) + { return CHIAKI_ERR_UNKNOWN; } diff --git a/lib/src/regist.c b/lib/src/regist.c index 47c6ab1..af17942 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -160,7 +160,6 @@ static int request_header_format(char *buf, size_t buf_size, size_t payload_size return cur; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget target, const uint8_t *ambassador, uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id, uint32_t pin) { size_t buf_size_val = *buf_size; diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 197580e..15b64d0 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1488,7 +1488,7 @@ static ChiakiErrorCode bright_ambassador(ChiakiTarget target, uint8_t *bright, u 0x20, 0x98, 0xfd, 0x34, 0xca, 0x7a, 0x66, 0x20, 0x58, 0xd2, 0x36, 0x7f, 0x2b, 0xa7, 0xd1, 0xde, 0x6f, 0x36, 0xb4, 0xf2, 0x3b, 0x20, 0x5d, 0x02 - }; + }; if(target < CHIAKI_TARGET_PS4_10) return CHIAKI_ERR_INVALID_DATA; @@ -1698,7 +1698,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_aeropause(ChiakiTarget target, size CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *nonce, const uint8_t *morning) { rpcrypt->target = target; - chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); + chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); } CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin) @@ -1865,7 +1865,6 @@ static const uint8_t *rpcrypt_hmac_key(ChiakiRPCrypt *rpcrypt) } } - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter) { @@ -1968,7 +1967,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, buf[CHIAKI_RPCRYPT_KEY_SIZE + 6] = (uint8_t)((counter >> 0x08) & 0xff); buf[CHIAKI_RPCRYPT_KEY_SIZE + 7] = (uint8_t)((counter >> 0x00) & 0xff); - uint8_t hmac[32]; unsigned int hmac_len = 0; if(!HMAC(EVP_sha256(), hmac_key, CHIAKI_RPCRYPT_KEY_SIZE, buf, sizeof(buf), hmac, &hmac_len)) diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index 1fa33d4..fc4e600 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -361,7 +361,7 @@ static ChiakiErrorCode senkusha_run_mtu_in_test(ChiakiSenkusha *senkusha, uint32 senkusha->state_failed = false; senkusha->mtu_id = ++request_id; - tkproto_SenkushaMtuCommand mtu_cmd; + tkproto_SenkushaMtuCommand mtu_cmd = { 0 }; mtu_cmd.id = request_id; mtu_cmd.mtu_req = cur; mtu_cmd.num = 1; @@ -645,7 +645,6 @@ static void senkusha_takion_data(ChiakiSenkusha *senkusha, ChiakiTakionMessageDa senkusha->state_finished = true; chiaki_cond_signal(&senkusha->state_cond); } - } chiaki_mutex_unlock(&senkusha->state_mutex); } @@ -878,4 +877,3 @@ static ChiakiErrorCode senkusha_send_data_wait_for_ack(ChiakiSenkusha *senkusha, return err; } - diff --git a/lib/src/session.c b/lib/src/session.c index de2f4e2..aa56e19 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -158,6 +158,8 @@ CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason) return "Unknown Error in Stream Connection"; case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED: return "Remote has disconnected from Stream Connection"; + case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN: + return "Remote has disconnected from Stream Connection the because Server shut down"; case CHIAKI_QUIT_REASON_NONE: default: return "Unknown"; @@ -227,6 +229,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki session->connect_info.video_profile = connect_info->video_profile; session->connect_info.video_profile_auto_downgrade = connect_info->video_profile_auto_downgrade; session->connect_info.enable_keyboard = connect_info->enable_keyboard; + session->connect_info.enable_dualsense = connect_info->enable_dualsense; return CHIAKI_ERR_SUCCESS; error_stop_pipe: @@ -504,7 +507,10 @@ ctrl_failed: if(err == CHIAKI_ERR_DISCONNECTED) { CHIAKI_LOGE(session->log, "Remote disconnected from StreamConnection"); - session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; + if(!strcmp(session->stream_connection.remote_disconnect_reason, "Server shutting down")) + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; + else + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; session->quit_reason_str = strdup(session->stream_connection.remote_disconnect_reason); } else if(err != CHIAKI_ERR_SUCCESS && err != CHIAKI_ERR_CANCELED) @@ -540,10 +546,8 @@ quit: #undef QUIT } - - - -typedef struct session_response_t { +typedef struct session_response_t +{ uint32_t error_code; const char *nonce; const char *rp_version; @@ -596,11 +600,11 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch set_port(sa, htons(SESSION_PORT)); // TODO: this can block, make cancelable somehow - int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, 0); + int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, NI_NUMERICHOST); if(r != 0) { - free(sa); - continue; + CHIAKI_LOGE(session->log, "getnameinfo failed with %s, filling the hostname with fallback", gai_strerror(r)); + memcpy(session->connect_info.hostname, "unknown", 8); } CHIAKI_LOGI(session->log, "Trying to request session from %s:%d", session->connect_info.hostname, SESSION_PORT); @@ -611,7 +615,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch #ifdef _WIN32 CHIAKI_LOGE(session->log, "Failed to create socket to request session"); #else - CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); + CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); #endif free(sa); continue; @@ -652,7 +656,6 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch break; } - if(CHIAKI_SOCKET_IS_INVALID(session_sock)) { CHIAKI_LOGE(session->log, "Session request connect failed eventually."); diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index dd069ef..003ad5d 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -27,9 +27,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_init(ChiakiStopPipe *stop_pipe) int addr_size = sizeof(stop_pipe->addr); stop_pipe->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if(stop_pipe->fd < 0){ + if(stop_pipe->fd < 0) return CHIAKI_ERR_UNKNOWN; - } stop_pipe->addr.sin_family = AF_INET; // bind to localhost stop_pipe->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); @@ -148,7 +147,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_select_single(ChiakiStopPipe *sto timeout = &timeout_s; } - int r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + int r; + do + { + r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + } while(r < 0 && errno == EINTR); if(r < 0) return CHIAKI_ERR_UNKNOWN; diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index c8ed43c..7187c1b 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -45,7 +45,11 @@ void chiaki_session_send_event(ChiakiSession *session, ChiakiEvent *event); static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user); static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection); +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection); static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection); static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); @@ -54,7 +58,6 @@ static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnect static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet); static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection); - CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnection *stream_connection, ChiakiSession *session) { stream_connection->session = session; @@ -78,6 +81,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti stream_connection->video_receiver = NULL; stream_connection->audio_receiver = NULL; + stream_connection->haptics_receiver = NULL; err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false); if(err != CHIAKI_ERR_SUCCESS) @@ -119,7 +123,6 @@ CHIAKI_EXPORT void chiaki_stream_connection_fini(ChiakiStreamConnection *stream_ chiaki_mutex_fini(&stream_connection->state_mutex); } - static bool state_finished_cond_check(void *user) { ChiakiStreamConnection *stream_connection = user; @@ -143,6 +146,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio takion_info.ip_dontfrag = false; takion_info.enable_crypt = true; + takion_info.enable_dualsense = session->connect_info.enable_dualsense; takion_info.protocol_version = chiaki_target_is_ps5(session->target) ? 12 : 9; takion_info.cb = stream_connection_takion_cb; @@ -164,12 +168,20 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio return CHIAKI_ERR_UNKNOWN; } + stream_connection->haptics_receiver = chiaki_audio_receiver_new(session, NULL); + if(!stream_connection->haptics_receiver) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Haptics Receiver"); + err = CHIAKI_ERR_UNKNOWN; + goto err_audio_receiver; + } + stream_connection->video_receiver = chiaki_video_receiver_new(session, &stream_connection->packet_stats); if(!stream_connection->video_receiver) { CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Video Receiver"); err = CHIAKI_ERR_UNKNOWN; - goto err_audio_receiver; + goto err_haptics_receiver; } stream_connection->state = STATE_TAKION_CONNECT; @@ -321,6 +333,10 @@ err_video_receiver: chiaki_video_receiver_free(stream_connection->video_receiver); stream_connection->video_receiver = NULL; +err_haptics_receiver: + chiaki_audio_receiver_free(stream_connection->haptics_receiver); + stream_connection->haptics_receiver = NULL; + err_audio_receiver: chiaki_audio_receiver_free(stream_connection->audio_receiver); stream_connection->audio_receiver = NULL; @@ -368,9 +384,24 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user) static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size) { - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF) - return; + switch(data_type) + { + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF: + stream_connection_takion_data_protobuf(stream_connection, buf, buf_size); + break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE: + stream_connection_takion_data_rumble(stream_connection, buf, buf_size); + break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS: + stream_connection_takion_data_trigger_effects(stream_connection, buf, buf_size); + break; + default: + break; + } +} +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ chiaki_mutex_lock(&stream_connection->state_mutex); switch(stream_connection->state) { @@ -387,6 +418,40 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect chiaki_mutex_unlock(&stream_connection->state_mutex); } +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 3) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got rumble packet with size %#llx < 3", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_RUMBLE; + event.rumble.unknown = buf[0]; + event.rumble.left = buf[1]; + event.rumble.right = buf[2]; + chiaki_session_send_event(stream_connection->session, &event); +} + + +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 25) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got trigger effects packet with size %#llx < 25", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_TRIGGER_EFFECTS; + event.trigger_effects.type_left = buf[1]; + event.trigger_effects.type_right = buf[2]; + memcpy(&event.trigger_effects.left, buf + 5, 10); + memcpy(&event.trigger_effects.right, buf + 15, 10); + chiaki_session_send_event(stream_connection->session, &event); +} + static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -432,7 +497,7 @@ static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_co return; } - CHIAKI_LOGV(stream_connection->log, "StreamConnection received data"); + CHIAKI_LOGV(stream_connection->log, "StreamConnection received data with msg.type == %d", msg.type); chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); if(msg.type == tkproto_TakionMessage_PayloadType_DISCONNECT) @@ -494,7 +559,8 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st return; } - CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else"); + CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else: %d", msg.type); + chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); return; } @@ -556,6 +622,12 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st // stream_connection->state_mutex is expected to be locked by the caller of this function stream_connection->state_finished = true; chiaki_cond_signal(&stream_connection->state_cond); + err = stream_connection_send_controller_connection(stream_connection); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection failed to send controller connection"); + goto error; + } return; error: stream_connection->state_failed = true; @@ -609,7 +681,6 @@ static bool pb_decode_resolution(pb_istream_t *stream, const pb_field_t *field, return true; } - static void stream_connection_takion_data_expect_streaminfo(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -678,11 +749,9 @@ error: chiaki_cond_signal(&stream_connection->state_cond); } - - static bool chiaki_pb_encode_zero_encrypted_key(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) { - if (!pb_encode_tag_for_field(stream, field)) + if(!pb_encode_tag_for_field(stream, field)) return false; uint8_t data[] = { 0, 0, 0, 0 }; return pb_encode_string(stream, data, sizeof(data)); @@ -786,6 +855,37 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream return err; } +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection) +{ + ChiakiSession *session = stream_connection->session; + tkproto_TakionMessage msg; + memset(&msg, 0, sizeof(msg)); + + msg.type = tkproto_TakionMessage_PayloadType_CONTROLLERCONNECTION; + msg.has_controller_connection_payload = true; + msg.controller_connection_payload.has_connected = true; + msg.controller_connection_payload.connected = true; + msg.controller_connection_payload.has_controller_id = false; + msg.controller_connection_payload.has_controller_type = true; + msg.controller_connection_payload.controller_type = session->connect_info.enable_dualsense + ? tkproto_ControllerConnectionPayload_ControllerType_DUALSENSE + : tkproto_ControllerConnectionPayload_ControllerType_DUALSHOCK4; + + uint8_t buf[2048]; + size_t buf_size; + + pb_ostream_t stream = pb_ostream_from_buffer(buf, sizeof(buf)); + bool pbr = pb_encode(&stream, tkproto_TakionMessage_fields, &msg); + if(!pbr) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection controller connection protobuf encoding failed"); + return CHIAKI_ERR_UNKNOWN; + } + + buf_size = stream.bytes_written; + return chiaki_takion_send_message_data(&stream_connection->takion, 1, 1, buf, buf_size, NULL); +} + static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg; @@ -836,18 +936,18 @@ static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection return err; } - static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet) { chiaki_gkcrypt_decrypt(stream_connection->gkcrypt_remote, packet->key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, packet->data, packet->data_size); if(packet->is_video) chiaki_video_receiver_av_packet(stream_connection->video_receiver, packet); + else if(packet->is_haptics) + chiaki_audio_receiver_av_packet(stream_connection->haptics_receiver, packet); else chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } - static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg = { 0 }; diff --git a/lib/src/takion.c b/lib/src/takion.c index 48a1537..baae28c 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -56,9 +56,9 @@ typedef enum takion_packet_type_t { TAKION_PACKET_TYPE_HANDSHAKE = 4, TAKION_PACKET_TYPE_CONGESTION = 5, TAKION_PACKET_TYPE_FEEDBACK_STATE = 6, - TAKION_PACKET_TYPE_RUMBLE_EVENT = 7, TAKION_PACKET_TYPE_CLIENT_INFO = 8, - TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9 + TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9, + TAKION_PACKET_TYPE_PAD_ADAPTIVE_TRIGGERS = 11, } TakionPacketType; /** @@ -108,7 +108,6 @@ typedef enum takion_chunk_type_t { TAKION_CHUNK_TYPE_COOKIE_ACK = 0xb, } TakionChunkType; - typedef struct takion_message_t { uint32_t tag; @@ -121,7 +120,6 @@ typedef struct takion_message_t uint8_t *payload; } TakionMessage; - typedef struct takion_message_payload_init_t { uint32_t tag; @@ -153,14 +151,12 @@ typedef struct uint16_t channel; } TakionDataPacketEntry; - typedef struct chiaki_takion_postponed_packet_t { uint8_t *buf; size_t buf_size; } ChiakiTakionPostponedPacket; - static void *takion_thread_func(void *user); static void takion_handle_packet(ChiakiTakion *takion, uint8_t *buf, size_t buf_size); static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size); @@ -220,6 +216,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki takion->postponed_packets = NULL; takion->postponed_packets_size = 0; takion->postponed_packets_count = 0; + takion->enable_dualsense = info->enable_dualsense; CHIAKI_LOGI(takion->log, "Takion connecting (version %u)", (unsigned int)info->protocol_version); @@ -508,7 +505,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion return err; uint8_t buf[CHIAKI_TAKION_CONGESTION_PACKET_SIZE]; - chiaki_takion_format_congestion(buf, packet, key_pos); + chiaki_takion_format_congestion(buf, packet, key_pos); return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } @@ -604,7 +601,6 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ CHIAKI_LOGI(takion->log, "Takion sent init"); - // INIT_ACK <- TakionMessagePayloadInitAck init_ack_payload; @@ -622,20 +618,17 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ } CHIAKI_LOGI(takion->log, "Takion received init ack with remote tag %#x, outbound streams: %#x, inbound streams: %#x", - init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); + init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); takion->tag_remote = init_ack_payload.tag; *seq_num_remote_initial = takion->tag_remote; //init_ack_payload.initial_seq_num; - if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 - || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS - || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) + if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) { CHIAKI_LOGE(takion->log, "Takion min/max check failed"); return CHIAKI_ERR_INVALID_RESPONSE; } - // COOKIE -> err = takion_send_message_cookie(takion, init_ack_payload.cookie); @@ -781,7 +774,6 @@ beach: return NULL; } - static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *buf_size, uint64_t timeout_ms) { ChiakiErrorCode err = chiaki_stop_pipe_select_single(&takion->stop_pipe, takion->sock, false, timeout_ms); @@ -806,7 +798,6 @@ static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *b return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { if(!takion->gkcrypt_remote) @@ -867,7 +858,6 @@ static void takion_postpone_packet(ChiakiTakion *takion, uint8_t *buf, size_t bu packet->buf_size = buf_size; } - /** * @param buf ownership of this buf is taken. */ @@ -935,7 +925,6 @@ static void takion_handle_packet_message(ChiakiTakion *takion, uint8_t *buf, siz } } - static void takion_flush_data_queue(ChiakiTakion *takion) { uint64_t seq_num = 0; @@ -961,7 +950,10 @@ static void takion_flush_data_queue(ChiakiTakion *takion) if(zero_a != 0) CHIAKI_LOGW(takion->log, "Takion received data with unexpected nonzero %#x at buf+6", zero_a); - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) + if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) { CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type); chiaki_log_hexdump(takion->log, CHIAKI_LOG_WARNING, entry->packet_buf, entry->packet_size); @@ -1103,7 +1095,6 @@ static ChiakiErrorCode takion_parse_message(ChiakiTakion *takion, uint8_t *buf, return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMessagePayloadInit *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10]; @@ -1120,8 +1111,6 @@ static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMess return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t *cookie) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + TAKION_COOKIE_SIZE]; @@ -1131,8 +1120,6 @@ static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, TakionMessagePayloadInitAck *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10 + TAKION_COOKIE_SIZE]; @@ -1180,7 +1167,6 @@ static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, Takion return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE]; @@ -1220,7 +1206,6 @@ static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) return CHIAKI_ERR_SUCCESS; } - static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { // HHIxIIx @@ -1326,7 +1311,7 @@ static ChiakiErrorCode av_packet_parse(bool v12, ChiakiTakionAVPacket *packet, C if(v12 && !packet->is_video) { - packet->byte_before_audio_data = *av; + packet->is_haptics = *av == 0x02; av += 1; av_size -= 1; } @@ -1452,5 +1437,4 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPac packet->data_size = buf_size; return CHIAKI_ERR_SUCCESS; - } diff --git a/lib/src/thread.c b/lib/src/thread.c index 1948311..0539e16 100644 --- a/lib/src/thread.c +++ b/lib/src/thread.c @@ -23,7 +23,8 @@ static DWORD WINAPI win32_thread_func(LPVOID param) #endif #ifdef __SWITCH__ -int64_t get_thread_limit(){ +int64_t get_thread_limit() +{ uint64_t resource_limit_handle_value = INVALID_HANDLE; svcGetInfo(&resource_limit_handle_value, InfoType_ResourceLimit, INVALID_HANDLE, 0); int64_t thread_cur_value = 0, thread_lim_value = 0; @@ -45,9 +46,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_create(ChiakiThread *thread, ChiakiT return CHIAKI_ERR_THREAD; #else #ifdef __SWITCH__ - if(get_thread_limit() <= 1){ + if(get_thread_limit() <= 1) return CHIAKI_ERR_THREAD; - } #endif int r = pthread_create(&thread->thread, NULL, func, arg); if(r != 0) @@ -90,13 +90,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_set_name(ChiakiThread *thread, const if(r != 0) return CHIAKI_ERR_THREAD; #else - (void)thread; (void)name; + (void)thread; + (void)name; #endif #endif return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_init(ChiakiMutex *mutex, bool rec) { #if _WIN32 @@ -172,9 +172,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_unlock(ChiakiMutex *mutex) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_init(ChiakiCond *cond) { #if _WIN32 @@ -214,8 +211,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_fini(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_wait(ChiakiCond *cond, ChiakiMutex *mutex) { #if _WIN32 @@ -323,7 +318,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_timedwait_pred(ChiakiCond *cond, Chiak #endif } return CHIAKI_ERR_SUCCESS; - } CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_signal(ChiakiCond *cond) @@ -350,9 +344,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_broadcast(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_init(ChiakiBoolPredCond *cond) { cond->pred = false; @@ -384,7 +375,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_fini(ChiakiBoolPredCond *con return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_lock(ChiakiBoolPredCond *cond) { return chiaki_mutex_lock(&cond->mutex); diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index 52beb02..6468fcd 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -17,6 +17,7 @@ CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receive video_receiver->frame_index_cur = -1; video_receiver->frame_index_prev = -1; + video_receiver->frame_index_prev_complete = 0; chiaki_frame_processor_init(&video_receiver->frame_processor, video_receiver->log); video_receiver->packet_stats = packet_stats; diff --git a/scripts/Dockerfile.noble b/scripts/Dockerfile.noble new file mode 100644 index 0000000..abc33c3 --- /dev/null +++ b/scripts/Dockerfile.noble @@ -0,0 +1,13 @@ + +FROM ubuntu:noble + +RUN apt-get update +# Hint: python3-setuptools should not be needed with nanopb >= 0.4.9 +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip \ + python3-protobuf protobuf-compiler \ + python3-setuptools \ + libssl-dev libopus-dev qtbase5-dev qtmultimedia5-dev libqt5multimedia5-plugins libqt5svg5-dev \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev file + +CMD [] + diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial deleted file mode 100644 index b741028..0000000 --- a/scripts/Dockerfile.xenial +++ /dev/null @@ -1,13 +0,0 @@ - -FROM ubuntu:xenial - -RUN apt-get update -RUN apt-get install -y software-properties-common -RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial -RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl unzip python3-pip \ - libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse - -CMD [] - diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index f751200..7bbc0f3 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -1,77 +1,92 @@ #!/bin/bash -echo "APPVEYOR_BUILD_FOLDER=$APPVEYOR_BUILD_FOLDER" +set -xe -mkdir ninja && cd ninja || exit 1 -wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip || exit 1 -cd .. || exit 1 +BUILD_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" +BUILD_ROOT="$(echo $BUILD_ROOT | sed 's|^/\([a-z]\)|\1:|g')" # replace /c/... by c:/... for cmake to understand it +echo "BUILD_ROOT=$BUILD_ROOT" -mkdir yasm && cd yasm || exit 1 -wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe || exit 1 -cd .. || exit 1 +mkdir ninja && cd ninja +wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip +cd .. + +mkdir yasm && cd yasm +wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe +cd .. export PATH="$PWD/ninja:$PWD/yasm:/c/Qt/5.12/msvc2017_64/bin:$PATH" -scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc || exit 1 +scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc -git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab || exit 1 -mkdir build && cd build || exit 1 +git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab +mkdir build && cd build cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="$APPVEYOR_BUILD_FOLDER/opus-prefix" \ - .. || exit 1 -ninja || exit 1 -ninja install || exit 1 -cd ../.. || exit 1 + -DCMAKE_INSTALL_PREFIX="$BUILD_ROOT/opus-prefix" \ + .. +ninja +ninja install +cd ../.. -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1d-dev.zip && 7z x openssl-1.1.1d-dev.zip || exit 1 +wget https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1s.zip && 7z x openssl-1.1.*.zip -wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 -export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 -export SDL_ROOT=${SDL_ROOT//[\\]//} || exit 1 +wget https://www.libsdl.org/release/SDL2-devel-2.26.2-VC.zip && 7z x SDL2-devel-2.26.2-VC.zip +export SDL_ROOT="$BUILD_ROOT/SDL2-2.26.2" +export SDL_ROOT=${SDL_ROOT//[\\]//} echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\") set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\") -set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" || exit 1 +set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\") +include($SDL_ROOT/cmake/sdl2-config-version.cmake)" > "$SDL_ROOT/SDL2Config.cmake" -mkdir protoc && cd protoc || exit 1 -wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip || exit 1 -cd .. || exit 1 -export PATH="$PWD/protoc/bin:$PATH" || exit 1 +mkdir protoc && cd protoc +wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip +cd .. +export PATH="$PWD/protoc/bin:$PATH" PYTHON="C:/Python37/python.exe" -"$PYTHON" -m pip install protobuf || exit 1 +"$PYTHON" -m pip install protobuf==3.19.5 -QT_PATH="C:/Qt/5.12/msvc2017_64" +QT_PATH="C:/Qt/5.15/msvc2019_64" COPY_DLLS="$PWD/openssl-1.1/x64/bin/libcrypto-1_1-x64.dll $PWD/openssl-1.1/x64/bin/libssl-1_1-x64.dll $SDL_ROOT/lib/x64/SDL2.dll" -mkdir build && cd build || exit 1 +echo "-- Configure" + +mkdir build && cd build + cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_C_FLAGS="-we4013" \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_PREFIX_PATH="$APPVEYOR_BUILD_FOLDER/ffmpeg-prefix;$APPVEYOR_BUILD_FOLDER/opus-prefix;$APPVEYOR_BUILD_FOLDER/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ + -DCMAKE_PREFIX_PATH="$BUILD_ROOT/ffmpeg-prefix;$BUILD_ROOT/opus-prefix;$BUILD_ROOT/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ -DPYTHON_EXECUTABLE="$PYTHON" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ - .. || exit 1 + .. -ninja || exit 1 +echo "-- Build" -test/chiaki-unit.exe || exit 1 +ninja -cd .. || exit 1 +echo "-- Test" + +cp $COPY_DLLS test/ +test/chiaki-unit.exe + +cd .. # Deploy -mkdir Chiaki && cp build/gui/chiaki.exe Chiaki || exit 1 -mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB || exit 1 +echo "-- Deploy" -"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe || exit 1 +mkdir Chiaki && cp build/gui/chiaki.exe Chiaki +mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB + +"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe cp -v $COPY_DLLS Chiaki diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 7ba4acf..05c0428 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -2,10 +2,11 @@ set -xe +# sometimes there are errors in linuxdeploy in docker/podman when the appdir is on a mount +appdir=${1:-`pwd`/appimage/appdir} + mkdir appimage -pip3 install --user protobuf -scripts/fetch-protoc.sh appimage export PATH="`pwd`/appimage/protoc/bin:$PATH" scripts/build-ffmpeg.sh appimage scripts/build-sdl2.sh appimage @@ -15,29 +16,31 @@ cd build_appimage cmake \ -GNinja \ -DCMAKE_BUILD_TYPE=Release \ - "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ + "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ + -DCHIAKI_ENABLE_GUI=ON \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ -DCMAKE_INSTALL_PREFIX=/usr \ .. cd .. + +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + ninja -C build_appimage build_appimage/test/chiaki-unit -DESTDIR=`pwd`/appimage/appdir ninja -C build_appimage install +DESTDIR="${appdir}" ninja -C build_appimage install cd appimage curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage chmod +x linuxdeploy-plugin-qt-x86_64.AppImage -set +e -source /opt/qt512/bin/qt512-env.sh -set -e export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl -./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage -mv Chiaki-*-x86_64.AppImage Chiaki.AppImage +./linuxdeploy-x86_64.AppImage --appdir="${appdir}" -e "${appdir}/usr/bin/chiaki" -d "${appdir}/usr/share/applications/chiaki.desktop" --plugin qt --output appimage +mv Chiaki*-x86_64.AppImage Chiaki.AppImage diff --git a/scripts/build-common.sh b/scripts/build-common.sh index 0eb87b9..dd4be2c 100755 --- a/scripts/build-common.sh +++ b/scripts/build-common.sh @@ -1,5 +1,8 @@ #!/bin/bash +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + mkdir build && cd build || exit 1 cmake \ -DCMAKE_BUILD_TYPE=Release \ diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 4761520..9b6348f 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -5,10 +5,10 @@ cd "./$1" shift ROOT="`pwd`" -TAG=n4.3.1 +TAG=n4.3.9 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index f06b9cc..716b486 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -6,9 +6,10 @@ cd $(dirname "${BASH_SOURCE[0]}")/.. cd "./$1" ROOT="`pwd`" -URL=https://www.libsdl.org/release/SDL2-2.0.10.tar.gz -FILE=SDL2-2.0.10.tar.gz -DIR=SDL2-2.0.10 +SDL_VER=2.26.1 +URL=https://www.libsdl.org/release/SDL2-${SDL_VER}.tar.gz +FILE=SDL2-${SDL_VER}.tar.gz +DIR=SDL2-${SDL_VER} if [ ! -d "$DIR" ]; then curl -L "$URL" -O @@ -21,14 +22,14 @@ mkdir -p build && cd build || exit 1 cmake \ -DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \ -DSDL_ATOMIC=OFF \ - -DSDL_AUDIO=OFF \ + -DSDL_AUDIO=ON \ -DSDL_CPUINFO=OFF \ -DSDL_EVENTS=ON \ -DSDL_FILE=OFF \ -DSDL_FILESYSTEM=OFF \ -DSDL_HAPTIC=ON \ -DSDL_JOYSTICK=ON \ - -DSDL_LOADSO=OFF \ + -DSDL_LOADSO=ON \ -DSDL_RENDER=OFF \ -DSDL_SHARED=ON \ -DSDL_STATIC=OFF \ diff --git a/scripts/fetch-protoc.sh b/scripts/fetch-protoc.sh deleted file mode 100755 index e1d2d2f..0000000 --- a/scripts/fetch-protoc.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -xe - -cd $(dirname "${BASH_SOURCE[0]}")/.. -cd "./$1" -ROOT="`pwd`" - -URL=https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-linux-x86_64.zip - -curl -L "$URL" -o protoc.zip -unzip protoc.zip -d protoc - diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 7cc6c43..daab8a0 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v1.3.0", - "commit": "702d31eb01d37518e77f5c1be3ea493df9f18323" + "tag": "v2.1.1", + "commit": "2257030adeb5c5a84380c78d34be4d783971663c" } ] } diff --git a/scripts/macos-dist-local.sh b/scripts/macos-dist-local.sh new file mode 100755 index 0000000..d2ac740 --- /dev/null +++ b/scripts/macos-dist-local.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Build Chiaki for macOS distribution using dependencies from MacPorts and custom ffmpeg + +set -xe +cd $(dirname "${BASH_SOURCE[0]}")/.. +scripts/build-ffmpeg.sh +export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix" +scripts/build-common.sh +cp -a build/gui/chiaki.app Chiaki.app +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app + +# Remove all LC_RPATH load commands that have absolute paths of the build machine +RPATHS=$(otool -l Chiaki.app/Contents/MacOS/chiaki | grep -A 2 LC_RPATH | grep 'path /' | awk '{print $2}') +for p in ${RPATHS}; do install_name_tool -delete_rpath "$p" Chiaki.app/Contents/MacOS/chiaki; done + +# This may warn because we already ran macdeployqt above +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app -dmg diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-docker-build-appimage.sh deleted file mode 100755 index 931499c..0000000 --- a/scripts/run-docker-build-appimage.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -xe -cd "`dirname $(readlink -f ${0})`" - -docker build -t chiaki-xenial . -f Dockerfile.xenial -cd .. -docker run --rm \ - -v "`pwd`:/build/chiaki" \ - -w "/build/chiaki" \ - --device /dev/fuse \ - --cap-add SYS_ADMIN \ - -t chiaki-xenial \ - /bin/bash -c "scripts/build-appimage.sh" - diff --git a/scripts/run-docker-build-bullseye.sh b/scripts/run-docker-build-bullseye.sh deleted file mode 100755 index fd19bb1..0000000 --- a/scripts/run-docker-build-bullseye.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -xe -cd "`dirname $(readlink -f ${0})`" - -docker build -t chiaki-bullseye . -f Dockerfile.bullseye -cd .. -docker run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " - cd /build && - mkdir build_bullseye && - cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && - ninja -C build_bullseye && - ninja -C build_bullseye test" - diff --git a/scripts/run-podman-build-appimage.sh b/scripts/run-podman-build-appimage.sh new file mode 100755 index 0000000..5024952 --- /dev/null +++ b/scripts/run-podman-build-appimage.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -xe +cd "`dirname $(readlink -f ${0})`" + +podman build --arch amd64 -t localhost/chiaki-noble . -f Dockerfile.noble +cd .. +podman run --rm \ + --arch amd64 \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + --device /dev/fuse \ + --cap-add SYS_ADMIN \ + -t localhost/chiaki-noble \ + /bin/bash -c "scripts/build-appimage.sh /build/appdir" + diff --git a/scripts/run-podman-build-bullseye.sh b/scripts/run-podman-build-bullseye.sh new file mode 100755 index 0000000..344a76e --- /dev/null +++ b/scripts/run-podman-build-bullseye.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -xe +cd "`dirname $(readlink -f ${0})`" + +podman build -t chiaki-bullseye . -f Dockerfile.bullseye +cd .. +podman run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " + cd /build && + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py && + mkdir build_bullseye && + cmake -Bbuild_bullseye -GNinja -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && + ninja -C build_bullseye && + ninja -C build_bullseye test" + diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index 1e03a2a..37b825d 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -5,10 +5,7 @@ set -xveo pipefail arg1=$1 build="./build" if [ "$arg1" != "linux" ]; then - # source /opt/devkitpro/switchvars.sh - # toolchain="${DEVKITPRO}/switch.cmake" toolchain="cmake/switch.cmake" - export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" build="./build_switch" fi @@ -19,6 +16,9 @@ build_chiaki (){ pushd "${BASEDIR}" #rm -rf ./build + # purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + cmake -B "${build}" \ -GNinja \ -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/dev-container.sh similarity index 55% rename from scripts/switch/run-docker-build-chiaki.sh rename to scripts/switch/dev-container.sh index c81c788..4cfedb3 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/dev-container.sh @@ -2,10 +2,9 @@ cd "`dirname $(readlink -f ${0})`/../.." -docker run \ +podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ - -t \ - thestr4ng3r/chiaki-build-switch \ - -c "scripts/switch/build.sh" - + -it \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ + /bin/bash diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-docker-build-chiaki.sh deleted file mode 100755 index ff66e40..0000000 --- a/scripts/switch/push-docker-build-chiaki.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -cd "`dirname $(readlink -f ${0})`/../.." - -docker run \ - -v "`pwd`:/build/chiaki" \ - -p 28771:28771 -ti \ - chiaki-switch \ - -c "/opt/devkitpro/tools/bin/nxlink -a $@ -s /build/chiaki/build_switch/switch/chiaki.nro" - diff --git a/scripts/switch/push-podman-build-chiaki.sh b/scripts/switch/push-podman-build-chiaki.sh new file mode 100755 index 0000000..ac82029 --- /dev/null +++ b/scripts/switch/push-podman-build-chiaki.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +cd "`dirname $(readlink -f ${0})`/../.." + +docker run \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -ti -p 28771:28771 \ + --entrypoint /opt/devkitpro/tools/bin/nxlink \ + thestr4ng3r/chiaki-build-switch \ + "$@" -s /build/chiaki/build_switch/switch/chiaki.nro + diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh new file mode 100755 index 0000000..513cdc8 --- /dev/null +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "`dirname $(readlink -f ${0})`/../.." + +podman run --rm \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -it \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ + ${1:-/bin/bash -c "scripts/switch/build.sh"} diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 90c1465..2dedb0c 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.2) project(libsetsu) -option(SETSU_BUILD_DEMO "Build testing executable for libsetsu" OFF) +option(SETSU_BUILD_DEMOS "Build testing executables for libsetsu" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -17,9 +17,10 @@ find_package(Udev REQUIRED) find_package(Evdev REQUIRED) target_link_libraries(setsu Udev::libudev Evdev::libevdev) -if(SETSU_BUILD_DEMO) - add_executable(setsu-demo - demo/main.c) - target_link_libraries(setsu-demo setsu) +if(SETSU_BUILD_DEMOS) + add_executable(setsu-demo-touchpad demo/touchpad.c) + target_link_libraries(setsu-demo-touchpad setsu) + add_executable(setsu-demo-motion demo/motion.c) + target_link_libraries(setsu-demo-motion setsu) endif() diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake index 5ea7dea..23f2515 100644 --- a/setsu/cmake/FindEvdev.cmake +++ b/setsu/cmake/FindEvdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libevdev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libevdev) + else() + pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) diff --git a/setsu/cmake/FindUdev.cmake b/setsu/cmake/FindUdev.cmake index fc1c81f..c9c8450 100644 --- a/setsu/cmake/FindUdev.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libudev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libudev) + else() + pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c new file mode 100644 index 0000000..ebb5fdd --- /dev/null +++ b/setsu/demo/motion.c @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include + +#include +#include +#include +#include +#include +#include +#include + +Setsu *setsu; + +#define NAME_LEN 8 +const char * const names[] = { + "accel x ", + "accel y ", + "accel z ", + " gyro x ", + " gyro y ", + " gyro z " +}; +union +{ + struct + { + float accel_x, accel_y, accel_z; + float gyro_x, gyro_y, gyro_z; + }; + float v[6]; +} vals; +uint32_t timestamp; +bool dirty = false; +bool log_mode; +volatile bool should_quit; + +#define LOG(...) do { if(log_mode) fprintf(stderr, __VA_ARGS__); } while(0) + +void sigint(int s) +{ + should_quit = true; +} + +#define BAR_LENGTH 100 +#define BAR_MAX 2.0f +#define BAR_MAX_GYRO M_PI + +void print_state() +{ + char buf[6 * (1 + NAME_LEN + BAR_LENGTH) + 1]; + size_t i = 0; + for(size_t b=0; b<6; b++) + { + buf[i++] = '\n'; + memcpy(buf + i, names[b], NAME_LEN); + i += NAME_LEN; + buf[i++] = '['; + size_t max = BAR_LENGTH-2; + for(size_t bi=0; bi 2 ? BAR_MAX_GYRO : BAR_MAX) * (2.0f * (float)(x) / (float)max - 1.0f)) + float cur = BAR_VAL(bi); + float prev = BAR_VAL((int)bi - 1); + if(prev < 0.0f && cur >= 0.0f) + { + buf[i++] = '|'; + continue; + } + bool cov = ((vals.v[b] < 0.0f) == (cur < 0.0f)) && fabsf(vals.v[b]) > fabsf(cur); + float next = BAR_VAL(bi + 1); +#define MARK_VAL (b > 2 ? 0.5f * M_PI : 1.0f) + bool mark = cur < -MARK_VAL && next >= -MARK_VAL || prev < MARK_VAL && cur >= MARK_VAL; + buf[i++] = cov ? (mark ? '#' : '=') : (mark ? '.' : ' '); +#undef BAR_VAL + } + buf[i++] = ']'; + } + buf[i++] = '\0'; + assert(i == sizeof(buf)); + printf("\033[2J%s", buf); + fflush(stdout); +} + +void event(SetsuEvent *event, void *user) +{ + dirty = true; + switch(event->type) + { + case SETSU_EVENT_DEVICE_ADDED: { + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_MOTION); + LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); + break; + } + case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + LOG("Device removed: %s\n", event->path); + break; + case SETSU_EVENT_MOTION: + LOG("Motion: %f, %f, %f / %f, %f, %f / %u\n", + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + (unsigned int)event->motion.timestamp); + vals.accel_x = event->motion.accel_x; + vals.accel_y = event->motion.accel_y; + vals.accel_z = event->motion.accel_z; + vals.gyro_x = event->motion.gyro_x; + vals.gyro_y = event->motion.gyro_y; + vals.gyro_z = event->motion.gyro_z; + timestamp = event->motion.timestamp; + dirty = true; + default: + break; + } +} + +void usage(const char *prog) +{ + printf("usage: %s [-l]\n -l log mode\n", prog); + exit(1); +} + +int main(int argc, const char *argv[]) +{ + log_mode = false; + if(argc == 2) + { + if(!strcmp(argv[1], "-l")) + log_mode = true; + else + usage(argv[0]); + } + else if(argc != 1) + usage(argv[0]); + + setsu = setsu_new(); + if(!setsu) + { + printf("Failed to init setsu\n"); + return 1; + } + + struct sigaction sa = {0}; + sa.sa_handler = sigint; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + + dirty = true; + while(!should_quit) + { + if(dirty && !log_mode) + print_state(); + dirty = false; + setsu_poll(setsu, event, NULL); + } + setsu_free(setsu); + printf("\nさよなら!\n"); + return 0; +} + diff --git a/setsu/demo/main.c b/setsu/demo/touchpad.c similarity index 83% rename from setsu/demo/main.c rename to setsu/demo/touchpad.c index 4219638..b51d628 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/touchpad.c @@ -68,21 +68,25 @@ void event(SetsuEvent *event, void *user) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: { - SetsuDevice *dev = setsu_connect(setsu, event->path); + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_TOUCHPAD); LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); break; } case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_TOUCH_DOWN: - LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); for(size_t i=0; itracking_id; + touches[i].tracking_id = event->touch.tracking_id; break; } } @@ -90,19 +94,19 @@ void event(SetsuEvent *event, void *user) case SETSU_EVENT_TOUCH_POSITION: case SETSU_EVENT_TOUCH_UP: if(event->type == SETSU_EVENT_TOUCH_UP) - LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); else LOG("Position for %s, tracking id %d: %u, %u\n", setsu_device_get_path(event->dev), - event->tracking_id, (unsigned int)event->x, (unsigned int)event->y); + event->touch.tracking_id, (unsigned int)event->touch.x, (unsigned int)event->touch.y); for(size_t i=0; itracking_id) + if(touches[i].down && touches[i].tracking_id == event->touch.tracking_id) { switch(event->type) { case SETSU_EVENT_TOUCH_POSITION: - touches[i].x = event->x; - touches[i].y = event->y; + touches[i].x = event->touch.x; + touches[i].y = event->touch.y; break; case SETSU_EVENT_TOUCH_UP: touches[i].down = false; @@ -118,6 +122,8 @@ void event(SetsuEvent *event, void *user) LOG("Button for %s: %llu %s\n", setsu_device_get_path(event->dev), (unsigned long long)event->button, event->type == SETSU_EVENT_BUTTON_DOWN ? "down" : "up"); break; + default: + break; } } diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 5e546bc..a3ed0e3 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -13,13 +13,18 @@ typedef struct setsu_t Setsu; typedef struct setsu_device_t SetsuDevice; typedef int SetsuTrackingId; +typedef enum { + SETSU_DEVICE_TYPE_TOUCHPAD, + SETSU_DEVICE_TYPE_MOTION +} SetsuDeviceType; + typedef enum { /* New device available to connect. - * Event will have path set to the new device. */ + * Event will have path and type set to the new device. */ SETSU_EVENT_DEVICE_ADDED, /* Previously available device removed. - * Event will have path set to the new device. + * Event will have path and type set to the removed device. * Any SetsuDevice connected to this path will automatically * be disconnected and their pointers will be invalid immediately * after the callback for this event returns. */ @@ -41,7 +46,10 @@ typedef enum { SETSU_EVENT_BUTTON_DOWN, /* Event will have dev and button set. */ - SETSU_EVENT_BUTTON_UP + SETSU_EVENT_BUTTON_UP, + + /* Event will have motion set. */ + SETSU_EVENT_MOTION } SetsuEventType; #define SETSU_BUTTON_0 (1u << 0) @@ -53,7 +61,11 @@ typedef struct setsu_event_t SetsuEventType type; union { - const char *path; + struct + { + const char *path; + SetsuDeviceType dev_type; + }; struct { SetsuDevice *dev; @@ -63,8 +75,14 @@ typedef struct setsu_event_t { SetsuTrackingId tracking_id; uint32_t x, y; - }; + } touch; SetsuButton button; + struct + { + float accel_x, accel_y, accel_z; // unit is 1G + float gyro_x, gyro_y, gyro_z; // unit is rad/sec + uint32_t timestamp; // microseconds + } motion; }; }; }; @@ -75,11 +93,12 @@ typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); -SetsuDevice *setsu_connect(Setsu *setsu, const char *path); +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type); void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); const char *setsu_device_get_path(SetsuDevice *dev); -uint32_t setsu_device_get_width(SetsuDevice *dev); -uint32_t setsu_device_get_height(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev); + #ifdef __cplusplus } diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index f899d07..c31fe83 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -22,9 +23,12 @@ #define SETSU_LOG(...) do {} while(0) #endif +#define DEG2RAD (2.0f * M_PI / 360.0f) + typedef struct setsu_avail_device_t { struct setsu_avail_device_t *next; + SetsuDeviceType type; char *path; bool connect_dirty; // whether the connect has not been sent as an event yet bool disconnect_dirty; // whether the disconnect has not been sent as an event yet @@ -36,27 +40,44 @@ typedef struct setsu_device_t { struct setsu_device_t *next; char *path; + SetsuDeviceType type; int fd; struct libevdev *evdev; - int min_x, min_y, max_x, max_y; - struct { - /* Saves the old tracking id that was just up-ed. - * also for handling "atomic" up->down - * i.e. when there is an up, then down with a different tracking id - * in a single frame (before SYN_REPORT), this saves the old - * tracking id that must be reported as up. */ - int tracking_id_prev; + union + { + struct + { + int min_x, min_y, max_x, max_y; - int tracking_id; - int x, y; - bool downed; - bool pos_dirty; - } slots[SLOTS_COUNT]; - unsigned int slot_cur; + struct + { + /* Saves the old tracking id that was just up-ed. + * also for handling "atomic" up->down + * i.e. when there is an up, then down with a different tracking id + * in a single frame (before SYN_REPORT), this saves the old + * tracking id that must be reported as up. */ + int tracking_id_prev; - uint64_t buttons_prev; - uint64_t buttons_cur; + int tracking_id; + int x, y; + bool downed; + bool pos_dirty; + } slots[SLOTS_COUNT]; + unsigned int slot_cur; + uint64_t buttons_prev; + uint64_t buttons_cur; + } touchpad; + struct + { + int accel_res_x, accel_res_y, accel_res_z; + int gyro_res_x, gyro_res_y, gyro_res_z; + int accel_x, accel_y, accel_z; + int gyro_x, gyro_y, gyro_z; + uint32_t timestamp; + bool dirty; + } motion; + }; } SetsuDevice; struct setsu_t @@ -131,8 +152,8 @@ static void scan_udev(Setsu *setsu) if(udev_enumerate_add_match_subsystem(udev_enum, "input") < 0) goto beach; - if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) - goto beach; + //if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) + // goto beach; if(udev_enumerate_scan_devices(udev_enum) < 0) goto beach; @@ -153,16 +174,32 @@ beach: udev_enumerate_unref(udev_enum); } -static bool is_device_interesting(struct udev_device *dev) +static bool is_device_interesting(struct udev_device *dev, SetsuDeviceType *type) { static const uint32_t device_ids[] = { // vendor id, model id - 0x054c, 0x05c4, // DualShock 4 Gen 1 USB - 0x054c, 0x09cc // DualShock 4 Gen 2 USB + 0x054c, 0x05c4, // DualShock 4 Gen 1 + 0x054c, 0x09cc, // DualShock 4 Gen 2 + 0x54c, 0x0ce6 // DualSense }; - // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: - if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + const char *touchpad_str = udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD"); + const char *accel_str = udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER"); + if(touchpad_str && !strcmp(touchpad_str, "1")) + { + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + return false; + *type = SETSU_DEVICE_TYPE_TOUCHPAD; + } + else if(accel_str && !strcmp(accel_str, "1")) + { + // Filter /dev/input/js* away and keep /dev/input/event* + if(!udev_device_get_property_value(dev, "ID_INPUT_WIDTH_MM")) + return false; + *type = SETSU_DEVICE_TYPE_MOTION; + } + else return false; uint32_t vendor; @@ -220,12 +257,14 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev) } // not yet added - if(!is_device_interesting(dev)) + SetsuDeviceType type; + if(!is_device_interesting(dev, &type)) return; SetsuAvailDevice *adev = calloc(1, sizeof(SetsuAvailDevice)); if(!adev) return; + adev->type = type; adev->path = strdup(path); if(!adev->path) { @@ -271,7 +310,7 @@ bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id) return true; } -SetsuDevice *setsu_connect(Setsu *setsu, const char *path) +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); if(!dev) @@ -280,10 +319,15 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) dev->path = strdup(path); if(!dev->path) goto error; + dev->type = type; dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); if(dev->fd == -1) + { + SETSU_LOG("Failed to open %s\n", dev->path); + perror("setsu_connect"); goto error; + } if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) { @@ -291,15 +335,29 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) goto error; } - dev->min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); - dev->min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); - dev->max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); - dev->max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); - - for(size_t i=0; islots[i].tracking_id_prev = -1; - dev->slots[i].tracking_id = -1; + case SETSU_DEVICE_TYPE_TOUCHPAD: + dev->touchpad.min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); + dev->touchpad.min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); + dev->touchpad.max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); + dev->touchpad.max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); + + for(size_t i=0; itouchpad.slots[i].tracking_id_prev = -1; + dev->touchpad.slots[i].tracking_id = -1; + } + break; + case SETSU_DEVICE_TYPE_MOTION: + dev->motion.accel_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_X); + dev->motion.accel_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_Y); + dev->motion.accel_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_Z); + dev->motion.gyro_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_RX); + dev->motion.gyro_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_RY); + dev->motion.gyro_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_RZ); + dev->motion.accel_y = dev->motion.accel_res_y; // 1G down + break; } dev->next = setsu->dev; @@ -341,14 +399,18 @@ const char *setsu_device_get_path(SetsuDevice *dev) return dev->path; } -uint32_t setsu_device_get_width(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev) { - return dev->max_x - dev->min_x; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_x - dev->touchpad.min_x; } -uint32_t setsu_device_get_height(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev) { - return dev->max_y - dev->min_y; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_y - dev->touchpad.min_y; } void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) @@ -393,6 +455,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_ADDED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); adev->connect_dirty = false; } @@ -401,6 +464,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_REMOVED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); // kill the device only after sending the event SetsuAvailDevice *next = adev->next; @@ -465,59 +529,106 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, libevdev_event_code_get_name(ev->type, ev->code), ev->value); #endif -#define S dev->slots[dev->slot_cur] - switch(ev->type) + if(ev->type == EV_SYN && ev->code == SYN_REPORT) { - case EV_ABS: - switch(ev->code) + device_drain(setsu, dev, cb, user); + return; + } + switch(dev->type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + switch(ev->type) { - case ABS_MT_SLOT: - if((unsigned int)ev->value >= SLOTS_COUNT) + case EV_ABS: +#define S dev->touchpad.slots[dev->touchpad.slot_cur] + switch(ev->code) { - SETSU_LOG("slot too high\n"); + case ABS_MT_SLOT: + if((unsigned int)ev->value >= SLOTS_COUNT) + { + SETSU_LOG("slot too high\n"); + break; + } + dev->touchpad.slot_cur = ev->value; + break; + case ABS_MT_TRACKING_ID: + if(S.tracking_id != -1 && S.tracking_id_prev == -1) + { + // up the tracking id + S.tracking_id_prev = S.tracking_id; + // reset the rest + S.x = S.y = 0; + S.pos_dirty = false; + } + S.tracking_id = ev->value; + if(ev->value != -1) + S.downed = true; + break; + case ABS_MT_POSITION_X: + S.x = ev->value; + S.pos_dirty = true; + break; + case ABS_MT_POSITION_Y: + S.y = ev->value; + S.pos_dirty = true; + break; + } + break; +#undef S + case EV_KEY: { + uint64_t button = button_from_evdev(ev->code); + if(!button) break; - } - dev->slot_cur = ev->value; + if(ev->value) + dev->touchpad.buttons_cur |= button; + else + dev->touchpad.buttons_cur &= ~button; break; - case ABS_MT_TRACKING_ID: - if(S.tracking_id != -1 && S.tracking_id_prev == -1) + } + } + break; + case SETSU_DEVICE_TYPE_MOTION: + switch(ev->type) + { + case EV_ABS: + switch(ev->code) { - // up the tracking id - S.tracking_id_prev = S.tracking_id; - // reset the rest - S.x = S.y = 0; - S.pos_dirty = false; + case ABS_X: + dev->motion.accel_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_Y: + dev->motion.accel_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_Z: + dev->motion.accel_z = ev->value; + dev->motion.dirty = true; + break; + case ABS_RX: + dev->motion.gyro_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_RY: + dev->motion.gyro_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_RZ: + dev->motion.gyro_z = ev->value; + dev->motion.dirty = true; + break; } - S.tracking_id = ev->value; - if(ev->value != -1) - S.downed = true; break; - case ABS_MT_POSITION_X: - S.x = ev->value; - S.pos_dirty = true; - break; - case ABS_MT_POSITION_Y: - S.y = ev->value; - S.pos_dirty = true; + case EV_MSC: + if(ev->code == MSC_TIMESTAMP) + { + dev->motion.timestamp = ev->value; + dev->motion.dirty = true; + } break; } break; - case EV_KEY: { - uint64_t button = button_from_evdev(ev->code); - if(!button) - break; - if(ev->value) - dev->buttons_cur |= button; - else - dev->buttons_cur &= ~button; - break; - } - case EV_SYN: - if(ev->code == SYN_REPORT) - device_drain(setsu, dev, cb, user); - break; } -#undef S } static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) @@ -525,48 +636,68 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * SetsuEvent event; #define BEGIN_EVENT(tp) do { memset(&event, 0, sizeof(event)); event.dev = dev; event.type = tp; } while(0) #define SEND_EVENT() do { cb(&event, user); } while (0) - for(size_t i=0; itype) { - if(dev->slots[i].tracking_id_prev != -1) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); - event.tracking_id = dev->slots[i].tracking_id_prev; - SEND_EVENT(); - dev->slots[i].tracking_id_prev = -1; - } - if(dev->slots[i].downed) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); - event.tracking_id = dev->slots[i].tracking_id; - SEND_EVENT(); - dev->slots[i].downed = false; - } - if(dev->slots[i].pos_dirty) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); - event.tracking_id = dev->slots[i].tracking_id; - event.x = (uint32_t)(dev->slots[i].x - dev->min_x); - event.y = (uint32_t)(dev->slots[i].y - dev->min_y); - SEND_EVENT(); - dev->slots[i].pos_dirty = false; - } - } + case SETSU_DEVICE_TYPE_TOUCHPAD: + for(size_t i=0; itouchpad.slots[i].tracking_id_prev != -1) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id_prev; + SEND_EVENT(); + dev->touchpad.slots[i].tracking_id_prev = -1; + } + if(dev->touchpad.slots[i].downed) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; + SEND_EVENT(); + dev->touchpad.slots[i].downed = false; + } + if(dev->touchpad.slots[i].pos_dirty) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; + event.touch.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); + event.touch.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); + SEND_EVENT(); + dev->touchpad.slots[i].pos_dirty = false; + } + } - uint64_t buttons_diff = dev->buttons_prev ^ dev->buttons_cur; - for(uint64_t i=0; i<64; i++) - { - if(buttons_diff & 1) - { - uint64_t button = 1 << i; - BEGIN_EVENT((dev->buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); - event.button = button; - SEND_EVENT(); - } - buttons_diff >>= 1; - if(!buttons_diff) + uint64_t buttons_diff = dev->touchpad.buttons_prev ^ dev->touchpad.buttons_cur; + for(uint64_t i=0; i<64; i++) + { + if(buttons_diff & 1) + { + uint64_t button = 1 << i; + BEGIN_EVENT((dev->touchpad.buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); + event.button = button; + SEND_EVENT(); + } + buttons_diff >>= 1; + if(!buttons_diff) + break; + } + dev->touchpad.buttons_prev = dev->touchpad.buttons_cur; + break; + case SETSU_DEVICE_TYPE_MOTION: + if(dev->motion.dirty) + { + BEGIN_EVENT(SETSU_EVENT_MOTION); + event.motion.accel_x = (float)dev->motion.accel_x / (float)dev->motion.accel_res_x; + event.motion.accel_y = (float)dev->motion.accel_y / (float)dev->motion.accel_res_y; + event.motion.accel_z = (float)dev->motion.accel_z / (float)dev->motion.accel_res_z; + event.motion.gyro_x = DEG2RAD * (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; + event.motion.gyro_y = DEG2RAD * (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; + event.motion.gyro_z = DEG2RAD * (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; + event.motion.timestamp = dev->motion.timestamp; + SEND_EVENT(); + dev->motion.dirty = false; + } break; } - dev->buttons_prev = dev->buttons_cur; #undef BEGIN_EVENT #undef SEND_EVENT } diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 89bd789..3d5639a 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -61,17 +61,16 @@ target_include_directories(borealis PUBLIC find_package(glfw3 REQUIRED) find_library(EGL EGL) -find_library(GLAPI glapi) -find_library(DRM_NOUVEAU drm_nouveau) -target_link_libraries(borealis +target_link_libraries(borealis PUBLIC glfw - ${EGL} - ${GLAPI} - ${DRM_NOUVEAU}) + ${EGL}) if(CHIAKI_IS_SWITCH) target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="romfs:/") + find_library(GLAPI glapi) + find_library(DRM_NOUVEAU drm_nouveau) + target_link_libraries(borealis PUBLIC ${GLAPI} ${DRM_NOUVEAU}) else() target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="./switch/res/") @@ -114,9 +113,7 @@ target_link_libraries(chiaki-borealis if(CHIAKI_IS_SWITCH) # libnx is forced by the switch toolchain find_library(Z z) - find_library(GLAPI glapi) # TODO: make it transitive from borealis - find_library(DRM_NOUVEAU drm_nouveau) # TODO: make it transitive from borealis - target_link_libraries(chiaki-borealis ${Z} ${GLAPI} ${DRM_NOUVEAU}) + target_link_libraries(chiaki-borealis ${Z} ${GLAPI}) endif() install(TARGETS chiaki-borealis @@ -126,9 +123,9 @@ install(TARGETS chiaki-borealis if(CHIAKI_IS_SWITCH) add_nro_target(chiaki chiaki-borealis - "chiaki" - "Chiaki team" + "Chiaki" + "H0neyBadger and thestr4ng3r" "${CHIAKI_VERSION}" - "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" - "${CMAKE_SOURCE_DIR}/switch/res") + "${CMAKE_CURRENT_SOURCE_DIR}/nro_icon.jpg" + "${CMAKE_CURRENT_SOURCE_DIR}/res") endif() diff --git a/switch/README.md b/switch/README.md index 709f900..2307fa4 100644 --- a/switch/README.md +++ b/switch/README.md @@ -4,26 +4,10 @@ this project requires the devkitpro toolchain. you can use your personal computer to install devkitpro but the easiest way is to use the following container. -Build container image ---------------------- -``` -bash scripts/switch/build-docker-image.sh -``` - -Run container -------------- -from the project's [root folder](../) -``` -docker run -it --rm \ - -v "$(pwd):/build" \ - -p 28771:28771 \ - chiaki-switch -``` - Build Project ------------- ``` -bash scripts/switch/run-docker-build-chiaki.sh +bash scripts/switch/run-podman-build-chiaki.sh ``` tools @@ -31,7 +15,7 @@ tools Push to homebrew Netloader ``` # where X.X.X.X is the IP of your switch -scripts/switch/push-docker-build-chiaki.sh 192.168.0.200 +bash scripts/switch/push-podman-build-chiaki.sh -a 192.168.0.200 ``` Troubleshoot @@ -52,7 +36,7 @@ this file contains sensitive data. (do not share this file) [PS*-***] # required: lan PlayStation IP address # IP from Settings > System > system information -host_ip = *.*.*.* +host_addr = *.*.*.* # required: sony oline id (login) psn_online_id = ps_online_id # required (PS4>7.0 Only): https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid diff --git a/switch/borealis b/switch/borealis index 205e97a..eae1371 160000 --- a/switch/borealis +++ b/switch/borealis @@ -1 +1 @@ -Subproject commit 205e97ab45922fa7f5c9fa6a85d5d686cd50b669 +Subproject commit eae1371831d6cebf11b8ebd4c611069bccc6fb9b diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index 76c504d..49c8dcc 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -24,7 +24,7 @@ class DiscoveryManager struct sockaddr * host_addr = nullptr; size_t host_addr_len = 0; uint32_t GetIPv4BroadcastAddr(); - bool service_enable; + bool service_enable = false; public: typedef enum hoststate diff --git a/switch/include/gui.h b/switch/include/gui.h index afd2bab..033cf67 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -50,10 +50,6 @@ class MainApplication IO *io; brls::TabFrame *rootFrame; std::map host_menuitems; - // add_host local settings - std::string remote_display_name = ""; - std::string remote_addr = ""; - ChiakiTarget remote_ps_version = CHIAKI_TARGET_PS5_1; bool BuildConfigurationMenu(brls::List *, Host *host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); @@ -73,12 +69,6 @@ class PSRemotePlay : public brls::View // to send gamepad inputs Host *host; brls::Label *label; - ChiakiControllerState state = {0}; - // FPS calculation - // double base_time; - // int frame_counter = 0; - // int fps = 0; - public: PSRemotePlay(Host *host); ~PSRemotePlay(); diff --git a/switch/include/host.h b/switch/include/host.h index ae6f05e..e28183e 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -54,7 +54,9 @@ class Host std::function chiaki_regist_event_type_finished_success = nullptr; std::function chiaki_event_connected_cb = nullptr; std::function chiaki_even_login_pin_request_cb = nullptr; + std::function chiaki_event_rumble_cb = nullptr; std::function chiaki_event_quit_cb = nullptr; + std::function *)> io_read_controller_cb = nullptr; // internal state bool discovered = false; @@ -73,6 +75,8 @@ class Host std::string server_nickname; ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + ChiakiControllerState controller_state = {0}; + std::map finger_id_touch_id; // mac address = 48 bits uint8_t server_mac[6] = {0}; @@ -90,13 +94,13 @@ class Host public: Host(std::string host_name); ~Host(); - int Register(std::string pin); + int Register(int pin); int Wakeup(); int InitSession(IO *); int FiniSession(); void StopSession(); void StartSession(); - void SendFeedbackState(ChiakiControllerState *); + void SendFeedbackState(); void RegistCB(ChiakiRegistEvent *); void ConnectionEventCB(ChiakiEvent *); bool GetVideoResolution(int *ret_width, int *ret_height); @@ -110,7 +114,9 @@ class Host void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success); void SetEventConnectedCallback(std::function chiaki_event_connected_cb); void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); + void SetEventRumbleCallback(std::function chiaki_event_rumble_cb); void SetEventQuitCallback(std::function chiaki_event_quit_cb); + void SetReadControllerCallback(std::function *)> io_read_controller_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 8427684..3bb6fd1 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -28,7 +28,15 @@ Omit khrplatform: False Reproducible: False */ +#ifdef __SWITCH__ +#include +#else +#include +#endif + #include +#include + extern "C" { #include @@ -59,12 +67,17 @@ class IO // default nintendo switch res int screen_width = 1280; int screen_height = 720; - AVCodec *codec; + const AVCodec *codec; AVCodecContext *codec_context; AVFrame *frame; SDL_AudioDeviceID sdl_audio_device_id = 0; SDL_Event sdl_event; SDL_Joystick *sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; +#ifdef __SWITCH__ + PadState pad; + HidSixAxisSensorHandle sixaxis_handles[4]; + HidVibrationDeviceHandle vibration_handles[2][2]; +#endif GLuint vao; GLuint vbo; GLuint tex[PLANES_COUNT]; @@ -85,8 +98,8 @@ class IO GLuint CreateAndCompileShader(GLenum type, const char *source); void SetOpenGlYUVPixels(AVFrame *frame); bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); - bool ReadGameTouchScreen(ChiakiControllerState *state); - + bool ReadGameTouchScreen(ChiakiControllerState *state, std::map *finger_id_touch_id); + bool ReadGameSixAxis(ChiakiControllerState *state); public: // singleton configuration IO(const IO&) = delete; @@ -100,10 +113,11 @@ class IO void AudioCB(int16_t *buf, size_t samples_count); bool InitVideo(int video_width, int video_height, int screen_width, int screen_height); bool FreeVideo(); - bool InitJoystick(); - bool FreeJoystick(); - bool ReadUserKeyboard(char *buffer, size_t buffer_size); - bool MainLoop(ChiakiControllerState *state); + bool InitController(); + bool FreeController(); + bool MainLoop(); + void UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id); + void SetRumble(uint8_t left, uint8_t right); }; #endif //CHIAKI_IO_H diff --git a/switch/include/settings.h b/switch/include/settings.h index 08ecbb2..6bcb445 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -49,8 +49,8 @@ class Settings // the goal is to read/write inernal flat configuration file const std::map re_map = { {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, - {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, - {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?")}, + {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?([^\"]*)\"?")}, + {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?([\\w_-]+)\"?")}, {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?")}, @@ -61,7 +61,6 @@ class Settings }; ConfigurationItem ParseLine(std::string * line, std::string * value); - size_t GetB64encodeSize(size_t); public: // singleton configuration diff --git a/switch/nro_icon.jpg b/switch/nro_icon.jpg new file mode 100644 index 0000000..0ed61ec Binary files /dev/null and b/switch/nro_icon.jpg differ diff --git a/switch/nro_icon.png b/switch/nro_icon.png new file mode 100644 index 0000000..65d745e Binary files /dev/null and b/switch/nro_icon.png differ diff --git a/switch/res/icon.jpg b/switch/res/icon.jpg deleted file mode 100644 index cb515da..0000000 Binary files a/switch/res/icon.jpg and /dev/null differ diff --git a/switch/res/icon.png b/switch/res/icon.png new file mode 100644 index 0000000..5b2ad9c Binary files /dev/null and b/switch/res/icon.png differ diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index 7f71578..7fa7122 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -16,9 +16,9 @@ #define HOSTS_MAX 16 #define DROP_PINGS 3 -static void Discovery(ChiakiDiscoveryHost * discovered_hosts, size_t hosts_count, void * user) +static void Discovery(ChiakiDiscoveryHost *discovered_hosts, size_t hosts_count, void *user) { - DiscoveryManager * dm = (DiscoveryManager *)user; + DiscoveryManager *dm = (DiscoveryManager *)user; for(size_t i = 0; i < hosts_count; i++) { dm->DiscoveryCB(discovered_hosts + i); @@ -35,19 +35,13 @@ DiscoveryManager::~DiscoveryManager() { // join discovery thread if(this->service_enable) - { SetService(false); - } - - chiaki_discovery_fini(&this->discovery); } void DiscoveryManager::SetService(bool enable) { if(this->service_enable == enable) - { return; - } this->service_enable = enable; @@ -105,7 +99,7 @@ uint32_t DiscoveryManager::GetIPv4BroadcastAddr() #endif } -int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) +int DiscoveryManager::Send(struct sockaddr *host_addr, size_t host_addr_len) { if(!host_addr) { @@ -120,9 +114,9 @@ int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) return 0; } -int DiscoveryManager::Send(const char * discover_ip_dest) +int DiscoveryManager::Send(const char *discover_ip_dest) { - struct addrinfo * host_addrinfos; + struct addrinfo *host_addrinfos; int r = getaddrinfo(discover_ip_dest, NULL, NULL, &host_addrinfos); if(r != 0) { @@ -130,10 +124,10 @@ int DiscoveryManager::Send(const char * discover_ip_dest) return 1; } - struct sockaddr * host_addr = nullptr; + struct sockaddr *host_addr = nullptr; socklen_t host_addr_len = 0; - for(struct addrinfo * ai = host_addrinfos; ai; ai = ai->ai_next) + for(struct addrinfo *ai = host_addrinfos; ai; ai = ai->ai_next) { if(ai->ai_protocol != IPPROTO_UDP) continue; @@ -170,13 +164,13 @@ int DiscoveryManager::Send() return DiscoveryManager::Send(this->host_addr, this->host_addr_len); } -void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) +void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost *discovered_host) { // the user ptr is passed as // chiaki_discovery_thread_start arg std::string key = discovered_host->host_name; - Host * host = this->settings->GetOrCreateHost(&key); + Host *host = this->settings->GetOrCreateHost(&key); CHIAKI_LOGI(this->log, "--"); CHIAKI_LOGI(this->log, "Discovered Host:"); diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e49852b..c23f470 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -42,6 +42,9 @@ HostInterface::HostInterface(Host *host) // when the host is connected this->host->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); + // allow host to update controller state + this->host->SetEventRumbleCallback(std::bind(&IO::SetRumble, this->io, std::placeholders::_1, std::placeholders::_2)); + this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1, std::placeholders::_2)); } HostInterface::~HostInterface() @@ -97,42 +100,31 @@ void HostInterface::Register(Host *host, std::function success_cb) host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); // the host is not registered yet - brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); - brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { - bool pin_provided = false; - char pin_input[9] = {0}; - std::string error_message; - - // use callback to ensure that the message is showed on screen - // before the the ReadUserKeyboard - peprpc->close(); - - pin_provided = io->ReadUserKeyboard(pin_input, sizeof(pin_input)); - if(pin_provided) + // use callback to ensure that the message is showed on screen + // before the Swkbd + auto pin_input_cb = [host](int pin) { + // prevent users form messing with the gui + brls::Application::blockInputs(); + int ret = host->Register(pin); + if(ret != HOST_REGISTER_OK) { - // prevent users form messing with the gui - brls::Application::blockInputs(); - int ret = host->Register(pin_input); - if(ret != HOST_REGISTER_OK) + switch(ret) { - switch(ret) - { - // account not configured - case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: - brls::Application::notify("No PSN Account ID provided"); - brls::Application::unblockInputs(); - break; - case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: - brls::Application::notify("No PSN Online ID provided"); - brls::Application::unblockInputs(); - break; - } + // account not configured + case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: + brls::Application::notify("No PSN Account ID provided"); + brls::Application::unblockInputs(); + break; + case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: + brls::Application::notify("No PSN Online ID provided"); + brls::Application::unblockInputs(); + break; } } }; - peprpc->addButton("Ok", cb_peprpc); - peprpc->setCancelable(false); - peprpc->open(); + // the pin is 8 digit + bool success = brls::Swkbd::openForNumber(pin_input_cb, + "Please enter your PlayStation registration PIN code", "8 digits without spaces", 8, "", "", ""); } void HostInterface::Register() @@ -256,7 +248,7 @@ MainApplication::MainApplication(DiscoveryManager *discoverymanager) MainApplication::~MainApplication() { this->discoverymanager->SetService(false); - //this->io->FreeJoystick(); + this->io->FreeController(); this->io->FreeVideo(); } @@ -275,22 +267,21 @@ bool MainApplication::Load() // init chiaki gl after borealis // let borealis manage the main screen/window - if(!io->InitVideo(0, 0, SCREEN_W, SCREEN_H)) { brls::Logger::error("Failed to initiate Video"); } - brls::Logger::info("Load sdl joysticks"); - if(!io->InitJoystick()) + brls::Logger::info("Load sdl/hid controller"); + if(!io->InitController()) { - brls::Logger::error("Faled to initiate Joysticks"); + brls::Logger::error("Faled to initiate Controller"); } // Create a view this->rootFrame = new brls::TabFrame(); this->rootFrame->setTitle("Chiaki: Open Source PlayStation Remote Play Client"); - this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); + this->rootFrame->setIcon(BOREALIS_ASSET("icon.png")); brls::List *config = new brls::List(); brls::List *add_host = new brls::List(); @@ -327,39 +318,36 @@ bool MainApplication::Load() bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) { std::string psn_account_id_string = this->settings->GetPSNAccountID(host); - brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); - psn_account_id->setValue(psn_account_id_string.c_str()); + brls::InputListItem *psn_account_id = new brls::InputListItem("PSN Account ID", psn_account_id_string, + "Account ID in base64 format", "PS5 or PS4 v7.0 and greater", CHIAKI_PSN_ACCOUNT_ID_SIZE * 2, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; - bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); - if(input) - { - // update gui - psn_account_id->setValue(account_id); - // push in setting - this->settings->SetPSNAccountID(host, account_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNAccountID(host, psn_account_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); ls->addView(psn_account_id); std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); - brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); - psn_online_id->setValue(psn_online_id_string.c_str()); + brls::InputListItem *psn_online_id = new brls::InputListItem("PSN Online ID", + psn_online_id_string, "", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { - char online_id[256] = {0}; - bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); - if(input) - { - // update gui - psn_online_id->setValue(online_id); - // push in setting - this->settings->SetPSNOnlineID(host, online_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNOnlineID(host, psn_online_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_online_id->getClickEvent()->subscribe(psn_online_id_cb); ls->addView(psn_online_id); @@ -380,7 +368,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *resolution = new brls::SelectListItem( - "Resolution", {"720p", "540p", "360p"}, value); + "Resolution", { "720p", "540p", "360p" }, value); auto resolution_cb = [this, host](int result) { ChiakiVideoResolutionPreset value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -414,7 +402,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *fps = new brls::SelectListItem( - "FPS", {"60", "30"}, value); + "FPS", { "60", "30" }, value); auto fps_cb = [this, host](int result) { ChiakiVideoFPSPreset value = CHIAKI_VIDEO_FPS_PRESET_60; @@ -465,34 +453,24 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::Label* add_host_label = new brls::Label(brls::LabelStyle::REGULAR, // "Add Host configuration", true); - brls::ListItem *display_name = new brls::ListItem("Display name"); - auto display_name_cb = [this, display_name](brls::View *view) { - char name[16] = {0}; - bool input = this->io->ReadUserKeyboard(name, sizeof(name)); - if(input) - { - // update gui - display_name->setValue(name); - // set internal value - this->remote_display_name = name; - } - }; - display_name->getClickEvent()->subscribe(display_name_cb); + brls::InputListItem *display_name = new brls::InputListItem("Display name", + "default", "configuration name", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(display_name); - brls::ListItem *address = new brls::ListItem("Remote IP/name"); - auto address_cb = [this, address](brls::View *view) { - char addr[256] = {0}; - bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); - if(input) - { - // update gui - address->setValue(addr); - // set internal value - this->remote_addr = addr; - } - }; - address->getClickEvent()->subscribe(address_cb); + brls::InputListItem *address = new brls::InputListItem("Remote IP/name", + "", "IP address or fqdn", "", 255, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(address); // TODO @@ -500,47 +478,49 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", - {"PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); - auto ps_version_cb = [this, ps_version](int result) { - switch(result) - { - case 0: - // ps5 v1 - this->remote_ps_version = CHIAKI_TARGET_PS5_1; - break; - case 1: - // ps4 v8 - this->remote_ps_version = CHIAKI_TARGET_PS4_10; - break; - case 2: - // ps4 v7 - this->remote_ps_version = CHIAKI_TARGET_PS4_9; - break; - case 3: - // ps4 v6 - this->remote_ps_version = CHIAKI_TARGET_PS4_8; - break; - } - }; - ps_version->getValueSelectedEvent()->subscribe(ps_version_cb); + { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7" }); add_host->addView(ps_version); brls::ListItem *register_host = new brls::ListItem("Register"); - auto register_host_cb = [this](brls::View *view) { + auto register_host_cb = [this, display_name, address, ps_version](brls::View *view) { bool err = false; - if(this->remote_display_name.length() <= 0) + std::string dn = display_name->getValue(); + std::string addr = address->getValue(); + ChiakiTarget version = CHIAKI_TARGET_PS4_UNKNOWN; + + switch(ps_version->getSelectedValue()) + { + case 0: + // ps5 v1 + version = CHIAKI_TARGET_PS5_1; + break; + case 1: + // ps4 v8 + version = CHIAKI_TARGET_PS4_10; + break; + case 2: + // ps4 v7 + version = CHIAKI_TARGET_PS4_9; + break; + case 3: + // ps4 v6 + version = CHIAKI_TARGET_PS4_8; + break; + } + + if(dn.length() <= 0) { brls::Application::notify("No Display name defined"); err = true; } - if(this->remote_addr.length() <= 0) + if(addr.length() <= 0) { brls::Application::notify("No Remote address provided"); err = true; } - if(this->remote_ps_version < 0) + if(version <= CHIAKI_TARGET_PS4_UNKNOWN) { brls::Application::notify("No PlayStation Version provided"); err = true; @@ -549,10 +529,9 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) if(err) return; - Host *host = this->settings->GetOrCreateHost(&this->remote_display_name); - host->SetHostAddr(this->remote_addr); - host->SetChiakiTarget(this->remote_ps_version); - + Host *host = this->settings->GetOrCreateHost(&dn); + host->SetHostAddr(addr); + host->SetChiakiTarget(version); HostInterface::Register(host); }; register_host->getClickEvent()->subscribe(register_host_cb); @@ -564,38 +543,12 @@ PSRemotePlay::PSRemotePlay(Host *host) : host(host) { this->io = IO::GetInstance(); - - // store joycon/touchpad keys - for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) - // start touchpad as "untouched" - this->state.touches[x].id = -1; - - // this->base_time=glfwGetTime(); } void PSRemotePlay::draw(NVGcontext *vg, int x, int y, unsigned width, unsigned height, brls::Style *style, brls::FrameContext *ctx) { - this->io->MainLoop(&this->state); - this->host->SendFeedbackState(&this->state); - - // FPS calculation - // this->frame_counter += 1; - // double frame_time = glfwGetTime(); - // if((frame_time - base_time) >= 1.0) - // { - // base_time += 1; - // //printf("FPS: %d\n", this->frame_counter); - // this->fps = this->frame_counter; - // this->frame_counter = 0; - // } - // nvgBeginPath(vg); - // nvgFillColor(vg, nvgRGBA(255,192,0,255)); - // nvgFontFaceId(vg, ctx->fontStash->regular); - // nvgFontSize(vg, style->Label.smallFontSize); - // nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); - // char fps_str[9] = {0}; - // sprintf(fps_str, "FPS: %000d", this->fps); - // nvgText(vg, 5,10, fps_str, NULL); + this->io->MainLoop(); + this->host->SendFeedbackState(); } PSRemotePlay::~PSRemotePlay() diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 6db9c50..39ac1b3 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -72,7 +72,7 @@ int Host::Wakeup() return ret; } -int Host::Register(std::string pin) +int Host::Register(int pin) { // use pin and accont_id to negociate secrets for session // @@ -103,7 +103,7 @@ int Host::Register(std::string pin) if(online_id.length() > 0) { regist_info.psn_online_id = this->psn_online_id.c_str(); - // regist_info.psn_account_id = {0}; + // regist_info.psn_account_id = '\0'; } else { @@ -117,16 +117,16 @@ int Host::Register(std::string pin) throw Exception("Undefined PS4 system version (please run discover first)"); } - this->regist_info.pin = atoi(pin.c_str()); + this->regist_info.pin = pin; this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; if(this->target >= CHIAKI_TARGET_PS4_9) - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin); else - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin); chiaki_regist_start(&this->regist, this->log, &this->regist_info, RegistEventCB, this); return HOST_REGISTER_OK; @@ -157,9 +157,13 @@ int Host::InitSession(IO *user) // audio setting_cb and frame_cb chiaki_opus_decoder_set_cb(&this->opus_decoder, InitAudioCB, AudioCB, user); chiaki_opus_decoder_get_sink(&this->opus_decoder, &audio_sink); - chiaki_session_set_audio_sink(&(this->session), &audio_sink); - chiaki_session_set_video_sample_cb(&(this->session), VideoCB, user); - chiaki_session_set_event_cb(&(this->session), EventCB, this); + chiaki_session_set_audio_sink(&this->session, &audio_sink); + chiaki_session_set_video_sample_cb(&this->session, VideoCB, user); + chiaki_session_set_event_cb(&this->session, EventCB, this); + + // init controller states + chiaki_controller_state_set_idle(&this->controller_state); + return 0; } @@ -190,10 +194,13 @@ void Host::StartSession() } } -void Host::SendFeedbackState(ChiakiControllerState *state) +void Host::SendFeedbackState() { // send controller/joystick key - chiaki_session_set_controller_state(&this->session, state); + if(this->io_read_controller_cb != nullptr) + this->io_read_controller_cb(&this->controller_state, &finger_id_touch_id); + + chiaki_session_set_controller_state(&this->session, &this->controller_state); } void Host::ConnectionEventCB(ChiakiEvent *event) @@ -210,6 +217,11 @@ void Host::ConnectionEventCB(ChiakiEvent *event) if(this->chiaki_even_login_pin_request_cb != nullptr) this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: + CHIAKI_LOGD(this->log, "EventCB CHIAKI_EVENT_RUMBLE"); + if(this->chiaki_event_rumble_cb != nullptr) + this->chiaki_event_rumble_cb(event->rumble.left, event->rumble.right); + break; case CHIAKI_EVENT_QUIT: CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); if(this->chiaki_event_quit_cb != nullptr) @@ -353,6 +365,16 @@ void Host::SetEventQuitCallback(std::function chiaki_ev this->chiaki_event_quit_cb = chiaki_event_quit_cb; } +void Host::SetEventRumbleCallback(std::function chiaki_event_rumble_cb) +{ + this->chiaki_event_rumble_cb = chiaki_event_rumble_cb; +} + +void Host::SetReadControllerCallback(std::function *)> io_read_controller_cb) +{ + this->io_read_controller_cb = io_read_controller_cb; +} + bool Host::IsRegistered() { return this->registered; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index d7c28f7..409bba0 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -1,11 +1,5 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL -#ifdef __SWITCH__ -#include -#else -#include -#endif - #include "io.h" #include "settings.h" @@ -291,6 +285,16 @@ void IO::AudioCB(int16_t *buf, size_t samples_count) else buf[x] = (int16_t)sample; } + + int audio_queued_size = SDL_GetQueuedAudioSize(this->sdl_audio_device_id); + if(audio_queued_size > 16000) + { + // clear audio queue to avoid big audio delay + // average values are close to 13000 bytes + CHIAKI_LOGW(this->log, "Triggering SDL_ClearQueuedAudio with queue size = %d", audio_queued_size); + SDL_ClearQueuedAudio(this->sdl_audio_device_id); + } + int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t) * samples_count * 2); if(success != 0) CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); @@ -335,91 +339,53 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) -{ -#ifndef __SWITCH__ - // use cin to get user input from linux - std::cin.getline(buffer, buffer_size); - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); -#else - // https://kvadevack.se/post/nintendo-switch-virtual-keyboard/ - SwkbdConfig kbd; - Result rc = swkbdCreate(&kbd, 0); - - if(R_SUCCEEDED(rc)) - { - swkbdConfigMakePresetDefault(&kbd); - rc = swkbdShow(&kbd, buffer, buffer_size); - - if(R_SUCCEEDED(rc)) - { - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); - } - else - { - CHIAKI_LOGE(this->log, "swkbdShow() error: %u\n", rc); - return false; - } - swkbdClose(&kbd); - } - else - { - CHIAKI_LOGE(this->log, "swkbdCreate() error: %u\n", rc); - return false; - } -#endif - return true; -} - -bool IO::ReadGameTouchScreen(ChiakiControllerState *state) +bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state, std::map *finger_id_touch_id) { #ifdef __SWITCH__ - hidScanInput(); - int touch_count = hidTouchCount(); + HidTouchScreenState sw_state = {0}; + bool ret = false; - if(!touch_count) + hidGetTouchScreenStates(&sw_state, 1); + // scale switch screen to the PS trackpad + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + + // un-touch all old touches + for(auto it = finger_id_touch_id->begin(); it != finger_id_touch_id->end();) { - for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + auto cur = it; + it++; + for(int i = 0; i < sw_state.count; i++) { - if(state->touches[i].id != -1) - { - state->touches[i].x = 0; - state->touches[i].y = 0; - state->touches[i].id = -1; - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - // the state changed - ret = true; - } + if(sw_state.touches[i].finger_id == cur->first) + goto cont; } - return ret; + if(cur->second >= 0) + chiaki_controller_state_stop_touch(chiaki_state, (uint8_t)cur->second); + finger_id_touch_id->erase(cur); +cont: + continue; } - touchPosition touch; - for(int i = 0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + + // touch or update all current touches + for(int i = 0; i < sw_state.count; i++) { - hidTouchRead(&touch, i); + uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); + // use nintendo switch border's 5% to trigger the touchpad button + if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // 1280×720 px (16:9) - // ps4 controller aspect ratio looks closer to 29:10 - uint16_t x = touch.px * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = touch.py * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); - - // use nintendo switch border's 5% to - if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) + auto it = finger_id_touch_id->find(sw_state.touches[i].finger_id); + if(it == finger_id_touch_id->end()) { - state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); + // new touch + (*finger_id_touch_id)[sw_state.touches[i].finger_id] = + chiaki_controller_state_start_touch(chiaki_state, x, y); } - else - { - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - } - - state->touches[i].x = x; - state->touches[i].y = y; - state->touches[i].id = i; - // printf("[point_id=%d] px=%03d, py=%03d, dx=%03d, dy=%03d, angle=%03d\n", - // i, touch.px, touch.py, touch.dx, touch.dy, touch.angle); + else if(it->second >= 0) + chiaki_controller_state_set_touch_pos(chiaki_state, (uint8_t)it->second, x, y); + // it->second < 0 ==> touch ignored because there were already too many multi-touches ret = true; } return ret; @@ -428,14 +394,143 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) #endif } +void IO::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef __SWITCH__ + Result rc = 0; + HidVibrationValue vibration_values[] = { + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }, + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }}; + + int target_device = padIsHandheld(&pad) ? 0 : 1; + if(left > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[0], left / 100, 5000); + vibration_values[0].amp_low = (float)left / (float)100; + vibration_values[0].amp_high = (float)left / (float)100; + vibration_values[0].freq_low *= (float)left / (float)100; + vibration_values[0].freq_high *= (float)left / (float)100; + } + + if(right > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[1], right / 100, 5000); + vibration_values[1].amp_low = (float)right / (float)100; + vibration_values[1].amp_high = (float)right / (float)100; + vibration_values[1].freq_low *= (float)left / (float)100; + vibration_values[1].freq_high *= (float)left / (float)100; + } + + // printf("left ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[0], + // vibration_values[0].amp_low, + // vibration_values[0].amp_high, + // vibration_values[0].freq_low, + // vibration_values[0].freq_high); + + // printf("right ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[1], + // vibration_values[1].amp_low, + // vibration_values[1].amp_high, + // vibration_values[1].freq_low, + // vibration_values[1].freq_high); + + rc = hidSendVibrationValues(this->vibration_handles[target_device], vibration_values, 2); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidSendVibrationValues() returned: 0x%x", rc); + +#endif +} + +bool IO::ReadGameSixAxis(ChiakiControllerState *state) +{ +#ifdef __SWITCH__ + // Read from the correct sixaxis handle depending on the current input style + HidSixAxisSensorState sixaxis = {0}; + uint64_t style_set = padGetStyleSet(&pad); + if(style_set & HidNpadStyleTag_NpadHandheld) + hidGetSixAxisSensorStates(this->sixaxis_handles[0], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadFullKey) + hidGetSixAxisSensorStates(this->sixaxis_handles[1], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadJoyDual) + { + // For JoyDual, read from either the Left or Right Joy-Con depending on which is/are connected + u64 attrib = padGetAttributes(&pad); + if(attrib & HidNpadAttribute_IsLeftConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[2], &sixaxis, 1); + else if(attrib & HidNpadAttribute_IsRightConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[3], &sixaxis, 1); + } + + state->gyro_x = sixaxis.angular_velocity.x * 2.0f * M_PI; + state->gyro_y = sixaxis.angular_velocity.z * 2.0f * M_PI; + state->gyro_z = -sixaxis.angular_velocity.y * 2.0f * M_PI; + state->accel_x = -sixaxis.acceleration.x; + state->accel_y = -sixaxis.acceleration.z; + state->accel_z = sixaxis.acceleration.y; + + // https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2015/01/matrix-to-quat.pdf + float (*dm)[3] = sixaxis.direction.direction; + float m[3][3] = { + { dm[0][0], dm[2][0], dm[1][0] }, + { dm[0][2], dm[2][2], dm[1][2] }, + { dm[0][1], dm[2][1], dm[1][1] } + }; + std::array q; + float t; + if(m[2][2] < 0) + { + if (m[0][0] > m[1][1]) + { + t = 1 + m[0][0] - m[1][1] - m[2][2]; + q = { t, m[0][1] + m[1][0], m[2][0] + m[0][2], m[1][2] - m[2][1] }; + } + else + { + t = 1 - m[0][0] + m[1][1] -m[2][2]; + q = { m[0][1] + m[1][0], t, m[1][2] + m[2][1], m[2][0] - m[0][2] }; + } + } + else + { + if(m[0][0] < -m[1][1]) + { + t = 1 - m[0][0] - m[1][1] + m[2][2]; + q = { m[2][0] + m[0][2], m[1][2] + m[2][1], t, m[0][1] - m[1][0] }; + } + else + { + t = 1 + m[0][0] + m[1][1] + m[2][2]; + q = { m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0], t }; + } + } + float fac = 0.5f / sqrt(t); + state->orient_x = q[0] * fac; + state->orient_y = q[1] * fac; + state->orient_z = -q[2] * fac; + state->orient_w = q[3] * fac; + return true; +#else + return false; +#endif +} + bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) { // return true if an event changed (gamepad input) // TODO // share vs PS button - // Gyro ? - // rumble ? bool ret = true; switch(event->type) { @@ -812,7 +907,7 @@ inline void IO::OpenGlDraw() D(glFinish()); } -bool IO::InitJoystick() +bool IO::InitController() { // https://github.com/switchbrew/switch-examples/blob/master/graphics/sdl2/sdl2-simple/source/main.cpp#L57 // open CONTROLLER_PLAYER_1 and CONTROLLER_PLAYER_2 @@ -826,24 +921,64 @@ bool IO::InitJoystick() CHIAKI_LOGE(this->log, "SDL_JoystickOpen: %s\n", SDL_GetError()); return false; } + // this->sdl_haptic_ptr[i] = SDL_HapticOpenFromJoystick(sdl_joystick_ptr[i]); + // SDL_HapticRumbleInit(this->sdl_haptic_ptr[i]); + // if(sdl_haptic_ptr[i] == nullptr) + // { + // CHIAKI_LOGE(this->log, "SDL_HapticRumbleInit: %s\n", SDL_GetError()); + // } } +#ifdef __SWITCH__ +Result rc = 0; + // Configure our supported input layout: a single player with standard controller styles + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + + // Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller) + padInitializeDefault(&this->pad); + // touchpad + hidInitializeTouchScreen(); + // It's necessary to initialize these separately as they all have different handle values + hidGetSixAxisSensorHandles(&this->sixaxis_handles[0], 1, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[1], 1, HidNpadIdType_No1, HidNpadStyleTag_NpadFullKey); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[2], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + hidStartSixAxisSensor(this->sixaxis_handles[0]); + hidStartSixAxisSensor(this->sixaxis_handles[1]); + hidStartSixAxisSensor(this->sixaxis_handles[2]); + hidStartSixAxisSensor(this->sixaxis_handles[3]); + + rc = hidInitializeVibrationDevices(this->vibration_handles[0], 2, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_Handheld returned: 0x%x", rc); + + rc = hidInitializeVibrationDevices(this->vibration_handles[1], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_No1 returned: 0x%x", rc); + +#endif return true; } -bool IO::FreeJoystick() +bool IO::FreeController() { for(int i = 0; i < SDL_JOYSTICK_COUNT; i++) { - if(SDL_JoystickGetAttached(sdl_joystick_ptr[i])) - SDL_JoystickClose(sdl_joystick_ptr[i]); + SDL_JoystickClose(this->sdl_joystick_ptr[i]); + // SDL_HapticClose(this->sdl_haptic_ptr[i]); } +#ifdef __SWITCH__ + hidStopSixAxisSensor(this->sixaxis_handles[0]); + hidStopSixAxisSensor(this->sixaxis_handles[1]); + hidStopSixAxisSensor(this->sixaxis_handles[2]); + hidStopSixAxisSensor(this->sixaxis_handles[3]); +#endif return true; } -bool IO::MainLoop(ChiakiControllerState *state) +void IO::UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id) { - D(glUseProgram(this->prog)); - +#ifdef __SWITCH__ + padUpdate(&this->pad); +#endif // handle SDL events while(SDL_PollEvent(&this->sdl_event)) { @@ -851,11 +986,17 @@ bool IO::MainLoop(ChiakiControllerState *state) switch(this->sdl_event.type) { case SDL_QUIT: - return false; + this->quit = true; } } - ReadGameTouchScreen(state); + ReadGameTouchScreen(state, finger_id_touch_id); + ReadGameSixAxis(state); +} + +bool IO::MainLoop() +{ + D(glUseProgram(this->prog)); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index c12d413..1dd33de 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -28,8 +28,6 @@ bool appletMainLoop() // use a custom nintendo switch socket config // chiaki requiers many threads with udp/tcp sockets static const SocketInitConfig g_chiakiSocketInitConfig = { - .bsdsockets_version = 1, - .tcp_tx_buf_size = 0x8000, .tcp_rx_buf_size = 0x10000, .tcp_tx_buf_max_size = 0x40000, @@ -83,12 +81,6 @@ extern "C" void userAppInit() // load socket custom config socketInitialize(&g_chiakiSocketInitConfig); setsysInitialize(); - - // padConfigureInput(1, HidNpadStyleSet_NpadStandard); - // PadState pad; - // padInitializeDefault(&pad); - - //hidInitializeTouchScreen(); } extern "C" void userAppExit() @@ -111,11 +103,11 @@ extern "C" void userAppExit() } #endif // __SWITCH__ -int main(int argc, char * argv[]) +int main(int argc, char *argv[]) { // load chiaki lib - Settings * settings = Settings::GetInstance(); - ChiakiLog * log = settings->GetLogger(); + Settings *settings = Settings::GetInstance(); + ChiakiLog *log = settings->GetLogger(); CHIAKI_LOGI(log, "Loading chaki lib"); @@ -126,7 +118,7 @@ int main(int argc, char * argv[]) return 1; } - CHIAKI_LOGI(log, "Loading SDL audio / joystick"); + CHIAKI_LOGI(log, "Loading SDL audio / joystick / haptic"); if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_JOYSTICK)) { CHIAKI_LOGE(log, "SDL initialization failed: %s", SDL_GetError()); @@ -135,8 +127,11 @@ int main(int argc, char * argv[]) // build sdl OpenGl and AV decoders graphical interface DiscoveryManager discoverymanager = DiscoveryManager(); - MainApplication app = MainApplication(&discoverymanager); - app.Load(); + { + // scope to delete MainApplication before SDL_Quit() + MainApplication app(&discoverymanager); + app.Load(); + } CHIAKI_LOGI(log, "Quit applet"); SDL_Quit(); diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index f0eb3e3..7d3300a 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -7,13 +7,13 @@ Settings::Settings() { #if defined(__SWITCH__) - chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE ^ CHIAKI_LOG_DEBUG, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~(CHIAKI_LOG_VERBOSE | CHIAKI_LOG_DEBUG), chiaki_log_cb_print, NULL); #else - chiaki_log_init(&this->log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif } -Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string * value) +Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string *value) { Settings::ConfigurationItem ci; std::smatch m; @@ -29,15 +29,11 @@ Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string return UNKNOWN; } -size_t Settings::GetB64encodeSize(size_t in) -{ - // calculate base64 buffer size after encode - return ((4 * in / 3) + 3) & ~3; -} +#define B64_ENCODED_SIZE(in) (((4 * in / 3) + 3) & ~3) -Settings * Settings::instance = nullptr; +Settings *Settings::instance = nullptr; -Settings * Settings::GetInstance() +Settings *Settings::GetInstance() { if(instance == nullptr) { @@ -47,17 +43,17 @@ Settings * Settings::GetInstance() return instance; } -ChiakiLog * Settings::GetLogger() +ChiakiLog *Settings::GetLogger() { return &this->log; } -std::map * Settings::GetHostsMap() +std::map *Settings::GetHostsMap() { return &this->hosts; } -Host * Settings::GetOrCreateHost(std::string * host_name) +Host *Settings::GetOrCreateHost(std::string *host_name) { bool created = false; // update of create Host instance @@ -69,7 +65,7 @@ Host * Settings::GetOrCreateHost(std::string * host_name) created = true; } - Host * host = &(this->hosts.at(*host_name)); + Host *host = &(this->hosts.at(*host_name)); if(created) { // copy default settings @@ -90,7 +86,7 @@ void Settings::ParseFile() std::string line; std::string value; bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; - Host * current_host = nullptr; + Host *current_host = nullptr; if(config_file.is_open()) { CHIAKI_LOGV(&this->log, "Config file opened"); @@ -102,63 +98,63 @@ void Settings::ParseFile() ci = this->ParseLine(&line, &value); switch(ci) { - // got to next line - case UNKNOWN: - CHIAKI_LOGV(&this->log, "UNKNOWN config"); - break; - case HOST_NAME: - CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); - // current host is in context - current_host = this->GetOrCreateHost(&value); - // all following case will edit the current_host config + // got to next line + case UNKNOWN: + CHIAKI_LOGV(&this->log, "UNKNOWN config"); + break; + case HOST_NAME: + CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); + // current host is in context + current_host = this->GetOrCreateHost(&value); + // all following case will edit the current_host config - rp_key_b = false; - rp_regist_key_b = false; - rp_key_type_b = false; - break; - case HOST_ADDR: - CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); - if(current_host != nullptr) - current_host->host_addr = value; - break; - case PSN_ONLINE_ID: - CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); - // current_host == nullptr - // means we are in global ini section - // update default setting - this->SetPSNOnlineID(current_host, value); - break; - case PSN_ACCOUNT_ID: - CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); - this->SetPSNAccountID(current_host, value); - break; - case RP_KEY: - CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_key_b = this->SetHostRPKey(current_host, value); - break; - case RP_KEY_TYPE: - CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); - if(current_host != nullptr) - // TODO Check possible rp_type values - rp_key_type_b = this->SetHostRPKeyType(current_host, value); - break; - case RP_REGIST_KEY: - CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); - break; - case VIDEO_RESOLUTION: - this->SetVideoResolution(current_host, value); - break; - case VIDEO_FPS: - this->SetVideoFPS(current_host, value); - break; - case TARGET: - CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); - if(current_host != nullptr) - this->SetChiakiTarget(current_host, value); - break; + rp_key_b = false; + rp_regist_key_b = false; + rp_key_type_b = false; + break; + case HOST_ADDR: + CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + break; + case PSN_ONLINE_ID: + CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); + // current_host == nullptr + // means we are in global ini section + // update default setting + this->SetPSNOnlineID(current_host, value); + break; + case PSN_ACCOUNT_ID: + CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); + this->SetPSNAccountID(current_host, value); + break; + case RP_KEY: + CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_key_b = this->SetHostRPKey(current_host, value); + break; + case RP_KEY_TYPE: + CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); + if(current_host != nullptr) + // TODO Check possible rp_type values + rp_key_type_b = this->SetHostRPKeyType(current_host, value); + break; + case RP_REGIST_KEY: + CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); + break; + case VIDEO_RESOLUTION: + this->SetVideoResolution(current_host, value); + break; + case VIDEO_FPS: + this->SetVideoFPS(current_host, value); + break; + case TARGET: + CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); + if(current_host != nullptr) + this->SetChiakiTarget(current_host, value); + break; } // ci switch if(rp_key_b && rp_regist_key_b && rp_key_type_b) // the current host contains rp key data @@ -210,9 +206,8 @@ int Settings::WriteFile() config_file << "[" << it->first << "]\n" << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" - << "target = " << it->second.GetChiakiTarget() << "\"\n"; + << "target = \"" << it->second.GetChiakiTarget() << "\"\n"; - config_file << "target = \"" << it->second.psn_account_id << "\"\n"; if(it->second.video_resolution) config_file << "video_resolution = \"" << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) @@ -231,7 +226,7 @@ int Settings::WriteFile() if(it->second.rp_key_data || it->second.registered) { - char rp_key_type[33] = {0}; + char rp_key_type[33] = { 0 }; snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); // save registered rp key for auto login config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" @@ -250,14 +245,14 @@ std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resol { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return "360p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return "540p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return "720p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return "1080p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return "360p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return "540p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return "720p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return "1080p"; } return "UNKNOWN"; } @@ -266,14 +261,14 @@ int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return 360; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return 540; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return 720; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return 1080; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return 360; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return 540; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return 720; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return 1080; } return 0; } @@ -300,10 +295,10 @@ std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return "30"; - case CHIAKI_VIDEO_FPS_PRESET_60: - return "60"; + case CHIAKI_VIDEO_FPS_PRESET_30: + return "30"; + case CHIAKI_VIDEO_FPS_PRESET_60: + return "60"; } return "UNKNOWN"; } @@ -312,10 +307,10 @@ int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return 30; - case CHIAKI_VIDEO_FPS_PRESET_60: - return 60; + case CHIAKI_VIDEO_FPS_PRESET_30: + return 30; + case CHIAKI_VIDEO_FPS_PRESET_60: + return 60; } return 0; } @@ -334,7 +329,7 @@ ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) return CHIAKI_VIDEO_FPS_PRESET_30; } -std::string Settings::GetHostName(Host * host) +std::string Settings::GetHostName(Host *host) { if(host != nullptr) return host->GetHostName(); @@ -343,7 +338,7 @@ std::string Settings::GetHostName(Host * host) return ""; } -std::string Settings::GetHostAddr(Host * host) +std::string Settings::GetHostAddr(Host *host) { if(host != nullptr) return host->GetHostAddr(); @@ -352,7 +347,7 @@ std::string Settings::GetHostAddr(Host * host) return ""; } -std::string Settings::GetPSNOnlineID(Host * host) +std::string Settings::GetPSNOnlineID(Host *host) { if(host == nullptr || host->psn_online_id.length() == 0) return this->global_psn_online_id; @@ -360,7 +355,7 @@ std::string Settings::GetPSNOnlineID(Host * host) return host->psn_online_id; } -void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) +void Settings::SetPSNOnlineID(Host *host, std::string psn_online_id) { if(host == nullptr) this->global_psn_online_id = psn_online_id; @@ -368,7 +363,7 @@ void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) host->psn_online_id = psn_online_id; } -std::string Settings::GetPSNAccountID(Host * host) +std::string Settings::GetPSNAccountID(Host *host) { if(host == nullptr || host->psn_account_id.length() == 0) return this->global_psn_account_id; @@ -376,7 +371,7 @@ std::string Settings::GetPSNAccountID(Host * host) return host->psn_account_id; } -void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) +void Settings::SetPSNAccountID(Host *host, std::string psn_account_id) { if(host == nullptr) this->global_psn_account_id = psn_account_id; @@ -384,7 +379,7 @@ void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) host->psn_account_id = psn_account_id; } -ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) +ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host *host) { if(host == nullptr) return this->global_video_resolution; @@ -392,7 +387,7 @@ ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) return host->video_resolution; } -void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value) +void Settings::SetVideoResolution(Host *host, ChiakiVideoResolutionPreset value) { if(host == nullptr) this->global_video_resolution = value; @@ -400,13 +395,13 @@ void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value host->video_resolution = value; } -void Settings::SetVideoResolution(Host * host, std::string value) +void Settings::SetVideoResolution(Host *host, std::string value) { ChiakiVideoResolutionPreset p = StringToResolutionPreset(value); this->SetVideoResolution(host, p); } -ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) +ChiakiVideoFPSPreset Settings::GetVideoFPS(Host *host) { if(host == nullptr) return this->global_video_fps; @@ -414,7 +409,7 @@ ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) return host->video_fps; } -void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) +void Settings::SetVideoFPS(Host *host, ChiakiVideoFPSPreset value) { if(host == nullptr) this->global_video_fps = value; @@ -422,18 +417,18 @@ void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) host->video_fps = value; } -void Settings::SetVideoFPS(Host * host, std::string value) +void Settings::SetVideoFPS(Host *host, std::string value) { ChiakiVideoFPSPreset p = StringToFPSPreset(value); this->SetVideoFPS(host, p); } -ChiakiTarget Settings::GetChiakiTarget(Host * host) +ChiakiTarget Settings::GetChiakiTarget(Host *host) { return host->GetChiakiTarget(); } -bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) +bool Settings::SetChiakiTarget(Host *host, ChiakiTarget target) { if(host != nullptr) { @@ -447,20 +442,19 @@ bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) } } -bool Settings::SetChiakiTarget(Host * host, std::string value) +bool Settings::SetChiakiTarget(Host *host, std::string value) { // TODO Check possible target values return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); } -std::string Settings::GetHostRPKey(Host * host) +std::string Settings::GetHostRPKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { - size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = {0}; + char rp_key_b64[B64_ENCODED_SIZE(0x10) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( host->rp_key, 0x10, @@ -478,7 +472,7 @@ std::string Settings::GetHostRPKey(Host * host) return ""; } -bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) +bool Settings::SetHostRPKey(Host *host, std::string rp_key_b64) { if(host != nullptr) { @@ -497,14 +491,13 @@ bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) return false; } -std::string Settings::GetHostRPRegistKey(Host * host) +std::string Settings::GetHostRPRegistKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { - size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; + char rp_regist_key_b64[B64_ENCODED_SIZE(CHIAKI_SESSION_AUTH_SIZE) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, @@ -522,7 +515,7 @@ std::string Settings::GetHostRPRegistKey(Host * host) return ""; } -bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) +bool Settings::SetHostRPRegistKey(Host *host, std::string rp_regist_key_b64) { if(host != nullptr) { @@ -541,7 +534,7 @@ bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) return false; } -int Settings::GetHostRPKeyType(Host * host) +int Settings::GetHostRPKeyType(Host *host) { if(host != nullptr) return host->rp_key_type; @@ -550,7 +543,7 @@ int Settings::GetHostRPKeyType(Host * host) return 0; } -bool Settings::SetHostRPKeyType(Host * host, std::string value) +bool Settings::SetHostRPKeyType(Host *host, std::string value) { if(host != nullptr) { @@ -562,7 +555,7 @@ bool Settings::SetHostRPKeyType(Host * host, std::string value) } #ifdef CHIAKI_ENABLE_SWITCH_OVERCLOCK -int Settings::GetCPUOverclock(Host * host) +int Settings::GetCPUOverclock(Host *host) { if(host == nullptr) return this->global_cpu_overclock; @@ -570,7 +563,7 @@ int Settings::GetCPUOverclock(Host * host) return host->cpu_overclock; } -void Settings::SetCPUOverclock(Host * host, int value) +void Settings::SetCPUOverclock(Host *host, int value) { int oc = OC_1326; if(value > OC_1580) @@ -592,11 +585,9 @@ void Settings::SetCPUOverclock(Host * host, int value) host->cpu_overclock = oc; } -void Settings::SetCPUOverclock(Host * host, std::string value) +void Settings::SetCPUOverclock(Host *host, std::string value) { int v = atoi(value.c_str()); this->SetCPUOverclock(host, v); } #endif - - diff --git a/test/reorderqueue.c b/test/reorderqueue.c index e5a90c9..f0fd5fe 100644 --- a/test/reorderqueue.c +++ b/test/reorderqueue.c @@ -16,7 +16,7 @@ typedef struct drop_record_t static void drop(uint64_t seq_num, void *elem_user, void *cb_user) { DropRecord *record = cb_user; - uint64_t v = (uint64_t)elem_user; + uint64_t v = (uint64_t)(size_t)elem_user; if(v > DROP_RECORD_MAX) { record->failed = true; @@ -58,7 +58,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); munit_assert(!drop_record.failed); munit_assert_uint64(drop_record.count[0], ==, 0); - munit_assert_uint64((uint64_t)user, ==, 0); + munit_assert_uint64((uint64_t)(size_t)user, ==, 0); munit_assert_uint64(seq_num, ==, 42); // push outdated @@ -105,22 +105,22 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 44); - munit_assert_uint64((uint64_t)user, ==, 3); + munit_assert_uint64((uint64_t)(size_t)user, ==, 3); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 45); - munit_assert_uint64((uint64_t)user, ==, 2); + munit_assert_uint64((uint64_t)(size_t)user, ==, 2); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 46); - munit_assert_uint64((uint64_t)user, ==, 1); + munit_assert_uint64((uint64_t)(size_t)user, ==, 1); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 47); - munit_assert_uint64((uint64_t)user, ==, 5); + munit_assert_uint64((uint64_t)(size_t)user, ==, 5); // should be empty now again pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); @@ -142,7 +142,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 1337); - munit_assert_uint64((uint64_t)user, ==, 6); + munit_assert_uint64((uint64_t)(size_t)user, ==, 6); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); // same as before, but with an element in the queue that will be dropped @@ -163,7 +163,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 2000); - munit_assert_uint64((uint64_t)user, ==, 8); + munit_assert_uint64((uint64_t)(size_t)user, ==, 8); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); chiaki_reorder_queue_fini(&queue); @@ -182,4 +182,4 @@ MunitTest tests_reorder_queue[] = { NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index b1f0b46..f6ebc40 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -4,11 +4,13 @@ if(NOT CHIAKI_USE_SYSTEM_NANOPB) # nanopb ################## + add_definitions(-DPB_C99_STATIC_ASSERT) # Fix PB_STATIC_ASSERT on msvc without using C11 for now add_subdirectory(nanopb EXCLUDE_FROM_ALL) set(NANOPB_GENERATOR_PY "${CMAKE_CURRENT_SOURCE_DIR}/nanopb/generator/nanopb_generator.py" PARENT_SCOPE) add_library(nanopb INTERFACE) target_link_libraries(nanopb INTERFACE protobuf-nanopb-static) target_include_directories(nanopb INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") + target_compile_definitions(nanopb INTERFACE -DPB_C99_STATIC_ASSERT) # see above add_library(Nanopb::nanopb ALIAS nanopb) endif() diff --git a/third-party/nanopb b/third-party/nanopb index ae9901f..afc499f 160000 --- a/third-party/nanopb +++ b/third-party/nanopb @@ -1 +1 @@ -Subproject commit ae9901f2a31500e8fdc93fa9804d24851c58bb1e +Subproject commit afc499f9a410fc9bbf6c9c48cdd8d8b199d49eb4