mirror of
https://github.com/cemu-project/Cemu.git
synced 2025-07-04 22:11:18 +12:00
Merge branch 'main' into metal
This commit is contained in:
commit
e8c7e9d093
53 changed files with 998 additions and 391 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
||||||
- name: "Install system dependencies"
|
- name: "Install system dependencies"
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq
|
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"
|
- name: "Setup cmake"
|
||||||
uses: jwlawson/actions-setup-cmake@v2
|
uses: jwlawson/actions-setup-cmake@v2
|
||||||
|
@ -96,7 +96,7 @@ jobs:
|
||||||
- name: "Install system dependencies"
|
- name: "Install system dependencies"
|
||||||
run: |
|
run: |
|
||||||
sudo apt update -qq
|
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"
|
- name: "Build AppImage"
|
||||||
run: |
|
run: |
|
||||||
|
|
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
|
### Dependencies
|
||||||
|
|
||||||
#### For Arch and derivatives:
|
#### 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:
|
#### 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.
|
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`
|
`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:
|
#### 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
|
### Build Cemu
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@ endif()
|
||||||
if (UNIX AND NOT APPLE)
|
if (UNIX AND NOT APPLE)
|
||||||
option(ENABLE_WAYLAND "Build with Wayland support" ON)
|
option(ENABLE_WAYLAND "Build with Wayland support" ON)
|
||||||
option(ENABLE_FERAL_GAMEMODE "Enables Feral Interactive GameMode Support" ON)
|
option(ENABLE_FERAL_GAMEMODE "Enables Feral Interactive GameMode Support" ON)
|
||||||
|
option(ENABLE_BLUEZ "Build with Bluez support" ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
|
@ -189,6 +190,12 @@ if (UNIX AND NOT APPLE)
|
||||||
endif()
|
endif()
|
||||||
find_package(GTK3 REQUIRED)
|
find_package(GTK3 REQUIRED)
|
||||||
|
|
||||||
|
if(ENABLE_BLUEZ)
|
||||||
|
find_package(bluez REQUIRED)
|
||||||
|
set(ENABLE_WIIMOTE ON)
|
||||||
|
add_compile_definitions(HAS_BLUEZ)
|
||||||
|
endif()
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ENABLE_VULKAN)
|
if (ENABLE_VULKAN)
|
||||||
|
|
BIN
bin/resources/ar/cemu.mo
Normal file
BIN
bin/resources/ar/cemu.mo
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
20
cmake/Findbluez.cmake
Normal file
20
cmake/Findbluez.cmake
Normal file
|
@ -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_ICON_FILE "cemu.icns")
|
||||||
set(MACOSX_BUNDLE_GUI_IDENTIFIER "info.cemu.Cemu")
|
set(MACOSX_BUNDLE_GUI_IDENTIFIER "info.cemu.Cemu")
|
||||||
set(MACOSX_BUNDLE_BUNDLE_NAME "Cemu")
|
set(MACOSX_BUNDLE_BUNDLE_NAME "Cemu")
|
||||||
set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${CMAKE_PROJECT_VERSION})
|
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}")
|
||||||
set(MACOSX_BUNDLE_BUNDLE_VERSION ${CMAKE_PROJECT_VERSION})
|
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_COPYRIGHT "Copyright © 2024 Cemu Project")
|
||||||
|
|
||||||
set(MACOSX_BUNDLE_CATEGORY "public.app-category.games")
|
set(MACOSX_BUNDLE_CATEGORY "public.app-category.games")
|
||||||
|
|
|
@ -594,6 +594,12 @@ set_property(TARGET CemuCafe PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CON
|
||||||
|
|
||||||
target_include_directories(CemuCafe PUBLIC "../")
|
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
|
target_link_libraries(CemuCafe PRIVATE
|
||||||
CemuAsm
|
CemuAsm
|
||||||
CemuAudio
|
CemuAudio
|
||||||
|
@ -609,7 +615,7 @@ target_link_libraries(CemuCafe PRIVATE
|
||||||
Boost::nowide
|
Boost::nowide
|
||||||
CURL::libcurl
|
CURL::libcurl
|
||||||
fmt::fmt
|
fmt::fmt
|
||||||
glslang::SPIRV
|
${glslang_target}
|
||||||
ih264d
|
ih264d
|
||||||
OpenSSL::Crypto
|
OpenSSL::Crypto
|
||||||
OpenSSL::SSL
|
OpenSSL::SSL
|
||||||
|
|
|
@ -347,7 +347,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
const auto preset_name = rules.FindOption("name");
|
const auto preset_name = rules.FindOption("name");
|
||||||
if (!preset_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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +371,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
}
|
}
|
||||||
catch (const std::exception & ex)
|
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"))
|
else if (boost::iequals(currentSectionName, "RAM"))
|
||||||
|
@ -385,7 +385,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
{
|
{
|
||||||
if (m_version <= 5)
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,12 +395,12 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
{
|
{
|
||||||
if (addrEnd <= addrStart)
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
else if ((addrStart & 0xFFF) != 0 || (addrEnd & 0xFFF) != 0)
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -410,7 +410,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
}
|
}
|
||||||
else
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,24 +424,32 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
std::unordered_map<std::string, std::vector<PresetPtr>> tmp_map;
|
std::unordered_map<std::string, std::vector<PresetPtr>> tmp_map;
|
||||||
|
|
||||||
// all vars must be defined in the default preset vars before
|
// 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())
|
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);
|
mismatchingPresetVars.emplace_back(presetEntry->name, presetVar.first);
|
||||||
throw std::exception();
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// overwrite var type with default var type
|
// overwrite var type with default var type
|
||||||
kv.second.first = it->second.first;
|
presetVar.second.first = it->second.first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!mismatchingPresetVars.empty())
|
||||||
|
{
|
||||||
|
cemuLog_log(LogType::Force, "Graphic pack \"{}\" contains preset variables which are not defined in the [Default] section:", GetNormalizedPathString());
|
||||||
|
for (const auto& [presetName, varName] : mismatchingPresetVars)
|
||||||
|
cemuLog_log(LogType::Force, "Preset: {} Variable: {}", presetName, varName);
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
|
||||||
// have first entry be default active for every category if no default= is set
|
// have first entry be default active for every category if no default= is set
|
||||||
for(auto entry : get_values(tmp_map))
|
for(auto entry : get_values(tmp_map))
|
||||||
{
|
{
|
||||||
|
@ -471,7 +479,7 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
auto& p2 = kv.second[i + 1];
|
auto& p2 = kv.second[i + 1];
|
||||||
if (p1->variables.size() != p2->variables.size())
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,14 +487,14 @@ GraphicPack2::GraphicPack2(fs::path rulesPath, IniParser& rules)
|
||||||
std::set<std::string> keys2(get_keys(p2->variables).begin(), get_keys(p2->variables).end());
|
std::set<std::string> keys2(get_keys(p2->variables).begin(), get_keys(p2->variables).end());
|
||||||
if (keys1 != keys2)
|
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();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(p1->is_default)
|
if(p1->is_default)
|
||||||
{
|
{
|
||||||
if(has_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;
|
p1->active = true;
|
||||||
has_default = true;
|
has_default = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -447,6 +447,34 @@ bool debugger_hasPatch(uint32 address)
|
||||||
return false;
|
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)
|
void debugger_stepInto(PPCInterpreter_t* hCPU, bool updateDebuggerWindow = true)
|
||||||
{
|
{
|
||||||
bool isRecEnabled = ppcRecompilerEnabled;
|
bool isRecEnabled = ppcRecompilerEnabled;
|
||||||
|
|
|
@ -114,6 +114,7 @@ void debugger_updateExecutionBreakpoint(uint32 address, bool forceRestore = fals
|
||||||
|
|
||||||
void debugger_createPatch(uint32 address, std::span<uint8> patchData);
|
void debugger_createPatch(uint32 address, std::span<uint8> patchData);
|
||||||
bool debugger_hasPatch(uint32 address);
|
bool debugger_hasPatch(uint32 address);
|
||||||
|
void debugger_removePatch(uint32 address);
|
||||||
|
|
||||||
void debugger_forceBreak(); // force breakpoint at the next possible instruction
|
void debugger_forceBreak(); // force breakpoint at the next possible instruction
|
||||||
bool debugger_isTrapped();
|
bool debugger_isTrapped();
|
||||||
|
|
|
@ -124,6 +124,7 @@ typedef struct
|
||||||
LattePerfStatCounter numGraphicPipelines;
|
LattePerfStatCounter numGraphicPipelines;
|
||||||
LattePerfStatCounter numImages;
|
LattePerfStatCounter numImages;
|
||||||
LattePerfStatCounter numImageViews;
|
LattePerfStatCounter numImageViews;
|
||||||
|
LattePerfStatCounter numSamplers;
|
||||||
LattePerfStatCounter numRenderPass;
|
LattePerfStatCounter numRenderPass;
|
||||||
LattePerfStatCounter numFramebuffer;
|
LattePerfStatCounter numFramebuffer;
|
||||||
|
|
||||||
|
|
|
@ -370,6 +370,8 @@ bool LatteDecompiler_IsALUTransInstruction(bool isOP3, uint32 opcode)
|
||||||
opcode == ALU_OP2_INST_LSHR_INT ||
|
opcode == ALU_OP2_INST_LSHR_INT ||
|
||||||
opcode == ALU_OP2_INST_MAX_INT ||
|
opcode == ALU_OP2_INST_MAX_INT ||
|
||||||
opcode == ALU_OP2_INST_MIN_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_FLOOR ||
|
||||||
opcode == ALU_OP2_INST_MOVA_INT ||
|
opcode == ALU_OP2_INST_MOVA_INT ||
|
||||||
opcode == ALU_OP2_INST_SETE_DX10 ||
|
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_SUB_INT:
|
||||||
case ALU_OP2_INST_MAX_INT:
|
case ALU_OP2_INST_MAX_INT:
|
||||||
case ALU_OP2_INST_MIN_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_SETE_INT:
|
||||||
case ALU_OP2_INST_SETGT_INT:
|
case ALU_OP2_INST_SETGT_INT:
|
||||||
case ALU_OP2_INST_SETGE_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 )
|
else if( aluInstruction->opcode == ALU_OP2_INST_ADD_INT )
|
||||||
_emitALUOperationBinary<LATTE_DECOMPILER_DTYPE_SIGNED_INT>(shaderContext, aluInstruction, " + ");
|
_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
|
// 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);
|
_emitInstructionOutputVariableName(shaderContext, aluInstruction);
|
||||||
if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT )
|
src->add(" = ");
|
||||||
src->add(" = max(");
|
_emitTypeConversionPrefix(shaderContext, opType, outputType);
|
||||||
|
if( aluInstruction->opcode == ALU_OP2_INST_MAX_INT || aluInstruction->opcode == ALU_OP2_INST_MAX_UINT )
|
||||||
|
src->add("max(");
|
||||||
else
|
else
|
||||||
src->add(" = min(");
|
src->add("min(");
|
||||||
_emitTypeConversionPrefix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType);
|
_emitOperandInputCode(shaderContext, aluInstruction, 0, opType);
|
||||||
_emitOperandInputCode(shaderContext, aluInstruction, 0, LATTE_DECOMPILER_DTYPE_SIGNED_INT);
|
|
||||||
src->add(", ");
|
src->add(", ");
|
||||||
_emitOperandInputCode(shaderContext, aluInstruction, 1, LATTE_DECOMPILER_DTYPE_SIGNED_INT);
|
_emitOperandInputCode(shaderContext, aluInstruction, 1, opType);
|
||||||
_emitTypeConversionSuffix(shaderContext, LATTE_DECOMPILER_DTYPE_SIGNED_INT, outputType);
|
_emitTypeConversionSuffix(shaderContext, opType, outputType);
|
||||||
src->add(");" _CRLF);
|
src->add(");" _CRLF);
|
||||||
}
|
}
|
||||||
else if( aluInstruction->opcode == ALU_OP2_INST_SUB_INT )
|
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_SUB_INT (0x035) // integer instruction
|
||||||
#define ALU_OP2_INST_MAX_INT (0x036) // integer instruction
|
#define ALU_OP2_INST_MAX_INT (0x036) // integer instruction
|
||||||
#define ALU_OP2_INST_MIN_INT (0x037) // 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_SETE_INT (0x03A) // integer instruction
|
||||||
#define ALU_OP2_INST_SETGT_INT (0x03B) // integer instruction
|
#define ALU_OP2_INST_SETGT_INT (0x03B) // integer instruction
|
||||||
#define ALU_OP2_INST_SETGE_INT (0x03C) // integer instruction
|
#define ALU_OP2_INST_SETGE_INT (0x03C) // integer instruction
|
||||||
|
|
|
@ -19,7 +19,7 @@ public:
|
||||||
|
|
||||||
virtual ~VKRMoveableRefCounter()
|
virtual ~VKRMoveableRefCounter()
|
||||||
{
|
{
|
||||||
cemu_assert_debug(refCount == 0);
|
cemu_assert_debug(m_refCount == 0);
|
||||||
|
|
||||||
// remove references
|
// remove references
|
||||||
#ifdef CEMU_DEBUG_ASSERT
|
#ifdef CEMU_DEBUG_ASSERT
|
||||||
|
@ -30,7 +30,11 @@ public:
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
for (auto itr : refs)
|
for (auto itr : refs)
|
||||||
itr->ref->refCount--;
|
{
|
||||||
|
itr->ref->m_refCount--;
|
||||||
|
if (itr->ref->m_refCount == 0)
|
||||||
|
itr->ref->RefCountReachedZero();
|
||||||
|
}
|
||||||
refs.clear();
|
refs.clear();
|
||||||
delete selfRef;
|
delete selfRef;
|
||||||
selfRef = nullptr;
|
selfRef = nullptr;
|
||||||
|
@ -41,8 +45,8 @@ public:
|
||||||
VKRMoveableRefCounter(VKRMoveableRefCounter&& rhs) noexcept
|
VKRMoveableRefCounter(VKRMoveableRefCounter&& rhs) noexcept
|
||||||
{
|
{
|
||||||
this->refs = std::move(rhs.refs);
|
this->refs = std::move(rhs.refs);
|
||||||
this->refCount = rhs.refCount;
|
this->m_refCount = rhs.m_refCount;
|
||||||
rhs.refCount = 0;
|
rhs.m_refCount = 0;
|
||||||
this->selfRef = rhs.selfRef;
|
this->selfRef = rhs.selfRef;
|
||||||
rhs.selfRef = nullptr;
|
rhs.selfRef = nullptr;
|
||||||
this->selfRef->ref = this;
|
this->selfRef->ref = this;
|
||||||
|
@ -57,7 +61,7 @@ public:
|
||||||
void addRef(VKRMoveableRefCounter* refTarget)
|
void addRef(VKRMoveableRefCounter* refTarget)
|
||||||
{
|
{
|
||||||
this->refs.emplace_back(refTarget->selfRef);
|
this->refs.emplace_back(refTarget->selfRef);
|
||||||
refTarget->refCount++;
|
refTarget->m_refCount++;
|
||||||
|
|
||||||
#ifdef CEMU_DEBUG_ASSERT
|
#ifdef CEMU_DEBUG_ASSERT
|
||||||
// add reverse ref
|
// add reverse ref
|
||||||
|
@ -68,16 +72,23 @@ public:
|
||||||
// methods to directly increment/decrement ref counter (for situations where no external object is available)
|
// methods to directly increment/decrement ref counter (for situations where no external object is available)
|
||||||
void incRef()
|
void incRef()
|
||||||
{
|
{
|
||||||
this->refCount++;
|
m_refCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
void decRef()
|
void decRef()
|
||||||
{
|
{
|
||||||
this->refCount--;
|
m_refCount--;
|
||||||
|
if (m_refCount == 0)
|
||||||
|
RefCountReachedZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int refCount{};
|
virtual void RefCountReachedZero()
|
||||||
|
{
|
||||||
|
// does nothing by default
|
||||||
|
}
|
||||||
|
|
||||||
|
int m_refCount{};
|
||||||
private:
|
private:
|
||||||
VKRMoveableRefCounterRef* selfRef;
|
VKRMoveableRefCounterRef* selfRef;
|
||||||
std::vector<VKRMoveableRefCounterRef*> refs;
|
std::vector<VKRMoveableRefCounterRef*> refs;
|
||||||
|
@ -88,7 +99,7 @@ private:
|
||||||
void moveObj(VKRMoveableRefCounter&& rhs)
|
void moveObj(VKRMoveableRefCounter&& rhs)
|
||||||
{
|
{
|
||||||
this->refs = std::move(rhs.refs);
|
this->refs = std::move(rhs.refs);
|
||||||
this->refCount = rhs.refCount;
|
this->m_refCount = rhs.m_refCount;
|
||||||
this->selfRef = rhs.selfRef;
|
this->selfRef = rhs.selfRef;
|
||||||
this->selfRef->ref = this;
|
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
|
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
|
class VKRObjectRenderPass : public VKRDestructibleObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -670,6 +670,8 @@ VulkanRenderer::~VulkanRenderer()
|
||||||
if (m_commandPool != VK_NULL_HANDLE)
|
if (m_commandPool != VK_NULL_HANDLE)
|
||||||
vkDestroyCommandPool(m_logicalDevice, m_commandPool, nullptr);
|
vkDestroyCommandPool(m_logicalDevice, m_commandPool, nullptr);
|
||||||
|
|
||||||
|
VKRObjectSampler::DestroyCache();
|
||||||
|
|
||||||
// destroy debug callback
|
// destroy debug callback
|
||||||
if (m_debugCallback)
|
if (m_debugCallback)
|
||||||
{
|
{
|
||||||
|
@ -3705,6 +3707,7 @@ void VulkanRenderer::AppendOverlayDebugInfo()
|
||||||
ImGui::Text("DS StorageBuf %u", performanceMonitor.vk.numDescriptorStorageBuffers.get());
|
ImGui::Text("DS StorageBuf %u", performanceMonitor.vk.numDescriptorStorageBuffers.get());
|
||||||
ImGui::Text("Images %u", performanceMonitor.vk.numImages.get());
|
ImGui::Text("Images %u", performanceMonitor.vk.numImages.get());
|
||||||
ImGui::Text("ImageView %u", performanceMonitor.vk.numImageViews.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("RenderPass %u", performanceMonitor.vk.numRenderPass.get());
|
||||||
ImGui::Text("Framebuffer %u", performanceMonitor.vk.numFramebuffer.get());
|
ImGui::Text("Framebuffer %u", performanceMonitor.vk.numFramebuffer.get());
|
||||||
m_spinlockDestructionQueue.lock();
|
m_spinlockDestructionQueue.lock();
|
||||||
|
@ -3750,7 +3753,7 @@ void VKRDestructibleObject::flagForCurrentCommandBuffer()
|
||||||
|
|
||||||
bool VKRDestructibleObject::canDestroy()
|
bool VKRDestructibleObject::canDestroy()
|
||||||
{
|
{
|
||||||
if (refCount > 0)
|
if (m_refCount > 0)
|
||||||
return false;
|
return false;
|
||||||
return VulkanRenderer::GetInstance()->HasCommandBufferFinished(m_lastCmdBufferId);
|
return VulkanRenderer::GetInstance()->HasCommandBufferFinished(m_lastCmdBufferId);
|
||||||
}
|
}
|
||||||
|
@ -3791,6 +3794,111 @@ VKRObjectTextureView::~VKRObjectTextureView()
|
||||||
performanceMonitor.vk.numImageViews.decrement();
|
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)
|
VKRObjectRenderPass::VKRObjectRenderPass(AttachmentInfo_t& attachmentInfo, sint32 colorAttachmentCount)
|
||||||
{
|
{
|
||||||
// generate helper hash for pipeline state
|
// generate helper hash for pipeline state
|
||||||
|
|
|
@ -727,7 +727,6 @@ VkDescriptorSetInfo* VulkanRenderer::draw_getOrCreateDescriptorSet(PipelineInfo*
|
||||||
|
|
||||||
VkSamplerCustomBorderColorCreateInfoEXT samplerCustomBorderColor{};
|
VkSamplerCustomBorderColorCreateInfoEXT samplerCustomBorderColor{};
|
||||||
|
|
||||||
VkSampler sampler;
|
|
||||||
VkSamplerCreateInfo samplerInfo{};
|
VkSamplerCreateInfo samplerInfo{};
|
||||||
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||||
|
|
||||||
|
@ -899,10 +898,9 @@ VkDescriptorSetInfo* VulkanRenderer::draw_getOrCreateDescriptorSet(PipelineInfo*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
VKRObjectSampler* samplerObj = VKRObjectSampler::GetOrCreateSampler(&samplerInfo);
|
||||||
if (vkCreateSampler(m_logicalDevice, &samplerInfo, nullptr, &sampler) != VK_SUCCESS)
|
vkObjDS->addRef(samplerObj);
|
||||||
UnrecoverableError("Failed to create texture sampler");
|
info.sampler = samplerObj->GetSampler();
|
||||||
info.sampler = sampler;
|
|
||||||
textureArray.emplace_back(info);
|
textureArray.emplace_back(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ typedef struct
|
||||||
/* +0x34 */ uint32be ukn34;
|
/* +0x34 */ uint32be ukn34;
|
||||||
/* +0x38 */ uint32be ukn38;
|
/* +0x38 */ uint32be ukn38;
|
||||||
/* +0x3C */ uint32be ukn3C;
|
/* +0x3C */ uint32be ukn3C;
|
||||||
/* +0x40 */ uint32be toolkitVersion;
|
/* +0x40 */ uint32be minimumToolkitVersion;
|
||||||
/* +0x44 */ uint32be ukn44;
|
/* +0x44 */ uint32be ukn44;
|
||||||
/* +0x48 */ uint32be ukn48;
|
/* +0x48 */ uint32be ukn48;
|
||||||
/* +0x4C */ uint32be ukn4C;
|
/* +0x4C */ uint32be ukn4C;
|
||||||
|
|
|
@ -181,7 +181,7 @@ namespace camera
|
||||||
sint32 CAMInit(uint32 cameraId, CAMInitInfo_t* camInitInfo, uint32be* error)
|
sint32 CAMInit(uint32 cameraId, CAMInitInfo_t* camInitInfo, uint32be* error)
|
||||||
{
|
{
|
||||||
CameraInstance* camInstance = new CameraInstance(camInitInfo->width, camInitInfo->height, camInitInfo->handlerFuncPtr);
|
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);
|
std::unique_lock<std::recursive_mutex> _lock(g_mutex_camera);
|
||||||
if (g_cameraCounter == 0)
|
if (g_cameraCounter == 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -156,12 +156,22 @@ namespace coreinit
|
||||||
return ¤tThread->crt.eh_mem_manage;
|
return ¤tThread->crt.eh_mem_manage;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* __gh_errno_ptr()
|
sint32be* __gh_errno_ptr()
|
||||||
{
|
{
|
||||||
OSThread_t* currentThread = coreinit::OSGetCurrentThread();
|
OSThread_t* currentThread = coreinit::OSGetCurrentThread();
|
||||||
return ¤tThread->context.ghs_errno;
|
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()
|
void* __get_eh_store_globals()
|
||||||
{
|
{
|
||||||
OSThread_t* currentThread = coreinit::OSGetCurrentThread();
|
OSThread_t* currentThread = coreinit::OSGetCurrentThread();
|
||||||
|
@ -272,6 +282,8 @@ namespace coreinit
|
||||||
cafeExportRegister("coreinit", __get_eh_globals, LogType::Placeholder);
|
cafeExportRegister("coreinit", __get_eh_globals, LogType::Placeholder);
|
||||||
cafeExportRegister("coreinit", __get_eh_mem_manage, LogType::Placeholder);
|
cafeExportRegister("coreinit", __get_eh_mem_manage, LogType::Placeholder);
|
||||||
cafeExportRegister("coreinit", __gh_errno_ptr, 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, LogType::Placeholder);
|
||||||
cafeExportRegister("coreinit", __get_eh_store_globals_tdeh, LogType::Placeholder);
|
cafeExportRegister("coreinit", __get_eh_store_globals_tdeh, LogType::Placeholder);
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,9 @@ namespace coreinit
|
||||||
{
|
{
|
||||||
void PrepareGHSRuntime();
|
void PrepareGHSRuntime();
|
||||||
|
|
||||||
|
sint32be* __gh_errno_ptr();
|
||||||
|
void __gh_set_errno(sint32 errNo);
|
||||||
|
sint32 __gh_get_errno();
|
||||||
|
|
||||||
void InitializeGHS();
|
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
|
thread->requestFlags = (OSThread_t::REQUEST_FLAG_BIT)(thread->requestFlags & OSThread_t::REQUEST_FLAG_CANCEL); // remove all flags except cancel flag
|
||||||
|
|
||||||
// update total cycles
|
// update total cycles
|
||||||
uint64 remainingCycles = std::min((uint64)hCPU->remainingCycles, (uint64)thread->quantumTicks);
|
sint64 executedCycles = (sint64)thread->quantumTicks - (sint64)hCPU->remainingCycles;
|
||||||
uint64 executedCycles = thread->quantumTicks - remainingCycles;
|
executedCycles = std::max<sint64>(executedCycles, 0);
|
||||||
if (executedCycles < hCPU->skippedCycles)
|
if (executedCycles < (sint64)hCPU->skippedCycles)
|
||||||
executedCycles = 0;
|
executedCycles = 0;
|
||||||
else
|
else
|
||||||
executedCycles -= hCPU->skippedCycles;
|
executedCycles -= hCPU->skippedCycles;
|
||||||
thread->totalCycles += executedCycles;
|
thread->totalCycles += (uint64)executedCycles;
|
||||||
// store context and set current thread to null
|
// store context and set current thread to null
|
||||||
__OSThreadStoreContext(hCPU, thread);
|
__OSThreadStoreContext(hCPU, thread);
|
||||||
OSSetCurrentThread(OSGetCoreId(), nullptr);
|
OSSetCurrentThread(OSGetCoreId(), nullptr);
|
||||||
|
|
|
@ -38,7 +38,7 @@ struct OSContext_t
|
||||||
/* +0x1E0 */ uint64be fp_ps1[32];
|
/* +0x1E0 */ uint64be fp_ps1[32];
|
||||||
/* +0x2E0 */ uint64be coretime[3];
|
/* +0x2E0 */ uint64be coretime[3];
|
||||||
/* +0x2F8 */ uint64be starttime;
|
/* +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;
|
/* +0x304 */ uint32be affinity;
|
||||||
/* +0x308 */ uint32be upmc1;
|
/* +0x308 */ uint32be upmc1;
|
||||||
/* +0x30C */ uint32be upmc2;
|
/* +0x30C */ uint32be upmc2;
|
||||||
|
|
|
@ -87,6 +87,11 @@ namespace GX2
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GX2RSetBufferName(GX2RBuffer* buffer, const char* name)
|
||||||
|
{
|
||||||
|
// no-op in production builds
|
||||||
|
}
|
||||||
|
|
||||||
void* GX2RLockBufferEx(GX2RBuffer* buffer, uint32 resFlags)
|
void* GX2RLockBufferEx(GX2RBuffer* buffer, uint32 resFlags)
|
||||||
{
|
{
|
||||||
return buffer->GetPtr();
|
return buffer->GetPtr();
|
||||||
|
@ -226,6 +231,7 @@ namespace GX2
|
||||||
cafeExportRegister("gx2", GX2RCreateBufferUserMemory, LogType::GX2);
|
cafeExportRegister("gx2", GX2RCreateBufferUserMemory, LogType::GX2);
|
||||||
cafeExportRegister("gx2", GX2RDestroyBufferEx, LogType::GX2);
|
cafeExportRegister("gx2", GX2RDestroyBufferEx, LogType::GX2);
|
||||||
cafeExportRegister("gx2", GX2RBufferExists, LogType::GX2);
|
cafeExportRegister("gx2", GX2RBufferExists, LogType::GX2);
|
||||||
|
cafeExportRegister("gx2", GX2RSetBufferName, LogType::GX2);
|
||||||
cafeExportRegister("gx2", GX2RLockBufferEx, LogType::GX2);
|
cafeExportRegister("gx2", GX2RLockBufferEx, LogType::GX2);
|
||||||
cafeExportRegister("gx2", GX2RUnlockBufferEx, LogType::GX2);
|
cafeExportRegister("gx2", GX2RUnlockBufferEx, LogType::GX2);
|
||||||
cafeExportRegister("gx2", GX2RInvalidateBuffer, LogType::GX2);
|
cafeExportRegister("gx2", GX2RInvalidateBuffer, LogType::GX2);
|
||||||
|
|
|
@ -421,7 +421,7 @@ namespace GX2
|
||||||
{
|
{
|
||||||
if(aluRegisterOffset&0x8000)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if((aluRegisterOffset+sizeInU32s) > 0x400)
|
if((aluRegisterOffset+sizeInU32s) > 0x400)
|
||||||
|
|
|
@ -675,6 +675,7 @@ namespace nsyshid
|
||||||
|
|
||||||
figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13);
|
figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13);
|
||||||
m_figureAddedRemovedResponses.push(figureChangeResponse);
|
m_figureAddedRemovedResponses.push(figureChangeResponse);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DimensionsUSB::CancelRemove(uint8 index)
|
bool DimensionsUSB::CancelRemove(uint8 index)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include "Cafe/OS/libs/coreinit/coreinit_Thread.h"
|
#include "Cafe/OS/libs/coreinit/coreinit_Thread.h"
|
||||||
#include "Cafe/IOSU/legacy/iosu_crypto.h"
|
#include "Cafe/IOSU/legacy/iosu_crypto.h"
|
||||||
#include "Cafe/OS/libs/coreinit/coreinit_Time.h"
|
#include "Cafe/OS/libs/coreinit/coreinit_Time.h"
|
||||||
|
#include "Cafe/OS/libs/coreinit/coreinit_GHS.h"
|
||||||
|
|
||||||
#include "Common/socket.h"
|
#include "Common/socket.h"
|
||||||
|
|
||||||
|
@ -117,20 +118,14 @@ void nsysnetExport_socket_lib_finish(PPCInterpreter_t* hCPU)
|
||||||
osLib_returnFromFunction(hCPU, 0); // 0 -> Success
|
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)
|
void _setSockError(sint32 errCode)
|
||||||
{
|
{
|
||||||
*(uint32be*)__gh_errno_ptr() = (uint32)errCode;
|
coreinit::__gh_set_errno(errCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
sint32 _getSockError()
|
sint32 _getSockError()
|
||||||
{
|
{
|
||||||
return (sint32)*(uint32be*)__gh_errno_ptr();
|
return coreinit::__gh_get_errno();
|
||||||
}
|
}
|
||||||
|
|
||||||
// error translation modes for _translateError
|
// error translation modes for _translateError
|
||||||
|
|
|
@ -198,14 +198,20 @@ bool ActiveSettings::ShaderPreventInfiniteLoopsEnabled()
|
||||||
{
|
{
|
||||||
const uint64 titleId = CafeSystem::GetForegroundTitleId();
|
const uint64 titleId = CafeSystem::GetForegroundTitleId();
|
||||||
// workaround for NSMBU (and variants) having a bug where shaders can get stuck in infinite loops
|
// 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 ||
|
return /* NSMBU JP */ titleId == 0x0005000010101C00 ||
|
||||||
/* NSMBU US */ titleId == 0x0005000010101D00 ||
|
/* NSMBU US */ titleId == 0x0005000010101D00 ||
|
||||||
/* NSMBU EU */ titleId == 0x0005000010101E00 ||
|
/* NSMBU EU */ titleId == 0x0005000010101E00 ||
|
||||||
/* NSMBU+L US */ titleId == 0x000500001014B700 ||
|
/* NSMBU+L US */ titleId == 0x000500001014B700 ||
|
||||||
/* NSMBU+L EU */ titleId == 0x000500001014B800 ||
|
/* NSMBU+L EU */ titleId == 0x000500001014B800 ||
|
||||||
/* NSLU US */ titleId == 0x0005000010142300 ||
|
/* 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()
|
bool ActiveSettings::FlushGPUCacheOnSwap()
|
||||||
|
|
|
@ -75,6 +75,8 @@ add_library(CemuGui
|
||||||
input/InputAPIAddWindow.h
|
input/InputAPIAddWindow.h
|
||||||
input/InputSettings2.cpp
|
input/InputSettings2.cpp
|
||||||
input/InputSettings2.h
|
input/InputSettings2.h
|
||||||
|
input/PairingDialog.cpp
|
||||||
|
input/PairingDialog.h
|
||||||
input/panels/ClassicControllerInputPanel.cpp
|
input/panels/ClassicControllerInputPanel.cpp
|
||||||
input/panels/ClassicControllerInputPanel.h
|
input/panels/ClassicControllerInputPanel.h
|
||||||
input/panels/InputPanel.cpp
|
input/panels/InputPanel.cpp
|
||||||
|
@ -97,8 +99,6 @@ add_library(CemuGui
|
||||||
MemorySearcherTool.h
|
MemorySearcherTool.h
|
||||||
PadViewFrame.cpp
|
PadViewFrame.cpp
|
||||||
PadViewFrame.h
|
PadViewFrame.h
|
||||||
PairingDialog.cpp
|
|
||||||
PairingDialog.h
|
|
||||||
TitleManager.cpp
|
TitleManager.cpp
|
||||||
TitleManager.h
|
TitleManager.h
|
||||||
EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp
|
EmulatedUSBDevices/EmulatedUSBDeviceFrame.cpp
|
||||||
|
|
|
@ -234,6 +234,12 @@ void CemuApp::InitializeExistingMLCOrFail(fs::path mlc)
|
||||||
g_config.Save();
|
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()
|
bool CemuApp::OnInit()
|
||||||
|
@ -508,6 +514,13 @@ bool CemuApp::CreateDefaultMLCFiles(const fs::path& mlc)
|
||||||
file.flush();
|
file.flush();
|
||||||
file.close();
|
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)
|
catch (const std::exception& ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
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);
|
fmt::format("{}|*.zip", _("Exported save entry (*.zip)")), wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
||||||
if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().IsEmpty())
|
if (path_dialog.ShowModal() != wxID_OK || path_dialog.GetPath().IsEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -64,6 +64,7 @@ wxBEGIN_EVENT_TABLE(DebuggerWindow2, wxFrame)
|
||||||
EVT_COMMAND(wxID_ANY, wxEVT_RUN, DebuggerWindow2::OnRunProgram)
|
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_LOADED, DebuggerWindow2::OnNotifyModuleLoaded)
|
||||||
EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded)
|
EVT_COMMAND(wxID_ANY, wxEVT_NOTIFY_MODULE_UNLOADED, DebuggerWindow2::OnNotifyModuleUnloaded)
|
||||||
|
EVT_COMMAND(wxID_ANY, wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, DebuggerWindow2::OnDisasmCtrlGotoAddress)
|
||||||
// file menu
|
// file menu
|
||||||
EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit)
|
EVT_MENU(MENU_ID_FILE_EXIT, DebuggerWindow2::OnExit)
|
||||||
// window
|
// window
|
||||||
|
@ -383,6 +384,12 @@ void DebuggerWindow2::OnMoveIP(wxCommandEvent& event)
|
||||||
m_disasm_ctrl->CenterOffset(ip);
|
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)
|
void DebuggerWindow2::OnParentMove(const wxPoint& main_position, const wxSize& main_size)
|
||||||
{
|
{
|
||||||
m_main_position = main_position;
|
m_main_position = main_position;
|
||||||
|
@ -416,7 +423,7 @@ void DebuggerWindow2::OnNotifyModuleLoaded(wxCommandEvent& event)
|
||||||
|
|
||||||
void DebuggerWindow2::OnNotifyModuleUnloaded(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);
|
SaveModuleStorage(module, true);
|
||||||
m_module_window->OnGameLoaded();
|
m_module_window->OnGameLoaded();
|
||||||
m_symbol_window->OnGameLoaded();
|
m_symbol_window->OnGameLoaded();
|
||||||
|
@ -659,7 +666,7 @@ void DebuggerWindow2::CreateMenuBar()
|
||||||
|
|
||||||
void DebuggerWindow2::UpdateModuleLabel(uint32 address)
|
void DebuggerWindow2::UpdateModuleLabel(uint32 address)
|
||||||
{
|
{
|
||||||
if(address == 0)
|
if (address == 0)
|
||||||
address = m_disasm_ctrl->GetViewBaseAddress();
|
address = m_disasm_ctrl->GetViewBaseAddress();
|
||||||
|
|
||||||
RPLModule* module = RPLLoader_FindModuleByCodeAddr(address);
|
RPLModule* module = RPLLoader_FindModuleByCodeAddr(address);
|
||||||
|
|
|
@ -86,6 +86,8 @@ private:
|
||||||
void OnMoveIP(wxCommandEvent& event);
|
void OnMoveIP(wxCommandEvent& event);
|
||||||
void OnNotifyModuleLoaded(wxCommandEvent& event);
|
void OnNotifyModuleLoaded(wxCommandEvent& event);
|
||||||
void OnNotifyModuleUnloaded(wxCommandEvent& event);
|
void OnNotifyModuleUnloaded(wxCommandEvent& event);
|
||||||
|
// events from DisasmCtrl
|
||||||
|
void OnDisasmCtrlGotoAddress(wxCommandEvent& event);
|
||||||
|
|
||||||
void CreateMenuBar();
|
void CreateMenuBar();
|
||||||
void UpdateModuleLabel(uint32 address = 0);
|
void UpdateModuleLabel(uint32 address = 0);
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
#include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h"
|
#include "Cafe/HW/Espresso/Debugger/DebugSymbolStorage.h"
|
||||||
#include <wx/mstream.h> // for wxMemoryInputStream
|
#include <wx/mstream.h> // for wxMemoryInputStream
|
||||||
|
|
||||||
|
wxDEFINE_EVENT(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS, wxCommandEvent);
|
||||||
|
|
||||||
#define MAX_SYMBOL_LEN (120)
|
#define MAX_SYMBOL_LEN (120)
|
||||||
|
|
||||||
#define COLOR_DEBUG_ACTIVE_BP 0xFFFFA0FF
|
#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);
|
auto tooltip_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString), 0, wxALL, 5);
|
tooltip_sizer->Add(new wxStaticText(m_tooltip_window, wxID_ANY, wxEmptyString), 0, wxALL, 5);
|
||||||
m_tooltip_window->SetSizer(tooltip_sizer);
|
m_tooltip_window->SetSizer(tooltip_sizer);
|
||||||
|
|
||||||
|
Bind(wxEVT_MENU, &DisasmCtrl::OnContextMenuEntryClicked, this, IDContextMenu_ToggleBreakpoint, IDContextMenu_Last);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisasmCtrl::Init()
|
void DisasmCtrl::Init()
|
||||||
|
@ -662,29 +666,67 @@ void DisasmCtrl::CopyToClipboard(std::string text) {
|
||||||
#endif
|
#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)
|
void DisasmCtrl::OnContextMenu(const wxPoint& position, uint32 line)
|
||||||
{
|
{
|
||||||
wxPoint pos = position;
|
|
||||||
auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height);
|
auto optVirtualAddress = LinePixelPosToAddress(position.y - GetViewStart().y * m_line_height);
|
||||||
if (!optVirtualAddress)
|
if (!optVirtualAddress)
|
||||||
return;
|
return;
|
||||||
MPTR virtualAddress = *optVirtualAddress;
|
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
|
void DisasmCtrl::OnContextMenuEntryClicked(wxCommandEvent& event)
|
||||||
if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE)
|
{
|
||||||
|
switch(event.GetId())
|
||||||
{
|
{
|
||||||
CopyToClipboard(fmt::format("{:#10x}", virtualAddress));
|
case IDContextMenu_ToggleBreakpoint:
|
||||||
return;
|
{
|
||||||
}
|
debugger_toggleExecuteBreakpoint(m_contextMenuAddress);
|
||||||
else if (pos.x <= OFFSET_ADDRESS + OFFSET_ADDRESS_RELATIVE + OFFSET_DISASSEMBLY)
|
wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE);
|
||||||
{
|
wxPostEvent(this->m_parent, evt);
|
||||||
// double-clicked on disassembly (operation and operand data)
|
break;
|
||||||
return;
|
}
|
||||||
}
|
case IDContextMenu_RestoreOriginalInstructions:
|
||||||
else
|
{
|
||||||
{
|
debugger_removePatch(m_contextMenuAddress);
|
||||||
// comment
|
wxCommandEvent evt(wxEVT_BREAKPOINT_CHANGE); // This also refreshes the disassembly view
|
||||||
return;
|
wxPostEvent(this->m_parent, evt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IDContextMenu_CopyAddress:
|
||||||
|
{
|
||||||
|
CopyToClipboard(fmt::format("{:#10x}", m_contextMenuAddress));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IDContextMenu_CopyUnrelocatedAddress:
|
||||||
|
{
|
||||||
|
uint32 unrelocatedAddress = GetUnrelocatedAddress(m_contextMenuAddress);
|
||||||
|
CopyToClipboard(fmt::format("{:#10x}", unrelocatedAddress));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
UNREACHABLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,7 +764,6 @@ std::optional<MPTR> DisasmCtrl::LinePixelPosToAddress(sint32 posY)
|
||||||
if (posY < 0)
|
if (posY < 0)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
|
|
||||||
sint32 lineIndex = posY / m_line_height;
|
sint32 lineIndex = posY / m_line_height;
|
||||||
if (lineIndex >= m_lineToAddress.size())
|
if (lineIndex >= m_lineToAddress.size())
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
@ -751,8 +792,6 @@ void DisasmCtrl::CenterOffset(uint32 offset)
|
||||||
|
|
||||||
m_active_line = line;
|
m_active_line = line;
|
||||||
RefreshLine(m_active_line);
|
RefreshLine(m_active_line);
|
||||||
|
|
||||||
debug_printf("scroll to %x\n", debuggerState.debugSession.instructionPointer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisasmCtrl::GoToAddressDialog()
|
void DisasmCtrl::GoToAddressDialog()
|
||||||
|
@ -765,6 +804,10 @@ void DisasmCtrl::GoToAddressDialog()
|
||||||
auto value = goto_dialog.GetValue().ToStdString();
|
auto value = goto_dialog.GetValue().ToStdString();
|
||||||
std::transform(value.begin(), value.end(), value.begin(), tolower);
|
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);
|
debugger_addParserSymbols(parser);
|
||||||
|
|
||||||
// try to parse expression as hex value first (it should interpret 1234 as 0x1234, not 1234)
|
// 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);
|
const auto result = (uint32)parser.Evaluate("0x"+value);
|
||||||
m_lastGotoTarget = result;
|
m_lastGotoTarget = result;
|
||||||
CenterOffset(result);
|
CenterOffset(result);
|
||||||
|
wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS);
|
||||||
|
evt.SetExtraLong(static_cast<long>(result));
|
||||||
|
wxPostEvent(GetParent(), evt);
|
||||||
}
|
}
|
||||||
else if (parser.IsConstantExpression(value))
|
else if (parser.IsConstantExpression(value))
|
||||||
{
|
{
|
||||||
const auto result = (uint32)parser.Evaluate(value);
|
const auto result = (uint32)parser.Evaluate(value);
|
||||||
m_lastGotoTarget = result;
|
m_lastGotoTarget = result;
|
||||||
CenterOffset(result);
|
CenterOffset(result);
|
||||||
|
wxCommandEvent evt(wxEVT_DISASMCTRL_NOTIFY_GOTO_ADDRESS);
|
||||||
|
evt.SetExtraLong(static_cast<long>(result));
|
||||||
|
wxPostEvent(GetParent(), evt);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
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);
|
const auto _ = (uint32)parser.Evaluate(value);
|
||||||
}
|
}
|
||||||
catch (const std::exception& ex)
|
catch (const std::exception& ex)
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "gui/components/TextList.h"
|
#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
|
class DisasmCtrl : public TextList
|
||||||
{
|
{
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
IDContextMenu_ToggleBreakpoint = wxID_HIGHEST + 1,
|
||||||
|
IDContextMenu_RestoreOriginalInstructions,
|
||||||
|
IDContextMenu_CopyAddress,
|
||||||
|
IDContextMenu_CopyUnrelocatedAddress,
|
||||||
|
IDContextMenu_Last
|
||||||
|
};
|
||||||
public:
|
public:
|
||||||
|
|
||||||
DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style);
|
DisasmCtrl(wxWindow* parent, const wxWindowID& id, const wxPoint& pos, const wxSize& size, long style);
|
||||||
|
|
||||||
void Init();
|
void Init();
|
||||||
|
@ -26,6 +37,7 @@ protected:
|
||||||
void OnKeyPressed(sint32 key_code, const wxPoint& position) override;
|
void OnKeyPressed(sint32 key_code, const wxPoint& position) override;
|
||||||
void OnMouseDClick(const wxPoint& position, uint32 line) override;
|
void OnMouseDClick(const wxPoint& position, uint32 line) override;
|
||||||
void OnContextMenu(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;
|
bool OnShowTooltip(const wxPoint& position, uint32 line) override;
|
||||||
void ScrollWindow(int dx, int dy, const wxRect* prect) 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_mouse_line, m_mouse_line_drawn;
|
||||||
sint32 m_active_line;
|
sint32 m_active_line;
|
||||||
uint32 m_lastGotoTarget{};
|
uint32 m_lastGotoTarget{};
|
||||||
|
uint32 m_contextMenuAddress{};
|
||||||
// code region info
|
// code region info
|
||||||
uint32 currentCodeRegionStart;
|
uint32 currentCodeRegionStart;
|
||||||
uint32 currentCodeRegionEnd;
|
uint32 currentCodeRegionEnd;
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
#include "gui/input/InputAPIAddWindow.h"
|
#include "gui/input/InputAPIAddWindow.h"
|
||||||
#include "input/ControllerFactory.h"
|
#include "input/ControllerFactory.h"
|
||||||
|
|
||||||
|
#include "gui/input/PairingDialog.h"
|
||||||
|
|
||||||
#include "gui/input/panels/VPADInputPanel.h"
|
#include "gui/input/panels/VPADInputPanel.h"
|
||||||
#include "gui/input/panels/ProControllerInputPanel.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;
|
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
|
// controller
|
||||||
auto* controller_bttns = new wxBoxSizer(wxHORIZONTAL);
|
auto* controller_bttns = new wxBoxSizer(wxHORIZONTAL);
|
||||||
auto* settings = new wxButton(page, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
|
auto* settings = new wxButton(page, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
|
||||||
|
|
300
src/gui/input/PairingDialog.cpp
Normal file
300
src/gui/input/PairingDialog.cpp
Normal file
|
@ -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,
|
Pairing,
|
||||||
Finished,
|
Finished,
|
||||||
NoBluetoothAvailable,
|
NoBluetoothAvailable,
|
||||||
BluetoothFailed,
|
SearchFailed,
|
||||||
PairingFailed,
|
PairingFailed,
|
||||||
BluetoothUnusable
|
BluetoothUnusable
|
||||||
};
|
};
|
|
@ -12,7 +12,6 @@
|
||||||
#include "input/emulated/WiimoteController.h"
|
#include "input/emulated/WiimoteController.h"
|
||||||
#include "gui/helpers/wxHelpers.h"
|
#include "gui/helpers/wxHelpers.h"
|
||||||
#include "gui/components/wxInputDraw.h"
|
#include "gui/components/wxInputDraw.h"
|
||||||
#include "gui/PairingDialog.h"
|
|
||||||
|
|
||||||
constexpr WiimoteController::ButtonId g_kFirstColumnItems[] =
|
constexpr WiimoteController::ButtonId g_kFirstColumnItems[] =
|
||||||
{
|
{
|
||||||
|
@ -40,11 +39,6 @@ WiimoteInputPanel::WiimoteInputPanel(wxWindow* parent)
|
||||||
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
|
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
auto* horiz_main_sizer = new wxBoxSizer(wxHORIZONTAL);
|
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);
|
auto* extensions_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
horiz_main_sizer->Add(extensions_sizer, wxSizerFlags(0).Align(wxALIGN_CENTER_VERTICAL));
|
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());
|
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.cpp
|
||||||
api/Wiimote/hidapi/HidapiWiimote.h
|
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 ()
|
endif ()
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,3 +102,8 @@ endif()
|
||||||
if (ENABLE_WXWIDGETS)
|
if (ENABLE_WXWIDGETS)
|
||||||
target_link_libraries(CemuInput PRIVATE wx::base wx::core)
|
target_link_libraries(CemuInput PRIVATE wx::base wx::core)
|
||||||
endif()
|
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/NativeWiimoteController.h"
|
||||||
#include "input/api/Wiimote/WiimoteMessages.h"
|
#include "input/api/Wiimote/WiimoteMessages.h"
|
||||||
|
|
||||||
|
#ifdef HAS_HIDAPI
|
||||||
#include "input/api/Wiimote/hidapi/HidapiWiimote.h"
|
#include "input/api/Wiimote/hidapi/HidapiWiimote.h"
|
||||||
|
#endif
|
||||||
|
#ifdef HAS_BLUEZ
|
||||||
|
#include "input/api/Wiimote/l2cap/L2CapWiimote.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <numbers>
|
#include <numbers>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
@ -12,6 +17,7 @@ WiimoteControllerProvider::WiimoteControllerProvider()
|
||||||
{
|
{
|
||||||
m_reader_thread = std::thread(&WiimoteControllerProvider::reader_thread, this);
|
m_reader_thread = std::thread(&WiimoteControllerProvider::reader_thread, this);
|
||||||
m_writer_thread = std::thread(&WiimoteControllerProvider::writer_thread, this);
|
m_writer_thread = std::thread(&WiimoteControllerProvider::writer_thread, this);
|
||||||
|
m_connectionThread = std::thread(&WiimoteControllerProvider::connectionThread, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
WiimoteControllerProvider::~WiimoteControllerProvider()
|
WiimoteControllerProvider::~WiimoteControllerProvider()
|
||||||
|
@ -21,48 +27,51 @@ WiimoteControllerProvider::~WiimoteControllerProvider()
|
||||||
m_running = false;
|
m_running = false;
|
||||||
m_writer_thread.join();
|
m_writer_thread.join();
|
||||||
m_reader_thread.join();
|
m_reader_thread.join();
|
||||||
|
m_connectionThread.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::shared_ptr<ControllerBase>> WiimoteControllerProvider::get_controllers()
|
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::scoped_lock lock(m_device_mutex);
|
||||||
|
|
||||||
std::queue<uint32> disconnected_wiimote_indices;
|
for (auto& device : devices)
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
if (!valid_new_device(device))
|
const auto writeable = device->write_data({kStatusRequest, 0x00});
|
||||||
|
if (!writeable)
|
||||||
continue;
|
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;
|
||||||
// Otherwise add them
|
for (ssize_t i = m_wiimotes.size() - 1; i >= 0; --i)
|
||||||
else {
|
{
|
||||||
m_wiimotes.push_back(std::make_unique<Wiimote>(device));
|
const auto& wiimoteDevice = m_wiimotes[i].device;
|
||||||
}
|
if (wiimoteDevice)
|
||||||
|
{
|
||||||
|
if (*wiimoteDevice == *device)
|
||||||
|
{
|
||||||
|
isDuplicate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lowestReplaceableIndex = i;
|
||||||
|
}
|
||||||
|
if (isDuplicate)
|
||||||
|
continue;
|
||||||
|
if (lowestReplaceableIndex != -1)
|
||||||
|
m_wiimotes.replace(lowestReplaceableIndex, std::make_unique<Wiimote>(device));
|
||||||
|
else
|
||||||
|
m_wiimotes.push_back(std::make_unique<Wiimote>(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::shared_ptr<ControllerBase>> result;
|
std::vector<std::shared_ptr<ControllerBase>> result;
|
||||||
|
result.reserve(m_wiimotes.size());
|
||||||
for (size_t i = 0; i < m_wiimotes.size(); ++i)
|
for (size_t i = 0; i < m_wiimotes.size(); ++i)
|
||||||
{
|
{
|
||||||
result.emplace_back(std::make_shared<NativeWiimoteController>(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)
|
bool WiimoteControllerProvider::is_connected(size_t index)
|
||||||
{
|
{
|
||||||
std::shared_lock lock(m_device_mutex);
|
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)
|
bool WiimoteControllerProvider::is_registered_device(size_t index)
|
||||||
|
@ -141,6 +150,30 @@ WiimoteControllerProvider::WiimoteState WiimoteControllerProvider::get_state(siz
|
||||||
return {};
|
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()
|
void WiimoteControllerProvider::reader_thread()
|
||||||
{
|
{
|
||||||
SetThreadName("Wiimote-reader");
|
SetThreadName("Wiimote-reader");
|
||||||
|
@ -148,7 +181,7 @@ void WiimoteControllerProvider::reader_thread()
|
||||||
while (m_running.load(std::memory_order_relaxed))
|
while (m_running.load(std::memory_order_relaxed))
|
||||||
{
|
{
|
||||||
const auto now = std::chrono::steady_clock::now();
|
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
|
// check for new connected wiimotes
|
||||||
get_controllers();
|
get_controllers();
|
||||||
|
@ -160,11 +193,16 @@ void WiimoteControllerProvider::reader_thread()
|
||||||
for (size_t index = 0; index < m_wiimotes.size(); ++index)
|
for (size_t index = 0; index < m_wiimotes.size(); ++index)
|
||||||
{
|
{
|
||||||
auto& wiimote = m_wiimotes[index];
|
auto& wiimote = m_wiimotes[index];
|
||||||
if (!wiimote.connected)
|
if (!wiimote.device)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const auto read_data = wiimote.device->read_data();
|
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;
|
continue;
|
||||||
receivedAnyPacket = true;
|
receivedAnyPacket = true;
|
||||||
|
|
||||||
|
@ -921,18 +959,18 @@ void WiimoteControllerProvider::writer_thread()
|
||||||
|
|
||||||
if (index != (size_t)-1 && !data.empty())
|
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;
|
data[1] |= 1;
|
||||||
|
if (!wiimote.device->write_data(data))
|
||||||
m_wiimotes[index].connected = m_wiimotes[index].device->write_data(data);
|
|
||||||
if (m_wiimotes[index].connected)
|
|
||||||
{
|
{
|
||||||
m_wiimotes[index].data_ts = std::chrono::high_resolution_clock::now();
|
wiimote.device.reset();
|
||||||
|
wiimote.rumble = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
wiimote.data_ts = std::chrono::high_resolution_clock::now();
|
||||||
m_wiimotes[index].rumble = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
device_lock.unlock();
|
device_lock.unlock();
|
||||||
|
|
||||||
|
|
|
@ -77,16 +77,17 @@ public:
|
||||||
private:
|
private:
|
||||||
std::atomic_bool m_running = false;
|
std::atomic_bool m_running = false;
|
||||||
std::thread m_reader_thread, m_writer_thread;
|
std::thread m_reader_thread, m_writer_thread;
|
||||||
|
|
||||||
std::shared_mutex m_device_mutex;
|
std::shared_mutex m_device_mutex;
|
||||||
|
|
||||||
|
std::thread m_connectionThread;
|
||||||
|
std::vector<WiimoteDevicePtr> m_connectedDevices;
|
||||||
|
std::mutex m_connectedDeviceMutex;
|
||||||
struct Wiimote
|
struct Wiimote
|
||||||
{
|
{
|
||||||
Wiimote(WiimoteDevicePtr device)
|
Wiimote(WiimoteDevicePtr device)
|
||||||
: device(std::move(device)) {}
|
: device(std::move(device)) {}
|
||||||
|
|
||||||
WiimoteDevicePtr device;
|
WiimoteDevicePtr device;
|
||||||
std::atomic_bool connected = true;
|
|
||||||
std::atomic_bool rumble = false;
|
std::atomic_bool rumble = false;
|
||||||
|
|
||||||
std::shared_mutex mutex;
|
std::shared_mutex mutex;
|
||||||
|
@ -103,6 +104,7 @@ private:
|
||||||
|
|
||||||
void reader_thread();
|
void reader_thread();
|
||||||
void writer_thread();
|
void writer_thread();
|
||||||
|
void connectionThread();
|
||||||
|
|
||||||
void calibrate(size_t index);
|
void calibrate(size_t index);
|
||||||
IRMode set_ir_camera(size_t index, bool state);
|
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 bool write_data(const std::vector<uint8>& data) = 0;
|
||||||
virtual std::optional<std::vector<uint8_t>> read_data() = 0;
|
virtual std::optional<std::vector<uint8_t>> read_data() = 0;
|
||||||
|
|
||||||
virtual bool operator==(WiimoteDevice& o) const = 0;
|
virtual bool operator==(const WiimoteDevice& o) const = 0;
|
||||||
bool operator!=(WiimoteDevice& o) const { return *this == o; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using WiimoteDevicePtr = std::shared_ptr<WiimoteDevice>;
|
using WiimoteDevicePtr = std::shared_ptr<WiimoteDevice>;
|
||||||
|
|
|
@ -47,8 +47,11 @@ std::vector<WiimoteDevicePtr> HidapiWiimote::get_devices() {
|
||||||
return wiimote_devices;
|
return wiimote_devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HidapiWiimote::operator==(WiimoteDevice& o) const {
|
bool HidapiWiimote::operator==(const WiimoteDevice& rhs) const {
|
||||||
return static_cast<HidapiWiimote const&>(o).m_path == m_path;
|
auto other = dynamic_cast<const HidapiWiimote*>(&rhs);
|
||||||
|
if (!other)
|
||||||
|
return false;
|
||||||
|
return m_path == other->m_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
HidapiWiimote::~HidapiWiimote() {
|
HidapiWiimote::~HidapiWiimote() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ public:
|
||||||
|
|
||||||
bool write_data(const std::vector<uint8> &data) override;
|
bool write_data(const std::vector<uint8> &data) override;
|
||||||
std::optional<std::vector<uint8>> read_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();
|
static std::vector<WiimoteDevicePtr> get_devices();
|
||||||
|
|
||||||
|
@ -19,5 +19,3 @@ private:
|
||||||
const std::string m_path;
|
const std::string m_path;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using WiimoteDevice_t = HidapiWiimote;
|
|
148
src/input/api/Wiimote/l2cap/L2CapWiimote.cpp
Normal file
148
src/input/api/Wiimote/l2cap/L2CapWiimote.cpp
Normal file
|
@ -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;
|
||||||
|
}
|
22
src/input/api/Wiimote/l2cap/L2CapWiimote.h
Normal file
22
src/input/api/Wiimote/l2cap/L2CapWiimote.h
Normal file
|
@ -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);
|
std::scoped_lock lock(m_rumble_mutex);
|
||||||
if (m_rumble_queue.size() >= 5)
|
if (m_rumble_queue.size() >= 5)
|
||||||
{
|
{
|
||||||
cemuLog_logDebug(LogType::Force, "too many cmds");
|
cemuLog_logDebugOnce(LogType::Force, "VPADControlMotor(): Pattern too long");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue