diff --git a/.ci/build-linux-aarch64.sh b/.ci/build-linux-aarch64.sh index fb7e6193a0..e067f23445 100755 --- a/.ci/build-linux-aarch64.sh +++ b/.ci/build-linux-aarch64.sh @@ -4,6 +4,8 @@ if [ -z "$CIRRUS_CI" ]; then cd rpcs3 || exit 1 fi +shellcheck .ci/*.sh + git config --global --add safe.directory '*' # Pull all the submodules except llvm, opencv, sdl and curl @@ -41,14 +43,14 @@ cmake .. \ -DOpenGL_GL_PREFERENCE=LEGACY \ -DLLVM_DIR=/opt/llvm/lib/cmake/llvm \ -DSTATIC_LINK_LLVM=ON \ + -DBUILD_RPCS3_TESTS="${RUN_UNIT_TESTS}" \ + -DRUN_RPCS3_TESTS="${RUN_UNIT_TESTS}" \ -G Ninja ninja; build_status=$?; cd .. -shellcheck .ci/*.sh - # If it compiled succesfully let's deploy. # Azure and Cirrus publish PRs as artifacts only. { [ "$CI_HAS_ARTIFACTS" = "true" ]; diff --git a/.ci/build-linux.sh b/.ci/build-linux.sh index a8d887e0d5..4d9e9d0d09 100755 --- a/.ci/build-linux.sh +++ b/.ci/build-linux.sh @@ -4,6 +4,8 @@ if [ -z "$CIRRUS_CI" ]; then cd rpcs3 || exit 1 fi +shellcheck .ci/*.sh + git config --global --add safe.directory '*' # Pull all the submodules except llvm, opencv, sdl and curl @@ -52,14 +54,14 @@ cmake .. \ -DOpenGL_GL_PREFERENCE=LEGACY \ -DLLVM_DIR=/opt/llvm/lib/cmake/llvm \ -DSTATIC_LINK_LLVM=ON \ + -DBUILD_RPCS3_TESTS="${RUN_UNIT_TESTS}" \ + -DRUN_RPCS3_TESTS="${RUN_UNIT_TESTS}" \ -G Ninja ninja; build_status=$?; cd .. -shellcheck .ci/*.sh - # If it compiled succesfully let's deploy. # Azure and Cirrus publish PRs as artifacts only. { [ "$CI_HAS_ARTIFACTS" = "true" ]; diff --git a/.ci/build-mac-arm64.sh b/.ci/build-mac-arm64.sh index 0abbf1e602..61bcf9cf50 100644 --- a/.ci/build-mac-arm64.sh +++ b/.ci/build-mac-arm64.sh @@ -115,6 +115,8 @@ mkdir build && cd build || exit 1 export MACOSX_DEPLOYMENT_TARGET=14.0 "$BREW_X64_PATH/bin/cmake" .. \ + -DBUILD_RPCS3_TESTS=OFF \ + -DRUN_RPCS3_TESTS=OFF \ -DUSE_SDL=ON \ -DUSE_DISCORD_RPC=ON \ -DUSE_VULKAN=ON \ diff --git a/.ci/build-mac.sh b/.ci/build-mac.sh index 421ea4184a..2d87f9d60e 100644 --- a/.ci/build-mac.sh +++ b/.ci/build-mac.sh @@ -81,6 +81,8 @@ mkdir build && cd build || exit 1 export MACOSX_DEPLOYMENT_TARGET=14.0 "$BREW_X64_PATH/bin/cmake" .. \ + -DBUILD_RPCS3_TESTS=OFF \ + -DRUN_RPCS3_TESTS=OFF \ -DUSE_SDL=ON \ -DUSE_DISCORD_RPC=ON \ -DUSE_VULKAN=ON \ diff --git a/.ci/docker.env b/.ci/docker.env index 2b36fb34c0..ee037bfb4d 100644 --- a/.ci/docker.env +++ b/.ci/docker.env @@ -8,6 +8,7 @@ BUILD_SOURCEBRANCHNAME APPDIR ARTDIR RELEASE_MESSAGE +RUN_UNIT_TESTS # Variables for build matrix COMPILER DEPLOY_APPIMAGE diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index 986bc715f5..2f68adefd5 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -56,6 +56,7 @@ jobs: COMPILER: ${{ matrix.compiler }} UPLOAD_COMMIT_HASH: ${{ matrix.UPLOAD_COMMIT_HASH }} UPLOAD_REPO_FULL_NAME: ${{ matrix.UPLOAD_REPO_FULL_NAME }} + RUN_UNIT_TESTS: github.event_name == 'pull_request' && 'ON' || 'OFF' steps: - name: Checkout repository uses: actions/checkout@main @@ -132,6 +133,12 @@ jobs: with: fetch-depth: 0 + - name: Setup NuGet + uses: nuget/setup-nuget@v2 + + - name: Restore NuGet packages + run: nuget restore rpcs3.sln + - name: Setup env shell: pwsh run: | @@ -171,7 +178,12 @@ jobs: - name: Compile RPCS3 shell: pwsh - run: msbuild rpcs3.sln /p:Configuration=Release /v:minimal /p:Platform=x64 /p:CLToolPath=${{ env.CCACHE_BIN_DIR }} /p:UseMultiToolTask=true /p:CustomAfterMicrosoftCommonTargets="${{ github.workspace }}\buildfiles\msvc\ci_only.targets" + run: msbuild rpcs3.sln /p:Configuration=Release /v:minimal /p:Platform=x64 /p:PreferredToolArchitecture=x64 /p:CLToolPath=${{ env.CCACHE_BIN_DIR }} /p:UseMultiToolTask=true /p:CustomAfterMicrosoftCommonTargets="${{ github.workspace }}\buildfiles\msvc\ci_only.targets" + + - name: Run Unit Tests + if: github.event_name == 'pull_request' + shell: pwsh + run: build\lib\Release-x64\rpcs3_test.exe - name: Pack up build artifacts run: | diff --git a/3rdparty/FAudio b/3rdparty/FAudio index 091c6b4693..6077ea740a 160000 --- a/3rdparty/FAudio +++ b/3rdparty/FAudio @@ -1 +1 @@ -Subproject commit 091c6b4693ce507ac48037836a5a884e35cd2860 +Subproject commit 6077ea740a7114a54f76ed9b7abe08cffc0034b6 diff --git a/3rdparty/libpng/libpng b/3rdparty/libpng/libpng index 872555f4ba..ea12796820 160000 --- a/3rdparty/libpng/libpng +++ b/3rdparty/libpng/libpng @@ -1 +1 @@ -Subproject commit 872555f4ba910252783af1507f9e7fe1653be252 +Subproject commit ea127968204cc5d10f3fc9250c306b9e8cbd9b80 diff --git a/3rdparty/version_check.sh b/3rdparty/version_check.sh new file mode 100644 index 0000000000..2b721fe28f --- /dev/null +++ b/3rdparty/version_check.sh @@ -0,0 +1,115 @@ +#!/bin/sh -ex + +verbose=0 +git_verbose=0 + +if [ "$1" = "-v" ]; then + verbose=1 +elif [ "$1" = "-vv" ]; then + verbose=1 + git_verbose=1 +fi + +max_dir_length=0 +result_dirs=() +result_msgs=() + +git_call() +{ + if [ "$git_verbose" -eq 1 ]; then + eval "git $@" + elif [[ "$1" == "fetch" ]]; then + eval "git $@ >/dev/null 2>&1" + else + eval "git $@ 2>/dev/null" + fi +} + +check_tags() +{ + path=$(echo "$1" | sed 's:/*$::') + + echo "Checking $path" + + git_call fetch --prune --all + + # Get the latest tag (by commit date, not tag name) + tag_list=$(git_call rev-list --tags --max-count=1) + latest_tag=$(git_call describe --tags "$tag_list") + + if [ -n "$latest_tag" ]; then + + # Get the current tag + current_tag=$(git_call describe --tags --abbrev=0) + + if [ -n "$current_tag" ]; then + + if [ "$verbose" -eq 1 ]; then + echo "$path -> latest: $latest_tag, current: $current_tag" + fi + + ts1=$(git_call log -1 --format=%ct $latest_tag) + ts2=$(git_call log -1 --format=%ct $current_tag) + + if (( ts1 > ts2 )); then + if [ "$verbose" -eq 1 ]; then + echo -e "\t $path: latest is newer" + elif [ "$verbose" -eq 0 ]; then + echo "$path -> latest: $latest_tag, current: $current_tag" + fi + + path_length=${#path} + if (( $path_length > $max_dir_length )); then + max_dir_length=$path_length + fi + result_dirs+=("$path") + result_msgs+=("latest: $latest_tag, current: $current_tag") + fi + + elif [ "$verbose" -eq 1 ]; then + echo "$path -> latest: $latest_tag" + fi + elif [ "$verbose" -eq 1 ]; then + + if [ -n "$current_tag" ]; then + echo "$path -> current: $current_tag" + else + echo "$path -> no tags found" + fi + fi +} + +for submoduledir in */ ; +do + cd "$submoduledir" || continue + + if [ -e ".git" ]; then + check_tags "$submoduledir" + else + # find */ -mindepth 1 -maxdepth 1 -type d | while read -r sub; + for sub in */ ; + do + if [ -e "$sub/.git" ]; then + cd "$sub" || continue + check_tags "$submoduledir$sub" + cd .. || exit + fi + done + fi + + cd .. || exit +done + +echo -e "\n\nResult:\n" +i=0 +for result_dir in "${result_dirs[@]}"; do + msg="" + diff=$(($max_dir_length - ${#result_dir})) + if (( $diff > 0 )); then + msg+=$(printf "%${diff}s" "") + fi + msg+="$result_dir" + echo "$msg -> ${result_msgs[$i]}" + ((i++)) +done +echo "" diff --git a/CMakeLists.txt b/CMakeLists.txt index ea1a194aec..9d2edd8365 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,8 @@ option(USE_SYSTEM_CURL "Prefer system Curl instead of the prebuild one" ON) option(USE_SYSTEM_OPENCV "Prefer system OpenCV instead of the builtin one" ON) option(HAS_MEMORY_BREAKPOINTS "Add support for memory breakpoints to the interpreter" OFF) option(USE_LTO "Use LTO for building" ON) +option(BUILD_RPCS3_TESTS "Build RPCS3 unit tests." OFF) +option(RUN_RPCS3_TESTS "Run RPCS3 unit tests. Requires BUILD_RPCS3_TESTS" OFF) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/buildfiles/cmake") diff --git a/Utilities/File.cpp b/Utilities/File.cpp index c5fecc9d14..aafcfe3c30 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -1707,7 +1707,7 @@ fs::file fs::file::from_native_handle(native_handle handle) fs::file result; #ifdef _WIN32 - result.m_file = std::make_unique((const HANDLE)handle); + result.m_file = std::make_unique(static_cast(handle)); #else result.m_file = std::make_unique(handle); #endif diff --git a/Utilities/StrFmt.cpp b/Utilities/StrFmt.cpp index 0b9baa048e..c628a90a8a 100644 --- a/Utilities/StrFmt.cpp +++ b/Utilities/StrFmt.cpp @@ -748,6 +748,12 @@ void fmt::raw_append(std::string& out, const char* fmt, const fmt_type_info* sup std::string fmt::replace_all(std::string_view src, std::string_view from, std::string_view to, usz count) { + if (src.empty()) + return {}; + + if (from.empty() || count == 0) + return std::string(src); + std::string target; target.reserve(src.size() + to.size()); @@ -869,45 +875,6 @@ std::string fmt::truncate(std::string_view src, usz length) return std::string(src.begin(), src.begin() + std::min(src.size(), length)); } -bool fmt::match(const std::string& source, const std::string& mask) -{ - usz source_position = 0, mask_position = 0; - - for (; source_position < source.size() && mask_position < mask.size(); ++mask_position, ++source_position) - { - switch (mask[mask_position]) - { - case '?': break; - - case '*': - for (usz test_source_position = source_position; test_source_position < source.size(); ++test_source_position) - { - if (match(source.substr(test_source_position), mask.substr(mask_position + 1))) - { - return true; - } - } - return false; - - default: - if (source[source_position] != mask[mask_position]) - { - return false; - } - - break; - } - } - - if (source_position != source.size()) - return false; - - if (mask_position != mask.size()) - return false; - - return true; -} - std::string get_file_extension(const std::string& file_path) { if (usz dotpos = file_path.find_last_of('.'); dotpos != std::string::npos && dotpos + 1 < file_path.size()) diff --git a/Utilities/StrUtil.h b/Utilities/StrUtil.h index 4614f6f623..c28efda863 100644 --- a/Utilities/StrUtil.h +++ b/Utilities/StrUtil.h @@ -39,11 +39,15 @@ std::string get_file_extension(const std::string& file_path); namespace fmt { - std::string replace_all(std::string_view src, std::string_view from, std::string_view to, usz count = -1); + // Replaces all occurrences of 'from' with 'to' until 'count' substrings were replaced. + std::string replace_all(std::string_view src, std::string_view from, std::string_view to, usz count = umax); template std::string replace_all(std::string src, const std::pair (&list)[list_size]) { + if constexpr (list_size == 0) + return src; + for (usz pos = 0; pos < src.length(); ++pos) { for (usz i = 0; i < list_size; ++i) @@ -71,6 +75,9 @@ namespace fmt template std::string replace_all(std::string src, const std::pair> (&list)[list_size]) { + if constexpr (list_size == 0) + return src; + for (usz pos = 0; pos < src.length(); ++pos) { for (usz i = 0; i < list_size; ++i) @@ -99,6 +106,9 @@ namespace fmt static inline std::string replace_all(std::string src, const std::vector>& list) { + if (list.empty()) + return src; + for (usz pos = 0; pos < src.length(); ++pos) { for (usz i = 0; i < list.size(); ++i) @@ -123,9 +133,16 @@ namespace fmt return src; } + // Splits the string into a vector of strings using the separators. The vector may contain empty strings unless is_skip_empty is true. std::vector split(std::string_view source, std::initializer_list separators, bool is_skip_empty = true); + + // Removes all preceding and trailing characters specified by 'values' from 'source'. std::string trim(const std::string& source, std::string_view values = " \t"); + + // Removes all preceding characters specified by 'values' from 'source'. std::string trim_front(const std::string& source, std::string_view values = " \t"); + + // Removes all trailing characters specified by 'values' from 'source'. void trim_back(std::string& source, std::string_view values = " \t"); template @@ -175,13 +192,15 @@ namespace fmt return result; } + // Returns the string transformed to uppercase std::string to_upper(std::string_view string); + + // Returns the string transformed to lowercase std::string to_lower(std::string_view string); + // Returns the string shortened to length std::string truncate(std::string_view src, usz length); - bool match(const std::string& source, const std::string& mask); - struct buf_to_hexstring { buf_to_hexstring(const u8* buf, usz len, usz line_length = 16, bool with_prefix = false) diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index f9b65540cf..727bee8cae 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -1,4 +1,4 @@ -# Define GNU standard installation directories +# Define GNU standard installation directories include(GNUInstallDirs) # Generate git-version.h at build time. @@ -51,6 +51,61 @@ endif() gen_git_version(${CMAKE_CURRENT_SOURCE_DIR}) if (NOT ANDROID) + # Build rpcs3_lib + add_library(rpcs3_lib STATIC) + + if(WIN32) + target_compile_definitions(rpcs3_lib PRIVATE UNICODE _UNICODE) + endif() + + set_target_properties(rpcs3_lib + PROPERTIES + AUTOMOC ON + AUTOUIC ON) + + target_link_libraries(rpcs3_lib + PUBLIC + 3rdparty::stblib + 3rdparty::libevdev + rpcs3_emu + PRIVATE + rpcs3_ui + 3rdparty::discordRPC + 3rdparty::qt6 + 3rdparty::hidapi + 3rdparty::libusb + 3rdparty::wolfssl + 3rdparty::libcurl + 3rdparty::zlib + 3rdparty::opencv + 3rdparty::fusion + ${ADDITIONAL_LIBS}) + + # Unix display manager + if(X11_FOUND) + target_link_libraries(rpcs3_lib PRIVATE X11::X11) + elseif(USE_VULKAN AND UNIX AND NOT WAYLAND_FOUND AND NOT APPLE AND NOT ANDROID) + # Wayland has been checked in 3rdparty/CMakeLists.txt already. + message(FATAL_ERROR "RPCS3 requires either X11 or Wayland (or both) for Vulkan.") + endif() + + if(UNIX) + set(CMAKE_THREAD_PREFER_PTHREAD TRUE) + find_package(Threads REQUIRED) + target_link_libraries(rpcs3_lib PRIVATE Threads::Threads) + endif() + + if(WIN32) + target_link_libraries(rpcs3_lib PRIVATE ws2_32 Iphlpapi Winmm Psapi gdi32 setupapi) + else() + target_link_libraries(rpcs3_lib PRIVATE ${CMAKE_DL_LIBS}) + endif() + + if(USE_PRECOMPILED_HEADERS) + target_precompile_headers(rpcs3_lib PRIVATE stdafx.h) + endif() + + # Build rpcs3 executable if(WIN32) add_executable(rpcs3 WIN32) target_sources(rpcs3 PRIVATE rpcs3.rc) @@ -68,85 +123,18 @@ if (NOT ANDROID) target_sources(rpcs3 PRIVATE - display_sleep_control.cpp - headless_application.cpp - main.cpp - main_application.cpp - module_verifier.cpp - rpcs3.cpp - rpcs3_version.cpp - stb_image.cpp - stdafx.cpp - - Input/basic_keyboard_handler.cpp - Input/basic_mouse_handler.cpp - Input/ds3_pad_handler.cpp - Input/ds4_pad_handler.cpp - Input/dualsense_pad_handler.cpp - Input/evdev_joystick_handler.cpp - Input/evdev_gun_handler.cpp - Input/gui_pad_thread.cpp - Input/hid_pad_handler.cpp - Input/keyboard_pad_handler.cpp - Input/mm_joystick_handler.cpp - Input/pad_thread.cpp - Input/product_info.cpp - Input/ps_move_calibration.cpp - Input/ps_move_config.cpp - Input/ps_move_handler.cpp - Input/ps_move_tracker.cpp - Input/raw_mouse_config.cpp - Input/raw_mouse_handler.cpp - Input/sdl_pad_handler.cpp - Input/skateboard_pad_handler.cpp - Input/xinput_pad_handler.cpp + main.cpp ) - set_target_properties(rpcs3 - PROPERTIES - AUTOMOC ON - AUTOUIC ON) - target_link_libraries(rpcs3 PRIVATE - rpcs3_emu - rpcs3_ui - 3rdparty::discordRPC - 3rdparty::qt6 - 3rdparty::hidapi - 3rdparty::libusb - 3rdparty::wolfssl - 3rdparty::libcurl - 3rdparty::zlib - 3rdparty::opencv - 3rdparty::fusion - ${ADDITIONAL_LIBS}) - - # Unix display manager - if(X11_FOUND) - target_link_libraries(rpcs3 PRIVATE X11::X11) - elseif(USE_VULKAN AND UNIX AND NOT WAYLAND_FOUND AND NOT APPLE AND NOT ANDROID) - # Wayland has been checked in 3rdparty/CMakeLists.txt already. - message(FATAL_ERROR "RPCS3 requires either X11 or Wayland (or both) for Vulkan.") - endif() - - if(UNIX) - set(CMAKE_THREAD_PREFER_PTHREAD TRUE) - find_package(Threads REQUIRED) - target_link_libraries(rpcs3 PRIVATE Threads::Threads) - endif() - - if(WIN32) - target_link_libraries(rpcs3 PRIVATE bcrypt ws2_32 Iphlpapi Winmm Psapi gdi32 setupapi pdh) - else() - target_link_libraries(rpcs3 PRIVATE ${CMAKE_DL_LIBS}) - endif() + rpcs3_lib + ) if(USE_PRECOMPILED_HEADERS) target_precompile_headers(rpcs3 PRIVATE stdafx.h) endif() - # Copy icons to executable directory if(APPLE) if (CMAKE_BUILD_TYPE MATCHES "Debug" OR CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") @@ -197,3 +185,43 @@ if (NOT ANDROID) DESTINATION ${CMAKE_INSTALL_DATADIR}/rpcs3) endif() endif() + +# Unit tests +if(BUILD_RPCS3_TESTS) + enable_testing() + find_package(GTest REQUIRED) + + message(STATUS "Building unit tests...") + + add_executable(rpcs3_test) + + target_sources(rpcs3_test + PRIVATE + tests/test.cpp + tests/test_fmt.cpp + tests/test_simple_array.cpp + ) + + target_link_libraries(rpcs3_test + PRIVATE + rpcs3_lib + GTest::gtest + ) + + target_include_directories(rpcs3_test + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ) + + include(GoogleTest) + gtest_discover_tests(rpcs3_test) + + if(RUN_RPCS3_TESTS) + add_custom_target(run_tests + ALL + COMMAND ${CMAKE_CTEST_COMMAND} -j -VV --output-on-failure + DEPENDS rpcs3_test + ) + endif() +endif() diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 61a81558a7..62a91d1d34 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(rpcs3_emu STATIC localized_string.cpp savestate_utils.cpp scoped_progress_dialog.cpp + stb_image.cpp System.cpp system_config.cpp system_config_types.cpp @@ -32,17 +33,6 @@ if(HAS_MEMORY_BREAKPOINTS) target_compile_definitions(rpcs3_emu PRIVATE RPCS3_HAS_MEMORY_BREAKPOINTS) endif() -target_link_libraries(rpcs3_emu - PRIVATE - 3rdparty::zlib 3rdparty::yaml-cpp 3rdparty::zstd - PUBLIC - 3rdparty::libevdev 3rdparty::flatbuffers) - - -find_package(Threads REQUIRED) -target_link_libraries(rpcs3_emu - PUBLIC Threads::Threads) - # For stdafx.h target_include_directories(rpcs3_emu PUBLIC @@ -95,10 +85,6 @@ endif() target_include_directories(rpcs3_emu PUBLIC "${CMAKE_SOURCE_DIR}") -target_link_libraries(rpcs3_emu - PUBLIC - 3rdparty::pugixml) - set_source_files_properties("../../Utilities/JITLLVM.cpp" "../../Utilities/JITASM.cpp" PROPERTIES COMPILE_FLAGS "$,/GR-,-fno-rtti>" SKIP_PRECOMPILE_HEADERS ON @@ -169,24 +155,9 @@ if(WIN32) Audio/XAudio2/xaudio2_enumerator.cpp ) target_compile_definitions(rpcs3_emu PRIVATE UNICODE _UNICODE _WIN32_WINNT=0x0A00) + target_link_libraries(rpcs3_emu PRIVATE pdh bcrypt) endif() -target_link_libraries(rpcs3_emu - PUBLIC - 3rdparty::openal) - -target_link_libraries(rpcs3_emu - PUBLIC - 3rdparty::cubeb) - -target_link_libraries(rpcs3_emu - PUBLIC - 3rdparty::soundtouch) - -target_link_libraries(rpcs3_emu - PUBLIC - 3rdparty::miniupnpc) - # Cell target_sources(rpcs3_emu PRIVATE Cell/ErrorCodes.cpp @@ -400,11 +371,6 @@ if(NOT MSVC) ) endif() -target_link_libraries(rpcs3_emu - PRIVATE - 3rdparty::stblib 3rdparty::libpng) - - # CPU target_sources(rpcs3_emu PRIVATE CPU/CPUThread.cpp @@ -420,15 +386,13 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|arm64|aarch64") ) endif() -target_link_libraries(rpcs3_emu - PUBLIC 3rdparty::llvm 3rdparty::asmjit) - # Io target_sources(rpcs3_emu PRIVATE Io/Buzz.cpp Io/camera_config.cpp Io/Dimensions.cpp + Io/evdev_gun_handler.cpp Io/GameTablet.cpp Io/GHLtar.cpp Io/GunCon3.cpp @@ -454,10 +418,8 @@ target_sources(rpcs3_emu PRIVATE Io/usb_device.cpp Io/usb_vfs.cpp Io/usio.cpp -) - -target_link_libraries(rpcs3_emu PRIVATE - 3rdparty::rtmidi + Io/LogitechG27.cpp + Io/LogitechG27Config.cpp ) # Np @@ -654,14 +616,35 @@ if(TARGET 3rdparty_vulkan) ) endif() +find_package(Threads REQUIRED) + target_link_libraries(rpcs3_emu PUBLIC - 3rdparty::ffmpeg 3rdparty::sdl3 - 3rdparty::opengl 3rdparty::stblib - 3rdparty::vulkan 3rdparty::glew - 3rdparty::libusb 3rdparty::wolfssl + 3rdparty::llvm + 3rdparty::asmjit + 3rdparty::ffmpeg + 3rdparty::sdl3 + 3rdparty::opengl + 3rdparty::stblib + 3rdparty::vulkan + 3rdparty::glew + 3rdparty::libusb + 3rdparty::wolfssl + 3rdparty::openal + 3rdparty::cubeb + 3rdparty::soundtouch + 3rdparty::miniupnpc + 3rdparty::libevdev + 3rdparty::flatbuffers + 3rdparty::pugixml + Threads::Threads PRIVATE 3rdparty::glslang + 3rdparty::libpng + 3rdparty::rtmidi + 3rdparty::yaml-cpp + 3rdparty::zlib + 3rdparty::zstd ) if(APPLE) diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index f051b67131..ddb582c5d8 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -19,7 +19,7 @@ #include "Input/ps_move_tracker.h" #ifdef HAVE_LIBEVDEV -#include "Input/evdev_gun_handler.h" +#include "Emu/Io/evdev_gun_handler.h" #endif #include // for fmod diff --git a/rpcs3/Emu/Cell/PPUAnalyser.cpp b/rpcs3/Emu/Cell/PPUAnalyser.cpp index ac5d285712..56d4398d09 100644 --- a/rpcs3/Emu/Cell/PPUAnalyser.cpp +++ b/rpcs3/Emu/Cell/PPUAnalyser.cpp @@ -532,7 +532,7 @@ namespace ppu_patterns } static constexpr struct const_tag{} is_const; -static constexpr struct range_tag{} is_range; +/*static constexpr*/ struct range_tag{} /*is_range*/; static constexpr struct min_value_tag{} minv; static constexpr struct max_value_tag{} maxv; static constexpr struct sign_bit_tag{} sign_bitv; diff --git a/rpcs3/Emu/Cell/lv2/sys_ss.cpp b/rpcs3/Emu/Cell/lv2/sys_ss.cpp index a602cd6ee7..0315c3f152 100644 --- a/rpcs3/Emu/Cell/lv2/sys_ss.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_ss.cpp @@ -553,7 +553,7 @@ error_code sys_ss_individual_info_manager(u64 pkg_id, u64 a2, vm::ptr out_s case 0x17002: { // TODO - vm::write(a5, a4); // Write back size of buffer + vm::write(static_cast(a5), a4); // Write back size of buffer break; } // Get EID size diff --git a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp index c8b5b7d6cb..0696481efa 100644 --- a/rpcs3/Emu/Cell/lv2/sys_usbd.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_usbd.cpp @@ -38,6 +38,9 @@ #include "Emu/Io/usio.h" #include "Emu/Io/usio_config.h" #include "Emu/Io/midi_config_types.h" +#ifdef HAVE_SDL3 +#include "Emu/Io/LogitechG27.h" +#endif #include @@ -208,7 +211,11 @@ private: // GT5 Wheels&co +#ifdef HAVE_SDL3 + {0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b", &usb_device_logitech_g27::get_num_emu_devices, &usb_device_logitech_g27::make_instance}, +#else {0x046D, 0xC283, 0xC29B, "lgFF_c283_c29b", nullptr, nullptr}, +#endif {0x044F, 0xB653, 0xB653, "Thrustmaster RGT FFB Pro", nullptr, nullptr}, {0x044F, 0xB65A, 0xB65A, "Thrustmaster F430", nullptr, nullptr}, {0x044F, 0xB65D, 0xB65D, "Thrustmaster FFB", nullptr, nullptr}, diff --git a/rpcs3/Emu/Io/LogitechG27.cpp b/rpcs3/Emu/Io/LogitechG27.cpp new file mode 100644 index 0000000000..21171e4c71 --- /dev/null +++ b/rpcs3/Emu/Io/LogitechG27.cpp @@ -0,0 +1,1554 @@ +// Logitech G27 + +// ffb ref +// https://opensource.logitech.com/wiki/force_feedback/Logitech_Force_Feedback_Protocol_V1.6.pdf +// https://github.com/mathijsvandenberg/g29emu/files/14395098/Logitech_Force_Feedback_Protocol_V1.6.pdf + +// shifter input ref +// https://github.com/sonik-br/lgff_wheel_adapter/blob/d97f7823154818e1b3edff6d51498a122c302728/pico_lgff_wheel_adapter/reports.h#L265-L310 + +#include "stdafx.h" + +#ifdef HAVE_SDL3 + +#include "LogitechG27.h" +#include "Emu/Cell/lv2/sys_usbd.h" +#include "Emu/system_config.h" +#include "Input/pad_thread.h" +#include "Input/sdl_instance.h" + +LOG_CHANNEL(logitech_g27_log, "LOGIG27"); + +usb_device_logitech_g27::usb_device_logitech_g27(u32 controller_index, const std::array& location) + : usb_device_emulated(location), m_controller_index(controller_index) +{ + device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{0x0200, 0, 0, 0, 16, 0x046d, 0xc29b, 0x1350, 1, 2, 0, 1}); + + // parse the raw response like with passthrough device + static const uint8_t raw_config[] = {0x9, 0x2, 0x29, 0x0, 0x1, 0x1, 0x4, 0x80, 0x31, 0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0, 0x9, 0x21, 0x11, 0x1, 0x21, 0x1, 0x22, 0x85, 0x0, 0x7, 0x5, 0x81, 0x3, 0x10, 0x0, 0x2, 0x7, 0x5, 0x1, 0x3, 0x10, 0x0, 0x2}; + auto& conf = device.add_node(UsbDescriptorNode(raw_config[0], raw_config[1], &raw_config[2])); + for (unsigned int index = raw_config[0]; index < sizeof(raw_config);) + { + conf.add_node(UsbDescriptorNode(raw_config[index], raw_config[index + 1], &raw_config[index + 2])); + index += raw_config[index]; + } + + // Initialize effect slots + for (int i = 0; i < 4; i++) + { + m_effect_slots[i].state = G27_FFB_INACTIVE; + m_effect_slots[i].effect_id = -1; + } + + SDL_HapticDirection direction = { + .type = SDL_HAPTIC_POLAR, + .dir = {27000, 0}}; + m_default_spring_effect.type = SDL_HAPTIC_SPRING; + m_default_spring_effect.condition.direction = direction; + m_default_spring_effect.condition.length = SDL_HAPTIC_INFINITY; + // for (int i = 0;i < 3;i++) + for (int i = 0; i < 1; i++) + { + m_default_spring_effect.condition.right_sat[i] = 0x7FFF; + m_default_spring_effect.condition.left_sat[i] = 0x7FFF; + m_default_spring_effect.condition.right_coeff[i] = 0x7FFF; + m_default_spring_effect.condition.left_coeff[i] = 0x7FFF; + } + + { + const std::lock_guard lock(m_thread_control_mutex); + m_stop_thread = false; + } + + g_cfg_logitech_g27.load(); + + bool sdl_init_state = sdl_instance::get_instance().initialize(); + + m_enabled = g_cfg_logitech_g27.enabled.get() && sdl_init_state; + + if (!m_enabled) + return; + + m_house_keeping_thread = std::thread([this]() + { + while (true) + { + this->m_thread_control_mutex.lock(); + if (this->m_stop_thread) + { + break; + } + this->m_thread_control_mutex.unlock(); + this->sdl_refresh(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + this->m_thread_control_mutex.unlock(); + }); +} + +bool usb_device_logitech_g27::open_device() +{ + return m_enabled; +} + +static void clear_sdl_joysticks(std::map>& joysticks) +{ + for (auto joystick_type : joysticks) + { + for (auto joystick : joystick_type.second) + { + if (joystick) + SDL_CloseJoystick(joystick); + } + } + joysticks.clear(); +} + +usb_device_logitech_g27::~usb_device_logitech_g27() +{ + // stop the house keeping thread + { + const std::lock_guard lock(m_thread_control_mutex); + m_stop_thread = true; + } + + // Close sdl handles + { + const std::lock_guard lock(m_sdl_handles_mutex); + if (m_haptic_handle != nullptr) + { + SDL_CloseHaptic(m_haptic_handle); + } + clear_sdl_joysticks(m_joysticks); + } + + // wait for the house keeping thread to finish + if (m_enabled) + m_house_keeping_thread.join(); +} + +std::shared_ptr usb_device_logitech_g27::make_instance(u32 controller_index, const std::array& location) +{ + return std::make_shared(controller_index, location); +} + +u16 usb_device_logitech_g27::get_num_emu_devices() +{ + return 1; +} + +void usb_device_logitech_g27::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) +{ + transfer->fake = true; + transfer->expected_count = buf_size; + transfer->expected_result = HC_CC_NOERR; + transfer->expected_time = get_timestamp() + 100; + + // Log these for now, might not need to implement anything + usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); +} + +static bool sdl_joysticks_equal(std::map>& left, std::map>& right) +{ + if (left.size() != right.size()) + { + return false; + } + for (auto left_joysticks_of_type : left) + { + auto right_joysticks_of_type = right.find(left_joysticks_of_type.first); + if (right_joysticks_of_type == right.end()) + { + return false; + } + if (left_joysticks_of_type.second.size() != right_joysticks_of_type->second.size()) + { + return false; + } + for (auto left_joystick : left_joysticks_of_type.second) + { + bool found = false; + for (auto right_joystick : right_joysticks_of_type->second) + { + if (left_joystick == right_joystick) + { + found = true; + break; + } + } + if (!found) + { + return false; + } + } + } + return true; +} + +static inline logitech_g27_sdl_mapping get_runtime_mapping() +{ + logitech_g27_sdl_mapping mapping; + +#define CONVERT_MAPPING(name) \ + { \ + mapping.name.device_type_id = g_cfg_logitech_g27.name.device_type_id.get(); \ + mapping.name.type = static_cast(g_cfg_logitech_g27.name.type.get()); \ + mapping.name.id = static_cast(g_cfg_logitech_g27.name.id.get()); \ + mapping.name.hat = static_cast(g_cfg_logitech_g27.name.hat.get()); \ + mapping.name.reverse = g_cfg_logitech_g27.name.reverse.get(); \ + mapping.name.positive_axis = false; \ + } + + CONVERT_MAPPING(steering); + CONVERT_MAPPING(throttle); + CONVERT_MAPPING(brake); + CONVERT_MAPPING(clutch); + CONVERT_MAPPING(shift_up); + CONVERT_MAPPING(shift_down); + + CONVERT_MAPPING(up); + CONVERT_MAPPING(down); + CONVERT_MAPPING(left); + CONVERT_MAPPING(right); + + CONVERT_MAPPING(triangle); + CONVERT_MAPPING(cross); + CONVERT_MAPPING(square); + CONVERT_MAPPING(circle); + + CONVERT_MAPPING(l2); + CONVERT_MAPPING(l3); + CONVERT_MAPPING(r2); + CONVERT_MAPPING(r3); + + CONVERT_MAPPING(plus); + CONVERT_MAPPING(minus); + + CONVERT_MAPPING(dial_clockwise); + CONVERT_MAPPING(dial_anticlockwise); + + CONVERT_MAPPING(select); + CONVERT_MAPPING(pause); + + CONVERT_MAPPING(shifter_1); + CONVERT_MAPPING(shifter_2); + CONVERT_MAPPING(shifter_3); + CONVERT_MAPPING(shifter_4); + CONVERT_MAPPING(shifter_5); + CONVERT_MAPPING(shifter_6); + CONVERT_MAPPING(shifter_r); + +#undef CONVERT_MAPPING + + return mapping; +} + +void usb_device_logitech_g27::sdl_refresh() +{ + g_cfg_logitech_g27.m_mutex.lock(); + m_mapping = get_runtime_mapping(); + + m_reverse_effects = g_cfg_logitech_g27.reverse_effects.get(); + + uint32_t ffb_vendor_id = g_cfg_logitech_g27.ffb_device_type_id.get() >> 16; + uint32_t ffb_product_id = g_cfg_logitech_g27.ffb_device_type_id.get() & 0xFFFF; + + uint32_t led_vendor_id = g_cfg_logitech_g27.led_device_type_id.get() >> 16; + uint32_t led_product_id = g_cfg_logitech_g27.led_device_type_id.get() & 0xFFFF; + g_cfg_logitech_g27.m_mutex.unlock(); + + SDL_Joystick* new_led_joystick_handle = nullptr; + SDL_Haptic* new_haptic_handle = nullptr; + std::map> new_joysticks; + + int joystick_count; + SDL_JoystickID* joystick_ids = SDL_GetJoysticks(&joystick_count); + if (joystick_ids != nullptr) + { + for (int i = 0; i < joystick_count; i++) + { + SDL_Joystick* cur_joystick = SDL_OpenJoystick(joystick_ids[i]); + if (cur_joystick == nullptr) + { + logitech_g27_log.error("Failed opening joystick %d, %s", joystick_ids[i], SDL_GetError()); + continue; + } + uint16_t cur_vendor_id = SDL_GetJoystickVendor(cur_joystick); + uint16_t cur_product_id = SDL_GetJoystickProduct(cur_joystick); + uint32_t joystick_type_id = (cur_vendor_id << 16) | cur_product_id; + auto joysticks_of_type = new_joysticks.find(joystick_type_id); + if (joysticks_of_type == new_joysticks.end()) + { + std::vector joystick_group = {cur_joystick}; + new_joysticks[joystick_type_id] = joystick_group; + } + else + { + joysticks_of_type->second.push_back(cur_joystick); + } + + if (cur_vendor_id == ffb_vendor_id && cur_product_id == ffb_product_id && new_haptic_handle == nullptr) + { + SDL_Haptic* cur_haptic = SDL_OpenHapticFromJoystick(cur_joystick); + if (cur_haptic == nullptr) + { + logitech_g27_log.error("Failed opening haptic device from selected ffb device %04x:%04x", cur_vendor_id, cur_product_id); + } + else + { + new_haptic_handle = cur_haptic; + } + } + + if (cur_vendor_id == led_vendor_id && cur_product_id == led_product_id && new_led_joystick_handle == nullptr) + { + new_led_joystick_handle = cur_joystick; + } + } + SDL_free(joystick_ids); + } + else + { + logitech_g27_log.error("Failed fetching joystick list, %s", SDL_GetError()); + } + + bool joysticks_changed = !sdl_joysticks_equal(m_joysticks, new_joysticks); + bool haptic_changed = m_haptic_handle != new_haptic_handle; + bool led_joystick_changed = m_led_joystick_handle != new_led_joystick_handle; + + // if we should touch the mutex + if (joysticks_changed || haptic_changed || led_joystick_changed) + { + const std::lock_guard lock(m_sdl_handles_mutex); + if (joysticks_changed) + { + clear_sdl_joysticks(m_joysticks); + m_joysticks = new_joysticks; + } + // reset effects if the ffb device is changed + if (haptic_changed) + { + if (m_haptic_handle) + SDL_CloseHaptic(m_haptic_handle); + for (int i = 0; i < 4; i++) + { + m_effect_slots[i].effect_id = -1; + } + m_default_spring_effect_id = -1; + m_led_joystick_handle = new_led_joystick_handle; + m_haptic_handle = new_haptic_handle; + } + if (led_joystick_changed) + { + m_led_joystick_handle = new_led_joystick_handle; + } + } + + if (!joysticks_changed) + { + clear_sdl_joysticks(new_joysticks); + } + + if (!haptic_changed) + { + if (new_haptic_handle) + SDL_CloseHaptic(new_haptic_handle); + } +} + +static inline int16_t logitech_g27_force_to_level(uint8_t force) +{ + if (force == 127 || force == 128) + { + return 0; + } + if (force > 128) + { + return ((force - 128) * 0x7FFF) / (255 - 128); + } + return ((127 - force) * 0x7FFF * -1) / (127 - 0); +} + +static inline int16_t logitech_g27_position_to_center(uint8_t left, uint8_t right) +{ + uint16_t center_unsigned = (((right + left) * 0xFFFF) / 255) / 2; + return center_unsigned - 0x8000; +} + +static inline int16_t logitech_g27_high_resolution_position_to_center(uint16_t left, uint16_t right) +{ + uint16_t center_unsigned = (((right + left) * 0xFFFF) / (0xFFFF >> 5)) / 2; + return center_unsigned - 0x8000; +} + +static inline uint16_t logitech_g27_position_to_width(uint8_t left, uint8_t right) +{ + return ((right - left) * 0xFFFF) / 255; +} + +static inline uint16_t logitech_g27_high_resolution_position_to_width(uint16_t left, uint16_t right) +{ + return ((right - left) * 0xFFFF) / (0xFFFF >> 5); +} + +static inline int16_t logitech_g27_coeff_to_coeff(uint8_t coeff, uint8_t invert) +{ + if (!invert) + { + return (coeff * 0x7FFF) / 7; + } + return (coeff * 0x7FFF * -1) / 7; +} + +static inline int16_t logitech_g27_high_resolution_coeff_to_coeff(uint8_t coeff, uint8_t invert) +{ + if (!invert) + { + return (coeff * 0x7FFF) / 15; + } + return (coeff * 0x7FFF * -1) / 15; +} + +static inline int16_t logitech_g27_friction_coeff_to_coeff(uint8_t coeff, uint8_t invert) +{ + if (!invert) + { + return (coeff * 0x7FFF) / 255; + } + return (coeff * 0x7FFF * -1) / 255; +} + +static inline int16_t logitech_g27_clip_to_saturation(uint8_t clip) +{ + return (clip * 0x7FFF) / 255; +} + +static inline int16_t logitech_g27_amplitude_to_magnitude(uint8_t amplitude) +{ + return ((amplitude * 0x7FFF) / 2) / 255; +} + +static inline uint16_t logitech_g27_loops_to_ms(uint16_t loops, bool afap) +{ + if (afap) + { + return loops; + } + return loops * 2; +} + +static inline uint16_t axis_to_logitech_g27_steering(int16_t axis) +{ + uint16_t unsigned_axis = axis + 0x8000; + return (unsigned_axis * (0xFFFF >> 2)) / 0xFFFF; +} + +static inline uint8_t axis_to_logitech_g27_pedal(int16_t axis) +{ + uint16_t unsigned_axis = axis + 0x8000; + return (unsigned_axis * (0xFF)) / 0xFFFF; +} + +extern bool is_input_allowed(); + +static uint8_t sdl_hat_to_logitech_g27_hat(uint8_t sdl_hat) +{ + switch (sdl_hat) + { + case SDL_HAT_CENTERED: + return 8; + case SDL_HAT_UP: + return 0; + case SDL_HAT_RIGHTUP: + return 1; + case SDL_HAT_RIGHT: + return 2; + case SDL_HAT_RIGHTDOWN: + return 3; + case SDL_HAT_DOWN: + return 4; + case SDL_HAT_LEFTDOWN: + return 5; + case SDL_HAT_LEFT: + return 6; + case SDL_HAT_LEFTUP: + return 7; + } + return 0; +} + +static uint8_t hat_components_to_logitech_g27_hat(bool up, bool down, bool left, bool right) +{ + uint8_t sdl_hat = 0; + if (up) + sdl_hat = sdl_hat | SDL_HAT_UP; + if (down) + sdl_hat = sdl_hat | SDL_HAT_DOWN; + if (left) + sdl_hat = sdl_hat | SDL_HAT_LEFT; + if (right) + sdl_hat = sdl_hat | SDL_HAT_RIGHT; + return sdl_hat_to_logitech_g27_hat(sdl_hat); +} + +static bool fetch_sdl_as_button(SDL_Joystick* joystick, const sdl_mapping& mapping) +{ + switch (mapping.type) + { + case MAPPING_BUTTON: + { + bool pressed = SDL_GetJoystickButton(joystick, mapping.id); + return mapping.reverse ? !pressed : pressed; + } + case MAPPING_HAT: + { + uint8_t hat_value = SDL_GetJoystickHat(joystick, mapping.id); + bool pressed = false; + switch (mapping.hat) + { + case HAT_UP: + pressed = (hat_value & SDL_HAT_UP) ? true : false; + break; + case HAT_DOWN: + pressed = (hat_value & SDL_HAT_DOWN) ? true : false; + break; + case HAT_LEFT: + pressed = (hat_value & SDL_HAT_LEFT) ? true : false; + break; + case HAT_RIGHT: + pressed = (hat_value & SDL_HAT_RIGHT) ? true : false; + break; + case HAT_NONE: + break; + } + return mapping.reverse ? !pressed : pressed; + } + case MAPPING_AXIS: + { + int32_t axis_value = SDL_GetJoystickAxis(joystick, mapping.id); + bool pressed = false; + if (mapping.positive_axis) + { + pressed = axis_value > (0x7FFF / 2); + } + else + { + pressed = axis_value < (0x7FFF / (-2)); + } + return mapping.reverse ? !pressed : pressed; + } + } + return false; +} + +static int16_t fetch_sdl_as_axis(SDL_Joystick* joystick, const sdl_mapping& mapping) +{ + const static int16_t MAX = 0x7FFF; + const static int16_t MIN = -0x8000; + const static int16_t MID = 0; + + switch (mapping.type) + { + case MAPPING_BUTTON: + { + bool pressed = SDL_GetJoystickButton(joystick, mapping.id); + if (mapping.reverse) + { + pressed = !pressed; + } + int16_t pressed_value = mapping.positive_axis ? MAX : MIN; + return pressed ? pressed_value : MID; + } + case MAPPING_HAT: + { + uint8_t hat_value = SDL_GetJoystickHat(joystick, mapping.id); + bool pressed = false; + switch (mapping.hat) + { + case HAT_UP: + pressed = (hat_value & SDL_HAT_UP) ? true : false; + break; + case HAT_DOWN: + pressed = (hat_value & SDL_HAT_DOWN) ? true : false; + break; + case HAT_LEFT: + pressed = (hat_value & SDL_HAT_LEFT) ? true : false; + break; + case HAT_RIGHT: + pressed = (hat_value & SDL_HAT_RIGHT) ? true : false; + break; + case HAT_NONE: + break; + } + if (mapping.reverse) + { + pressed = !pressed; + } + int16_t pressed_value = mapping.positive_axis ? MAX : MIN; + return pressed ? pressed_value : MID; + } + case MAPPING_AXIS: + { + int32_t axis_value = SDL_GetJoystickAxis(joystick, mapping.id); + if (mapping.reverse) + axis_value = axis_value * (-1); + if (axis_value > MAX) + axis_value = MAX; + if (axis_value < MIN) + axis_value = MIN; + if (axis_value == (MIN + 1)) + axis_value = MIN; + return axis_value; + } + } + return 0; +} + +static int16_t fetch_sdl_axis_avg(std::map>& joysticks, const sdl_mapping& mapping) +{ + const static int16_t MAX = 0x7FFF; + const static int16_t MIN = -0x8000; + + auto joysticks_of_type = joysticks.find(mapping.device_type_id); + if (joysticks_of_type == joysticks.end()) + { + return mapping.reverse ? MAX : MIN; + } + + if (joysticks_of_type->second.size() == 0) + { + return mapping.reverse ? MAX : MIN; + } + + // TODO account for deadzone and only pick up active devices + int32_t sdl_joysticks_total_value = 0; + for (auto joystick : joysticks_of_type->second) + { + sdl_joysticks_total_value += fetch_sdl_as_axis(joystick, mapping); + } + + return sdl_joysticks_total_value / joysticks_of_type->second.size(); +} + +static bool sdl_to_logitech_g27_button(std::map>& joysticks, const sdl_mapping& mapping) +{ + auto joysticks_of_type = joysticks.find(mapping.device_type_id); + if (joysticks_of_type == joysticks.end()) + { + return mapping.reverse; + } + + if (joysticks_of_type->second.size() == 0) + { + return mapping.reverse; + } + + bool pressed = false; + for (auto joystick : joysticks_of_type->second) + { + pressed = pressed || fetch_sdl_as_button(joystick, mapping); + } + return pressed; +} + +static uint16_t sdl_to_logitech_g27_steering(std::map>& joysticks, const sdl_mapping& mapping) +{ + int16_t avg = fetch_sdl_axis_avg(joysticks, mapping); + uint16_t unsigned_avg = avg + 0x8000; + return unsigned_avg * (0xFFFF >> 2) / 0xFFFF; +} + +static uint8_t sdl_to_logitech_g27_pedal(std::map>& joysticks, const sdl_mapping& mapping) +{ + int16_t avg = fetch_sdl_axis_avg(joysticks, mapping); + uint16_t unsigned_avg = avg + 0x8000; + return unsigned_avg * 0xFF / 0xFFFF; +} + +static inline void set_bit(uint8_t* buf, int bit_num, bool set) +{ + int byte_num = bit_num / 8; + bit_num = bit_num % 8; + uint8_t mask = 1 << bit_num; + if (set) + buf[byte_num] = buf[byte_num] | mask; + else + buf[byte_num] = buf[byte_num] & (~mask); +} + +void usb_device_logitech_g27::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) +{ + transfer->fake = true; + transfer->expected_result = HC_CC_NOERR; + // G29 in G27 mode polls at 500 hz, let's try a delay of 1ms for now, for wheels that updates that fast + transfer->expected_time = get_timestamp() + 1000; + + if (endpoint & (1 << 7)) + { + if (buf_size < 11) + { + logitech_g27_log.error("Not populating input buffer with a buffer of the size of %u", buf_size); + return; + } + ensure(buf_size >= 11); + memset(buf, 0, buf_size); + + transfer->expected_count = 11; + + sdl_instance::get_instance().pump_events(); + + // Fetch input states from SDL + m_sdl_handles_mutex.lock(); + uint16_t steering = sdl_to_logitech_g27_steering(m_joysticks, m_mapping.steering); + uint8_t throttle = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.throttle); + uint8_t brake = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.brake); + uint8_t clutch = sdl_to_logitech_g27_pedal(m_joysticks, m_mapping.clutch); + bool shift_up = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_up); + bool shift_down = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shift_down); + + bool up = sdl_to_logitech_g27_button(m_joysticks, m_mapping.up); + bool down = sdl_to_logitech_g27_button(m_joysticks, m_mapping.down); + bool left = sdl_to_logitech_g27_button(m_joysticks, m_mapping.left); + bool right = sdl_to_logitech_g27_button(m_joysticks, m_mapping.right); + + bool triangle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.triangle); + bool cross = sdl_to_logitech_g27_button(m_joysticks, m_mapping.cross); + bool square = sdl_to_logitech_g27_button(m_joysticks, m_mapping.square); + bool circle = sdl_to_logitech_g27_button(m_joysticks, m_mapping.circle); + + bool l2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l2); + bool l3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.l3); + bool r2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r2); + bool r3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.r3); + + bool plus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.plus); + bool minus = sdl_to_logitech_g27_button(m_joysticks, m_mapping.minus); + + bool dial_clockwise = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_clockwise); + bool dial_anticlockwise = sdl_to_logitech_g27_button(m_joysticks, m_mapping.dial_anticlockwise); + + bool select = sdl_to_logitech_g27_button(m_joysticks, m_mapping.select); + bool pause = sdl_to_logitech_g27_button(m_joysticks, m_mapping.pause); + + bool shifter_1 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_1); + bool shifter_2 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_2); + bool shifter_3 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_3); + bool shifter_4 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_4); + bool shifter_5 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_5); + bool shifter_6 = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_6); + bool shifter_r = sdl_to_logitech_g27_button(m_joysticks, m_mapping.shifter_r); + m_sdl_handles_mutex.unlock(); + + // populate buffer + buf[0] = hat_components_to_logitech_g27_hat(up, down, left, right); + set_bit(buf, 8, shift_up); + set_bit(buf, 9, shift_down); + + set_bit(buf, 7, triangle); + set_bit(buf, 4, cross); + set_bit(buf, 5, square); + set_bit(buf, 6, circle); + + set_bit(buf, 11, l2); + set_bit(buf, 15, l3); + set_bit(buf, 10, r2); + set_bit(buf, 14, r3); + + set_bit(buf, 22, dial_clockwise); + set_bit(buf, 23, dial_anticlockwise); + + set_bit(buf, 24, plus); + set_bit(buf, 25, minus); + + set_bit(buf, 12, select); + set_bit(buf, 13, pause); + + set_bit(buf, 16, shifter_1); + set_bit(buf, 17, shifter_2); + set_bit(buf, 18, shifter_3); + set_bit(buf, 19, shifter_4); + set_bit(buf, 20, shifter_5); + set_bit(buf, 21, shifter_6); + set_bit(buf, 80, shifter_r); + + // calibrated, unsure + set_bit(buf, 82, true); + // shifter connected + set_bit(buf, 83, true); + // shifter stick down + set_bit(buf, 86, shifter_1 || shifter_2 || shifter_3 || shifter_4 || shifter_5 || shifter_6 || shifter_r); + + buf[3] = (steering << 2) | buf[3]; + buf[4] = steering >> 6; + buf[5] = throttle; + buf[6] = brake; + buf[7] = clutch; + + buf[8] = 0x80; // shifter x, don't own one to test gear/coord mapping + buf[9] = 0x80; // shifter y + buf[10] = buf[10] | (m_wheel_range > 360 ? 0x90 : 0x10); + + // logitech_g27_log.error("%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10]); + + return; + } + else + { + // Sending data to wheel + if (buf_size < 7) + { + char* hex_buf = reinterpret_cast(malloc(buf_size * 3 + 1)); + if (hex_buf == nullptr) + { + logitech_g27_log.error("Unhandled wheel command with size %u != 16", buf_size); + return; + } + int offset = 0; + for (uint32_t i = 0; i < buf_size; i++) + { + offset += sprintf(&hex_buf[offset], "%02x ", buf[i]); + } + logitech_g27_log.error("Unhandled wheel command with size %u != 16, %s", buf_size, hex_buf); + free(hex_buf); + return; + } + + transfer->expected_count = buf_size; + + // logitech_g27_log.error("%02x %02x %02x %02x %02x %02x %02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + // printf("%02x %02x %02x %02x %02x %02x %02x\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + + SDL_HapticDirection direction = { + .type = SDL_HAPTIC_POLAR, + .dir = {27000, 0}}; + if (m_reverse_effects) + { + direction.dir[0] = 9000; + } + + // TODO maybe force clipping from cfg + + // Process effects + if (buf[0] == 0xf8) + { + const std::lock_guard lock(m_sdl_handles_mutex); + switch (buf[1]) + { + case 0x01: + { + // Change to DFP + logitech_g27_log.error("Drive Force Pro mode switch command ignored"); + break; + } + case 0x02: + { + // Change wheel range to 200 degrees + logitech_g27_log.error("Change wheel range to 200 degrees command not forwarded"); + m_wheel_range = 200; + break; + } + case 0x03: + { + // Change wheel range to 900 degrees + logitech_g27_log.error("Change wheel range to 900 degrees command not forwarded"); + m_wheel_range = 900; + break; + } + case 0x09: + { + // Change device mode + logitech_g27_log.error("Change device mode to %d %s detaching command ignored", buf[2], buf[3] ? "with" : "without"); + break; + } + case 0x0a: + { + // Revert indentity + logitech_g27_log.error("Revert device identity after reset %s command ignored", buf[2] ? "enable" : "disable"); + break; + } + case 0x10: + { + // Switch to G25 with detach + logitech_g27_log.error("Switch to G25 with detach command ignored"); + break; + } + case 0x11: + { + // Switch to G25 without detach + logitech_g27_log.error("Switch to G25 without detach command ignored"); + break; + } + case 0x12: + { + // Incoming data is a 5 bit mask, for each individual bulb + if (m_led_joystick_handle == nullptr) + { + break; + } + // Mux into total amount of bulbs on, since sdl only takes intensity + uint8_t new_led_level = 0; + for (int i = 0; i < 5; i++) + { + new_led_level += (buf[2] & (1 << i)) ? 1 : 0; + } + + uint8_t intensity = new_led_level * 255 / 5; + SDL_SetJoystickLED(m_led_joystick_handle, intensity, intensity, intensity); + break; + } + case 0x81: + { + // Wheel range change + m_wheel_range = (buf[3] << 8) | buf[2]; + logitech_g27_log.error("Wheel range change to %u command not forwarded", m_wheel_range); + break; + } + default: + { + logitech_g27_log.error("Unknown extended command %02x %02x %02x %02x %02x %02x %02x ignored", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + break; + } + } + } + else + { + const std::lock_guard lock(m_sdl_handles_mutex); + uint8_t cmd = buf[0] & 0xf; + uint8_t slot_mask = buf[0] >> 4; + switch (cmd) + { + case 0x00: + case 0x01: + case 0x0c: + { + // Download/Download play/Refresh + for (int i = 0; i < 4; i++) + { + SDL_HapticEffect new_effect = {0}; + // hack: need to reduce Download play spams for some drivers + bool update_hack = false; + if (!(slot_mask & (1 << i))) + { + continue; + } + bool unknown_effect = false; + switch (buf[1]) + { + case 0x00: + { + // Constant force + new_effect.type = SDL_HAPTIC_CONSTANT; + new_effect.constant.direction = direction; + new_effect.constant.length = SDL_HAPTIC_INFINITY; + new_effect.constant.level = logitech_g27_force_to_level(buf[2 + i]); + break; + } + case 0x01: + case 0x0b: + { + // Spring/High resolution spring + new_effect.type = SDL_HAPTIC_SPRING; + new_effect.condition.direction = direction; + new_effect.condition.length = SDL_HAPTIC_INFINITY; + uint8_t s1 = buf[5] & 1; + uint8_t s2 = (buf[5] >> 4) & 1; + // TODO direction cfg + uint16_t saturation = logitech_g27_clip_to_saturation(buf[6]); + int16_t center = 0; + uint16_t deadband = 0; + int16_t left_coeff = 0; + int16_t right_coeff = 0; + if (buf[1] == 0x01) + { + uint8_t d1 = buf[2]; + uint8_t d2 = buf[3]; + uint8_t k1 = buf[4] & (0xf >> 1); + uint8_t k2 = (buf[4] >> 4) & (0xf >> 1); + center = logitech_g27_position_to_center(d1, d2); + deadband = logitech_g27_position_to_width(d1, d2); + left_coeff = logitech_g27_coeff_to_coeff(k1, s1); + right_coeff = logitech_g27_coeff_to_coeff(k2, s2); + } + else + { + uint16_t d1 = (buf[2] << 3) | ((buf[5] >> 1) & (0xf >> 1)); + uint16_t d2 = (buf[3] << 3) | (buf[5] >> 5); + uint8_t k1 = buf[4] & 0xf; + uint8_t k2 = buf[4] >> 4; + center = logitech_g27_high_resolution_position_to_center(d1, d2); + deadband = logitech_g27_high_resolution_position_to_width(d1, d2); + left_coeff = logitech_g27_high_resolution_coeff_to_coeff(k1, s1); + right_coeff = logitech_g27_high_resolution_coeff_to_coeff(k2, s2); + } + if (m_reverse_effects) + { + int16_t coeff = right_coeff; + right_coeff = left_coeff; + left_coeff = coeff; + } + // for(int j = 0;j < 3;j++) + for (int j = 0; j < 1; j++) + { + new_effect.condition.right_sat[j] = saturation; + new_effect.condition.left_sat[j] = saturation; + new_effect.condition.right_coeff[j] = right_coeff; + new_effect.condition.left_coeff[j] = left_coeff; + new_effect.condition.deadband[j] = deadband; + new_effect.condition.center[j] = center; + } + break; + } + case 0x02: + case 0x0c: + { + // Damper/High resolution damper + new_effect.type = SDL_HAPTIC_DAMPER; + new_effect.condition.direction = direction; + new_effect.condition.length = SDL_HAPTIC_INFINITY; + uint8_t s1 = buf[3] & 1; + uint8_t s2 = buf[5] & 1; + // TODO direction cfg + uint16_t saturation = 0x7FFF; + int16_t left_coeff = 0; + int16_t right_coeff = 0; + if (buf[1] == 0x02) + { + uint8_t k1 = buf[2] & (0xf >> 1); + uint8_t k2 = buf[4] & (0xf >> 1); + left_coeff = logitech_g27_coeff_to_coeff(k1, s1); + right_coeff = logitech_g27_coeff_to_coeff(k2, s2); + } + else + { + uint8_t k1 = buf[2] & 0xf; + uint8_t k2 = buf[4] & 0xf; + left_coeff = logitech_g27_high_resolution_coeff_to_coeff(k1, s1); + right_coeff = logitech_g27_high_resolution_coeff_to_coeff(k2, s2); + saturation = logitech_g27_clip_to_saturation(buf[6]); + } + if (m_reverse_effects) + { + int16_t coeff = right_coeff; + right_coeff = left_coeff; + left_coeff = coeff; + } + // for(int j = 0;j < 3;j++) + for (int j = 0; j < 1; j++) + { + new_effect.condition.right_sat[j] = saturation; + new_effect.condition.left_sat[j] = saturation; + new_effect.condition.right_coeff[j] = right_coeff; + new_effect.condition.left_coeff[j] = left_coeff; + } + break; + } + case 0x0e: + { + // Friction + new_effect.type = SDL_HAPTIC_FRICTION; + new_effect.condition.direction = direction; + new_effect.condition.length = SDL_HAPTIC_INFINITY; + uint8_t k1 = buf[2]; + uint8_t k2 = buf[3]; + uint8_t s1 = buf[5] & 1; + uint8_t s2 = (buf[5] >> 4) & 1; + // TODO direction cfg + int16_t left_coeff = logitech_g27_friction_coeff_to_coeff(k1, s1); + int16_t right_coeff = logitech_g27_friction_coeff_to_coeff(k2, s2); + int16_t saturation = logitech_g27_clip_to_saturation(buf[4]); + if (m_reverse_effects) + { + int16_t coeff = right_coeff; + right_coeff = left_coeff; + left_coeff = coeff; + } + // for(int j = 0;j < 3;j++) + for (int j = 0; j < 1; j++) + { + new_effect.condition.right_sat[j] = saturation; + new_effect.condition.left_sat[j] = saturation; + new_effect.condition.right_coeff[j] = right_coeff; + new_effect.condition.left_coeff[j] = left_coeff; + } + break; + } + case 0x03: + case 0x0d: + { + // Auto center spring/High resolution auto center spring + new_effect.type = SDL_HAPTIC_SPRING; + new_effect.condition.direction = direction; + new_effect.condition.length = SDL_HAPTIC_INFINITY; + // TODO direction cfg + uint16_t saturation = logitech_g27_clip_to_saturation(buf[4]); + uint16_t deadband = 2 * 0xFFFF / 255; + int16_t center = 0; + int16_t left_coeff = 0; + int16_t right_coeff = 0; + if (buf[1] == 0x03) + { + uint8_t k1 = buf[2] & (0xf >> 1); + uint8_t k2 = buf[3] & (0xf >> 1); + left_coeff = logitech_g27_coeff_to_coeff(k1, 0); + right_coeff = logitech_g27_coeff_to_coeff(k2, 0); + } + else + { + uint8_t k1 = buf[2] & 0xf; + uint8_t k2 = buf[3] & 0xf; + left_coeff = logitech_g27_high_resolution_coeff_to_coeff(k1, 0); + right_coeff = logitech_g27_high_resolution_coeff_to_coeff(k2, 0); + } + if (m_reverse_effects) + { + int16_t coeff = right_coeff; + right_coeff = left_coeff; + left_coeff = coeff; + } + // for(int j = 0;j < 3;j++) + for (int j = 0; j < 1; j++) + { + new_effect.condition.right_sat[j] = saturation; + new_effect.condition.left_sat[j] = saturation; + new_effect.condition.right_coeff[j] = right_coeff; + new_effect.condition.left_coeff[j] = left_coeff; + new_effect.condition.deadband[j] = deadband; + new_effect.condition.center[j] = center; + } + break; + } + case 0x04: + case 0x05: + { + // Sawtooth up/Sawtooth down + new_effect.type = buf[1] == 0x04 ? SDL_HAPTIC_SAWTOOTHUP : SDL_HAPTIC_SAWTOOTHDOWN; + new_effect.periodic.direction = direction; + new_effect.periodic.length = SDL_HAPTIC_INFINITY; + uint8_t l1 = buf[2]; + uint8_t l2 = buf[3]; + uint8_t l0 = buf[4]; + uint8_t t3 = buf[6] >> 4; + uint8_t inc = buf[6] & 0xf; + if (inc != 0) + new_effect.periodic.period = ((l1 - l2) * logitech_g27_loops_to_ms(t3, !m_fixed_loop)) / inc; + else + { + logitech_g27_log.error("cannot evaluate slope for saw tooth effect, loops per step %u level per step %u", t3, inc); + new_effect.periodic.period = 1000; + } + new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2); + new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset; + new_effect.periodic.phase = buf[1] == 0x04 ? 36000 * (l1 - l0) / (l1 - l2) : 36000 * (l0 - l2) / (l1 - l2); + break; + } + case 0x06: + { + // Trapezoid, convert to SDL_HAPTIC_SQUARE or SDL_HAPTIC_TRIANGLE + new_effect.periodic.direction = direction; + new_effect.periodic.length = SDL_HAPTIC_INFINITY; + uint8_t l1 = buf[2]; + uint8_t l2 = buf[3]; + uint8_t t1 = buf[4]; + uint8_t t2 = buf[5]; + uint8_t t3 = buf[6] >> 4; + uint8_t s = buf[6] & 0xf; + uint16_t total_flat_time = logitech_g27_loops_to_ms(t1 + t2, !m_fixed_loop); + uint16_t total_slope_time = (((l1 - l2) * logitech_g27_loops_to_ms(t3, !m_fixed_loop)) / s) * 2; + if (total_flat_time > total_slope_time) + { + new_effect.type = SDL_HAPTIC_SQUARE; + } + else + { + new_effect.type = SDL_HAPTIC_TRIANGLE; + } + new_effect.periodic.period = total_slope_time + total_flat_time; + new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2); + new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset; + break; + } + case 0x07: + { + // Rectangle, convert to SDL_HAPTIC_SQUARE + new_effect.type = SDL_HAPTIC_SQUARE; + new_effect.periodic.direction = direction; + new_effect.periodic.length = SDL_HAPTIC_INFINITY; + uint8_t l1 = buf[2]; + uint8_t l2 = buf[3]; + uint8_t t1 = buf[4]; + uint8_t t2 = buf[5]; + uint8_t p = buf[6]; + new_effect.periodic.period = logitech_g27_loops_to_ms(t1, !m_fixed_loop) + logitech_g27_loops_to_ms(t2, !m_fixed_loop); + new_effect.periodic.offset = logitech_g27_force_to_level((l1 + l2) / 2); + new_effect.periodic.magnitude = logitech_g27_force_to_level(l1) - new_effect.periodic.offset; + if (new_effect.periodic.period != 0) + new_effect.periodic.phase = 36000 * logitech_g27_loops_to_ms(p, !m_fixed_loop) / new_effect.periodic.period; + else + { + logitech_g27_log.error("cannot evaluate phase for square effect"); + new_effect.periodic.phase = 0; + } + break; + } + case 0x08: + case 0x09: + { + // Variable/Ramp, convert to SDL_HAPTIC_CONSTANT + if (i % 2 != 0) + { + continue; + } + new_effect.type = SDL_HAPTIC_CONSTANT; + new_effect.constant.direction = direction; + uint8_t l1 = buf[2]; + uint8_t l2 = buf[3]; + uint8_t t1 = buf[4] >> 4; + uint8_t s1 = buf[4] & 0xf; + uint8_t t2 = buf[5] >> 4; + uint8_t s2 = buf[5] & 0xf; + uint8_t d1 = buf[6] & 1; + uint8_t d2 = (buf[6] >> 4) & 1; + if (buf[1] == 0x08) + { + uint8_t t = i == 0 ? t1 : t2; + uint8_t s = i == 0 ? s1 : s2; + uint8_t d = i == 0 ? d1 : d2; + uint8_t l = i == 0 ? l1 : l2; + new_effect.constant.length = SDL_HAPTIC_INFINITY; + if (s == 0 || t == 0) + { + // gran turismo 6 does this, gives a variable force with no step so it just behaves as constant force + new_effect.constant.level = logitech_g27_force_to_level(l); + // hack: gran turismo 6 spams download and play + update_hack = true; + } + else + { + new_effect.constant.attack_level = logitech_g27_force_to_level(l); + if (d) + { + new_effect.constant.level = 0; + new_effect.constant.attack_length = l * logitech_g27_loops_to_ms(t, !m_fixed_loop) / s; + } + else + { + new_effect.constant.level = 0x7FFF; + new_effect.constant.attack_length = (255 - l) * logitech_g27_loops_to_ms(t, !m_fixed_loop) / s; + } + } + } + else + { + if (s2 == 0 || t2 == 0) + { + logitech_g27_log.error("cannot evaluate slope for ramp effect, loops per step %u level per step %u", t2, s2); + } + else + { + new_effect.constant.length = (l1 - l2) * logitech_g27_loops_to_ms(t2, !m_fixed_loop) / s2; + new_effect.constant.attack_length = new_effect.constant.length; + new_effect.constant.attack_level = d1 ? logitech_g27_force_to_level(l1) : logitech_g27_force_to_level(l2); + } + new_effect.constant.level = d1 ? logitech_g27_force_to_level(l2) : logitech_g27_force_to_level(l1); + } + break; + } + case 0x0a: + { + // Square + new_effect.type = SDL_HAPTIC_SQUARE; + new_effect.periodic.direction = direction; + uint8_t a = buf[2]; + uint8_t tl = buf[3]; + uint8_t th = buf[4]; + uint8_t n = buf[5]; + uint16_t t = (th << 8) | tl; + new_effect.periodic.period = logitech_g27_loops_to_ms(t * 2, !m_fixed_loop); + new_effect.periodic.magnitude = logitech_g27_amplitude_to_magnitude(a); + if (n == 0) + new_effect.periodic.length = new_effect.periodic.period * 256; + else + new_effect.periodic.length = new_effect.periodic.period * n; + break; + } + default: + { + unknown_effect = true; + } + } + + if (unknown_effect) + { + logitech_g27_log.error("Command %02x %02x %02x %02x %02x %02x %02x with unknown effect ignored", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + continue; + } + + bool play_effect = (cmd == 0x01 || (cmd == 0x0c && m_effect_slots[i].effect_id == -1)); + + if (update_hack) + { + if (m_effect_slots[i].effect_id == -1) + update_hack = false; + if (m_effect_slots[i].last_effect.type != new_effect.type) + update_hack = false; + } + + if (cmd == 0x00 || play_effect) + { + if (m_effect_slots[i].effect_id != -1 && m_haptic_handle != nullptr && !update_hack) + { + SDL_DestroyHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id); + m_effect_slots[i].effect_id = -1; + } + if (m_haptic_handle != nullptr && m_effect_slots[i].effect_id == -1) + { + m_effect_slots[i].effect_id = SDL_CreateHapticEffect(m_haptic_handle, &new_effect); + } + if (update_hack) + { + if (!SDL_UpdateHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, &new_effect)) + logitech_g27_log.error("Failed refreshing slot %d sdl effect %d, %s", i, new_effect.type, SDL_GetError()); + } + m_effect_slots[i].state = G27_FFB_DOWNLOADED; + m_effect_slots[i].last_effect = new_effect; + m_effect_slots[i].last_update = SDL_GetTicks(); + if (m_effect_slots[i].effect_id == -1 && m_haptic_handle != nullptr) + { + logitech_g27_log.error("Failed uploading effect %02x %02x %02x %02x %02x %02x %02x to slot %i, %s", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], i, SDL_GetError()); + } + } + if (play_effect && m_haptic_handle != nullptr) + { + if (m_effect_slots[i].effect_id != -1) + { + if (!SDL_RunHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, 1)) + { + logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError()); + } + } + else + { + logitech_g27_log.error("Tried to play effect slot %d with sdl effect %d, but upload failed previously", i, m_effect_slots[i].last_effect.type); + } + m_effect_slots[i].state = G27_FFB_PLAYING; + } + if (cmd == 0xc && !play_effect && m_haptic_handle != nullptr) + { + if (!SDL_UpdateHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, &new_effect)) + { + logitech_g27_log.error("Failed refreshing slot %d sdl effect %d, %s", i, new_effect.type, SDL_GetError()); + } + } + } + break; + } + case 0x02: + case 0x03: + { + for (int i = 0; i < 4; i++) + { + // Play/Stop + if (!(slot_mask & (1 << i))) + { + continue; + } + if (m_effect_slots[i].state == G27_FFB_PLAYING || m_effect_slots[i].state == G27_FFB_DOWNLOADED) + { + m_effect_slots[i].state = cmd == 0x02 ? G27_FFB_PLAYING : G27_FFB_DOWNLOADED; + if (m_haptic_handle != nullptr) + { + if (m_effect_slots[i].effect_id == -1) + { + m_effect_slots[i].effect_id = SDL_CreateHapticEffect(m_haptic_handle, &m_effect_slots[i].last_effect); + } + if (m_effect_slots[i].effect_id != -1) + { + if (cmd == 0x02) + { + if (!SDL_RunHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id, 1)) + { + logitech_g27_log.error("Failed playing sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError()); + } + } + else + { + if (!SDL_StopHapticEffect(m_haptic_handle, m_effect_slots[i].effect_id)) + { + logitech_g27_log.error("Failed stopping sdl effect %d on slot %d, %s\n", m_effect_slots[i].last_effect.type, i, SDL_GetError()); + } + } + } + else + { + if (cmd == 0x02) + { + logitech_g27_log.error("Tried to play effect slot %d with sdl effect %d, but upload failed previously", i, m_effect_slots[i].last_effect.type); + } + else + { + logitech_g27_log.error("Tried to stop effect slot %d with sdl effect %d, but upload failed previously", i, m_effect_slots[i].last_effect.type); + } + } + } + } + else + { + if (cmd == 0x02) + { + logitech_g27_log.error("Tried to play effect slot %d but it was never uploaded\n", i); + } + else + { + logitech_g27_log.error("Tried to stop effect slot %d but it was never uploaded\n", i); + } + } + } + break; + } + case 0x0e: + { + // Set Default Spring + uint8_t k1 = buf[2] & (0xf >> 1); + uint8_t k2 = buf[3] & (0xf >> 1); + uint16_t saturation = logitech_g27_clip_to_saturation(buf[4]); + int16_t left_coeff = logitech_g27_coeff_to_coeff(k1, 0); + int16_t right_coeff = logitech_g27_coeff_to_coeff(k2, 0); + uint16_t deadband = 2 * 0xFFFF / 255; + int16_t center = 0; + if (m_reverse_effects) + { + int16_t coeff = right_coeff; + right_coeff = left_coeff; + left_coeff = coeff; + } + // for (int i = 0;i < 3;i++){ + for (int i = 0; i < 1; i++) + { + // TODO direction cfg + m_default_spring_effect.condition.right_sat[i] = saturation; + m_default_spring_effect.condition.left_sat[i] = saturation; + m_default_spring_effect.condition.right_coeff[i] = right_coeff; + m_default_spring_effect.condition.left_coeff[i] = left_coeff; + m_default_spring_effect.condition.deadband[i] = deadband; + m_default_spring_effect.condition.center[i] = center; + } + + if (m_haptic_handle == nullptr) + { + break; + } + + if (m_default_spring_effect_id == -1) + { + m_default_spring_effect_id = SDL_CreateHapticEffect(m_haptic_handle, &m_default_spring_effect); + if (m_default_spring_effect_id == -1) + { + logitech_g27_log.error("Failed creating default spring effect, %s", SDL_GetError()); + } + } + else + { + if (!SDL_UpdateHapticEffect(m_haptic_handle, m_default_spring_effect_id, &m_default_spring_effect)) + { + logitech_g27_log.error("Failed updating default spring effect, %s", SDL_GetError()); + } + } + break; + } + case 0x04: + case 0x05: + { + // Default spring on/Default spring off + if (m_haptic_handle == nullptr) + { + break; + } + + if (m_default_spring_effect_id == -1) + { + m_default_spring_effect_id = SDL_CreateHapticEffect(m_haptic_handle, &m_default_spring_effect); + if (m_default_spring_effect_id == -1) + { + logitech_g27_log.error("Failed creating default spring effect, %s", SDL_GetError()); + } + } + if (m_default_spring_effect_id != -1) + { + if (cmd == 0x04) + { + if (!SDL_RunHapticEffect(m_haptic_handle, m_default_spring_effect_id, 1)) + { + logitech_g27_log.error("Failed playing default spring effect, %s", SDL_GetError()); + } + } + else + { + if (!SDL_StopHapticEffect(m_haptic_handle, m_default_spring_effect_id)) + { + logitech_g27_log.error("Failed stopping default spring effect, %s", SDL_GetError()); + } + } + } + break; + } + case 0x08: + { + // Normal Mode / Extended + logitech_g27_log.error("Normal mode restore command ignored"); + break; + } + case 0x09: + { + // Set LED + if (m_led_joystick_handle == nullptr) + { + break; + } + + uint8_t new_led_level = 0; + for (int i = 0; i < 8; i++) + { + new_led_level += (buf[1] & (1 << i)) ? 1 : 0; + } + uint8_t intensity = new_led_level * 255 / 7; + SDL_SetJoystickLED(m_led_joystick_handle, intensity, intensity, intensity); + break; + } + case 0x0a: + { + // Set watchdog + logitech_g27_log.error("Watchdog command with duration of %u loops ignored", buf[1]); + break; + } + case 0x0b: + { + // Raw mode + logitech_g27_log.error("Raw mode command ignored"); + break; + } + case 0x0d: + { + // Fixed time loop toggle + m_fixed_loop = buf[1] ? true : false; + if (!m_fixed_loop) + { + logitech_g27_log.error("as fast as possible mode requested, effect durations might be inaccurate"); + } + break; + } + case 0x0f: + { + // Set dead band + logitech_g27_log.error("Set dead band command ignored"); + break; + } + default: + { + logitech_g27_log.error("Unknown command %02x %02x %02x %02x %02x %02x %02x ignored", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + } + } + } + } +} + +#endif diff --git a/rpcs3/Emu/Io/LogitechG27.h b/rpcs3/Emu/Io/LogitechG27.h new file mode 100644 index 0000000000..72979d0316 --- /dev/null +++ b/rpcs3/Emu/Io/LogitechG27.h @@ -0,0 +1,123 @@ +#pragma once + +#include "Emu/Io/usb_device.h" +#include "LogitechG27Config.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include +#include +#include + +enum logitech_g27_ffb_state +{ + G27_FFB_INACTIVE, + G27_FFB_DOWNLOADED, + G27_FFB_PLAYING +}; + +struct logitech_g27_ffb_slot +{ + logitech_g27_ffb_state state; + uint64_t last_update; + SDL_HapticEffect last_effect; + int effect_id; +}; + +struct sdl_mapping +{ + uint32_t device_type_id; // (vendor_id << 16) | product_id + sdl_mapping_type type; + uint64_t id; + hat_component hat; + bool reverse; + bool positive_axis; +}; + +struct logitech_g27_sdl_mapping +{ + sdl_mapping steering; + sdl_mapping throttle; + sdl_mapping brake; + sdl_mapping clutch; + sdl_mapping shift_up; + sdl_mapping shift_down; + + sdl_mapping up; + sdl_mapping down; + sdl_mapping left; + sdl_mapping right; + + sdl_mapping triangle; + sdl_mapping cross; + sdl_mapping square; + sdl_mapping circle; + + // mappings based on g27 compat mode on g29 + sdl_mapping l2; + sdl_mapping l3; + sdl_mapping r2; + sdl_mapping r3; + + sdl_mapping plus; + sdl_mapping minus; + + sdl_mapping dial_clockwise; + sdl_mapping dial_anticlockwise; + + sdl_mapping select; + sdl_mapping pause; + + sdl_mapping shifter_1; + sdl_mapping shifter_2; + sdl_mapping shifter_3; + sdl_mapping shifter_4; + sdl_mapping shifter_5; + sdl_mapping shifter_6; + sdl_mapping shifter_r; +}; + +class usb_device_logitech_g27 : public usb_device_emulated +{ +public: + usb_device_logitech_g27(u32 controller_index, const std::array& location); + ~usb_device_logitech_g27(); + + static std::shared_ptr make_instance(u32 controller_index, const std::array& location); + static u16 get_num_emu_devices(); + + void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override; + void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override; + bool open_device() override; + +private: + u32 m_controller_index; + + logitech_g27_sdl_mapping m_mapping; + bool m_reverse_effects; + + std::mutex m_sdl_handles_mutex; + SDL_Joystick* m_led_joystick_handle = nullptr; + SDL_Haptic* m_haptic_handle = nullptr; + std::map> m_joysticks; + bool m_fixed_loop = false; + uint16_t m_wheel_range = 200; + logitech_g27_ffb_slot m_effect_slots[4]; + SDL_HapticEffect m_default_spring_effect = {0}; + int m_default_spring_effect_id = -1; + + bool m_enabled = false; + + std::thread m_house_keeping_thread; + std::mutex m_thread_control_mutex; + bool m_stop_thread; + + void sdl_refresh(); +}; diff --git a/rpcs3/Emu/Io/LogitechG27Config.cpp b/rpcs3/Emu/Io/LogitechG27Config.cpp new file mode 100644 index 0000000000..e67e1773eb --- /dev/null +++ b/rpcs3/Emu/Io/LogitechG27Config.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" + +#ifdef HAVE_SDL3 + +#include "Utilities/File.h" +#include "LogitechG27Config.h" + +emulated_logitech_g27_config g_cfg_logitech_g27; + +LOG_CHANNEL(cfg_log, "CFG"); + +emulated_logitech_g27_config::emulated_logitech_g27_config() + : m_path(fmt::format("%s%s.yml", fs::get_config_dir(true), "LogitechG27")) +{ +} + +void emulated_logitech_g27_config::reset() +{ + const std::lock_guard lock(m_mutex); + cfg::node::from_default(); +} + +void emulated_logitech_g27_config::save(bool lock_mutex) +{ + std::unique_lock lock(m_mutex, std::defer_lock); + if (lock_mutex) + { + lock.lock(); + } + cfg_log.notice("Saving LogitechG27 config: '%s'", m_path); + + if (!fs::create_path(fs::get_parent_dir(m_path))) + { + cfg_log.fatal("Failed to create path: '%s' (%s)", m_path, fs::g_tls_error); + } + + if (!cfg::node::save(m_path)) + { + cfg_log.error("Failed to save LogitechG27 config to '%s' (error=%s)", m_path, fs::g_tls_error); + } +} + +bool emulated_logitech_g27_config::load() +{ + const std::lock_guard lock(m_mutex); + + cfg_log.notice("Loading LogitechG27 config: %s", m_path); + + from_default(); + + if (fs::file cfg_file{m_path, fs::read}) + { + if (const std::string content = cfg_file.to_string(); !content.empty()) + { + return from_string(content); + } + } + else + { + save(false); + } + + return true; +} + +#endif diff --git a/rpcs3/Emu/Io/LogitechG27Config.h b/rpcs3/Emu/Io/LogitechG27Config.h new file mode 100644 index 0000000000..929c2372df --- /dev/null +++ b/rpcs3/Emu/Io/LogitechG27Config.h @@ -0,0 +1,104 @@ +#pragma once + +#include "Utilities/Config.h" + +#include + +enum sdl_mapping_type +{ + MAPPING_BUTTON = 0, + MAPPING_HAT, + MAPPING_AXIS, +}; + +enum hat_component +{ + HAT_NONE = 0, + HAT_UP, + HAT_DOWN, + HAT_LEFT, + HAT_RIGHT +}; + +struct emulated_logitech_g27_mapping : cfg::node +{ + cfg::uint<0, 0xFFFFFFFF> device_type_id; + cfg::uint<0, 0xFFFFFFFF> type; + cfg::uint<0, 0xFFFFFFFFFFFFFFFF> id; + cfg::uint<0, 0xFFFFFFFF> hat; + cfg::_bool reverse; + + emulated_logitech_g27_mapping(cfg::node* owner, std::string name, uint32_t device_type_id_def, sdl_mapping_type type_def, uint64_t id_def, hat_component hat_def, bool reverse_def) + : cfg::node(owner, std::move(name)), + device_type_id(this, "device_type_id", device_type_id_def), + type(this, "type", type_def), + id(this, "id", id_def), + hat(this, "hat", hat_def), + reverse(this, "reverse", reverse_def) + { + } +}; + +struct emulated_logitech_g27_config : cfg::node +{ +public: + std::mutex m_mutex; + + // TODO these defaults are for a shifter-less G29 + a xbox controller for shifter testing, perhaps find a new default + + emulated_logitech_g27_mapping steering{this, "steering", 0x046dc24f, MAPPING_AXIS, 0, HAT_NONE, false}; + emulated_logitech_g27_mapping throttle{this, "throttle", 0x046dc24f, MAPPING_AXIS, 2, HAT_NONE, false}; + emulated_logitech_g27_mapping brake{this, "brake", 0x046dc24f, MAPPING_AXIS, 3, HAT_NONE, false}; + emulated_logitech_g27_mapping clutch{this, "clutch", 0x046dc24f, MAPPING_AXIS, 1, HAT_NONE, false}; + emulated_logitech_g27_mapping shift_up{this, "shift_up", 0x046dc24f, MAPPING_BUTTON, 4, HAT_NONE, false}; + emulated_logitech_g27_mapping shift_down{this, "shift_down", 0x046dc24f, MAPPING_BUTTON, 5, HAT_NONE, false}; + + emulated_logitech_g27_mapping up{this, "up", 0x046dc24f, MAPPING_HAT, 0, HAT_UP, false}; + emulated_logitech_g27_mapping down{this, "down", 0x046dc24f, MAPPING_HAT, 0, HAT_DOWN, false}; + emulated_logitech_g27_mapping left{this, "left", 0x046dc24f, MAPPING_HAT, 0, HAT_LEFT, false}; + emulated_logitech_g27_mapping right{this, "right", 0x046dc24f, MAPPING_HAT, 0, HAT_RIGHT, false}; + + emulated_logitech_g27_mapping triangle{this, "triangle", 0x046dc24f, MAPPING_BUTTON, 3, HAT_NONE, false}; + emulated_logitech_g27_mapping cross{this, "cross", 0x046dc24f, MAPPING_BUTTON, 0, HAT_NONE, false}; + emulated_logitech_g27_mapping square{this, "square", 0x046dc24f, MAPPING_BUTTON, 1, HAT_NONE, false}; + emulated_logitech_g27_mapping circle{this, "circle", 0x046dc24f, MAPPING_BUTTON, 2, HAT_NONE, false}; + + emulated_logitech_g27_mapping l2{this, "l2", 0x046dc24f, MAPPING_BUTTON, 7, HAT_NONE, false}; + emulated_logitech_g27_mapping l3{this, "l3", 0x046dc24f, MAPPING_BUTTON, 11, HAT_NONE, false}; + emulated_logitech_g27_mapping r2{this, "r2", 0x046dc24f, MAPPING_BUTTON, 6, HAT_NONE, false}; + emulated_logitech_g27_mapping r3{this, "r3", 0x046dc24f, MAPPING_BUTTON, 10, HAT_NONE, false}; + + emulated_logitech_g27_mapping plus{this, "plus", 0x046dc24f, MAPPING_BUTTON, 19, HAT_NONE, false}; + emulated_logitech_g27_mapping minus{this, "minus", 0x046dc24f, MAPPING_BUTTON, 20, HAT_NONE, false}; + + emulated_logitech_g27_mapping dial_clockwise{this, "dial_clockwise", 0x046dc24f, MAPPING_BUTTON, 21, HAT_NONE, false}; + emulated_logitech_g27_mapping dial_anticlockwise{this, "dial_anticlockwise", 0x046dc24f, MAPPING_BUTTON, 22, HAT_NONE, false}; + + emulated_logitech_g27_mapping select{this, "select", 0x046dc24f, MAPPING_BUTTON, 8, HAT_NONE, false}; + emulated_logitech_g27_mapping pause{this, "pause", 0x046dc24f, MAPPING_BUTTON, 9, HAT_NONE, false}; + + emulated_logitech_g27_mapping shifter_1{this, "shifter_1", 0x045e028e, MAPPING_BUTTON, 3, HAT_NONE, false}; + emulated_logitech_g27_mapping shifter_2{this, "shifter_2", 0x045e028e, MAPPING_BUTTON, 0, HAT_NONE, false}; + emulated_logitech_g27_mapping shifter_3{this, "shifter_3", 0x045e028e, MAPPING_BUTTON, 2, HAT_NONE, false}; + emulated_logitech_g27_mapping shifter_4{this, "shifter_4", 0x045e028e, MAPPING_BUTTON, 1, HAT_NONE, false}; + emulated_logitech_g27_mapping shifter_5{this, "shifter_5", 0x045e028e, MAPPING_HAT, 0, HAT_UP, false}; + emulated_logitech_g27_mapping shifter_6{this, "shifter_6", 0x045e028e, MAPPING_HAT, 0, HAT_DOWN, false}; + emulated_logitech_g27_mapping shifter_r{this, "shifter_r", 0x045e028e, MAPPING_HAT, 0, HAT_LEFT, false}; + + cfg::_bool reverse_effects{this, "reverse_effects", true}; + cfg::uint<0, 0xFFFFFFFF> ffb_device_type_id{this, "ffb_device_type_id", 0x046dc24f}; + cfg::uint<0, 0xFFFFFFFF> led_device_type_id{this, "led_device_type_id", 0x046dc24f}; + + cfg::_bool enabled{this, "enabled", false}; + + emulated_logitech_g27_config(); + + bool load(); + void save(bool lock_mutex = true); + void reset(); + +private: + const std::string m_path; +}; + +extern emulated_logitech_g27_config g_cfg_logitech_g27; diff --git a/rpcs3/Input/evdev_gun_handler.cpp b/rpcs3/Emu/Io/evdev_gun_handler.cpp similarity index 100% rename from rpcs3/Input/evdev_gun_handler.cpp rename to rpcs3/Emu/Io/evdev_gun_handler.cpp diff --git a/rpcs3/Input/evdev_gun_handler.h b/rpcs3/Emu/Io/evdev_gun_handler.h similarity index 100% rename from rpcs3/Input/evdev_gun_handler.h rename to rpcs3/Emu/Io/evdev_gun_handler.h diff --git a/rpcs3/Emu/RSX/Common/simple_array.hpp b/rpcs3/Emu/RSX/Common/simple_array.hpp index 08bb97cb86..dfec324eeb 100644 --- a/rpcs3/Emu/RSX/Common/simple_array.hpp +++ b/rpcs3/Emu/RSX/Common/simple_array.hpp @@ -23,9 +23,9 @@ namespace rsx Ty* _data = _local_capacity ? reinterpret_cast(_local_storage) : nullptr; u32 _size = 0; - inline u64 offset(const_iterator pos) + inline u32 offset(const_iterator pos) { - return (_data) ? u64(pos - _data) : 0ull; + return (_data) ? u32(pos - _data) : 0u; } bool is_local_storage() const diff --git a/rpcs3/stb_image.cpp b/rpcs3/Emu/stb_image.cpp similarity index 100% rename from rpcs3/stb_image.cpp rename to rpcs3/Emu/stb_image.cpp diff --git a/rpcs3/Input/sdl_instance.cpp b/rpcs3/Input/sdl_instance.cpp new file mode 100644 index 0000000000..a27a8e40de --- /dev/null +++ b/rpcs3/Input/sdl_instance.cpp @@ -0,0 +1,132 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include "util/logs.hpp" + +#include "sdl_instance.h" + +LOG_CHANNEL(sdl_log, "SDL"); + +sdl_instance::~sdl_instance() +{ + // Only quit SDL once on exit. SDL uses a global state internally... + if (m_initialized) + { + sdl_log.notice("Quitting SDL ..."); + SDL_Quit(); + } +} + +sdl_instance& sdl_instance::get_instance() +{ + static sdl_instance instance{}; + return instance; +} + +void sdl_instance::pump_events() +{ + const std::lock_guard lock(m_instance_mutex); + if (m_initialized) + SDL_PumpEvents(); +} + +bool sdl_instance::initialize() +{ + const std::lock_guard lock(m_instance_mutex); + // Only init SDL once. SDL uses a global state internally... + if (m_initialized) + { + return true; + } + + sdl_log.notice("Initializing SDL ..."); + + // Set non-dynamic hints before SDL_Init + if (!SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1")) + { + sdl_log.error("Could not set SDL_HINT_JOYSTICK_THREAD: %s", SDL_GetError()); + } + + if (!SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) + { + sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError()); + return false; + } + + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); + SDL_SetLogOutputFunction([](void*, int category, SDL_LogPriority priority, const char* message) + { + std::string category_name; + switch (category) + { + case SDL_LOG_CATEGORY_APPLICATION: + category_name = "app"; + break; + case SDL_LOG_CATEGORY_ERROR: + category_name = "error"; + break; + case SDL_LOG_CATEGORY_ASSERT: + category_name = "assert"; + break; + case SDL_LOG_CATEGORY_SYSTEM: + category_name = "system"; + break; + case SDL_LOG_CATEGORY_AUDIO: + category_name = "audio"; + break; + case SDL_LOG_CATEGORY_VIDEO: + category_name = "video"; + break; + case SDL_LOG_CATEGORY_RENDER: + category_name = "render"; + break; + case SDL_LOG_CATEGORY_INPUT: + category_name = "input"; + break; + case SDL_LOG_CATEGORY_TEST: + category_name = "test"; + break; + default: + category_name = fmt::format("unknown(%d)", category); + break; + } + + switch (priority) + { + case SDL_LOG_PRIORITY_VERBOSE: + case SDL_LOG_PRIORITY_DEBUG: + sdl_log.trace("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_INFO: + sdl_log.notice("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_WARN: + sdl_log.warning("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_ERROR: + sdl_log.error("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_CRITICAL: + sdl_log.error("%s: %s", category_name, message); + break; + default: + break; + } + }, + nullptr); + + m_initialized = true; + return true; +} + +#endif diff --git a/rpcs3/Input/sdl_instance.h b/rpcs3/Input/sdl_instance.h new file mode 100644 index 0000000000..8a1d1b6029 --- /dev/null +++ b/rpcs3/Input/sdl_instance.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +struct sdl_instance +{ +public: + sdl_instance() = default; + ~sdl_instance(); + + static sdl_instance& get_instance(); + + bool initialize(); + void pump_events(); + +private: + bool m_initialized = false; + std::mutex m_instance_mutex; +}; diff --git a/rpcs3/Input/sdl_pad_handler.cpp b/rpcs3/Input/sdl_pad_handler.cpp index 38bd6bc1bd..244cd6006a 100644 --- a/rpcs3/Input/sdl_pad_handler.cpp +++ b/rpcs3/Input/sdl_pad_handler.cpp @@ -5,122 +5,12 @@ #include "Emu/system_utils.hpp" #include "Emu/system_config.h" #include "Emu/System.h" +#include "sdl_instance.h" #include LOG_CHANNEL(sdl_log, "SDL"); -struct sdl_instance -{ -public: - sdl_instance() = default; - ~sdl_instance() - { - // Only quit SDL once on exit. SDL uses a global state internally... - if (m_initialized) - { - sdl_log.notice("Quitting SDL ..."); - SDL_Quit(); - } - } - - static sdl_instance& get_instance() - { - static sdl_instance instance {}; - return instance; - } - - bool initialize() - { - // Only init SDL once. SDL uses a global state internally... - if (m_initialized) - { - return true; - } - - sdl_log.notice("Initializing SDL ..."); - - // Set non-dynamic hints before SDL_Init - if (!SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1")) - { - sdl_log.error("Could not set SDL_HINT_JOYSTICK_THREAD: %s", SDL_GetError()); - } - - if (!SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) - { - sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError()); - return false; - } - - SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); - SDL_SetLogOutputFunction([](void*, int category, SDL_LogPriority priority, const char* message) - { - std::string category_name; - switch (category) - { - case SDL_LOG_CATEGORY_APPLICATION: - category_name = "app"; - break; - case SDL_LOG_CATEGORY_ERROR: - category_name = "error"; - break; - case SDL_LOG_CATEGORY_ASSERT: - category_name = "assert"; - break; - case SDL_LOG_CATEGORY_SYSTEM: - category_name = "system"; - break; - case SDL_LOG_CATEGORY_AUDIO: - category_name = "audio"; - break; - case SDL_LOG_CATEGORY_VIDEO: - category_name = "video"; - break; - case SDL_LOG_CATEGORY_RENDER: - category_name = "render"; - break; - case SDL_LOG_CATEGORY_INPUT: - category_name = "input"; - break; - case SDL_LOG_CATEGORY_TEST: - category_name = "test"; - break; - default: - category_name = fmt::format("unknown(%d)", category); - break; - } - - switch (priority) - { - case SDL_LOG_PRIORITY_VERBOSE: - case SDL_LOG_PRIORITY_DEBUG: - sdl_log.trace("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_INFO: - sdl_log.notice("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_WARN: - sdl_log.warning("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_ERROR: - sdl_log.error("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_CRITICAL: - sdl_log.error("%s: %s", category_name, message); - break; - default: - break; - } - }, nullptr); - - m_initialized = true; - return true; - } - -private: - bool m_initialized = false; -}; - sdl_pad_handler::sdl_pad_handler() : PadHandlerBase(pad_handler::sdl) { button_list = @@ -314,7 +204,7 @@ void sdl_pad_handler::process() if (!m_is_init) return; - SDL_PumpEvents(); + sdl_instance::get_instance().pump_events(); PadHandlerBase::process(); } @@ -771,7 +661,7 @@ void sdl_pad_handler::get_motion_sensors(const std::string& pad_id, const motion if (!m_is_init) return; - SDL_PumpEvents(); + sdl_instance::get_instance().pump_events(); PadHandlerBase::get_motion_sensors(pad_id, callback, fail_callback, preview_values, sensors); } @@ -781,7 +671,7 @@ PadHandlerBase::connection sdl_pad_handler::get_next_button_press(const std::str if (!m_is_init) return connection::disconnected; - SDL_PumpEvents(); + sdl_instance::get_instance().pump_events(); return PadHandlerBase::get_next_button_press(padId, callback, fail_callback, call_type, buttons); } diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 07414051ed..446a21c2b9 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -40,7 +40,7 @@ Use - ..\3rdparty\miniupnp\miniupnp\miniupnpc\include;..\3rdparty\wolfssl\wolfssl;..\3rdparty\flatbuffers\include;..\3rdparty\libusb\libusb\libusb;..\3rdparty\yaml-cpp\yaml-cpp\include;..\3rdparty\SoundTouch\soundtouch\include;..\3rdparty\rtmidi\rtmidi;..\3rdparty\zlib\zlib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(VULKAN_SDK)\Include;..\3rdparty\zstd\zstd\lib;$(SolutionDir)3rdparty\fusion\fusion\Fusion;$(SolutionDir)3rdparty\wolfssl\extra\win32 + ..\3rdparty\miniupnp\miniupnp\miniupnpc\include;..\3rdparty\wolfssl\wolfssl;..\3rdparty\flatbuffers\include;..\3rdparty\libusb\libusb\libusb;..\3rdparty\yaml-cpp\yaml-cpp\include;..\3rdparty\SoundTouch\soundtouch\include;..\3rdparty\rtmidi\rtmidi;..\3rdparty\zlib\zlib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm\include;$(SolutionDir)build\lib_ext\$(Configuration)-$(Platform)\llvm_build\include;$(VULKAN_SDK)\Include;..\3rdparty\zstd\zstd\lib;$(SolutionDir)3rdparty\fusion\fusion\Fusion;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\libsdl-org\SDL\include MaxSpeed AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;%(PreprocessorDefinitions) AL_LIBTYPE_STATIC;MINIUPNP_STATICLIB;HAVE_VULKAN;HAVE_SDL3;ZLIB_CONST;WOLFSSL_USER_SETTINGS;%(PreprocessorDefinitions) @@ -82,6 +82,9 @@ + + true + @@ -96,6 +99,8 @@ + + @@ -530,7 +535,7 @@ - + Create @@ -574,6 +579,9 @@ + + true + @@ -602,6 +610,8 @@ + + @@ -1064,4 +1074,4 @@ - \ No newline at end of file + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index c0ef133d1a..ef03ca416f 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -945,6 +945,12 @@ Emu\Io + + Emu\Io + + + Emu\Io + Emu\Io @@ -1083,7 +1089,7 @@ Emu - + Emu\GPU\RSX\Program @@ -1357,6 +1363,9 @@ Emu\GPU\RSX\Overlays + + Emu\Io + @@ -2157,6 +2166,12 @@ Emu\Io + + Emu\Io + + + Emu\Io + Emu\Io @@ -2725,6 +2740,9 @@ Emu\GPU\RSX\Overlays + + Emu\Io + @@ -2860,4 +2878,4 @@ Emu\GPU\RSX\Program\MSAA - \ No newline at end of file + diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 6540926d9c..8e93263c1b 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -54,7 +54,6 @@ $(SolutionDir)build\tmp\$(ProjectName)-$(Configuration)-$(Platform)\ rpcs3 true - true $(SolutionDir)bin\ $(SolutionDir)build\tmp\$(ProjectName)-$(Configuration)-$(Platform)\ rpcs3d @@ -158,8 +157,6 @@ true Windows true - - mainCRTStartup @@ -193,7 +190,6 @@ - @@ -203,6 +199,7 @@ + @@ -270,6 +267,9 @@ true + + true + true @@ -555,6 +555,9 @@ true + + true + true @@ -801,6 +804,7 @@ + @@ -920,7 +924,6 @@ - @@ -1042,6 +1045,7 @@ + @@ -1358,6 +1362,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\rtmidi\rtmidi" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\rtmidi\rtmidi" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DMINIUPNP_STATICLIB -DHAVE_SDL3 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\rtmidi\rtmidi" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\libsdl-org\SDL\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + Moc%27ing %(Identity)... @@ -1915,24 +1929,6 @@ - - - Document - true - $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) - cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:strictStrings -Zc:throwingNew- -Zi -MDd -GR -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E -Za $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >debug\moc_predefs.h - Generate moc_predefs.h - debug\moc_predefs.h;%(Outputs) - - - Document - $(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs) - cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:strictStrings -Zc:throwingNew- -O2 -MD -GR -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E -Za $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >release\moc_predefs.h - Generate moc_predefs.h - release\moc_predefs.h;%(Outputs) - true - - %(FullPath);%(AdditionalInputs) @@ -2166,6 +2162,10 @@ + + + + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 6d368a1a1f..2e344fa2ca 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -993,12 +993,12 @@ Generated Files\Release - - Io\evdev - Io\SDL + + Io\SDL + Gui\log @@ -1143,6 +1143,15 @@ Generated Files\Release + + Gui\settings + + + Generated Files\Debug + + + Generated Files\Release + Gui\settings @@ -1349,15 +1358,15 @@ Gui\settings - - Io\evdev - rpcs3 Io\SDL + + Io\SDL + Generated Files @@ -1426,12 +1435,6 @@ - - Generated Files - - - Generated Files - Resource Files @@ -1747,6 +1750,9 @@ Gui\settings + + Gui\settings + Gui\settings @@ -1900,4 +1906,12 @@ CI - \ No newline at end of file + + + buildfiles\cmake + + + buildfiles\cmake + + + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 68fa4b9e76..c49daf0346 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(rpcs3_ui STATIC emu_settings.cpp elf_memory_dumping_dialog.cpp emulated_pad_settings_dialog.cpp + emulated_logitech_g27_settings_dialog.cpp fatal_error_dialog.cpp find_dialog.cpp flow_layout.cpp @@ -131,6 +132,37 @@ add_library(rpcs3_ui STATIC shortcut_dialog.ui welcome_dialog.ui + ../display_sleep_control.cpp + ../headless_application.cpp + ../main_application.cpp + ../module_verifier.cpp + ../rpcs3.cpp + ../rpcs3_version.cpp + ../stdafx.cpp + + ../Input/basic_keyboard_handler.cpp + ../Input/basic_mouse_handler.cpp + ../Input/ds3_pad_handler.cpp + ../Input/ds4_pad_handler.cpp + ../Input/dualsense_pad_handler.cpp + ../Input/evdev_joystick_handler.cpp + ../Input/gui_pad_thread.cpp + ../Input/hid_pad_handler.cpp + ../Input/keyboard_pad_handler.cpp + ../Input/mm_joystick_handler.cpp + ../Input/pad_thread.cpp + ../Input/product_info.cpp + ../Input/ps_move_calibration.cpp + ../Input/ps_move_config.cpp + ../Input/ps_move_handler.cpp + ../Input/ps_move_tracker.cpp + ../Input/raw_mouse_config.cpp + ../Input/raw_mouse_handler.cpp + ../Input/sdl_pad_handler.cpp + ../Input/sdl_instance.cpp + ../Input/skateboard_pad_handler.cpp + ../Input/xinput_pad_handler.cpp + "../resources.qrc" ) @@ -157,20 +189,20 @@ target_compile_definitions(rpcs3_ui PRIVATE WIN32_LEAN_AND_MEAN) target_link_libraries(rpcs3_ui PUBLIC + rpcs3_emu 3rdparty::qt6 3rdparty::yaml-cpp - - PRIVATE - rpcs3_emu 3rdparty::zlib - 3rdparty::pugixml 3rdparty::discordRPC 3rdparty::hidapi 3rdparty::libusb - 3rdparty::libpng - 3rdparty::7zip 3rdparty::wolfssl 3rdparty::libcurl 3rdparty::opencv 3rdparty::fusion + + PRIVATE + 3rdparty::pugixml + 3rdparty::libpng + 3rdparty::7zip 3rdparty::rtmidi) diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp new file mode 100644 index 0000000000..e7a894d6ae --- /dev/null +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.cpp @@ -0,0 +1,972 @@ +#include "stdafx.h" + +#ifdef HAVE_SDL3 + +#include "emulated_logitech_g27_settings_dialog.h" +#include "Emu/Io/LogitechG27.h" +#include "Input/sdl_instance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_CHANNEL(logitech_g27_cfg_log, "LOGIG27"); + +static constexpr const char* DEFAULT_STATUS = " "; + +enum mapping_device_choice +{ + CHOICE_NONE = -1, + CHOICE_STEERING = 0, + CHOICE_THROTTLE, + CHOICE_BRAKE, + CHOICE_CLUTCH, + CHOICE_SHIFT_UP, + CHOICE_SHIFT_DOWN, + + CHOICE_UP, + CHOICE_DOWN, + CHOICE_LEFT, + CHOICE_RIGHT, + + CHOICE_TRIANGLE, + CHOICE_CROSS, + CHOICE_SQUARE, + CHOICE_CIRCLE, + + CHOICE_L2, + CHOICE_L3, + CHOICE_R2, + CHOICE_R3, + + CHOICE_PLUS, + CHOICE_MINUS, + + CHOICE_DIAL_CLOCKWISE, + CHOICE_DIAL_ANTICLOCKWISE, + + CHOICE_SELECT, + CHOICE_PAUSE, + + CHOICE_SHIFTER_1, + CHOICE_SHIFTER_2, + CHOICE_SHIFTER_3, + CHOICE_SHIFTER_4, + CHOICE_SHIFTER_5, + CHOICE_SHIFTER_6, + CHOICE_SHIFTER_R +}; + +class DeviceChoice : public QWidget +{ +public: + DeviceChoice(QWidget* parent, const char* name) + : QWidget(parent) + { + auto layout = new QHBoxLayout(this); + setLayout(layout); + + QLabel* label = new QLabel(this); + label->setText(QString(name)); + label->setMinimumWidth(400); + layout->addWidget(label); + + m_dropdown = new QComboBox(this); + layout->addWidget(m_dropdown); + + m_disable_button = new QPushButton(QString("DISABLE"), this); + layout->addWidget(m_disable_button); + + connect(m_dropdown, &QComboBox::currentIndexChanged, this, [this]() + { + m_device_choice = static_cast(this->m_dropdown->currentIndex()); + update_display(); + }); + + connect(m_disable_button, &QPushButton::clicked, this, [this]() + { + m_device_choice = CHOICE_NONE; + update_display(); + }); + + m_dropdown->setPlaceholderText(QString("-- Disabled --")); + + m_dropdown->addItem(QString("Steering"), QVariant(CHOICE_STEERING)); + m_dropdown->addItem(QString("Throttle"), QVariant(CHOICE_THROTTLE)); + m_dropdown->addItem(QString("Brake"), QVariant(CHOICE_BRAKE)); + m_dropdown->addItem(QString("Clutch"), QVariant(CHOICE_CLUTCH)); + m_dropdown->addItem(QString("Shift up"), QVariant(CHOICE_SHIFT_UP)); + m_dropdown->addItem(QString("Shift down"), QVariant(CHOICE_SHIFT_DOWN)); + + m_dropdown->addItem(QString("Up"), QVariant(CHOICE_UP)); + m_dropdown->addItem(QString("Down"), QVariant(CHOICE_DOWN)); + m_dropdown->addItem(QString("Left"), QVariant(CHOICE_LEFT)); + m_dropdown->addItem(QString("Right"), QVariant(CHOICE_RIGHT)); + + m_dropdown->addItem(QString("Triangle"), QVariant(CHOICE_TRIANGLE)); + m_dropdown->addItem(QString("Cross"), QVariant(CHOICE_CROSS)); + m_dropdown->addItem(QString("Square"), QVariant(CHOICE_SQUARE)); + m_dropdown->addItem(QString("Circle"), QVariant(CHOICE_CIRCLE)); + + m_dropdown->addItem(QString("L2"), QVariant(CHOICE_L2)); + m_dropdown->addItem(QString("L3"), QVariant(CHOICE_L3)); + m_dropdown->addItem(QString("R2"), QVariant(CHOICE_R2)); + m_dropdown->addItem(QString("R3"), QVariant(CHOICE_R3)); + + m_dropdown->addItem(QString("L4"), QVariant(CHOICE_PLUS)); + m_dropdown->addItem(QString("L5"), QVariant(CHOICE_MINUS)); + + m_dropdown->addItem(QString("R4"), QVariant(CHOICE_DIAL_CLOCKWISE)); + m_dropdown->addItem(QString("R5"), QVariant(CHOICE_DIAL_ANTICLOCKWISE)); + + m_dropdown->addItem(QString("Select"), QVariant(CHOICE_SELECT)); + m_dropdown->addItem(QString("Pause"), QVariant(CHOICE_PAUSE)); + + m_dropdown->addItem(QString("Gear 1"), QVariant(CHOICE_SHIFTER_1)); + m_dropdown->addItem(QString("Gear 2"), QVariant(CHOICE_SHIFTER_2)); + m_dropdown->addItem(QString("Gear 3"), QVariant(CHOICE_SHIFTER_3)); + m_dropdown->addItem(QString("Gear 4"), QVariant(CHOICE_SHIFTER_4)); + m_dropdown->addItem(QString("Gear 5"), QVariant(CHOICE_SHIFTER_5)); + m_dropdown->addItem(QString("Gear 6"), QVariant(CHOICE_SHIFTER_6)); + m_dropdown->addItem(QString("Gear r"), QVariant(CHOICE_SHIFTER_R)); + + update_display(); + } + + mapping_device_choice get_device_choice() + { + return m_device_choice; + } + + void set_device_choice(mapping_device_choice choice) + { + m_device_choice = choice; + update_display(); + } + + void set_enable(bool enable) + { + m_dropdown->setEnabled(enable); + m_disable_button->setEnabled(enable); + } + +private: + mapping_device_choice m_device_choice = CHOICE_NONE; + QComboBox* m_dropdown = nullptr; + QPushButton* m_disable_button = nullptr; + + void update_display() + { + m_dropdown->setCurrentIndex(static_cast(m_device_choice)); + } +}; + +class Mapping : public QGroupBox +{ +public: + Mapping(QWidget* parent, emulated_logitech_g27_settings_dialog* dialog, bool is_axis, const char* name, bool flip_axis_display) + : QGroupBox(parent), m_setting_dialog(dialog), m_is_axis(is_axis), m_name(name), m_flip_axis_display(flip_axis_display) + { + QVBoxLayout* layout = new QVBoxLayout(this); + setLayout(layout); + + QWidget* horizontal_container = new QWidget(this); + QHBoxLayout* horizontal_layout = new QHBoxLayout(horizontal_container); + horizontal_container->setLayout(horizontal_layout); + + layout->addWidget(horizontal_container); + + QLabel* label = new QLabel(horizontal_container); + label->setText(QString(name)); + + m_display_box = new QLabel(horizontal_container); + m_display_box->setTextFormat(Qt::RichText); + m_display_box->setWordWrap(true); + m_display_box->setFrameStyle(QFrame::Box); + m_display_box->setMinimumWidth(150); + + m_map_button = new QPushButton(QString("MAP"), horizontal_container); + m_unmap_button = new QPushButton(QString("UNMAP"), horizontal_container); + m_reverse_checkbox = new QCheckBox(QString("Reverse"), horizontal_container); + + if (!this->m_is_axis) + { + m_axis_status = nullptr; + m_button_status = new QCheckBox(QString("Pressed"), horizontal_container); + m_button_status->setDisabled(true); + } + else + { + m_button_status = nullptr; + m_axis_status = new QSlider(Qt::Horizontal, this); + m_axis_status->setDisabled(true); + m_axis_status->setMinimum(-0x8000); + m_axis_status->setMaximum(0x7FFF); + m_axis_status->setValue(-0x8000); + } + + update_display(); + + horizontal_layout->addWidget(label); + horizontal_layout->addWidget(m_display_box); + if (!this->m_is_axis) + horizontal_layout->addWidget(m_button_status); + horizontal_layout->addWidget(m_map_button); + horizontal_layout->addWidget(m_unmap_button); + horizontal_layout->addWidget(m_reverse_checkbox); + + if (this->m_is_axis) + layout->addWidget(m_axis_status); + + connect(m_map_button, &QPushButton::clicked, this, [this]() + { + this->m_mapping_in_progress = true; + this->m_timeout_msec = 5500; + this->m_setting_dialog->set_enable(false); + this->m_last_joystick_states = this->m_setting_dialog->get_joystick_states(); + }); + + connect(m_unmap_button, &QPushButton::clicked, this, [this]() + { + this->m_mapping.device_type_id = 0; + update_display(); + }); + + connect(m_reverse_checkbox, &QCheckBox::clicked, this, [this]() + { + this->m_mapping.reverse = this->m_reverse_checkbox->isChecked(); + }); + + m_tick_timer = new QTimer(this); + connect(m_tick_timer, &QTimer::timeout, this, [this]() + { + if (this->m_mapping_in_progress) + { + char text_buf[128]; + + int timeout_sec = this->m_timeout_msec / 1000; + + const std::map& new_joystick_states = this->m_setting_dialog->get_joystick_states(); + + sprintf(text_buf, "Input %s for %s, timeout in %d %s", this->m_is_axis ? "axis" : "button/hat", this->m_name.c_str(), timeout_sec, timeout_sec >= 2 ? "seconds" : "second"); + this->m_setting_dialog->set_state_text(text_buf); + + for (auto new_joystick_state : new_joystick_states) + { + auto last_joystick_state = this->m_last_joystick_states.find(new_joystick_state.first); + if (last_joystick_state == this->m_last_joystick_states.end()) + { + continue; + } + + if (this->m_is_axis) + { + const static int16_t axis_change_threshold = 0x7FFF / 5; + if (last_joystick_state->second.axes.size() != new_joystick_state.second.axes.size()) + { + logitech_g27_cfg_log.error("during input state change diff, number of axes on %04x:%04x changed", new_joystick_state.first >> 16, new_joystick_state.first & 0xFFFF); + continue; + } + for (std::vector::size_type i = 0; i < new_joystick_state.second.axes.size(); i++) + { + int32_t diff = std::abs(last_joystick_state->second.axes[i] - new_joystick_state.second.axes[i]); + if (diff > axis_change_threshold) + { + this->m_mapping_in_progress = false; + this->m_setting_dialog->set_state_text(DEFAULT_STATUS); + this->m_setting_dialog->set_enable(true); + this->m_mapping.device_type_id = new_joystick_state.first; + this->m_mapping.type = MAPPING_AXIS; + this->m_mapping.id = i; + this->m_mapping.hat = HAT_NONE; + break; + } + } + } + else + { + if (last_joystick_state->second.buttons.size() != new_joystick_state.second.buttons.size()) + { + logitech_g27_cfg_log.error("during input state change diff, number of buttons on %04x:%04x changed", new_joystick_state.first >> 16, new_joystick_state.first & 0xFFFF); + continue; + } + if (last_joystick_state->second.hats.size() != new_joystick_state.second.hats.size()) + { + logitech_g27_cfg_log.error("during input state change diff, number of hats on %04x:%04x changed", new_joystick_state.first >> 16, new_joystick_state.first & 0xFFFF); + continue; + } + for (std::vector::size_type i = 0; i < new_joystick_state.second.buttons.size(); i++) + { + if (last_joystick_state->second.buttons[i] != new_joystick_state.second.buttons[i]) + { + this->m_mapping_in_progress = false; + this->m_setting_dialog->set_state_text(DEFAULT_STATUS); + this->m_setting_dialog->set_enable(true); + this->m_mapping.device_type_id = new_joystick_state.first; + this->m_mapping.type = MAPPING_BUTTON; + this->m_mapping.id = i; + this->m_mapping.hat = HAT_NONE; + break; + } + } + for (std::vector::size_type i = 0; i < new_joystick_state.second.hats.size(); i++) + { + if (last_joystick_state->second.hats[i] != new_joystick_state.second.hats[i] && new_joystick_state.second.hats[i] != HAT_NONE) + { + this->m_mapping_in_progress = false; + this->m_setting_dialog->set_state_text(DEFAULT_STATUS); + this->m_setting_dialog->set_enable(true); + this->m_mapping.device_type_id = new_joystick_state.first; + this->m_mapping.type = MAPPING_HAT; + this->m_mapping.id = i; + this->m_mapping.hat = new_joystick_state.second.hats[i]; + break; + } + } + } + } + + this->m_timeout_msec = this->m_timeout_msec - 25; + if (this->m_timeout_msec <= 0) + { + this->m_mapping_in_progress = false; + this->m_setting_dialog->set_state_text(DEFAULT_STATUS); + this->m_setting_dialog->set_enable(true); + } + } + + update_display(); + }); + m_tick_timer->start(25); + } + + void set_enable(bool enable) + { + m_map_button->setEnabled(enable); + m_unmap_button->setEnabled(enable); + m_reverse_checkbox->setEnabled(enable); + } + + void set_mapping(const sdl_mapping& mapping) + { + this->m_mapping = mapping; + update_display(); + } + + const sdl_mapping& get_mapping() + { + return m_mapping; + } + +private: + emulated_logitech_g27_settings_dialog* m_setting_dialog = nullptr; + DeviceChoice* m_ffb_device = nullptr; + DeviceChoice* m_led_device = nullptr; + sdl_mapping m_mapping = {0}; + bool m_is_axis = false; + std::string m_name = ""; + bool m_flip_axis_display = false; + + QLabel* m_display_box = nullptr; + QPushButton* m_map_button = nullptr; + QPushButton* m_unmap_button = nullptr; + QCheckBox* m_reverse_checkbox = nullptr; + + bool m_mapping_in_progress = false; + int m_timeout_msec = 5500; + QTimer* m_tick_timer = nullptr; + std::map m_last_joystick_states; + + QCheckBox* m_button_status = nullptr; + QSlider* m_axis_status = nullptr; + + void update_display() + { + char text_buf[64]; + const char* type_string = nullptr; + switch (m_mapping.type) + { + case MAPPING_BUTTON: + type_string = "button"; + break; + case MAPPING_HAT: + type_string = "hat"; + break; + case MAPPING_AXIS: + type_string = "axis"; + break; + } + const char* hat_string = nullptr; + switch (m_mapping.hat) + { + case HAT_UP: + hat_string = "up"; + break; + case HAT_DOWN: + hat_string = "down"; + break; + case HAT_LEFT: + hat_string = "left"; + break; + case HAT_RIGHT: + hat_string = "right"; + break; + case HAT_NONE: + hat_string = ""; + break; + } + sprintf(text_buf, "%04x:%04x, %s %u %s", m_mapping.device_type_id >> 16, m_mapping.device_type_id & 0xFFFF, type_string, m_mapping.id, hat_string); + m_display_box->setText(QString(text_buf)); + + m_reverse_checkbox->setChecked(m_mapping.reverse); + + if (m_button_status) + m_button_status->setChecked(m_mapping.reverse); + + if (m_axis_status) + { + int32_t axis_value = (-0x8000); + if (m_mapping.reverse) + axis_value = axis_value * (-1); + if (m_flip_axis_display) + axis_value = axis_value * (-1); + if (axis_value > 0x7FFF) + axis_value = 0x7FFF; + if (axis_value < (-0x8000)) + axis_value = (-0x8000); + m_axis_status->setValue(axis_value); + } + + const std::map& joystick_states = m_setting_dialog->get_joystick_states(); + auto joystick_state = joystick_states.find(m_mapping.device_type_id); + + if (joystick_state != joystick_states.end()) + { + switch (m_mapping.type) + { + case MAPPING_BUTTON: + { + if (joystick_state->second.buttons.size() <= m_mapping.id) + break; + bool value = joystick_state->second.buttons[m_mapping.id]; + if (m_mapping.reverse) + value = !value; + m_button_status->setChecked(value); + break; + } + case MAPPING_HAT: + { + if (joystick_state->second.hats.size() <= m_mapping.id) + break; + bool value = joystick_state->second.hats[m_mapping.id] == m_mapping.hat; + if (m_mapping.reverse) + value = !value; + m_button_status->setChecked(value); + break; + } + case MAPPING_AXIS: + { + if (joystick_state->second.axes.size() <= m_mapping.id) + break; + int32_t value = joystick_state->second.axes[m_mapping.id]; + if (m_mapping.reverse) + value = value * (-1); + if (m_flip_axis_display) + value = value * (-1); + if (value > 0x7FFF) + value = 0x7FFF; + else if (value < (-0x8000)) + value = (-0x8000); + m_axis_status->setValue(value); + break; + } + } + } + } +}; + +void emulated_logitech_g27_settings_dialog::save_ui_state_to_config() +{ +#define SAVE_MAPPING(name, device_choice) \ + { \ + const sdl_mapping& m = m_##name->get_mapping(); \ + g_cfg_logitech_g27.name.device_type_id.set(m.device_type_id); \ + g_cfg_logitech_g27.name.type.set(m.type); \ + g_cfg_logitech_g27.name.id.set(m.id); \ + g_cfg_logitech_g27.name.hat.set(m.hat); \ + g_cfg_logitech_g27.name.reverse.set(m.reverse); \ + if (m_ffb_device->get_device_choice() == device_choice) \ + { \ + g_cfg_logitech_g27.ffb_device_type_id.set(m.device_type_id); \ + } \ + if (m_led_device->get_device_choice() == device_choice) \ + { \ + g_cfg_logitech_g27.led_device_type_id.set(m.device_type_id); \ + } \ + } + + SAVE_MAPPING(steering, CHOICE_STEERING); + SAVE_MAPPING(throttle, CHOICE_THROTTLE); + SAVE_MAPPING(brake, CHOICE_BRAKE); + SAVE_MAPPING(clutch, CHOICE_CLUTCH); + SAVE_MAPPING(shift_up, CHOICE_SHIFT_UP); + SAVE_MAPPING(shift_down, CHOICE_SHIFT_DOWN); + + SAVE_MAPPING(up, CHOICE_UP); + SAVE_MAPPING(down, CHOICE_DOWN); + SAVE_MAPPING(left, CHOICE_LEFT); + SAVE_MAPPING(right, CHOICE_RIGHT); + + SAVE_MAPPING(triangle, CHOICE_TRIANGLE); + SAVE_MAPPING(cross, CHOICE_CROSS); + SAVE_MAPPING(square, CHOICE_SQUARE); + SAVE_MAPPING(circle, CHOICE_CIRCLE); + + SAVE_MAPPING(l2, CHOICE_L2); + SAVE_MAPPING(l3, CHOICE_L3); + SAVE_MAPPING(r2, CHOICE_R2); + SAVE_MAPPING(r3, CHOICE_R3); + + SAVE_MAPPING(plus, CHOICE_PLUS); + SAVE_MAPPING(minus, CHOICE_MINUS); + + SAVE_MAPPING(dial_clockwise, CHOICE_DIAL_CLOCKWISE); + SAVE_MAPPING(dial_anticlockwise, CHOICE_DIAL_ANTICLOCKWISE); + + SAVE_MAPPING(select, CHOICE_SELECT); + SAVE_MAPPING(pause, CHOICE_PAUSE); + + SAVE_MAPPING(shifter_1, CHOICE_SHIFTER_1); + SAVE_MAPPING(shifter_2, CHOICE_SHIFTER_2); + SAVE_MAPPING(shifter_3, CHOICE_SHIFTER_3); + SAVE_MAPPING(shifter_4, CHOICE_SHIFTER_4); + SAVE_MAPPING(shifter_5, CHOICE_SHIFTER_5); + SAVE_MAPPING(shifter_6, CHOICE_SHIFTER_6); + SAVE_MAPPING(shifter_r, CHOICE_SHIFTER_R); + +#undef SAVE_MAPPING + + g_cfg_logitech_g27.enabled.set(m_enabled->isChecked()); + g_cfg_logitech_g27.reverse_effects.set(m_reverse_effects->isChecked()); + + if (m_ffb_device->get_device_choice() == CHOICE_NONE) + { + g_cfg_logitech_g27.ffb_device_type_id.set(0); + } + + if (m_led_device->get_device_choice() == CHOICE_NONE) + { + g_cfg_logitech_g27.led_device_type_id.set(0); + } +} + +void emulated_logitech_g27_settings_dialog::load_ui_state_from_config() +{ +#define LOAD_MAPPING(name, device_choice) \ + { \ + const sdl_mapping m = { \ + .device_type_id = static_cast(g_cfg_logitech_g27.name.device_type_id.get()), \ + .type = static_cast(g_cfg_logitech_g27.name.type.get()), \ + .id = static_cast(g_cfg_logitech_g27.name.id.get()), \ + .hat = static_cast(g_cfg_logitech_g27.name.hat.get()), \ + .reverse = g_cfg_logitech_g27.name.reverse.get(), \ + .positive_axis = false}; \ + m_##name->set_mapping(m); \ + if (g_cfg_logitech_g27.ffb_device_type_id.get() == m.device_type_id && m_ffb_device->get_device_choice() == CHOICE_NONE) \ + { \ + m_ffb_device->set_device_choice(device_choice); \ + } \ + if (g_cfg_logitech_g27.led_device_type_id.get() == m.device_type_id && m_led_device->get_device_choice() == CHOICE_NONE) \ + { \ + m_led_device->set_device_choice(device_choice); \ + } \ + } + + LOAD_MAPPING(steering, CHOICE_STEERING); + LOAD_MAPPING(throttle, CHOICE_THROTTLE); + LOAD_MAPPING(brake, CHOICE_BRAKE); + LOAD_MAPPING(clutch, CHOICE_CLUTCH); + LOAD_MAPPING(shift_up, CHOICE_SHIFT_UP); + LOAD_MAPPING(shift_down, CHOICE_SHIFT_DOWN); + + LOAD_MAPPING(up, CHOICE_UP); + LOAD_MAPPING(down, CHOICE_DOWN); + LOAD_MAPPING(left, CHOICE_LEFT); + LOAD_MAPPING(right, CHOICE_RIGHT); + + LOAD_MAPPING(triangle, CHOICE_TRIANGLE); + LOAD_MAPPING(cross, CHOICE_CROSS); + LOAD_MAPPING(square, CHOICE_SQUARE); + LOAD_MAPPING(circle, CHOICE_CIRCLE); + + LOAD_MAPPING(l2, CHOICE_L2); + LOAD_MAPPING(l3, CHOICE_L3); + LOAD_MAPPING(r2, CHOICE_R2); + LOAD_MAPPING(r3, CHOICE_R3); + + LOAD_MAPPING(plus, CHOICE_PLUS); + LOAD_MAPPING(minus, CHOICE_MINUS); + + LOAD_MAPPING(dial_clockwise, CHOICE_DIAL_CLOCKWISE); + LOAD_MAPPING(dial_anticlockwise, CHOICE_DIAL_ANTICLOCKWISE); + + LOAD_MAPPING(select, CHOICE_SELECT); + LOAD_MAPPING(pause, CHOICE_PAUSE); + + LOAD_MAPPING(shifter_1, CHOICE_SHIFTER_1); + LOAD_MAPPING(shifter_2, CHOICE_SHIFTER_2); + LOAD_MAPPING(shifter_3, CHOICE_SHIFTER_3); + LOAD_MAPPING(shifter_4, CHOICE_SHIFTER_4); + LOAD_MAPPING(shifter_5, CHOICE_SHIFTER_5); + LOAD_MAPPING(shifter_6, CHOICE_SHIFTER_6); + LOAD_MAPPING(shifter_r, CHOICE_SHIFTER_R); + +#undef LOAD_MAPPING + + m_enabled->setChecked(g_cfg_logitech_g27.enabled.get()); + m_reverse_effects->setChecked(g_cfg_logitech_g27.reverse_effects.get()); +} + +emulated_logitech_g27_settings_dialog::emulated_logitech_g27_settings_dialog(QWidget* parent) + : QDialog(parent) +{ + setObjectName("emulated_logitech_g27_settings_dialog"); + setWindowTitle(tr("Configure Emulated Logitech G27 Wheel")); + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_StyledBackground); + setModal(true); + + QVBoxLayout* v_layout = new QVBoxLayout(this); + + QDialogButtonBox* buttons = new QDialogButtonBox(this); + buttons->setStandardButtons(QDialogButtonBox::Apply | QDialogButtonBox::Cancel | QDialogButtonBox::Save | QDialogButtonBox::RestoreDefaults); + + g_cfg_logitech_g27.load(); + + connect(buttons, &QDialogButtonBox::clicked, this, [this, buttons](QAbstractButton* button) + { + if (button == buttons->button(QDialogButtonBox::Apply)) + { + save_ui_state_to_config(); + g_cfg_logitech_g27.save(); + load_ui_state_from_config(); + } + else if (button == buttons->button(QDialogButtonBox::Save)) + { + save_ui_state_to_config(); + g_cfg_logitech_g27.save(); + accept(); + } + else if (button == buttons->button(QDialogButtonBox::RestoreDefaults)) + { + if (QMessageBox::question(this, tr("Confirm Reset"), tr("Reset all?")) != QMessageBox::Yes) + return; + g_cfg_logitech_g27.reset(); + load_ui_state_from_config(); + g_cfg_logitech_g27.save(); + } + else if (button == buttons->button(QDialogButtonBox::Cancel)) + { + reject(); + } + }); + + QLabel* warning = new QLabel(QString("Warning: Force feedback output were meant for Logitech G27, on stronger wheels please adjust force strength accordingly in your wheel software."), this); + warning->setStyleSheet("color: red;"); + warning->setWordWrap(true); + v_layout->addWidget(warning); + + m_enabled = new QCheckBox(QString("Enabled (requires game restart)"), this); + v_layout->addWidget(m_enabled); + + m_reverse_effects = new QCheckBox(QString("Reverse force feedback effects"), this); + v_layout->addWidget(m_reverse_effects); + + m_state_text = new QLabel(QString(DEFAULT_STATUS), this); + v_layout->addWidget(m_state_text); + + m_ffb_device = new DeviceChoice(this, "Use the device with the following mapping for force feedback:"); + m_led_device = new DeviceChoice(this, "Use the device with the following mapping for LED:"); + + m_mapping_scroll_area = new QScrollArea(this); + QWidget* mapping_widget = new QWidget(m_mapping_scroll_area); + QVBoxLayout* mapping_layout = new QVBoxLayout(mapping_widget); + mapping_widget->setLayout(mapping_layout); + m_mapping_scroll_area->setWidget(mapping_widget); + m_mapping_scroll_area->setWidgetResizable(true); + m_mapping_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_mapping_scroll_area->setMinimumHeight(400); + m_mapping_scroll_area->setMinimumWidth(700); + + v_layout->addWidget(m_mapping_scroll_area); + + QLabel* axis_label = new QLabel(QString("Axes:"), mapping_widget); + mapping_layout->addWidget(axis_label); + + auto add_mapping_setting = [mapping_widget, this, mapping_layout](Mapping*& target, bool is_axis, const char* display_name, bool flip_axis_display) + { + target = new Mapping(mapping_widget, this, is_axis, display_name, flip_axis_display); + mapping_layout->addWidget(target); + }; + + add_mapping_setting(m_steering, true, "Steering", false); + add_mapping_setting(m_throttle, true, "Throttle", true); + add_mapping_setting(m_brake, true, "Brake", true); + add_mapping_setting(m_clutch, true, "Clutch", true); + + QLabel* button_label = new QLabel(QString("Buttons:"), mapping_widget); + mapping_layout->addWidget(button_label); + + add_mapping_setting(m_shift_up, false, "Shift up", false); + add_mapping_setting(m_shift_down, false, "Shift down", false); + + add_mapping_setting(m_up, false, "Up", false); + add_mapping_setting(m_down, false, "Down", false); + add_mapping_setting(m_left, false, "Left", false); + add_mapping_setting(m_right, false, "Right", false); + + add_mapping_setting(m_triangle, false, "Triangle", false); + add_mapping_setting(m_cross, false, "Cross", false); + add_mapping_setting(m_square, false, "Square", false); + add_mapping_setting(m_circle, false, "Circle", false); + + add_mapping_setting(m_l2, false, "L2", false); + add_mapping_setting(m_l3, false, "L3", false); + add_mapping_setting(m_r2, false, "R2", false); + add_mapping_setting(m_r3, false, "R3", false); + + add_mapping_setting(m_plus, false, "L4", false); + add_mapping_setting(m_minus, false, "L5", false); + + add_mapping_setting(m_dial_clockwise, false, "R4", false); + add_mapping_setting(m_dial_anticlockwise, false, "R5", false); + + add_mapping_setting(m_select, false, "Select", false); + add_mapping_setting(m_pause, false, "Start", false); + + add_mapping_setting(m_shifter_1, false, "Gear 1", false); + add_mapping_setting(m_shifter_2, false, "Gear 2", false); + add_mapping_setting(m_shifter_3, false, "Gear 3", false); + add_mapping_setting(m_shifter_4, false, "Gear 4", false); + add_mapping_setting(m_shifter_5, false, "Gear 5", false); + add_mapping_setting(m_shifter_6, false, "Gear 6", false); + add_mapping_setting(m_shifter_r, false, "Gear R", false); + + v_layout->addSpacing(20); + + v_layout->addWidget(m_ffb_device); + v_layout->addWidget(m_led_device); + + v_layout->addWidget(buttons); + setLayout(v_layout); + + load_ui_state_from_config(); + + m_sdl_initialized = sdl_instance::get_instance().initialize(); + + if (m_sdl_initialized) + get_joystick_states(); +} + +emulated_logitech_g27_settings_dialog::~emulated_logitech_g27_settings_dialog() +{ + for (auto joystick_handle : m_joystick_handles) + { + if (joystick_handle) + SDL_CloseJoystick(reinterpret_cast(joystick_handle)); + } +} + +static inline hat_component get_sdl_hat_component(uint8_t sdl_hat) +{ + if (sdl_hat & SDL_HAT_UP) + { + return HAT_UP; + } + + if (sdl_hat & SDL_HAT_DOWN) + { + return HAT_DOWN; + } + + if (sdl_hat & SDL_HAT_LEFT) + { + return HAT_LEFT; + } + + if (sdl_hat & SDL_HAT_RIGHT) + { + return HAT_RIGHT; + } + + return HAT_NONE; +} + +const std::map& emulated_logitech_g27_settings_dialog::get_joystick_states() +{ + if (!m_sdl_initialized) + { + return m_last_joystick_states; + } + + uint64_t now = SDL_GetTicks(); + + if (SDL_GetTicks() - m_last_joystick_states_update < 25) + { + return m_last_joystick_states; + } + + m_last_joystick_states_update = now; + + std::map new_joystick_states; + + sdl_instance::get_instance().pump_events(); + + int joystick_count; + SDL_JoystickID* joystick_ids = SDL_GetJoysticks(&joystick_count); + + std::vector new_joystick_handles; + + if (joystick_ids != nullptr) + { + for (int i = 0; i < joystick_count; i++) + { + SDL_Joystick* cur_joystick = SDL_OpenJoystick(joystick_ids[i]); + if (cur_joystick == nullptr) + { + continue; + } + new_joystick_handles.push_back(cur_joystick); + + uint32_t device_type_id = (SDL_GetJoystickVendor(cur_joystick) << 16) | SDL_GetJoystickProduct(cur_joystick); + + auto cur_state = new_joystick_states.find(device_type_id); + if (cur_state == new_joystick_states.end()) + { + joystick_state s; + int num_axes = SDL_GetNumJoystickAxes(cur_joystick); + int num_buttons = SDL_GetNumJoystickButtons(cur_joystick); + int num_hats = SDL_GetNumJoystickHats(cur_joystick); + for (int j = 0; j < num_axes; j++) + { + s.axes.push_back(SDL_GetJoystickAxis(cur_joystick, j)); + } + for (int j = 0; j < num_buttons; j++) + { + s.buttons.push_back(SDL_GetJoystickButton(cur_joystick, j)); + } + for (int j = 0; j < num_hats; j++) + { + uint8_t sdl_hat = SDL_GetJoystickHat(cur_joystick, j); + s.hats.push_back(get_sdl_hat_component(sdl_hat)); + } + new_joystick_states[device_type_id] = s; + } + else + { + for (std::vector::size_type j = 0; j < cur_state->second.axes.size(); j++) + { + cur_state->second.axes[j] = (cur_state->second.axes[j] + SDL_GetJoystickAxis(cur_joystick, j)) / 2; + } + for (std::vector::size_type j = 0; j < cur_state->second.buttons.size(); j++) + { + cur_state->second.buttons[j] = cur_state->second.buttons[j] || SDL_GetJoystickButton(cur_joystick, j); + } + for (std::vector::size_type j = 0; j < cur_state->second.hats.size(); j++) + { + if (cur_state->second.hats[j] != HAT_NONE) + continue; + uint8_t sdl_hat = SDL_GetJoystickHat(cur_joystick, j); + cur_state->second.hats[j] = get_sdl_hat_component(sdl_hat); + } + } + } + } + + for (auto joystick_handle : m_joystick_handles) + { + if (joystick_handle) + SDL_CloseJoystick(reinterpret_cast(joystick_handle)); + } + + m_joystick_handles = new_joystick_handles; + m_last_joystick_states = new_joystick_states; + + return m_last_joystick_states; +} + +void emulated_logitech_g27_settings_dialog::set_state_text(const char* text) +{ + m_state_text->setText(QString(text)); +} + +void emulated_logitech_g27_settings_dialog::set_enable(bool enable) +{ + const int slider_position = m_mapping_scroll_area->verticalScrollBar()->sliderPosition(); + + m_steering->set_enable(enable); + m_throttle->set_enable(enable); + m_brake->set_enable(enable); + m_clutch->set_enable(enable); + m_shift_up->set_enable(enable); + m_shift_down->set_enable(enable); + + m_up->set_enable(enable); + m_down->set_enable(enable); + m_left->set_enable(enable); + m_right->set_enable(enable); + + m_triangle->set_enable(enable); + m_cross->set_enable(enable); + m_square->set_enable(enable); + m_circle->set_enable(enable); + + m_l2->set_enable(enable); + m_l3->set_enable(enable); + m_r2->set_enable(enable); + m_r3->set_enable(enable); + + m_plus->set_enable(enable); + m_minus->set_enable(enable); + + m_dial_clockwise->set_enable(enable); + m_dial_anticlockwise->set_enable(enable); + + m_select->set_enable(enable); + m_pause->set_enable(enable); + + m_shifter_1->set_enable(enable); + m_shifter_2->set_enable(enable); + m_shifter_3->set_enable(enable); + m_shifter_4->set_enable(enable); + m_shifter_5->set_enable(enable); + m_shifter_6->set_enable(enable); + m_shifter_r->set_enable(enable); + + m_enabled->setEnabled(enable); + m_reverse_effects->setEnabled(enable); + + m_ffb_device->set_enable(enable); + m_led_device->set_enable(enable); + + m_mapping_scroll_area->verticalScrollBar()->setEnabled(enable); + m_mapping_scroll_area->verticalScrollBar()->setSliderPosition(slider_position); +} + +#else + +// minimal symbols for sdl-less builds automoc +#include "emulated_logitech_g27_settings_dialog.h" + +emulated_logitech_g27_settings_dialog::emulated_logitech_g27_settings_dialog(QWidget* parent) + : QDialog(parent) {} +emulated_logitech_g27_settings_dialog::~emulated_logitech_g27_settings_dialog() {}; + +#endif diff --git a/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h new file mode 100644 index 0000000000..ef28f2eff7 --- /dev/null +++ b/rpcs3/rpcs3qt/emulated_logitech_g27_settings_dialog.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef HAVE_SDL3 +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif +#endif + +#include +#include +#include + +struct joystick_state +{ + std::vector axes; + std::vector buttons; + std::vector hats; +}; + +class Mapping; +class DeviceChoice; + +class emulated_logitech_g27_settings_dialog : public QDialog +{ + Q_OBJECT + +public: + emulated_logitech_g27_settings_dialog(QWidget* parent = nullptr); + ~emulated_logitech_g27_settings_dialog(); + void set_state_text(const char*); + const std::map& get_joystick_states(); + void set_enable(bool enable); + +private: + std::map m_last_joystick_states; + // hack: need a completed dummy class when linking automoc generated with sdl-less build + std::vector m_joystick_handles; + uint64_t m_last_joystick_states_update = 0; + bool m_sdl_initialized = false; + + // ui elements + QLabel* m_state_text = nullptr; + + QCheckBox* m_enabled = nullptr; + QCheckBox* m_reverse_effects = nullptr; + + Mapping* m_steering = nullptr; + Mapping* m_throttle = nullptr; + Mapping* m_brake = nullptr; + Mapping* m_clutch = nullptr; + Mapping* m_shift_up = nullptr; + Mapping* m_shift_down = nullptr; + + Mapping* m_up = nullptr; + Mapping* m_down = nullptr; + Mapping* m_left = nullptr; + Mapping* m_right = nullptr; + + Mapping* m_triangle = nullptr; + Mapping* m_cross = nullptr; + Mapping* m_square = nullptr; + Mapping* m_circle = nullptr; + + Mapping* m_l2 = nullptr; + Mapping* m_l3 = nullptr; + Mapping* m_r2 = nullptr; + Mapping* m_r3 = nullptr; + + Mapping* m_plus = nullptr; + Mapping* m_minus = nullptr; + + Mapping* m_dial_clockwise = nullptr; + Mapping* m_dial_anticlockwise = nullptr; + + Mapping* m_select = nullptr; + Mapping* m_pause = nullptr; + + Mapping* m_shifter_1 = nullptr; + Mapping* m_shifter_2 = nullptr; + Mapping* m_shifter_3 = nullptr; + Mapping* m_shifter_4 = nullptr; + Mapping* m_shifter_5 = nullptr; + Mapping* m_shifter_6 = nullptr; + Mapping* m_shifter_r = nullptr; + + DeviceChoice* m_ffb_device = nullptr; + DeviceChoice* m_led_device = nullptr; + + QScrollArea* m_mapping_scroll_area = nullptr; + + void load_ui_state_from_config(); + void save_ui_state_to_config(); +}; diff --git a/rpcs3/rpcs3qt/hex_validator.h b/rpcs3/rpcs3qt/hex_validator.h index 513e5b1545..e11624483d 100644 --- a/rpcs3/rpcs3qt/hex_validator.h +++ b/rpcs3/rpcs3qt/hex_validator.h @@ -44,7 +44,7 @@ public: if (!ok) return QValidator::Invalid; - if (m_max_bits < 64) + if (m_max_bits < 64) { const qulonglong max_val = (qulonglong(1) << m_max_bits) - 1; if (value > max_val) diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 5fd5bac9c6..1326f112d9 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -38,6 +38,7 @@ #include "shortcut_dialog.h" #include "system_cmd_dialog.h" #include "emulated_pad_settings_dialog.h" +#include "emulated_logitech_g27_settings_dialog.h" #include "basic_mouse_settings_dialog.h" #include "vfs_tool_dialog.h" #include "welcome_dialog.h" @@ -2902,6 +2903,16 @@ void main_window::CreateConnects() dlg->show(); }); +#ifndef HAVE_SDL3 + ui->confLogitechG27Act->setVisible(false); +#else + connect(ui->confLogitechG27Act, &QAction::triggered, this, [this] + { + emulated_logitech_g27_settings_dialog* dlg = new emulated_logitech_g27_settings_dialog(this); + dlg->show(); + }); +#endif + connect(ui->actionBasic_Mouse, &QAction::triggered, this, [this] { basic_mouse_settings_dialog* dlg = new basic_mouse_settings_dialog(this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 845874d00b..91654cbb67 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -257,6 +257,7 @@ + @@ -1366,6 +1367,11 @@ Top Shot Fearmaster + + + Logitech G27 Wheel + + Basic Mouse diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index 00b62f6636..05ca115d7f 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -10,6 +10,9 @@ x64 + + false + {d1cbf84e-07f8-4acb-9cd2-bd205fdeee1e} Application @@ -37,6 +40,7 @@ + false X64;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) Level3 ProgramDatabase @@ -47,8 +51,7 @@ rpcs3.lib;opencv_world4100.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;OGLCompiler.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) $(SolutionDir)3rdparty\opencv\opencv\opencv410\build\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib_ext\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) Console - false - 0x10000 + true @@ -60,6 +63,7 @@ Disabled + false X64;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) Level3 $(IntDir) @@ -69,8 +73,7 @@ rpcs3d.lib;opencv_world4100.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;OGLCompilerd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) $(SolutionDir)3rdparty\opencv\opencv\opencv410\build\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) Console - false - 0x10000 + true @@ -79,12 +82,13 @@ - + - + + @@ -97,4 +101,4 @@ - \ No newline at end of file + diff --git a/rpcs3/tests/test_fmt.cpp b/rpcs3/tests/test_fmt.cpp index 5ffa842567..e4988b24c6 100644 --- a/rpcs3/tests/test_fmt.cpp +++ b/rpcs3/tests/test_fmt.cpp @@ -145,6 +145,274 @@ namespace fmt EXPECT_EQ(str, fmt::truncate(str, str.size() + 1)); } + TEST(StrUtil, test_replace_all) + { + EXPECT_EQ(""s, fmt::replace_all("", "", "")); + EXPECT_EQ(""s, fmt::replace_all("", "", " ")); + EXPECT_EQ(""s, fmt::replace_all("", "", "word")); + EXPECT_EQ(""s, fmt::replace_all("", " ", "")); + EXPECT_EQ(""s, fmt::replace_all("", "word", "")); + EXPECT_EQ(""s, fmt::replace_all("", "word", "drow")); + + EXPECT_EQ(" "s, fmt::replace_all(" ", "", "")); + EXPECT_EQ(" "s, fmt::replace_all(" ", "", " ")); + EXPECT_EQ(" "s, fmt::replace_all(" ", "", "word")); + EXPECT_EQ(""s, fmt::replace_all(" ", " ", "")); + EXPECT_EQ(" "s, fmt::replace_all(" ", "word", "")); + EXPECT_EQ(" "s, fmt::replace_all(" ", "word", "drow")); + + EXPECT_EQ("word"s, fmt::replace_all("word", "", "")); + EXPECT_EQ("word"s, fmt::replace_all("word", "", " ")); + EXPECT_EQ("word"s, fmt::replace_all("word", "", "word")); + EXPECT_EQ("word"s, fmt::replace_all("word", " ", "")); + EXPECT_EQ(""s, fmt::replace_all("word", "word", "")); + EXPECT_EQ("drow"s, fmt::replace_all("word", "word", "drow")); + + EXPECT_EQ(" word "s, fmt::replace_all(" word ", "", "")); + EXPECT_EQ(" word "s, fmt::replace_all(" word ", "", " ")); + EXPECT_EQ(" word "s, fmt::replace_all(" word ", "", "word")); + EXPECT_EQ("word"s, fmt::replace_all(" word ", " ", "")); + EXPECT_EQ(" "s, fmt::replace_all(" word ", "word", "")); + EXPECT_EQ(" drow "s, fmt::replace_all(" word ", "word", "drow")); + + EXPECT_EQ("word word"s, fmt::replace_all("word word", "", "")); + EXPECT_EQ("word word"s, fmt::replace_all("word word", "", " ")); + EXPECT_EQ("word word"s, fmt::replace_all("word word", "", "word")); + EXPECT_EQ("wordword"s, fmt::replace_all("word word", " ", "")); + EXPECT_EQ(" "s, fmt::replace_all("word word", "word", "")); + EXPECT_EQ("drow drow"s, fmt::replace_all("word word", "word", "drow")); + + // Test count + EXPECT_EQ("word word word"s, fmt::replace_all("word word word", "word", "drow", 0)); + EXPECT_EQ("drow word word"s, fmt::replace_all("word word word", "word", "drow", 1)); + EXPECT_EQ("drow drow word"s, fmt::replace_all("word word word", "word", "drow", 2)); + EXPECT_EQ("drow drow drow"s, fmt::replace_all("word word word", "word", "drow", 3)); + EXPECT_EQ("drow drow drow"s, fmt::replace_all("word word word", "word", "drow", umax)); + EXPECT_EQ("drow drow drow"s, fmt::replace_all("word word word", "word", "drow", -1)); + } + + TEST(StrUtil, test_split) + { + using vec = std::vector; + + EXPECT_EQ(vec{""}, fmt::split("", {}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {""}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {" "}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {"a"}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {"a "}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {"a b"}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {"a", " "}, false)); + EXPECT_EQ(vec{""}, fmt::split("", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{" "}, fmt::split(" ", {}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {""}, false)); + EXPECT_EQ(vec{""}, fmt::split(" ", {" "}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a"}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a "}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a b"}, false)); + EXPECT_EQ(vec{""}, fmt::split(" ", {"a", " "}, false)); + EXPECT_EQ(vec{""}, fmt::split(" ", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{" "}, fmt::split(" ", {}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {""}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split(" ", {" "}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a"}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a "}, false)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a b"}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split(" ", {"a", " "}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split(" ", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{"a"}, fmt::split("a", {}, false)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {""}, false)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {" "}, false)); + EXPECT_EQ(vec{""}, fmt::split("a", {"a"}, false)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {"a "}, false)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {"a b"}, false)); + EXPECT_EQ(vec{""}, fmt::split("a", {"a", " "}, false)); + EXPECT_EQ(vec{""}, fmt::split("a", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {}, false)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {""}, false)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {" "}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split("aa", {"a"}, false)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {"a "}, false)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {"a b"}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split("aa", {"a", " "}, false)); + EXPECT_EQ(vec({"", ""}), fmt::split("aa", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{"a b"}, fmt::split("a b", {}, false)); + EXPECT_EQ(vec{"a b"}, fmt::split("a b", {""}, false)); + EXPECT_EQ(vec({"a", "b"}), fmt::split("a b", {" "}, false)); + EXPECT_EQ(vec({"", " b"}), fmt::split("a b", {"a"}, false)); + EXPECT_EQ(vec({"", "b"}), fmt::split("a b", {"a "}, false)); + EXPECT_EQ(vec{""}, fmt::split("a b", {"a b"}, false)); + EXPECT_EQ(vec({"", "", "b"}), fmt::split("a b", {"a", " "}, false)); + EXPECT_EQ(vec({"", "", ""}), fmt::split("a b", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{"a b c c b a"}, fmt::split("a b c c b a", {}, false)); + EXPECT_EQ(vec{"a b c c b a"}, fmt::split("a b c c b a", {""}, false)); + EXPECT_EQ(vec({"a", "b", "c", "c", "b", "a"}), fmt::split("a b c c b a", {" "}, false)); + EXPECT_EQ(vec({"", " b c c b "}), fmt::split("a b c c b a", {"a"}, false)); + EXPECT_EQ(vec({"", "b c c b a"}), fmt::split("a b c c b a", {"a "}, false)); + EXPECT_EQ(vec({"", " c c b a"}), fmt::split("a b c c b a", {"a b"}, false)); + EXPECT_EQ(vec({"", "", "b", "c", "c", "b", ""}), fmt::split("a b c c b a", {"a", " "}, false)); + EXPECT_EQ(vec({"", "", "", "", "c", "c", "", "", ""}), fmt::split("a b c c b a", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {}, false)); + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {""}, false)); + EXPECT_EQ(vec({"", "This", "is", "a", "test!"}), fmt::split(" This is a test! ", {" "}, false)); + EXPECT_EQ(vec({" This is ", " test! "}), fmt::split(" This is a test! ", {"a"}, false)); + EXPECT_EQ(vec({" This is ", "test! "}), fmt::split(" This is a test! ", {"a "}, false)); + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {"a b"}, false)); + EXPECT_EQ(vec({"", "This", "is", "", "", "test!"}), fmt::split(" This is a test! ", {"a", " "}, false)); + EXPECT_EQ(vec({"", "This", "is", "", "", "test!"}), fmt::split(" This is a test! ", {"a", " ", "b"}, false)); + + EXPECT_EQ(vec{}, fmt::split("", {}, true)); + EXPECT_EQ(vec{}, fmt::split("", {""}, true)); + EXPECT_EQ(vec{}, fmt::split("", {" "}, true)); + EXPECT_EQ(vec{}, fmt::split("", {"a"}, true)); + EXPECT_EQ(vec{}, fmt::split("", {"a "}, true)); + EXPECT_EQ(vec{}, fmt::split("", {"a b"}, true)); + EXPECT_EQ(vec{}, fmt::split("", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split("", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{" "}, fmt::split(" ", {}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {""}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {" "}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a"}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a "}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a b"}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{" "}, fmt::split(" ", {}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {""}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {" "}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a"}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a "}, true)); + EXPECT_EQ(vec{" "}, fmt::split(" ", {"a b"}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split(" ", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{"a"}, fmt::split("a", {}, true)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {""}, true)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {" "}, true)); + EXPECT_EQ(vec{}, fmt::split("a", {"a"}, true)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {"a "}, true)); + EXPECT_EQ(vec{"a"}, fmt::split("a", {"a b"}, true)); + EXPECT_EQ(vec{}, fmt::split("a", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split("a", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {}, true)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {""}, true)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {" "}, true)); + EXPECT_EQ(vec{}, fmt::split("aa", {"a"}, true)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {"a "}, true)); + EXPECT_EQ(vec{"aa"}, fmt::split("aa", {"a b"}, true)); + EXPECT_EQ(vec{}, fmt::split("aa", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split("aa", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{"a b"}, fmt::split("a b", {}, true)); + EXPECT_EQ(vec{"a b"}, fmt::split("a b", {""}, true)); + EXPECT_EQ(vec({"a", "b"}), fmt::split("a b", {" "}, true)); + EXPECT_EQ(vec{" b"}, fmt::split("a b", {"a"}, true)); + EXPECT_EQ(vec{"b"}, fmt::split("a b", {"a "}, true)); + EXPECT_EQ(vec{}, fmt::split("a b", {"a b"}, true)); + EXPECT_EQ(vec{"b"}, fmt::split("a b", {"a", " "}, true)); + EXPECT_EQ(vec{}, fmt::split("a b", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{"a b c c b a"}, fmt::split("a b c c b a", {}, true)); + EXPECT_EQ(vec{"a b c c b a"}, fmt::split("a b c c b a", {""}, true)); + EXPECT_EQ(vec({"a", "b", "c", "c", "b", "a"}), fmt::split("a b c c b a", {" "}, true)); + EXPECT_EQ(vec{" b c c b "}, fmt::split("a b c c b a", {"a"}, true)); + EXPECT_EQ(vec{"b c c b a"}, fmt::split("a b c c b a", {"a "}, true)); + EXPECT_EQ(vec{" c c b a"}, fmt::split("a b c c b a", {"a b"}, true)); + EXPECT_EQ(vec({"b", "c", "c", "b"}), fmt::split("a b c c b a", {"a", " "}, true)); + EXPECT_EQ(vec({"c", "c"}), fmt::split("a b c c b a", {"a", " ", "b"}, true)); + + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {}, true)); + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {""}, true)); + EXPECT_EQ(vec({"This", "is", "a", "test!"}), fmt::split(" This is a test! ", {" "}, true)); + EXPECT_EQ(vec({" This is ", " test! "}), fmt::split(" This is a test! ", {"a"}, true)); + EXPECT_EQ(vec({" This is ", "test! "}), fmt::split(" This is a test! ", {"a "}, true)); + EXPECT_EQ(vec{" This is a test! "}, fmt::split(" This is a test! ", {"a b"}, true)); + EXPECT_EQ(vec({"This", "is", "test!"}), fmt::split(" This is a test! ", {"a", " "}, true)); + EXPECT_EQ(vec({"This", "is", "test!"}), fmt::split(" This is a test! ", {"a", " ", "b"}, true)); + } + + TEST(StrUtil, test_merge) + { + using vec = std::vector; + using lst = std::initializer_list>; + + // Vector of strings + EXPECT_EQ(""s, fmt::merge(vec{}, "")); + EXPECT_EQ(""s, fmt::merge(vec{}, " ")); + EXPECT_EQ(""s, fmt::merge(vec{}, "-")); + EXPECT_EQ(""s, fmt::merge(vec{}, " *-* ")); + + EXPECT_EQ(""s, fmt::merge(vec{""}, "")); + EXPECT_EQ(""s, fmt::merge(vec{""}, " ")); + EXPECT_EQ(""s, fmt::merge(vec{""}, "-")); + EXPECT_EQ(""s, fmt::merge(vec{""}, " *-* ")); + + EXPECT_EQ("a"s, fmt::merge(vec{"a"}, "")); + EXPECT_EQ("a"s, fmt::merge(vec{"a"}, " ")); + EXPECT_EQ("a"s, fmt::merge(vec{"a"}, "-")); + EXPECT_EQ("a"s, fmt::merge(vec{"a"}, " *-* ")); + + EXPECT_EQ("ab"s, fmt::merge(vec{"a", "b"}, "")); + EXPECT_EQ("a b"s, fmt::merge(vec{"a", "b"}, " ")); + EXPECT_EQ("a-b"s, fmt::merge(vec{"a", "b"}, "-")); + EXPECT_EQ("a *-* b"s, fmt::merge(vec{"a", "b"}, " *-* ")); + + EXPECT_EQ("abc"s, fmt::merge(vec{"a", "b", "c"}, "")); + EXPECT_EQ("a b c"s, fmt::merge(vec{"a", "b", "c"}, " ")); + EXPECT_EQ("a-b-c"s, fmt::merge(vec{"a", "b", "c"}, "-")); + EXPECT_EQ("a *-* b *-* c"s, fmt::merge(vec{"a", "b", "c"}, " *-* ")); + + // Initializer list of vector of strings + EXPECT_EQ(""s, fmt::merge(lst{}, "")); + EXPECT_EQ(""s, fmt::merge(lst{}, " ")); + EXPECT_EQ(""s, fmt::merge(lst{}, "-")); + EXPECT_EQ(""s, fmt::merge(lst{}, " *-* ")); + + EXPECT_EQ(""s, fmt::merge(lst{vec{}}, "")); + EXPECT_EQ(""s, fmt::merge(lst{vec{}}, " ")); + EXPECT_EQ(""s, fmt::merge(lst{vec{}}, "-")); + EXPECT_EQ(""s, fmt::merge(lst{vec{}}, " *-* ")); + + EXPECT_EQ("a"s, fmt::merge(lst{vec{"a"}}, "")); + EXPECT_EQ("a"s, fmt::merge(lst{vec{"a"}}, " ")); + EXPECT_EQ("a"s, fmt::merge(lst{vec{"a"}}, "-")); + EXPECT_EQ("a"s, fmt::merge(lst{vec{"a"}}, " *-* ")); + + EXPECT_EQ("ab"s, fmt::merge(lst{vec{"a", "b"}}, "")); + EXPECT_EQ("a b"s, fmt::merge(lst{vec{"a", "b"}}, " ")); + EXPECT_EQ("a-b"s, fmt::merge(lst{vec{"a", "b"}}, "-")); + EXPECT_EQ("a *-* b"s, fmt::merge(lst{vec{"a", "b"}}, " *-* ")); + + EXPECT_EQ("abc"s, fmt::merge(lst{vec{"a", "b", "c"}}, "")); + EXPECT_EQ("a b c"s, fmt::merge(lst{vec{"a", "b", "c"}}, " ")); + EXPECT_EQ("a-b-c"s, fmt::merge(lst{vec{"a", "b", "c"}}, "-")); + EXPECT_EQ("a *-* b *-* c"s, fmt::merge(lst{vec{"a", "b", "c"}}, " *-* ")); + + EXPECT_EQ("ab"s, fmt::merge(lst{vec{"a"}, vec{"b"}}, "")); + EXPECT_EQ("a b"s, fmt::merge(lst{vec{"a"}, vec{"b"}}, " ")); + EXPECT_EQ("a-b"s, fmt::merge(lst{vec{"a"}, vec{"b"}}, "-")); + EXPECT_EQ("a *-* b"s, fmt::merge(lst{vec{"a"}, vec{"b"}}, " *-* ")); + + EXPECT_EQ("abc"s, fmt::merge(lst{vec{"a"}, vec{"b"}, vec{"c"}}, "")); + EXPECT_EQ("a b c"s, fmt::merge(lst{vec{"a"}, vec{"b"}, vec{"c"}}, " ")); + EXPECT_EQ("a-b-c"s, fmt::merge(lst{vec{"a"}, vec{"b"}, vec{"c"}}, "-")); + EXPECT_EQ("a *-* b *-* c"s, fmt::merge(lst{vec{"a"}, vec{"b"}, vec{"c"}}, " *-* ")); + + EXPECT_EQ("a1b2"s, fmt::merge(lst{vec{"a", "1"}, vec{"b", "2"}}, "")); + EXPECT_EQ("a 1 b 2"s, fmt::merge(lst{vec{"a", "1"}, vec{"b", "2"}}, " ")); + EXPECT_EQ("a-1-b-2"s, fmt::merge(lst{vec{"a", "1"}, vec{"b", "2"}}, "-")); + EXPECT_EQ("a *-* 1 *-* b *-* 2"s, fmt::merge(lst{vec{"a", "1"}, vec{"b", "2"}}, " *-* ")); + } + TEST(StrUtil, test_get_file_extension) { EXPECT_EQ(""s, get_file_extension("")); diff --git a/rpcs3/tests/test_simple_array.cpp b/rpcs3/tests/test_simple_array.cpp new file mode 100644 index 0000000000..f64e01200e --- /dev/null +++ b/rpcs3/tests/test_simple_array.cpp @@ -0,0 +1,192 @@ +#include + +#define private public +#include "Emu/RSX/Common/simple_array.hpp" +#undef private + +namespace rsx +{ + TEST(SimpleArray, DefaultConstructor) + { + rsx::simple_array arr; + + EXPECT_TRUE(arr.empty()); + EXPECT_EQ(arr.size(), 0); + EXPECT_GE(arr.capacity(), 1u); + } + + TEST(SimpleArray, InitialSizeConstructor) + { + rsx::simple_array arr(5); + + EXPECT_FALSE(arr.empty()); + EXPECT_EQ(arr.size(), 5); + EXPECT_GE(arr.capacity(), 5u); + } + + TEST(SimpleArray, InitialSizeValueConstructor) + { + rsx::simple_array arr(3, 42); + + EXPECT_EQ(arr.size(), 3); + for (int i = 0; i < 3; ++i) + { + EXPECT_EQ(arr[i], 42); + } + } + + TEST(SimpleArray, InitializerListConstructor) + { + rsx::simple_array arr{ 1, 2, 3, 4, 5 }; + + EXPECT_EQ(arr.size(), 5); + for (int i = 0; i < 5; ++i) + { + EXPECT_EQ(arr[i], i + 1); + } + } + + TEST(SimpleArray, CopyConstructor) + { + rsx::simple_array arr1{ 1, 2, 3 }; + rsx::simple_array arr2(arr1); + + EXPECT_EQ(arr1.size(), arr2.size()); + for (u32 i = 0; i < arr1.size(); ++i) + { + EXPECT_EQ(arr1[i], arr2[i]); + } + } + + TEST(SimpleArray, MoveConstructor) + { + rsx::simple_array arr1{ 1, 2, 3 }; + u32 original_size = arr1.size(); + rsx::simple_array arr2(std::move(arr1)); + + EXPECT_EQ(arr2.size(), original_size); + EXPECT_TRUE(arr1.empty()); + } + + TEST(SimpleArray, PushBackAndAccess) + { + rsx::simple_array arr; + arr.push_back(1); + arr.push_back(2); + arr.push_back(3); + + EXPECT_EQ(arr.size(), 3); + EXPECT_EQ(arr[0], 1); + EXPECT_EQ(arr[1], 2); + EXPECT_EQ(arr[2], 3); + EXPECT_EQ(arr.front(), 1); + EXPECT_EQ(arr.back(), 3); + } + + TEST(SimpleArray, PopBack) + { + rsx::simple_array arr{ 1, 2, 3 }; + + EXPECT_EQ(arr.pop_back(), 3); + EXPECT_EQ(arr.size(), 2); + EXPECT_EQ(arr.back(), 2); + } + + TEST(SimpleArray, Insert) + { + rsx::simple_array arr{ 1, 3, 4 }; + auto it = arr.insert(arr.begin() + 1, 2); + + EXPECT_EQ(*it, 2); + EXPECT_EQ(arr.size(), 4); + + for (int i = 0; i < 4; ++i) + { + EXPECT_EQ(arr[i], i + 1); + } + } + + TEST(SimpleArray, Clear) + { + rsx::simple_array arr{ 1, 2, 3 }; + arr.clear(); + + EXPECT_TRUE(arr.empty()); + EXPECT_EQ(arr.size(), 0); + } + + TEST(SimpleArray, SmallBufferOptimization) + { + // Test with a small type that should use stack storage + rsx::simple_array small_arr(3, 'a'); + EXPECT_TRUE(small_arr.is_local_storage()); + + // Test with a larger type or more elements that should use heap storage + struct LargeType { char data[128]; }; + rsx::simple_array large_arr(10); + EXPECT_FALSE(large_arr.is_local_storage()); + } + + TEST(SimpleArray, Iterator) + { + rsx::simple_array arr{ 1, 2, 3, 4, 5 }; + int sum = 0; + for (const auto& val : arr) + { + sum += val; + } + + EXPECT_EQ(sum, 15); + } + + TEST(SimpleArray, EraseIf) + { + rsx::simple_array arr{ 1, 2, 3, 4, 5 }; + bool modified = arr.erase_if([](const int& val) { return val % 2 == 0; }); + arr.sort(FN(x < y)); + + EXPECT_TRUE(modified); + EXPECT_EQ(arr.size(), 3); + EXPECT_EQ(arr[0], 1); + EXPECT_EQ(arr[1], 3); + EXPECT_EQ(arr[2], 5); + } + + TEST(SimpleArray, Map) + { + rsx::simple_array arr{ 1, 2, 3 }; + auto result = arr.map([](const int& val) { return val * 2; }); + + EXPECT_EQ(result.size(), 3); + EXPECT_EQ(result[0], 2); + EXPECT_EQ(result[1], 4); + EXPECT_EQ(result[2], 6); + } + + TEST(SimpleArray, Reduce) + { + rsx::simple_array arr{ 1, 2, 3, 4, 5 }; + int sum = arr.reduce(0, [](const int& acc, const int& val) { return acc + val; }); + + EXPECT_EQ(sum, 15); + } + + TEST(SimpleArray, Any) + { + rsx::simple_array arr{ 1, 2, 3, 4, 5 }; + + EXPECT_TRUE(arr.any([](const int& val) { return val > 3; })); + EXPECT_FALSE(arr.any([](const int& val) { return val > 5; })); + } + + TEST(SimpleArray, Sort) + { + rsx::simple_array arr{ 5, 3, 1, 4, 2 }; + arr.sort([](const int& a, const int& b) { return a < b; }); + + for (u32 i = 0; i < arr.size(); ++i) + { + EXPECT_EQ(arr[i], i + 1); + } + } +}