mirror of https://github.com/cemu-project/Cemu.git
Merge branch 'refs/heads/main' into vulkanfixesandcleanup
# Conflicts: # src/Cafe/HW/Latte/Renderer/RendererOuputShader.cpp # src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRendererCore.cpp
This commit is contained in:
commit
234a013111
|
@ -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: |
|
||||
|
|
|
@ -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:
|
|
@ -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
|
6
BUILD.md
6
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,20 @@
|
|||
# SPDX-FileCopyrightText: 2022 Andrea Pappacoda <andrea@pappacoda.it>
|
||||
# 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
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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$<$<CON
|
|||
|
||||
target_include_directories(CemuCafe PUBLIC "../")
|
||||
|
||||
if (glslang_VERSION VERSION_LESS "15.0.0")
|
||||
set(glslang_target "glslang::SPIRV")
|
||||
else()
|
||||
set(glslang_target "glslang")
|
||||
endif()
|
||||
|
||||
target_link_libraries(CemuCafe PRIVATE
|
||||
CemuAsm
|
||||
CemuAudio
|
||||
|
@ -545,7 +553,7 @@ target_link_libraries(CemuCafe PRIVATE
|
|||
Boost::nowide
|
||||
CURL::libcurl
|
||||
fmt::fmt
|
||||
glslang::SPIRV
|
||||
${glslang_target}
|
||||
ih264d
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
|
|
|
@ -637,40 +637,40 @@ namespace CafeSystem
|
|||
fsc_unmount("/cemuBossStorage/", FSC_PRIORITY_BASE);
|
||||
}
|
||||
|
||||
STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId)
|
||||
PREPARE_STATUS_CODE LoadAndMountForegroundTitle(TitleId titleId)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Mounting title {:016x}", (uint64)titleId);
|
||||
sGameInfo_ForegroundTitle = CafeTitleList::GetGameInfo(titleId);
|
||||
if (!sGameInfo_ForegroundTitle.IsValid())
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Mounting failed: Game meta information is either missing, inaccessible or not valid (missing or invalid .xml files in code and meta folder)");
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
}
|
||||
// check base
|
||||
TitleInfo& titleBase = sGameInfo_ForegroundTitle.GetBase();
|
||||
if (!titleBase.IsValid())
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
if(!titleBase.ParseXmlInfo())
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
cemuLog_log(LogType::Force, "Base: {}", titleBase.GetPrintPath());
|
||||
// mount base
|
||||
if (!titleBase.Mount("/vol/content", "content", FSC_PRIORITY_BASE) || !titleBase.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_BASE))
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Mounting failed");
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
}
|
||||
// check update
|
||||
TitleInfo& titleUpdate = sGameInfo_ForegroundTitle.GetUpdate();
|
||||
if (titleUpdate.IsValid())
|
||||
{
|
||||
if (!titleUpdate.ParseXmlInfo())
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
cemuLog_log(LogType::Force, "Update: {}", titleUpdate.GetPrintPath());
|
||||
// mount update
|
||||
if (!titleUpdate.Mount("/vol/content", "content", FSC_PRIORITY_PATCH) || !titleUpdate.Mount(GetInternalVirtualCodeFolder(), "code", FSC_PRIORITY_PATCH))
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Mounting failed");
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -682,20 +682,20 @@ namespace CafeSystem
|
|||
// todo - support for multi-title AOC
|
||||
TitleInfo& titleAOC = aocList[0];
|
||||
if (!titleAOC.ParseXmlInfo())
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
cemu_assert_debug(titleAOC.IsValid());
|
||||
cemuLog_log(LogType::Force, "DLC: {}", titleAOC.GetPrintPath());
|
||||
// mount AOC
|
||||
if (!titleAOC.Mount(fmt::format("/vol/aoc{:016x}", titleAOC.GetAppTitleId()), "content", FSC_PRIORITY_PATCH))
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Mounting failed");
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
}
|
||||
}
|
||||
else
|
||||
cemuLog_log(LogType::Force, "DLC: Not present");
|
||||
sForegroundTitleId = titleId;
|
||||
return STATUS_CODE::SUCCESS;
|
||||
return PREPARE_STATUS_CODE::SUCCESS;
|
||||
}
|
||||
|
||||
void UnmountForegroundTitle()
|
||||
|
@ -723,7 +723,7 @@ namespace CafeSystem
|
|||
}
|
||||
}
|
||||
|
||||
STATUS_CODE SetupExecutable()
|
||||
PREPARE_STATUS_CODE SetupExecutable()
|
||||
{
|
||||
// set rpx path from cos.xml if available
|
||||
_pathToBaseExecutable = _pathToExecutable;
|
||||
|
@ -755,7 +755,7 @@ namespace CafeSystem
|
|||
}
|
||||
}
|
||||
LoadMainExecutable();
|
||||
return STATUS_CODE::SUCCESS;
|
||||
return PREPARE_STATUS_CODE::SUCCESS;
|
||||
}
|
||||
|
||||
void SetupMemorySpace()
|
||||
|
@ -769,7 +769,7 @@ namespace CafeSystem
|
|||
memory_unmapForCurrentTitle();
|
||||
}
|
||||
|
||||
STATUS_CODE PrepareForegroundTitle(TitleId titleId)
|
||||
PREPARE_STATUS_CODE PrepareForegroundTitle(TitleId titleId)
|
||||
{
|
||||
CafeTitleList::WaitForMandatoryScan();
|
||||
sLaunchModeIsStandalone = false;
|
||||
|
@ -780,21 +780,21 @@ namespace CafeSystem
|
|||
// mount mlc storage
|
||||
MountBaseDirectories();
|
||||
// mount title folders
|
||||
STATUS_CODE r = LoadAndMountForegroundTitle(titleId);
|
||||
if (r != STATUS_CODE::SUCCESS)
|
||||
PREPARE_STATUS_CODE r = LoadAndMountForegroundTitle(titleId);
|
||||
if (r != PREPARE_STATUS_CODE::SUCCESS)
|
||||
return r;
|
||||
gameProfile_load();
|
||||
// setup memory space and PPC recompiler
|
||||
SetupMemorySpace();
|
||||
PPCRecompiler_init();
|
||||
r = SetupExecutable(); // load RPX
|
||||
if (r != STATUS_CODE::SUCCESS)
|
||||
if (r != PREPARE_STATUS_CODE::SUCCESS)
|
||||
return r;
|
||||
InitVirtualMlcStorage();
|
||||
return STATUS_CODE::SUCCESS;
|
||||
return PREPARE_STATUS_CODE::SUCCESS;
|
||||
}
|
||||
|
||||
STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path)
|
||||
PREPARE_STATUS_CODE PrepareForegroundTitleFromStandaloneRPX(const fs::path& path)
|
||||
{
|
||||
sLaunchModeIsStandalone = true;
|
||||
cemuLog_log(LogType::Force, "Launching executable in standalone mode due to incorrect layout or missing meta files");
|
||||
|
@ -812,7 +812,7 @@ namespace CafeSystem
|
|||
if (!r)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to mount {}", _pathToUtf8(contentPath));
|
||||
return STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
return PREPARE_STATUS_CODE::UNABLE_TO_MOUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -824,7 +824,7 @@ namespace CafeSystem
|
|||
// since a lot of systems (including save folder location) rely on a TitleId, we derive a placeholder id from the executable hash
|
||||
auto execData = fsc_extractFile(_pathToExecutable.c_str());
|
||||
if (!execData)
|
||||
return STATUS_CODE::INVALID_RPX;
|
||||
return PREPARE_STATUS_CODE::INVALID_RPX;
|
||||
uint32 h = generateHashFromRawRPXData(execData->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()
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<FSTVolume::ClusterHashMode>(partitionHeaderSI.fstHashType));
|
||||
auto siFST = OpenFST(dataSource.get(), (uint64)partitionHeaderSI.fstSector * DISC_SECTOR_SIZE, partitionHeaderSI.fstSize, &discTitleKey, static_cast<FSTVolume::ClusterHashMode>(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<uint8> 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<uint8> 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<FSTVolume::ClusterHashMode>(partitionHeaderGM.fstHashType));
|
||||
FSTVolume* r = OpenFST(std::move(dataSource), (uint64)partitionHeaderGM.fstSector * DISC_SECTOR_SIZE, partitionHeaderGM.fstSize, &gmTitleKey, static_cast<FSTVolume::ClusterHashMode>(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<FSTVolume::ClusterHashMode>((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; i<contentList.size(); i++)
|
||||
{
|
||||
auto& cluster = clusterTable[i];
|
||||
auto& content = contentList[i];
|
||||
cluster.hasContentHash = true;
|
||||
cluster.contentHashIsSHA1 = HAS_FLAG(contentList[i].contentFlags, NCrypto::TMDParser::TMDContentFlags::FLAG_SHA1);
|
||||
cluster.contentSize = content.size;
|
||||
static_assert(sizeof(content.hash32) == sizeof(cluster.contentHash32));
|
||||
memcpy(cluster.contentHash32, content.hash32, sizeof(cluster.contentHash32));
|
||||
// if unhashed mode, then initialize the hash context
|
||||
if(cluster.hashMode == ClusterHashMode::RAW || cluster.hashMode == ClusterHashMode::RAW_STREAM)
|
||||
{
|
||||
cluster.singleHashCtx.reset(EVP_MD_CTX_new());
|
||||
EVP_DigestInit_ex(cluster.singleHashCtx.get(), cluster.contentHashIsSHA1 ? EVP_sha1() : EVP_sha256(), nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
// preprocess FST table
|
||||
FSTHeader_FileEntry* fileTable = (FSTHeader_FileEntry*)(clusterDataTable + numCluster);
|
||||
|
@ -491,16 +540,17 @@ FSTVolume* FSTVolume::OpenFST(FSTDataSource* dataSource, uint64 fstOffset, uint3
|
|||
fstVolume->m_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<FSTDataSource> dataSource, uint64 fstOffset, uint32 fstSize, NCrypto::AesKey* partitionTitleKey, ClusterHashMode fstHashMode)
|
||||
FSTVolume* FSTVolume::OpenFST(std::unique_ptr<FSTDataSource> 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<uint8> 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<uint64>::max();
|
||||
if(dropRawItr != m_cacheDecryptedRawBlocks.end())
|
||||
lastAccess = dropRawItr->second->lastAccess;
|
||||
if(dropHashedItr != m_cacheDecryptedHashedBlocks.end())
|
||||
lastAccess = std::min<uint64>(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<uint32>(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<uint32>(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)
|
||||
|
|
|
@ -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<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> 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<FSTCluster> m_cluster;
|
||||
std::vector<FSTEntry> m_entries;
|
||||
std::vector<char> 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<uint64, struct FSTCachedRawBlock*> m_cacheDecryptedRawBlocks;
|
||||
std::unordered_map<uint64, struct FSTCachedHashedBlock*> 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<FSTDataSource> 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<FSTDataSource> 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<char>& nameStringTable, std::vector<FSTEntry>& fstEntries);
|
||||
|
||||
bool MatchFSTEntryName(FSTEntry& entry, std::string_view comparedName)
|
||||
|
|
|
@ -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,22 +422,30 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
|||
std::unordered_map<std::string, std::vector<PresetPtr>> tmp_map;
|
||||
|
||||
// all vars must be defined in the default preset vars before
|
||||
for (const auto& entry : m_presets)
|
||||
std::vector<std::pair<std::string, std::string>> 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
|
||||
presetVar.second.first = it->second.first;
|
||||
}
|
||||
}
|
||||
|
||||
// overwrite var type with default var type
|
||||
kv.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
|
||||
|
@ -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<std::string> 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;
|
||||
}
|
||||
|
|
|
@ -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<void>(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;
|
||||
|
|
|
@ -114,6 +114,7 @@ void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore = fals
|
|||
|
||||
void debugger_createPatch(uint32 address, std::span<uint8> patchData);
|
||||
bool debugger_hasPatch(uint32 address);
|
||||
void debugger_removePatch(uint32 address);
|
||||
|
||||
void debugger_forceBreak(); // force breakpoint at the next possible instruction
|
||||
bool debugger_isTrapped();
|
||||
|
|
|
@ -124,6 +124,7 @@ typedef struct
|
|||
LattePerfStatCounter numGraphicPipelines;
|
||||
LattePerfStatCounter numImages;
|
||||
LattePerfStatCounter numImageViews;
|
||||
LattePerfStatCounter numSamplers;
|
||||
LattePerfStatCounter numRenderPass;
|
||||
LattePerfStatCounter numFramebuffer;
|
||||
|
||||
|
|
|
@ -934,13 +934,6 @@ void LatteRenderTarget_copyToBackbuffer(LatteTextureView* textureView, bool isPa
|
|||
{
|
||||
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)
|
||||
{
|
||||
if(renderUpsideDown)
|
||||
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1415,19 +1415,23 @@ void _emitALUOP2InstructionCode(LatteDecompilerShaderContext* shaderContext, Lat
|
|||
}
|
||||
else if( aluInstruction->opcode == ALU_OP2_INST_ADD_INT )
|
||||
_emitALUOperationBinary<LATTE_DECOMPILER_DTYPE_SIGNED_INT>(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 )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
sint32 effectiveWidth, effectiveHeight;
|
||||
texture_view.baseTexture->GetEffectiveSize(effectiveWidth, effectiveHeight, 0);
|
||||
auto setUniforms = [&](RendererShader* shader, const UniformLocations& locations){
|
||||
float res[2];
|
||||
// vertex shader
|
||||
if (m_attributes[0].m_loc_texture_src_resolution != -1)
|
||||
if (locations.m_loc_textureSrcResolution != -1)
|
||||
{
|
||||
res[0] = (float)effectiveWidth;
|
||||
res[1] = (float)effectiveHeight;
|
||||
shader->SetUniform2fv(locations.m_loc_textureSrcResolution, res, 1);
|
||||
}
|
||||
|
||||
if (locations.m_loc_nativeResolution != -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);
|
||||
shader->SetUniform2fv(locations.m_loc_nativeResolution, 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 (m_attributes[0].m_loc_output_resolution != -1)
|
||||
if (locations.m_loc_outputResolution != -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);
|
||||
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,7 +310,12 @@ void RendererOutputShader::InitializeStatic()
|
|||
{
|
||||
vertex_source = GetOpenGlVertexSource(false);
|
||||
vertex_source_ud = GetOpenGlVertexSource(true);
|
||||
|
||||
}
|
||||
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);
|
||||
|
||||
|
@ -358,21 +324,6 @@ void RendererOutputShader::InitializeStatic()
|
|||
|
||||
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);*/
|
||||
}
|
||||
}
|
||||
|
||||
void RendererOutputShader::ShutdownStatic()
|
||||
|
|
|
@ -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<RendererShader> m_vertex_shader;
|
||||
std::unique_ptr<RendererShader> 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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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<VKRMoveableRefCounterRef*> 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<uint64, VKRObjectSampler*> s_samplerCache;
|
||||
VkSampler m_sampler{ VK_NULL_HANDLE };
|
||||
uint64 m_hash;
|
||||
};
|
||||
|
||||
class VKRObjectRenderPass : public VKRDestructibleObject
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -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<uint64, VKRObjectSampler*> 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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<std::recursive_mutex> _lock(g_mutex_camera);
|
||||
if (g_cameraCounter == 0)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -4,5 +4,9 @@ namespace coreinit
|
|||
{
|
||||
void PrepareGHSRuntime();
|
||||
|
||||
sint32be* __gh_errno_ptr();
|
||||
void __gh_set_errno(sint32 errNo);
|
||||
sint32 __gh_get_errno();
|
||||
|
||||
void InitializeGHS();
|
||||
};
|
|
@ -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<sint64>(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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -9,32 +9,45 @@
|
|||
#include <wx/msgdlg.h>
|
||||
|
||||
#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<ErrorDialogType> 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<float>(1.0f, (float)elapsedTick / (float)coreinit::EspressoTime::ConvertMsToTimerTicks(FADE_TIME));
|
||||
else
|
||||
return std::max<float>(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<sint32> m_resultCode;
|
||||
/* +0x239C */ betype<BUTTON_SELECTION> m_buttonSelection;
|
||||
/* +0x23A0 */ betype<sint32> m_resultCodeForLeftButton;
|
||||
/* +0x23A4 */ betype<sint32> m_resultCodeForRightButton;
|
||||
};
|
||||
|
||||
struct ErrEula_t
|
||||
{
|
||||
SysAllocator<coreinit::OSMutex> mutex;
|
||||
|
@ -87,18 +228,12 @@ namespace erreula
|
|||
uint32 langType;
|
||||
MEMPTR<coreinit::FSClient_t> fsClient;
|
||||
|
||||
AppearArg_t currentDialog;
|
||||
uint32 state;
|
||||
bool buttonPressed;
|
||||
bool rightButtonPressed;
|
||||
std::unique_ptr<ErrEulaInstance> errEulaInstance;
|
||||
|
||||
AppearError currentDialog;
|
||||
bool homeNixSignVisible;
|
||||
|
||||
std::chrono::steady_clock::time_point stateTimer{};
|
||||
} g_errEula = {};
|
||||
|
||||
|
||||
|
||||
std::wstring GetText(uint16be* text)
|
||||
{
|
||||
std::wstringstream result;
|
||||
|
@ -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<ErrEulaInstance>();
|
||||
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<std::chrono::milliseconds>(tick_cached() - g_errEula.stateTimer).count() <= 1000)
|
||||
{
|
||||
if(g_errEula.errEulaInstance->GetState() != ErrEulaState::Visible && g_errEula.errEulaInstance->GetState() != ErrEulaState::Appearing && g_errEula.errEulaInstance->GetState() != ErrEulaState::Disappearing)
|
||||
return;
|
||||
}
|
||||
|
||||
g_errEula.state = ERREULA_STATE_VISIBLE;
|
||||
g_errEula.stateTimer = tick_cached();
|
||||
}
|
||||
/*else if(g_errEula.state == STATE_VISIBLE)
|
||||
{
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(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<std::chrono::milliseconds>(tick_cached() - g_errEula.stateTimer).count() >= 2000)
|
||||
{
|
||||
g_errEula.state = ERREULA_STATE_HIDDEN;
|
||||
g_errEula.stateTimer = tick_cached();
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -280,13 +387,24 @@ namespace erreula
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<InfinityBaseDevice>();
|
||||
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<DimensionsToypadDevice>();
|
||||
AttachDevice(device);
|
||||
}
|
||||
}
|
||||
} // namespace nsyshid::backend::emulated
|
|
@ -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;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,108 @@
|
|||
#include <mutex>
|
||||
|
||||
#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<FileStream> dimFile;
|
||||
std::array<uint8, 0x2D * 0x04> data{};
|
||||
uint8 index = 255;
|
||||
uint8 pad = 255;
|
||||
uint32 id = 0;
|
||||
void Save();
|
||||
};
|
||||
|
||||
void SendCommand(std::span<const uint8, 32> buf);
|
||||
std::array<uint8, 32> GetStatus();
|
||||
|
||||
void GenerateRandomNumber(std::span<const uint8, 8> buf, uint8 sequence,
|
||||
std::array<uint8, 32>& replyBuf);
|
||||
void InitializeRNG(uint32 seed);
|
||||
void GetChallengeResponse(std::span<const uint8, 8> buf, uint8 sequence,
|
||||
std::array<uint8, 32>& replyBuf);
|
||||
void QueryBlock(uint8 index, uint8 page, std::array<uint8, 32>& replyBuf,
|
||||
uint8 sequence);
|
||||
void WriteBlock(uint8 index, uint8 page, std::span<const uint8, 4> toWriteBuf, std::array<uint8, 32>& replyBuf,
|
||||
uint8 sequence);
|
||||
void GetModel(std::span<const uint8, 8> buf, uint8 sequence,
|
||||
std::array<uint8, 32>& replyBuf);
|
||||
|
||||
bool RemoveFigure(uint8 pad, uint8 index, bool fullRemove);
|
||||
bool TempRemove(uint8 index);
|
||||
bool CancelRemove(uint8 index);
|
||||
uint32 LoadFigure(const std::array<uint8, 0x2D * 0x04>& buf, std::unique_ptr<FileStream> 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<const uint32, const char*> GetListMinifigs();
|
||||
static std::map<const uint32, const char*> GetListTokens();
|
||||
std::string FindFigure(uint32 figNum);
|
||||
|
||||
protected:
|
||||
std::mutex m_dimensionsMutex;
|
||||
std::array<DimensionsMini, 7> m_figures{};
|
||||
|
||||
private:
|
||||
void RandomUID(std::array<uint8, 0x2D * 0x04>& uidBuffer);
|
||||
uint8 GenerateChecksum(const std::array<uint8, 32>& data,
|
||||
int numOfBytes) const;
|
||||
std::array<uint8, 8> Decrypt(std::span<const uint8, 8> buf, std::optional<std::array<uint8, 16>> key);
|
||||
std::array<uint8, 8> Encrypt(std::span<const uint8, 8> buf, std::optional<std::array<uint8, 16>> key);
|
||||
std::array<uint8, 16> GenerateFigureKey(const std::array<uint8, 0x2D * 0x04>& uid);
|
||||
std::array<uint8, 4> PWDGenerate(const std::array<uint8, 0x2D * 0x04>& uid);
|
||||
std::array<uint8, 4> DimensionsRandomize(const std::vector<uint8> key, uint8 count);
|
||||
uint32 GetFigureId(const std::array<uint8, 0x2D * 0x04>& buf);
|
||||
uint32 Scramble(const std::array<uint8, 7>& 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<std::array<uint8, 32>> m_figureAddedRemovedResponses;
|
||||
std::queue<std::array<uint8, 32>> m_queries;
|
||||
};
|
||||
extern DimensionsUSB g_dimensionstoypad;
|
||||
|
||||
} // namespace nsyshid
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -521,6 +521,7 @@ struct CemuConfig
|
|||
{
|
||||
ConfigValue<bool> emulate_skylander_portal{false};
|
||||
ConfigValue<bool> emulate_infinity_base{false};
|
||||
ConfigValue<bool> emulate_dimensions_toypad{false};
|
||||
}emulated_usb_devices{};
|
||||
|
||||
private:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include "gui/EmulatedUSBDevices/EmulatedUSBDeviceFrame.h"
|
||||
#include "EmulatedUSBDeviceFrame.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
@ -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 <wx/arrstr.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/combobox.h>
|
||||
#include <wx/checkbox.h>
|
||||
#include <wx/combobox.h>
|
||||
#include <wx/filedlg.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/notebook.h>
|
||||
#include <wx/panel.h>
|
||||
|
@ -29,7 +32,6 @@
|
|||
#include <wx/wfstream.h>
|
||||
|
||||
#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"), "", "",
|
||||
|
@ -308,7 +393,7 @@ CreateSkylanderDialog::CreateSkylanderDialog(wxWindow* parent, uint8 slot)
|
|||
|
||||
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<FileStream> infFile(FileStream::openFile2(_utf8ToPath(path.utf8_string()), true));
|
||||
if (!infFile)
|
||||
{
|
||||
wxMessageDialog errorMessage(this, "File Open Error");
|
||||
errorMessage.ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<uint8, nsyshid::INF_FIGURE_SIZE> 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<FileStream> 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<uint8, 0x2D * 0x04> file_data;
|
||||
|
||||
void EmulatedUSBDeviceFrame::LoadFigurePath(uint8 slot, wxString path)
|
||||
{
|
||||
std::unique_ptr<FileStream> 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<uint8, nsyshid::INF_FIGURE_SIZE> 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);
|
||||
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<void*>(0xFFFFFFFF));
|
||||
wxArrayString filterlist;
|
||||
for (const auto& it : nsyshid::g_dimensionstoypad.GetListMinifigs())
|
||||
{
|
||||
const uint32 figure = it.first;
|
||||
comboBox->Append(it.second, reinterpret_cast<void*>(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<uint32> 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<uint64>(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<std::optional<uint32>, 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<uint32> 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<std::optional<uint32>, 7> EmulatedUSBDeviceFrame::GetCurrentMinifigs()
|
||||
{
|
||||
return m_dimSlots;
|
||||
}
|
|
@ -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<std::optional<uint32>, 7> GetCurrentMinifigs();
|
||||
|
||||
private:
|
||||
wxCheckBox* m_emulatePortal;
|
||||
wxCheckBox* m_emulateBase;
|
||||
wxCheckBox* m_emulateToypad;
|
||||
std::array<wxTextCtrl*, nsyshid::MAX_SKYLANDERS> m_skylanderSlots;
|
||||
std::array<wxTextCtrl*, nsyshid::MAX_FIGURES> m_infinitySlots;
|
||||
std::array<wxTextCtrl*, 7> m_dimensionSlots;
|
||||
std::array<std::optional<std::tuple<uint8, uint16, uint16>>, nsyshid::MAX_SKYLANDERS> m_skySlots;
|
||||
std::array<std::optional<uint32>, 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,7 +66,8 @@ 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;
|
||||
|
@ -60,3 +75,28 @@ class CreateInfinityFigureDialog : public wxDialog {
|
|||
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<uint32> currentId);
|
||||
};
|
|
@ -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: ");
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
#include "gui/wxgui.h"
|
||||
#include "gui/PairingDialog.h"
|
||||
|
||||
#if BOOST_OS_WINDOWS
|
||||
#include <bluetoothapis.h>
|
||||
#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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<uint32>(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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
#include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h"
|
||||
#include <wx/mstream.h> // 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;
|
||||
case IDContextMenu_ToggleBreakpoint:
|
||||
{
|
||||
debugger_toggleExecuteBreakpoint(m_contextMenuAddress);
|
||||
wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE);
|
||||
wxPostEvent(this->m_parent, evt);
|
||||
break;
|
||||
}
|
||||
else if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + OFFSET_DISASSEMBLY)
|
||||
case IDContextMenu_RestoreOriginalInstructions:
|
||||
{
|
||||
// double-clicked on disassembly (operation and operand data)
|
||||
return;
|
||||
debugger_removePatch(m_contextMenuAddress);
|
||||
wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); // This also refreshes the disassembly view
|
||||
wxPostEvent(this->m_parent, evt);
|
||||
break;
|
||||
}
|
||||
else
|
||||
case IDContextMenu_CopyAddress:
|
||||
{
|
||||
// comment
|
||||
return;
|
||||
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<MPTR> 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<long>(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<long>(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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,300 @@
|
|||
#include "gui/wxgui.h"
|
||||
#include "PairingDialog.h"
|
||||
|
||||
#if BOOST_OS_WINDOWS
|
||||
#include <bluetoothapis.h>
|
||||
#endif
|
||||
#if BOOST_OS_LINUX
|
||||
#include <bluetooth/bluetooth.h>
|
||||
#include <bluetooth/hci.h>
|
||||
#include <bluetooth/hci_lib.h>
|
||||
#include <input/api/Wiimote/l2cap/L2CapWiimote.h>
|
||||
#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);
|
||||
}
|
|
@ -17,7 +17,7 @@ private:
|
|||
Pairing,
|
||||
Finished,
|
||||
NoBluetoothAvailable,
|
||||
BluetoothFailed,
|
||||
SearchFailed,
|
||||
PairingFailed,
|
||||
BluetoothUnusable
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 ()
|
|
@ -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 <numbers>
|
||||
#include <queue>
|
||||
|
@ -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<std::shared_ptr<ControllerBase>> WiimoteControllerProvider::get_controllers()
|
||||
{
|
||||
m_connectedDeviceMutex.lock();
|
||||
auto devices = m_connectedDevices;
|
||||
m_connectedDeviceMutex.unlock();
|
||||
|
||||
std::scoped_lock lock(m_device_mutex);
|
||||
|
||||
std::queue<uint32> 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<WiimoteDevice> & 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<Wiimote>(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;
|
||||
}
|
||||
// Otherwise add them
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
lowestReplaceableIndex = i;
|
||||
}
|
||||
if (isDuplicate)
|
||||
continue;
|
||||
if (lowestReplaceableIndex != -1)
|
||||
m_wiimotes.replace(lowestReplaceableIndex, std::make_unique<Wiimote>(device));
|
||||
else
|
||||
m_wiimotes.push_back(std::make_unique<Wiimote>(device));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<ControllerBase>> result;
|
||||
result.reserve(m_wiimotes.size());
|
||||
for (size_t i = 0; i < m_wiimotes.size(); ++i)
|
||||
{
|
||||
result.emplace_back(std::make_shared<NativeWiimoteController>(i));
|
||||
|
@ -74,7 +83,7 @@ std::vector<std::shared_ptr<ControllerBase>> 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<WiimoteDevicePtr> 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<std::chrono::seconds>(now - lastCheck) > std::chrono::seconds(2))
|
||||
if (std::chrono::duration_cast<std::chrono::seconds>(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();
|
||||
|
||||
|
|
|
@ -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<WiimoteDevicePtr> 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);
|
||||
|
|
|
@ -9,8 +9,7 @@ public:
|
|||
virtual bool write_data(const std::vector<uint8>& data) = 0;
|
||||
virtual std::optional<std::vector<uint8_t>> 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<WiimoteDevice>;
|
||||
|
|
|
@ -47,8 +47,11 @@ std::vector<WiimoteDevicePtr> HidapiWiimote::get_devices() {
|
|||
return wiimote_devices;
|
||||
}
|
||||
|
||||
bool HidapiWiimote::operator==(WiimoteDevice& o) const {
|
||||
return static_cast<HidapiWiimote const&>(o).m_path == m_path;
|
||||
bool HidapiWiimote::operator==(const WiimoteDevice& rhs) const {
|
||||
auto other = dynamic_cast<const HidapiWiimote*>(&rhs);
|
||||
if (!other)
|
||||
return false;
|
||||
return m_path == other->m_path;
|
||||
}
|
||||
|
||||
HidapiWiimote::~HidapiWiimote() {
|
||||
|
|
|
@ -10,7 +10,7 @@ public:
|
|||
|
||||
bool write_data(const std::vector<uint8> &data) override;
|
||||
std::optional<std::vector<uint8>> read_data() override;
|
||||
bool operator==(WiimoteDevice& o) const override;
|
||||
bool operator==(const WiimoteDevice& o) const override;
|
||||
|
||||
static std::vector<WiimoteDevicePtr> get_devices();
|
||||
|
||||
|
@ -19,5 +19,3 @@ private:
|
|||
const std::string m_path;
|
||||
|
||||
};
|
||||
|
||||
using WiimoteDevice_t = HidapiWiimote;
|
|
@ -0,0 +1,148 @@
|
|||
#include "L2CapWiimote.h"
|
||||
#include <bluetooth/l2cap.h>
|
||||
|
||||
constexpr auto comparator = [](const bdaddr_t& a, const bdaddr_t& b) {
|
||||
return bacmp(&a, &b);
|
||||
};
|
||||
|
||||
static auto s_addresses = std::map<bdaddr_t, bool, decltype(comparator)>(comparator);
|
||||
static std::mutex s_addressMutex;
|
||||
|
||||
static bool AttemptConnect(int sockFd, const sockaddr_l2& addr)
|
||||
{
|
||||
auto res = connect(sockFd, reinterpret_cast<const sockaddr*>(&addr),
|
||||
sizeof(sockaddr_l2));
|
||||
if (res == 0)
|
||||
return true;
|
||||
return connect(sockFd, reinterpret_cast<const sockaddr*>(&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<WiimoteDevicePtr> L2CapWiimote::get_devices()
|
||||
{
|
||||
s_addressMutex.lock();
|
||||
std::vector<bdaddr_t> unconnected;
|
||||
for (const auto& [addr, connected] : s_addresses)
|
||||
{
|
||||
if (!connected)
|
||||
unconnected.push_back(addr);
|
||||
}
|
||||
s_addressMutex.unlock();
|
||||
|
||||
std::vector<WiimoteDevicePtr> 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<L2CapWiimote>(sendFd, recvFd, addr));
|
||||
|
||||
s_addressMutex.lock();
|
||||
s_addresses[addr] = true;
|
||||
s_addressMutex.unlock();
|
||||
}
|
||||
return outDevices;
|
||||
}
|
||||
|
||||
bool L2CapWiimote::write_data(const std::vector<uint8>& 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<std::vector<uint8>> L2CapWiimote::read_data()
|
||||
{
|
||||
uint8 buffer[23];
|
||||
const auto nBytes = recv(m_sendFd, buffer, 23, 0);
|
||||
|
||||
if (nBytes < 0 && errno == EWOULDBLOCK)
|
||||
return std::vector<uint8>{};
|
||||
// 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<const L2CapWiimote*>(&rhs);
|
||||
if (!mote)
|
||||
return false;
|
||||
return bacmp(&m_addr, &mote->m_addr) == 0;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
#include <input/api/Wiimote/WiimoteDevice.h>
|
||||
#include <bluetooth/bluetooth.h>
|
||||
|
||||
class L2CapWiimote : public WiimoteDevice
|
||||
{
|
||||
public:
|
||||
L2CapWiimote(int recvFd, int sendFd, bdaddr_t addr);
|
||||
~L2CapWiimote() override;
|
||||
|
||||
bool write_data(const std::vector<uint8>& data) override;
|
||||
std::optional<std::vector<uint8>> read_data() override;
|
||||
bool operator==(const WiimoteDevice& o) const override;
|
||||
|
||||
static void AddCandidateAddress(bdaddr_t addr);
|
||||
static std::vector<WiimoteDevicePtr> get_devices();
|
||||
private:
|
||||
int m_recvFd;
|
||||
int m_sendFd;
|
||||
bdaddr_t m_addr;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue