diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72bbcf52..6ae4b892 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: - name: "Install system dependencies" run: | sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev nasm ninja-build + sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libudev-dev nasm ninja-build libbluetooth-dev - name: "Setup cmake" uses: jwlawson/actions-setup-cmake@v2 @@ -96,7 +96,7 @@ jobs: - name: "Install system dependencies" run: | sudo apt update -qq - sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev nasm ninja-build appstream + sudo apt install -y clang-15 cmake freeglut3-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev nasm ninja-build appstream libbluetooth-dev - name: "Build AppImage" run: | diff --git a/.github/workflows/deploy_experimental_release.yml b/.github/workflows/deploy_release.yml similarity index 98% rename from .github/workflows/deploy_experimental_release.yml rename to .github/workflows/deploy_release.yml index 97e0c69e..2b9ee491 100644 --- a/.github/workflows/deploy_experimental_release.yml +++ b/.github/workflows/deploy_release.yml @@ -1,4 +1,4 @@ -name: Deploy experimental release +name: Deploy release on: workflow_dispatch: inputs: @@ -54,7 +54,7 @@ jobs: next_version_major: ${{ needs.calculate-version.outputs.next_version_major }} next_version_minor: ${{ needs.calculate-version.outputs.next_version_minor }} deploy: - name: Deploy experimental release + name: Deploy release runs-on: ubuntu-22.04 needs: [call-release-build, calculate-version] steps: diff --git a/.github/workflows/deploy_stable_release.yml b/.github/workflows/deploy_stable_release.yml deleted file mode 100644 index fd339e7d..00000000 --- a/.github/workflows/deploy_stable_release.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Create new release -on: - workflow_dispatch: - inputs: - PlaceholderInput: - description: PlaceholderInput - required: false -jobs: - call-release-build: - uses: ./.github/workflows/build.yml - with: - deploymode: release - deploy: - name: Deploy release - runs-on: ubuntu-20.04 - needs: call-release-build - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-linux-x64 - path: cemu-bin-linux-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-appimage-x64 - path: cemu-appimage-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-windows-x64 - path: cemu-bin-windows-x64 - - - uses: actions/download-artifact@v4 - with: - name: cemu-bin-macos-x64 - path: cemu-bin-macos-x64 - - - name: Initialize - run: | - mkdir upload - sudo apt update -qq - sudo apt install -y zip - - - name: Get Cemu release version - run: | - gcc -o getversion .github/getversion.cpp - echo "Cemu CI version: $(./getversion)" - echo "CEMU_FOLDER_NAME=Cemu_$(./getversion)" >> $GITHUB_ENV - echo "CEMU_VERSION=$(./getversion)" >> $GITHUB_ENV - - - name: Create release from windows-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-windows-x64/Cemu.exe ./${{ env.CEMU_FOLDER_NAME }}/Cemu.exe - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-windows-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create appimage - run: | - VERSION=${{ env.CEMU_VERSION }} - echo "Cemu Version is $VERSION" - ls cemu-appimage-x64 - mv cemu-appimage-x64/Cemu-*-x86_64.AppImage upload/Cemu-$VERSION-x86_64.AppImage - - - name: Create release from ubuntu-bin - run: | - ls ./ - ls ./bin/ - cp -R ./bin ./${{ env.CEMU_FOLDER_NAME }} - mv cemu-bin-linux-x64/Cemu ./${{ env.CEMU_FOLDER_NAME }}/Cemu - zip -9 -r upload/cemu-${{ env.CEMU_VERSION }}-ubuntu-20.04-x64.zip ${{ env.CEMU_FOLDER_NAME }} - rm -r ./${{ env.CEMU_FOLDER_NAME }} - - - name: Create release from macos-bin - run: cp cemu-bin-macos-x64/Cemu.dmg upload/cemu-${{ env.CEMU_VERSION }}-macos-12-x64.dmg - - - name: Create release - run: | - wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.15.0/ghr_v0.15.0_linux_amd64.tar.gz - tar xvzf ghr.tar.gz; rm ghr.tar.gz - ghr_v0.15.0_linux_amd64/ghr -t ${{ secrets.GITHUB_TOKEN }} -n "Cemu ${{ env.CEMU_VERSION }}" -b "Changelog:" v${{ env.CEMU_VERSION }} ./upload diff --git a/BUILD.md b/BUILD.md index 44d69c6c..41de928e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -46,10 +46,10 @@ To compile Cemu, a recent enough compiler and STL with C++20 support is required ### Dependencies #### For Arch and derivatives: -`sudo pacman -S --needed base-devel clang cmake freeglut git glm gtk3 libgcrypt libpulse libsecret linux-headers llvm nasm ninja systemd unzip zip` +`sudo pacman -S --needed base-devel bluez-libs clang cmake freeglut git glm gtk3 libgcrypt libpulse libsecret linux-headers llvm nasm ninja systemd unzip zip` #### For Debian, Ubuntu and derivatives: -`sudo apt install -y cmake curl clang-15 freeglut3-dev git libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libtool nasm ninja-build` +`sudo apt install -y cmake curl clang-15 freeglut3-dev git libbluetooth-dev libgcrypt20-dev libglm-dev libgtk-3-dev libpulse-dev libsecret-1-dev libsystemd-dev libtool nasm ninja-build` You may also need to install `libusb-1.0-0-dev` as a workaround for an issue with the vcpkg hidapi package. @@ -57,7 +57,7 @@ At Step 3 in [Build Cemu using cmake and clang](#build-cemu-using-cmake-and-clan `cmake -S . -B build -DCMAKE_BUILD_TYPE=release -DCMAKE_C_COMPILER=/usr/bin/clang-15 -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 -G Ninja -DCMAKE_MAKE_PROGRAM=/usr/bin/ninja` #### For Fedora and derivatives: -`sudo dnf install clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel zlib-static` +`sudo dnf install bluez-libs clang cmake cubeb-devel freeglut-devel git glm-devel gtk3-devel kernel-headers libgcrypt-devel libsecret-devel libtool libusb1-devel llvm nasm ninja-build perl-core systemd-devel zlib-devel zlib-static` ### Build Cemu diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b5cff6c..cf04b235 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ endif() if (UNIX AND NOT APPLE) option(ENABLE_WAYLAND "Build with Wayland support" ON) option(ENABLE_FERAL_GAMEMODE "Enables Feral Interactive GameMode Support" ON) + option(ENABLE_BLUEZ "Build with Bluez support" ON) endif() option(ENABLE_OPENGL "Enables the OpenGL backend" ON) @@ -179,6 +180,12 @@ if (UNIX AND NOT APPLE) endif() find_package(GTK3 REQUIRED) + if(ENABLE_BLUEZ) + find_package(bluez REQUIRED) + set(ENABLE_WIIMOTE ON) + add_compile_definitions(HAS_BLUEZ) + endif() + endif() if (ENABLE_VULKAN) diff --git a/bin/resources/ar/‏‏cemu.mo b/bin/resources/ar/‏‏cemu.mo new file mode 100644 index 00000000..4062628b Binary files /dev/null and b/bin/resources/ar/‏‏cemu.mo differ diff --git a/bin/resources/de/cemu.mo b/bin/resources/de/cemu.mo index 8dc4e8cc..cd9edd3c 100644 Binary files a/bin/resources/de/cemu.mo and b/bin/resources/de/cemu.mo differ diff --git a/bin/resources/ru/cemu.mo b/bin/resources/ru/cemu.mo index 4ff04e2b..eb8f372f 100644 Binary files a/bin/resources/ru/cemu.mo and b/bin/resources/ru/cemu.mo differ diff --git a/bin/resources/sv/cemu.mo b/bin/resources/sv/cemu.mo index 3e850b36..c8fd68ee 100644 Binary files a/bin/resources/sv/cemu.mo and b/bin/resources/sv/cemu.mo differ diff --git a/cmake/Findbluez.cmake b/cmake/Findbluez.cmake new file mode 100644 index 00000000..007cdac9 --- /dev/null +++ b/cmake/Findbluez.cmake @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Andrea Pappacoda +# SPDX-License-Identifier: ISC + +find_package(bluez CONFIG) +if (NOT bluez_FOUND) + find_package(PkgConfig) + if (PKG_CONFIG_FOUND) + pkg_search_module(bluez IMPORTED_TARGET GLOBAL bluez-1.0 bluez) + if (bluez_FOUND) + add_library(bluez::bluez ALIAS PkgConfig::bluez) + endif () + endif () +endif () + +find_package_handle_standard_args(bluez + REQUIRED_VARS + bluez_LINK_LIBRARIES + bluez_FOUND + VERSION_VAR bluez_VERSION +) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7d64d91b..79471321 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,8 +82,8 @@ if (MACOS_BUNDLE) set(MACOSX_BUNDLE_ICON_FILE "cemu.icns") set(MACOSX_BUNDLE_GUI_IDENTIFIER "info.cemu.Cemu") set(MACOSX_BUNDLE_BUNDLE_NAME "Cemu") - set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${CMAKE_PROJECT_VERSION}) - set(MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION}) + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") set(MACOSX_BUNDLE_COPYRIGHT "Copyright © 2024 Cemu Project") set(MACOSX_BUNDLE_CATEGORY "public.app-category.games") @@ -101,12 +101,18 @@ if (MACOS_BUNDLE) COMMAND ${CMAKE_COMMAND} ARGS -E copy_directory "${CMAKE_SOURCE_DIR}/bin/${folder}" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/SharedSupport/${folder}") endforeach(folder) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(LIBUSB_PATH "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/debug/lib/libusb-1.0.0.dylib") + else() + set(LIBUSB_PATH "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib") + endif() + add_custom_command (TARGET CemuBin POST_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E copy "/usr/local/lib/libMoltenVK.dylib" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libMoltenVK.dylib" - COMMAND ${CMAKE_COMMAND} ARGS -E copy "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib" + COMMAND ${CMAKE_COMMAND} ARGS -E copy "${LIBUSB_PATH}" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/Frameworks/libusb-1.0.0.dylib" COMMAND ${CMAKE_COMMAND} ARGS -E copy "${CMAKE_SOURCE_DIR}/src/resource/update.sh" "${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/update.sh" COMMAND bash -c "install_name_tool -add_rpath @executable_path/../Frameworks ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}" - COMMAND bash -c "install_name_tool -change /Users/runner/work/Cemu/Cemu/build/vcpkg_installed/x64-osx/lib/libusb-1.0.0.dylib @executable_path/../Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") + COMMAND bash -c "install_name_tool -change ${LIBUSB_PATH} @executable_path/../Frameworks/libusb-1.0.0.dylib ${CMAKE_SOURCE_DIR}/bin/${OUTPUT_NAME}.app/Contents/MacOS/${OUTPUT_NAME}") endif() set_target_properties(CemuBin PROPERTIES diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 91d257b2..76dba007 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -465,6 +465,8 @@ add_library(CemuCafe OS/libs/nsyshid/BackendLibusb.h OS/libs/nsyshid/BackendWindowsHID.cpp OS/libs/nsyshid/BackendWindowsHID.h + OS/libs/nsyshid/Dimensions.cpp + OS/libs/nsyshid/Dimensions.h OS/libs/nsyshid/Infinity.cpp OS/libs/nsyshid/Infinity.h OS/libs/nsyshid/Skylander.cpp @@ -530,6 +532,12 @@ set_property(TARGET CemuCafe PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$data(), execData->size()); sForegroundTitleId = 0xFFFFFFFF00000000ULL | (uint64)h; cemuLog_log(LogType::Force, "Generated placeholder TitleId: {:016x}", sForegroundTitleId); @@ -834,7 +834,7 @@ namespace CafeSystem // load executable SetupExecutable(); InitVirtualMlcStorage(); - return STATUS_CODE::SUCCESS; + return PREPARE_STATUS_CODE::SUCCESS; } void _LaunchTitleThread() diff --git a/src/Cafe/CafeSystem.h b/src/Cafe/CafeSystem.h index c4043a59..e9de8d7d 100644 --- a/src/Cafe/CafeSystem.h +++ b/src/Cafe/CafeSystem.h @@ -15,20 +15,19 @@ namespace CafeSystem virtual void CafeRecreateCanvas() = 0; }; - enum class STATUS_CODE + enum class PREPARE_STATUS_CODE { SUCCESS, INVALID_RPX, UNABLE_TO_MOUNT, // failed to mount through TitleInfo (most likely caused by an invalid or outdated path) - //BAD_META_DATA, - the title list only stores titles with valid meta, so this error code is impossible }; void Initialize(); void SetImplementation(SystemImplementation* impl); void Shutdown(); - STATUS_CODE PrepareForegroundTitle(TitleId titleId); - STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path); + PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId); + PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path); void LaunchForegroundTitle(); bool IsTitleRunning(); diff --git a/src/Cafe/Filesystem/FST/FST.cpp b/src/Cafe/Filesystem/FST/FST.cpp index 570671d4..f1255778 100644 --- a/src/Cafe/Filesystem/FST/FST.cpp +++ b/src/Cafe/Filesystem/FST/FST.cpp @@ -3,8 +3,7 @@ #include "Cemu/ncrypto/ncrypto.h" #include "Cafe/Filesystem/WUD/wud.h" #include "util/crypto/aes128.h" -#include "openssl/evp.h" /* EVP_Digest */ -#include "openssl/sha.h" /* SHA1 / SHA256_DIGEST_LENGTH */ +#include "openssl/sha.h" /* SHA1 / SHA256 */ #include "fstUtil.h" #include "FST.h" @@ -141,7 +140,7 @@ struct DiscPartitionTableHeader static constexpr uint32 MAGIC_VALUE = 0xCCA6E67B; /* +0x00 */ uint32be magic; - /* +0x04 */ uint32be sectorSize; // must be 0x8000? + /* +0x04 */ uint32be blockSize; // must be 0x8000? /* +0x08 */ uint8 partitionTableHash[20]; // hash of the data range at +0x800 to end of sector (0x8000) /* +0x1C */ uint32be numPartitions; }; @@ -164,10 +163,10 @@ struct DiscPartitionHeader static constexpr uint32 MAGIC_VALUE = 0xCC93A4F5; /* +0x00 */ uint32be magic; - /* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE + /* +0x04 */ uint32be sectorSize; // must match DISC_SECTOR_SIZE for hashed blocks /* +0x08 */ uint32be ukn008; - /* +0x0C */ uint32be ukn00C; + /* +0x0C */ uint32be ukn00C; // h3 array size? /* +0x10 */ uint32be h3HashNum; /* +0x14 */ uint32be fstSize; // in bytes /* +0x18 */ uint32be fstSector; // relative to partition start @@ -178,13 +177,15 @@ struct DiscPartitionHeader /* +0x24 */ uint8 fstHashType; /* +0x25 */ uint8 fstEncryptionType; // purpose of this isn't really understood. Maybe it controls which key is being used? (1 -> disc key, 2 -> partition key) - /* +0x26 */ uint8 versionA; - /* +0x27 */ uint8 ukn027; // also a version field? + /* +0x26 */ uint8be versionA; + /* +0x27 */ uint8be ukn027; // also a version field? // there is an array at +0x40 ? Related to H3 list. Also related to value at +0x0C and h3HashNum + /* +0x28 */ uint8be _uknOrPadding028[0x18]; + /* +0x40 */ uint8be h3HashArray[32]; // dynamic size. Only present if fstHashType != 0 }; -static_assert(sizeof(DiscPartitionHeader) == 0x28); +static_assert(sizeof(DiscPartitionHeader) == 0x40+0x20); bool FSTVolume::FindDiscKey(const fs::path& path, NCrypto::AesKey& discTitleKey) { @@ -269,7 +270,7 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemuLog_log(LogType::Force, "Disc image rejected because decryption failed"); return nullptr; } - if (partitionHeader->sectorSize != DISC_SECTOR_SIZE) + if (partitionHeader->blockSize != DISC_SECTOR_SIZE) { cemuLog_log(LogType::Force, "Disc image rejected because partition sector size is invalid"); return nullptr; @@ -336,6 +337,9 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemu_assert_debug(partitionHeaderSI.fstEncryptionType == 1); // todo - check other fields? + if(partitionHeaderSI.fstHashType == 0 && partitionHeaderSI.h3HashNum != 0) + cemuLog_log(LogType::Force, "FST: Partition uses unhashed blocks but stores a non-zero amount of H3 hashes"); + // GM partition DiscPartitionHeader partitionHeaderGM{}; if (!readPartitionHeader(partitionHeaderGM, gmPartitionIndex)) @@ -349,9 +353,10 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d // if decryption is necessary // load SI FST dataSource->SetBaseOffset((uint64)partitionArray[siPartitionIndex].partitionAddress * DISC_SECTOR_SIZE); - auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast(partitionHeaderSI.fstHashType)); + auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast(partitionHeaderSI.fstHashType), nullptr); if (!siFST) return nullptr; + cemu_assert_debug(!(siFST->HashIsDisabled() && partitionHeaderSI.h3HashNum != 0)); // if hash is disabled, no H3 data may be present // load ticket file for partition that we want to decrypt NCrypto::ETicketParser ticketParser; std::vector ticketData = siFST->ExtractFile(fmt::format("{:02x}/title.tik", gmPartitionIndex)); @@ -360,16 +365,32 @@ FSTVolume* FSTVolume::OpenFromDiscImage(const fs::path& path, NCrypto::AesKey& d cemuLog_log(LogType::Force, "Disc image ticket file is invalid"); return nullptr; } +#if 0 + // each SI partition seems to contain a title.tmd that we could parse and which should have information about the associated GM partition + // but the console seems to ignore this file for disc images, at least when mounting, so we shouldn't rely on it either + std::vector tmdData = siFST->ExtractFile(fmt::format("{:02x}/title.tmd", gmPartitionIndex)); + if (tmdData.empty()) + { + cemuLog_log(LogType::Force, "Disc image TMD file is missing"); + return nullptr; + } + // parse TMD + NCrypto::TMDParser tmdParser; + if (!tmdParser.parse(tmdData.data(), tmdData.size())) + { + cemuLog_log(LogType::Force, "Disc image TMD file is invalid"); + return nullptr; + } +#endif delete siFST; - NCrypto::AesKey gmTitleKey; ticketParser.GetTitleKey(gmTitleKey); - // load GM partition dataSource->SetBaseOffset((uint64)partitionArray[gmPartitionIndex].partitionAddress * DISC_SECTOR_SIZE); - FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast(partitionHeaderGM.fstHashType)); + FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast(partitionHeaderGM.fstHashType), nullptr); if (r) SET_FST_ERROR(OK); + cemu_assert_debug(!(r->HashIsDisabled() && partitionHeaderGM.h3HashNum != 0)); // if hash is disabled, no H3 data may be present return r; } @@ -426,15 +447,15 @@ FSTVolume* FSTVolume::OpenFromContentFolder(fs::path folderPath, ErrorCode* erro } // load FST // fstSize = size of first cluster? - FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode); + FSTVolume* fstVolume = FSTVolume::OpenFST(std::move(dataSource), 0, fstSize, &titleKey, fstHashMode, &tmdParser); if (fstVolume) SET_FST_ERROR(OK); return fstVolume; } -FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode) +FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD) { - cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW2); + cemu_assert_debug(fstHashMode != ClusterHashMode::RAW || fstHashMode != ClusterHashMode::RAW_STREAM); if (fstSize < sizeof(FSTHeader)) return nullptr; constexpr uint64 FST_CLUSTER_OFFSET = 0; @@ -465,6 +486,34 @@ FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint3 clusterTable[i].offset = clusterDataTable[i].offset; clusterTable[i].size = clusterDataTable[i].size; clusterTable[i].hashMode = static_cast((uint8)clusterDataTable[i].hashMode); + clusterTable[i].hasContentHash = false; // from the TMD file (H4?) + } + // if the TMD is available (when opening .app files) we can use the extra info from it to validate unhashed clusters + // each content entry in the TMD corresponds to one cluster used by the FST + if(optionalTMD) + { + if(numCluster != optionalTMD->GetContentList().size()) + { + cemuLog_log(LogType::Force, "FST: Number of clusters does not match TMD content list"); + return nullptr; + } + auto& contentList = optionalTMD->GetContentList(); + for(size_t i=0; im_offsetFactor = fstHeader->offsetFactor; fstVolume->m_sectorSize = DISC_SECTOR_SIZE; fstVolume->m_partitionTitlekey = *partitionTitleKey; - std::swap(fstVolume->m_cluster, clusterTable); - std::swap(fstVolume->m_entries, fstEntries); - std::swap(fstVolume->m_nameStringTable, nameStringTable); + fstVolume->m_hashIsDisabled = fstHeader->hashIsDisabled != 0; + fstVolume->m_cluster = std::move(clusterTable); + fstVolume->m_entries = std::move(fstEntries); + fstVolume->m_nameStringTable = std::move(nameStringTable); return fstVolume; } -FSTVolume* FSTVolume::OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode) +FSTVolume* FSTVolume::OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD) { FSTDataSource* ds = dataSource.release(); - FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode); + FSTVolume* fstVolume = OpenFST(ds, fstOffset, fstSize, partitionTitleKey, fstHashMode, optionalTMD); if (!fstVolume) { delete ds; @@ -757,7 +807,7 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size return 0; cemu_assert_debug(!HAS_FLAG(entry.GetFlags(), FSTEntry::FLAGS::FLAG_LINK)); FSTCluster& cluster = m_cluster[entry.fileInfo.clusterIndex]; - if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW2) + if (cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW_STREAM) return ReadFile_HashModeRaw(entry.fileInfo.clusterIndex, entry, offset, size, dataOut); else if (cluster.hashMode == ClusterHashMode::HASH_INTERLEAVED) return ReadFile_HashModeHashed(entry.fileInfo.clusterIndex, entry, offset, size, dataOut); @@ -765,87 +815,15 @@ uint32 FSTVolume::ReadFile(FSTFileHandle& fileHandle, uint32 offset, uint32 size return 0; } -uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) -{ - const uint32 readSizeInput = readSize; - uint8* dataOutU8 = (uint8*)dataOut; - if (readOffset >= entry.fileInfo.fileSize) - return 0; - else if ((readOffset + readSize) >= entry.fileInfo.fileSize) - readSize = (entry.fileInfo.fileSize - readOffset); - - const FSTCluster& cluster = m_cluster[clusterIndex]; - uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize; - uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; - - // make sure the raw range we read is aligned to AES block size (16) - uint64 readAddrStart = absFileOffset & ~0xF; - uint64 readAddrEnd = (absFileOffset + readSize + 0xF) & ~0xF; - - bool usesInitialIV = readOffset < 16; - if (!usesInitialIV) - readAddrStart -= 16; // read previous AES block since we require it for the IV - uint32 prePadding = (uint32)(absFileOffset - readAddrStart); // number of extra bytes we read before readOffset (for AES alignment and IV calculation) - uint32 postPadding = (uint32)(readAddrEnd - (absFileOffset + readSize)); - - uint8 readBuffer[64 * 1024]; - // read first chunk - // if file read offset (readOffset) is within the first AES-block then use initial IV calculated from cluster index - // otherwise read previous AES-block is the IV (AES-CBC) - uint64 readAddrCurrent = readAddrStart; - uint32 rawBytesToRead = (uint32)std::min((readAddrEnd - readAddrStart), (uint64)sizeof(readBuffer)); - if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, rawBytesToRead) != rawBytesToRead) - { - cemuLog_log(LogType::Force, "FST read error in raw content"); - return 0; - } - readAddrCurrent += rawBytesToRead; - - uint8 iv[16]{}; - if (usesInitialIV) - { - // for the first AES block, the IV is initialized from cluster index - iv[0] = (uint8)(clusterIndex >> 8); - iv[1] = (uint8)(clusterIndex >> 0); - AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, rawBytesToRead, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding); - dataOutU8 += (rawBytesToRead - prePadding - postPadding); - readSize -= (rawBytesToRead - prePadding - postPadding); - } - else - { - // IV is initialized from previous AES block (AES-CBC) - std::memcpy(iv, readBuffer, 16); - AES128_CBC_decrypt_updateIV(readBuffer + 16, readBuffer + 16, rawBytesToRead - 16, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer + prePadding, rawBytesToRead - prePadding - postPadding); - dataOutU8 += (rawBytesToRead - prePadding - postPadding); - readSize -= (rawBytesToRead - prePadding - postPadding); - } - - // read remaining chunks - while (readSize > 0) - { - uint32 bytesToRead = (uint32)std::min((uint32)sizeof(readBuffer), readSize); - uint32 alignedBytesToRead = (bytesToRead + 15) & ~0xF; - if (m_dataSource->readData(clusterIndex, clusterOffset, readAddrCurrent, readBuffer, alignedBytesToRead) != alignedBytesToRead) - { - cemuLog_log(LogType::Force, "FST read error in raw content"); - return 0; - } - AES128_CBC_decrypt_updateIV(readBuffer, readBuffer, alignedBytesToRead, m_partitionTitlekey.b, iv); - std::memcpy(dataOutU8, readBuffer, bytesToRead); - dataOutU8 += bytesToRead; - readSize -= bytesToRead; - readAddrCurrent += alignedBytesToRead; - } - - return readSizeInput - readSize; -} - constexpr size_t BLOCK_SIZE = 0x10000; constexpr size_t BLOCK_HASH_SIZE = 0x0400; constexpr size_t BLOCK_FILE_SIZE = 0xFC00; +struct FSTRawBlock +{ + std::vector rawData; // unhashed block size depends on sector size field in partition header +}; + struct FSTHashedBlock { uint8 rawData[BLOCK_SIZE]; @@ -887,12 +865,160 @@ struct FSTHashedBlock static_assert(sizeof(FSTHashedBlock) == BLOCK_SIZE); +struct FSTCachedRawBlock +{ + FSTRawBlock blockData; + uint8 ivForNextBlock[16]; + uint64 lastAccess; +}; + struct FSTCachedHashedBlock { FSTHashedBlock blockData; uint64 lastAccess; }; +// Checks cache fill state and if necessary drops least recently accessed block from the cache. Optionally allows to recycle the released cache entry to cut down cost of memory allocation and clearing +void FSTVolume::TrimCacheIfRequired(FSTCachedRawBlock** droppedRawBlock, FSTCachedHashedBlock** droppedHashedBlock) +{ + // calculate size used by cache + size_t cacheSize = 0; + for (auto& itr : m_cacheDecryptedRawBlocks) + cacheSize += itr.second->blockData.rawData.size(); + for (auto& itr : m_cacheDecryptedHashedBlocks) + cacheSize += sizeof(FSTCachedHashedBlock) + sizeof(FSTHashedBlock); + // only trim if cache is full (larger than 2MB) + if (cacheSize < 2*1024*1024) // 2MB + return; + // scan both cache lists to find least recently accessed block to drop + auto dropRawItr = std::min_element(m_cacheDecryptedRawBlocks.begin(), m_cacheDecryptedRawBlocks.end(), [](const auto& a, const auto& b) -> bool + { return a.second->lastAccess < b.second->lastAccess; }); + auto dropHashedItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool + { return a.second->lastAccess < b.second->lastAccess; }); + uint64 lastAccess = std::numeric_limits::max(); + if(dropRawItr != m_cacheDecryptedRawBlocks.end()) + lastAccess = dropRawItr->second->lastAccess; + if(dropHashedItr != m_cacheDecryptedHashedBlocks.end()) + lastAccess = std::min(lastAccess, dropHashedItr->second->lastAccess); + if(dropRawItr != m_cacheDecryptedRawBlocks.end() && dropRawItr->second->lastAccess == lastAccess) + { + if (droppedRawBlock) + *droppedRawBlock = dropRawItr->second; + else + delete dropRawItr->second; + m_cacheDecryptedRawBlocks.erase(dropRawItr); + return; + } + else if(dropHashedItr != m_cacheDecryptedHashedBlocks.end() && dropHashedItr->second->lastAccess == lastAccess) + { + if (droppedHashedBlock) + *droppedHashedBlock = dropHashedItr->second; + else + delete dropHashedItr->second; + m_cacheDecryptedHashedBlocks.erase(dropHashedItr); + } +} + +void FSTVolume::DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16]) +{ + memset(ivOut, 0, sizeof(ivOut)); + if(blockIndex == 0) + { + ivOut[0] = (uint8)(clusterIndex >> 8); + ivOut[1] = (uint8)(clusterIndex >> 0); + } + else + { + // the last 16 encrypted bytes of the previous block are the IV (AES CBC) + // if the previous block is cached we can grab the IV from there. Otherwise we have to read the 16 bytes from the data source + uint32 prevBlockIndex = blockIndex - 1; + uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)prevBlockIndex; + auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId); + if (itr != m_cacheDecryptedRawBlocks.end()) + { + memcpy(ivOut, itr->second->ivForNextBlock, 16); + } + else + { + cemu_assert(m_sectorSize >= 16); + uint64 clusterOffset = (uint64)m_cluster[clusterIndex].offset * m_sectorSize; + uint8 prevIV[16]; + if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize - 16, prevIV, 16) != 16) + { + cemuLog_log(LogType::Force, "Failed to read IV for raw FST block"); + m_detectedCorruption = true; + return; + } + memcpy(ivOut, prevIV, 16); + } + } +} + +FSTCachedRawBlock* FSTVolume::GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex) +{ + FSTCluster& cluster = m_cluster[clusterIndex]; + uint64 clusterOffset = (uint64)cluster.offset * m_sectorSize; + // generate id for cache + uint64 cacheBlockId = ((uint64)clusterIndex << (64 - 16)) | (uint64)blockIndex; + // lookup block in cache + FSTCachedRawBlock* block = nullptr; + auto itr = m_cacheDecryptedRawBlocks.find(cacheBlockId); + if (itr != m_cacheDecryptedRawBlocks.end()) + { + block = itr->second; + block->lastAccess = ++m_cacheAccessCounter; + return block; + } + // if cache already full, drop least recently accessed block and recycle FSTCachedRawBlock object if possible + TrimCacheIfRequired(&block, nullptr); + if (!block) + block = new FSTCachedRawBlock(); + block->blockData.rawData.resize(m_sectorSize); + // block not cached, read new + block->lastAccess = ++m_cacheAccessCounter; + if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * m_sectorSize, block->blockData.rawData.data(), m_sectorSize) != m_sectorSize) + { + cemuLog_log(LogType::Force, "Failed to read raw FST block"); + delete block; + m_detectedCorruption = true; + return nullptr; + } + // decrypt hash data + uint8 iv[16]{}; + DetermineUnhashedBlockIV(clusterIndex, blockIndex, iv); + memcpy(block->ivForNextBlock, block->blockData.rawData.data() + m_sectorSize - 16, 16); + AES128_CBC_decrypt(block->blockData.rawData.data(), block->blockData.rawData.data(), m_sectorSize, m_partitionTitlekey.b, iv); + // if this is the next block, then hash it + if(cluster.hasContentHash) + { + if(cluster.singleHashNumBlocksHashed == blockIndex) + { + cemu_assert_debug(!(cluster.contentSize % m_sectorSize)); // size should be multiple of sector size? Regardless, the hashing code below can handle non-aligned sizes + bool isLastBlock = blockIndex == (std::max(cluster.contentSize / m_sectorSize, 1) - 1); + uint32 hashSize = m_sectorSize; + if(isLastBlock) + hashSize = cluster.contentSize - (uint64)blockIndex*m_sectorSize; + EVP_DigestUpdate(cluster.singleHashCtx.get(), block->blockData.rawData.data(), hashSize); + cluster.singleHashNumBlocksHashed++; + if(isLastBlock) + { + uint8 hash[32]; + EVP_DigestFinal_ex(cluster.singleHashCtx.get(), hash, nullptr); + if(memcmp(hash, cluster.contentHash32, cluster.contentHashIsSHA1 ? 20 : 32) != 0) + { + cemuLog_log(LogType::Force, "FST: Raw section hash mismatch"); + delete block; + m_detectedCorruption = true; + return nullptr; + } + } + } + } + // register in cache + m_cacheDecryptedRawBlocks.emplace(cacheBlockId, block); + return block; +} + FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex) { const FSTCluster& cluster = m_cluster[clusterIndex]; @@ -908,22 +1034,17 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui block->lastAccess = ++m_cacheAccessCounter; return block; } - // if cache already full, drop least recently accessed block (but recycle the FSTHashedBlock* object) - if (m_cacheDecryptedHashedBlocks.size() >= 16) - { - auto dropItr = std::min_element(m_cacheDecryptedHashedBlocks.begin(), m_cacheDecryptedHashedBlocks.end(), [](const auto& a, const auto& b) -> bool - { return a.second->lastAccess < b.second->lastAccess; }); - block = dropItr->second; - m_cacheDecryptedHashedBlocks.erase(dropItr); - } - else + // if cache already full, drop least recently accessed block and recycle FSTCachedHashedBlock object if possible + TrimCacheIfRequired(nullptr, &block); + if (!block) block = new FSTCachedHashedBlock(); // block not cached, read new block->lastAccess = ++m_cacheAccessCounter; if (m_dataSource->readData(clusterIndex, clusterOffset, blockIndex * BLOCK_SIZE, block->blockData.rawData, BLOCK_SIZE) != BLOCK_SIZE) { - cemuLog_log(LogType::Force, "Failed to read FST block"); + cemuLog_log(LogType::Force, "Failed to read hashed FST block"); delete block; + m_detectedCorruption = true; return nullptr; } // decrypt hash data @@ -931,11 +1052,46 @@ FSTCachedHashedBlock* FSTVolume::GetDecryptedHashedBlock(uint32 clusterIndex, ui AES128_CBC_decrypt(block->blockData.getHashData(), block->blockData.getHashData(), BLOCK_HASH_SIZE, m_partitionTitlekey.b, iv); // decrypt file data AES128_CBC_decrypt(block->blockData.getFileData(), block->blockData.getFileData(), BLOCK_FILE_SIZE, m_partitionTitlekey.b, block->blockData.getH0Hash(blockIndex%16)); + // compare with H0 to verify data integrity + NCrypto::CHash160 h0; + SHA1(block->blockData.getFileData(), BLOCK_FILE_SIZE, h0.b); + uint32 h0Index = (blockIndex % 4096); + if (memcmp(h0.b, block->blockData.getH0Hash(h0Index & 0xF), sizeof(h0.b)) != 0) + { + cemuLog_log(LogType::Force, "FST: Hash H0 mismatch in hashed block (section {} index {})", clusterIndex, blockIndex); + delete block; + m_detectedCorruption = true; + return nullptr; + } // register in cache m_cacheDecryptedHashedBlocks.emplace(cacheBlockId, block); return block; } +uint32 FSTVolume::ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) +{ + uint8* dataOutU8 = (uint8*)dataOut; + if (readOffset >= entry.fileInfo.fileSize) + return 0; + else if ((readOffset + readSize) >= entry.fileInfo.fileSize) + readSize = (entry.fileInfo.fileSize - readOffset); + uint64 absFileOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; + uint32 remainingReadSize = readSize; + while (remainingReadSize > 0) + { + const FSTCachedRawBlock* rawBlock = this->GetDecryptedRawBlock(clusterIndex, absFileOffset/m_sectorSize); + if (!rawBlock) + break; + uint32 blockOffset = (uint32)(absFileOffset % m_sectorSize); + uint32 bytesToRead = std::min(remainingReadSize, m_sectorSize - blockOffset); + std::memcpy(dataOutU8, rawBlock->blockData.rawData.data() + blockOffset, bytesToRead); + dataOutU8 += bytesToRead; + remainingReadSize -= bytesToRead; + absFileOffset += bytesToRead; + } + return readSize - remainingReadSize; +} + uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut) { /* @@ -966,7 +1122,6 @@ uint32 FSTVolume::ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, */ const FSTCluster& cluster = m_cluster[clusterIndex]; - uint64 clusterBaseOffset = (uint64)cluster.offset * m_sectorSize; uint64 fileReadOffset = entry.fileInfo.fileOffset * m_offsetFactor + readOffset; uint32 blockIndex = (uint32)(fileReadOffset / BLOCK_FILE_SIZE); uint32 bytesRemaining = readSize; @@ -1019,6 +1174,8 @@ bool FSTVolume::Next(FSTDirectoryIterator& directoryIterator, FSTFileHandle& fil FSTVolume::~FSTVolume() { + for (auto& itr : m_cacheDecryptedRawBlocks) + delete itr.second; for (auto& itr : m_cacheDecryptedHashedBlocks) delete itr.second; if (m_sourceIsOwned) @@ -1115,4 +1272,4 @@ bool FSTVerifier::VerifyHashedContentFile(FileStream* fileContent, const NCrypto void FSTVolumeTest() { FSTPathUnitTest(); -} \ No newline at end of file +} diff --git a/src/Cafe/Filesystem/FST/FST.h b/src/Cafe/Filesystem/FST/FST.h index 24fc39ea..601799ce 100644 --- a/src/Cafe/Filesystem/FST/FST.h +++ b/src/Cafe/Filesystem/FST/FST.h @@ -1,5 +1,6 @@ #pragma once #include "Cemu/ncrypto/ncrypto.h" +#include "openssl/evp.h" struct FSTFileHandle { @@ -45,6 +46,7 @@ public: ~FSTVolume(); uint32 GetFileCount() const; + bool HasCorruption() const { return m_detectedCorruption; } bool OpenFile(std::string_view path, FSTFileHandle& fileHandleOut, bool openOnlyFiles = false); @@ -86,15 +88,25 @@ private: enum class ClusterHashMode : uint8 { RAW = 0, // raw data + encryption, no hashing? - RAW2 = 1, // raw data + encryption, with hash stored in tmd? + RAW_STREAM = 1, // raw data + encryption, with hash stored in tmd? HASH_INTERLEAVED = 2, // hashes + raw interleaved in 0x10000 blocks (0x400 bytes of hashes at the beginning, followed by 0xFC00 bytes of data) }; struct FSTCluster { + FSTCluster() : singleHashCtx(nullptr, &EVP_MD_CTX_free) {} + uint32 offset; uint32 size; ClusterHashMode hashMode; + // extra data if TMD is available + bool hasContentHash; + uint8 contentHash32[32]; + bool contentHashIsSHA1; // if true then it's SHA1 (with extra bytes zeroed out), otherwise it's SHA256 + uint64 contentSize; // size of the content (in blocks) + // hash context for single hash mode (content hash must be available) + std::unique_ptr singleHashCtx; // unique_ptr to make this move-only + uint32 singleHashNumBlocksHashed{0}; }; struct FSTEntry @@ -164,17 +176,30 @@ private: bool m_sourceIsOwned{}; uint32 m_sectorSize{}; // for cluster offsets uint32 m_offsetFactor{}; // for file offsets + bool m_hashIsDisabled{}; // disables hash verification (for all clusters of this volume?) std::vector m_cluster; std::vector m_entries; std::vector m_nameStringTable; NCrypto::AesKey m_partitionTitlekey; + bool m_detectedCorruption{false}; - /* Cache for decrypted hashed blocks */ + bool HashIsDisabled() const + { + return m_hashIsDisabled; + } + + /* Cache for decrypted raw and hashed blocks */ + std::unordered_map m_cacheDecryptedRawBlocks; std::unordered_map m_cacheDecryptedHashedBlocks; uint64 m_cacheAccessCounter{}; + void DetermineUnhashedBlockIV(uint32 clusterIndex, uint32 blockIndex, uint8 ivOut[16]); + + struct FSTCachedRawBlock* GetDecryptedRawBlock(uint32 clusterIndex, uint32 blockIndex); struct FSTCachedHashedBlock* GetDecryptedHashedBlock(uint32 clusterIndex, uint32 blockIndex); + void TrimCacheIfRequired(struct FSTCachedRawBlock** droppedRawBlock, struct FSTCachedHashedBlock** droppedHashedBlock); + /* File reading */ uint32 ReadFile_HashModeRaw(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut); uint32 ReadFile_HashModeHashed(uint32 clusterIndex, FSTEntry& entry, uint32 readOffset, uint32 readSize, void* dataOut); @@ -185,7 +210,10 @@ private: /* +0x00 */ uint32be magic; /* +0x04 */ uint32be offsetFactor; /* +0x08 */ uint32be numCluster; - /* +0x0C */ uint32be ukn0C; + /* +0x0C */ uint8be hashIsDisabled; + /* +0x0D */ uint8be ukn0D; + /* +0x0E */ uint8be ukn0E; + /* +0x0F */ uint8be ukn0F; /* +0x10 */ uint32be ukn10; /* +0x14 */ uint32be ukn14; /* +0x18 */ uint32be ukn18; @@ -262,8 +290,8 @@ private: static_assert(sizeof(FSTHeader_FileEntry) == 0x10); - static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode); - static FSTVolume* OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode); + static FSTVolume* OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD); + static FSTVolume* OpenFST(std::unique_ptr dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode, NCrypto::TMDParser* optionalTMD); static bool ProcessFST(FSTHeader_FileEntry* fileTable, uint32 numFileEntries, uint32 numCluster, std::vector& nameStringTable, std::vector& fstEntries); bool MatchFSTEntryName(FSTEntry& entry, std::string_view comparedName) diff --git a/src/Cafe/GraphicPack/GraphicPack2.cpp b/src/Cafe/GraphicPack/GraphicPack2.cpp index c54c31cb..f21bb89d 100644 --- a/src/Cafe/GraphicPack/GraphicPack2.cpp +++ b/src/Cafe/GraphicPack/GraphicPack2.cpp @@ -345,7 +345,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) const auto preset_name = rules.FindOption("name"); if (!preset_name) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": Preset in line {} skipped because it has no name option defined", m_name, rules.GetCurrentSectionLineNumber()); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": Preset in line {} skipped because it has no name option defined", GetNormalizedPathString(), rules.GetCurrentSectionLineNumber()); continue; } @@ -369,7 +369,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) } catch (const std::exception & ex) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": Can't parse preset \"{}\": {}", m_name, *preset_name, ex.what()); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": Can't parse preset \"{}\": {}", GetNormalizedPathString(), *preset_name, ex.what()); } } else if (boost::iequals(currentSectionName, "RAM")) @@ -383,7 +383,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) { if (m_version <= 5) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": [RAM] options are only available for graphic pack version 6 or higher", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": [RAM] options are only available for graphic pack version 6 or higher", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } @@ -393,12 +393,12 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) { if (addrEnd <= addrStart) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": start address (0x{:08x}) must be greater than end address (0x{:08x}) for {}", m_name, addrStart, addrEnd, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": start address (0x{:08x}) must be greater than end address (0x{:08x}) for {}", GetNormalizedPathString(), addrStart, addrEnd, optionNameBuf); throw std::exception(); } else if ((addrStart & 0xFFF) != 0 || (addrEnd & 0xFFF) != 0) { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": addresses for %s are not aligned to 0x1000", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": addresses for %s are not aligned to 0x1000", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } else @@ -408,7 +408,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) } else { - cemuLog_log(LogType::Force, "Graphic pack \"{}\": has invalid syntax for option {}", m_name, optionNameBuf); + cemuLog_log(LogType::Force, "Graphic pack \"{}\": has invalid syntax for option {}", GetNormalizedPathString(), optionNameBuf); throw std::exception(); } } @@ -422,24 +422,32 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) std::unordered_map> tmp_map; // all vars must be defined in the default preset vars before - for (const auto& entry : m_presets) + std::vector> mismatchingPresetVars; + for (const auto& presetEntry : m_presets) { - tmp_map[entry->category].emplace_back(entry); + tmp_map[presetEntry->category].emplace_back(presetEntry); - for (auto& kv : entry->variables) + for (auto& presetVar : presetEntry->variables) { - const auto it = m_preset_vars.find(kv.first); + const auto it = m_preset_vars.find(presetVar.first); if (it == m_preset_vars.cend()) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains preset variables which are not defined in the default section", m_name); - throw std::exception(); + mismatchingPresetVars.emplace_back(presetEntry->name, presetVar.first); + continue; } - // overwrite var type with default var type - kv.second.first = it->second.first; + presetVar.second.first = it->second.first; } } + if(!mismatchingPresetVars.empty()) + { + cemuLog_log(LogType::Force, "Graphic pack \"{}\" contains preset variables which are not defined in the [Default] section:", GetNormalizedPathString()); + for (const auto& [presetName, varName] : mismatchingPresetVars) + cemuLog_log(LogType::Force, "Preset: {} Variable: {}", presetName, varName); + throw std::exception(); + } + // have first entry be default active for every category if no default= is set for(auto entry : get_values(tmp_map)) { @@ -469,7 +477,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) auto& p2 = kv.second[i + 1]; if (p1->variables.size() != p2->variables.size()) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", m_name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", GetNormalizedPathString()); throw std::exception(); } @@ -477,14 +485,14 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules) std::set keys2(get_keys(p2->variables).begin(), get_keys(p2->variables).end()); if (keys1 != keys2) { - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", m_name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" contains inconsistent preset variables", GetNormalizedPathString()); throw std::exception(); } if(p1->is_default) { if(has_default) - cemuLog_log(LogType::Force, "Graphic pack: \"{}\" has more than one preset with the default key set for the same category \"{}\"", m_name, p1->name); + cemuLog_log(LogType::Force, "Graphic pack: \"{}\" has more than one preset with the default key set for the same category \"{}\"", GetNormalizedPathString(), p1->name); p1->active = true; has_default = true; } @@ -960,7 +968,7 @@ bool GraphicPack2::Activate() auto option_upscale = rules.FindOption("upscaleMagFilter"); if(option_upscale && boost::iequals(*option_upscale, "NearestNeighbor")) m_output_settings.upscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; - auto option_downscale = rules.FindOption("NearestNeighbor"); + auto option_downscale = rules.FindOption("downscaleMinFilter"); if (option_downscale && boost::iequals(*option_downscale, "NearestNeighbor")) m_output_settings.downscale_filter = LatteTextureView::MagFilter::kNearestNeighbor; } diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp index e7369af6..1fed07cd 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.cpp +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.cpp @@ -447,6 +447,34 @@ bool debugger_hasPatch(uint32 address) return false; } +void debugger_removePatch(uint32 address) +{ + for (sint32 i = 0; i < debuggerState.patches.size(); i++) + { + auto& patch = debuggerState.patches[i]; + if (address < patch->address || address >= (patch->address + patch->length)) + continue; + MPTR startAddress = patch->address; + MPTR endAddress = patch->address + patch->length; + // remove any breakpoints overlapping with the patch + for (auto& bp : debuggerState.breakpoints) + { + if (bp->address + 4 > startAddress && bp->address < endAddress) + { + bp->enabled = false; + debugger_updateExecutionBreakpoint(bp->address); + } + } + // restore original data + memcpy(MEMPTR(startAddress).GetPtr(), patch->origData.data(), patch->length); + PPCRecompiler_invalidateRange(startAddress, endAddress); + // remove patch + delete patch; + debuggerState.patches.erase(debuggerState.patches.begin() + i); + return; + } +} + void debugger_stepInto(PPCInterpreter_t* hCPU, bool updateDebuggerWindow = true) { bool isRecEnabled = ppcRecompilerEnabled; diff --git a/src/Cafe/HW/Espresso/Debugger/Debugger.h b/src/Cafe/HW/Espresso/Debugger/Debugger.h index 717df28a..249c47b8 100644 --- a/src/Cafe/HW/Espresso/Debugger/Debugger.h +++ b/src/Cafe/HW/Espresso/Debugger/Debugger.h @@ -114,6 +114,7 @@ void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore = fals void debugger_createPatch(uint32 address, std::span patchData); bool debugger_hasPatch(uint32 address); +void debugger_removePatch(uint32 address); void debugger_forceBreak(); // force breakpoint at the next possible instruction bool debugger_isTrapped(); diff --git a/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h b/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h index 713e094e..ac75bb1b 100644 --- a/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h +++ b/src/Cafe/HW/Latte/Core/LattePerformanceMonitor.h @@ -124,6 +124,7 @@ typedef struct LattePerfStatCounter numGraphicPipelines; LattePerfStatCounter numImages; LattePerfStatCounter numImageViews; + LattePerfStatCounter numSamplers; LattePerfStatCounter numRenderPass; LattePerfStatCounter numFramebuffer; diff --git a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp index ca6a2a4d..3bb6c7e3 100644 --- a/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp +++ b/src/Cafe/HW/Latte/Core/LatteRenderTarget.cpp @@ -933,13 +933,6 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa if (shader == nullptr) { sint32 scaling_filter = downscaling ? GetConfig().downscale_filter : GetConfig().upscale_filter; - - if (g_renderer->GetType() == RendererAPI::Vulkan) - { - // force linear or nearest neighbor filter - if(scaling_filter != kLinearFilter && scaling_filter != kNearestNeighborFilter) - scaling_filter = kLinearFilter; - } if (scaling_filter == kLinearFilter) { @@ -957,7 +950,7 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa else shader = RendererOutputShader::s_bicubic_shader; - filter = LatteTextureView::MagFilter::kNearestNeighbor; + filter = LatteTextureView::MagFilter::kLinear; } else if (scaling_filter == kBicubicHermiteFilter) { diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp index c3f7c19e..5972aacc 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompiler.cpp @@ -370,6 +370,8 @@ bool LatteDecompiler_IsALUTransInstruction(bool isOP3, uint32 opcode) opcode == ALU_OP2_INST_LSHR_INT || opcode == ALU_OP2_INST_MAX_INT || opcode == ALU_OP2_INST_MIN_INT || + opcode == ALU_OP2_INST_MAX_UINT || + opcode == ALU_OP2_INST_MIN_UINT || opcode == ALU_OP2_INST_MOVA_FLOOR || opcode == ALU_OP2_INST_MOVA_INT || opcode == ALU_OP2_INST_SETE_DX10 || diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp index 19604e0c..ff64988c 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerAnalyzer.cpp @@ -140,6 +140,8 @@ bool _isIntegerInstruction(const LatteDecompilerALUInstruction& aluInstruction) case ALU_OP2_INST_SUB_INT: case ALU_OP2_INST_MAX_INT: case ALU_OP2_INST_MIN_INT: + case ALU_OP2_INST_MAX_UINT: + case ALU_OP2_INST_MIN_UINT: case ALU_OP2_INST_SETE_INT: case ALU_OP2_INST_SETGT_INT: case ALU_OP2_INST_SETGE_INT: diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp index 7a6605f8..e7ebcf3a 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerEmitGLSL.cpp @@ -1415,19 +1415,23 @@ void _emitALUOP2InstructionCode(LatteDecompilerShaderContext* shaderContext, Lat } else if( aluInstruction->opcode == ALU_OP2_INST_ADD_INT ) _emitALUOperationBinary(shaderContext, aluInstruction, " + "); - else if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MIN_INT ) + else if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MIN_INT || + aluInstruction->opcode == ALU_OP2_INST_MAX_UINT || aluInstruction->opcode == ALU_OP2_INST_MIN_UINT) { // not verified + bool isUnsigned = aluInstruction->opcode == ALU_OP2_INST_MAX_UINT || aluInstruction->opcode == ALU_OP2_INST_MIN_UINT; + auto opType = isUnsigned ? LATTE_DECOMPILER_DTYPE_UNSIGNED_INT : LATTE_DECOMPILER_DTYPE_SIGNED_INT; _emitInstructionOutputVariableName(shaderContext, aluInstruction); - if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT ) - src->add(" = max("); + src->add(" = "); + _emitTypeConversionPrefix(shaderContext, opType, outputType); + if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MAX_UINT ) + src->add("max("); else - src->add(" = min("); - _emitTypeConversionPrefix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType); - _emitOperandInputCode(shaderContext, aluInstruction, 0, LATTE_DECOMPILER_DTYPE_SIGNED_INT); + src->add("min("); + _emitOperandInputCode(shaderContext, aluInstruction, 0, opType); src->add(", "); - _emitOperandInputCode(shaderContext, aluInstruction, 1, LATTE_DECOMPILER_DTYPE_SIGNED_INT); - _emitTypeConversionSuffix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType); + _emitOperandInputCode(shaderContext, aluInstruction, 1, opType); + _emitTypeConversionSuffix(shaderContext, opType, outputType); src->add(");" _CRLF); } else if( aluInstruction->opcode == ALU_OP2_INST_SUB_INT ) diff --git a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h index 4cb1982e..6c029b46 100644 --- a/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h +++ b/src/Cafe/HW/Latte/LegacyShaderDecompiler/LatteDecompilerInstructions.h @@ -60,6 +60,8 @@ #define ALU_OP2_INST_SUB_INT (0x035) // integer instruction #define ALU_OP2_INST_MAX_INT (0x036) // integer instruction #define ALU_OP2_INST_MIN_INT (0x037) // integer instruction +#define ALU_OP2_INST_MAX_UINT (0x038) // integer instruction +#define ALU_OP2_INST_MIN_UINT (0x039) // integer instruction #define ALU_OP2_INST_SETE_INT (0x03A) // integer instruction #define ALU_OP2_INST_SETGT_INT (0x03B) // integer instruction #define ALU_OP2_INST_SETGE_INT (0x03C) // integer instruction diff --git a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp index cf134a5d..bbf988bc 100644 --- a/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.cpp @@ -570,13 +570,10 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu g_renderer->ClearColorbuffer(padView); } - sint32 effectiveWidth, effectiveHeight; - texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); - shader_unbind(RendererShader::ShaderType::kGeometry); shader_bind(shader->GetVertexShader()); shader_bind(shader->GetFragmentShader()); - shader->SetUniformParameters(*texView, { effectiveWidth, effectiveHeight }, { imageWidth, imageHeight }); + shader->SetUniformParameters(*texView, {imageWidth, imageHeight}); // set viewport glViewportIndexedf(0, imageX, imageY, imageWidth, imageHeight); @@ -584,6 +581,12 @@ void OpenGLRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu LatteTextureViewGL* texViewGL = (LatteTextureViewGL*)texView; texture_bindAndActivate(texView, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + texViewGL->samplerState.clampS = texViewGL->samplerState.clampT = 0xFF; + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST); + texViewGL->samplerState.filterMin = 0xFFFFFFFF; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, useLinearTexFilter ? GL_LINEAR : GL_NEAREST); texViewGL->samplerState.filterMag = 0xFFFFFFFF; diff --git a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp index de8e84cd..19511747 100644 --- a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp +++ b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp @@ -2,18 +2,7 @@ #include "Cafe/HW/Latte/Renderer/OpenGL/OpenGLRenderer.h" const std::string RendererOutputShader::s_copy_shader_source = -R"(#version 420 - -#ifdef VULKAN -layout(location = 0) in vec2 passUV; -layout(binding = 0) uniform sampler2D textureSrc; -layout(location = 0) out vec4 colorOut0; -#else -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -layout(location = 0) out vec4 colorOut0; -#endif - +R"( void main() { colorOut0 = vec4(texture(textureSrc, passUV).rgb,1.0); @@ -22,20 +11,6 @@ void main() const std::string RendererOutputShader::s_bicubic_shader_source = R"( -#version 420 - -#ifdef VULKAN -layout(location = 0) in vec2 passUV; -layout(binding = 0) uniform sampler2D textureSrc; -layout(binding = 1) uniform vec2 textureSrcResolution; -layout(location = 0) out vec4 colorOut0; -#else -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -uniform vec2 textureSrcResolution; -layout(location = 0) out vec4 colorOut0; -#endif - vec4 cubic(float x) { float x2 = x * x; @@ -48,24 +23,23 @@ vec4 cubic(float x) return w / 6.0; } -vec4 bcFilter(vec2 texcoord, vec2 texscale) +vec4 bcFilter(vec2 uv, vec4 texelSize) { - float fx = fract(texcoord.x); - float fy = fract(texcoord.y); - texcoord.x -= fx; - texcoord.y -= fy; + vec2 pixel = uv*texelSize.zw - 0.5; + vec2 pixelFrac = fract(pixel); + vec2 pixelInt = pixel - pixelFrac; - vec4 xcubic = cubic(fx); - vec4 ycubic = cubic(fy); + vec4 xcubic = cubic(pixelFrac.x); + vec4 ycubic = cubic(pixelFrac.y); - vec4 c = vec4(texcoord.x - 0.5, texcoord.x + 1.5, texcoord.y - 0.5, texcoord.y + 1.5); + vec4 c = vec4(pixelInt.x - 0.5, pixelInt.x + 1.5, pixelInt.y - 0.5, pixelInt.y + 1.5); vec4 s = vec4(xcubic.x + xcubic.y, xcubic.z + xcubic.w, ycubic.x + ycubic.y, ycubic.z + ycubic.w); vec4 offset = c + vec4(xcubic.y, xcubic.w, ycubic.y, ycubic.w) / s; - vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texscale); - vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texscale); - vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texscale); - vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texscale); + vec4 sample0 = texture(textureSrc, vec2(offset.x, offset.z) * texelSize.xy); + vec4 sample1 = texture(textureSrc, vec2(offset.y, offset.z) * texelSize.xy); + vec4 sample2 = texture(textureSrc, vec2(offset.x, offset.w) * texelSize.xy); + vec4 sample3 = texture(textureSrc, vec2(offset.y, offset.w) * texelSize.xy); float sx = s.x / (s.x + s.y); float sy = s.z / (s.z + s.w); @@ -76,20 +50,13 @@ vec4 bcFilter(vec2 texcoord, vec2 texscale) } void main(){ - colorOut0 = vec4(bcFilter(passUV*textureSrcResolution, vec2(1.0,1.0)/textureSrcResolution).rgb,1.0); + vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy); + colorOut0 = vec4(bcFilter(passUV, texelSize).rgb,1.0); } )"; const std::string RendererOutputShader::s_hermite_shader_source = -R"(#version 420 - -in vec4 gl_FragCoord; -in vec2 passUV; -layout(binding=0) uniform sampler2D textureSrc; -uniform vec2 textureSrcResolution; -uniform vec2 outputResolution; -layout(location = 0) out vec4 colorOut0; - +R"( // https://www.shadertoy.com/view/MllSzX vec3 CubicHermite (vec3 A, vec3 B, vec3 C, vec3 D, float t) @@ -111,7 +78,7 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize) vec2 frac = fract(pixel); pixel = floor(pixel) / texelSize.zw - vec2(texelSize.xy/2.0); - vec4 doubleSize = texelSize*texelSize; + vec4 doubleSize = texelSize*2.0; vec3 C00 = texture(textureSrc, pixel + vec2(-texelSize.x ,-texelSize.y)).rgb; vec3 C10 = texture(textureSrc, pixel + vec2( 0.0 ,-texelSize.y)).rgb; @@ -142,15 +109,17 @@ vec3 BicubicHermiteTexture(vec2 uv, vec4 texelSize) } void main(){ - vec4 texelSize = vec4( 1.0 / outputResolution.xy, outputResolution.xy); + vec4 texelSize = vec4( 1.0 / textureSrcResolution.xy, textureSrcResolution.xy); colorOut0 = vec4(BicubicHermiteTexture(passUV, texelSize), 1.0); } )"; RendererOutputShader::RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source) { + auto finalFragmentSrc = PrependFragmentPreamble(fragment_source); + m_vertex_shader.reset(g_renderer->shader_create(RendererShader::ShaderType::kVertex, 0, 0, vertex_source, false, false)); - m_fragment_shader.reset(g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, fragment_source, false, false)); + m_fragment_shader.reset(g_renderer->shader_create(RendererShader::ShaderType::kFragment, 0, 0, finalFragmentSrc, false, false)); m_vertex_shader->PreponeCompilation(true); m_fragment_shader->PreponeCompilation(true); @@ -163,74 +132,45 @@ RendererOutputShader::RendererOutputShader(const std::string& vertex_source, con if (g_renderer->GetType() == RendererAPI::OpenGL) { - m_attributes[0].m_loc_texture_src_resolution = m_vertex_shader->GetUniformLocation("textureSrcResolution"); - m_attributes[0].m_loc_input_resolution = m_vertex_shader->GetUniformLocation("inputResolution"); - m_attributes[0].m_loc_output_resolution = m_vertex_shader->GetUniformLocation("outputResolution"); + m_uniformLocations[0].m_loc_textureSrcResolution = m_vertex_shader->GetUniformLocation("textureSrcResolution"); + m_uniformLocations[0].m_loc_nativeResolution = m_vertex_shader->GetUniformLocation("nativeResolution"); + m_uniformLocations[0].m_loc_outputResolution = m_vertex_shader->GetUniformLocation("outputResolution"); - m_attributes[1].m_loc_texture_src_resolution = m_fragment_shader->GetUniformLocation("textureSrcResolution"); - m_attributes[1].m_loc_input_resolution = m_fragment_shader->GetUniformLocation("inputResolution"); - m_attributes[1].m_loc_output_resolution = m_fragment_shader->GetUniformLocation("outputResolution"); + m_uniformLocations[1].m_loc_textureSrcResolution = m_fragment_shader->GetUniformLocation("textureSrcResolution"); + m_uniformLocations[1].m_loc_nativeResolution = m_fragment_shader->GetUniformLocation("nativeResolution"); + m_uniformLocations[1].m_loc_outputResolution = m_fragment_shader->GetUniformLocation("outputResolution"); } - else - { - cemuLog_logDebug(LogType::Force, "RendererOutputShader() - todo for Vulkan"); - m_attributes[0].m_loc_texture_src_resolution = -1; - m_attributes[0].m_loc_input_resolution = -1; - m_attributes[0].m_loc_output_resolution = -1; - - m_attributes[1].m_loc_texture_src_resolution = -1; - m_attributes[1].m_loc_input_resolution = -1; - m_attributes[1].m_loc_output_resolution = -1; - } - } -void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const +void RendererOutputShader::SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const { - float res[2]; - // vertex shader - if (m_attributes[0].m_loc_texture_src_resolution != -1) - { - res[0] = (float)texture_view.baseTexture->width; - res[1] = (float)texture_view.baseTexture->height; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_texture_src_resolution, res, 1); - } + sint32 effectiveWidth, effectiveHeight; + texture_view.baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); + auto setUniforms = [&](RendererShader* shader, const UniformLocations& locations){ + float res[2]; + if (locations.m_loc_textureSrcResolution != -1) + { + res[0] = (float)effectiveWidth; + res[1] = (float)effectiveHeight; + shader->SetUniform2fv(locations.m_loc_textureSrcResolution, res, 1); + } - if (m_attributes[0].m_loc_input_resolution != -1) - { - res[0] = (float)input_res.x; - res[1] = (float)input_res.y; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_input_resolution, res, 1); - } + if (locations.m_loc_nativeResolution != -1) + { + res[0] = (float)texture_view.baseTexture->width; + res[1] = (float)texture_view.baseTexture->height; + shader->SetUniform2fv(locations.m_loc_nativeResolution, res, 1); + } - if (m_attributes[0].m_loc_output_resolution != -1) - { - res[0] = (float)output_res.x; - res[1] = (float)output_res.y; - m_vertex_shader->SetUniform2fv(m_attributes[0].m_loc_output_resolution, res, 1); - } - - // fragment shader - if (m_attributes[1].m_loc_texture_src_resolution != -1) - { - res[0] = (float)texture_view.baseTexture->width; - res[1] = (float)texture_view.baseTexture->height; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_texture_src_resolution, res, 1); - } - - if (m_attributes[1].m_loc_input_resolution != -1) - { - res[0] = (float)input_res.x; - res[1] = (float)input_res.y; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_input_resolution, res, 1); - } - - if (m_attributes[1].m_loc_output_resolution != -1) - { - res[0] = (float)output_res.x; - res[1] = (float)output_res.y; - m_fragment_shader->SetUniform2fv(m_attributes[1].m_loc_output_resolution, res, 1); - } + if (locations.m_loc_outputResolution != -1) + { + res[0] = (float)output_res.x; + res[1] = (float)output_res.y; + shader->SetUniform2fv(locations.m_loc_outputResolution, res, 1); + } + }; + setUniforms(m_vertex_shader, m_uniformLocations[0]); + setUniforms(m_fragment_shader, m_uniformLocations[1]); } RendererOutputShader* RendererOutputShader::s_copy_shader; @@ -341,6 +281,27 @@ void main(){ )"; return vertex_source.str(); } + +std::string RendererOutputShader::PrependFragmentPreamble(const std::string& shaderSrc) +{ + return R"(#version 430 +#ifdef VULKAN +layout(push_constant) uniform pc { + vec2 textureSrcResolution; + vec2 nativeResolution; + vec2 outputResolution; +}; +#else +uniform vec2 textureSrcResolution; +uniform vec2 nativeResolution; +uniform vec2 outputResolution; +#endif + +layout(location = 0) in vec2 passUV; +layout(binding = 0) uniform sampler2D textureSrc; +layout(location = 0) out vec4 colorOut0; +)" + shaderSrc; +} void RendererOutputShader::InitializeStatic() { std::string vertex_source, vertex_source_ud; @@ -349,30 +310,20 @@ void RendererOutputShader::InitializeStatic() { vertex_source = GetOpenGlVertexSource(false); vertex_source_ud = GetOpenGlVertexSource(true); - - s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); - s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); - - s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); - s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); - - s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); - s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source); } else { vertex_source = GetVulkanVertexSource(false); vertex_source_ud = GetVulkanVertexSource(true); - - s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); - s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); - - /* s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); TODO - s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); - - s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); - s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source);*/ } + s_copy_shader = new RendererOutputShader(vertex_source, s_copy_shader_source); + s_copy_shader_ud = new RendererOutputShader(vertex_source_ud, s_copy_shader_source); + + s_bicubic_shader = new RendererOutputShader(vertex_source, s_bicubic_shader_source); + s_bicubic_shader_ud = new RendererOutputShader(vertex_source_ud, s_bicubic_shader_source); + + s_hermit_shader = new RendererOutputShader(vertex_source, s_hermite_shader_source); + s_hermit_shader_ud = new RendererOutputShader(vertex_source_ud, s_hermite_shader_source); } void RendererOutputShader::ShutdownStatic() diff --git a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h index 494b5247..b12edf8b 100644 --- a/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h +++ b/src/Cafe/HW/Latte/Renderer/RendererOuputShader.h @@ -17,7 +17,7 @@ public: RendererOutputShader(const std::string& vertex_source, const std::string& fragment_source); virtual ~RendererOutputShader() = default; - void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& input_res, const Vector2i& output_res) const; + void SetUniformParameters(const LatteTextureView& texture_view, const Vector2i& output_res) const; RendererShader* GetVertexShader() const { @@ -44,16 +44,18 @@ public: static std::string GetVulkanVertexSource(bool render_upside_down); static std::string GetOpenGlVertexSource(bool render_upside_down); + static std::string PrependFragmentPreamble(const std::string& shaderSrc); + protected: std::unique_ptr m_vertex_shader; std::unique_ptr m_fragment_shader; - struct + struct UniformLocations { - sint32 m_loc_texture_src_resolution = -1; - sint32 m_loc_input_resolution = -1; - sint32 m_loc_output_resolution = -1; - } m_attributes[2]{}; + sint32 m_loc_textureSrcResolution = -1; + sint32 m_loc_nativeResolution = -1; + sint32 m_loc_outputResolution = -1; + } m_uniformLocations[2]{}; private: static const std::string s_copy_shader_source; diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp index f0e2295e..de76f76d 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/LatteTextureViewVk.cpp @@ -202,6 +202,13 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter) VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + // emulate OpenGL minFilters + // see note under: https://docs.vulkan.org/spec/latest/chapters/samplers.html#VkSamplerCreateInfo + // if maxLod = 0 then magnification is always performed + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.25f; + if (useLinearTexFilter) { samplerInfo.magFilter = VK_FILTER_LINEAR; @@ -212,6 +219,9 @@ VkSampler LatteTextureViewVk::GetDefaultTextureSampler(bool useLinearTexFilter) samplerInfo.magFilter = VK_FILTER_NEAREST; samplerInfo.minFilter = VK_FILTER_NEAREST; } + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; if (vkCreateSampler(m_device, &samplerInfo, nullptr, &sampler) != VK_SUCCESS) { diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h b/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h index 06b53d08..acc81efc 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VKRBase.h @@ -19,7 +19,7 @@ public: virtual ~VKRMoveableRefCounter() { - cemu_assert_debug(refCount == 0); + cemu_assert_debug(m_refCount == 0); // remove references #ifdef CEMU_DEBUG_ASSERT @@ -30,7 +30,11 @@ public: } #endif for (auto itr : refs) - itr->ref->refCount--; + { + itr->ref->m_refCount--; + if (itr->ref->m_refCount == 0) + itr->ref->RefCountReachedZero(); + } refs.clear(); delete selfRef; selfRef = nullptr; @@ -41,8 +45,8 @@ public: VKRMoveableRefCounter(VKRMoveableRefCounter&& rhs) noexcept { this->refs = std::move(rhs.refs); - this->refCount = rhs.refCount; - rhs.refCount = 0; + this->m_refCount = rhs.m_refCount; + rhs.m_refCount = 0; this->selfRef = rhs.selfRef; rhs.selfRef = nullptr; this->selfRef->ref = this; @@ -57,7 +61,7 @@ public: void addRef(VKRMoveableRefCounter* refTarget) { this->refs.emplace_back(refTarget->selfRef); - refTarget->refCount++; + refTarget->m_refCount++; #ifdef CEMU_DEBUG_ASSERT // add reverse ref @@ -68,16 +72,23 @@ public: // methods to directly increment/decrement ref counter (for situations where no external object is available) void incRef() { - this->refCount++; + m_refCount++; } void decRef() { - this->refCount--; + m_refCount--; + if (m_refCount == 0) + RefCountReachedZero(); } protected: - int refCount{}; + virtual void RefCountReachedZero() + { + // does nothing by default + } + + int m_refCount{}; private: VKRMoveableRefCounterRef* selfRef; std::vector refs; @@ -88,7 +99,7 @@ private: void moveObj(VKRMoveableRefCounter&& rhs) { this->refs = std::move(rhs.refs); - this->refCount = rhs.refCount; + this->m_refCount = rhs.m_refCount; this->selfRef = rhs.selfRef; this->selfRef->ref = this; } @@ -131,6 +142,25 @@ public: VkSampler m_textureDefaultSampler[2] = { VK_NULL_HANDLE, VK_NULL_HANDLE }; // relict from LatteTextureViewVk, get rid of it eventually }; + +class VKRObjectSampler : public VKRDestructibleObject +{ + public: + VKRObjectSampler(VkSamplerCreateInfo* samplerInfo); + ~VKRObjectSampler() override; + + static VKRObjectSampler* GetOrCreateSampler(VkSamplerCreateInfo* samplerInfo); + static void DestroyCache(); + + void RefCountReachedZero() override; // sampler objects are destroyed when not referenced anymore + + VkSampler GetSampler() const { return m_sampler; } + private: + static std::unordered_map s_samplerCache; + VkSampler m_sampler{ VK_NULL_HANDLE }; + uint64 m_hash; +}; + class VKRObjectRenderPass : public VKRDestructibleObject { public: diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 47c9a9b1..dd9949f3 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -699,6 +699,8 @@ VulkanRenderer::~VulkanRenderer() if (m_commandPool != VK_NULL_HANDLE) vkDestroyCommandPool(m_logicalDevice, m_commandPool, nullptr); + VKRObjectSampler::DestroyCache(); + // destroy debug callback if (m_debugCallback) { @@ -2611,10 +2613,18 @@ VkPipeline VulkanRenderer::backbufferBlit_createGraphicsPipeline(VkDescriptorSet colorBlending.blendConstants[2] = 0.0f; colorBlending.blendConstants[3] = 0.0f; + VkPushConstantRange pushConstantRange{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = 3 * sizeof(float) * 2 // 3 vec2's + }; + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &descriptorLayout; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; VkResult result = vkCreatePipelineLayout(m_logicalDevice, &pipelineLayoutInfo, nullptr, &m_pipelineLayout); if (result != VK_SUCCESS) @@ -2993,6 +3003,25 @@ void VulkanRenderer::DrawBackbufferQuad(LatteTextureView* texView, RendererOutpu vkCmdBindDescriptorSets(m_state.currentCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, &descriptSet, 0, nullptr); + // update push constants + Vector2f pushData[3]; + + // textureSrcResolution + sint32 effectiveWidth, effectiveHeight; + texView->baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0); + pushData[0] = {(float)effectiveWidth, (float)effectiveHeight}; + + // nativeResolution + pushData[1] = { + (float)texViewVk->baseTexture->width, + (float)texViewVk->baseTexture->height, + }; + + // outputResolution + pushData[2] = {(float)imageWidth,(float)imageHeight}; + + vkCmdPushConstants(m_state.currentCommandBuffer, m_pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(float) * 2 * 3, &pushData); + vkCmdDraw(m_state.currentCommandBuffer, 6, 1, 0, 0); vkCmdEndRenderPass(m_state.currentCommandBuffer); @@ -3719,6 +3748,7 @@ void VulkanRenderer::AppendOverlayDebugInfo() ImGui::Text("DS StorageBuf %u", performanceMonitor.vk.numDescriptorStorageBuffers.get()); ImGui::Text("Images %u", performanceMonitor.vk.numImages.get()); ImGui::Text("ImageView %u", performanceMonitor.vk.numImageViews.get()); + ImGui::Text("ImageSampler %u", performanceMonitor.vk.numSamplers.get()); ImGui::Text("RenderPass %u", performanceMonitor.vk.numRenderPass.get()); ImGui::Text("Framebuffer %u", performanceMonitor.vk.numFramebuffer.get()); m_spinlockDestructionQueue.lock(); @@ -3764,7 +3794,7 @@ void VKRDestructibleObject::flagForCurrentCommandBuffer() bool VKRDestructibleObject::canDestroy() { - if (refCount > 0) + if (m_refCount > 0) return false; return VulkanRenderer::GetInstance()->HasCommandBufferFinished(m_lastCmdBufferId); } @@ -3805,6 +3835,111 @@ VKRObjectTextureView::~VKRObjectTextureView() performanceMonitor.vk.numImageViews.decrement(); } +static uint64 CalcHashSamplerCreateInfo(const VkSamplerCreateInfo& info) +{ + uint64 h = 0xcbf29ce484222325ULL; + auto fnvHashCombine = [](uint64_t &h, auto val) { + using T = decltype(val); + static_assert(sizeof(T) <= 8); + uint64_t val64 = 0; + std::memcpy(&val64, &val, sizeof(val)); + h ^= val64; + h *= 0x100000001b3ULL; + }; + cemu_assert_debug(info.sType == VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO); + fnvHashCombine(h, info.flags); + fnvHashCombine(h, info.magFilter); + fnvHashCombine(h, info.minFilter); + fnvHashCombine(h, info.mipmapMode); + fnvHashCombine(h, info.addressModeU); + fnvHashCombine(h, info.addressModeV); + fnvHashCombine(h, info.addressModeW); + fnvHashCombine(h, info.mipLodBias); + fnvHashCombine(h, info.anisotropyEnable); + if(info.anisotropyEnable == VK_TRUE) + fnvHashCombine(h, info.maxAnisotropy); + fnvHashCombine(h, info.compareEnable); + if(info.compareEnable == VK_TRUE) + fnvHashCombine(h, info.compareOp); + fnvHashCombine(h, info.minLod); + fnvHashCombine(h, info.maxLod); + fnvHashCombine(h, info.borderColor); + fnvHashCombine(h, info.unnormalizedCoordinates); + // handle custom border color + VkBaseOutStructure* ext = (VkBaseOutStructure*)info.pNext; + while(ext) + { + if(ext->sType == VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT) + { + auto* extInfo = (VkSamplerCustomBorderColorCreateInfoEXT*)ext; + fnvHashCombine(h, extInfo->customBorderColor.uint32[0]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[1]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[2]); + fnvHashCombine(h, extInfo->customBorderColor.uint32[3]); + } + else + { + cemu_assert_unimplemented(); + } + ext = ext->pNext; + } + return h; +} + +std::unordered_map VKRObjectSampler::s_samplerCache; + +VKRObjectSampler::VKRObjectSampler(VkSamplerCreateInfo* samplerInfo) +{ + auto* vulkanRenderer = VulkanRenderer::GetInstance(); + if (vkCreateSampler(vulkanRenderer->GetLogicalDevice(), samplerInfo, nullptr, &m_sampler) != VK_SUCCESS) + vulkanRenderer->UnrecoverableError("Failed to create texture sampler"); + performanceMonitor.vk.numSamplers.increment(); + m_hash = CalcHashSamplerCreateInfo(*samplerInfo); +} + +VKRObjectSampler::~VKRObjectSampler() +{ + vkDestroySampler(VulkanRenderer::GetInstance()->GetLogicalDevice(), m_sampler, nullptr); + performanceMonitor.vk.numSamplers.decrement(); + // remove from cache + auto it = s_samplerCache.find(m_hash); + if(it != s_samplerCache.end()) + s_samplerCache.erase(it); +} + +void VKRObjectSampler::RefCountReachedZero() +{ + VulkanRenderer::GetInstance()->ReleaseDestructibleObject(this); +} + +VKRObjectSampler* VKRObjectSampler::GetOrCreateSampler(VkSamplerCreateInfo* samplerInfo) +{ + auto* vulkanRenderer = VulkanRenderer::GetInstance(); + uint64 hash = CalcHashSamplerCreateInfo(*samplerInfo); + auto it = s_samplerCache.find(hash); + if (it != s_samplerCache.end()) + { + auto* sampler = it->second; + return sampler; + } + auto* sampler = new VKRObjectSampler(samplerInfo); + s_samplerCache[hash] = sampler; + return sampler; +} + +void VKRObjectSampler::DestroyCache() +{ + // assuming all other objects which depend on vkSampler are destroyed, this cache should also have been emptied already + // but just to be sure lets still clear the cache + cemu_assert_debug(s_samplerCache.empty()); + for(auto& sampler : s_samplerCache) + { + cemu_assert_debug(sampler.second->m_refCount == 0); + delete sampler.second; + } + s_samplerCache.clear(); +} + VKRObjectRenderPass::VKRObjectRenderPass(AttachmentInfo_t& attachmentInfo, sint32 colorAttachmentCount) { // generate helper hash for pipeline state diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp index 2dd8ac00..9465645c 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp @@ -899,12 +899,9 @@ VkDescriptorSetInfo* VulkanRenderer::draw_getOrCreateDescriptorSet(PipelineInfo* } } - auto vkObjSampler = dsInfo->m_vkObjSamplers.emplace_back(new VKRObjectSampler); - dsInfo->m_vkObjDescriptorSet->addRef(vkObjSampler); - - if (vkCreateSampler(m_logicalDevice, &samplerInfo, nullptr, &vkObjSampler->sampler) != VK_SUCCESS) - UnrecoverableError("Failed to create texture sampler"); - info.sampler = vkObjSampler->sampler; + VKRObjectSampler* samplerObj = VKRObjectSampler::GetOrCreateSampler(&samplerInfo); + vkObjDS->addRef(samplerObj); + info.sampler = samplerObj->GetSampler(); textureArray.emplace_back(info); } diff --git a/src/Cafe/OS/RPL/rpl_structs.h b/src/Cafe/OS/RPL/rpl_structs.h index 998ec8d7..c66f6136 100644 --- a/src/Cafe/OS/RPL/rpl_structs.h +++ b/src/Cafe/OS/RPL/rpl_structs.h @@ -116,7 +116,7 @@ typedef struct /* +0x34 */ uint32be ukn34; /* +0x38 */ uint32be ukn38; /* +0x3C */ uint32be ukn3C; - /* +0x40 */ uint32be toolkitVersion; + /* +0x40 */ uint32be minimumToolkitVersion; /* +0x44 */ uint32be ukn44; /* +0x48 */ uint32be ukn48; /* +0x4C */ uint32be ukn4C; diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 4debb37f..03e01bfc 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -181,7 +181,7 @@ namespace camera sint32 CAMInit(uint32 cameraId, CAMInitInfo_t* camInitInfo, uint32be* error) { CameraInstance* camInstance = new CameraInstance(camInitInfo->width, camInitInfo->height, camInitInfo->handlerFuncPtr); - + *error = 0; // Hunter's Trophy 2 will fail to boot if we don't set this std::unique_lock _lock(g_mutex_camera); if (g_cameraCounter == 0) { diff --git a/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp b/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp index e2864fb9..33c8eedc 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_GHS.cpp @@ -156,12 +156,22 @@ namespace coreinit return ¤tThread->crt.eh_mem_manage; } - void* __gh_errno_ptr() + sint32be* __gh_errno_ptr() { OSThread_t* currentThread = coreinit::OSGetCurrentThread(); return ¤tThread->context.ghs_errno; } + void __gh_set_errno(sint32 errNo) + { + *__gh_errno_ptr() = errNo; + } + + sint32 __gh_get_errno() + { + return *__gh_errno_ptr(); + } + void* __get_eh_store_globals() { OSThread_t* currentThread = coreinit::OSGetCurrentThread(); @@ -272,6 +282,8 @@ namespace coreinit cafeExportRegister("coreinit", __get_eh_globals, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_mem_manage, LogType::Placeholder); cafeExportRegister("coreinit", __gh_errno_ptr, LogType::Placeholder); + cafeExportRegister("coreinit", __gh_set_errno, LogType::Placeholder); + cafeExportRegister("coreinit", __gh_get_errno, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_store_globals, LogType::Placeholder); cafeExportRegister("coreinit", __get_eh_store_globals_tdeh, LogType::Placeholder); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_GHS.h b/src/Cafe/OS/libs/coreinit/coreinit_GHS.h index 0ac09e94..5f000732 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_GHS.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_GHS.h @@ -4,5 +4,9 @@ namespace coreinit { void PrepareGHSRuntime(); + sint32be* __gh_errno_ptr(); + void __gh_set_errno(sint32 errNo); + sint32 __gh_get_errno(); + void InitializeGHS(); }; \ No newline at end of file diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp index 2f3808b7..db457047 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.cpp @@ -1114,13 +1114,13 @@ namespace coreinit thread->requestFlags = (OSThread_t::REQUEST_FLAG_BIT)(thread->requestFlags & OSThread_t::REQUEST_FLAG_CANCEL); // remove all flags except cancel flag // update total cycles - uint64 remainingCycles = std::min((uint64)hCPU->remainingCycles, (uint64)thread->quantumTicks); - uint64 executedCycles = thread->quantumTicks - remainingCycles; - if (executedCycles < hCPU->skippedCycles) + sint64 executedCycles = (sint64)thread->quantumTicks - (sint64)hCPU->remainingCycles; + executedCycles = std::max(executedCycles, 0); + if (executedCycles < (sint64)hCPU->skippedCycles) executedCycles = 0; else executedCycles -= hCPU->skippedCycles; - thread->totalCycles += executedCycles; + thread->totalCycles += (uint64)executedCycles; // store context and set current thread to null __OSThreadStoreContext(hCPU, thread); OSSetCurrentThread(OSGetCoreId(), nullptr); diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h index df787bf0..1a93022b 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Thread.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Thread.h @@ -38,7 +38,7 @@ struct OSContext_t /* +0x1E0 */ uint64be fp_ps1[32]; /* +0x2E0 */ uint64be coretime[3]; /* +0x2F8 */ uint64be starttime; - /* +0x300 */ uint32be ghs_errno; // returned by __gh_errno_ptr() (used by socketlasterr) + /* +0x300 */ sint32be ghs_errno; // returned by __gh_errno_ptr() (used by socketlasterr) /* +0x304 */ uint32be affinity; /* +0x308 */ uint32be upmc1; /* +0x30C */ uint32be upmc2; diff --git a/src/Cafe/OS/libs/coreinit/coreinit_Time.h b/src/Cafe/OS/libs/coreinit/coreinit_Time.h index 018e8eb7..3aa92b99 100644 --- a/src/Cafe/OS/libs/coreinit/coreinit_Time.h +++ b/src/Cafe/OS/libs/coreinit/coreinit_Time.h @@ -40,7 +40,12 @@ namespace coreinit inline TimerTicks ConvertNsToTimerTicks(uint64 ns) { - return ((GetTimerClock() / 31250LL) * ((ns)) / 32000LL); + return ((GetTimerClock() / 31250LL) * ((TimerTicks)ns) / 32000LL); + } + + inline TimerTicks ConvertMsToTimerTicks(uint64 ms) + { + return (TimerTicks)ms * GetTimerClock() / 1000LL; } }; diff --git a/src/Cafe/OS/libs/erreula/erreula.cpp b/src/Cafe/OS/libs/erreula/erreula.cpp index a7f2f35c..342e8b64 100644 --- a/src/Cafe/OS/libs/erreula/erreula.cpp +++ b/src/Cafe/OS/libs/erreula/erreula.cpp @@ -9,32 +9,45 @@ #include #include "Cafe/OS/libs/coreinit/coreinit_FS.h" +#include "Cafe/OS/libs/coreinit/coreinit_Time.h" #include "Cafe/OS/libs/vpad/vpad.h" namespace nn { namespace erreula { -#define RESULTTYPE_NONE 0 -#define RESULTTYPE_FINISH 1 -#define RESULTTYPE_NEXT 2 -#define RESULTTYPE_JUMP 3 -#define RESULTTYPE_PASSWORD 4 -#define ERRORTYPE_CODE 0 -#define ERRORTYPE_TEXT 1 -#define ERRORTYPE_TEXT_ONE_BUTTON 2 -#define ERRORTYPE_TEXT_TWO_BUTTON 3 - -#define ERREULA_STATE_HIDDEN 0 -#define ERREULA_STATE_APPEARING 1 -#define ERREULA_STATE_VISIBLE 2 -#define ERREULA_STATE_DISAPPEARING 3 - - struct AppearArg_t + enum class ErrorDialogType : uint32 { - AppearArg_t() = default; - AppearArg_t(const AppearArg_t& o) + Code = 0, + Text = 1, + TextOneButton = 2, + TextTwoButton = 3 + }; + + static const sint32 FADE_TIME = 80; + + enum class ErrEulaState : uint32 + { + Hidden = 0, + Appearing = 1, + Visible = 2, + Disappearing = 3 + }; + + enum class ResultType : uint32 + { + None = 0, + Finish = 1, + Next = 2, + Jump = 3, + Password = 4 + }; + + struct AppearError + { + AppearError() = default; + AppearError(const AppearError& o) { errorType = o.errorType; screenType = o.screenType; @@ -49,7 +62,7 @@ namespace erreula drawCursor = o.drawCursor; } - uint32be errorType; + betype errorType; uint32be screenType; uint32be controllerType; uint32be holdType; @@ -63,7 +76,9 @@ namespace erreula bool drawCursor{}; }; - static_assert(sizeof(AppearArg_t) == 0x2C); // maybe larger + using AppearArg = AppearError; + + static_assert(sizeof(AppearError) == 0x2C); // maybe larger struct HomeNixSignArg_t { @@ -80,6 +95,132 @@ namespace erreula static_assert(sizeof(ControllerInfo_t) == 0x14); // maybe larger + class ErrEulaInstance + { + public: + enum class BUTTON_SELECTION : uint32 + { + NONE = 0xFFFFFFFF, + LEFT = 0, + RIGHT = 1, + }; + + void Init() + { + m_buttonSelection = BUTTON_SELECTION::NONE; + m_resultCode = -1; + m_resultCodeForLeftButton = 0; + m_resultCodeForRightButton = 0; + SetState(ErrEulaState::Hidden); + } + + void DoAppearError(AppearArg* arg) + { + m_buttonSelection = BUTTON_SELECTION::NONE; + m_resultCode = -1; + m_resultCodeForLeftButton = -1; + m_resultCodeForRightButton = -1; + // for standard dialog its 0 and 1? + m_resultCodeForLeftButton = 0; + m_resultCodeForRightButton = 1; + SetState(ErrEulaState::Appearing); + } + + void DoDisappearError() + { + if(m_state != ErrEulaState::Visible) + return; + SetState(ErrEulaState::Disappearing); + } + + void DoCalc() + { + // appearing and disappearing state will automatically advance after some time + if (m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing) + { + uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange; + if (elapsedTick > coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)) + { + SetState(m_state == ErrEulaState::Appearing ? ErrEulaState::Visible : ErrEulaState::Hidden); + } + } + } + + bool IsDecideSelectButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::NONE; + } + + bool IsDecideSelectLeftButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::LEFT; + } + + bool IsDecideSelectRightButtonError() const + { + return m_buttonSelection != BUTTON_SELECTION::RIGHT; + } + + void SetButtonSelection(BUTTON_SELECTION selection) + { + cemu_assert_debug(m_buttonSelection == BUTTON_SELECTION::NONE); + m_buttonSelection = selection; + cemu_assert_debug(selection == BUTTON_SELECTION::LEFT || selection == BUTTON_SELECTION::RIGHT); + m_resultCode = selection == BUTTON_SELECTION::LEFT ? m_resultCodeForLeftButton : m_resultCodeForRightButton; + } + + ErrEulaState GetState() const + { + return m_state; + } + + sint32 GetResultCode() const + { + return m_resultCode; + } + + ResultType GetResultType() const + { + if(m_resultCode == -1) + return ResultType::None; + if(m_resultCode < 10) + return ResultType::Finish; + if(m_resultCode >= 9999) + return ResultType::Next; + if(m_resultCode == 40) + return ResultType::Password; + return ResultType::Jump; + } + + float GetFadeTransparency() const + { + if(m_state == ErrEulaState::Appearing || m_state == ErrEulaState::Disappearing) + { + uint32 elapsedTick = coreinit::OSGetTime() - m_lastStateChange; + if(m_state == ErrEulaState::Appearing) + return std::min(1.0f, (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)); + else + return std::max(0.0f, 1.0f - (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME)); + } + return 1.0f; + } + + private: + void SetState(ErrEulaState state) + { + m_state = state; + m_lastStateChange = coreinit::OSGetTime(); + } + + ErrEulaState m_state; + uint32 m_lastStateChange; + + /* +0x30 */ betype m_resultCode; + /* +0x239C */ betype m_buttonSelection; + /* +0x23A0 */ betype m_resultCodeForLeftButton; + /* +0x23A4 */ betype m_resultCodeForRightButton; + }; + struct ErrEula_t { SysAllocator mutex; @@ -87,17 +228,11 @@ namespace erreula uint32 langType; MEMPTR fsClient; - AppearArg_t currentDialog; - uint32 state; - bool buttonPressed; - bool rightButtonPressed; + std::unique_ptr errEulaInstance; + AppearError currentDialog; bool homeNixSignVisible; - - std::chrono::steady_clock::time_point stateTimer{}; } g_errEula = {}; - - std::wstring GetText(uint16be* text) { @@ -113,22 +248,61 @@ namespace erreula } - void export_ErrEulaCreate(PPCInterpreter_t* hCPU) + void ErrEulaCreate(void* workmem, uint32 regionType, uint32 langType, coreinit::FSClient_t* fsClient) { - ppcDefineParamMEMPTR(thisptr, uint8, 0); - ppcDefineParamU32(regionType, 1); - ppcDefineParamU32(langType, 2); - ppcDefineParamMEMPTR(fsClient, coreinit::FSClient_t, 3); - coreinit::OSLockMutex(&g_errEula.mutex); g_errEula.regionType = regionType; g_errEula.langType = langType; g_errEula.fsClient = fsClient; + cemu_assert_debug(!g_errEula.errEulaInstance); + g_errEula.errEulaInstance = std::make_unique(); + g_errEula.errEulaInstance->Init(); coreinit::OSUnlockMutex(&g_errEula.mutex); + } - osLib_returnFromFunction(hCPU, 0); + void ErrEulaDestroy() + { + g_errEula.errEulaInstance.reset(); + } + + // check if any dialog button was selected + bool IsDecideSelectButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectButtonError(); + } + + // check if left dialog button was selected + bool IsDecideSelectLeftButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectLeftButtonError(); + } + + // check if right dialog button was selected + bool IsDecideSelectRightButtonError() + { + if(!g_errEula.errEulaInstance) + return false; + return g_errEula.errEulaInstance->IsDecideSelectRightButtonError(); + } + + sint32 GetResultCode() + { + if(!g_errEula.errEulaInstance) + return -1; + return g_errEula.errEulaInstance->GetResultCode(); + } + + ResultType GetResultType() + { + if(!g_errEula.errEulaInstance) + return ResultType::None; + return g_errEula.errEulaInstance->GetResultType(); } void export_AppearHomeNixSign(PPCInterpreter_t* hCPU) @@ -137,28 +311,24 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_AppearError(PPCInterpreter_t* hCPU) + void ErrEulaAppearError(AppearArg* arg) { - ppcDefineParamMEMPTR(arg, AppearArg_t, 0); - - g_errEula.currentDialog = *arg.GetPtr(); - g_errEula.state = ERREULA_STATE_APPEARING; - g_errEula.buttonPressed = false; - g_errEula.rightButtonPressed = false; - - g_errEula.stateTimer = tick_cached(); - - osLib_returnFromFunction(hCPU, 0); + g_errEula.currentDialog = *arg; + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoAppearError(arg); } - void export_GetStateErrorViewer(PPCInterpreter_t* hCPU) + void ErrEulaDisappearError() { - osLib_returnFromFunction(hCPU, g_errEula.state); + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoDisappearError(); } - void export_DisappearError(PPCInterpreter_t* hCPU) + + ErrEulaState ErrEulaGetStateErrorViewer() { - g_errEula.state = ERREULA_STATE_HIDDEN; - osLib_returnFromFunction(hCPU, 0); + if(!g_errEula.errEulaInstance) + return ErrEulaState::Hidden; + return g_errEula.errEulaInstance->GetState(); } void export_ChangeLang(PPCInterpreter_t* hCPU) @@ -168,27 +338,6 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_IsDecideSelectButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.buttonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.buttonPressed); - } - - void export_IsDecideSelectLeftButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.buttonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectLeftButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.buttonPressed); - } - - void export_IsDecideSelectRightButtonError(PPCInterpreter_t* hCPU) - { - if (g_errEula.rightButtonPressed) - cemuLog_logDebug(LogType::Force, "IsDecideSelectRightButtonError: TRUE"); - osLib_returnFromFunction(hCPU, g_errEula.rightButtonPressed); - } - void export_IsAppearHomeNixSign(PPCInterpreter_t* hCPU) { osLib_returnFromFunction(hCPU, g_errEula.homeNixSignVisible); @@ -200,61 +349,19 @@ namespace erreula osLib_returnFromFunction(hCPU, 0); } - void export_GetResultType(PPCInterpreter_t* hCPU) + void ErrEulaCalc(ControllerInfo_t* controllerInfo) { - uint32 result = RESULTTYPE_NONE; - if (g_errEula.buttonPressed || g_errEula.rightButtonPressed) - { - cemuLog_logDebug(LogType::Force, "GetResultType: FINISH"); - result = RESULTTYPE_FINISH; - } - - osLib_returnFromFunction(hCPU, result); - } - - void export_Calc(PPCInterpreter_t* hCPU) - { - ppcDefineParamMEMPTR(controllerInfo, ControllerInfo_t, 0); - // TODO: check controller buttons bla to accept dialog? - osLib_returnFromFunction(hCPU, 0); + if(g_errEula.errEulaInstance) + g_errEula.errEulaInstance->DoCalc(); } void render(bool mainWindow) { - if(g_errEula.state == ERREULA_STATE_HIDDEN) + if(!g_errEula.errEulaInstance) return; - - if(g_errEula.state == ERREULA_STATE_APPEARING) - { - if(std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() <= 1000) - { - return; - } - - g_errEula.state = ERREULA_STATE_VISIBLE; - g_errEula.stateTimer = tick_cached(); - } - /*else if(g_errEula.state == STATE_VISIBLE) - { - if (std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() >= 1000) - { - g_errEula.state = STATE_DISAPPEARING; - g_errEula.stateTimer = tick_cached(); - return; - } - }*/ - else if(g_errEula.state == ERREULA_STATE_DISAPPEARING) - { - if (std::chrono::duration_cast(tick_cached() - g_errEula.stateTimer).count() >= 2000) - { - g_errEula.state = ERREULA_STATE_HIDDEN; - g_errEula.stateTimer = tick_cached(); - } - + if(g_errEula.errEulaInstance->GetState() != ErrEulaState::Visible && g_errEula.errEulaInstance->GetState() != ErrEulaState::Appearing && g_errEula.errEulaInstance->GetState() != ErrEulaState::Disappearing) return; - } - - const AppearArg_t& appearArg = g_errEula.currentDialog; + const AppearError& appearArg = g_errEula.currentDialog; std::string text; const uint32 errorCode = (uint32)appearArg.errorCode; if (errorCode != 0) @@ -276,17 +383,28 @@ namespace erreula ImGui::SetNextWindowPos(position, ImGuiCond_Always, pivot); ImGui::SetNextWindowBgAlpha(0.9f); ImGui::PushFont(font); - + std::string title; if (appearArg.title) title = boost::nowide::narrow(GetText(appearArg.title.GetPtr())); - if(title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty + if (title.empty()) // ImGui doesn't allow empty titles, so set one if appearArg.title is not set or empty title = "ErrEula"; + + float fadeTransparency = 1.0f; + if (g_errEula.errEulaInstance->GetState() == ErrEulaState::Appearing || g_errEula.errEulaInstance->GetState() == ErrEulaState::Disappearing) + { + fadeTransparency = g_errEula.errEulaInstance->GetFadeTransparency(); + } + + float originalAlpha = ImGui::GetStyle().Alpha; + ImGui::GetStyle().Alpha = fadeTransparency; + ImGui::SetNextWindowBgAlpha(0.9f * fadeTransparency); if (ImGui::Begin(title.c_str(), nullptr, kPopupFlags)) { const float startx = ImGui::GetWindowSize().x / 2.0f; + bool hasLeftButtonPressed = false, hasRightButtonPressed = false; - switch ((uint32)appearArg.errorType) + switch (appearArg.errorType) { default: { @@ -294,11 +412,10 @@ namespace erreula ImGui::TextUnformatted(text.c_str(), text.c_str() + text.size()); ImGui::Spacing(); ImGui::SetCursorPosX(startx - 50); - g_errEula.buttonPressed |= ImGui::Button("OK", {100, 0}); - + hasLeftButtonPressed = ImGui::Button("OK", {100, 0}); break; } - case ERRORTYPE_TEXT: + case ErrorDialogType::Text: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -309,10 +426,10 @@ namespace erreula ImGui::Spacing(); ImGui::SetCursorPosX(startx - 50); - g_errEula.buttonPressed |= ImGui::Button("OK", { 100, 0 }); + hasLeftButtonPressed = ImGui::Button("OK", { 100, 0 }); break; } - case ERRORTYPE_TEXT_ONE_BUTTON: + case ErrorDialogType::TextOneButton: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -328,10 +445,10 @@ namespace erreula float width = std::max(100.0f, ImGui::CalcTextSize(button1.c_str()).x + 10.0f); ImGui::SetCursorPosX(startx - (width / 2.0f)); - g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width, 0 }); + hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width, 0 }); break; } - case ERRORTYPE_TEXT_TWO_BUTTON: + case ErrorDialogType::TextTwoButton: { std::string txtTmp = "Unknown Error"; if (appearArg.text) @@ -352,42 +469,52 @@ namespace erreula float width2 = std::max(100.0f, ImGui::CalcTextSize(button2.c_str()).x + 10.0f); ImGui::SetCursorPosX(startx - (width1 / 2.0f) - (width2 / 2.0f) - 10); - g_errEula.buttonPressed |= ImGui::Button(button1.c_str(), { width1, 0 }); + hasLeftButtonPressed = ImGui::Button(button1.c_str(), { width1, 0 }); ImGui::SameLine(); - g_errEula.rightButtonPressed |= ImGui::Button(button2.c_str(), { width2, 0 }); + hasRightButtonPressed = ImGui::Button(button2.c_str(), { width2, 0 }); break; } } + if (!g_errEula.errEulaInstance->IsDecideSelectButtonError()) + { + if (hasLeftButtonPressed) + g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::LEFT); + if (hasRightButtonPressed) + g_errEula.errEulaInstance->SetButtonSelection(ErrEulaInstance::BUTTON_SELECTION::RIGHT); + } } ImGui::End(); ImGui::PopFont(); - - if(g_errEula.buttonPressed || g_errEula.rightButtonPressed) - { - g_errEula.state = ERREULA_STATE_DISAPPEARING; - g_errEula.stateTimer = tick_cached(); - } + ImGui::GetStyle().Alpha = originalAlpha; } void load() { + g_errEula.errEulaInstance.reset(); + OSInitMutexEx(&g_errEula.mutex, nullptr); - //osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10", export_ErrEulaCreate); // copy ctor? - osLib_addFunction("erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", export_ErrEulaCreate); + cafeExportRegisterFunc(ErrEulaCreate, "erreula", "ErrEulaCreate__3RplFPUcQ3_2nn7erreula10RegionTypeQ3_2nn7erreula8LangTypeP8FSClient", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaDestroy, "erreula", "ErrEulaDestroy__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(IsDecideSelectButtonError, "erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(IsDecideSelectLeftButtonError, "erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(IsDecideSelectRightButtonError, "erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(GetResultCode, "erreula", "ErrEulaGetResultCode__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(GetResultType, "erreula", "ErrEulaGetResultType__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(ErrEulaAppearError, "erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaDisappearError, "erreula", "ErrEulaDisappearError__3RplFv", LogType::Placeholder); + cafeExportRegisterFunc(ErrEulaGetStateErrorViewer, "erreula", "ErrEulaGetStateErrorViewer__3RplFv", LogType::Placeholder); + + cafeExportRegisterFunc(ErrEulaCalc, "erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", LogType::Placeholder); + osLib_addFunction("erreula", "ErrEulaAppearHomeNixSign__3RplFRCQ3_2nn7erreula14HomeNixSignArg", export_AppearHomeNixSign); - osLib_addFunction("erreula", "ErrEulaAppearError__3RplFRCQ3_2nn7erreula9AppearArg", export_AppearError); - osLib_addFunction("erreula", "ErrEulaGetStateErrorViewer__3RplFv", export_GetStateErrorViewer); osLib_addFunction("erreula", "ErrEulaChangeLang__3RplFQ3_2nn7erreula8LangType", export_ChangeLang); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectButtonError__3RplFv", export_IsDecideSelectButtonError); - osLib_addFunction("erreula", "ErrEulaCalc__3RplFRCQ3_2nn7erreula14ControllerInfo", export_Calc); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectLeftButtonError__3RplFv", export_IsDecideSelectLeftButtonError); - osLib_addFunction("erreula", "ErrEulaIsDecideSelectRightButtonError__3RplFv", export_IsDecideSelectRightButtonError); osLib_addFunction("erreula", "ErrEulaIsAppearHomeNixSign__3RplFv", export_IsAppearHomeNixSign); osLib_addFunction("erreula", "ErrEulaDisappearHomeNixSign__3RplFv", export_DisappearHomeNixSign); - osLib_addFunction("erreula", "ErrEulaGetResultType__3RplFv", export_GetResultType); - osLib_addFunction("erreula", "ErrEulaDisappearError__3RplFv", export_DisappearError); } } } diff --git a/src/Cafe/OS/libs/gx2/GX2_Resource.cpp b/src/Cafe/OS/libs/gx2/GX2_Resource.cpp index 97f51a0d..a6029de9 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Resource.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Resource.cpp @@ -87,6 +87,11 @@ namespace GX2 return true; } + void GX2RSetBufferName(GX2RBuffer* buffer, const char* name) + { + // no-op in production builds + } + void* GX2RLockBufferEx(GX2RBuffer* buffer, uint32 resFlags) { return buffer->GetPtr(); @@ -226,6 +231,7 @@ namespace GX2 cafeExportRegister("gx2", GX2RCreateBufferUserMemory, LogType::GX2); cafeExportRegister("gx2", GX2RDestroyBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RBufferExists, LogType::GX2); + cafeExportRegister("gx2", GX2RSetBufferName, LogType::GX2); cafeExportRegister("gx2", GX2RLockBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RUnlockBufferEx, LogType::GX2); cafeExportRegister("gx2", GX2RInvalidateBuffer, LogType::GX2); diff --git a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp index dfbbfcff..7a153737 100644 --- a/src/Cafe/OS/libs/gx2/GX2_Shader.cpp +++ b/src/Cafe/OS/libs/gx2/GX2_Shader.cpp @@ -421,7 +421,7 @@ namespace GX2 { if(aluRegisterOffset&0x8000) { - cemuLog_logDebug(LogType::Force, "_GX2SubmitUniformReg(): Unhandled loop const special case or invalid offset"); + cemuLog_logDebugOnce(LogType::Force, "_GX2SubmitUniformReg(): Unhandled loop const special case or invalid offset"); return; } if((aluRegisterOffset+sizeInU32s) > 0x400) diff --git a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp index 95eaf06a..533d349e 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendEmulated.cpp @@ -1,4 +1,6 @@ #include "BackendEmulated.h" + +#include "Dimensions.h" #include "Infinity.h" #include "Skylander.h" #include "config/CemuConfig.h" @@ -33,5 +35,12 @@ namespace nsyshid::backend::emulated auto device = std::make_shared(); AttachDevice(device); } + if (GetConfig().emulated_usb_devices.emulate_dimensions_toypad && !FindDeviceById(0x0E6F, 0x0241)) + { + cemuLog_logDebug(LogType::Force, "Attaching Emulated Toypad"); + // Add Dimensions Toypad + auto device = std::make_shared(); + AttachDevice(device); + } } } // namespace nsyshid::backend::emulated \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp index 7548c998..ab355136 100644 --- a/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp +++ b/src/Cafe/OS/libs/nsyshid/BackendLibusb.cpp @@ -15,7 +15,7 @@ namespace nsyshid::backend::libusb if (m_initReturnCode < 0) { m_ctx = nullptr; - cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb with return code %i", + cemuLog_logDebug(LogType::Force, "nsyshid::BackendLibusb: failed to initialize libusb, return code: {}", m_initReturnCode); return; } @@ -35,7 +35,7 @@ namespace nsyshid::backend::libusb if (ret != LIBUSB_SUCCESS) { cemuLog_logDebug(LogType::Force, - "nsyshid::BackendLibusb: failed to register hotplug callback with return code %i", + "nsyshid::BackendLibusb: failed to register hotplug callback with return code {}", ret); } else @@ -415,7 +415,7 @@ namespace nsyshid::backend::libusb if (ret < 0) { cemuLog_log(LogType::Force, - "nsyshid::DeviceLibusb::open(): failed to get device descriptor; return code: %i", + "nsyshid::DeviceLibusb::open(): failed to get device descriptor, return code: {}", ret); libusb_free_device_list(devices, 1); return false; @@ -439,8 +439,8 @@ namespace nsyshid::backend::libusb { this->m_libusbHandle = nullptr; cemuLog_log(LogType::Force, - "nsyshid::DeviceLibusb::open(): failed to open device; return code: %i", - ret); + "nsyshid::DeviceLibusb::open(): failed to open device: {}", + libusb_strerror(ret)); libusb_free_device_list(devices, 1); return false; } diff --git a/src/Cafe/OS/libs/nsyshid/Dimensions.cpp b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp new file mode 100644 index 00000000..8a2acc76 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Dimensions.cpp @@ -0,0 +1,1163 @@ +#include "Dimensions.h" + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +#include +#include + +namespace nsyshid +{ + static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, + 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; + + static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, 0x3C, 0xA8, + 0xD8, 0x75, 0x47, 0x68, 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; + + static constexpr std::array PWD_CONSTANT = {0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, + 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4C, 0x45, + 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; + + DimensionsUSB g_dimensionstoypad; + + const std::map s_listMinis = { + {0, "Blank Tag"}, + {1, "Batman"}, + {2, "Gandalf"}, + {3, "Wyldstyle"}, + {4, "Aquaman"}, + {5, "Bad Cop"}, + {6, "Bane"}, + {7, "Bart Simpson"}, + {8, "Benny"}, + {9, "Chell"}, + {10, "Cole"}, + {11, "Cragger"}, + {12, "Cyborg"}, + {13, "Cyberman"}, + {14, "Doc Brown"}, + {15, "The Doctor"}, + {16, "Emmet"}, + {17, "Eris"}, + {18, "Gimli"}, + {19, "Gollum"}, + {20, "Harley Quinn"}, + {21, "Homer Simpson"}, + {22, "Jay"}, + {23, "Joker"}, + {24, "Kai"}, + {25, "ACU Trooper"}, + {26, "Gamer Kid"}, + {27, "Krusty the Clown"}, + {28, "Laval"}, + {29, "Legolas"}, + {30, "Lloyd"}, + {31, "Marty McFly"}, + {32, "Nya"}, + {33, "Owen Grady"}, + {34, "Peter Venkman"}, + {35, "Slimer"}, + {36, "Scooby-Doo"}, + {37, "Sensei Wu"}, + {38, "Shaggy"}, + {39, "Stay Puft"}, + {40, "Superman"}, + {41, "Unikitty"}, + {42, "Wicked Witch of the West"}, + {43, "Wonder Woman"}, + {44, "Zane"}, + {45, "Green Arrow"}, + {46, "Supergirl"}, + {47, "Abby Yates"}, + {48, "Finn the Human"}, + {49, "Ethan Hunt"}, + {50, "Lumpy Space Princess"}, + {51, "Jake the Dog"}, + {52, "Harry Potter"}, + {53, "Lord Voldemort"}, + {54, "Michael Knight"}, + {55, "B.A. Baracus"}, + {56, "Newt Scamander"}, + {57, "Sonic the Hedgehog"}, + {58, "Future Update (unreleased)"}, + {59, "Gizmo"}, + {60, "Stripe"}, + {61, "E.T."}, + {62, "Tina Goldstein"}, + {63, "Marceline the Vampire Queen"}, + {64, "Batgirl"}, + {65, "Robin"}, + {66, "Sloth"}, + {67, "Hermione Granger"}, + {68, "Chase McCain"}, + {69, "Excalibur Batman"}, + {70, "Raven"}, + {71, "Beast Boy"}, + {72, "Betelgeuse"}, + {73, "Lord Vortech (unreleased)"}, + {74, "Blossom"}, + {75, "Bubbles"}, + {76, "Buttercup"}, + {77, "Starfire"}, + {78, "World 15 (unreleased)"}, + {79, "World 16 (unreleased)"}, + {80, "World 17 (unreleased)"}, + {81, "World 18 (unreleased)"}, + {82, "World 19 (unreleased)"}, + {83, "World 20 (unreleased)"}, + {768, "Unknown 768"}, + {769, "Supergirl Red Lantern"}, + {770, "Unknown 770"}}; + + const std::map s_listTokens = { + {1000, "Police Car"}, + {1001, "Aerial Squad Car"}, + {1002, "Missile Striker"}, + {1003, "Gravity Sprinter"}, + {1004, "Street Shredder"}, + {1005, "Sky Clobberer"}, + {1006, "Batmobile"}, + {1007, "Batblaster"}, + {1008, "Sonic Batray"}, + {1009, "Benny's Spaceship"}, + {1010, "Lasercraft"}, + {1011, "The Annihilator"}, + {1012, "DeLorean Time Machine"}, + {1013, "Electric Time Machine"}, + {1014, "Ultra Time Machine"}, + {1015, "Hoverboard"}, + {1016, "Cyclone Board"}, + {1017, "Ultimate Hoverjet"}, + {1018, "Eagle Interceptor"}, + {1019, "Eagle Sky Blazer"}, + {1020, "Eagle Swoop Diver"}, + {1021, "Swamp Skimmer"}, + {1022, "Cragger's Fireship"}, + {1023, "Croc Command Sub"}, + {1024, "Cyber-Guard"}, + {1025, "Cyber-Wrecker"}, + {1026, "Laser Robot Walker"}, + {1027, "K-9"}, + {1028, "K-9 Ruff Rover"}, + {1029, "K-9 Laser Cutter"}, + {1030, "TARDIS"}, + {1031, "Laser-Pulse TARDIS"}, + {1032, "Energy-Burst TARDIS"}, + {1033, "Emmet's Excavator"}, + {1034, "Destroy Dozer"}, + {1035, "Destruct-o-Mech"}, + {1036, "Winged Monkey"}, + {1037, "Battle Monkey"}, + {1038, "Commander Monkey"}, + {1039, "Axe Chariot"}, + {1040, "Axe Hurler"}, + {1041, "Soaring Chariot"}, + {1042, "Shelob the Great"}, + {1043, "8-Legged Stalker"}, + {1044, "Poison Slinger"}, + {1045, "Homer's Car"}, + {1047, "The SubmaHomer"}, + {1046, "The Homercraft"}, + {1048, "Taunt-o-Vision"}, + {1050, "The MechaHomer"}, + {1049, "Blast Cam"}, + {1051, "Velociraptor"}, + {1053, "Venom Raptor"}, + {1052, "Spike Attack Raptor"}, + {1054, "Gyrosphere"}, + {1055, "Sonic Beam Gyrosphere"}, + {1056, " Gyrosphere"}, + {1057, "Clown Bike"}, + {1058, "Cannon Bike"}, + {1059, "Anti-Gravity Rocket Bike"}, + {1060, "Mighty Lion Rider"}, + {1061, "Lion Blazer"}, + {1062, "Fire Lion"}, + {1063, "Arrow Launcher"}, + {1064, "Seeking Shooter"}, + {1065, "Triple Ballista"}, + {1066, "Mystery Machine"}, + {1067, "Mystery Tow & Go"}, + {1068, "Mystery Monster"}, + {1069, "Boulder Bomber"}, + {1070, "Boulder Blaster"}, + {1071, "Cyclone Jet"}, + {1072, "Storm Fighter"}, + {1073, "Lightning Jet"}, + {1074, "Electro-Shooter"}, + {1075, "Blade Bike"}, + {1076, "Flight Fire Bike"}, + {1077, "Blades of Fire"}, + {1078, "Samurai Mech"}, + {1079, "Samurai Shooter"}, + {1080, "Soaring Samurai Mech"}, + {1081, "Companion Cube"}, + {1082, "Laser Deflector"}, + {1083, "Gold Heart Emitter"}, + {1084, "Sentry Turret"}, + {1085, "Turret Striker"}, + {1086, "Flight Turret Carrier"}, + {1087, "Scooby Snack"}, + {1088, "Scooby Fire Snack"}, + {1089, "Scooby Ghost Snack"}, + {1090, "Cloud Cuckoo Car"}, + {1091, "X-Stream Soaker"}, + {1092, "Rainbow Cannon"}, + {1093, "Invisible Jet"}, + {1094, "Laser Shooter"}, + {1095, "Torpedo Bomber"}, + {1096, "NinjaCopter"}, + {1097, "Glaciator"}, + {1098, "Freeze Fighter"}, + {1099, "Travelling Time Train"}, + {1100, "Flight Time Train"}, + {1101, "Missile Blast Time Train"}, + {1102, "Aqua Watercraft"}, + {1103, "Seven Seas Speeder"}, + {1104, "Trident of Fire"}, + {1105, "Drill Driver"}, + {1106, "Bane Dig 'n' Drill"}, + {1107, "Bane Drill 'n' Blast"}, + {1108, "Quinn Mobile"}, + {1109, "Quinn Ultra Racer"}, + {1110, "Missile Launcher"}, + {1111, "The Joker's Chopper"}, + {1112, "Mischievous Missile Blaster"}, + {1113, "Lock 'n' Laser Jet"}, + {1114, "Hover Pod"}, + {1115, "Krypton Striker"}, + {1116, "Super Stealth Pod"}, + {1117, "Dalek"}, + {1118, "Fire 'n' Ride Dalek"}, + {1119, "Silver Shooter Dalek"}, + {1120, "Ecto-1"}, + {1121, "Ecto-1 Blaster"}, + {1122, "Ecto-1 Water Diver"}, + {1123, "Ghost Trap"}, + {1124, "Ghost Stun 'n' Trap"}, + {1125, "Proton Zapper"}, + {1126, "Unknown"}, + {1127, "Unknown"}, + {1128, "Unknown"}, + {1129, "Unknown"}, + {1130, "Unknown"}, + {1131, "Unknown"}, + {1132, "Lloyd's Golden Dragon"}, + {1133, "Sword Projector Dragon"}, + {1134, "Unknown"}, + {1135, "Unknown"}, + {1136, "Unknown"}, + {1137, "Unknown"}, + {1138, "Unknown"}, + {1139, "Unknown"}, + {1140, "Unknown"}, + {1141, "Unknown"}, + {1142, "Unknown"}, + {1143, "Unknown"}, + {1144, "Mega Flight Dragon"}, + {1145, "Unknown"}, + {1146, "Unknown"}, + {1147, "Unknown"}, + {1148, "Unknown"}, + {1149, "Unknown"}, + {1150, "Unknown"}, + {1151, "Unknown"}, + {1152, "Unknown"}, + {1153, "Unknown"}, + {1154, "Unknown"}, + {1155, "Flying White Dragon"}, + {1156, "Golden Fire Dragon"}, + {1157, "Ultra Destruction Dragon"}, + {1158, "Arcade Machine"}, + {1159, "8-Bit Shooter"}, + {1160, "The Pixelator"}, + {1161, "G-6155 Spy Hunter"}, + {1162, "Interdiver"}, + {1163, "Aerial Spyhunter"}, + {1164, "Slime Shooter"}, + {1165, "Slime Exploder"}, + {1166, "Slime Streamer"}, + {1167, "Terror Dog"}, + {1168, "Terror Dog Destroyer"}, + {1169, "Soaring Terror Dog"}, + {1170, "Ancient Psychic Tandem War Elephant"}, + {1171, "Cosmic Squid"}, + {1172, "Psychic Submarine"}, + {1173, "BMO"}, + {1174, "DOGMO"}, + {1175, "SNAKEMO"}, + {1176, "Jakemobile"}, + {1177, "Snail Dude Jake"}, + {1178, "Hover Jake"}, + {1179, "Lumpy Car"}, + {1181, "Lumpy Land Whale"}, + {1180, "Lumpy Truck"}, + {1182, "Lunatic Amp"}, + {1183, "Shadow Scorpion"}, + {1184, "Heavy Metal Monster"}, + {1185, "B.A.'s Van"}, + {1186, "Fool Smasher"}, + {1187, "Pain Plane"}, + {1188, "Phone Home"}, + {1189, "Mobile Uplink"}, + {1190, "Super-Charged Satellite"}, + {1191, "Niffler"}, + {1192, "Sinister Scorpion"}, + {1193, "Vicious Vulture"}, + {1194, "Swooping Evil"}, + {1195, "Brutal Bloom"}, + {1196, "Crawling Creeper"}, + {1197, "Ecto-1 (2016)"}, + {1198, "Ectozer"}, + {1199, "PerfEcto"}, + {1200, "Flash 'n' Finish"}, + {1201, "Rampage Record Player"}, + {1202, "Stripe's Throne"}, + {1203, "R.C. Racer"}, + {1204, "Gadget-O-Matic"}, + {1205, "Scarlet Scorpion"}, + {1206, "Hogwarts Express"}, + {1208, "Steam Warrior"}, + {1207, "Soaring Steam Plane"}, + {1209, "Enchanted Car"}, + {1210, "Shark Sub"}, + {1211, "Monstrous Mouth"}, + {1212, "IMF Scrambler"}, + {1213, "Shock Cycle"}, + {1214, "IMF Covert Jet"}, + {1215, "IMF Sports Car"}, + {1216, "IMF Tank"}, + {1217, "IMF Splorer"}, + {1218, "Sonic Speedster"}, + {1219, "Blue Typhoon"}, + {1220, "Moto Bug"}, + {1221, "The Tornado"}, + {1222, "Crabmeat"}, + {1223, "Eggcatcher"}, + {1224, "K.I.T.T."}, + {1225, "Goliath Armored Semi"}, + {1226, "K.I.T.T. Jet"}, + {1227, "Police Helicopter"}, + {1228, "Police Hovercraft"}, + {1229, "Police Plane"}, + {1230, "Bionic Steed"}, + {1231, "Bat-Raptor"}, + {1232, "Ultrabat"}, + {1233, "Batwing"}, + {1234, "The Black Thunder"}, + {1235, "Bat-Tank"}, + {1236, "Skeleton Organ"}, + {1237, "Skeleton Jukebox"}, + {1238, "Skele-Turkey"}, + {1239, "One-Eyed Willy's Pirate Ship"}, + {1240, "Fanged Fortune"}, + {1241, "Inferno Cannon"}, + {1242, "Buckbeak"}, + {1243, "Giant Owl"}, + {1244, "Fierce Falcon"}, + {1245, "Saturn's Sandworm"}, + {1247, "Haunted Vacuum"}, + {1246, "Spooky Spider"}, + {1248, "PPG Smartphone"}, + {1249, "PPG Hotline"}, + {1250, "Powerpuff Mag-Net"}, + {1253, "Mega Blast Bot"}, + {1251, "Ka-Pow Cannon"}, + {1252, "Slammin' Guitar"}, + {1254, "Octi"}, + {1255, "Super Skunk"}, + {1256, "Sonic Squid"}, + {1257, "T-Car"}, + {1258, "T-Forklift"}, + {1259, "T-Plane"}, + {1260, "Spellbook of Azarath"}, + {1261, "Raven Wings"}, + {1262, "Giant Hand"}, + {1263, "Titan Robot"}, + {1264, "T-Rocket"}, + {1265, "Robot Retriever"}}; + + DimensionsToypadDevice::DimensionsToypadDevice() + : Device(0x0E6F, 0x0241, 1, 2, 0) + { + m_IsOpened = false; + } + + bool DimensionsToypadDevice::Open() + { + if (!IsOpened()) + { + m_IsOpened = true; + } + return true; + } + + void DimensionsToypadDevice::Close() + { + if (IsOpened()) + { + m_IsOpened = false; + } + } + + bool DimensionsToypadDevice::IsOpened() + { + return m_IsOpened; + } + + Device::ReadResult DimensionsToypadDevice::Read(ReadMessage* message) + { + memcpy(message->data, g_dimensionstoypad.GetStatus().data(), message->length); + message->bytesRead = message->length; + return Device::ReadResult::Success; + } + + Device::WriteResult DimensionsToypadDevice::Write(WriteMessage* message) + { + if (message->length != 32) + return Device::WriteResult::Error; + + g_dimensionstoypad.SendCommand(std::span{message->data, 32}); + message->bytesWritten = message->length; + return Device::WriteResult::Success; + } + + bool DimensionsToypadDevice::GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) + { + uint8 configurationDescriptor[0x29]; + + uint8* currentWritePtr; + + // configuration descriptor + currentWritePtr = configurationDescriptor + 0; + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 2; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0029; // wTotalLength + *(uint8*)(currentWritePtr + 4) = 1; // bNumInterfaces + *(uint8*)(currentWritePtr + 5) = 1; // bConfigurationValue + *(uint8*)(currentWritePtr + 6) = 0; // iConfiguration + *(uint8*)(currentWritePtr + 7) = 0x80; // bmAttributes + *(uint8*)(currentWritePtr + 8) = 0xFA; // MaxPower + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x04; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0; // bInterfaceNumber + *(uint8*)(currentWritePtr + 3) = 0; // bAlternateSetting + *(uint8*)(currentWritePtr + 4) = 2; // bNumEndpoints + *(uint8*)(currentWritePtr + 5) = 3; // bInterfaceClass + *(uint8*)(currentWritePtr + 6) = 0; // bInterfaceSubClass + *(uint8*)(currentWritePtr + 7) = 0; // bInterfaceProtocol + *(uint8*)(currentWritePtr + 8) = 0; // iInterface + currentWritePtr = currentWritePtr + 9; + // configuration descriptor + *(uint8*)(currentWritePtr + 0) = 9; // bLength + *(uint8*)(currentWritePtr + 1) = 0x21; // bDescriptorType + *(uint16be*)(currentWritePtr + 2) = 0x0111; // bcdHID + *(uint8*)(currentWritePtr + 4) = 0x00; // bCountryCode + *(uint8*)(currentWritePtr + 5) = 0x01; // bNumDescriptors + *(uint8*)(currentWritePtr + 6) = 0x22; // bDescriptorType + *(uint16be*)(currentWritePtr + 7) = 0x001D; // wDescriptorLength + currentWritePtr = currentWritePtr + 9; + // endpoint descriptor 1 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress + *(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 4) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 6) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + // endpoint descriptor 2 + *(uint8*)(currentWritePtr + 0) = 7; // bLength + *(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType + *(uint8*)(currentWritePtr + 1) = 0x02; // bEndpointAddress + *(uint8*)(currentWritePtr + 2) = 0x03; // bmAttributes + *(uint16be*)(currentWritePtr + 3) = 0x40; // wMaxPacketSize + *(uint8*)(currentWritePtr + 5) = 0x01; // bInterval + currentWritePtr = currentWritePtr + 7; + + cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29); + + memcpy(output, configurationDescriptor, + std::min(outputMaxLength, sizeof(configurationDescriptor))); + return true; + } + + bool DimensionsToypadDevice::SetProtocol(uint8 ifIndex, uint8 protocol) + { + cemuLog_log(LogType::Force, "Toypad Protocol"); + return true; + } + + bool DimensionsToypadDevice::SetReport(ReportMessage* message) + { + cemuLog_log(LogType::Force, "Toypad Report"); + return true; + } + + std::array DimensionsUSB::GetStatus() + { + std::array response = {}; + + bool responded = false; + do + { + if (!m_queries.empty()) + { + response = m_queries.front(); + m_queries.pop(); + responded = true; + } + else if (!m_figureAddedRemovedResponses.empty() && m_isAwake) + { + std::lock_guard lock(m_dimensionsMutex); + response = m_figureAddedRemovedResponses.front(); + m_figureAddedRemovedResponses.pop(); + responded = true; + } + else + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + while (responded == false); + return response; + } + + void DimensionsUSB::SendCommand(std::span buf) + { + const uint8 command = buf[2]; + const uint8 sequence = buf[3]; + + std::array q_result{}; + + switch (command) + { + case 0xB0: // Wake + { + // Consistent device response to the wake command + q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, + 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x46}; + break; + } + case 0xB1: // Seed + { + // Initialise a random number generator using the seed provided + g_dimensionstoypad.GenerateRandomNumber(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xB3: // Challenge + { + // Get the next number in the sequence based on the RNG from 0xB1 command + g_dimensionstoypad.GetChallengeResponse(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xC0: // Color + case 0xC1: // Get Pad Color + case 0xC2: // Fade + case 0xC3: // Flash + case 0xC4: // Fade Random + case 0xC6: // Fade All + case 0xC7: // Flash All + case 0xC8: // Color All + { + // Send a blank response to acknowledge color has been sent to toypad + q_result = {0x55, 0x01, sequence}; + q_result[3] = GenerateChecksum(q_result, 3); + break; + } + case 0xD2: // Read + { + // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] + g_dimensionstoypad.QueryBlock(buf[4], buf[5], q_result, sequence); + break; + } + case 0xD3: // Write + { + // Write 4 bytes to page buf[5] to the figure at index buf[4] + g_dimensionstoypad.WriteBlock(buf[4], buf[5], std::span{buf.begin() + 6, 4}, q_result, sequence); + break; + } + case 0xD4: // Model + { + // Get the model id of the figure at index buf[4] + g_dimensionstoypad.GetModel(std::span{buf.begin() + 4, 8}, sequence, q_result); + break; + } + case 0xD0: // Tag List + case 0xE1: // PWD + case 0xE5: // Active + case 0xFF: // LEDS Query + { + // Further investigation required + cemuLog_log(LogType::Force, "Unimplemented LD Function: {:x}", command); + break; + } + default: + { + cemuLog_log(LogType::Force, "Unknown LD Function: {:x}", command); + break; + } + } + + m_queries.push(q_result); + } + + uint32 DimensionsUSB::LoadFigure(const std::array& buf, std::unique_ptr file, uint8 pad, uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + const uint32 id = GetFigureId(buf); + + DimensionsMini& figure = GetFigureByIndex(index); + figure.dimFile = std::move(file); + figure.id = id; + figure.pad = pad; + figure.index = index + 1; + figure.data = buf; + // When a figure is added to the toypad, respond to the game with the pad they were added to, their index, + // the direction (0x00 in byte 6 for added) and their UID + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return id; + } + + bool DimensionsUSB::RemoveFigure(uint8 pad, uint8 index, bool fullRemove) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // When a figure is removed from the toypad, respond to the game with the pad they were removed from, their index, + // the direction (0x01 in byte 6 for removed) and their UID + if (fullRemove) + { + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + figure.Save(); + figure.dimFile.reset(); + } + + figure.index = 255; + figure.pad = 255; + figure.id = 0; + + return true; + } + + bool DimensionsUSB::TempRemove(uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // Send a response to the game that the figure has been "Picked up" from existing slot, + // until either the movement is cancelled, or user chooses a space to move to + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + return true; + } + + bool DimensionsUSB::CancelRemove(uint8 index) + { + std::lock_guard lock(m_dimensionsMutex); + + DimensionsMini& figure = GetFigureByIndex(index); + if (figure.index == 255) + return false; + + // Cancel the previous movement of the figure + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, + figure.data[0], figure.data[1], figure.data[2], + figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figureAddedRemovedResponses.push(figureChangeResponse); + + return true; + } + + bool DimensionsUSB::CreateFigure(fs::path pathName, uint32 id) + { + FileStream* dimFile(FileStream::createFile2(pathName)); + if (!dimFile) + return false; + + std::array fileData{}; + RandomUID(fileData); + fileData[3] = id & 0xFF; + + std::array uid = {fileData[0], fileData[1], fileData[2], fileData[4], fileData[5], fileData[6], fileData[7]}; + + // Only characters are created with their ID encrypted and stored in pages 36 and 37, + // as well as a password stored in page 43. Blank tags have their information populated + // by the game when it calls the write_block command. + if (id != 0) + { + const std::array figureKey = GenerateFigureKey(fileData); + + std::array valueToEncrypt = {uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF), + uint8(id & 0xFF), uint8((id >> 8) & 0xFF), uint8((id >> 16) & 0xFF), uint8((id >> 24) & 0xFF)}; + + std::array encrypted = Encrypt(valueToEncrypt, figureKey); + + std::memcpy(&fileData[36 * 4], &encrypted[0], 4); + std::memcpy(&fileData[37 * 4], &encrypted[4], 4); + + std::memcpy(&fileData[43 * 4], PWDGenerate(fileData).data(), 4); + } + else + { + // Page 38 is used as verification for blank tags + fileData[(38 * 4) + 1] = 1; + } + + if (fileData.size() != dimFile->writeData(fileData.data(), fileData.size())) + { + delete dimFile; + return false; + } + delete dimFile; + return true; + } + + bool DimensionsUSB::MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex) + { + if (oldIndex == index) + { + // Don't bother removing and loading again, just send response to the game + CancelRemove(index); + return true; + } + + // When moving figures between spaces on the toypad, remove any figure from the space they are moving to, + // then remove them from their current space, then load them to the space they are moving to + RemoveFigure(pad, index, true); + + DimensionsMini& figure = GetFigureByIndex(oldIndex); + const std::array data = figure.data; + std::unique_ptr inFile = std::move(figure.dimFile); + + RemoveFigure(oldPad, oldIndex, false); + + LoadFigure(data, std::move(inFile), pad, index); + + return true; + } + + void DimensionsUSB::GenerateRandomNumber(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload into an 8 byte array + std::array value = Decrypt(buf, std::nullopt); + // Seed is the first 4 bytes (little endian) of the decrypted payload + uint32 seed = (uint32&)value[0]; + // Confirmation is the second 4 bytes (big endian) of the decrypted payload + uint32 conf = (uint32be&)value[4]; + // Initialize rng using the seed from decrypted payload + InitializeRNG(seed); + // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are blank + std::array valueToEncrypt = {value[4], value[5], value[6], value[7], 0, 0, 0, 0}; + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + // Copy encrypted value to response data + memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + } + + void DimensionsUSB::GetChallengeResponse(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload into an 8 byte array + std::array value = Decrypt(buf, std::nullopt); + // Confirmation is the first 4 bytes of the decrypted payload + uint32 conf = (uint32be&)value[0]; + // Generate next random number based on RNG + uint32 nextRandom = GetNext(); + // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) + // followed by the confirmation from the decrypted payload + std::array valueToEncrypt = {uint8(nextRandom & 0xFF), uint8((nextRandom >> 8) & 0xFF), + uint8((nextRandom >> 16) & 0xFF), uint8((nextRandom >> 24) & 0xFF), + value[0], value[1], value[2], value[3]}; + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x09; + replyBuf[2] = sequence; + // Copy encrypted value to response data + memcpy(&replyBuf[3], encrypted.data(), encrypted.size()); + replyBuf[11] = GenerateChecksum(replyBuf, 11); + + if (!m_isAwake) + m_isAwake = true; + } + + void DimensionsUSB::InitializeRNG(uint32 seed) + { + m_randomA = 0xF1EA5EED; + m_randomB = seed; + m_randomC = seed; + m_randomD = seed; + + for (int i = 0; i < 42; i++) + { + GetNext(); + } + } + + uint32 DimensionsUSB::GetNext() + { + uint32 e = m_randomA - std::rotl(m_randomB, 21); + m_randomA = m_randomB ^ std::rotl(m_randomC, 19); + m_randomB = m_randomC + std::rotl(m_randomD, 6); + m_randomC = m_randomD + e; + m_randomD = e + m_randomA; + return m_randomD; + } + + std::array DimensionsUSB::Decrypt(std::span buf, std::optional> key) + { + // Value to decrypt is separated in to two little endian 32 bit unsigned integers + uint32 dataOne = (uint32&)buf[0]; + uint32 dataTwo = (uint32&)buf[4]; + + // Use the key as 4 32 bit little endian unsigned integers + uint32 keyOne; + uint32 keyTwo; + uint32 keyThree; + uint32 keyFour; + + if (key) + { + keyOne = (uint32&)key.value()[0]; + keyTwo = (uint32&)key.value()[4]; + keyThree = (uint32&)key.value()[8]; + keyFour = (uint32&)key.value()[12]; + } + else + { + keyOne = (uint32&)COMMAND_KEY[0]; + keyTwo = (uint32&)COMMAND_KEY[4]; + keyThree = (uint32&)COMMAND_KEY[8]; + keyFour = (uint32&)COMMAND_KEY[12]; + } + + uint32 sum = 0xC6EF3720; + uint32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + dataTwo -= (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); + dataOne -= (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); + sum -= delta; + } + + cemu_assert(sum == 0); + + std::array decrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), + uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), + uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), + uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; + return decrypted; + } + std::array DimensionsUSB::Encrypt(std::span buf, std::optional> key) + { + // Value to encrypt is separated in to two little endian 32 bit unsigned integers + uint32 dataOne = (uint32&)buf[0]; + uint32 dataTwo = (uint32&)buf[4]; + + // Use the key as 4 32 bit little endian unsigned integers + uint32 keyOne; + uint32 keyTwo; + uint32 keyThree; + uint32 keyFour; + + if (key) + { + keyOne = (uint32&)key.value()[0]; + keyTwo = (uint32&)key.value()[4]; + keyThree = (uint32&)key.value()[8]; + keyFour = (uint32&)key.value()[12]; + } + else + { + keyOne = (uint32&)COMMAND_KEY[0]; + keyTwo = (uint32&)COMMAND_KEY[4]; + keyThree = (uint32&)COMMAND_KEY[8]; + keyFour = (uint32&)COMMAND_KEY[12]; + } + + uint32 sum = 0; + uint32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) + { + sum += delta; + dataOne += (((dataTwo << 4) + keyOne) ^ (dataTwo + sum) ^ ((dataTwo >> 5) + keyTwo)); + dataTwo += (((dataOne << 4) + keyThree) ^ (dataOne + sum) ^ ((dataOne >> 5) + keyFour)); + } + + cemu_assert(sum == 0xC6EF3720); + + std::array encrypted = {uint8(dataOne & 0xFF), uint8((dataOne >> 8) & 0xFF), + uint8((dataOne >> 16) & 0xFF), uint8((dataOne >> 24) & 0xFF), + uint8(dataTwo & 0xFF), uint8((dataTwo >> 8) & 0xFF), + uint8((dataTwo >> 16) & 0xFF), uint8((dataTwo >> 24) & 0xFF)}; + return encrypted; + } + + std::array DimensionsUSB::GenerateFigureKey(const std::array& buf) + { + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + uint32 scrambleA = Scramble(uid, 3); + uint32 scrambleB = Scramble(uid, 4); + uint32 scrambleC = Scramble(uid, 5); + uint32 scrambleD = Scramble(uid, 6); + + return {uint8((scrambleA >> 24) & 0xFF), uint8((scrambleA >> 16) & 0xFF), + uint8((scrambleA >> 8) & 0xFF), uint8(scrambleA & 0xFF), + uint8((scrambleB >> 24) & 0xFF), uint8((scrambleB >> 16) & 0xFF), + uint8((scrambleB >> 8) & 0xFF), uint8(scrambleB & 0xFF), + uint8((scrambleC >> 24) & 0xFF), uint8((scrambleC >> 16) & 0xFF), + uint8((scrambleC >> 8) & 0xFF), uint8(scrambleC & 0xFF), + uint8((scrambleD >> 24) & 0xFF), uint8((scrambleD >> 16) & 0xFF), + uint8((scrambleD >> 8) & 0xFF), uint8(scrambleD & 0xFF)}; + } + + uint32 DimensionsUSB::Scramble(const std::array& uid, uint8 count) + { + std::vector toScramble; + toScramble.reserve(uid.size() + CHAR_CONSTANT.size()); + for (uint8 x : uid) + { + toScramble.push_back(x); + } + for (uint8 c : CHAR_CONSTANT) + { + toScramble.push_back(c); + } + toScramble[(count * 4) - 1] = 0xaa; + + std::array randomized = DimensionsRandomize(toScramble, count); + + return (uint32be&)randomized[0]; + } + + std::array DimensionsUSB::PWDGenerate(const std::array& buf) + { + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + std::vector pwdCalc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; + for (uint8 i = 0; i < uid.size(); i++) + { + pwdCalc.insert(pwdCalc.begin() + i, uid[i]); + } + + return DimensionsRandomize(pwdCalc, 8); + } + + std::array DimensionsUSB::DimensionsRandomize(const std::vector key, uint8 count) + { + uint32 scrambled = 0; + for (uint8 i = 0; i < count; i++) + { + const uint32 v4 = std::rotr(scrambled, 25); + const uint32 v5 = std::rotr(scrambled, 10); + const uint32 b = (uint32&)key[i * 4]; + scrambled = b + v4 + v5 - scrambled; + } + return {uint8(scrambled & 0xFF), uint8(scrambled >> 8 & 0xFF), uint8(scrambled >> 16 & 0xFF), uint8(scrambled >> 24 & 0xFF)}; + } + + uint32 DimensionsUSB::GetFigureId(const std::array& buf) + { + const std::array figureKey = GenerateFigureKey(buf); + + const std::span modelNumber = std::span{buf.begin() + (36 * 4), 8}; + + const std::array decrypted = Decrypt(modelNumber, figureKey); + + const uint32 figNum = (uint32&)decrypted[0]; + // Characters have their model number encrypted in page 36 + if (figNum < 1000) + { + return figNum; + } + // Vehicles/Gadgets have their model number written as little endian in page 36 + return (uint32&)modelNumber[0]; + } + + DimensionsUSB::DimensionsMini& + DimensionsUSB::GetFigureByIndex(uint8 index) + { + return m_figures[index]; + } + + void DimensionsUSB::QueryBlock(uint8 index, uint8 page, + std::array& replyBuf, + uint8 sequence) + { + std::lock_guard lock(m_dimensionsMutex); + + replyBuf[0] = 0x55; + replyBuf[1] = 0x12; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + const DimensionsMini& figure = GetFigureByIndex(figureIndex); + + // Query 4 pages of 4 bytes from the figure, copy this to the response + if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) + { + std::memcpy(&replyBuf[4], figure.data.data() + (4 * page), 16); + } + } + replyBuf[20] = GenerateChecksum(replyBuf, 20); + } + + void DimensionsUSB::WriteBlock(uint8 index, uint8 page, std::span toWriteBuf, + std::array& replyBuf, uint8 sequence) + { + std::lock_guard lock(m_dimensionsMutex); + + replyBuf[0] = 0x55; + replyBuf[1] = 0x02; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + DimensionsMini& figure = GetFigureByIndex(figureIndex); + + // Copy 4 bytes to the page on the figure requested by the game + if (figure.index != 255 && page < 0x2D) + { + // Id is written to page 36 + if (page == 36) + { + figure.id = (uint32&)toWriteBuf[0]; + } + std::memcpy(figure.data.data() + (page * 4), toWriteBuf.data(), 4); + figure.Save(); + } + } + replyBuf[4] = GenerateChecksum(replyBuf, 4); + } + + void DimensionsUSB::GetModel(std::span buf, uint8 sequence, + std::array& replyBuf) + { + // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation + std::array value = Decrypt(buf, std::nullopt); + uint8 index = value[0]; + uint32 conf = (uint32be&)value[4]; + // Response is the figure's id (little endian) followed by the confirmation from payload + // Index from game begins at 1 rather than 0, so minus 1 here + std::array valueToEncrypt = {}; + if (const uint8 figureIndex = index - 1; figureIndex < 7) + { + const DimensionsMini& figure = GetFigureByIndex(figureIndex); + valueToEncrypt = {uint8(figure.id & 0xFF), uint8((figure.id >> 8) & 0xFF), + uint8((figure.id >> 16) & 0xFF), uint8((figure.id >> 24) & 0xFF), + value[4], value[5], value[6], value[7]}; + } + std::array encrypted = Encrypt(valueToEncrypt, std::nullopt); + replyBuf[0] = 0x55; + replyBuf[1] = 0x0a; + replyBuf[2] = sequence; + replyBuf[3] = 0x00; + memcpy(&replyBuf[4], encrypted.data(), encrypted.size()); + replyBuf[12] = GenerateChecksum(replyBuf, 12); + } + + void DimensionsUSB::RandomUID(std::array& uid_buffer) + { + uid_buffer[0] = 0x04; + uid_buffer[7] = 0x80; + + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0, 255); + + uid_buffer[1] = dist(mt); + uid_buffer[2] = dist(mt); + uid_buffer[4] = dist(mt); + uid_buffer[5] = dist(mt); + uid_buffer[6] = dist(mt); + } + + uint8 DimensionsUSB::GenerateChecksum(const std::array& data, + int num_of_bytes) const + { + int checksum = 0; + for (int i = 0; i < num_of_bytes; i++) + { + checksum += data[i]; + } + return (checksum & 0xFF); + } + + void DimensionsUSB::DimensionsMini::Save() + { + if (!dimFile) + return; + + dimFile->SetPosition(0); + dimFile->writeData(data.data(), data.size()); + } + + std::map DimensionsUSB::GetListMinifigs() + { + return s_listMinis; + } + + std::map DimensionsUSB::GetListTokens() + { + return s_listTokens; + } + + std::string DimensionsUSB::FindFigure(uint32 figNum) + { + for (const auto& it : GetListMinifigs()) + { + if (it.first == figNum) + { + return it.second; + } + } + for (const auto& it : GetListTokens()) + { + if (it.first == figNum) + { + return it.second; + } + } + return fmt::format("Unknown ({})", figNum); + } +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsyshid/Dimensions.h b/src/Cafe/OS/libs/nsyshid/Dimensions.h new file mode 100644 index 00000000..d5a2a529 --- /dev/null +++ b/src/Cafe/OS/libs/nsyshid/Dimensions.h @@ -0,0 +1,108 @@ +#include + +#include "nsyshid.h" +#include "Backend.h" + +#include "Common/FileStream.h" + +namespace nsyshid +{ + class DimensionsToypadDevice final : public Device + { + public: + DimensionsToypadDevice(); + ~DimensionsToypadDevice() = default; + + bool Open() override; + + void Close() override; + + bool IsOpened() override; + + ReadResult Read(ReadMessage* message) override; + + WriteResult Write(WriteMessage* message) override; + + bool GetDescriptor(uint8 descType, + uint8 descIndex, + uint8 lang, + uint8* output, + uint32 outputMaxLength) override; + + bool SetProtocol(uint8 ifIndex, uint8 protocol) override; + + bool SetReport(ReportMessage* message) override; + + private: + bool m_IsOpened; + }; + + class DimensionsUSB + { + public: + struct DimensionsMini final + { + std::unique_ptr dimFile; + std::array data{}; + uint8 index = 255; + uint8 pad = 255; + uint32 id = 0; + void Save(); + }; + + void SendCommand(std::span buf); + std::array GetStatus(); + + void GenerateRandomNumber(std::span buf, uint8 sequence, + std::array& replyBuf); + void InitializeRNG(uint32 seed); + void GetChallengeResponse(std::span buf, uint8 sequence, + std::array& replyBuf); + void QueryBlock(uint8 index, uint8 page, std::array& replyBuf, + uint8 sequence); + void WriteBlock(uint8 index, uint8 page, std::span toWriteBuf, std::array& replyBuf, + uint8 sequence); + void GetModel(std::span buf, uint8 sequence, + std::array& replyBuf); + + bool RemoveFigure(uint8 pad, uint8 index, bool fullRemove); + bool TempRemove(uint8 index); + bool CancelRemove(uint8 index); + uint32 LoadFigure(const std::array& buf, std::unique_ptr file, uint8 pad, uint8 index); + bool CreateFigure(fs::path pathName, uint32 id); + bool MoveFigure(uint8 pad, uint8 index, uint8 oldPad, uint8 oldIndex); + static std::map GetListMinifigs(); + static std::map GetListTokens(); + std::string FindFigure(uint32 figNum); + + protected: + std::mutex m_dimensionsMutex; + std::array m_figures{}; + + private: + void RandomUID(std::array& uidBuffer); + uint8 GenerateChecksum(const std::array& data, + int numOfBytes) const; + std::array Decrypt(std::span buf, std::optional> key); + std::array Encrypt(std::span buf, std::optional> key); + std::array GenerateFigureKey(const std::array& uid); + std::array PWDGenerate(const std::array& uid); + std::array DimensionsRandomize(const std::vector key, uint8 count); + uint32 GetFigureId(const std::array& buf); + uint32 Scramble(const std::array& uid, uint8 count); + uint32 GetNext(); + DimensionsMini& GetFigureByIndex(uint8 index); + + uint32 m_randomA; + uint32 m_randomB; + uint32 m_randomC; + uint32 m_randomD; + + bool m_isAwake = false; + + std::queue> m_figureAddedRemovedResponses; + std::queue> m_queries; + }; + extern DimensionsUSB g_dimensionstoypad; + +} // namespace nsyshid \ No newline at end of file diff --git a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp index 5a0ddc59..c83915db 100644 --- a/src/Cafe/OS/libs/nsysnet/nsysnet.cpp +++ b/src/Cafe/OS/libs/nsysnet/nsysnet.cpp @@ -3,6 +3,7 @@ #include "Cafe/OS/libs/coreinit/coreinit_Thread.h" #include "Cafe/IOSU/legacy/iosu_crypto.h" #include "Cafe/OS/libs/coreinit/coreinit_Time.h" +#include "Cafe/OS/libs/coreinit/coreinit_GHS.h" #include "Common/socket.h" @@ -117,20 +118,14 @@ void nsysnetExport_socket_lib_finish(PPCInterpreter_t* hCPU) osLib_returnFromFunction(hCPU, 0); // 0 -> Success } -static uint32be* __gh_errno_ptr() -{ - OSThread_t* osThread = coreinit::OSGetCurrentThread(); - return &osThread->context.ghs_errno; -} - void _setSockError(sint32 errCode) { - *(uint32be*)__gh_errno_ptr() = (uint32)errCode; + coreinit::__gh_set_errno(errCode); } sint32 _getSockError() { - return (sint32)*(uint32be*)__gh_errno_ptr(); + return coreinit::__gh_get_errno(); } // error translation modes for _translateError diff --git a/src/Cafe/OS/libs/proc_ui/proc_ui.cpp b/src/Cafe/OS/libs/proc_ui/proc_ui.cpp index dd9a460f..ff38abbb 100644 --- a/src/Cafe/OS/libs/proc_ui/proc_ui.cpp +++ b/src/Cafe/OS/libs/proc_ui/proc_ui.cpp @@ -427,7 +427,7 @@ namespace proc_ui } if(callbackType != ProcUICallbackId::AcquireForeground) priority = -priority; - AddCallbackInternal(funcPtr, userParam, priority, 0, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]); + AddCallbackInternal(funcPtr, userParam, 0, priority, s_CallbackTables[stdx::to_underlying(callbackType)][coreIndex]); } void ProcUIRegisterCallback(ProcUICallbackId callbackType, void* funcPtr, void* userParam, sint32 priority) @@ -437,7 +437,7 @@ namespace proc_ui void ProcUIRegisterBackgroundCallback(void* funcPtr, void* userParam, uint64 tickDelay) { - AddCallbackInternal(funcPtr, userParam, 0, tickDelay, s_backgroundCallbackList); + AddCallbackInternal(funcPtr, userParam, tickDelay, 0, s_backgroundCallbackList); } void FreeCallbackChain(ProcUICallbackList& callbackList) diff --git a/src/config/ActiveSettings.cpp b/src/config/ActiveSettings.cpp index 560f2986..f81f8336 100644 --- a/src/config/ActiveSettings.cpp +++ b/src/config/ActiveSettings.cpp @@ -198,14 +198,20 @@ bool ActiveSettings::ShaderPreventInfiniteLoopsEnabled() { const uint64 titleId = CafeSystem::GetForegroundTitleId(); // workaround for NSMBU (and variants) having a bug where shaders can get stuck in infinite loops - // update: As of Cemu 1.20.0 this should no longer be required + // Fatal Frame has an actual infinite loop in shader 0xb6a67c19f6472e00 encountered during a cutscene for the second drop (eShop version only?) + // update: As of Cemu 1.20.0 this should no longer be required for NSMBU/NSLU due to fixes with uniform handling. But we leave it here for good measure + // todo - Once we add support for loop config registers this workaround should become unnecessary return /* NSMBU JP */ titleId == 0x0005000010101C00 || /* NSMBU US */ titleId == 0x0005000010101D00 || /* NSMBU EU */ titleId == 0x0005000010101E00 || /* NSMBU+L US */ titleId == 0x000500001014B700 || /* NSMBU+L EU */ titleId == 0x000500001014B800 || /* NSLU US */ titleId == 0x0005000010142300 || - /* NSLU EU */ titleId == 0x0005000010142400; + /* NSLU EU */ titleId == 0x0005000010142400 || + /* Project Zero: Maiden of Black Water (EU) */ titleId == 0x00050000101D0300 || + /* Fatal Frame: Maiden of Black Water (US) */ titleId == 0x00050000101D0600 || + /* Project Zero: Maiden of Black Water (JP) */ titleId == 0x000500001014D200 || + /* Project Zero: Maiden of Black Water (Trial, EU) */ titleId == 0x00050000101D3F00; } bool ActiveSettings::FlushGPUCacheOnSwap() diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index e7920e84..26f420a5 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -38,7 +38,7 @@ void CemuConfig::Load(XMLConfigParser& parser) fullscreen_menubar = parser.get("fullscreen_menubar", false); feral_gamemode = parser.get("feral_gamemode", false); check_update = parser.get("check_update", check_update); - receive_untested_updates = parser.get("receive_untested_updates", check_update); + receive_untested_updates = parser.get("receive_untested_updates", receive_untested_updates); save_screenshot = parser.get("save_screenshot", save_screenshot); did_show_vulkan_warning = parser.get("vk_warning", did_show_vulkan_warning); did_show_graphic_pack_download = parser.get("gp_download", did_show_graphic_pack_download); @@ -346,6 +346,7 @@ void CemuConfig::Load(XMLConfigParser& parser) auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); emulated_usb_devices.emulate_infinity_base = usbdevices.get("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base); + emulated_usb_devices.emulate_dimensions_toypad = usbdevices.get("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad); } void CemuConfig::Save(XMLConfigParser& parser) @@ -545,6 +546,7 @@ void CemuConfig::Save(XMLConfigParser& parser) auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); usbdevices.set("EmulateInfinityBase", emulated_usb_devices.emulate_infinity_base.GetValue()); + usbdevices.set("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad.GetValue()); } GameEntry* CemuConfig::GetGameEntryByTitleId(uint64 titleId) diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 2f22cd76..be131266 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -521,6 +521,7 @@ struct CemuConfig { ConfigValue emulate_skylander_portal{false}; ConfigValue emulate_infinity_base{false}; + ConfigValue emulate_dimensions_toypad{false}; }emulated_usb_devices{}; private: diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 02f96a9c..e1a04ec0 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -75,6 +75,8 @@ add_library(CemuGui input/InputAPIAddWindow.h input/InputSettings2.cpp input/InputSettings2.h + input/PairingDialog.cpp + input/PairingDialog.h input/panels/ClassicControllerInputPanel.cpp input/panels/ClassicControllerInputPanel.h input/panels/InputPanel.cpp @@ -97,8 +99,6 @@ add_library(CemuGui MemorySearcherTool.h PadViewFrame.cpp PadViewFrame.h - PairingDialog.cpp - PairingDialog.h TitleManager.cpp TitleManager.h EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp diff --git a/src/gui/CemuApp.cpp b/src/gui/CemuApp.cpp index 50ff3b89..c4b1f4e4 100644 --- a/src/gui/CemuApp.cpp +++ b/src/gui/CemuApp.cpp @@ -234,6 +234,12 @@ void CemuApp::InitializeExistingMLCOrFail(fs::path mlc) g_config.Save(); } } + else + { + // default path is not writeable. Just let the user know and quit. Unsure if it would be a good idea to ask the user to choose an alternative path instead + wxMessageBox(formatWxString(_("Cemu failed to write to the default mlc directory.\nThe path is:\n{}"), wxHelper::FromPath(mlc)), _("Error"), wxOK | wxCENTRE | wxICON_ERROR); + exit(0); + } } bool CemuApp::OnInit() @@ -507,6 +513,13 @@ bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc) file.flush(); file.close(); } + // create a dummy file in the mlc folder to check if it's writable + const auto dummyFile = fs::path(mlc).append("writetestdummy"); + std::ofstream file(dummyFile); + if (!file.is_open()) + return false; + file.close(); + fs::remove(dummyFile); } catch (const std::exception& ex) { diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp index 3a0f534a..c77ae081 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp @@ -1,4 +1,4 @@ -#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h" +#include "EmulatedUSBDeviceFrame.h" #include @@ -8,14 +8,17 @@ #include "util/helpers/helpers.h" #include "Cafe/OS/libs/nsyshid/nsyshid.h" +#include "Cafe/OS/libs/nsyshid/Dimensions.h" #include "Common/FileStream.h" #include #include +#include #include #include #include +#include #include #include #include @@ -29,7 +32,6 @@ #include #include "resource/embedded/resources.h" -#include "EmulatedUSBDeviceFrame.h" EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) : wxFrame(parent, wxID_ANY, _("Emulated USB Devices"), wxDefaultPosition, @@ -44,6 +46,7 @@ EmulatedUSBDeviceFrame::EmulatedUSBDeviceFrame(wxWindow* parent) notebook->AddPage(AddSkylanderPage(notebook), _("Skylanders Portal")); notebook->AddPage(AddInfinityPage(notebook), _("Infinity Base")); + notebook->AddPage(AddDimensionsPage(notebook), _("Dimensions Toypad")); sizer->Add(notebook, 1, wxEXPAND | wxALL, 2); @@ -120,8 +123,52 @@ wxPanel* EmulatedUSBDeviceFrame::AddInfinityPage(wxNotebook* notebook) return panel; } -wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, - wxStaticBox* box) +wxPanel* EmulatedUSBDeviceFrame::AddDimensionsPage(wxNotebook* notebook) +{ + auto* panel = new wxPanel(notebook); + auto* panel_sizer = new wxBoxSizer(wxVERTICAL); + auto* box = new wxStaticBox(panel, wxID_ANY, _("Dimensions Manager")); + auto* box_sizer = new wxStaticBoxSizer(box, wxVERTICAL); + + auto* row = new wxBoxSizer(wxHORIZONTAL); + + m_emulateToypad = + new wxCheckBox(box, wxID_ANY, _("Emulate Dimensions Toypad")); + m_emulateToypad->SetValue( + GetConfig().emulated_usb_devices.emulate_dimensions_toypad); + m_emulateToypad->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + GetConfig().emulated_usb_devices.emulate_dimensions_toypad = + m_emulateToypad->IsChecked(); + g_config.Save(); + }); + row->Add(m_emulateToypad, 1, wxEXPAND | wxALL, 2); + box_sizer->Add(row, 1, wxEXPAND | wxALL, 2); + auto* top_row = new wxBoxSizer(wxHORIZONTAL); + auto* bottom_row = new wxBoxSizer(wxHORIZONTAL); + + auto* dummy = new wxStaticText(box, wxID_ANY, ""); + + top_row->Add(AddDimensionPanel(2, 0, box), 1, wxEXPAND | wxALL, 2); + top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); + top_row->Add(AddDimensionPanel(1, 1, box), 1, wxEXPAND | wxALL, 2); + top_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 2); + top_row->Add(AddDimensionPanel(3, 2, box), 1, wxEXPAND | wxALL, 2); + + bottom_row->Add(AddDimensionPanel(2, 3, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(AddDimensionPanel(2, 4, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(dummy, 1, wxEXPAND | wxLEFT | wxRIGHT, 0); + bottom_row->Add(AddDimensionPanel(3, 5, box), 1, wxEXPAND | wxALL, 2); + bottom_row->Add(AddDimensionPanel(3, 6, box), 1, wxEXPAND | wxALL, 2); + + box_sizer->Add(top_row, 1, wxEXPAND | wxALL, 2); + box_sizer->Add(bottom_row, 1, wxEXPAND | wxALL, 2); + panel_sizer->Add(box_sizer, 1, wxEXPAND | wxALL, 2); + panel->SetSizerAndFit(panel_sizer); + + return panel; +} + +wxBoxSizer* EmulatedUSBDeviceFrame::AddSkylanderRow(uint8 rowNumber, wxStaticBox* box) { auto* row = new wxBoxSizer(wxHORIZONTAL); @@ -184,6 +231,44 @@ wxBoxSizer* EmulatedUSBDeviceFrame::AddInfinityRow(wxString name, uint8 rowNumbe return row; } +wxBoxSizer* EmulatedUSBDeviceFrame::AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box) +{ + auto* panel = new wxBoxSizer(wxVERTICAL); + + auto* combo_row = new wxBoxSizer(wxHORIZONTAL); + m_dimensionSlots[index] = new wxTextCtrl(box, wxID_ANY, _("None"), wxDefaultPosition, wxDefaultSize, + wxTE_READONLY); + combo_row->Add(m_dimensionSlots[index], 1, wxEXPAND | wxALL, 2); + auto* move_button = new wxButton(box, wxID_ANY, _("Move")); + move_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + MoveMinifig(pad, index); + }); + + combo_row->Add(move_button, 1, wxEXPAND | wxALL, 2); + + auto* button_row = new wxBoxSizer(wxHORIZONTAL); + auto* load_button = new wxButton(box, wxID_ANY, _("Load")); + load_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + LoadMinifig(pad, index); + }); + auto* clear_button = new wxButton(box, wxID_ANY, _("Clear")); + clear_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + ClearMinifig(pad, index); + }); + auto* create_button = new wxButton(box, wxID_ANY, _("Create")); + create_button->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + CreateMinifig(pad, index); + }); + button_row->Add(clear_button, 1, wxEXPAND | wxALL, 2); + button_row->Add(create_button, 1, wxEXPAND | wxALL, 2); + button_row->Add(load_button, 1, wxEXPAND | wxALL, 2); + + panel->Add(combo_row, 1, wxEXPAND | wxALL, 2); + panel->Add(button_row, 1, wxEXPAND | wxALL, 2); + + return panel; +} + void EmulatedUSBDeviceFrame::LoadSkylander(uint8 slot) { wxFileDialog openFileDialog(this, _("Open Skylander dump"), "", "", @@ -307,8 +392,8 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot) return; m_filePath = saveFileDialog.GetPath(); - - if(!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) + + if (!nsyshid::g_skyportal.CreateSkylander(_utf8ToPath(m_filePath.utf8_string()), skyId, skyVar)) { wxMessageDialog errorMessage(this, "Failed to create file"); errorMessage.ShowModal(); @@ -351,6 +436,80 @@ wxString CreateSkylanderDialog::GetFilePath() const return m_filePath; } +void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() +{ + for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) + { + std::string displayString; + if (auto sd = m_skySlots[i]) + { + auto [portalSlot, skyId, skyVar] = sd.value(); + displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); + } + else + { + displayString = "None"; + } + + m_skylanderSlots[i]->ChangeValue(displayString); + } +} + +void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) +{ + wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", + "BIN files (*.bin)|*.bin", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + { + wxMessageDialog errorMessage(this, "File Okay Error"); + errorMessage.ShowModal(); + return; + } + + LoadFigurePath(slot, openFileDialog.GetPath()); +} + +void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) +{ + std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); + if (!infFile) + { + wxMessageDialog errorMessage(this, "File Open Error"); + errorMessage.ShowModal(); + return; + } + + std::array fileData; + if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) + { + wxMessageDialog open_error(this, "Failed to read file! File was too small"); + open_error.ShowModal(); + return; + } + ClearFigure(slot); + + uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); + m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); +} + +void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) +{ + cemuLog_log(LogType::Force, "Create Figure: {}", slot); + CreateInfinityFigureDialog create_dlg(this, slot); + create_dlg.ShowModal(); + if (create_dlg.GetReturnCode() == 1) + { + LoadFigurePath(slot, create_dlg.GetFilePath()); + } +} + +void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) +{ + m_infinitySlots[slot]->ChangeValue("None"); + nsyshid::g_infinitybase.RemoveFigure(slot); +} + CreateInfinityFigureDialog::CreateInfinityFigureDialog(wxWindow* parent, uint8 slot) : wxDialog(parent, wxID_ANY, _("Infinity Figure Creator"), wxDefaultPosition, wxSize(500, 150)) { @@ -447,76 +606,231 @@ wxString CreateInfinityFigureDialog::GetFilePath() const return m_filePath; } -void EmulatedUSBDeviceFrame::LoadFigure(uint8 slot) +void EmulatedUSBDeviceFrame::LoadMinifig(uint8 pad, uint8 index) { - wxFileDialog openFileDialog(this, _("Open Infinity Figure dump"), "", "", - "BIN files (*.bin)|*.bin", + wxFileDialog openFileDialog(this, _("Load Dimensions Figure"), "", "", + "Dimensions files (*.bin)|*.bin", wxFD_OPEN | wxFD_FILE_MUST_EXIST); if (openFileDialog.ShowModal() != wxID_OK || openFileDialog.GetPath().empty()) + return; + + LoadMinifigPath(openFileDialog.GetPath(), pad, index); +} + +void EmulatedUSBDeviceFrame::LoadMinifigPath(wxString path_name, uint8 pad, uint8 index) +{ + std::unique_ptr dim_file(FileStream::openFile2(_utf8ToPath(path_name.utf8_string()), true)); + if (!dim_file) { - wxMessageDialog errorMessage(this, "File Okay Error"); + wxMessageDialog errorMessage(this, "Failed to open minifig file"); errorMessage.ShowModal(); return; } - LoadFigurePath(slot, openFileDialog.GetPath()); -} + std::array file_data; -void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path) -{ - std::unique_ptr infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true)); - if (!infFile) + if (dim_file->readData(file_data.data(), file_data.size()) != file_data.size()) { - wxMessageDialog errorMessage(this, "File Open Error"); + wxMessageDialog errorMessage(this, "Failed to read minifig file data"); errorMessage.ShowModal(); return; } - std::array fileData; - if (infFile->readData(fileData.data(), fileData.size()) != fileData.size()) - { - wxMessageDialog open_error(this, "Failed to read file! File was too small"); - open_error.ShowModal(); - return; - } - ClearFigure(slot); + ClearMinifig(pad, index); - uint32 number = nsyshid::g_infinitybase.LoadFigure(fileData, std::move(infFile), slot); - m_infinitySlots[slot]->ChangeValue(nsyshid::g_infinitybase.FindFigure(number).second); + uint32 id = nsyshid::g_dimensionstoypad.LoadFigure(file_data, std::move(dim_file), pad, index); + m_dimensionSlots[index]->ChangeValue(nsyshid::g_dimensionstoypad.FindFigure(id)); + m_dimSlots[index] = id; } -void EmulatedUSBDeviceFrame::CreateFigure(uint8 slot) +void EmulatedUSBDeviceFrame::ClearMinifig(uint8 pad, uint8 index) { - cemuLog_log(LogType::Force, "Create Figure: {}", slot); - CreateInfinityFigureDialog create_dlg(this, slot); + nsyshid::g_dimensionstoypad.RemoveFigure(pad, index, true); + m_dimensionSlots[index]->ChangeValue("None"); + m_dimSlots[index] = std::nullopt; +} + +void EmulatedUSBDeviceFrame::CreateMinifig(uint8 pad, uint8 index) +{ + CreateDimensionFigureDialog create_dlg(this); create_dlg.ShowModal(); if (create_dlg.GetReturnCode() == 1) { - LoadFigurePath(slot, create_dlg.GetFilePath()); + LoadMinifigPath(create_dlg.GetFilePath(), pad, index); } } -void EmulatedUSBDeviceFrame::ClearFigure(uint8 slot) +void EmulatedUSBDeviceFrame::MoveMinifig(uint8 pad, uint8 index) { - m_infinitySlots[slot]->ChangeValue("None"); - nsyshid::g_infinitybase.RemoveFigure(slot); -} + if (!m_dimSlots[index]) + return; -void EmulatedUSBDeviceFrame::UpdateSkylanderEdits() -{ - for (auto i = 0; i < nsyshid::MAX_SKYLANDERS; i++) + MoveDimensionFigureDialog move_dlg(this, index); + nsyshid::g_dimensionstoypad.TempRemove(index); + move_dlg.ShowModal(); + if (move_dlg.GetReturnCode() == 1) { - std::string displayString; - if (auto sd = m_skySlots[i]) + nsyshid::g_dimensionstoypad.MoveFigure(move_dlg.GetNewPad(), move_dlg.GetNewIndex(), pad, index); + if (index != move_dlg.GetNewIndex()) { - auto [portalSlot, skyId, skyVar] = sd.value(); - displayString = nsyshid::g_skyportal.FindSkylander(skyId, skyVar); + m_dimSlots[move_dlg.GetNewIndex()] = m_dimSlots[index]; + m_dimensionSlots[move_dlg.GetNewIndex()]->ChangeValue(m_dimensionSlots[index]->GetValue()); + m_dimSlots[index] = std::nullopt; + m_dimensionSlots[index]->ChangeValue("None"); } - else - { - displayString = "None"; - } - - m_skylanderSlots[i]->ChangeValue(displayString); } + else + { + nsyshid::g_dimensionstoypad.CancelRemove(index); + } +} + +CreateDimensionFigureDialog::CreateDimensionFigureDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Dimensions Figure Creator"), wxDefaultPosition, wxSize(500, 200)) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + + auto* comboRow = new wxBoxSizer(wxHORIZONTAL); + + auto* comboBox = new wxComboBox(this, wxID_ANY); + comboBox->Append("---Select---", reinterpret_cast(0xFFFFFFFF)); + wxArrayString filterlist; + for (const auto& it : nsyshid::g_dimensionstoypad.GetListMinifigs()) + { + const uint32 figure = it.first; + comboBox->Append(it.second, reinterpret_cast(figure)); + filterlist.Add(it.second); + } + comboBox->SetSelection(0); + bool enabled = comboBox->AutoComplete(filterlist); + comboRow->Add(comboBox, 1, wxEXPAND | wxALL, 2); + + auto* figNumRow = new wxBoxSizer(wxHORIZONTAL); + + wxIntegerValidator validator; + + auto* labelFigNum = new wxStaticText(this, wxID_ANY, "Figure Number:"); + auto* editFigNum = new wxTextCtrl(this, wxID_ANY, _("0"), wxDefaultPosition, wxDefaultSize, 0, validator); + + figNumRow->Add(labelFigNum, 1, wxALL, 5); + figNumRow->Add(editFigNum, 1, wxALL, 5); + + auto* buttonRow = new wxBoxSizer(wxHORIZONTAL); + + auto* createButton = new wxButton(this, wxID_ANY, _("Create")); + createButton->Bind(wxEVT_BUTTON, [editFigNum, this](wxCommandEvent&) { + long longFigNum; + if (!editFigNum->GetValue().ToLong(&longFigNum) || longFigNum > 0xFFFF) + { + wxMessageDialog idError(this, "Error Converting Figure Number!", "Number Entered is Invalid"); + idError.ShowModal(); + this->EndModal(0); + } + uint16 figNum = longFigNum & 0xFFFF; + auto figure = nsyshid::g_dimensionstoypad.FindFigure(figNum); + wxString predefName = figure + ".bin"; + wxFileDialog + saveFileDialog(this, _("Create Dimensions Figure file"), "", predefName, + "BIN files (*.bin)|*.bin", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + + if (saveFileDialog.ShowModal() == wxID_CANCEL) + this->EndModal(0); + + m_filePath = saveFileDialog.GetPath(); + + nsyshid::g_dimensionstoypad.CreateFigure(_utf8ToPath(m_filePath.utf8_string()), figNum); + + this->EndModal(1); + }); + auto* cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); + cancelButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + this->EndModal(0); + }); + + comboBox->Bind(wxEVT_COMBOBOX, [comboBox, editFigNum, this](wxCommandEvent&) { + const uint64 fig_info = reinterpret_cast(comboBox->GetClientData(comboBox->GetSelection())); + if (fig_info != 0xFFFF) + { + const uint16 figNum = fig_info & 0xFFFF; + + editFigNum->SetValue(wxString::Format(wxT("%i"), figNum)); + } + }); + + buttonRow->Add(createButton, 1, wxALL, 5); + buttonRow->Add(cancelButton, 1, wxALL, 5); + + sizer->Add(comboRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(figNumRow, 1, wxEXPAND | wxALL, 2); + sizer->Add(buttonRow, 1, wxEXPAND | wxALL, 2); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxString CreateDimensionFigureDialog::GetFilePath() const +{ + return m_filePath; +} + +MoveDimensionFigureDialog::MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex) + : wxDialog(parent, wxID_ANY, _("Dimensions Figure Mover"), wxDefaultPosition, wxSize(700, 300)) +{ + auto* sizer = new wxGridSizer(2, 5, 10, 10); + + std::array, 7> ids = parent->GetCurrentMinifigs(); + + sizer->Add(AddMinifigSlot(2, 0, currentIndex, ids[0]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(1, 1, currentIndex, ids[1]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 2, currentIndex, ids[2]), 1, wxALL, 5); + + sizer->Add(AddMinifigSlot(2, 3, currentIndex, ids[3]), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(2, 4, currentIndex, ids[4]), 1, wxALL, 5); + sizer->Add(new wxStaticText(this, wxID_ANY, ""), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 5, currentIndex, ids[5]), 1, wxALL, 5); + sizer->Add(AddMinifigSlot(3, 6, currentIndex, ids[6]), 1, wxALL, 5); + + this->SetSizer(sizer); + this->Centre(wxBOTH); +} + +wxBoxSizer* MoveDimensionFigureDialog::AddMinifigSlot(uint8 pad, uint8 index, uint8 currentIndex, std::optional currentId) +{ + auto* panel = new wxBoxSizer(wxVERTICAL); + + auto* label = new wxStaticText(this, wxID_ANY, "None"); + if (currentId) + label->SetLabel(nsyshid::g_dimensionstoypad.FindFigure(currentId.value())); + + auto* moveButton = new wxButton(this, wxID_ANY, _("Move Here")); + if (index == currentIndex) + moveButton->SetLabelText("Pick up and Place"); + + moveButton->Bind(wxEVT_BUTTON, [pad, index, this](wxCommandEvent&) { + m_newPad = pad; + m_newIndex = index; + this->EndModal(1); + }); + + panel->Add(label, 1, wxALL, 5); + panel->Add(moveButton, 1, wxALL, 5); + + return panel; +} + +uint8 MoveDimensionFigureDialog::GetNewPad() const +{ + return m_newPad; +} + +uint8 MoveDimensionFigureDialog::GetNewIndex() const +{ + return m_newIndex; +} + +std::array, 7> EmulatedUSBDeviceFrame::GetCurrentMinifigs() +{ + return m_dimSlots; } \ No newline at end of file diff --git a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h index ae29a036..78c70a4a 100644 --- a/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h +++ b/src/gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h @@ -17,33 +17,47 @@ class wxStaticBox; class wxString; class wxTextCtrl; -class EmulatedUSBDeviceFrame : public wxFrame { +class EmulatedUSBDeviceFrame : public wxFrame +{ public: EmulatedUSBDeviceFrame(wxWindow* parent); ~EmulatedUSBDeviceFrame(); + std::array, 7> GetCurrentMinifigs(); private: wxCheckBox* m_emulatePortal; wxCheckBox* m_emulateBase; + wxCheckBox* m_emulateToypad; std::array m_skylanderSlots; std::array m_infinitySlots; + std::array m_dimensionSlots; std::array>, nsyshid::MAX_SKYLANDERS> m_skySlots; + std::array, 7> m_dimSlots; wxPanel* AddSkylanderPage(wxNotebook* notebook); wxPanel* AddInfinityPage(wxNotebook* notebook); + wxPanel* AddDimensionsPage(wxNotebook* notebook); wxBoxSizer* AddSkylanderRow(uint8 row_number, wxStaticBox* box); wxBoxSizer* AddInfinityRow(wxString name, uint8 row_number, wxStaticBox* box); + wxBoxSizer* AddDimensionPanel(uint8 pad, uint8 index, wxStaticBox* box); void LoadSkylander(uint8 slot); void LoadSkylanderPath(uint8 slot, wxString path); void CreateSkylander(uint8 slot); void ClearSkylander(uint8 slot); + void UpdateSkylanderEdits(); void LoadFigure(uint8 slot); void LoadFigurePath(uint8 slot, wxString path); void CreateFigure(uint8 slot); void ClearFigure(uint8 slot); - void UpdateSkylanderEdits(); + void LoadMinifig(uint8 pad, uint8 index); + void LoadMinifigPath(wxString path_name, uint8 pad, uint8 index); + void CreateMinifig(uint8 pad, uint8 index); + void ClearMinifig(uint8 pad, uint8 index); + void MoveMinifig(uint8 pad, uint8 index); }; -class CreateSkylanderDialog : public wxDialog { + +class CreateSkylanderDialog : public wxDialog +{ public: explicit CreateSkylanderDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; @@ -52,11 +66,37 @@ class CreateSkylanderDialog : public wxDialog { wxString m_filePath; }; -class CreateInfinityFigureDialog : public wxDialog { +class CreateInfinityFigureDialog : public wxDialog +{ public: explicit CreateInfinityFigureDialog(wxWindow* parent, uint8 slot); wxString GetFilePath() const; protected: wxString m_filePath; +}; + +class CreateDimensionFigureDialog : public wxDialog +{ + public: + explicit CreateDimensionFigureDialog(wxWindow* parent); + wxString GetFilePath() const; + + protected: + wxString m_filePath; +}; + +class MoveDimensionFigureDialog : public wxDialog +{ + public: + explicit MoveDimensionFigureDialog(EmulatedUSBDeviceFrame* parent, uint8 currentIndex); + uint8 GetNewPad() const; + uint8 GetNewIndex() const; + + protected: + uint8 m_newIndex = 0; + uint8 m_newPad = 0; + + private: + wxBoxSizer* AddMinifigSlot(uint8 pad, uint8 index, uint8 oldIndex, std::optional currentId); }; \ No newline at end of file diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c83ab16b..69ff4e99 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -483,20 +483,20 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); - if (r == CafeSystem::STATUS_CODE::INVALID_RPX) + CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId); + if (r == CafeSystem::PREPARE_STATUS_CODE::INVALID_RPX) { cemu_assert_debug(false); return false; } - else if (r == CafeSystem::STATUS_CODE::UNABLE_TO_MOUNT) + else if (r == CafeSystem::PREPARE_STATUS_CODE::UNABLE_TO_MOUNT) { wxString t = _("Unable to mount title.\nMake sure the configured game paths are still valid and refresh the game list.\n\nFile which failed to load:\n"); t.append(_pathToUtf8(launchPath)); wxMessageBox(t, _("Error"), wxOK | wxCENTRE | wxICON_ERROR); return false; } - else if (r != CafeSystem::STATUS_CODE::SUCCESS) + else if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { wxString t = _("Failed to launch game."); t.append(_pathToUtf8(launchPath)); @@ -511,8 +511,8 @@ bool MainWindow::FileLoad(const fs::path launchPath, wxLaunchGameEvent::INITIATE CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath); if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF) { - CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); - if (r != CafeSystem::STATUS_CODE::SUCCESS) + CafeSystem::PREPARE_STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath); + if (r != CafeSystem::PREPARE_STATUS_CODE::SUCCESS) { cemu_assert_debug(false); // todo wxString t = _("Failed to launch executable. Path: "); diff --git a/src/gui/PairingDialog.cpp b/src/gui/PairingDialog.cpp deleted file mode 100644 index f90e6d13..00000000 --- a/src/gui/PairingDialog.cpp +++ /dev/null @@ -1,236 +0,0 @@ -#include "gui/wxgui.h" -#include "gui/PairingDialog.h" - -#if BOOST_OS_WINDOWS -#include -#endif - -wxDECLARE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); -wxDEFINE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); - -PairingDialog::PairingDialog(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Pairing..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) -{ - auto* sizer = new wxBoxSizer(wxVERTICAL); - m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(350, 20), wxGA_HORIZONTAL); - m_gauge->SetValue(0); - sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); - - auto* rows = new wxFlexGridSizer(0, 2, 0, 0); - rows->AddGrowableCol(1); - - m_text = new wxStaticText(this, wxID_ANY, _("Searching for controllers...")); - rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); - - { - auto* right_side = new wxBoxSizer(wxHORIZONTAL); - - m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); - m_cancelButton->Bind(wxEVT_BUTTON, &PairingDialog::OnCancelButton, this); - right_side->Add(m_cancelButton, 0, wxALL, 5); - - rows->Add(right_side, 1, wxALIGN_RIGHT, 5); - } - - sizer->Add(rows, 0, wxALL | wxEXPAND, 5); - - SetSizerAndFit(sizer); - Centre(wxBOTH); - - Bind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); - Bind(wxEVT_PROGRESS_PAIR, &PairingDialog::OnGaugeUpdate, this); - - m_thread = std::thread(&PairingDialog::WorkerThread, this); -} - -PairingDialog::~PairingDialog() -{ - Unbind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); -} - -void PairingDialog::OnClose(wxCloseEvent& event) -{ - event.Skip(); - - m_threadShouldQuit = true; - if (m_thread.joinable()) - m_thread.join(); -} - -void PairingDialog::OnCancelButton(const wxCommandEvent& event) -{ - Close(); -} - -void PairingDialog::OnGaugeUpdate(wxCommandEvent& event) -{ - PairingState state = (PairingState)event.GetInt(); - - switch (state) - { - case PairingState::Pairing: - { - m_text->SetLabel(_("Found controller. Pairing...")); - m_gauge->SetValue(50); - break; - } - - case PairingState::Finished: - { - m_text->SetLabel(_("Successfully paired the controller.")); - m_gauge->SetValue(100); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::NoBluetoothAvailable: - { - m_text->SetLabel(_("Failed to find a suitable Bluetooth radio.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::BluetoothFailed: - { - m_text->SetLabel(_("Failed to search for controllers.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::PairingFailed: - { - m_text->SetLabel(_("Failed to pair with the found controller.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - case PairingState::BluetoothUnusable: - { - m_text->SetLabel(_("Please use your system's Bluetooth manager instead.")); - m_gauge->SetValue(0); - m_cancelButton->SetLabel(_("Close")); - break; - } - - - default: - { - break; - } - } -} - -void PairingDialog::WorkerThread() -{ - const std::wstring wiimoteName = L"Nintendo RVL-CNT-01"; - const std::wstring wiiUProControllerName = L"Nintendo RVL-CNT-01-UC"; - -#if BOOST_OS_WINDOWS - const GUID bthHidGuid = {0x00001124,0x0000,0x1000,{0x80,0x00,0x00,0x80,0x5F,0x9B,0x34,0xFB}}; - - const BLUETOOTH_FIND_RADIO_PARAMS radioFindParams = - { - .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS) - }; - - HANDLE radio = INVALID_HANDLE_VALUE; - HBLUETOOTH_RADIO_FIND radioFind = BluetoothFindFirstRadio(&radioFindParams, &radio); - if (radioFind == nullptr) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } - - BluetoothFindRadioClose(radioFind); - - BLUETOOTH_RADIO_INFO radioInfo = - { - .dwSize = sizeof(BLUETOOTH_RADIO_INFO) - }; - - DWORD result = BluetoothGetRadioInfo(radio, &radioInfo); - if (result != ERROR_SUCCESS) - { - UpdateCallback(PairingState::NoBluetoothAvailable); - return; - } - - const BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS), - - .fReturnAuthenticated = FALSE, - .fReturnRemembered = FALSE, - .fReturnUnknown = TRUE, - .fReturnConnected = FALSE, - - .fIssueInquiry = TRUE, - .cTimeoutMultiplier = 5, - - .hRadio = radio - }; - - BLUETOOTH_DEVICE_INFO info = - { - .dwSize = sizeof(BLUETOOTH_DEVICE_INFO) - }; - - while (!m_threadShouldQuit) - { - HBLUETOOTH_DEVICE_FIND deviceFind = BluetoothFindFirstDevice(&searchParams, &info); - if (deviceFind == nullptr) - { - UpdateCallback(PairingState::BluetoothFailed); - return; - } - - while (!m_threadShouldQuit) - { - if (info.szName == wiimoteName || info.szName == wiiUProControllerName) - { - BluetoothFindDeviceClose(deviceFind); - - UpdateCallback(PairingState::Pairing); - - wchar_t passwd[6] = { radioInfo.address.rgBytes[0], radioInfo.address.rgBytes[1], radioInfo.address.rgBytes[2], radioInfo.address.rgBytes[3], radioInfo.address.rgBytes[4], radioInfo.address.rgBytes[5] }; - DWORD bthResult = BluetoothAuthenticateDevice(nullptr, radio, &info, passwd, 6); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } - - bthResult = BluetoothSetServiceState(radio, &info, &bthHidGuid, BLUETOOTH_SERVICE_ENABLE); - if (bthResult != ERROR_SUCCESS) - { - UpdateCallback(PairingState::PairingFailed); - return; - } - - UpdateCallback(PairingState::Finished); - return; - } - - BOOL nextDevResult = BluetoothFindNextDevice(deviceFind, &info); - if (nextDevResult == FALSE) - { - break; - } - } - - BluetoothFindDeviceClose(deviceFind); - } -#else - UpdateCallback(PairingState::BluetoothUnusable); -#endif -} - -void PairingDialog::UpdateCallback(PairingState state) -{ - auto* event = new wxCommandEvent(wxEVT_PROGRESS_PAIR); - event->SetInt((int)state); - wxQueueEvent(this, event); -} \ No newline at end of file diff --git a/src/gui/TitleManager.cpp b/src/gui/TitleManager.cpp index 00e7992f..4a4f7f56 100644 --- a/src/gui/TitleManager.cpp +++ b/src/gui/TitleManager.cpp @@ -632,7 +632,7 @@ void TitleManager::OnSaveExport(wxCommandEvent& event) const auto persistent_id = (uint32)(uintptr_t)m_save_account_list->GetClientData(selection_index); - wxFileDialog path_dialog(this, _("Select a target file to export the save entry"), entry->path.string(), wxEmptyString, + wxFileDialog path_dialog(this, _("Select a target file to export the save entry"), wxHelper::FromPath(entry->path), wxEmptyString, fmt::format("{}|*.zip", _("Exported save entry (*.zip)")), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().IsEmpty()) return; diff --git a/src/gui/debugger/DebuggerWindow2.cpp b/src/gui/debugger/DebuggerWindow2.cpp index 969e40bd..9f25cf96 100644 --- a/src/gui/debugger/DebuggerWindow2.cpp +++ b/src/gui/debugger/DebuggerWindow2.cpp @@ -64,6 +64,7 @@ wxBEGIN_EVENT_TABLE(DebuggerWindow2, wxFrame) EVT_COMMAND(wxID_ANY, wxEVT_RUN, DebuggerWindow2::OnRunProgram) EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_LOADED, DebuggerWindow2::OnNotifyModuleLoaded) EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded) + EVT_COMMAND(wxID_ANY, wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, DebuggerWindow2::OnDisasmCtrlGotoAddress) // file menu EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit) // window @@ -383,6 +384,12 @@ void DebuggerWindow2::OnMoveIP(wxCommandEvent& event) m_disasm_ctrl->CenterOffset(ip); } +void DebuggerWindow2::OnDisasmCtrlGotoAddress(wxCommandEvent& event) +{ + uint32 address = static_cast(event.GetExtraLong()); + UpdateModuleLabel(address); +} + void DebuggerWindow2::OnParentMove(const wxPoint& main_position, const wxSize& main_size) { m_main_position = main_position; @@ -416,7 +423,7 @@ void DebuggerWindow2::OnNotifyModuleLoaded(wxCommandEvent& event) void DebuggerWindow2::OnNotifyModuleUnloaded(wxCommandEvent& event) { - RPLModule* module = (RPLModule*)event.GetClientData(); + RPLModule* module = (RPLModule*)event.GetClientData(); // todo - the RPL module is already unloaded at this point. Find a better way to handle this SaveModuleStorage(module, true); m_module_window->OnGameLoaded(); m_symbol_window->OnGameLoaded(); @@ -659,7 +666,7 @@ void DebuggerWindow2::CreateMenuBar() void DebuggerWindow2::UpdateModuleLabel(uint32 address) { - if(address == 0) + if (address == 0) address = m_disasm_ctrl->GetViewBaseAddress(); RPLModule* module = RPLLoader_FindModuleByCodeAddr(address); diff --git a/src/gui/debugger/DebuggerWindow2.h b/src/gui/debugger/DebuggerWindow2.h index 0ca44c44..145b5e1d 100644 --- a/src/gui/debugger/DebuggerWindow2.h +++ b/src/gui/debugger/DebuggerWindow2.h @@ -86,6 +86,8 @@ private: void OnMoveIP(wxCommandEvent& event); void OnNotifyModuleLoaded(wxCommandEvent& event); void OnNotifyModuleUnloaded(wxCommandEvent& event); + // events from DisasmCtrl + void OnDisasmCtrlGotoAddress(wxCommandEvent& event); void CreateMenuBar(); void UpdateModuleLabel(uint32 address = 0); diff --git a/src/gui/debugger/DisasmCtrl.cpp b/src/gui/debugger/DisasmCtrl.cpp index c2cd5722..2f38d55e 100644 --- a/src/gui/debugger/DisasmCtrl.cpp +++ b/src/gui/debugger/DisasmCtrl.cpp @@ -15,6 +15,8 @@ #include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h" #include // for wxMemoryInputStream +wxDEFINE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); + #define MAX_SYMBOL_LEN (120) #define COLOR_DEBUG_ACTIVE_BP 0xFFFFA0FF @@ -74,6 +76,8 @@ DisasmCtrl::DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& po auto tooltip_sizer = new wxBoxSizer(wxVERTICAL); tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString), 0, wxALL, 5); m_tooltip_window->SetSizer(tooltip_sizer); + + Bind(wxEVT_MENU, &DisasmCtrl::OnContextMenuEntryClicked, this, IDContextMenu_ToggleBreakpoint, IDContextMenu_Last); } void DisasmCtrl::Init() @@ -662,29 +666,67 @@ void DisasmCtrl::CopyToClipboard(std::string text) { #endif } +static uint32 GetUnrelocatedAddress(MPTR address) +{ + RPLModule* rplModule = RPLLoader_FindModuleByCodeAddr(address); + if (!rplModule) + return 0; + if (address >= rplModule->regionMappingBase_text.GetMPTR() && address < (rplModule->regionMappingBase_text.GetMPTR() + rplModule->regionSize_text)) + return 0x02000000 + (address - rplModule->regionMappingBase_text.GetMPTR()); + return 0; +} + void DisasmCtrl::OnContextMenu(const wxPoint& position, uint32 line) { - wxPoint pos = position; auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height); if (!optVirtualAddress) return; MPTR virtualAddress = *optVirtualAddress; + m_contextMenuAddress = virtualAddress; + // show dialog + wxMenu menu; + menu.Append(IDContextMenu_ToggleBreakpoint, _("Toggle breakpoint")); + if(debugger_hasPatch(virtualAddress)) + menu.Append(IDContextMenu_RestoreOriginalInstructions, _("Restore original instructions")); + menu.AppendSeparator(); + menu.Append(IDContextMenu_CopyAddress, _("Copy address")); + uint32 unrelocatedAddress = GetUnrelocatedAddress(virtualAddress); + if (unrelocatedAddress && unrelocatedAddress != virtualAddress) + menu.Append(IDContextMenu_CopyUnrelocatedAddress, _("Copy virtual address (for IDA/Ghidra)")); + PopupMenu(&menu); +} - // address - if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE) +void DisasmCtrl::OnContextMenuEntryClicked(wxCommandEvent& event) +{ + switch(event.GetId()) { - CopyToClipboard(fmt::format("{:#10x}", virtualAddress)); - return; - } - else if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + OFFSET_DISASSEMBLY) - { - // double-clicked on disassembly (operation and operand data) - return; - } - else - { - // comment - return; + case IDContextMenu_ToggleBreakpoint: + { + debugger_toggleExecuteBreakpoint(m_contextMenuAddress); + wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); + wxPostEvent(this->m_parent, evt); + break; + } + case IDContextMenu_RestoreOriginalInstructions: + { + debugger_removePatch(m_contextMenuAddress); + wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); // This also refreshes the disassembly view + wxPostEvent(this->m_parent, evt); + break; + } + case IDContextMenu_CopyAddress: + { + CopyToClipboard(fmt::format("{:#10x}", m_contextMenuAddress)); + break; + } + case IDContextMenu_CopyUnrelocatedAddress: + { + uint32 unrelocatedAddress = GetUnrelocatedAddress(m_contextMenuAddress); + CopyToClipboard(fmt::format("{:#10x}", unrelocatedAddress)); + break; + } + default: + UNREACHABLE; } } @@ -722,7 +764,6 @@ std::optional DisasmCtrl::LinePixelPosToAddress(sint32 posY) if (posY < 0) return std::nullopt; - sint32 lineIndex = posY / m_line_height; if (lineIndex >= m_lineToAddress.size()) return std::nullopt; @@ -751,8 +792,6 @@ void DisasmCtrl::CenterOffset(uint32 offset) m_active_line = line; RefreshLine(m_active_line); - - debug_printf("scroll to %x\n", debuggerState.debugSession.instructionPointer); } void DisasmCtrl::GoToAddressDialog() @@ -765,6 +804,10 @@ void DisasmCtrl::GoToAddressDialog() auto value = goto_dialog.GetValue().ToStdString(); std::transform(value.begin(), value.end(), value.begin(), tolower); + // trim any leading spaces + while(!value.empty() && value[0] == ' ') + value.erase(value.begin()); + debugger_addParserSymbols(parser); // try to parse expression as hex value first (it should interpret 1234 as 0x1234, not 1234) @@ -773,17 +816,24 @@ void DisasmCtrl::GoToAddressDialog() const auto result = (uint32)parser.Evaluate("0x"+value); m_lastGotoTarget = result; CenterOffset(result); + wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); + evt.SetExtraLong(static_cast(result)); + wxPostEvent(GetParent(), evt); } else if (parser.IsConstantExpression(value)) { const auto result = (uint32)parser.Evaluate(value); m_lastGotoTarget = result; CenterOffset(result); + wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS); + evt.SetExtraLong(static_cast(result)); + wxPostEvent(GetParent(), evt); } else { try { + // if not a constant expression (i.e. relying on unknown variables), then evaluating will throw an exception with a detailed error message const auto _ = (uint32)parser.Evaluate(value); } catch (const std::exception& ex) diff --git a/src/gui/debugger/DisasmCtrl.h b/src/gui/debugger/DisasmCtrl.h index 993d5697..5a67e49a 100644 --- a/src/gui/debugger/DisasmCtrl.h +++ b/src/gui/debugger/DisasmCtrl.h @@ -1,9 +1,20 @@ #pragma once #include "gui/components/TextList.h" +wxDECLARE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent); // Notify parent that goto address operation completed. Event contains the address that was jumped to. + class DisasmCtrl : public TextList { + enum + { + IDContextMenu_ToggleBreakpoint = wxID_HIGHEST + 1, + IDContextMenu_RestoreOriginalInstructions, + IDContextMenu_CopyAddress, + IDContextMenu_CopyUnrelocatedAddress, + IDContextMenu_Last + }; public: + DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style); void Init(); @@ -26,6 +37,7 @@ protected: void OnKeyPressed(sint32 key_code, const wxPoint& position) override; void OnMouseDClick(const wxPoint& position, uint32 line) override; void OnContextMenu(const wxPoint& position, uint32 line) override; + void OnContextMenuEntryClicked(wxCommandEvent& event); bool OnShowTooltip(const wxPoint& position, uint32 line) override; void ScrollWindow(int dx, int dy, const wxRect* prect) override; @@ -40,6 +52,7 @@ private: sint32 m_mouse_line, m_mouse_line_drawn; sint32 m_active_line; uint32 m_lastGotoTarget{}; + uint32 m_contextMenuAddress{}; // code region info uint32 currentCodeRegionStart; uint32 currentCodeRegionEnd; diff --git a/src/gui/input/InputSettings2.cpp b/src/gui/input/InputSettings2.cpp index 72bf4f7d..2ae8a74b 100644 --- a/src/gui/input/InputSettings2.cpp +++ b/src/gui/input/InputSettings2.cpp @@ -20,6 +20,8 @@ #include "gui/input/InputAPIAddWindow.h" #include "input/ControllerFactory.h" +#include "gui/input/PairingDialog.h" + #include "gui/input/panels/VPADInputPanel.h" #include "gui/input/panels/ProControllerInputPanel.h" @@ -252,6 +254,13 @@ wxWindow* InputSettings2::initialize_page(size_t index) page_data.m_controller_api_remove = remove_api; } + auto* pairingDialog = new wxButton(page, wxID_ANY, _("Pair Wii/Wii U Controller")); + pairingDialog->Bind(wxEVT_BUTTON, [this](wxEvent&) { + PairingDialog pairing_dialog(this); + pairing_dialog.ShowModal(); + }); + sizer->Add(pairingDialog, wxGBPosition(5, 0), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxALL, 5); + // controller auto* controller_bttns = new wxBoxSizer(wxHORIZONTAL); auto* settings = new wxButton(page, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0); diff --git a/src/gui/input/PairingDialog.cpp b/src/gui/input/PairingDialog.cpp new file mode 100644 index 00000000..350fce81 --- /dev/null +++ b/src/gui/input/PairingDialog.cpp @@ -0,0 +1,300 @@ +#include "gui/wxgui.h" +#include "PairingDialog.h" + +#if BOOST_OS_WINDOWS +#include +#endif +#if BOOST_OS_LINUX +#include +#include +#include +#include +#endif + +wxDECLARE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); +wxDEFINE_EVENT(wxEVT_PROGRESS_PAIR, wxCommandEvent); + +PairingDialog::PairingDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Pairing..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxMINIMIZE_BOX | wxSYSTEM_MENU | wxTAB_TRAVERSAL | wxCLOSE_BOX) +{ + auto* sizer = new wxBoxSizer(wxVERTICAL); + m_gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxSize(350, 20), wxGA_HORIZONTAL); + m_gauge->SetValue(0); + sizer->Add(m_gauge, 0, wxALL | wxEXPAND, 5); + + auto* rows = new wxFlexGridSizer(0, 2, 0, 0); + rows->AddGrowableCol(1); + + m_text = new wxStaticText(this, wxID_ANY, _("Searching for controllers...")); + rows->Add(m_text, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); + + { + auto* right_side = new wxBoxSizer(wxHORIZONTAL); + + m_cancelButton = new wxButton(this, wxID_ANY, _("Cancel")); + m_cancelButton->Bind(wxEVT_BUTTON, &PairingDialog::OnCancelButton, this); + right_side->Add(m_cancelButton, 0, wxALL, 5); + + rows->Add(right_side, 1, wxALIGN_RIGHT, 5); + } + + sizer->Add(rows, 0, wxALL | wxEXPAND, 5); + + SetSizerAndFit(sizer); + Centre(wxBOTH); + + Bind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); + Bind(wxEVT_PROGRESS_PAIR, &PairingDialog::OnGaugeUpdate, this); + + m_thread = std::thread(&PairingDialog::WorkerThread, this); +} + +PairingDialog::~PairingDialog() +{ + Unbind(wxEVT_CLOSE_WINDOW, &PairingDialog::OnClose, this); +} + +void PairingDialog::OnClose(wxCloseEvent& event) +{ + event.Skip(); + + m_threadShouldQuit = true; + if (m_thread.joinable()) + m_thread.join(); +} + +void PairingDialog::OnCancelButton(const wxCommandEvent& event) +{ + Close(); +} + +void PairingDialog::OnGaugeUpdate(wxCommandEvent& event) +{ + PairingState state = (PairingState)event.GetInt(); + + switch (state) + { + case PairingState::Pairing: + { + m_text->SetLabel(_("Found controller. Pairing...")); + m_gauge->SetValue(50); + break; + } + + case PairingState::Finished: + { + m_text->SetLabel(_("Successfully paired the controller.")); + m_gauge->SetValue(100); + m_cancelButton->SetLabel(_("Close")); + break; + } + + case PairingState::NoBluetoothAvailable: + { + m_text->SetLabel(_("Failed to find a suitable Bluetooth radio.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } + + case PairingState::SearchFailed: + { + m_text->SetLabel(_("Failed to find controllers.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } + + case PairingState::PairingFailed: + { + m_text->SetLabel(_("Failed to pair with the found controller.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } + + case PairingState::BluetoothUnusable: + { + m_text->SetLabel(_("Please use your system's Bluetooth manager instead.")); + m_gauge->SetValue(0); + m_cancelButton->SetLabel(_("Close")); + break; + } + + default: + { + break; + } + } +} + +#if BOOST_OS_WINDOWS +void PairingDialog::WorkerThread() +{ + const std::wstring wiimoteName = L"Nintendo RVL-CNT-01"; + const std::wstring wiiUProControllerName = L"Nintendo RVL-CNT-01-UC"; + + const GUID bthHidGuid = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}}; + + const BLUETOOTH_FIND_RADIO_PARAMS radioFindParams = + { + .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS)}; + + HANDLE radio = INVALID_HANDLE_VALUE; + HBLUETOOTH_RADIO_FIND radioFind = BluetoothFindFirstRadio(&radioFindParams, &radio); + if (radioFind == nullptr) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } + + BluetoothFindRadioClose(radioFind); + + BLUETOOTH_RADIO_INFO radioInfo = + { + .dwSize = sizeof(BLUETOOTH_RADIO_INFO)}; + + DWORD result = BluetoothGetRadioInfo(radio, &radioInfo); + if (result != ERROR_SUCCESS) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } + + const BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = + { + .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS), + + .fReturnAuthenticated = FALSE, + .fReturnRemembered = FALSE, + .fReturnUnknown = TRUE, + .fReturnConnected = FALSE, + + .fIssueInquiry = TRUE, + .cTimeoutMultiplier = 5, + + .hRadio = radio}; + + BLUETOOTH_DEVICE_INFO info = + { + .dwSize = sizeof(BLUETOOTH_DEVICE_INFO)}; + + while (!m_threadShouldQuit) + { + HBLUETOOTH_DEVICE_FIND deviceFind = BluetoothFindFirstDevice(&searchParams, &info); + if (deviceFind == nullptr) + { + UpdateCallback(PairingState::SearchFailed); + return; + } + + while (!m_threadShouldQuit) + { + if (info.szName == wiimoteName || info.szName == wiiUProControllerName) + { + BluetoothFindDeviceClose(deviceFind); + + UpdateCallback(PairingState::Pairing); + + wchar_t passwd[6] = {radioInfo.address.rgBytes[0], radioInfo.address.rgBytes[1], radioInfo.address.rgBytes[2], radioInfo.address.rgBytes[3], radioInfo.address.rgBytes[4], radioInfo.address.rgBytes[5]}; + DWORD bthResult = BluetoothAuthenticateDevice(nullptr, radio, &info, passwd, 6); + if (bthResult != ERROR_SUCCESS) + { + UpdateCallback(PairingState::PairingFailed); + return; + } + + bthResult = BluetoothSetServiceState(radio, &info, &bthHidGuid, BLUETOOTH_SERVICE_ENABLE); + if (bthResult != ERROR_SUCCESS) + { + UpdateCallback(PairingState::PairingFailed); + return; + } + + UpdateCallback(PairingState::Finished); + return; + } + + BOOL nextDevResult = BluetoothFindNextDevice(deviceFind, &info); + if (nextDevResult == FALSE) + { + break; + } + } + + BluetoothFindDeviceClose(deviceFind); + } +} +#elif BOOST_OS_LINUX +void PairingDialog::WorkerThread() +{ + constexpr static uint8_t LIAC_LAP[] = {0x00, 0x8b, 0x9e}; + + constexpr static auto isWiimoteName = [](std::string_view name) { + return name == "Nintendo RVL-CNT-01" || name == "Nintendo RVL-CNT-01-TR"; + }; + + // Get default BT device + const auto hostId = hci_get_route(nullptr); + if (hostId < 0) + { + UpdateCallback(PairingState::NoBluetoothAvailable); + return; + } + + // Search for device + inquiry_info* infos = nullptr; + m_cancelButton->Disable(); + const auto respCount = hci_inquiry(hostId, 7, 4, LIAC_LAP, &infos, IREQ_CACHE_FLUSH); + m_cancelButton->Enable(); + if (respCount <= 0) + { + UpdateCallback(PairingState::SearchFailed); + return; + } + stdx::scope_exit infoFree([&]() { bt_free(infos);}); + + if (m_threadShouldQuit) + return; + + // Open dev to read name + const auto hostDev = hci_open_dev(hostId); + stdx::scope_exit devClose([&]() { hci_close_dev(hostDev);}); + + char nameBuffer[HCI_MAX_NAME_LENGTH] = {}; + + bool foundADevice = false; + // Get device name and compare. Would use product and vendor id from SDP, but many third-party Wiimotes don't store them + for (const auto& devInfo : std::span(infos, respCount)) + { + const auto& addr = devInfo.bdaddr; + const auto err = hci_read_remote_name(hostDev, &addr, HCI_MAX_NAME_LENGTH, nameBuffer, + 2000); + if (m_threadShouldQuit) + return; + if (err || !isWiimoteName(nameBuffer)) + continue; + + L2CapWiimote::AddCandidateAddress(addr); + foundADevice = true; + const auto& b = addr.b; + cemuLog_log(LogType::Force, "Pairing Dialog: Found '{}' with address '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'", + nameBuffer, b[5], b[4], b[3], b[2], b[1], b[0]); + } + if (foundADevice) + UpdateCallback(PairingState::Finished); + else + UpdateCallback(PairingState::SearchFailed); +} +#else +void PairingDialog::WorkerThread() +{ + UpdateCallback(PairingState::BluetoothUnusable); +} +#endif +void PairingDialog::UpdateCallback(PairingState state) +{ + auto* event = new wxCommandEvent(wxEVT_PROGRESS_PAIR); + event->SetInt((int)state); + wxQueueEvent(this, event); +} \ No newline at end of file diff --git a/src/gui/PairingDialog.h b/src/gui/input/PairingDialog.h similarity index 96% rename from src/gui/PairingDialog.h rename to src/gui/input/PairingDialog.h index 6c7612d1..02cab4fc 100644 --- a/src/gui/PairingDialog.h +++ b/src/gui/input/PairingDialog.h @@ -17,7 +17,7 @@ private: Pairing, Finished, NoBluetoothAvailable, - BluetoothFailed, + SearchFailed, PairingFailed, BluetoothUnusable }; diff --git a/src/gui/input/panels/WiimoteInputPanel.cpp b/src/gui/input/panels/WiimoteInputPanel.cpp index 050baad1..44a7c001 100644 --- a/src/gui/input/panels/WiimoteInputPanel.cpp +++ b/src/gui/input/panels/WiimoteInputPanel.cpp @@ -12,7 +12,6 @@ #include "input/emulated/WiimoteController.h" #include "gui/helpers/wxHelpers.h" #include "gui/components/wxInputDraw.h" -#include "gui/PairingDialog.h" constexpr WiimoteController::ButtonId g_kFirstColumnItems[] = { @@ -40,11 +39,6 @@ WiimoteInputPanel::WiimoteInputPanel(wxWindow* parent) auto* main_sizer = new wxBoxSizer(wxVERTICAL); auto* horiz_main_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* pair_button = new wxButton(this, wxID_ANY, _("Pair a Wii or Wii U controller")); - pair_button->Bind(wxEVT_BUTTON, &WiimoteInputPanel::on_pair_button, this); - horiz_main_sizer->Add(pair_button); - horiz_main_sizer->AddSpacer(10); - auto* extensions_sizer = new wxBoxSizer(wxHORIZONTAL); horiz_main_sizer->Add(extensions_sizer, wxSizerFlags(0).Align(wxALIGN_CENTER_VERTICAL)); @@ -264,9 +258,3 @@ void WiimoteInputPanel::load_controller(const EmulatedControllerPtr& emulated_co set_active_device_type(wiimote->get_device_type()); } } - -void WiimoteInputPanel::on_pair_button(wxCommandEvent& event) -{ - PairingDialog pairing_dialog(this); - pairing_dialog.ShowModal(); -} diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index 9f7873a1..004dc2ba 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -73,6 +73,11 @@ if (ENABLE_WIIMOTE) api/Wiimote/hidapi/HidapiWiimote.cpp api/Wiimote/hidapi/HidapiWiimote.h ) + if (UNIX AND NOT APPLE) + target_sources(CemuInput PRIVATE + api/Wiimote/l2cap/L2CapWiimote.cpp + api/Wiimote/l2cap/L2CapWiimote.h) + endif() endif () @@ -97,3 +102,8 @@ endif() if (ENABLE_WXWIDGETS) target_link_libraries(CemuInput PRIVATE wx::base wx::core) endif() + + +if (UNIX AND NOT APPLE) + target_link_libraries(CemuInput PRIVATE bluez::bluez) +endif () \ No newline at end of file diff --git a/src/input/api/Wiimote/WiimoteControllerProvider.cpp b/src/input/api/Wiimote/WiimoteControllerProvider.cpp index c80f3fbe..221d75a7 100644 --- a/src/input/api/Wiimote/WiimoteControllerProvider.cpp +++ b/src/input/api/Wiimote/WiimoteControllerProvider.cpp @@ -2,7 +2,12 @@ #include "input/api/Wiimote/NativeWiimoteController.h" #include "input/api/Wiimote/WiimoteMessages.h" +#ifdef HAS_HIDAPI #include "input/api/Wiimote/hidapi/HidapiWiimote.h" +#endif +#ifdef HAS_BLUEZ +#include "input/api/Wiimote/l2cap/L2CapWiimote.h" +#endif #include #include @@ -12,6 +17,7 @@ WiimoteControllerProvider::WiimoteControllerProvider() { m_reader_thread = std::thread(&WiimoteControllerProvider::reader_thread, this); m_writer_thread = std::thread(&WiimoteControllerProvider::writer_thread, this); + m_connectionThread = std::thread(&WiimoteControllerProvider::connectionThread, this); } WiimoteControllerProvider::~WiimoteControllerProvider() @@ -21,48 +27,51 @@ WiimoteControllerProvider::~WiimoteControllerProvider() m_running = false; m_writer_thread.join(); m_reader_thread.join(); + m_connectionThread.join(); } } std::vector> WiimoteControllerProvider::get_controllers() { + m_connectedDeviceMutex.lock(); + auto devices = m_connectedDevices; + m_connectedDeviceMutex.unlock(); + std::scoped_lock lock(m_device_mutex); - std::queue disconnected_wiimote_indices; - for (auto i{0u}; i < m_wiimotes.size(); ++i){ - if (!(m_wiimotes[i].connected = m_wiimotes[i].device->write_data({kStatusRequest, 0x00}))){ - disconnected_wiimote_indices.push(i); - } - } - - const auto valid_new_device = [&](std::shared_ptr & device) { - const auto writeable = device->write_data({kStatusRequest, 0x00}); - const auto not_already_connected = - std::none_of(m_wiimotes.cbegin(), m_wiimotes.cend(), - [device](const auto& it) { - return (*it.device == *device) && it.connected; - }); - return writeable && not_already_connected; - }; - - for (auto& device : WiimoteDevice_t::get_devices()) + for (auto& device : devices) { - if (!valid_new_device(device)) + const auto writeable = device->write_data({kStatusRequest, 0x00}); + if (!writeable) continue; - // Replace disconnected wiimotes - if (!disconnected_wiimote_indices.empty()){ - const auto idx = disconnected_wiimote_indices.front(); - disconnected_wiimote_indices.pop(); - m_wiimotes.replace(idx, std::make_unique(device)); - } - // Otherwise add them - else { - m_wiimotes.push_back(std::make_unique(device)); - } + bool isDuplicate = false; + ssize_t lowestReplaceableIndex = -1; + for (ssize_t i = m_wiimotes.size() - 1; i >= 0; --i) + { + const auto& wiimoteDevice = m_wiimotes[i].device; + if (wiimoteDevice) + { + if (*wiimoteDevice == *device) + { + isDuplicate = true; + break; + } + continue; + } + + lowestReplaceableIndex = i; + } + if (isDuplicate) + continue; + if (lowestReplaceableIndex != -1) + m_wiimotes.replace(lowestReplaceableIndex, std::make_unique(device)); + else + m_wiimotes.push_back(std::make_unique(device)); } std::vector> result; + result.reserve(m_wiimotes.size()); for (size_t i = 0; i < m_wiimotes.size(); ++i) { result.emplace_back(std::make_shared(i)); @@ -74,7 +83,7 @@ std::vector> WiimoteControllerProvider::get_cont bool WiimoteControllerProvider::is_connected(size_t index) { std::shared_lock lock(m_device_mutex); - return index < m_wiimotes.size() && m_wiimotes[index].connected; + return index < m_wiimotes.size() && m_wiimotes[index].device; } bool WiimoteControllerProvider::is_registered_device(size_t index) @@ -141,6 +150,30 @@ WiimoteControllerProvider::WiimoteState WiimoteControllerProvider::get_state(siz return {}; } +void WiimoteControllerProvider::connectionThread() +{ + SetThreadName("Wiimote-connect"); + while (m_running.load(std::memory_order_relaxed)) + { + std::vector devices; +#ifdef HAS_HIDAPI + const auto& hidDevices = HidapiWiimote::get_devices(); + std::ranges::move(hidDevices, std::back_inserter(devices)); +#endif +#ifdef HAS_BLUEZ + const auto& l2capDevices = L2CapWiimote::get_devices(); + std::ranges::move(l2capDevices, std::back_inserter(devices)); +#endif + { + std::scoped_lock lock(m_connectedDeviceMutex); + m_connectedDevices.clear(); + std::ranges::move(devices, std::back_inserter(m_connectedDevices)); + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } +} + + void WiimoteControllerProvider::reader_thread() { SetThreadName("Wiimote-reader"); @@ -148,7 +181,7 @@ void WiimoteControllerProvider::reader_thread() while (m_running.load(std::memory_order_relaxed)) { const auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastCheck) > std::chrono::seconds(2)) + if (std::chrono::duration_cast(now - lastCheck) > std::chrono::milliseconds(500)) { // check for new connected wiimotes get_controllers(); @@ -160,11 +193,16 @@ void WiimoteControllerProvider::reader_thread() for (size_t index = 0; index < m_wiimotes.size(); ++index) { auto& wiimote = m_wiimotes[index]; - if (!wiimote.connected) + if (!wiimote.device) continue; const auto read_data = wiimote.device->read_data(); - if (!read_data || read_data->empty()) + if (!read_data) + { + wiimote.device.reset(); + continue; + } + if (read_data->empty()) continue; receivedAnyPacket = true; @@ -921,18 +959,18 @@ void WiimoteControllerProvider::writer_thread() if (index != (size_t)-1 && !data.empty()) { - if (m_wiimotes[index].rumble) + auto& wiimote = m_wiimotes[index]; + if (!wiimote.device) + continue; + if (wiimote.rumble) data[1] |= 1; - - m_wiimotes[index].connected = m_wiimotes[index].device->write_data(data); - if (m_wiimotes[index].connected) + if (!wiimote.device->write_data(data)) { - m_wiimotes[index].data_ts = std::chrono::high_resolution_clock::now(); + wiimote.device.reset(); + wiimote.rumble = false; } else - { - m_wiimotes[index].rumble = false; - } + wiimote.data_ts = std::chrono::high_resolution_clock::now(); } device_lock.unlock(); diff --git a/src/input/api/Wiimote/WiimoteControllerProvider.h b/src/input/api/Wiimote/WiimoteControllerProvider.h index 7629b641..90f28d5c 100644 --- a/src/input/api/Wiimote/WiimoteControllerProvider.h +++ b/src/input/api/Wiimote/WiimoteControllerProvider.h @@ -77,16 +77,17 @@ public: private: std::atomic_bool m_running = false; std::thread m_reader_thread, m_writer_thread; - std::shared_mutex m_device_mutex; + std::thread m_connectionThread; + std::vector m_connectedDevices; + std::mutex m_connectedDeviceMutex; struct Wiimote { Wiimote(WiimoteDevicePtr device) : device(std::move(device)) {} WiimoteDevicePtr device; - std::atomic_bool connected = true; std::atomic_bool rumble = false; std::shared_mutex mutex; @@ -103,6 +104,7 @@ private: void reader_thread(); void writer_thread(); + void connectionThread(); void calibrate(size_t index); IRMode set_ir_camera(size_t index, bool state); diff --git a/src/input/api/Wiimote/WiimoteDevice.h b/src/input/api/Wiimote/WiimoteDevice.h index 7938bbdf..8ea5b321 100644 --- a/src/input/api/Wiimote/WiimoteDevice.h +++ b/src/input/api/Wiimote/WiimoteDevice.h @@ -9,8 +9,7 @@ public: virtual bool write_data(const std::vector& data) = 0; virtual std::optional> read_data() = 0; - virtual bool operator==(WiimoteDevice& o) const = 0; - bool operator!=(WiimoteDevice& o) const { return *this == o; } + virtual bool operator==(const WiimoteDevice& o) const = 0; }; using WiimoteDevicePtr = std::shared_ptr; diff --git a/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp b/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp index db185675..5780909f 100644 --- a/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp +++ b/src/input/api/Wiimote/hidapi/HidapiWiimote.cpp @@ -47,8 +47,11 @@ std::vector HidapiWiimote::get_devices() { return wiimote_devices; } -bool HidapiWiimote::operator==(WiimoteDevice& o) const { - return static_cast(o).m_path == m_path; +bool HidapiWiimote::operator==(const WiimoteDevice& rhs) const { + auto other = dynamic_cast(&rhs); + if (!other) + return false; + return m_path == other->m_path; } HidapiWiimote::~HidapiWiimote() { diff --git a/src/input/api/Wiimote/hidapi/HidapiWiimote.h b/src/input/api/Wiimote/hidapi/HidapiWiimote.h index 858cb1f3..952a36f0 100644 --- a/src/input/api/Wiimote/hidapi/HidapiWiimote.h +++ b/src/input/api/Wiimote/hidapi/HidapiWiimote.h @@ -10,7 +10,7 @@ public: bool write_data(const std::vector &data) override; std::optional> read_data() override; - bool operator==(WiimoteDevice& o) const override; + bool operator==(const WiimoteDevice& o) const override; static std::vector get_devices(); @@ -19,5 +19,3 @@ private: const std::string m_path; }; - -using WiimoteDevice_t = HidapiWiimote; \ No newline at end of file diff --git a/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp b/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp new file mode 100644 index 00000000..28a123f3 --- /dev/null +++ b/src/input/api/Wiimote/l2cap/L2CapWiimote.cpp @@ -0,0 +1,148 @@ +#include "L2CapWiimote.h" +#include + +constexpr auto comparator = [](const bdaddr_t& a, const bdaddr_t& b) { + return bacmp(&a, &b); +}; + +static auto s_addresses = std::map(comparator); +static std::mutex s_addressMutex; + +static bool AttemptConnect(int sockFd, const sockaddr_l2& addr) +{ + auto res = connect(sockFd, reinterpret_cast(&addr), + sizeof(sockaddr_l2)); + if (res == 0) + return true; + return connect(sockFd, reinterpret_cast(&addr), + sizeof(sockaddr_l2)) == 0; +} + +static bool AttemptSetNonBlock(int sockFd) +{ + return fcntl(sockFd, F_SETFL, fcntl(sockFd, F_GETFL) | O_NONBLOCK) == 0; +} + +L2CapWiimote::L2CapWiimote(int recvFd, int sendFd, bdaddr_t addr) + : m_recvFd(recvFd), m_sendFd(sendFd), m_addr(addr) +{ +} + +L2CapWiimote::~L2CapWiimote() +{ + close(m_recvFd); + close(m_sendFd); + const auto& b = m_addr.b; + cemuLog_logDebug(LogType::Force, "Wiimote at {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} disconnected", b[5], b[4], b[3], b[2], b[1], b[0]); + + // Re-add to candidate vec + s_addressMutex.lock(); + s_addresses[m_addr] = false; + s_addressMutex.unlock(); +} + +void L2CapWiimote::AddCandidateAddress(bdaddr_t addr) +{ + std::scoped_lock lock(s_addressMutex); + s_addresses.try_emplace(addr, false); +} + +std::vector L2CapWiimote::get_devices() +{ + s_addressMutex.lock(); + std::vector unconnected; + for (const auto& [addr, connected] : s_addresses) + { + if (!connected) + unconnected.push_back(addr); + } + s_addressMutex.unlock(); + + std::vector outDevices; + for (const auto& addr : unconnected) + { + // Socket for sending data to controller, PSM 0x11 + auto sendFd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (sendFd < 0) + { + cemuLog_logDebug(LogType::Force, "Failed to open send socket: {}", strerror(errno)); + continue; + } + + sockaddr_l2 sendAddr{}; + sendAddr.l2_family = AF_BLUETOOTH; + sendAddr.l2_psm = htobs(0x11); + sendAddr.l2_bdaddr = addr; + + if (!AttemptConnect(sendFd, sendAddr) || !AttemptSetNonBlock(sendFd)) + { + const auto& b = addr.b; + cemuLog_logDebug(LogType::Force, "Failed to connect send socket to '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}': {}", + b[5], b[4], b[3], b[2], b[1], b[0], strerror(errno)); + close(sendFd); + continue; + } + + // Socket for receiving data from controller, PSM 0x13 + auto recvFd = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (recvFd < 0) + { + cemuLog_logDebug(LogType::Force, "Failed to open recv socket: {}", strerror(errno)); + close(sendFd); + continue; + } + sockaddr_l2 recvAddr{}; + recvAddr.l2_family = AF_BLUETOOTH; + recvAddr.l2_psm = htobs(0x13); + recvAddr.l2_bdaddr = addr; + + if (!AttemptConnect(recvFd, recvAddr) || !AttemptSetNonBlock(recvFd)) + { + const auto& b = addr.b; + cemuLog_logDebug(LogType::Force, "Failed to connect recv socket to '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}': {}", + b[5], b[4], b[3], b[2], b[1], b[0], strerror(errno)); + close(sendFd); + close(recvFd); + continue; + } + outDevices.emplace_back(std::make_shared(sendFd, recvFd, addr)); + + s_addressMutex.lock(); + s_addresses[addr] = true; + s_addressMutex.unlock(); + } + return outDevices; +} + +bool L2CapWiimote::write_data(const std::vector& data) +{ + const auto size = data.size(); + cemu_assert_debug(size < 23); + uint8 buffer[23]; + // All outgoing messages must be prefixed with 0xA2 + buffer[0] = 0xA2; + std::memcpy(buffer + 1, data.data(), size); + const auto outSize = size + 1; + return send(m_sendFd, buffer, outSize, 0) == outSize; +} + +std::optional> L2CapWiimote::read_data() +{ + uint8 buffer[23]; + const auto nBytes = recv(m_sendFd, buffer, 23, 0); + + if (nBytes < 0 && errno == EWOULDBLOCK) + return std::vector{}; + // All incoming messages must be prefixed with 0xA1 + if (nBytes < 2 || buffer[0] != 0xA1) + return std::nullopt; + return std::vector(buffer + 1, buffer + 1 + nBytes - 1); +} + +bool L2CapWiimote::operator==(const WiimoteDevice& rhs) const +{ + auto mote = dynamic_cast(&rhs); + if (!mote) + return false; + return bacmp(&m_addr, &mote->m_addr) == 0; +} \ No newline at end of file diff --git a/src/input/api/Wiimote/l2cap/L2CapWiimote.h b/src/input/api/Wiimote/l2cap/L2CapWiimote.h new file mode 100644 index 00000000..cc8d071b --- /dev/null +++ b/src/input/api/Wiimote/l2cap/L2CapWiimote.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +class L2CapWiimote : public WiimoteDevice +{ + public: + L2CapWiimote(int recvFd, int sendFd, bdaddr_t addr); + ~L2CapWiimote() override; + + bool write_data(const std::vector& data) override; + std::optional> read_data() override; + bool operator==(const WiimoteDevice& o) const override; + + static void AddCandidateAddress(bdaddr_t addr); + static std::vector get_devices(); + private: + int m_recvFd; + int m_sendFd; + bdaddr_t m_addr; +}; + diff --git a/src/input/emulated/VPADController.cpp b/src/input/emulated/VPADController.cpp index f1ab1bc4..81615c9b 100644 --- a/src/input/emulated/VPADController.cpp +++ b/src/input/emulated/VPADController.cpp @@ -408,7 +408,7 @@ bool VPADController::push_rumble(uint8* pattern, uint8 length) std::scoped_lock lock(m_rumble_mutex); if (m_rumble_queue.size() >= 5) { - cemuLog_logDebug(LogType::Force, "too many cmds"); + cemuLog_logDebugOnce(LogType::Force, "VPADControlMotor(): Pattern too long"); return false; }