mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-23 04:16:57 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b95ea4bb5f | |||
| e93b4b0378 | |||
| 0fd41ae206 | |||
| 9202bddad2 | |||
| 32587337b5 | |||
| 02ead0e2e1 | |||
| 4f1f3b1bb3 | |||
| 42bb0da520 | |||
| 5dec523143 | |||
| d01633e18e | |||
| e7ac242039 | |||
| d7974dc763 | |||
| b899717900 | |||
| d58aefde49 | |||
| 4c6be6cd1d | |||
| 584c2e8ca6 | |||
| 717b50155a | |||
| 57b161019d | |||
| 3ff6606fcb | |||
| 6a0a666a41 |
+11
-11
@@ -113,7 +113,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
shader_recompiler_compute_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -388,7 +388,7 @@ jobs:
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
@@ -397,7 +397,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -407,12 +407,12 @@ jobs:
|
||||
- name: Verify artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
file _Build/linux/install/launcher
|
||||
file _Build/linux/install/kyty_launcher
|
||||
file _Build/linux/install/kyty_emulator
|
||||
test -x _Build/linux/install/kyty_updater
|
||||
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
|
||||
ldd _Build/linux/install/kyty_emulator > /dev/null
|
||||
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
readelf -d _Build/linux/install/kyty_launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
while IFS= read -r -d '' binary; do
|
||||
while read -r dependency; do
|
||||
test -e "_Build/linux/install/lib/$dependency"
|
||||
@@ -421,8 +421,8 @@ jobs:
|
||||
sed -n 's/.*Shared library: \[\(libQt6[^]]*\|libicu[^]]*\)\].*/\1/p'
|
||||
)
|
||||
done < <(
|
||||
find _Build/linux/install/launcher _Build/linux/install/plugins \
|
||||
-type f \( -name launcher -o -name '*.so' \) -print0
|
||||
find _Build/linux/install/kyty_launcher _Build/linux/install/plugins \
|
||||
-type f \( -name kyty_launcher -o -name '*.so' \) -print0
|
||||
)
|
||||
_Build/linux/install/kyty_emulator --help > /dev/null
|
||||
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
linux_dir="artifacts/KytyPS5-Linux-x86_64"
|
||||
test -d "$linux_dir"
|
||||
chmod a+x \
|
||||
"$linux_dir/launcher" \
|
||||
"$linux_dir/kyty_launcher" \
|
||||
"$linux_dir/kyty_emulator" \
|
||||
"$linux_dir/kyty_updater"
|
||||
tar -C "$linux_dir" -czf \
|
||||
|
||||
Vendored
+2
-2
@@ -5,7 +5,7 @@
|
||||
"name": "Debug launcher UI",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\launcher.exe",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\kyty_launcher.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\_Build\\vscode-clang\\launcher",
|
||||
@@ -13,7 +13,7 @@
|
||||
"externalConsole": false,
|
||||
"linux": {
|
||||
"type": "cppdbg",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/launcher",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/kyty_launcher",
|
||||
"cwd": "${workspaceFolder}/_Build/vscode-clang/launcher",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/gdb"
|
||||
|
||||
+15
-1
@@ -350,7 +350,7 @@ set(inc_headers
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
|
||||
${KYTY_THIRD_PARTY_DIR}/fmt/include
|
||||
${KYTY_THIRD_PARTY_DIR}/xxhash
|
||||
${KYTY_THIRD_PARTY_DIR}/xxHash
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
${KYTY_THIRD_PARTY_DIR}/renderdoc
|
||||
${KYTY_THIRD_PARTY_DIR}/stb
|
||||
@@ -526,6 +526,10 @@ add_executable(controller_settings_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(host_input_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/HostInputTests.cpp")
|
||||
target_link_libraries(host_input_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(host_input_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
|
||||
)
|
||||
@@ -644,6 +648,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
||||
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
|
||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
|
||||
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
|
||||
@@ -723,6 +728,7 @@ if(BUILD_TESTING)
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
controller_settings_tests
|
||||
host_input_tests
|
||||
save_data_memory_tests
|
||||
png_enc_tests
|
||||
ces_tests
|
||||
@@ -760,6 +766,14 @@ add_custom_target(kyty_trophy_sound
|
||||
add_dependencies(kyty_emulator kyty_trophy_sound)
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
|
||||
|
||||
add_custom_target(kyty_touch_cursors
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors"
|
||||
"$<TARGET_FILE_DIR:kyty_emulator>/assets/cursors"
|
||||
VERBATIM)
|
||||
add_dependencies(kyty_emulator kyty_touch_cursors)
|
||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
|
||||
|
||||
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||
if (WIN32)
|
||||
target_link_libraries(kyty_emulator opengl32)
|
||||
|
||||
@@ -275,11 +275,11 @@ From the repository root, follow the launcher configure, build and install steps
|
||||
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
|
||||
Fedora's packaged Qt 6 automatically.
|
||||
|
||||
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
|
||||
Run `./_Build/linux/install/kyty_launcher` on regular Fedora or inside the container. To launch a
|
||||
container build from the host, leave the container with `exit`, then run from the repository root:
|
||||
|
||||
```bash
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/launcher
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
|
||||
@@ -345,7 +345,7 @@ Use `_Build/linux` instead of `_Build/windows` for a Linux build.
|
||||
|
||||
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
|
||||
Tools to build the project with Ninja and `clang-cl` and provides launch profiles for both
|
||||
`launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
`kyty_launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
compiler per platform, so on Linux configure from the command line as shown above.
|
||||
|
||||
Before using it:
|
||||
@@ -365,11 +365,11 @@ Update your graphics driver before reporting rendering problems.
|
||||
To use the graphical launcher:
|
||||
|
||||
```powershell
|
||||
.\_Build\windows\install\launcher.exe
|
||||
.\_Build\windows\install\kyty_launcher.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
./_Build/linux/install/launcher
|
||||
./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Notification localization
|
||||
|
||||
Trophy notification subtitles use the configured console language. Trophy names still come
|
||||
from the game's metadata. The launcher trophy dialogs are unchanged.
|
||||
|
||||
Edit `assets/localization/<locale>.json`, using flat UTF-8 string values:
|
||||
|
||||
```json
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
```
|
||||
|
||||
CMake converts the catalogs into C++ byte arrays in the build directory. The emulator parses
|
||||
the embedded catalogs once and falls back to `en-US.json` for missing translations. No language
|
||||
files or translation service are needed at runtime. Rebuild after editing a catalog.
|
||||
|
||||
For local testing, rebuild `kyty_emulator`, choose a game's Console Language in the launcher,
|
||||
and unlock a trophy with notifications enabled. Installed system fonts provide non-Latin
|
||||
glyphs; the selected language needs an appropriate font on the host.
|
||||
@@ -12,9 +12,23 @@ endif()
|
||||
|
||||
add_library(common STATIC ${common_src} ${common_headers})
|
||||
|
||||
set(localization_dir "${PROJECT_SOURCE_DIR}/assets/localization")
|
||||
file(GLOB localization_catalogs CONFIGURE_DEPENDS "${localization_dir}/*.json")
|
||||
set(localization_data "${CMAKE_CURRENT_BINARY_DIR}/localizationData.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${localization_data}"
|
||||
COMMAND ${CMAKE_COMMAND} "-DINPUTS=${localization_catalogs}" "-DOUTPUT=${localization_data}"
|
||||
-P "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
DEPENDS ${localization_catalogs} "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
COMMENT "Embed notification localization catalogs"
|
||||
VERBATIM
|
||||
)
|
||||
target_sources(common PRIVATE "${localization_data}")
|
||||
|
||||
target_include_directories(common
|
||||
PUBLIC ${KYTY_SOURCE_DIR}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "localizationData.h"
|
||||
|
||||
namespace Common::Localization {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 30> Locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
|
||||
size_t LanguageIndex(int console_language) {
|
||||
return console_language >= 0 && console_language < static_cast<int>(Locales.size())
|
||||
? static_cast<size_t>(console_language)
|
||||
: 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view LocaleName(int console_language) {
|
||||
return Locales[LanguageIndex(console_language)];
|
||||
}
|
||||
|
||||
std::string_view Get(int console_language, std::string_view key) {
|
||||
static const auto catalogs = [] {
|
||||
std::array<nlohmann::json, Locales.size()> result;
|
||||
for (size_t i = 0; i < Locales.size(); ++i) {
|
||||
const auto bytes = EmbeddedCatalog(Locales[i]);
|
||||
result[i] = nlohmann::json::parse(bytes.begin(), bytes.end(), nullptr, false);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
for (const auto index: std::array {LanguageIndex(console_language), size_t {1}}) {
|
||||
const auto& catalog = catalogs[index];
|
||||
const auto found = catalog.find(key);
|
||||
if (found != catalog.end() && found->is_string()) {
|
||||
const auto& text = found->get_ref<const std::string&>();
|
||||
if (!text.empty()) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace Common::Localization {
|
||||
|
||||
// Console language indices follow the system configuration; invalid indices use English.
|
||||
[[nodiscard]] std::string_view LocaleName(int console_language);
|
||||
|
||||
// Translations have static lifetime. Missing translations use English, then the supplied key.
|
||||
[[nodiscard]] std::string_view Get(int console_language, std::string_view key);
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fmt/format.h>
|
||||
#include <memory>
|
||||
@@ -45,8 +46,8 @@ std::shared_ptr<spdlog::logger> MakeFileLogger(std::string name
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
auto sink =
|
||||
std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), true);
|
||||
const bool truncate = std::getenv(Log::RelaunchedVariable) == nullptr;
|
||||
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), truncate);
|
||||
return MakeLogger(std::move(name), std::move(sink));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ struct Lifecycle {
|
||||
|
||||
enum class Direction { Silent, Console, File };
|
||||
|
||||
// Set by LoadExec for the program it starts, which continues the log file of the one it replaces
|
||||
// instead of starting it over.
|
||||
inline constexpr const char* RelaunchedVariable = "KYTY_RELAUNCHED";
|
||||
|
||||
Direction GetDirection();
|
||||
bool IsSilent();
|
||||
void Write(std::string_view text);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -105,13 +105,7 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
!conf["defaultLanguage"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
static constexpr std::array<std::string_view, 30> locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
const auto locale =
|
||||
locales[console_language >= 0 && console_language < locales.size() ? console_language : 1];
|
||||
const auto locale = Localization::LocaleName(console_language);
|
||||
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
|
||||
if (meta.is_null()) {
|
||||
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
if(NOT DEFINED INPUTS OR NOT DEFINED OUTPUT)
|
||||
message(FATAL_ERROR "embed_localization.cmake requires INPUTS and OUTPUT")
|
||||
endif()
|
||||
|
||||
set(contents "// Generated from assets/localization; do not edit.\n#pragma once\n#include <span>\n#include <string_view>\n\nnamespace {\n")
|
||||
set(lookup "std::span<const unsigned char> EmbeddedCatalog(std::string_view locale) {\n")
|
||||
set(continuation "[89ab][0-9a-f]")
|
||||
string(CONCAT utf8_character
|
||||
"[0-7][0-9a-f]|(c[2-9a-f]|d[0-9a-f])${continuation}"
|
||||
"|e0[ab][0-9a-f]${continuation}|e[1-9abcef]${continuation}${continuation}"
|
||||
"|ed[89][0-9a-f]${continuation}|f0[9ab][0-9a-f]${continuation}${continuation}"
|
||||
"|f[123]${continuation}${continuation}${continuation}|f48[0-9a-f]${continuation}${continuation}")
|
||||
foreach(catalog IN LISTS INPUTS)
|
||||
file(READ "${catalog}" bytes HEX)
|
||||
# Validate UTF-8, excluding overlong sequences, surrogates, and values above U+10FFFF.
|
||||
if(NOT bytes MATCHES "^(${utf8_character})*$")
|
||||
message(FATAL_ERROR "Localization must be UTF-8: ${catalog}")
|
||||
endif()
|
||||
file(READ "${catalog}" json)
|
||||
string(JSON type ERROR_VARIABLE error TYPE "${json}")
|
||||
if(error OR NOT type STREQUAL "OBJECT")
|
||||
message(FATAL_ERROR "Invalid localization object in ${catalog}: ${error}")
|
||||
endif()
|
||||
string(JSON count LENGTH "${json}")
|
||||
if(count GREATER 0)
|
||||
math(EXPR last "${count} - 1")
|
||||
foreach(index RANGE ${last})
|
||||
string(JSON key MEMBER "${json}" ${index})
|
||||
string(JSON type TYPE "${json}" "${key}")
|
||||
if(NOT type STREQUAL "STRING")
|
||||
message(FATAL_ERROR "Localization value '${key}' must be a string: ${catalog}")
|
||||
endif()
|
||||
string(JSON text GET "${json}" "${key}")
|
||||
if(key STREQUAL "" OR text STREQUAL "")
|
||||
message(FATAL_ERROR "Localization keys and values must not be empty: ${catalog}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
get_filename_component(locale "${catalog}" NAME_WE)
|
||||
if(locale STREQUAL "en-US")
|
||||
string(JSON earned ERROR_VARIABLE error GET "${json}" "trophy.toast.earned")
|
||||
if(error OR earned STREQUAL "")
|
||||
message(FATAL_ERROR "English localization requires trophy.toast.earned: ${catalog}")
|
||||
endif()
|
||||
set(english_found TRUE)
|
||||
endif()
|
||||
string(MAKE_C_IDENTIFIER "${locale}" symbol)
|
||||
string(REGEX REPLACE "(..)" "0x\\1," bytes "${bytes}")
|
||||
string(APPEND contents "constexpr unsigned char catalog_${symbol}[] = {${bytes}};\n")
|
||||
string(APPEND lookup "\tif (locale == \"${locale}\") return catalog_${symbol};\n")
|
||||
endforeach()
|
||||
if(NOT english_found)
|
||||
message(FATAL_ERROR "Localization requires an en-US.json fallback")
|
||||
endif()
|
||||
string(APPEND contents "\n${lookup}\treturn {};\n}\n} // namespace\n")
|
||||
|
||||
get_filename_component(output_dir "${OUTPUT}" DIRECTORY)
|
||||
file(MAKE_DIRECTORY "${output_dir}")
|
||||
file(WRITE "${OUTPUT}.tmp" "${contents}")
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${OUTPUT}.tmp" "${OUTPUT}"
|
||||
COMMAND_ERROR_IS_FATAL ANY)
|
||||
file(REMOVE "${OUTPUT}.tmp")
|
||||
@@ -66,6 +66,18 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
|
||||
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
m_bda_sync.Add(begin, end - begin);
|
||||
}
|
||||
|
||||
RangeSet MemoryTracker::TakeBdaSync() {
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
return std::exchange(m_bda_sync, RangeSet {});
|
||||
}
|
||||
|
||||
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
CheckNotInUploadCallback();
|
||||
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
@@ -88,6 +100,7 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
std::scoped_lock lock(manager->lock);
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
|
||||
@@ -128,6 +141,8 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
|
||||
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
locks.clear();
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -29,6 +29,10 @@ public:
|
||||
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UntrackMemory(uint64_t vaddr, uint64_t size);
|
||||
// Queue after changing ownership; a concurrent drain must not consume an event
|
||||
// before the dirty pages it describes become visible.
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] RangeSet TakeBdaSync();
|
||||
// Removes protection from a range and flushes GPU-owned data when required.
|
||||
template <typename Flush>
|
||||
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
|
||||
@@ -51,6 +55,7 @@ public:
|
||||
on_flush();
|
||||
}
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
#if KYTY_BUILD == KYTY_BUILD_DEBUG
|
||||
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
|
||||
@@ -149,6 +154,8 @@ private:
|
||||
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
|
||||
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
|
||||
std::mutex m_region_mutex;
|
||||
std::mutex m_bda_sync_mutex;
|
||||
RangeSet m_bda_sync;
|
||||
PageManager& m_page_manager;
|
||||
};
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ void BufferCache::ChangeRegister(BufferId id) {
|
||||
}
|
||||
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
|
||||
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
|
||||
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
|
||||
} else {
|
||||
const auto found = m_buffers.find(buffer.CpuAddress());
|
||||
EXIT_IF(found == m_buffers.end() || found->second != id);
|
||||
@@ -685,6 +686,16 @@ void BufferCache::ProcessFaultBuffer() {
|
||||
m_fault_manager.ProcessFaultBuffer();
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
|
||||
// Take before walking: writes queued during synchronization remain pending.
|
||||
const auto pending = m_memory_tracker.TakeBdaSync();
|
||||
pending.ForEach([&](uint64_t begin, uint64_t end) {
|
||||
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
|
||||
SynchronizeBuffersInRange(start, finish - start);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
|
||||
const auto end = vaddr + size;
|
||||
auto it = m_buffers.upper_bound(vaddr);
|
||||
|
||||
+5
-1
@@ -80,7 +80,10 @@ public:
|
||||
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void ProcessFaultBuffer();
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
void SynchronizeBda(const RangeSet& mapped);
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
m_memory_tracker.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
void RunGarbageCollector();
|
||||
|
||||
private:
|
||||
@@ -114,6 +117,7 @@ private:
|
||||
void DeleteBuffer(BufferId id);
|
||||
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
|
||||
bool is_written, bool is_texel_buffer);
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
|
||||
uint64_t total_size);
|
||||
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
|
||||
|
||||
+1
-3
@@ -67,11 +67,9 @@ Buffer::Buffer(GraphicContext& graphics, CommandScheduler& scheduler, MemoryUsag
|
||||
buffer_info.usage = flags;
|
||||
|
||||
const bool with_bda = bool(flags & vk::BufferUsageFlagBits::eShaderDeviceAddress);
|
||||
const VmaAllocationCreateFlags bda_flag =
|
||||
with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0;
|
||||
VmaAllocationCreateInfo allocation_info {};
|
||||
allocation_info.flags =
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | bda_flag | AllocationFlags(usage);
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | AllocationFlags(usage);
|
||||
allocation_info.usage = AllocationUsage(usage);
|
||||
allocation_info.preferredFlags = usage == MemoryUsage::DeviceLocal
|
||||
? VkMemoryPropertyFlags {}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kytyGitVersion.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -81,7 +80,8 @@ std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties)
|
||||
uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u];
|
||||
uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu];
|
||||
}
|
||||
return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION,
|
||||
// Driver cache entries already distinguish shader code and pipeline state across builds.
|
||||
return fmt::format("KytyPC2:{:08x}:{:08x}:{:08x}:{}\n",
|
||||
properties.vendorID, properties.deviceID, properties.driverVersion, uuid);
|
||||
}
|
||||
|
||||
@@ -516,17 +516,6 @@ void PipelineCache::InitializeDriverCache() {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)");
|
||||
return;
|
||||
}
|
||||
const std::string_view git_hash = KYTY_GIT_HASH;
|
||||
const std::string_view git_revision = KYTY_GIT_REVISION;
|
||||
if (git_hash == "unknown" || git_revision == "unknown") {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)");
|
||||
return;
|
||||
}
|
||||
if (git_hash.ends_with("-dirty")) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)");
|
||||
return;
|
||||
}
|
||||
|
||||
m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin");
|
||||
const auto path = Common::PathToString(m_driver_cache_path);
|
||||
const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path);
|
||||
@@ -559,7 +548,7 @@ void PipelineCache::InitializeDriverCache() {
|
||||
XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) {
|
||||
initial_data.clear();
|
||||
PipelineCacheLog(
|
||||
"Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)",
|
||||
"Vulkan pipeline cache: invalidating {} (driver or data mismatch)",
|
||||
path);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -94,9 +94,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
constexpr std::array formats {
|
||||
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
|
||||
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
|
||||
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
|
||||
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
|
||||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
|
||||
descriptor.Base48() == 0) {
|
||||
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
|
||||
@@ -108,7 +109,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
? group_x
|
||||
: static_cast<uint64_t>(group_x) * input.threads_num[0];
|
||||
const auto size = descriptor.GetSize();
|
||||
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
|
||||
const auto elements = descriptor.NumRecords();
|
||||
const bool covered = fill.bounded_elements != 0
|
||||
? fill.bounded_elements == elements && invocations >= elements
|
||||
: invocations == elements;
|
||||
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
|
||||
(input.dispatch_thread_dimensions &&
|
||||
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
|
||||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
|
||||
@@ -252,10 +257,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
return;
|
||||
}
|
||||
|
||||
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
|
||||
|
||||
@@ -90,6 +90,7 @@ bool RenderContext::IsMapped(uint64_t vaddr, uint64_t size) const noexcept {
|
||||
void RenderContext::MapMemory(uint64_t vaddr, uint64_t size) {
|
||||
std::lock_guard lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.Add(vaddr, size);
|
||||
m_buffer_cache.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
||||
@@ -129,9 +130,7 @@ void RenderContext::PrepareBda() {
|
||||
m_bda_logged = true;
|
||||
}
|
||||
std::shared_lock lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.ForEach([this](uint64_t start, uint64_t end) {
|
||||
m_buffer_cache.SynchronizeBuffersInRange(start, end - start);
|
||||
});
|
||||
m_buffer_cache.SynchronizeBda(m_mapped_ranges);
|
||||
m_fault_process_pending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -536,7 +536,10 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
}
|
||||
}
|
||||
if (feedback_aspects && !m_context.GetGraphics().attachment_feedback_loop_enabled) {
|
||||
EXIT("depth attachment feedback loop is not supported by the host\n");
|
||||
static std::atomic_bool logged = false;
|
||||
if (!logged.exchange(true, std::memory_order_relaxed)) {
|
||||
std::printf("depth attachment feedback loop is not supported by the host\n");
|
||||
}
|
||||
}
|
||||
auto layout = depth_attachment_layout(depth);
|
||||
if (sampled_aspects & ~DepthReadableAspects(layout)) {
|
||||
|
||||
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
|
||||
return;
|
||||
}
|
||||
if (state == RenderDocState::Capturing) {
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
|
||||
++g_captured_flips;
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
|
||||
if (g_captured_flips < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "graphics/presentation/window/hostInput.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@@ -29,6 +31,7 @@
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -149,6 +152,47 @@ bool GetOverlaySnapshot(OverlaySnapshot* snapshot) {
|
||||
}
|
||||
|
||||
constexpr size_t INPUT_QUEUE_CAPACITY = 128;
|
||||
constexpr int MAX_TOUCH_CURSOR_IMAGE_DIMENSION = 2048;
|
||||
constexpr float TOUCH_CURSOR_IMAGE_SIZE = 48.0f;
|
||||
|
||||
enum class TouchParticleSkin : uint8_t { Classic, BlueNeon, Monochrome, Crimson, RedBlue };
|
||||
|
||||
TouchParticleSkin GetTouchParticleSkin() {
|
||||
static const auto skin = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
constexpr std::string_view key = "TouchParticleSkin=";
|
||||
if (std::string_view(value).starts_with(key)) {
|
||||
const auto name = std::string_view(value).substr(key.size());
|
||||
if (name == "BlueNeon") {
|
||||
return TouchParticleSkin::BlueNeon;
|
||||
}
|
||||
if (name == "Monochrome") {
|
||||
return TouchParticleSkin::Monochrome;
|
||||
}
|
||||
if (name == "Crimson") {
|
||||
return TouchParticleSkin::Crimson;
|
||||
}
|
||||
if (name == "RedBlue") {
|
||||
return TouchParticleSkin::RedBlue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TouchParticleSkin::Classic;
|
||||
}();
|
||||
return skin;
|
||||
}
|
||||
|
||||
bool TouchCursorImageEnabled() {
|
||||
static const bool enabled = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
if (std::string_view(value) == "TouchCursorImage=Off") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
enum class InputKind : uint8_t {
|
||||
Button,
|
||||
@@ -238,6 +282,61 @@ std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_
|
||||
}
|
||||
}
|
||||
|
||||
// ImGui's built-in font covers Latin only. Merge installed fonts for the selected console language.
|
||||
void AddSystemFontFallbacks(ImGuiIO& io) {
|
||||
io.Fonts->AddFontDefault();
|
||||
const auto add = [&](std::initializer_list<const char*> paths) {
|
||||
for (const char* path: paths) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::is_regular_file(path, error)) {
|
||||
ImFontConfig config;
|
||||
config.MergeMode = true;
|
||||
config.Flags = ImFontFlags_NoLoadError;
|
||||
if (io.Fonts->AddFontFromFileTTF(path, 0.0f, &config) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool found =
|
||||
add({"C:/Windows/Fonts/segoeui.ttf", "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/Library/Fonts/Arial Unicode.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/noto/NotoSans-Regular.ttf"});
|
||||
const auto locale = Common::Localization::LocaleName(Config::GetConsoleLanguage());
|
||||
const char* noto_cjk[] = {"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc"};
|
||||
if (locale == "ja-JP") {
|
||||
found = add({"C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc",
|
||||
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc", noto_cjk[0], noto_cjk[1],
|
||||
noto_cjk[2]});
|
||||
} else if (locale == "ko-KR") {
|
||||
found = add({"C:/Windows/Fonts/malgun.ttf", "/System/Library/Fonts/AppleSDGothicNeo.ttc",
|
||||
noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "zh-Hans" || locale == "zh-Hant") {
|
||||
found =
|
||||
add({locale == "zh-Hans" ? "C:/Windows/Fonts/msyh.ttc" : "C:/Windows/Fonts/msjh.ttc",
|
||||
"/System/Library/Fonts/PingFang.ttc", noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "th-TH") {
|
||||
found = add({"C:/Windows/Fonts/leelawui.ttf", "C:/Windows/Fonts/tahoma.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Ayuthaya.ttf",
|
||||
"/usr/share/fonts/truetype/tlwg/Garuda.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansThai-Regular.ttf"});
|
||||
} else if (locale == "ar-AE") {
|
||||
found = add({"C:/Windows/Fonts/tahoma.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansArabic-Regular.ttf"});
|
||||
}
|
||||
if (!found) {
|
||||
LOGF("[Trophy] no installed fallback font for %.*s\n", static_cast<int>(locale.size()),
|
||||
locale.data());
|
||||
}
|
||||
}
|
||||
|
||||
void ClearInputEvents() {
|
||||
std::scoped_lock lock(g_input_mutex);
|
||||
g_input_events.clear();
|
||||
@@ -399,6 +498,63 @@ std::u16string Utf8ToUtf16(std::string_view text) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// The Arabic notification is a single RTL run. Use presentation forms because ImGui does not
|
||||
// shape Arabic. This intentionally applies only to the catalog subtitle, never game-provided names.
|
||||
std::string PrepareTrophySubtitle(int language) {
|
||||
const auto text = Common::Localization::Get(language, "trophy.toast.earned");
|
||||
if (Common::Localization::LocaleName(language) != "ar-AE") {
|
||||
return std::string(text);
|
||||
}
|
||||
struct ArabicForm {
|
||||
char16_t letter;
|
||||
char16_t isolated;
|
||||
bool joins_next;
|
||||
};
|
||||
static constexpr ArabicForm FORMS[] = {
|
||||
{0x0621, 0xfe80, false}, {0x0622, 0xfe81, false}, {0x0623, 0xfe83, false},
|
||||
{0x0624, 0xfe85, false}, {0x0625, 0xfe87, false}, {0x0626, 0xfe89, true},
|
||||
{0x0627, 0xfe8d, false}, {0x0628, 0xfe8f, true}, {0x0629, 0xfe93, false},
|
||||
{0x062a, 0xfe95, true}, {0x062b, 0xfe99, true}, {0x062c, 0xfe9d, true},
|
||||
{0x062d, 0xfea1, true}, {0x062e, 0xfea5, true}, {0x062f, 0xfea9, false},
|
||||
{0x0630, 0xfeab, false}, {0x0631, 0xfead, false}, {0x0632, 0xfeaf, false},
|
||||
{0x0633, 0xfeb1, true}, {0x0634, 0xfeb5, true}, {0x0635, 0xfeb9, true},
|
||||
{0x0636, 0xfebd, true}, {0x0637, 0xfec1, true}, {0x0638, 0xfec5, true},
|
||||
{0x0639, 0xfec9, true}, {0x063a, 0xfecd, true}, {0x0641, 0xfed1, true},
|
||||
{0x0642, 0xfed5, true}, {0x0643, 0xfed9, true}, {0x0644, 0xfedd, true},
|
||||
{0x0645, 0xfee1, true}, {0x0646, 0xfee5, true}, {0x0647, 0xfee9, true},
|
||||
{0x0648, 0xfeed, false}, {0x0649, 0xfeef, false}, {0x064a, 0xfef1, true},
|
||||
};
|
||||
const auto form = [](char16_t c) -> const ArabicForm* {
|
||||
for (const auto& value: FORMS) {
|
||||
if (value.letter == c) {
|
||||
return &value;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const auto logical = Utf8ToUtf16(text);
|
||||
if (std::none_of(logical.begin(), logical.end(),
|
||||
[](char16_t c) { return c >= 0x0600 && c <= 0x06ff; })) {
|
||||
return std::string(text);
|
||||
}
|
||||
auto visual = logical;
|
||||
for (size_t i = 0; i < logical.size(); ++i) {
|
||||
const auto* current = form(logical[i]);
|
||||
if (current == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto* previous = i > 0 ? form(logical[i - 1]) : nullptr;
|
||||
const auto* next = i + 1 < logical.size() ? form(logical[i + 1]) : nullptr;
|
||||
const bool joins_previous =
|
||||
previous != nullptr && previous->joins_next && current->letter != 0x0621;
|
||||
const bool joins_next = current->joins_next && next != nullptr && next->letter != 0x0621;
|
||||
visual[i] = static_cast<char16_t>(current->isolated + (joins_next ? 2 : 0) +
|
||||
(joins_previous ? 1 : 0));
|
||||
}
|
||||
std::reverse(visual.begin(), visual.end());
|
||||
return Common::Utf16ToUtf8(visual);
|
||||
}
|
||||
|
||||
std::string VisibleText(const Ime::HostSnapshot& snapshot, size_t max_units) {
|
||||
const size_t cursor = std::min<size_t>(snapshot.cursor, snapshot.text.size());
|
||||
const size_t payload_units = max_units > 4 ? max_units - 4 : 1;
|
||||
@@ -552,9 +708,11 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
const bool splash = g_splash_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash,
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash ||
|
||||
(cursor.enabled && cursor.cursor_visible),
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u)};
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -694,6 +852,20 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
enum class TouchParticleIcon : uint8_t { Square, Triangle, Cross, Circle };
|
||||
|
||||
struct TouchParticle {
|
||||
ImVec2 position;
|
||||
Clock::time_point born;
|
||||
TouchParticleIcon icon;
|
||||
};
|
||||
|
||||
struct TouchTrailSegment {
|
||||
ImVec2 from;
|
||||
ImVec2 to;
|
||||
Clock::time_point born;
|
||||
};
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
@@ -705,6 +877,12 @@ struct SystemOverlay::Impl {
|
||||
ReleaseVulkan();
|
||||
if (imgui_context != nullptr) {
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
if (hand_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(hand_cursor_image.get());
|
||||
}
|
||||
if (drawing_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(drawing_cursor_image.get());
|
||||
}
|
||||
if (trophy_notification_image) {
|
||||
ImGui::UnregisterUserTexture(trophy_notification_image.get());
|
||||
}
|
||||
@@ -730,11 +908,14 @@ struct SystemOverlay::Impl {
|
||||
io.ConfigNavCursorVisibleAlways = true;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
io.BackendPlatformName = "Kyty system overlay input";
|
||||
AddSystemFontFallbacks(io);
|
||||
ImGui::StyleColorsDark();
|
||||
auto& style = ImGui::GetStyle();
|
||||
style.WindowRounding = 10.0f;
|
||||
style.FrameRounding = 6.0f;
|
||||
style.ItemSpacing = {8.0f, 8.0f};
|
||||
hand_cursor_image = LoadTouchCursorImage("hand.png");
|
||||
drawing_cursor_image = LoadTouchCursorImage("drawing.png");
|
||||
}
|
||||
|
||||
void PlayTrophySound() {
|
||||
@@ -775,6 +956,55 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTouchCursorImage(std::string_view filename) {
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
if (base_path == nullptr) {
|
||||
LOGF("Could not locate the emulator folder for touchpad cursor images: %s\n",
|
||||
SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path(base_path) / "assets" / "cursors" / std::string(filename);
|
||||
|
||||
Common::File file;
|
||||
if (!file.Open(path, Common::File::Mode::Read)) {
|
||||
LOGF("Touchpad cursor image not found: %s\n", Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
const auto encoded = file.ReadWholeBuffer();
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
LOGF("Touchpad cursor image is empty or too large: %s\n",
|
||||
Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
auto* pixels = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(encoded.data()), static_cast<int>(encoded.size()),
|
||||
&width, &height, nullptr, 4);
|
||||
if (pixels == nullptr || width <= 0 || height <= 0 ||
|
||||
width > MAX_TOUCH_CURSOR_IMAGE_DIMENSION ||
|
||||
height > MAX_TOUCH_CURSOR_IMAGE_DIMENSION) {
|
||||
LOGF("Could not load touchpad cursor image %s: %s\n",
|
||||
Common::PathToString(path).c_str(),
|
||||
pixels == nullptr ? stbi_failure_reason() : "image dimensions exceed 2048x2048");
|
||||
stbi_image_free(pixels);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<ImTextureData>();
|
||||
texture->Create(ImTextureFormat_RGBA32, width, height);
|
||||
std::memcpy(texture->Pixels, pixels,
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
|
||||
texture->UseColors = true;
|
||||
stbi_image_free(pixels);
|
||||
ImGui::RegisterUserTexture(texture.get());
|
||||
LOGF("Loaded touchpad cursor image: %s\n", Common::PathToString(path).c_str());
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
@@ -1191,6 +1421,248 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void EmitTouchParticles(ImVec2 from, ImVec2 to, Clock::time_point now) {
|
||||
const float dx = to.x - from.x;
|
||||
const float dy = to.y - from.y;
|
||||
const float distance = std::hypot(dx, dy);
|
||||
if (distance < 0.01f) {
|
||||
return;
|
||||
}
|
||||
if (distance >= 1.0f) {
|
||||
trail_segments.push_back({from, to, now});
|
||||
if (trail_segments.size() > MAX_TOUCH_TRAIL_SEGMENTS) {
|
||||
trail_segments.erase(trail_segments.begin());
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float PARTICLE_SPACING = 24.0f;
|
||||
float traveled = 0.0f;
|
||||
while (particle_distance + distance - traveled >= PARTICLE_SPACING) {
|
||||
traveled += PARTICLE_SPACING - particle_distance;
|
||||
const float fraction = std::clamp(traveled / distance, 0.0f, 1.0f);
|
||||
particles.push_back(
|
||||
{{from.x + dx * fraction, from.y + dy * fraction}, now, next_particle_icon});
|
||||
next_particle_icon = static_cast<TouchParticleIcon>(
|
||||
(static_cast<uint8_t>(next_particle_icon) + 1) % 4);
|
||||
if (particles.size() > MAX_TOUCH_PARTICLES) {
|
||||
particles.erase(particles.begin());
|
||||
}
|
||||
particle_distance = 0.0f;
|
||||
}
|
||||
particle_distance += distance - traveled;
|
||||
}
|
||||
|
||||
void DrawTouchParticles(ImDrawList* draw, Clock::time_point now) {
|
||||
const auto skin = GetTouchParticleSkin();
|
||||
constexpr float PARTICLE_LIFETIME = 1.0f;
|
||||
constexpr float TRAIL_LIFETIME = 0.35f;
|
||||
std::erase_if(trail_segments, [now](const auto& segment) {
|
||||
return std::chrono::duration<float>(now - segment.born).count() >= TRAIL_LIFETIME;
|
||||
});
|
||||
std::erase_if(particles, [now](const auto& particle) {
|
||||
return std::chrono::duration<float>(now - particle.born).count() >=
|
||||
PARTICLE_LIFETIME;
|
||||
});
|
||||
for (const auto& segment: trail_segments) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - segment.born).count() / TRAIL_LIFETIME;
|
||||
const float fade = 1.0f - age;
|
||||
if (skin == TouchParticleSkin::RedBlue) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(20, 75, 220, static_cast<int>(105.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 45, 65, static_cast<int>(230.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(110, 0, 8, static_cast<int>(130.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 35, 20, static_cast<int>(235.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(35, 145, 255, static_cast<int>(180.0f * fade)),
|
||||
5.0f * (1.0f - age * 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& particle: particles) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - particle.born).count() / PARTICLE_LIFETIME;
|
||||
const float alpha = 1.0f - age;
|
||||
const float size = 9.0f * (1.0f - age * 0.35f);
|
||||
const float drift = (static_cast<uint8_t>(particle.icon) % 2 == 0 ? -1.0f : 1.0f) *
|
||||
8.0f * age;
|
||||
const ImVec2 center {particle.position.x + drift, particle.position.y - 18.0f * age};
|
||||
const auto faded = [alpha, skin, icon = particle.icon](int r, int g, int b) {
|
||||
if (skin == TouchParticleSkin::BlueNeon) {
|
||||
r = 75;
|
||||
g = 220;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::RedBlue) {
|
||||
const bool red = icon == TouchParticleIcon::Square ||
|
||||
icon == TouchParticleIcon::Cross;
|
||||
r = red ? 255 : 75;
|
||||
g = red ? 65 : 175;
|
||||
b = red ? 85 : 255;
|
||||
} else if (skin == TouchParticleSkin::Monochrome) {
|
||||
r = 235;
|
||||
g = 245;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
const bool ember = icon == TouchParticleIcon::Triangle ||
|
||||
icon == TouchParticleIcon::Circle;
|
||||
r = 255;
|
||||
g = ember ? 205 : 115;
|
||||
b = ember ? 70 : 15;
|
||||
}
|
||||
return IM_COL32(r, g, b, static_cast<int>(255.0f * alpha));
|
||||
};
|
||||
|
||||
if (skin != TouchParticleSkin::Classic) {
|
||||
const ImU32 glow =
|
||||
skin == TouchParticleSkin::BlueNeon
|
||||
? IM_COL32(0, 115, 255, static_cast<int>(38.0f * alpha))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? IM_COL32(75, 100, 255, static_cast<int>(55.0f * alpha))
|
||||
: (skin == TouchParticleSkin::Crimson
|
||||
? IM_COL32(255, 70, 0, static_cast<int>(70.0f * alpha))
|
||||
: IM_COL32(115, 165, 255, static_cast<int>(30.0f * alpha))));
|
||||
draw->AddCircleFilled(center, size + 6.0f, glow);
|
||||
}
|
||||
const ImU32 particle_glow =
|
||||
skin == TouchParticleSkin::Crimson
|
||||
? (particle.icon == TouchParticleIcon::Triangle ||
|
||||
particle.icon == TouchParticleIcon::Circle
|
||||
? IM_COL32(255, 195, 30, static_cast<int>(135.0f * alpha))
|
||||
: IM_COL32(255, 95, 0, static_cast<int>(135.0f * alpha)))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? (particle.icon == TouchParticleIcon::Square ||
|
||||
particle.icon == TouchParticleIcon::Cross
|
||||
? IM_COL32(255, 40, 65, static_cast<int>(105.0f * alpha))
|
||||
: IM_COL32(35, 125, 255, static_cast<int>(105.0f * alpha)))
|
||||
: IM_COL32(35, 145, 255,
|
||||
static_cast<int>((skin == TouchParticleSkin::BlueNeon
|
||||
? 100.0f
|
||||
: 65.0f) *
|
||||
alpha)));
|
||||
draw->AddCircleFilled(center, size + 2.0f, particle_glow);
|
||||
switch (particle.icon) {
|
||||
case TouchParticleIcon::Square:
|
||||
draw->AddRect({center.x - size * 0.65f, center.y - size * 0.65f},
|
||||
{center.x + size * 0.65f, center.y + size * 0.65f},
|
||||
faded(255, 105, 180), 2.0f, 0, 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Triangle:
|
||||
draw->AddLine({center.x, center.y - size}, {center.x - size, center.y + size},
|
||||
faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x - size, center.y + size},
|
||||
{center.x + size, center.y + size}, faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x + size, center.y + size},
|
||||
{center.x, center.y - size}, faded(80, 230, 130), 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Cross:
|
||||
draw->AddLine({center.x - size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x + size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
draw->AddLine({center.x + size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x - size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
break;
|
||||
case TouchParticleIcon::Circle:
|
||||
draw->AddCircle(center, size * 0.8f, faded(255, 105, 105), 0, 2.2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTouchPadCursor(const HostInputTouchPadVisualState& state) {
|
||||
if (!TouchCursorImageEnabled()) {
|
||||
particles.clear();
|
||||
trail_segments.clear();
|
||||
return;
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (state.enabled && state.position_valid) {
|
||||
const ImVec2 target {state.x * extent.width, state.y * extent.height};
|
||||
if (!cursor_initialized || state.gesture != cursor_gesture) {
|
||||
cursor_position = target;
|
||||
cursor_initialized = true;
|
||||
cursor_gesture = state.gesture;
|
||||
particle_distance = 0.0f;
|
||||
} else {
|
||||
const float dt = last_cursor_update == Clock::time_point {}
|
||||
? 1.0f / 60.0f
|
||||
: std::clamp(std::chrono::duration<float>(now -
|
||||
last_cursor_update)
|
||||
.count(),
|
||||
1.0f / 1000.0f, 0.05f);
|
||||
const ImVec2 previous = cursor_position;
|
||||
const float follow = 1.0f - std::exp(-18.0f * dt);
|
||||
cursor_position.x += (target.x - cursor_position.x) * follow;
|
||||
cursor_position.y += (target.y - cursor_position.y) * follow;
|
||||
if (state.drawing) {
|
||||
EmitTouchParticles(previous, cursor_position, now);
|
||||
}
|
||||
}
|
||||
last_cursor_update = now;
|
||||
}
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
DrawTouchParticles(draw, now);
|
||||
if (!state.enabled || !state.cursor_visible ||
|
||||
!state.position_valid || !cursor_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 position = cursor_position;
|
||||
const auto& cursor_image =
|
||||
state.drawing ? drawing_cursor_image : hand_cursor_image;
|
||||
if (cursor_image != nullptr) {
|
||||
const float scale = TOUCH_CURSOR_IMAGE_SIZE /
|
||||
static_cast<float>(std::max(cursor_image->Height, 1));
|
||||
const ImVec2 size {static_cast<float>(cursor_image->Width) * scale,
|
||||
static_cast<float>(cursor_image->Height) * scale};
|
||||
draw->AddImage(cursor_image->GetTexRef(), position,
|
||||
{position.x + size.x, position.y + size.y});
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 blue = IM_COL32(35, 145, 255, 220);
|
||||
const ImVec2 hand[] = {
|
||||
{position.x, position.y}, {position.x, position.y + 20.0f},
|
||||
{position.x + 4.0f, position.y + 16.0f},
|
||||
{position.x + 9.0f, position.y + 27.0f},
|
||||
{position.x + 13.0f, position.y + 25.0f},
|
||||
{position.x + 10.0f, position.y + 17.0f},
|
||||
{position.x + 18.0f, position.y + 18.0f},
|
||||
{position.x + 20.0f, position.y + 15.0f},
|
||||
{position.x + 19.0f, position.y + 12.0f},
|
||||
{position.x + 12.0f, position.y + 10.0f},
|
||||
{position.x + 12.0f, position.y + 4.0f},
|
||||
{position.x + 10.0f, position.y + 2.0f},
|
||||
{position.x + 8.0f, position.y + 3.0f},
|
||||
{position.x + 8.0f, position.y + 10.0f},
|
||||
{position.x + 6.0f, position.y + 2.0f},
|
||||
{position.x + 4.0f, position.y + 2.0f},
|
||||
{position.x + 3.0f, position.y + 10.0f},
|
||||
{position.x + 2.0f, position.y + 1.0f},
|
||||
};
|
||||
draw->AddConvexPolyFilled(hand, IM_ARRAYSIZE(hand), IM_COL32(250, 250, 250, 255));
|
||||
for (std::size_t i = 0; i < IM_ARRAYSIZE(hand); i++) {
|
||||
draw->AddLine(hand[i], hand[(i + 1) % IM_ARRAYSIZE(hand)],
|
||||
IM_COL32(20, 25, 35, 255), 1.5f);
|
||||
}
|
||||
if (state.drawing) {
|
||||
const ImVec2 pen_start {position.x + 12.0f, position.y + 8.0f};
|
||||
const ImVec2 pen_end {position.x + 23.0f, position.y - 3.0f};
|
||||
draw->AddLine(pen_start, pen_end, IM_COL32(10, 45, 95, 255), 5.0f);
|
||||
draw->AddLine(pen_start, pen_end, blue, 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
@@ -1221,8 +1693,9 @@ struct SystemOverlay::Impl {
|
||||
trophy_notification = std::move(g_trophy_notifications.front());
|
||||
g_trophy_notifications.pop_front();
|
||||
}
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
trophy_notification_subtitle = PrepareTrophySubtitle(Config::GetConsoleLanguage());
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
PlayTrophySound();
|
||||
}
|
||||
const float age = std::chrono::duration<float>(now - trophy_notification_born).count();
|
||||
@@ -1306,8 +1779,17 @@ struct SystemOverlay::Impl {
|
||||
line.data(), line.data() + line.size());
|
||||
line_y += line_height;
|
||||
}
|
||||
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), "Trophy earned!");
|
||||
const float subtitle_size = 20.0f * screen_scale;
|
||||
const float subtitle_width =
|
||||
ImGui::GetFont()
|
||||
->CalcTextSizeA(subtitle_size, std::numeric_limits<float>::max(), 0.0f,
|
||||
trophy_notification_subtitle.c_str())
|
||||
.x;
|
||||
const float available_width = std::max(1.0f, width - text_offset - text_padding);
|
||||
const float fitted_size = std::max(
|
||||
1.0f, subtitle_size * std::min(1.0f, available_width / std::max(1.0f, subtitle_width)));
|
||||
draw->AddText(nullptr, fitted_size, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), trophy_notification_subtitle.c_str());
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
@@ -1316,7 +1798,8 @@ struct SystemOverlay::Impl {
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire)) {
|
||||
!g_splash_active.load(std::memory_order_acquire) &&
|
||||
!GetHostInputTouchPadVisualState().enabled) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -1356,6 +1839,7 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
return true;
|
||||
@@ -1410,7 +1894,20 @@ struct SystemOverlay::Impl {
|
||||
ImVec2 right_stick {};
|
||||
OverlaySession session;
|
||||
std::unique_ptr<ImTextureData> trophy_notification_image;
|
||||
std::unique_ptr<ImTextureData> hand_cursor_image;
|
||||
std::unique_ptr<ImTextureData> drawing_cursor_image;
|
||||
bool cursor_initialized = false;
|
||||
ImVec2 cursor_position {};
|
||||
float particle_distance = 0.0f;
|
||||
TouchParticleIcon next_particle_icon = TouchParticleIcon::Square;
|
||||
Clock::time_point last_cursor_update;
|
||||
uint64_t cursor_gesture = 0;
|
||||
std::vector<TouchParticle> particles;
|
||||
std::vector<TouchTrailSegment> trail_segments;
|
||||
static constexpr std::size_t MAX_TOUCH_PARTICLES = 64;
|
||||
static constexpr std::size_t MAX_TOUCH_TRAIL_SEGMENTS = 128;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
std::string trophy_notification_subtitle;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -27,6 +29,8 @@ struct ControlInfo {
|
||||
float touch_x = 0.0f;
|
||||
|
||||
Controller::Setting setting {};
|
||||
bool gyro = false;
|
||||
bool touchpad_mode = false;
|
||||
};
|
||||
|
||||
static constexpr std::array CONTROL_INFO = {
|
||||
@@ -61,10 +65,66 @@ static constexpr std::array CONTROL_INFO = {
|
||||
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
|
||||
ControlInfo {.name = "TriggerEffectIntensity",
|
||||
.setting = Controller::Setting::TriggerEffectIntensity},
|
||||
ControlInfo {.name = "GyroToggle", .gyro = true},
|
||||
ControlInfo {.name = "TouchPadMode", .touchpad_mode = true},
|
||||
};
|
||||
|
||||
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
|
||||
|
||||
struct PadSource {
|
||||
std::string_view name;
|
||||
int button; // SDL_GamepadButton, or -1 for an analog trigger
|
||||
};
|
||||
|
||||
constexpr std::array PAD_SOURCES = {
|
||||
PadSource {"South", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
PadSource {"East", SDL_GAMEPAD_BUTTON_EAST},
|
||||
PadSource {"West", SDL_GAMEPAD_BUTTON_WEST},
|
||||
PadSource {"North", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
PadSource {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||
PadSource {"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
PadSource {"Start", SDL_GAMEPAD_BUTTON_START},
|
||||
PadSource {"LeftStick", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
PadSource {"RightStick", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
PadSource {"LeftShoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
PadSource {"RightShoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
PadSource {"DpadUp", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
PadSource {"DpadDown", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
PadSource {"DpadLeft", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
PadSource {"DpadRight", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
PadSource {"Misc1", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
PadSource {"RightPaddle1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
PadSource {"LeftPaddle1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
PadSource {"RightPaddle2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
PadSource {"LeftPaddle2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
PadSource {"Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
PadSource {"LeftTrigger", -1},
|
||||
PadSource {"RightTrigger", -1},
|
||||
};
|
||||
|
||||
constexpr std::size_t LEFT_TRIGGER_SOURCE = PAD_SOURCES.size() - 2;
|
||||
constexpr std::size_t RIGHT_TRIGGER_SOURCE = PAD_SOURCES.size() - 1;
|
||||
constexpr std::size_t INVALID_SOURCE = PAD_SOURCES.size();
|
||||
|
||||
struct PadDefault {
|
||||
std::string_view control;
|
||||
std::string_view source;
|
||||
};
|
||||
|
||||
constexpr std::array PAD_DEFAULTS = {
|
||||
PadDefault {"Cross", "South"}, PadDefault {"Circle", "East"},
|
||||
PadDefault {"Square", "West"}, PadDefault {"Triangle", "North"},
|
||||
PadDefault {"Options", "Start"}, PadDefault {"L3", "LeftStick"},
|
||||
PadDefault {"R3", "RightStick"}, PadDefault {"L1", "LeftShoulder"},
|
||||
PadDefault {"R1", "RightShoulder"}, PadDefault {"L2", "LeftTrigger"},
|
||||
PadDefault {"R2", "RightTrigger"}, PadDefault {"Up", "DpadUp"},
|
||||
PadDefault {"Down", "DpadDown"}, PadDefault {"Left", "DpadLeft"},
|
||||
PadDefault {"Right", "DpadRight"}, PadDefault {"TouchPad", "Touchpad"},
|
||||
PadDefault {"GyroToggle", "Back"},
|
||||
};
|
||||
|
||||
constexpr std::string_view PAD_PREFIX = "Pad:";
|
||||
|
||||
struct Binding {
|
||||
SDL_Keycode key = SDLK_UNKNOWN;
|
||||
uint8_t mouse_button = 0;
|
||||
@@ -74,6 +134,7 @@ struct Binding {
|
||||
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
|
||||
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
|
||||
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
|
||||
constexpr std::string_view TOUCH_PARTICLE_SKIN = "TouchParticleSkin=";
|
||||
|
||||
struct MouseJoystickState {
|
||||
bool enabled = false;
|
||||
@@ -85,6 +146,38 @@ MouseJoystickState g_mouse;
|
||||
SDL_Window* g_mouse_window = nullptr;
|
||||
uint64_t g_cursor_hide_at = 0;
|
||||
|
||||
struct MouseTouchPadState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
struct GamepadTouchPadState {
|
||||
bool enabled = false;
|
||||
bool drawing = false;
|
||||
int id = -1;
|
||||
int x_axis = 128;
|
||||
int y_axis = 128;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t next_poll = 0;
|
||||
uint64_t last_poll = 0;
|
||||
};
|
||||
|
||||
constexpr int GAMEPAD_TOUCHPAD_POLL_MS = 8;
|
||||
constexpr int GAMEPAD_TOUCHPAD_DEADZONE = 18;
|
||||
constexpr float GAMEPAD_TOUCHPAD_SPEED = 0.8f;
|
||||
|
||||
MouseTouchPadState g_mouse_touchpad;
|
||||
GamepadTouchPadState g_gamepad_touchpad;
|
||||
std::mutex g_touchpad_mutex;
|
||||
std::unordered_map<int, std::array<bool, 2>> g_pad_trigger_pressed;
|
||||
|
||||
std::size_t ControlFromName(std::string_view name) {
|
||||
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
|
||||
[name](const auto& item) { return item.name == name; });
|
||||
@@ -126,6 +219,14 @@ public:
|
||||
InputMap() {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
const std::string_view entry = value;
|
||||
if (entry == "Keyboard=Disabled") {
|
||||
m_custom = true;
|
||||
continue;
|
||||
}
|
||||
if (entry == "Pad=Disabled") {
|
||||
m_custom_pad = true;
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(MOUSE_SENSITIVITY)) {
|
||||
const float sensitivity =
|
||||
std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr);
|
||||
@@ -133,6 +234,13 @@ public:
|
||||
std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f);
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(TOUCH_PARTICLE_SKIN) || entry.starts_with("TouchCursorImage=")) {
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(PAD_PREFIX)) {
|
||||
AddPadEntry(entry.substr(PAD_PREFIX.size()), value);
|
||||
continue;
|
||||
}
|
||||
const auto split = entry.find('=');
|
||||
|
||||
Binding binding;
|
||||
@@ -153,10 +261,25 @@ public:
|
||||
}
|
||||
Add(binding);
|
||||
}
|
||||
if (!m_custom_pad) {
|
||||
for (const auto& item: PAD_DEFAULTS) {
|
||||
AssignPad(ControlFromName(item.control), SourceFromName(item.source));
|
||||
}
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool Custom() const { return m_size != 0; }
|
||||
[[nodiscard]] bool Custom() const { return m_custom; }
|
||||
[[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; }
|
||||
|
||||
[[nodiscard]] std::size_t PadControl(std::size_t source) const {
|
||||
return source < m_pad_controls.size() ? m_pad_controls[source] : INVALID_CONTROL;
|
||||
}
|
||||
|
||||
static std::size_t SourceFromButton(int button) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[button](const auto& s) { return s.button == button; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t FindKey(int key_code) const {
|
||||
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
|
||||
const auto binding =
|
||||
@@ -173,7 +296,39 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t SourceFromName(std::string_view name) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[name](const auto& s) { return s.name == name; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
void AssignPad(std::size_t control, std::size_t source) {
|
||||
for (auto& existing: m_pad_controls) {
|
||||
if (existing == control) {
|
||||
existing = INVALID_CONTROL;
|
||||
}
|
||||
}
|
||||
m_pad_controls[source] = control;
|
||||
}
|
||||
|
||||
void AddPadEntry(std::string_view entry, const std::string& original) {
|
||||
const auto split = entry.find('=');
|
||||
const auto control = split != std::string_view::npos ? ControlFromName(entry.substr(0, split))
|
||||
: INVALID_CONTROL;
|
||||
const auto source = split != std::string_view::npos ? SourceFromName(entry.substr(split + 1))
|
||||
: INVALID_SOURCE;
|
||||
if (control == INVALID_CONTROL || source == INVALID_SOURCE) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == 0 && !info.gyro && info.axis != Controller::Axis::AxisMax) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
m_custom_pad = true;
|
||||
AssignPad(control, source);
|
||||
}
|
||||
void Add(const Binding& binding) {
|
||||
m_custom = true;
|
||||
for (std::size_t index = 0; index < m_size;) {
|
||||
if (Conflicts(m_bindings[index], binding)) {
|
||||
m_bindings[index] = m_bindings[--m_size];
|
||||
@@ -186,6 +341,13 @@ private:
|
||||
}
|
||||
|
||||
std::array<Binding, CONTROL_INFO.size()> m_bindings {};
|
||||
std::array<std::size_t, PAD_SOURCES.size()> m_pad_controls = [] {
|
||||
std::array<std::size_t, PAD_SOURCES.size()> a {};
|
||||
a.fill(INVALID_CONTROL);
|
||||
return a;
|
||||
}();
|
||||
bool m_custom = false;
|
||||
bool m_custom_pad = false;
|
||||
std::size_t m_size = 0;
|
||||
float m_mouse_sensitivity = 1.0f;
|
||||
};
|
||||
@@ -211,6 +373,39 @@ void SetTouchPad(float x, bool down) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 0, down, x, 0.5f);
|
||||
}
|
||||
|
||||
void SetSwipeTouchPad(bool down, float x, float y) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 1, down, x, y);
|
||||
}
|
||||
|
||||
void RecordMouseTouchPadPosition(float x, float y) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (g_mouse_window != nullptr) {
|
||||
SDL_GetWindowSize(g_mouse_window, &width, &height);
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
x = std::clamp(x / static_cast<float>(width), 0.0f, 1.0f);
|
||||
y = std::clamp(y / static_cast<float>(height), 0.0f, 1.0f);
|
||||
} else {
|
||||
x = 0.5f;
|
||||
y = 0.5f;
|
||||
}
|
||||
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
g_mouse_touchpad.x = x;
|
||||
g_mouse_touchpad.y = y;
|
||||
g_mouse_touchpad.position_valid = true;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
|
||||
bool TouchPadModeActive() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
return g_mouse_touchpad.enabled || g_gamepad_touchpad.enabled;
|
||||
}
|
||||
|
||||
uint32_t DefaultKeyboardButton(int key_code) {
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_UP: return Controller::PAD_BUTTON_UP;
|
||||
@@ -270,6 +465,18 @@ void SetControl(std::size_t control, bool down) {
|
||||
SetButton(info.button, down);
|
||||
return;
|
||||
}
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(Controller::HOST_INPUT_CONTROLLER_ID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
return;
|
||||
@@ -297,6 +504,11 @@ void DefaultKeyboardInput(int key_code, bool down) {
|
||||
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
|
||||
case SDLK_F6:
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
case SDLK_TAB: SetTouchPad(0.75f, down); return;
|
||||
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
|
||||
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
|
||||
@@ -399,8 +611,134 @@ bool IsCursorActivity(const SDL_Event& event) {
|
||||
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
|
||||
}
|
||||
|
||||
std::array<int, 2> GamepadLeftStick(int id) {
|
||||
auto* pad = SDL_GetGamepadFromID(id);
|
||||
if (pad == nullptr) {
|
||||
return {128, 128};
|
||||
}
|
||||
const auto value = [pad](SDL_GamepadAxis axis) {
|
||||
return Controller::controller_get_axis(SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX,
|
||||
SDL_GetGamepadAxis(pad, axis));
|
||||
};
|
||||
return {value(SDL_GAMEPAD_AXIS_LEFTX), value(SDL_GAMEPAD_AXIS_LEFTY)};
|
||||
}
|
||||
|
||||
void ApplyPadControl(int id, std::size_t control, std::size_t source, bool down) {
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(id);
|
||||
}
|
||||
} else if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleGamepadTouchPad(id);
|
||||
}
|
||||
} else if (info.button == Controller::PAD_BUTTON_CROSS && HostInputGamepadDrawButton(id, down)) {
|
||||
return;
|
||||
} else if (info.button == Controller::PAD_BUTTON_TOUCH_PAD) {
|
||||
// Real touchpads report their own finger position; other inputs emulate a click.
|
||||
if (source != PAD_SOURCES.size() && PAD_SOURCES[source].button != SDL_GAMEPAD_BUTTON_TOUCHPAD) {
|
||||
Controller::SetTouchPad(id, 0, down, info.touch_x != 0.0f ? info.touch_x : 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(id, Controller::PAD_BUTTON_TOUCH_PAD, down);
|
||||
} else if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, down ? 255 : 0);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, down ? 255 : 0);
|
||||
} else if (info.button != 0) {
|
||||
Controller::SetButton(id, info.button, down);
|
||||
} else if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
}
|
||||
}
|
||||
|
||||
int PollGamepadTouchPad(uint64_t now_ms) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
int id = -1;
|
||||
bool drawing = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled) {
|
||||
return -1;
|
||||
}
|
||||
if (now_ms < g_gamepad_touchpad.next_poll) {
|
||||
return static_cast<int>(g_gamepad_touchpad.next_poll - now_ms);
|
||||
}
|
||||
|
||||
const float dt = g_gamepad_touchpad.last_poll == 0 || now_ms <= g_gamepad_touchpad.last_poll
|
||||
? 0.008f
|
||||
: std::min(static_cast<float>(now_ms - g_gamepad_touchpad.last_poll) *
|
||||
0.001f,
|
||||
0.05f);
|
||||
g_gamepad_touchpad.last_poll = now_ms;
|
||||
g_gamepad_touchpad.next_poll = now_ms + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
const auto velocity = [](int axis) {
|
||||
const int offset = axis - 128;
|
||||
if (std::abs(offset) <= GAMEPAD_TOUCHPAD_DEADZONE) {
|
||||
return 0.0f;
|
||||
}
|
||||
const int adjusted =
|
||||
offset > 0 ? offset - GAMEPAD_TOUCHPAD_DEADZONE : offset + GAMEPAD_TOUCHPAD_DEADZONE;
|
||||
return static_cast<float>(adjusted) / static_cast<float>(127 - GAMEPAD_TOUCHPAD_DEADZONE);
|
||||
};
|
||||
const float old_x = g_gamepad_touchpad.x;
|
||||
const float old_y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad.x = std::clamp(
|
||||
old_x + velocity(g_gamepad_touchpad.x_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
g_gamepad_touchpad.y = std::clamp(
|
||||
old_y + velocity(g_gamepad_touchpad.y_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
if (old_x != g_gamepad_touchpad.x || old_y != g_gamepad_touchpad.y) {
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
id = g_gamepad_touchpad.id;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
drawing = g_gamepad_touchpad.drawing;
|
||||
}
|
||||
if (drawing) {
|
||||
Controller::SetTouchPad(id, 1, true, x, y);
|
||||
}
|
||||
return GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HostInputPadButton(int id, int button, bool down) {
|
||||
const auto source = InputMap::SourceFromButton(button);
|
||||
const auto& map = GetInputMap();
|
||||
ApplyPadControl(id, map.PadControl(source), source, down);
|
||||
}
|
||||
|
||||
void HostInputPadTrigger(int id, bool left, int value) {
|
||||
const auto source = left ? LEFT_TRIGGER_SOURCE : RIGHT_TRIGGER_SOURCE;
|
||||
const auto control = GetInputMap().PadControl(source);
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, value);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, value);
|
||||
} else {
|
||||
auto& state = g_pad_trigger_pressed[id][left ? 0 : 1];
|
||||
const bool down = state ? value > 32 : value > 96;
|
||||
if (down != state) {
|
||||
state = down;
|
||||
ApplyPadControl(id, control, source, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepad(int id) {
|
||||
g_pad_trigger_pressed.erase(id);
|
||||
HostInputDisconnectGamepadTouchPad(id);
|
||||
}
|
||||
|
||||
void HostInputInit(SDL_Window* window) {
|
||||
GetInputMap();
|
||||
g_mouse_window = window;
|
||||
@@ -410,6 +748,16 @@ void HostInputInit(SDL_Window* window) {
|
||||
}
|
||||
|
||||
void HostInputShutdown() {
|
||||
g_pad_trigger_pressed.clear();
|
||||
HostInputSetTouchPadMode(false);
|
||||
int gamepad_touch_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
gamepad_touch_id = g_gamepad_touchpad.enabled ? g_gamepad_touchpad.id : -1;
|
||||
}
|
||||
if (gamepad_touch_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_touch_id);
|
||||
}
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
CenterMouseStick();
|
||||
@@ -438,6 +786,219 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputMouseTouchPad(bool down, float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
if (down && !g_mouse_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_mouse_touchpad.drawing = down;
|
||||
g_mouse_touchpad.revision++;
|
||||
x = g_mouse_touchpad.x;
|
||||
y = g_mouse_touchpad.y;
|
||||
}
|
||||
SetSwipeTouchPad(down, x, y);
|
||||
}
|
||||
|
||||
void HostInputMouseMotion(float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled && g_mouse_touchpad.drawing) {
|
||||
SetSwipeTouchPad(true, g_mouse_touchpad.x, g_mouse_touchpad.y);
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputSetTouchPadMode(bool enabled) {
|
||||
if (enabled) {
|
||||
int gamepad_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
gamepad_id = g_gamepad_touchpad.id;
|
||||
}
|
||||
}
|
||||
if (gamepad_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_id);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled == enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
(void) SDL_GetMouseState(&x, &y);
|
||||
HostInputMouseTouchPad(false, x, y);
|
||||
}
|
||||
bool show_system_cursor = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
g_mouse_touchpad.enabled = enabled;
|
||||
g_mouse_touchpad.cursor_visible = true;
|
||||
g_mouse_touchpad.drawing = false;
|
||||
g_mouse_touchpad.position_valid = false;
|
||||
g_mouse_touchpad.gesture++;
|
||||
g_mouse_touchpad.revision++;
|
||||
show_system_cursor = !enabled && !g_gamepad_touchpad.enabled;
|
||||
}
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else if (show_system_cursor && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
LOGF("Mouse touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void HostInputToggleTouchPadMode() {
|
||||
bool enabled = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
enabled = !g_mouse_touchpad.enabled;
|
||||
}
|
||||
HostInputSetTouchPadMode(enabled);
|
||||
}
|
||||
|
||||
void HostInputToggleGamepadTouchPad(int id) {
|
||||
bool mouse_mode = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
mouse_mode = g_mouse_touchpad.enabled;
|
||||
}
|
||||
if (mouse_mode) {
|
||||
HostInputSetTouchPadMode(false);
|
||||
}
|
||||
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
bool enabled = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled && g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
enabled = !g_gamepad_touchpad.enabled;
|
||||
if (enabled) {
|
||||
g_gamepad_touchpad = {};
|
||||
g_gamepad_touchpad.enabled = true;
|
||||
g_gamepad_touchpad.id = id;
|
||||
g_gamepad_touchpad.x_axis = stick[0];
|
||||
g_gamepad_touchpad.y_axis = stick[1];
|
||||
g_gamepad_touchpad.last_poll = SDL_GetTicks();
|
||||
g_gamepad_touchpad.next_poll = g_gamepad_touchpad.last_poll + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
g_mouse_touchpad.gesture++;
|
||||
} else {
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
}
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, enabled ? 128 : stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, enabled ? 128 : stick[1]);
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for gamepad touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
if (!Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad touchpad mode: %s\n",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
LOGF("Gamepad touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool HostInputGamepadDrawButton(int id, bool down) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
if (!down && !g_gamepad_touchpad.drawing) {
|
||||
return false;
|
||||
}
|
||||
if (down && !g_gamepad_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_gamepad_touchpad.drawing = down;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, down, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HostInputGamepadAxis(int id, bool horizontal, int value) {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
(horizontal ? g_gamepad_touchpad.x_axis : g_gamepad_touchpad.y_axis) = std::clamp(value, 0, 255);
|
||||
g_gamepad_touchpad.next_poll = SDL_GetTicks();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepadTouchPad(int id) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, stick[1]);
|
||||
if (!TouchPadModeActive() && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad disconnect: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
HostInputTouchPadVisualState GetHostInputTouchPadVisualState() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
HostInputTouchPadVisualState state {};
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
state.enabled = true;
|
||||
state.cursor_visible = true;
|
||||
state.drawing = g_gamepad_touchpad.drawing;
|
||||
state.x = g_gamepad_touchpad.x;
|
||||
state.y = g_gamepad_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = true;
|
||||
return state;
|
||||
}
|
||||
state.enabled = g_mouse_touchpad.enabled;
|
||||
state.cursor_visible = g_mouse_touchpad.cursor_visible;
|
||||
state.drawing = g_mouse_touchpad.drawing;
|
||||
state.x = g_mouse_touchpad.x;
|
||||
state.y = g_mouse_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = g_mouse_touchpad.position_valid;
|
||||
return state;
|
||||
}
|
||||
|
||||
void HostInputToggleMouseToJoystick() {
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
@@ -467,16 +1028,19 @@ bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
timeout = PollMouse(SDL_GetTicks());
|
||||
}
|
||||
|
||||
if (g_cursor_hide_at != 0) {
|
||||
if (g_cursor_hide_at != 0 && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
const int cursor_timeout =
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
if (const int touch_timeout = PollGamepadTouchPad(SDL_GetTicks()); touch_timeout >= 0) {
|
||||
timeout = timeout < 0 ? touch_timeout : std::min(timeout, touch_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
if (Config::HideCursorEnabled() && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
|
||||
SDL_GetWindowFromEvent(event) == g_mouse_window) {
|
||||
|
||||
@@ -8,10 +8,33 @@ union SDL_Event;
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct HostInputTouchPadVisualState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
void HostInputInit(SDL_Window* window);
|
||||
void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputMouseTouchPad(bool down, float x, float y);
|
||||
void HostInputMouseMotion(float x, float y);
|
||||
void HostInputPadButton(int id, int sdl_button, bool down);
|
||||
void HostInputPadTrigger(int id, bool left, int value);
|
||||
void HostInputDisconnectGamepad(int id);
|
||||
void HostInputSetTouchPadMode(bool enabled);
|
||||
void HostInputToggleTouchPadMode();
|
||||
void HostInputToggleGamepadTouchPad(int id);
|
||||
[[nodiscard]] bool HostInputGamepadDrawButton(int id, bool down);
|
||||
[[nodiscard]] bool HostInputGamepadAxis(int id, bool horizontal, int value);
|
||||
void HostInputDisconnectGamepadTouchPad(int id);
|
||||
[[nodiscard]] HostInputTouchPadVisualState GetHostInputTouchPadVisualState();
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
|
||||
@@ -945,7 +945,6 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
auto* frame = m_impl->frames.Acquire({width, height}, format);
|
||||
frame->hdr = false;
|
||||
frame->blank = true;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
|
||||
vk::ClearColorValue clear {};
|
||||
clear.float32[3] = opaque ? 1.0f : 0.0f;
|
||||
@@ -953,6 +952,7 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
EXIT_IF(producer->IsInvalid());
|
||||
frame->Clear(*producer, clear);
|
||||
} else {
|
||||
Common::LockGuard lock(m_impl->present_mutex);
|
||||
auto& command = m_impl->present_scheduler.BeginCommand();
|
||||
frame->Clear(command, clear);
|
||||
frame->present_tick = m_impl->present_scheduler.Submit();
|
||||
@@ -1036,18 +1036,17 @@ void Presenter::Impl::Present() {
|
||||
RecoverSwapchain(status);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
Common::LockGuard render_lock(renderer.GetMutex());
|
||||
auto& command = present_scheduler.BeginCommand();
|
||||
const bool draw_system_overlay =
|
||||
overlay_visual.active && swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
// present_mutex owns the prepared frames and this scheduler. Only queue submission
|
||||
// is shared with rendering, and it has its own queue_mutex.
|
||||
auto& command = present_scheduler.BeginCommand();
|
||||
const bool draw_system_overlay =
|
||||
overlay_visual.active && swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
}
|
||||
status = swapchain.Present();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "graphics/presentation/window.h"
|
||||
#include "graphics/presentation/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -54,25 +54,6 @@ struct EventKeyboard {
|
||||
double timestamp_seconds;
|
||||
};
|
||||
|
||||
static uint32_t ControllerButtonToPadButton(int button) {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH: return Controller::PAD_BUTTON_CROSS;
|
||||
case SDL_GAMEPAD_BUTTON_EAST: return Controller::PAD_BUTTON_CIRCLE;
|
||||
case SDL_GAMEPAD_BUTTON_WEST: return Controller::PAD_BUTTON_SQUARE;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH: return Controller::PAD_BUTTON_TRIANGLE;
|
||||
case SDL_GAMEPAD_BUTTON_START: return Controller::PAD_BUTTON_OPTIONS;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_STICK: return Controller::PAD_BUTTON_L3;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return Controller::PAD_BUTTON_R3;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return Controller::PAD_BUTTON_L1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Controller::PAD_BUTTON_R1;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP: return Controller::PAD_BUTTON_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return Controller::PAD_BUTTON_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return Controller::PAD_BUTTON_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return Controller::PAD_BUTTON_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_TOUCHPAD: return Controller::PAD_BUTTON_TOUCH_PAD;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static Controller::Axis ControllerAxisFromSdl(int axis_id) {
|
||||
switch (axis_id) {
|
||||
@@ -268,6 +249,15 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
|
||||
}
|
||||
|
||||
HostInputMouseButton(mouse_button, mb.down);
|
||||
if (!mb.touch && mb.left) {
|
||||
HostInputMouseTouchPad(mb.down, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
} else if (mb.motion && !mb.touch) {
|
||||
if (mb.left) {
|
||||
HostInputMouseTouchPad(true, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
} else {
|
||||
HostInputMouseMotion(static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +306,7 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.removed) {
|
||||
HostInputDisconnectGamepad(f.id);
|
||||
if (auto* pad = SDL_GetGamepadFromID(f.id); pad != nullptr) {
|
||||
Controller::Disconnect(f.id);
|
||||
SDL_CloseGamepad(pad);
|
||||
@@ -323,30 +314,22 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.down || f.up) {
|
||||
const auto button = ControllerButtonToPadButton(f.button);
|
||||
if (button != 0) {
|
||||
Controller::SetButton(f.id, button, f.down);
|
||||
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
|
||||
// blocks games that require it. Use their Back/Select button as a centered click.
|
||||
auto* pad = SDL_GetGamepadFromID(f.id);
|
||||
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
|
||||
if (f.down) {
|
||||
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
|
||||
if (f.up) {
|
||||
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
HostInputPadButton(f.id, f.button, f.down);
|
||||
}
|
||||
|
||||
if (f.axis) {
|
||||
const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
if (axis != Controller::Axis::AxisMax) {
|
||||
Controller::SetAxis(f.id, axis,
|
||||
if (f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ||
|
||||
f.axis_id == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
HostInputPadTrigger(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
ControllerAxisValueFromSdl(f.axis_id, f.axis_value));
|
||||
} else if (const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
axis != Controller::Axis::AxisMax) {
|
||||
const int value = ControllerAxisValueFromSdl(f.axis_id, f.axis_value);
|
||||
if ((f.axis_id == SDL_GAMEPAD_AXIS_LEFTX || f.axis_id == SDL_GAMEPAD_AXIS_LEFTY) &&
|
||||
HostInputGamepadAxis(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFTX, value)) {
|
||||
return;
|
||||
}
|
||||
Controller::SetAxis(f.id, axis, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +414,7 @@ void WindowContext::ProcessWindowEvent(const SDL_WindowEvent& event) {
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
LOGF("Mouse left window %" PRIu32 "\n", window_event.windowID);
|
||||
HostInputSetTouchPadMode(false);
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
LOGF("Window %" PRIu32 " gained keyboard focus\n", window_event.windowID);
|
||||
|
||||
@@ -230,8 +230,11 @@ uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1)
|
||||
}
|
||||
|
||||
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
|
||||
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
|
||||
clamped);
|
||||
}
|
||||
|
||||
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
|
||||
@@ -52,9 +52,12 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
|
||||
const auto value = state.builder.AllocateId();
|
||||
const auto bits = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, TypeBool(state), value, variable);
|
||||
// PS5 initializes v_front_face with float +1.0/-1.0 bits.
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value,
|
||||
ConstantU32(state, 0x3f800000u), ConstantU32(state, 0xbf800000u));
|
||||
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects uint masks or float +/-1.
|
||||
const bool all_bits =
|
||||
state.input_info.pixel != nullptr && state.input_info.pixel->ps_front_face_all_bits;
|
||||
const auto front = ConstantU32(state, all_bits ? 0xffffffffu : 0x3f800000u);
|
||||
const auto back = ConstantU32(state, all_bits ? 0u : 0xbf800000u);
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), bits, value, front, back);
|
||||
return bits;
|
||||
}
|
||||
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
|
||||
|
||||
@@ -296,6 +296,7 @@ inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicIAdd64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
|
||||
@@ -325,6 +326,7 @@ inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
|
||||
|
||||
@@ -504,6 +504,7 @@ uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFor
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap32:
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
@@ -516,6 +517,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicIAdd64:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd32:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd64:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd64:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
case IR::ValueOpcode::BufferAtomicISub32:
|
||||
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
|
||||
@@ -560,8 +562,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
|
||||
const bool wide = inst.GetType() == IR::Type::U64;
|
||||
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicCmpSwap32) {
|
||||
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
|
||||
std::swap(desired, comparator);
|
||||
}
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
|
||||
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
|
||||
|
||||
@@ -67,6 +67,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
|
||||
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
||||
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
|
||||
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
@@ -114,6 +115,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
{0x27u, Opcode::DS_MIN_RTN_U32, 1, 32}, {0x28u, Opcode::DS_MAX_RTN_U32, 1, 32},
|
||||
{0x29u, Opcode::DS_AND_RTN_B32, 1, 32}, {0x2au, Opcode::DS_OR_RTN_B32, 1, 32},
|
||||
{0x2bu, Opcode::DS_XOR_RTN_B32, 1, 32}, {0x2du, Opcode::DS_WRXCHG_RTN_B32, 1, 32},
|
||||
{0x30u, Opcode::DS_CMPST_RTN_B32, 1, 32},
|
||||
{0x35u, Opcode::DS_SWIZZLE_B32, 1, 32}, {0x36u, Opcode::DS_READ_B32, 1, 32},
|
||||
{0x37u, Opcode::DS_READ2_B32, 2, 32}, {0x38u, Opcode::DS_READ2ST64_B32, 2, 32},
|
||||
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
|
||||
@@ -200,6 +202,7 @@ bool IsDsAtomicOpcode(Opcode opcode) {
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
@@ -220,6 +223,7 @@ uint32_t DsSourceCount(Opcode opcode) {
|
||||
case Opcode::DS_WRITE2ST64_B32:
|
||||
case Opcode::DS_WRITE2_B64:
|
||||
case Opcode::DS_WRITE2ST64_B64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_MSKOR_B32: return 3u;
|
||||
case Opcode::DS_MIN_F32:
|
||||
@@ -454,6 +458,9 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
|
||||
}
|
||||
if (inst.gds && inst.opcode == Opcode::DS_CMPST_RTN_B32) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS compare-store return B32 GDS variant is not implemented");
|
||||
}
|
||||
if (inst.gds &&
|
||||
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_PERMUTE_B32 ||
|
||||
inst.opcode == Opcode::DS_BPERMUTE_B32 ||
|
||||
|
||||
@@ -670,6 +670,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::BUFFER_ATOMIC_CMPSWAP:
|
||||
case Opcode::BUFFER_ATOMIC_SWAP_X2:
|
||||
case Opcode::BUFFER_ATOMIC_ADD:
|
||||
case Opcode::BUFFER_ATOMIC_ADD_X2:
|
||||
case Opcode::BUFFER_ATOMIC_SUB:
|
||||
case Opcode::BUFFER_ATOMIC_SMIN:
|
||||
case Opcode::BUFFER_ATOMIC_UMIN:
|
||||
@@ -717,6 +718,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
|
||||
@@ -586,6 +586,7 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_CMPSWAP,
|
||||
BUFFER_ATOMIC_SWAP_X2,
|
||||
BUFFER_ATOMIC_ADD,
|
||||
BUFFER_ATOMIC_ADD_X2,
|
||||
BUFFER_ATOMIC_SUB,
|
||||
BUFFER_ATOMIC_SMIN,
|
||||
BUFFER_ATOMIC_UMIN,
|
||||
@@ -631,6 +632,7 @@ enum class Opcode {
|
||||
DS_MAX_U32,
|
||||
DS_MAX_RTN_U32,
|
||||
DS_MAX_RTN_U64,
|
||||
DS_CMPST_RTN_B32,
|
||||
DS_CMPST_RTN_B64,
|
||||
DS_AND_B32,
|
||||
DS_AND_RTN_B32,
|
||||
|
||||
@@ -1046,6 +1046,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMin32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_FMAX:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMax32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_ADD_X2:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicIAdd64);
|
||||
|
||||
case Decoder::Opcode::DS_ADD_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
|
||||
@@ -1083,6 +1085,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap32, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
|
||||
case Decoder::Opcode::DS_AND_B32:
|
||||
|
||||
@@ -25,6 +25,7 @@ struct UniformFill {
|
||||
std::array<uint32_t, 3> group_stride {};
|
||||
uint32_t words = 0;
|
||||
uint32_t value = 0;
|
||||
uint32_t bounded_elements = 0;
|
||||
|
||||
bool operator==(const UniformFill&) const = default;
|
||||
};
|
||||
|
||||
@@ -622,6 +622,8 @@ struct CompiledShaderInfo {
|
||||
struct UniformFillPlan {
|
||||
UniformFill fill;
|
||||
std::array<Value, 4> values;
|
||||
Value pattern_size;
|
||||
Value element_count;
|
||||
};
|
||||
|
||||
// Resource analysis retained by the shader cache. It owns immutable descriptor/SRT,
|
||||
|
||||
@@ -118,6 +118,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::BufferAtomicCmpSwap32:
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicIAdd32:
|
||||
case ValueOpcode::BufferAtomicIAdd64:
|
||||
case ValueOpcode::BufferAtomicISub32:
|
||||
case ValueOpcode::BufferAtomicSMin32:
|
||||
case ValueOpcode::BufferAtomicUMin32:
|
||||
@@ -137,6 +138,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
uint32_t BufferComponentCount(ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicIAdd64:
|
||||
case ValueOpcode::BufferAtomicAnd64:
|
||||
case ValueOpcode::BufferAtomicOr64:
|
||||
case ValueOpcode::LoadBufferU32x2:
|
||||
@@ -176,6 +178,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::SharedAtomicSMax32:
|
||||
case ValueOpcode::SharedAtomicUMax32:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap32:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::SharedAtomicAnd32:
|
||||
case ValueOpcode::SharedAtomicOr32:
|
||||
|
||||
@@ -268,6 +268,7 @@ VALUE_OPCODE(BufferAtomicSwap32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicCmpSwap32, U32, BufferResource, U32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSwap64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicIAdd32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
@@ -306,6 +307,7 @@ VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
|
||||
|
||||
@@ -608,13 +608,23 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
ReplaceBinaryIdentity(inst, Type::U32, UINT32_MAX)) return;
|
||||
for (uint32_t arm = 0; arm < 2u; ++arm) {
|
||||
const auto mask = Arg(inst, arm);
|
||||
if (!IsImmediate(mask, Type::U32)) continue;
|
||||
if (mask.U32() == 0u) {
|
||||
Replace(inst, Value(0u));
|
||||
return;
|
||||
}
|
||||
const auto* shifted = Arg(inst, arm ^ 1u).TryInstruction();
|
||||
if (!IsImmediate(mask, Type::U32) || shifted == nullptr ||
|
||||
shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || !HasOneUse(*shifted)) continue;
|
||||
if (shifted == nullptr || shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32) continue;
|
||||
const auto shift = Arg(*shifted, 1);
|
||||
if (!IsImmediate(shift, Type::U32) || shift.U32() >= 32u) continue;
|
||||
if (!IsImmediate(shift, Type::U32)) continue;
|
||||
const auto shifted_mask = mask.U32() >> (shift.U32() & 31u);
|
||||
if (shifted_mask == 0u) {
|
||||
Replace(inst, Value(0u));
|
||||
return;
|
||||
}
|
||||
if (!HasOneUse(*shifted) || shift.U32() >= 32u) continue;
|
||||
const auto masked = worklist.Prepend(block, instruction, ValueOpcode::BitwiseAnd32,
|
||||
{Arg(*shifted, 0), Value(mask.U32() >> shift.U32())});
|
||||
{Arg(*shifted, 0), Value(shifted_mask)});
|
||||
Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::ShiftLeftLogical32,
|
||||
{Value(&*masked), shift})));
|
||||
return;
|
||||
|
||||
@@ -1424,10 +1424,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
|
||||
plan.value_storage.erase(first, plan.value_storage.end());
|
||||
}
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
}
|
||||
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
|
||||
? program.uniform_fill : AnalyzeUniformFill(program);
|
||||
for (auto& value: plan.uniform_fill.values) value = Clone(value);
|
||||
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
|
||||
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
|
||||
plan.capture_specialization_reads |=
|
||||
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
|
||||
if (plan.capture_specialization_reads) {
|
||||
@@ -1513,15 +1514,21 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
}
|
||||
snapshot.uniform_fill = {};
|
||||
const auto& fill = program.uniform_fill;
|
||||
const auto words = fill.fill.words;
|
||||
auto words = fill.fill.words;
|
||||
std::array<uint32_t, 4> stored {};
|
||||
bool uniform_fill = words != 0;
|
||||
if (uniform_fill && !fill.pattern_size.IsEmpty())
|
||||
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
|
||||
uint32_t elements = 0;
|
||||
if (uniform_fill && !fill.element_count.IsEmpty())
|
||||
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
|
||||
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
|
||||
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
|
||||
}
|
||||
if (uniform_fill) {
|
||||
snapshot.uniform_fill = fill.fill;
|
||||
snapshot.uniform_fill.value = stored[0];
|
||||
snapshot.uniform_fill.bounded_elements = elements;
|
||||
}
|
||||
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
|
||||
if (source >= program.descriptor_sources.size()) {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#define XXH_INLINE_ALL
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
namespace {
|
||||
@@ -4040,11 +4042,71 @@ private:
|
||||
bool m_shader_writes = false;
|
||||
};
|
||||
|
||||
UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& decoded,
|
||||
ShaderStageInputInfo input) {
|
||||
struct Pattern {
|
||||
size_t code_words;
|
||||
uint64_t hash;
|
||||
uint32_t wave_size;
|
||||
bool bounded;
|
||||
};
|
||||
constexpr std::array patterns {
|
||||
// Bounded 64-lane variant.
|
||||
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size = 64, .bounded = true},
|
||||
// Dispatch thread dimensions, 64-lane variants.
|
||||
Pattern {.code_words = 42, .hash = 0x92ce57f21310ef02ull, .wave_size = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size = 64, .bounded = false},
|
||||
// Dispatch thread dimensions, 32-lane variant.
|
||||
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size = 32, .bounded = false},
|
||||
};
|
||||
if (program.stage != ShaderType::Compute || input.compute == nullptr ||
|
||||
decoded.instructions.empty() || program.user_data_base != 0 || program.user_data_count < 10 ||
|
||||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
|
||||
const auto& cs = *input.compute;
|
||||
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
|
||||
cs.group_id[0] && !cs.group_id[1] && !cs.group_id[2] && cs.threads_num[0] == 64 &&
|
||||
cs.threads_num[1] == 1 && cs.threads_num[2] == 1 && cs.float_mode == 0xc0;
|
||||
if (!matching_inputs) return {};
|
||||
const auto& last = decoded.instructions.back();
|
||||
const size_t words = last.pc / sizeof(uint32_t) + last.word_count;
|
||||
if (words > decoded.code.size() ||
|
||||
std::ranges::find(patterns, words, &Pattern::code_words) == patterns.end()) return {};
|
||||
// Hash executable instructions through S_ENDPGM, excluding binary metadata.
|
||||
// Verified variants implement Destination[id] = value[id % pattern_size].
|
||||
const auto hash = XXH3_64bits(decoded.code.data(), words * sizeof(uint32_t));
|
||||
const auto pattern = std::ranges::find_if(patterns, [&](const Pattern& candidate) {
|
||||
return candidate.code_words == words && candidate.hash == hash;
|
||||
});
|
||||
if (pattern == patterns.end() || program.wave_size != pattern->wave_size ||
|
||||
cs.dispatch_thread_dimensions != !pattern->bounded) return {};
|
||||
std::array<Value, 10> userdata;
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::GetUserData) continue;
|
||||
const auto reg = RegIndex(inst.Arg(0).ScalarRegister());
|
||||
if (reg < userdata.size()) userdata[reg] = Value(&inst);
|
||||
}
|
||||
}
|
||||
for (uint32_t reg = 4; reg < 10; ++reg)
|
||||
if ((reg != 8 || pattern->bounded) && userdata[reg].IsEmpty()) return {};
|
||||
UniformFillPlan result;
|
||||
result.fill.kind = UniformFillKind::Buffer;
|
||||
result.fill.words = 1;
|
||||
result.fill.group_stride[0] = 64;
|
||||
std::copy_n(userdata.begin() + 4, 4, result.values.begin());
|
||||
result.pattern_size = userdata[9];
|
||||
// Only the bounded variant reads an explicit count; the other uses thread dimensions.
|
||||
if (pattern->bounded) result.element_count = userdata[8];
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
|
||||
ShaderStageInputInfo input) {
|
||||
Tracker(program, decoded, native_cfg, input).Run();
|
||||
program.uniform_fill = RecognizePatternFill(program, decoded, input);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
|
||||
@@ -344,6 +344,8 @@ static void ShaderGetStaticInputInfoPS(
|
||||
ps_info.ps_pos_z = (active_inputs & 0x00000400u) != 0;
|
||||
ps_info.ps_pos_w = (active_inputs & 0x00000800u) != 0;
|
||||
ps_info.ps_front_face = (active_inputs & 0x00001000u) != 0;
|
||||
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects integer masks instead of float +/-1.
|
||||
ps_info.ps_front_face_all_bits = (sh.baryc_cntl & 0x01000000u) != 0;
|
||||
ps_info.ps_ancillary = (active_inputs & 0x00002000u) != 0;
|
||||
ps_info.ps_sample_shading = (active_inputs & 0x00000011u) != 0;
|
||||
ps_info.ps_no_perspective = (sh.ps_input_ena & sh.ps_input_addr & 0x00000020u) != 0;
|
||||
@@ -464,6 +466,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_z));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_w));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face_all_bits));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_ancillary));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_no_perspective));
|
||||
key.push_back(static_cast<uint32_t>(info.parameter_mode));
|
||||
@@ -737,6 +740,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_pos_z = %s\n"
|
||||
"\t ps_pos_w = %s\n"
|
||||
"\t ps_front_face = %s\n"
|
||||
"\t ps_front_face_all_bits = %s\n"
|
||||
"\t ps_ancillary = %s\n"
|
||||
"\t ps_sample_shading = %s\n"
|
||||
"\t ps_no_perspective = %s\n"
|
||||
@@ -745,11 +749,10 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_execute_on_noop = %s\n",
|
||||
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
|
||||
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
|
||||
info.ps_perspective_centroid_vgpr,
|
||||
info.ps_pos_x ? "true" : "false",
|
||||
info.ps_perspective_centroid_vgpr, info.ps_pos_x ? "true" : "false",
|
||||
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
|
||||
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
|
||||
info.ps_ancillary ? "true" : "false",
|
||||
info.ps_front_face_all_bits ? "true" : "false", info.ps_ancillary ? "true" : "false",
|
||||
info.ps_sample_shading ? "true" : "false", info.ps_no_perspective ? "true" : "false",
|
||||
info.ps_pixel_kill_enable ? "true" : "false", info.ps_early_z ? "true" : "false",
|
||||
info.ps_execute_on_noop ? "true" : "false");
|
||||
|
||||
@@ -196,6 +196,7 @@ struct ShaderPixelInputInfo {
|
||||
bool ps_pos_z = false;
|
||||
bool ps_pos_w = false;
|
||||
bool ps_front_face = false;
|
||||
bool ps_front_face_all_bits = false;
|
||||
bool ps_ancillary = false;
|
||||
bool ps_no_perspective = false;
|
||||
ShaderPixelParameterMode parameter_mode = ShaderPixelParameterMode::FirstVertex;
|
||||
|
||||
@@ -338,14 +338,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat
|
||||
std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
// Guests join mount points and names with extra slashes (e.g. "/addcont0//file").
|
||||
std::string normalized = Common::FixFilenameSlash(mounted_name);
|
||||
normalized.erase(std::unique(normalized.begin(), normalized.end(),
|
||||
[](char a, char b) { return a == '/' && b == '/'; }),
|
||||
normalized.end());
|
||||
|
||||
// Match the entire guest path so a mount root works with or without a trailing slash.
|
||||
const auto mounted_path = Common::FixDirectorySlash(mounted_name);
|
||||
const auto mounted_path = Common::FixDirectorySlash(normalized);
|
||||
const auto it = std::find_if(
|
||||
m_mount_pairs.begin(), m_mount_pairs.end(),
|
||||
[&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); });
|
||||
if (it != m_mount_pairs.end()) {
|
||||
const auto& p = *it;
|
||||
auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size());
|
||||
auto rel_path = Common::RemoveFirst(normalized, p.point.size());
|
||||
while (rel_path.starts_with('/')) {
|
||||
rel_path = Common::RemoveFirst(rel_path, 1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
#define EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
|
||||
#include "common/abi.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
|
||||
using KernelModule = int32_t;
|
||||
struct KernelLoadModuleOpt;
|
||||
struct KernelUnloadModuleOpt;
|
||||
|
||||
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res);
|
||||
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res);
|
||||
|
||||
} // namespace Libs::LibKernel
|
||||
|
||||
#endif // EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
@@ -21,7 +21,8 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static
|
||||
LibAtrac9 kyty_git_version common)
|
||||
if(LINUX)
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
target_link_libraries(launcher Qt6::DBus)
|
||||
@@ -76,7 +77,7 @@ if(WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(launcher_name "launcher")
|
||||
set(launcher_name "kyty_launcher")
|
||||
if(APPLE)
|
||||
set(launcher_name "KytyPS5")
|
||||
endif()
|
||||
@@ -101,10 +102,20 @@ install(TARGETS launcher
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION .)
|
||||
|
||||
if(NOT APPLE)
|
||||
# Older releases require the previous executable name when validating updates.
|
||||
install(CODE "
|
||||
file(INSTALL DESTINATION \"\${CMAKE_INSTALL_PREFIX}\"
|
||||
TYPE PROGRAM RENAME \"launcher${CMAKE_EXECUTABLE_SUFFIX}\"
|
||||
FILES \"\${CMAKE_INSTALL_PREFIX}/${launcher_name}${CMAKE_EXECUTABLE_SUFFIX}\")
|
||||
")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/cursors" DESTINATION "KytyPS5.app/Contents/Resources/assets")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
if(NOT KYTY_MACDEPLOYQT)
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
void SetSuspended(bool suspended);
|
||||
|
||||
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
|
||||
[[nodiscard]] const QSet<QString>& RawPressed() const { return m_raw_pressed; }
|
||||
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
|
||||
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
|
||||
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
|
||||
@@ -44,6 +45,7 @@ private:
|
||||
bool m_preview_enabled = false;
|
||||
bool m_suspended = false;
|
||||
QSet<QString> m_pressed;
|
||||
QSet<QString> m_raw_pressed;
|
||||
QPointF m_left_stick;
|
||||
QPointF m_right_stick;
|
||||
QString m_device_name;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class ControllerPreview;
|
||||
class DualSenseWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
class InputMappingDialog final: public QDialog {
|
||||
public:
|
||||
@@ -20,20 +25,30 @@ public:
|
||||
[[nodiscard]] QStringList Mapping() const;
|
||||
|
||||
private:
|
||||
void ChangeBinding(const QString& id);
|
||||
void RestoreDefaults();
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
void UpdateHighlights();
|
||||
QWidget* BuildTab(bool pad);
|
||||
void ChangeKeyboardBinding(const QString& id);
|
||||
void ChangePadBinding(const QString& id);
|
||||
void ClearBinding(bool pad, const QString& id);
|
||||
void RestoreDefaults();
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
void UpdateHighlights();
|
||||
|
||||
ControllerPreview* m_preview;
|
||||
DualSenseWidget* m_controller = nullptr;
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
QHash<QString, QString> m_bindings;
|
||||
QHash<QString, QString> m_pad_bindings;
|
||||
QHash<QString, QPushButton*> m_buttons;
|
||||
QHash<QString, QPushButton*> m_pad_buttons;
|
||||
QSet<QString> m_pad_pressed;
|
||||
QString m_selected;
|
||||
QLabel* m_status = nullptr;
|
||||
QDoubleSpinBox* m_sensitivity = nullptr;
|
||||
QComboBox* m_particle_skin = nullptr;
|
||||
QCheckBox* m_touch_cursor = nullptr;
|
||||
bool m_custom_bindings = false;
|
||||
bool m_custom_pad = false;
|
||||
};
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFutureWatcher>
|
||||
#include <QObject>
|
||||
#include <QPromise>
|
||||
#include <QString>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct SDL_AudioStream;
|
||||
|
||||
class TitleSound final: public QObject {
|
||||
public:
|
||||
explicit TitleSound(QObject* parent = nullptr);
|
||||
~TitleSound() override;
|
||||
|
||||
void SetTitle(const QString& base);
|
||||
|
||||
private:
|
||||
struct Sound {
|
||||
std::vector<float> pcm;
|
||||
int channels = 0;
|
||||
int rate = 0;
|
||||
};
|
||||
|
||||
static Sound Decode(const QByteArray& data, const QPromise<Sound>& promise);
|
||||
void Stop();
|
||||
|
||||
QThreadPool m_decode_pool;
|
||||
QFutureWatcher<Sound> m_decode;
|
||||
QString m_base;
|
||||
SDL_AudioStream* m_stream = nullptr;
|
||||
bool m_audio_initialized = false;
|
||||
};
|
||||
@@ -20,6 +20,34 @@ constexpr struct {
|
||||
{SDL_GAMEPAD_BUTTON_START, "Options"},
|
||||
};
|
||||
|
||||
// Host-side names; they must match PAD_SOURCES in hostInput.cpp.
|
||||
constexpr struct {
|
||||
SDL_GamepadButton button;
|
||||
const char* name;
|
||||
} RAW_BUTTONS[] = {
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, "South"},
|
||||
{SDL_GAMEPAD_BUTTON_EAST, "East"},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, "West"},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, "North"},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, "Back"},
|
||||
{SDL_GAMEPAD_BUTTON_GUIDE, "Guide"},
|
||||
{SDL_GAMEPAD_BUTTON_START, "Start"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "LeftStick"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_STICK, "RightStick"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LeftShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RightShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DpadUp"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DpadDown"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DpadLeft"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DpadRight"},
|
||||
{SDL_GAMEPAD_BUTTON_MISC1, "Misc1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, "RightPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, "LeftPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, "RightPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, "LeftPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_TOUCHPAD, "Touchpad"},
|
||||
};
|
||||
|
||||
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
|
||||
const int value = SDL_GetGamepadAxis(gamepad, axis);
|
||||
return value / (value < 0 ? 32768.0 : 32767.0);
|
||||
@@ -100,6 +128,7 @@ void ControllerPreview::UpdateRunning() {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
m_pressed.clear();
|
||||
m_raw_pressed.clear();
|
||||
m_left_stick = {};
|
||||
m_right_stick = {};
|
||||
m_device_name.clear();
|
||||
@@ -131,12 +160,25 @@ void ControllerPreview::Refresh() {
|
||||
}
|
||||
|
||||
QSet<QString> pressed;
|
||||
QSet<QString> raw;
|
||||
QPointF left;
|
||||
QPointF right;
|
||||
QString name;
|
||||
if (!m_gamepads.isEmpty()) {
|
||||
auto* gamepad = m_gamepads.first();
|
||||
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
|
||||
for (const auto& binding: RAW_BUTTONS) {
|
||||
if (SDL_GamepadHasButton(gamepad, binding.button) &&
|
||||
SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
raw.insert(QString::fromLatin1(binding.name));
|
||||
}
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("LeftTrigger"));
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("RightTrigger"));
|
||||
}
|
||||
for (const auto& binding: BUTTONS) {
|
||||
if (SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
pressed.insert(QString::fromLatin1(binding.control));
|
||||
@@ -180,9 +222,10 @@ void ControllerPreview::Refresh() {
|
||||
pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
}
|
||||
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
|
||||
name != m_device_name) {
|
||||
if (pressed != m_pressed || raw != m_raw_pressed || left != m_left_stick ||
|
||||
right != m_right_stick || name != m_device_name) {
|
||||
m_pressed = pressed;
|
||||
m_raw_pressed = raw;
|
||||
m_left_stick = left;
|
||||
m_right_stick = right;
|
||||
m_device_name = name;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "inputMappingDialog.h"
|
||||
#include "inputMappingDialog.h"
|
||||
|
||||
#include "controllerPreview.h"
|
||||
#include "dualsenseWidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGridLayout>
|
||||
@@ -16,56 +18,113 @@
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
|
||||
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
|
||||
constexpr char TOUCH_PARTICLE_SKIN[] = "TouchParticleSkin=";
|
||||
constexpr char TOUCH_CURSOR_OFF[] = "TouchCursorImage=Off";
|
||||
constexpr char KEYBOARD_DISABLED[] = "Keyboard=Disabled";
|
||||
constexpr char PAD_DISABLED[] = "Pad=Disabled";
|
||||
|
||||
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
|
||||
constexpr char PAD_PREFIX[] = "Pad:";
|
||||
|
||||
enum Section { Face, Dpad, Shoulders, Sticks, System, LeftStick, RightStick, Shortcuts, SectionCount };
|
||||
|
||||
const char* const SECTION_TITLES[SectionCount] = {
|
||||
QT_TR_NOOP("Face buttons"), QT_TR_NOOP("D-pad"),
|
||||
QT_TR_NOOP("Shoulders && triggers"), QT_TR_NOOP("Stick clicks"),
|
||||
QT_TR_NOOP("System"), QT_TR_NOOP("Left stick"),
|
||||
QT_TR_NOOP("Right stick"), QT_TR_NOOP("In-game shortcuts (cycle)"),
|
||||
};
|
||||
|
||||
// pad_default is null for controls that have no gamepad equivalent.
|
||||
struct PadControl {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* default_binding;
|
||||
Group group;
|
||||
int row;
|
||||
int column;
|
||||
const char* keyboard_default;
|
||||
const char* pad_default;
|
||||
Section section;
|
||||
};
|
||||
|
||||
constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Up", "Up", "Up", Dpad, 0, 1},
|
||||
{"Down", "Down", "Down", Dpad, 2, 1},
|
||||
{"Left", "Left", "Left", Dpad, 1, 0},
|
||||
{"Right", "Right", "Right", Dpad, 1, 2},
|
||||
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
|
||||
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
|
||||
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
|
||||
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
|
||||
{"RightStickUp", "Up", "T", RightStick, 0, 1},
|
||||
{"RightStickDown", "Down", "G", RightStick, 2, 1},
|
||||
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
|
||||
{"RightStickRight", "Right", "H", RightStick, 1, 2},
|
||||
{"Triangle", "Triangle", "I", Face, 0, 1},
|
||||
{"Circle", "Circle", "L", Face, 1, 2},
|
||||
{"Cross", "Cross", "J", Face, 2, 1},
|
||||
{"Square", "Square", "K", Face, 1, 0},
|
||||
{"L1", "L1", "Q", Shoulders, 0, 1},
|
||||
{"R1", "R1", "E", Shoulders, 0, 2},
|
||||
{"L2", "L2", "Z", Shoulders, 0, 0},
|
||||
{"R2", "R2", "C", Shoulders, 0, 3},
|
||||
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
|
||||
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
|
||||
{"Options", "Options", "Return", Center, 0, 1},
|
||||
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
|
||||
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
|
||||
{"Cross", "Cross", "J", "South", Face},
|
||||
{"Circle", "Circle", "L", "East", Face},
|
||||
{"Square", "Square", "K", "West", Face},
|
||||
{"Triangle", "Triangle", "I", "North", Face},
|
||||
{"Up", "Up", "Up", "DpadUp", Dpad},
|
||||
{"Down", "Down", "Down", "DpadDown", Dpad},
|
||||
{"Left", "Left", "Left", "DpadLeft", Dpad},
|
||||
{"Right", "Right", "Right", "DpadRight", Dpad},
|
||||
{"L1", "L1", "Q", "LeftShoulder", Shoulders},
|
||||
{"R1", "R1", "E", "RightShoulder", Shoulders},
|
||||
{"L2", "L2", "Z", "LeftTrigger", Shoulders},
|
||||
{"R2", "R2", "C", "RightTrigger", Shoulders},
|
||||
{"L3", "L3 (left stick click)", "Left Shift", "LeftStick", Sticks},
|
||||
{"R3", "R3 (right stick click)", "Left Ctrl", "RightStick", Sticks},
|
||||
{"Options", "Options", "Return", "Start", System},
|
||||
{"TouchPad", "Touch pad click", "Backspace", "Touchpad", System},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", nullptr, System},
|
||||
{"GyroToggle", "Gyro emulation toggle", "", "Back", System},
|
||||
{"TouchPadMode", "Emulated touch pad mode", "F6", "", System},
|
||||
{"LeftStickUp", "Up", "W", nullptr, LeftStick},
|
||||
{"LeftStickDown", "Down", "S", nullptr, LeftStick},
|
||||
{"LeftStickLeft", "Left", "A", nullptr, LeftStick},
|
||||
{"LeftStickRight", "Right", "D", nullptr, LeftStick},
|
||||
{"RightStickUp", "Up", "T", nullptr, RightStick},
|
||||
{"RightStickDown", "Down", "G", nullptr, RightStick},
|
||||
{"RightStickLeft", "Left", "F", nullptr, RightStick},
|
||||
{"RightStickRight", "Right", "H", nullptr, RightStick},
|
||||
{"SpeakerVolume", "Speaker volume", "1", nullptr, Shortcuts},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", nullptr, Shortcuts},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", nullptr, Shortcuts},
|
||||
};
|
||||
|
||||
struct PadSourceName {
|
||||
const char* id;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
constexpr PadSourceName PAD_SOURCES[] = {
|
||||
{"South", "A / Cross"},
|
||||
{"East", "B / Circle"},
|
||||
{"West", "X / Square"},
|
||||
{"North", "Y / Triangle"},
|
||||
{"Back", "View / Back / Share"},
|
||||
{"Guide", "Guide / Home"},
|
||||
{"Start", "Menu / Start"},
|
||||
{"LeftStick", "Left stick click"},
|
||||
{"RightStick", "Right stick click"},
|
||||
{"LeftShoulder", "LB / L1"},
|
||||
{"RightShoulder", "RB / R1"},
|
||||
{"DpadUp", "D-pad Up"},
|
||||
{"DpadDown", "D-pad Down"},
|
||||
{"DpadLeft", "D-pad Left"},
|
||||
{"DpadRight", "D-pad Right"},
|
||||
{"Misc1", "Share / Misc"},
|
||||
{"RightPaddle1", "Right paddle 1"},
|
||||
{"LeftPaddle1", "Left paddle 1"},
|
||||
{"RightPaddle2", "Right paddle 2"},
|
||||
{"LeftPaddle2", "Left paddle 2"},
|
||||
{"Touchpad", "Touch pad click"},
|
||||
{"LeftTrigger", "LT / L2"},
|
||||
{"RightTrigger", "RT / R2"},
|
||||
};
|
||||
|
||||
QString PadSourceLabel(const QString& id) {
|
||||
for (const auto& source: PAD_SOURCES) {
|
||||
if (id == QLatin1String(source.id)) {
|
||||
return QObject::tr(source.label);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
QString KeypadName(int key) {
|
||||
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
|
||||
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
|
||||
@@ -191,6 +250,54 @@ private:
|
||||
QString m_binding;
|
||||
};
|
||||
|
||||
class PadCaptureDialog final: public QDialog {
|
||||
public:
|
||||
PadCaptureDialog(ControllerPreview* preview, QWidget* parent): QDialog(parent) {
|
||||
setWindowTitle(tr("Set Binding"));
|
||||
setModal(true);
|
||||
setMinimumWidth(360);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
m_label = new QLabel(this);
|
||||
m_label->setAlignment(Qt::AlignCenter);
|
||||
m_label->setWordWrap(true);
|
||||
layout->addWidget(m_label);
|
||||
|
||||
if (preview == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Inputs already held when the dialog opens must be released before they count.
|
||||
m_held = preview->RawPressed();
|
||||
connect(preview, &ControllerPreview::StateChanged, this, [this, preview] {
|
||||
const auto& pressed = preview->RawPressed();
|
||||
m_held.intersect(pressed);
|
||||
for (const auto& name: pressed) {
|
||||
if (!m_held.contains(name)) {
|
||||
m_binding = name;
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
UpdateText(preview);
|
||||
});
|
||||
UpdateText(preview);
|
||||
}
|
||||
|
||||
[[nodiscard]] const QString& Binding() const { return m_binding; }
|
||||
|
||||
private:
|
||||
void UpdateText(const ControllerPreview* preview) {
|
||||
m_label->setText(preview->DeviceName().isEmpty()
|
||||
? tr("No controller connected.\nConnect one, or press Esc to cancel.")
|
||||
: tr("Press a button or trigger on %1.\nEsc cancels.")
|
||||
.arg(preview->DeviceName()));
|
||||
}
|
||||
|
||||
QLabel* m_label = nullptr;
|
||||
QString m_binding;
|
||||
QSet<QString> m_held;
|
||||
};
|
||||
|
||||
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
|
||||
for (auto item = bindings.begin(); item != bindings.end();) {
|
||||
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
|
||||
@@ -205,7 +312,11 @@ void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const Q
|
||||
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
QHash<QString, QString> result;
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
if (entry == QLatin1String(KEYBOARD_DISABLED) || entry == QLatin1String(PAD_DISABLED) ||
|
||||
entry.startsWith(QLatin1String(MOUSE_SENSITIVITY)) ||
|
||||
entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN)) ||
|
||||
entry.startsWith(QLatin1String("TouchCursorImage=")) ||
|
||||
entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto separator = entry.indexOf(QLatin1Char('='));
|
||||
@@ -216,6 +327,15 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QString ParseTouchParticleSkin(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN))) {
|
||||
return entry.mid(sizeof(TOUCH_PARTICLE_SKIN) - 1);
|
||||
}
|
||||
}
|
||||
return QStringLiteral("Classic");
|
||||
}
|
||||
|
||||
double ParseMouseSensitivity(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
@@ -231,106 +351,67 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_preview(preview) {
|
||||
setWindowTitle(tr("Controls"));
|
||||
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
|
||||
resize(QSize(1360, 780).boundedTo(screen()->availableGeometry().size()));
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty() || mapping.contains(QLatin1String(KEYBOARD_DISABLED));
|
||||
m_custom_pad = mapping.contains(QLatin1String(PAD_DISABLED));
|
||||
for (const auto& entry: mapping) {
|
||||
if (!entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto body = entry.mid(static_cast<int>(sizeof(PAD_PREFIX)) - 1);
|
||||
const auto separator = body.indexOf(QLatin1Char('='));
|
||||
if (separator > 0 && separator + 1 < body.size()) {
|
||||
AssignBinding(m_pad_bindings, body.left(separator), body.mid(separator + 1));
|
||||
m_custom_pad = true;
|
||||
}
|
||||
}
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
if (!m_custom_bindings && control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
if (!m_custom_pad && control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(id, QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(10);
|
||||
auto* title = new QLabel(tr("Controller setup"), this);
|
||||
auto font = title->font();
|
||||
font.setPointSize(font.pointSize() + 7);
|
||||
font.setBold(true);
|
||||
title->setFont(font);
|
||||
layout->addWidget(title);
|
||||
auto* hint = new QLabel(
|
||||
tr("Select a button to remap. Press controller buttons to test."), this);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
m_status = new QLabel(this);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
|
||||
auto* scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* grid = new QGridLayout(content);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setSpacing(12);
|
||||
QGroupBox* groups[GroupCount];
|
||||
QGridLayout* cells[GroupCount];
|
||||
const char* titles[] = {"D-Pad",
|
||||
"Left stick",
|
||||
"Right stick",
|
||||
"Face buttons",
|
||||
"Shoulders and triggers",
|
||||
"Touch pad and options",
|
||||
"In-game shortcuts (cycle)"};
|
||||
for (int index = 0; index < GroupCount; ++index) {
|
||||
groups[index] = new QGroupBox(tr(titles[index]), content);
|
||||
cells[index] = new QGridLayout(groups[index]);
|
||||
cells[index]->setSpacing(5);
|
||||
}
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
|
||||
auto* cell_layout = new QVBoxLayout(cell);
|
||||
cell_layout->setContentsMargins(3, 5, 3, 3);
|
||||
auto* button = new QPushButton(cell);
|
||||
button->setObjectName(QStringLiteral("binding_") + id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(64);
|
||||
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
|
||||
tr(control.label));
|
||||
cell_layout->addWidget(button);
|
||||
cells[control.group]->addWidget(cell, control.row, control.column);
|
||||
m_buttons.insert(id, button);
|
||||
if (!m_custom_bindings) {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
|
||||
}
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
left->addWidget(groups[Dpad]);
|
||||
left->addWidget(groups[LeftStick]);
|
||||
left->addStretch();
|
||||
grid->addLayout(left, 0, 0);
|
||||
auto* center = new QVBoxLayout;
|
||||
center->addWidget(groups[Shoulders]);
|
||||
m_controller = new DualSenseWidget(content);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
center->addWidget(m_controller, 1);
|
||||
center->addWidget(groups[Center]);
|
||||
grid->addLayout(center, 0, 1);
|
||||
auto* right = new QVBoxLayout;
|
||||
right->addWidget(groups[Face]);
|
||||
right->addWidget(groups[RightStick]);
|
||||
right->addStretch();
|
||||
grid->addLayout(right, 0, 2);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
|
||||
scroll->setWidget(content);
|
||||
layout->addWidget(scroll, 1);
|
||||
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
|
||||
m_sensitivity = new QDoubleSpinBox(this);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_tabs = new QTabWidget(this);
|
||||
m_tabs->setObjectName(QStringLiteral("inputTabs"));
|
||||
m_tabs->setDocumentMode(true);
|
||||
m_tabs->addTab(BuildTab(true), tr("Gamepad"));
|
||||
m_tabs->addTab(BuildTab(false), tr("Keyboard && Mouse"));
|
||||
layout->addWidget(m_tabs, 1);
|
||||
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
auto* touch_group = new QGroupBox(tr("Emulated touch pad (shared by both tabs)"), this);
|
||||
auto* touch_layout = new QHBoxLayout(touch_group);
|
||||
touch_layout->addWidget(new QLabel(tr("Particle skin"), touch_group));
|
||||
m_particle_skin = new QComboBox(touch_group);
|
||||
m_particle_skin->setObjectName(QStringLiteral("touchParticleSkin"));
|
||||
m_particle_skin->addItem(tr("Classic"), QStringLiteral("Classic"));
|
||||
m_particle_skin->addItem(tr("Blue neon"), QStringLiteral("BlueNeon"));
|
||||
m_particle_skin->addItem(tr("Monochrome"), QStringLiteral("Monochrome"));
|
||||
m_particle_skin->addItem(tr("Crimson"), QStringLiteral("Crimson"));
|
||||
m_particle_skin->addItem(tr("Red/Blue"), QStringLiteral("RedBlue"));
|
||||
touch_layout->addWidget(m_particle_skin);
|
||||
m_touch_cursor = new QCheckBox(tr("Show hand cursor and trail"), touch_group);
|
||||
m_touch_cursor->setObjectName(QStringLiteral("touchCursorImage"));
|
||||
m_touch_cursor->setToolTip(tr("When off, the emulated touch pad still works but the hand "
|
||||
"image and drawing trail are not shown."));
|
||||
m_touch_cursor->setChecked(!mapping.contains(QLatin1String(TOUCH_CURSOR_OFF)));
|
||||
touch_layout->addWidget(m_touch_cursor);
|
||||
touch_layout->addStretch();
|
||||
touch_layout->addWidget(new QLabel(
|
||||
tr("Toggle with the touch pad mode key (F6 by default); draw with left mouse / Cross."),
|
||||
touch_group));
|
||||
layout->addWidget(touch_group);
|
||||
const auto skin_index = m_particle_skin->findData(ParseTouchParticleSkin(mapping));
|
||||
m_particle_skin->setCurrentIndex(skin_index >= 0 ? skin_index : 0);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
|
||||
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
|
||||
@@ -338,9 +419,14 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
|
||||
defaults->setObjectName(QStringLiteral("defaults"));
|
||||
defaults->setAutoDefault(false);
|
||||
layout->addWidget(buttons);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
&InputMappingDialog::ChangeBinding);
|
||||
defaults->setToolTip(tr("Restore the defaults of the current tab."));
|
||||
auto* tip = new QLabel(tr("Tip: right-click a binding to unmap it."), this);
|
||||
auto* footer = new QHBoxLayout;
|
||||
footer->addWidget(tip);
|
||||
footer->addStretch();
|
||||
footer->addWidget(buttons);
|
||||
layout->addLayout(footer);
|
||||
|
||||
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
@@ -359,6 +445,128 @@ InputMappingDialog::~InputMappingDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
QWidget* InputMappingDialog::BuildTab(bool pad) {
|
||||
auto* page = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(page);
|
||||
layout->setSpacing(10);
|
||||
|
||||
auto* hint = new QLabel(
|
||||
pad ? tr("Choose which button on your gamepad triggers each PS5 control. Click a "
|
||||
"binding, then press the button or trigger you want to use.")
|
||||
: tr("Choose which keyboard key or mouse button triggers each PS5 control. Click a "
|
||||
"binding, then press the key or mouse button."),
|
||||
page);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
if (pad) {
|
||||
m_status = new QLabel(page);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
}
|
||||
|
||||
auto* body = new QHBoxLayout;
|
||||
body->setSpacing(14);
|
||||
layout->addLayout(body, 1);
|
||||
|
||||
auto* scroll = new QScrollArea(page);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* columns = new QGridLayout(content);
|
||||
columns->setContentsMargins(0, 0, 8, 0);
|
||||
columns->setSpacing(12);
|
||||
columns->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Section order and the column each one lives in.
|
||||
static constexpr Section ORDER[] = {Face, Dpad, Shoulders, Sticks, System, LeftStick,
|
||||
RightStick, Shortcuts};
|
||||
int column_rows[2] = {0, 0};
|
||||
for (const auto section: ORDER) {
|
||||
QGroupBox* group = nullptr;
|
||||
QGridLayout* rows = nullptr;
|
||||
int row = 0;
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.section != section || (pad && control.pad_default == nullptr)) {
|
||||
continue;
|
||||
}
|
||||
if (group == nullptr) {
|
||||
group = new QGroupBox(tr(SECTION_TITLES[section]), content);
|
||||
rows = new QGridLayout(group);
|
||||
rows->setHorizontalSpacing(12);
|
||||
rows->setVerticalSpacing(6);
|
||||
rows->setColumnStretch(0, 1);
|
||||
rows->setColumnMinimumWidth(1, 190);
|
||||
}
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto label = !pad && id == QLatin1String("TouchPad") ? tr("Touch pad left")
|
||||
: tr(control.label);
|
||||
auto* name = new QLabel(label, group);
|
||||
auto* button = new QPushButton(group);
|
||||
button->setObjectName((pad ? QStringLiteral("padBinding_") : QStringLiteral("binding_")) +
|
||||
id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(190);
|
||||
button->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
button->setAccessibleName(tr(SECTION_TITLES[section]) + QStringLiteral(" / ") + label);
|
||||
rows->addWidget(name, row, 0);
|
||||
rows->addWidget(button, row, 1);
|
||||
++row;
|
||||
(pad ? m_pad_buttons : m_buttons).insert(id, button);
|
||||
connect(button, &QPushButton::clicked, this, [this, pad, id] {
|
||||
pad ? ChangePadBinding(id) : ChangeKeyboardBinding(id);
|
||||
});
|
||||
connect(button, &QPushButton::customContextMenuRequested, this,
|
||||
[this, pad, id] { ClearBinding(pad, id); });
|
||||
}
|
||||
if (group != nullptr) {
|
||||
const int target = column_rows[0] <= column_rows[1] ? 0 : 1;
|
||||
columns->addWidget(group, column_rows[target]++, target);
|
||||
}
|
||||
}
|
||||
columns->setColumnStretch(0, 1);
|
||||
columns->setColumnStretch(1, 1);
|
||||
scroll->setWidget(content);
|
||||
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll->setMinimumWidth(pad ? 640 : 700);
|
||||
body->addWidget(scroll, 3);
|
||||
|
||||
if (pad) {
|
||||
m_controller = new DualSenseWidget(page);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
m_controller->setMinimumWidth(320);
|
||||
body->addWidget(m_controller, 2);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
[this](const QString& id) {
|
||||
ChangePadBinding(id == QLatin1String("TouchPadRight") ? QStringLiteral("TouchPad")
|
||||
: id);
|
||||
});
|
||||
auto* note = new QLabel(tr("The gyro emulation toggle activates tilt mode on gamepads "
|
||||
"without a gyroscope: the right stick then controls pitch and "
|
||||
"roll. It defaults to the View/Back button."),
|
||||
page);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
} else {
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), page));
|
||||
m_sensitivity = new QDoubleSpinBox(page);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), page));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
QStringList InputMappingDialog::Mapping() const {
|
||||
QStringList result;
|
||||
if (m_custom_bindings) {
|
||||
@@ -369,15 +577,38 @@ QStringList InputMappingDialog::Mapping() const {
|
||||
result.append(id + QLatin1Char('=') + binding);
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.append(QLatin1String(KEYBOARD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_custom_pad) {
|
||||
const auto start = result.size();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto source = m_pad_bindings.value(id);
|
||||
if (!source.isEmpty()) {
|
||||
result.append(QLatin1String(PAD_PREFIX) + id + QLatin1Char('=') + source);
|
||||
}
|
||||
}
|
||||
if (result.size() == start) {
|
||||
result.append(QLatin1String(PAD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_sensitivity->value() != DEFAULT_MOUSE_SENSITIVITY) {
|
||||
result.append(QLatin1String(MOUSE_SENSITIVITY) +
|
||||
QString::number(m_sensitivity->value(), 'f', 1));
|
||||
}
|
||||
const auto skin = m_particle_skin->currentData().toString();
|
||||
if (skin != QLatin1String("Classic")) {
|
||||
result.append(QLatin1String(TOUCH_PARTICLE_SKIN) + skin);
|
||||
}
|
||||
if (!m_touch_cursor->isChecked()) {
|
||||
result.append(QLatin1String(TOUCH_CURSOR_OFF));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
void InputMappingDialog::ChangeKeyboardBinding(const QString& id) {
|
||||
if (!m_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
@@ -393,14 +624,54 @@ void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.default_binding));
|
||||
void InputMappingDialog::ChangePadBinding(const QString& id) {
|
||||
if (!m_pad_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
m_selected = id;
|
||||
UpdateHighlights();
|
||||
PadCaptureDialog dialog(m_preview, this);
|
||||
dialog.setWindowTitle(tr("Set binding — %1").arg(m_pad_buttons.value(id)->accessibleName()));
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
AssignBinding(m_pad_bindings, id, dialog.Binding());
|
||||
m_custom_pad = true;
|
||||
}
|
||||
m_selected.clear();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::ClearBinding(bool pad, const QString& id) {
|
||||
if (pad) {
|
||||
m_pad_bindings.remove(id);
|
||||
m_custom_pad = true;
|
||||
} else {
|
||||
m_bindings.remove(id);
|
||||
m_custom_bindings = true;
|
||||
}
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
if (m_tabs->currentIndex() == 0) {
|
||||
m_pad_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
m_custom_pad = false;
|
||||
} else {
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
@@ -410,14 +681,42 @@ void InputMappingDialog::UpdateButtons() {
|
||||
const auto binding = m_bindings.value(item.key());
|
||||
const auto label = binding.isEmpty() ? tr("None") : binding;
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
auto* button = item.value();
|
||||
const auto source = m_pad_bindings.value(item.key());
|
||||
const auto label = source.isEmpty() ? tr("None") : PadSourceLabel(source);
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
UpdateHighlights();
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdatePreview() {
|
||||
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
|
||||
m_controller->SetPressed(pressed);
|
||||
m_pad_pressed.clear();
|
||||
if (m_preview != nullptr) {
|
||||
const auto& raw = m_preview->RawPressed();
|
||||
for (auto item = m_pad_bindings.begin(); item != m_pad_bindings.end(); ++item) {
|
||||
if (raw.contains(item.value())) {
|
||||
m_pad_pressed.insert(item.key());
|
||||
}
|
||||
}
|
||||
if (m_pad_pressed.contains(QStringLiteral("TouchPad"))) {
|
||||
m_pad_pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
for (const auto& name: m_preview->Pressed()) {
|
||||
// Stick directions and touch pad fingers are shown as the pad reports them.
|
||||
if (name.startsWith(QLatin1String("LeftStick")) ||
|
||||
name.startsWith(QLatin1String("RightStick")) ||
|
||||
name.startsWith(QLatin1String("TouchPad"))) {
|
||||
m_pad_pressed.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_controller->SetPressed(m_pad_pressed);
|
||||
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
|
||||
m_preview != nullptr ? m_preview->RightStick() : QPointF());
|
||||
UpdateHighlights();
|
||||
@@ -432,8 +731,8 @@ void InputMappingDialog::UpdatePreview() {
|
||||
|
||||
void InputMappingDialog::UpdateHighlights() {
|
||||
m_controller->SetSelected(m_selected);
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
const bool down = m_pad_pressed.contains(item.key());
|
||||
auto colors = palette();
|
||||
if (down) {
|
||||
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
|
||||
@@ -446,4 +745,11 @@ void InputMappingDialog::UpdateHighlights() {
|
||||
item.value()->setPalette(colors);
|
||||
item.value()->setFont(label_font);
|
||||
}
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
auto colors = palette();
|
||||
if (item.key() == m_selected) {
|
||||
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
|
||||
}
|
||||
item.value()->setPalette(colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerPreview.h"
|
||||
#include "gameContent.h"
|
||||
#include "titleSound.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -90,6 +91,7 @@ private:
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
UpdateChecker* m_update_checker = nullptr;
|
||||
ControllerPreview m_controller_preview;
|
||||
TitleSound m_title_sound;
|
||||
QString m_interpreter;
|
||||
|
||||
QProcess m_process;
|
||||
@@ -124,7 +126,13 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
|
||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() {
|
||||
Update();
|
||||
const auto* item = m_ui->widget->GetSelectedItem();
|
||||
m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr
|
||||
? item->GetInfo().basedir
|
||||
: QString());
|
||||
});
|
||||
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
|
||||
[this](const QString& color) {
|
||||
if (m_process.state() == QProcess::NotRunning) {
|
||||
@@ -530,6 +538,7 @@ void MainDialogPrivate::Run() {
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
m_controller_preview.SetSuspended(true);
|
||||
m_title_sound.SetTitle({});
|
||||
|
||||
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
|
||||
m_main_dialog->RunInterpreter(&m_process, *info);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "titleSound.h"
|
||||
|
||||
#include "gameContent.h"
|
||||
#include "libs/ajm/atrac9_decoder.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
|
||||
TitleSound::Sound TitleSound::Decode(const QByteArray& data, const QPromise<Sound>& promise) {
|
||||
using namespace Libs::Audio::Ajm;
|
||||
AjmAt9Decoder decoder(0, 0, AjmSampleEncoding::Float,
|
||||
AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER);
|
||||
AjmGaplessState gapless;
|
||||
std::array<float, 16 * 1024> batch;
|
||||
Sound sound;
|
||||
size_t offset = 0;
|
||||
while (offset < static_cast<size_t>(data.size()) && !gapless.IsEnd()) {
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto result = decoder.Decode(data.constData() + offset, data.size() - offset,
|
||||
batch.data(), sizeof(batch), true, &gapless);
|
||||
if ((result.result != 0 && result.result != AJM_RESULT_NOT_ENOUGH_ROOM) ||
|
||||
result.input_consumed == 0) {
|
||||
return {};
|
||||
}
|
||||
const auto samples = result.output_written / sizeof(float);
|
||||
constexpr size_t MaxSamples = (128u << 20u) / sizeof(float);
|
||||
if (samples > MaxSamples - sound.pcm.size()) {
|
||||
return {};
|
||||
}
|
||||
sound.pcm.insert(sound.pcm.end(), batch.begin(), batch.begin() + samples);
|
||||
offset += result.input_consumed;
|
||||
}
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto format = decoder.GetFormat();
|
||||
sound.channels = static_cast<int>(format.channel_num);
|
||||
sound.rate = static_cast<int>(format.sampling_frequency);
|
||||
return sound;
|
||||
}
|
||||
|
||||
TitleSound::TitleSound(QObject* parent): QObject(parent) {
|
||||
m_decode_pool.setMaxThreadCount(1);
|
||||
connect(&m_decode, &QFutureWatcher<Sound>::finished, this, [this] {
|
||||
if (m_decode.isCanceled() || m_decode.future().resultCount() == 0) {
|
||||
return;
|
||||
}
|
||||
auto sound = m_decode.future().takeResult();
|
||||
if (sound.pcm.empty()) {
|
||||
qWarning() << "Cannot decode title sound:" << m_base;
|
||||
return;
|
||||
}
|
||||
m_audio_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
if (m_audio_initialized) {
|
||||
const SDL_AudioSpec spec {SDL_AUDIO_F32, sound.channels, sound.rate};
|
||||
m_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr,
|
||||
nullptr);
|
||||
}
|
||||
if (m_stream == nullptr ||
|
||||
!SDL_PutAudioStreamData(m_stream, sound.pcm.data(),
|
||||
static_cast<int>(sound.pcm.size() * sizeof(float))) ||
|
||||
!SDL_FlushAudioStream(m_stream) || !SDL_ResumeAudioStreamDevice(m_stream)) {
|
||||
qWarning() << "Cannot play title sound:" << SDL_GetError();
|
||||
Stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TitleSound::~TitleSound() {
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
}
|
||||
|
||||
void TitleSound::SetTitle(const QString& base) {
|
||||
if (base == m_base) {
|
||||
return;
|
||||
}
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
m_base = base;
|
||||
if (!base.isEmpty()) {
|
||||
m_decode.setFuture(QtConcurrent::run(&m_decode_pool, [base](QPromise<Sound>& promise) {
|
||||
const auto data =
|
||||
GameContent::ReadFile(base, QStringLiteral("sce_sys/snd0.at9"), 32u << 20u);
|
||||
if (!data.isEmpty() && !promise.isCanceled()) {
|
||||
promise.addResult(Decode(data, promise));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void TitleSound::Stop() {
|
||||
if (m_stream != nullptr) {
|
||||
SDL_DestroyAudioStream(m_stream);
|
||||
m_stream = nullptr;
|
||||
}
|
||||
if (m_audio_initialized) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
m_audio_initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,11 @@ constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
QString LauncherRelativePath() {
|
||||
#if defined(_WIN32)
|
||||
return QStringLiteral("launcher.exe");
|
||||
return QStringLiteral("kyty_launcher.exe");
|
||||
#elif defined(__APPLE__)
|
||||
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
|
||||
#else
|
||||
return QStringLiteral("launcher");
|
||||
return QStringLiteral("kyty_launcher");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+101
-1
@@ -126,6 +126,7 @@ public:
|
||||
void Motion(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void SetMotionSensorState(bool enable);
|
||||
void ResetOrientation();
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
@@ -147,6 +148,8 @@ private:
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
|
||||
void UpdateEmulatedTilt();
|
||||
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
|
||||
void ApplyVibration();
|
||||
bool SendTriggerEffect(const PadTriggerEffectParam& param);
|
||||
@@ -160,6 +163,9 @@ private:
|
||||
int m_connected_count = 0;
|
||||
bool m_motion_enabled = true;
|
||||
uint64_t m_gyro_time = 0;
|
||||
bool m_gyro_emulation = false;
|
||||
uint64_t m_emulated_time = 0;
|
||||
std::array<int, 2> m_emulated_stick {128, 128};
|
||||
// Accelerometer gravity direction in the reset frame.
|
||||
std::array<float, 3> m_up_reference {0.0f, 1.0f, 0.0f};
|
||||
bool m_up_reference_valid = false;
|
||||
@@ -479,6 +485,9 @@ void GameController::CheckActive() {
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
m_gyro_emulation = false;
|
||||
m_emulated_time = 0;
|
||||
m_emulated_stick = {128, 128};
|
||||
m_up_reference_valid = false;
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
@@ -524,6 +533,12 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
|
||||
|
||||
EXIT_IF(axis_id < 0 || axis_id >= static_cast<int>(Controller::Axis::AxisMax));
|
||||
|
||||
if (m_gyro_emulation && (axis == Controller::Axis::RightX || axis == Controller::Axis::RightY)) {
|
||||
m_emulated_stick[axis == Controller::Axis::RightY ? 1 : 0] = value;
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.axes[axis_id] = value;
|
||||
|
||||
uint32_t trigger = 0;
|
||||
@@ -545,7 +560,12 @@ void GameController::RightStick(int id, int x, int y) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {x, y};
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = x;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = y;
|
||||
AddState();
|
||||
@@ -677,6 +697,82 @@ void GameController::SetMotionSensorState(bool enable) {
|
||||
}
|
||||
}
|
||||
|
||||
static Quat QuatMultiply(const Quat& a, const Quat& b) {
|
||||
return {a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
||||
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
||||
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
||||
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]};
|
||||
}
|
||||
|
||||
void GameController::UpdateEmulatedTilt() {
|
||||
constexpr float max_angle = 0.7853982f; // 45 degrees at full stick deflection
|
||||
const auto angle = [](int value) {
|
||||
return std::clamp((value - 128) / 127.0f, -1.0f, 1.0f) * max_angle;
|
||||
};
|
||||
const float roll = angle(m_emulated_stick[0]);
|
||||
const float pitch = angle(m_emulated_stick[1]);
|
||||
|
||||
const Quat q_pitch {std::sin(pitch * 0.5f), 0.0f, 0.0f, std::cos(pitch * 0.5f)};
|
||||
const Quat q_roll {0.0f, 0.0f, std::sin(-roll * 0.5f), std::cos(-roll * 0.5f)};
|
||||
Quat q = QuatMultiply(q_roll, q_pitch);
|
||||
|
||||
const uint64_t now = LibKernel::KernelGetProcessTime();
|
||||
Vec3 rate {};
|
||||
if (m_emulated_time != 0 && now > m_emulated_time) {
|
||||
const float dt = std::clamp(static_cast<float>(now - m_emulated_time) * 0.000001f,
|
||||
0.001f, 0.1f);
|
||||
auto delta = QuatMultiply(QuatConjugate(m_state.orientation), q);
|
||||
if (delta[3] < 0.0f) {
|
||||
for (auto& v: delta) {
|
||||
v = -v;
|
||||
}
|
||||
}
|
||||
rate = {2.0f * delta[0] / dt, 2.0f * delta[1] / dt, 2.0f * delta[2] / dt};
|
||||
}
|
||||
m_emulated_time = now;
|
||||
|
||||
m_state.time = now;
|
||||
m_state.orientation = q;
|
||||
m_state.accel = QuatRotate(QuatConjugate(q), {0.0f, 1.0f, 0.0f});
|
||||
m_state.gyro = rate;
|
||||
AddState();
|
||||
// Angular velocity is only nonzero while the tilt changes.
|
||||
m_state.gyro = {};
|
||||
}
|
||||
|
||||
void GameController::ToggleGyroEmulation(int id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (id != HOST_INPUT_CONTROLLER_ID && id != m_active_id) {
|
||||
return;
|
||||
}
|
||||
// Pads with a real gyroscope report it directly.
|
||||
if (m_active_id != HOST_INPUT_CONTROLLER_ID) {
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
|
||||
if (pad != nullptr && SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_gyro_emulation = !m_gyro_emulation;
|
||||
m_emulated_time = 0;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {m_state.axes[static_cast<int>(Axis::RightX)],
|
||||
m_state.axes[static_cast<int>(Axis::RightY)]};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = 128;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = 128;
|
||||
UpdateEmulatedTilt();
|
||||
} else {
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = m_emulated_stick[0];
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = m_emulated_stick[1];
|
||||
AddState();
|
||||
}
|
||||
LOGF("Emulated gyro tilt: %s\n", m_gyro_emulation ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void GameController::ResetOrientation() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
@@ -931,6 +1027,10 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
|
||||
void ToggleGyroEmulation(int id) {
|
||||
g_controller->ToggleGyroEmulation(id);
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y);
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
|
||||
|
||||
@@ -326,6 +326,33 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
|
||||
if (frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!stream->speaker &&
|
||||
SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
|
||||
CloseDevice(stream);
|
||||
stream->next_check = 0;
|
||||
// Non-DualSense pads have no haptic audio path: approximate it with the rumble motors.
|
||||
float peak[2] = {0.0f, 0.0f};
|
||||
for (uint32_t frame = 0; frame < frames; frame++) {
|
||||
for (uint32_t ch = 0; ch < 2; ch++) {
|
||||
const auto src_ch = channels == 1 ? 0 : ch;
|
||||
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
|
||||
float value = is_float ? static_cast<const float*>(data)[index]
|
||||
: static_cast<const int16_t*>(data)[index] / 32768.0f;
|
||||
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
|
||||
peak[ch] = std::max(peak[ch], std::fabs(value));
|
||||
}
|
||||
}
|
||||
const auto to_motor = [](float v) {
|
||||
return static_cast<uint16_t>(std::clamp(v * 2.0f, 0.0f, 1.0f) * 65535.0f);
|
||||
};
|
||||
SDL_LockJoysticks();
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
pad != nullptr) {
|
||||
(void) SDL_RumbleGamepad(pad, to_motor(peak[0]), to_motor(peak[1]), 100);
|
||||
}
|
||||
SDL_UnlockJoysticks();
|
||||
return static_cast<uint64_t>(frames) * 1000000 / stream->freq;
|
||||
}
|
||||
const bool wireless = IsWireless(controller);
|
||||
if (!wireless && !CanUseDevice(controller)) {
|
||||
CloseDevice(stream);
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <atomic>
|
||||
#include <cinttypes>
|
||||
#include <iterator>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
@@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi
|
||||
return OK;
|
||||
}
|
||||
|
||||
// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results.
|
||||
void InvalidateFailedResolves() {
|
||||
std::scoped_lock lock(g_mutex);
|
||||
for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) {
|
||||
it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it);
|
||||
}
|
||||
}
|
||||
|
||||
static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) {
|
||||
if (guest_path == nullptr || guest_path[0] == '\0') {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
@@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
|
||||
log_missing = true;
|
||||
}
|
||||
}
|
||||
if (!log_missing && info.result == OK && path.starts_with("/addcont")) {
|
||||
LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path,
|
||||
Common::PathToString(info.host_path).c_str(), info.file_size);
|
||||
}
|
||||
if (log_missing) {
|
||||
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
|
||||
}
|
||||
@@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
|
||||
uint64_t bytes_read = 0;
|
||||
auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination,
|
||||
command.size, &bytes_read);
|
||||
static std::atomic<uint32_t> s_logged_dlc_reads {0};
|
||||
if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) {
|
||||
LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64
|
||||
", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n",
|
||||
command.file_id, command.file_offset, command.size, bytes_read,
|
||||
static_cast<uint32_t>(result));
|
||||
}
|
||||
if (result != OK) {
|
||||
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
|
||||
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
|
||||
|
||||
@@ -22,13 +22,16 @@
|
||||
|
||||
namespace Libs {
|
||||
|
||||
namespace AprShared {
|
||||
void InvalidateFailedResolves();
|
||||
}
|
||||
|
||||
LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
|
||||
|
||||
namespace AppContent {
|
||||
|
||||
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
|
||||
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
|
||||
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
|
||||
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
|
||||
@@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
|
||||
|
||||
for (const auto& m: g_addcont_mounts) {
|
||||
if (m.label == entry->entitlement_label) {
|
||||
return APP_CONTENT_ERROR_BUSY;
|
||||
const auto guest_point = m.point.substr(0, m.point.size() - 1);
|
||||
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
|
||||
LOGF("\t already mounted %s\n", guest_point.c_str());
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
|
||||
LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
|
||||
Common::PathToString(entry->mount_dir).c_str());
|
||||
|
||||
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1);
|
||||
AprShared::InvalidateFailedResolves();
|
||||
|
||||
// Titles scan /download0/pdlc for downloaded content; it must exist even when empty.
|
||||
if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) {
|
||||
std::filesystem::create_directories(pdlc, ec);
|
||||
}
|
||||
|
||||
const auto guest_point = mount.point.substr(0, mount.point.size() - 1);
|
||||
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
|
||||
g_addcont_mounts.push_back(std::move(mount));
|
||||
|
||||
return OK;
|
||||
@@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
|
||||
return APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
}
|
||||
|
||||
const std::string point(mount_point->data,
|
||||
strnlen(mount_point->data, sizeof(mount_point->data)));
|
||||
const std::string point = Common::FixDirectorySlash(
|
||||
std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data))));
|
||||
|
||||
std::lock_guard lock(g_addcont_mutex);
|
||||
|
||||
@@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
|
||||
}
|
||||
LibKernel::FileSystem::Umount(point);
|
||||
g_addcont_mounts.erase(it);
|
||||
AprShared::InvalidateFailedResolves();
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "kernel/eventQueue.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kernel/module.h"
|
||||
#include "kernel/pthread.h"
|
||||
#include "kernel/semaphore.h"
|
||||
#include "kernel/syncOnAddress.h"
|
||||
@@ -65,7 +66,6 @@ LIB_DEFINE(InitLibKernel_1_Apr);
|
||||
|
||||
namespace LibKernel {
|
||||
|
||||
using KernelModule = int32_t;
|
||||
using get_thread_atexit_count_func_t = KYTY_SYSV_ABI int (*)(KernelModule);
|
||||
using thread_atexit_report_func_t = KYTY_SYSV_ABI void (*)(KernelModule);
|
||||
|
||||
@@ -1201,9 +1201,9 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res) {
|
||||
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res) {
|
||||
PRINT_NAME();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
@@ -1255,9 +1255,9 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
|
||||
return static_cast<KernelModule>(handle);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res) {
|
||||
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res) {
|
||||
PRINT_NAME();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
|
||||
@@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list,
|
||||
if (out_entries == nullptr) {
|
||||
return PLAYGO_ERROR_BAD_POINTER;
|
||||
}
|
||||
if (number_of_entries != 0 && out_chunk_id_list == nullptr) {
|
||||
return PLAYGO_ERROR_BAD_POINTER;
|
||||
if (out_chunk_id_list != nullptr && number_of_entries == 0) {
|
||||
return PLAYGO_ERROR_BAD_SIZE;
|
||||
}
|
||||
ensure_chunks_loaded();
|
||||
|
||||
// A null output buffer asks for the total chunk count.
|
||||
if (out_chunk_id_list == nullptr) {
|
||||
*out_entries = g_chunks_num;
|
||||
return OK;
|
||||
}
|
||||
|
||||
const uint32_t entries = std::min(number_of_entries, g_chunks_num);
|
||||
for (uint32_t i = 0; i < entries; i++) {
|
||||
out_chunk_id_list[i] = static_cast<uint16_t>(i);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/common.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
namespace LibRazorCpu {
|
||||
@@ -16,8 +16,18 @@ static KYTY_SYSV_ABI uint32_t RazorCpuIsCapturing() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI RazorCpuPopMarker() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI RazorCpuPushMarker(const char*, uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitRazorCpu_1) {
|
||||
LIB_FUNC("EboejOQvLL4", LibRazorCpu::RazorCpuIsCapturing);
|
||||
LIB_FUNC("YpkGsMXP3ew", LibRazorCpu::RazorCpuPopMarker);
|
||||
LIB_FUNC("zw+celG7zSI", LibRazorCpu::RazorCpuPushMarker);
|
||||
}
|
||||
|
||||
} // namespace LibRazorCpu
|
||||
+47
-16
@@ -3,6 +3,7 @@
|
||||
#include "common/common.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "kernel/module.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
@@ -20,14 +21,40 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
|
||||
|
||||
namespace Sysmodule {
|
||||
|
||||
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
|
||||
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
|
||||
constexpr int SYSMODULE_ERROR_FATAL = static_cast<int>(0x805a10ffu);
|
||||
|
||||
static std::mutex g_module_mutex;
|
||||
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
|
||||
static constexpr struct NativeModule {
|
||||
uint16_t id;
|
||||
const char* path;
|
||||
} g_native_modules[] = {{0x0115, "/app0/sce_module/libSceNpCppWebApi.prx"}};
|
||||
|
||||
static void LoadModule(uint16_t id) {
|
||||
struct Module {
|
||||
uint32_t references = 0;
|
||||
LibKernel::KernelModule handle = -1;
|
||||
};
|
||||
|
||||
static std::mutex g_module_mutex;
|
||||
static std::unordered_map<uint16_t, Module> g_modules;
|
||||
|
||||
static int LoadModule(uint16_t id) {
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
++g_module_refs[id];
|
||||
if (const auto module = g_modules.find(id); module != g_modules.end()) {
|
||||
++module->second.references;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LibKernel::KernelModule handle = -1;
|
||||
for (const auto& module: g_native_modules) {
|
||||
if (module.id != id) continue;
|
||||
handle = LibKernel::KernelLoadStartModule(module.path, 0, nullptr, 0, nullptr, nullptr);
|
||||
if (handle < 0) {
|
||||
return handle;
|
||||
}
|
||||
break;
|
||||
}
|
||||
g_modules.emplace(id, Module {1, handle});
|
||||
return 0;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
|
||||
@@ -40,8 +67,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
|
||||
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
LoadModule(id);
|
||||
return 0;
|
||||
return LoadModule(id);
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
|
||||
@@ -50,12 +76,19 @@ static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
const auto module = g_module_refs.find(id);
|
||||
if (module == g_module_refs.end()) {
|
||||
const auto module = g_modules.find(id);
|
||||
if (module == g_modules.end()) {
|
||||
return SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
if (--module->second == 0) {
|
||||
g_module_refs.erase(module);
|
||||
if (module->second.references == 1) {
|
||||
if (module->second.handle >= 0 &&
|
||||
LibKernel::KernelStopUnloadModule(module->second.handle, 0, nullptr, 0, nullptr,
|
||||
nullptr) < 0) {
|
||||
return SYSMODULE_ERROR_FATAL;
|
||||
}
|
||||
g_modules.erase(id);
|
||||
} else {
|
||||
--module->second.references;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -71,10 +104,8 @@ static KYTY_SYSV_ABI int SysmoduleLoadModuleInternalWithArg(uint16_t id, int arg
|
||||
EXIT_IF(arg3 != 0);
|
||||
EXIT_IF(ret == nullptr);
|
||||
|
||||
LoadModule(id);
|
||||
*ret = 0;
|
||||
|
||||
return 0;
|
||||
*ret = LoadModule(id);
|
||||
return *ret;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
|
||||
@@ -83,7 +114,7 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
|
||||
return g_modules.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
|
||||
} // namespace Sysmodule
|
||||
|
||||
+11
-9
@@ -58,16 +58,17 @@ struct TlsRegStub {
|
||||
}
|
||||
static uint64_t GetSize() { return 32; }
|
||||
|
||||
// sub rsp,0x80
|
||||
// lea rsp,[rsp-0x80]
|
||||
// nop
|
||||
// push rax
|
||||
// call safe_call
|
||||
// mov <reg>,rax
|
||||
// pop rax
|
||||
// add rsp,0x80
|
||||
// lea rsp,[rsp+0x80]
|
||||
// ret
|
||||
uint8_t code[32] = {0x48, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0x50, 0xE8, 0x00, 0x00,
|
||||
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x81, 0xC4, 0x80, 0x00,
|
||||
0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
|
||||
uint8_t code[32] = {0x48, 0x8D, 0x64, 0x24, 0x80, 0x66, 0x90, 0x50, 0xE8, 0x00, 0x00,
|
||||
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x8D, 0xA4, 0x24, 0x80,
|
||||
0x00, 0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
|
||||
};
|
||||
|
||||
struct SafeCall {
|
||||
@@ -78,7 +79,8 @@ struct SafeCall {
|
||||
static uint64_t GetSize() { return 0x1000; }
|
||||
|
||||
uint8_t code[0x90] = {
|
||||
/*00*/ 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, // sub rsp,0x80
|
||||
/*00*/ 0x48, 0x8d, 0x64, 0x24, 0x80, // lea rsp,[rsp-0x80]
|
||||
/*05*/ 0x66, 0x90, // nop
|
||||
/*07*/ 0x9c, // pushfq
|
||||
/*08*/ 0x51, // push rcx
|
||||
/*09*/ 0x52, // push rdx
|
||||
@@ -108,9 +110,9 @@ struct SafeCall {
|
||||
/*40*/ 0x5a, // pop rdx
|
||||
/*41*/ 0x59, // pop rcx
|
||||
/*42*/ 0x9d, // popfq
|
||||
/*43*/ 0x48, 0x81, 0xc4, 0x80, 0x00, 0x00,
|
||||
0x00, // add rsp,0x80
|
||||
/*4a*/ 0xc3, // ret
|
||||
/*43*/ 0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00,
|
||||
0x00, // lea rsp,[rsp+0x80]
|
||||
/*4b*/ 0xc3, // ret
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+70
-8
@@ -4,7 +4,9 @@
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -56,8 +58,16 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
bytes += size;
|
||||
arguments.emplace_back(argv[i], size - 1);
|
||||
}
|
||||
LOGF("LoadExec: %s\n", path);
|
||||
// Shown on the console too: the output that follows comes from the new program.
|
||||
auto message = fmt::format("LoadExec: switching to {}", path);
|
||||
for (size_t i = 2; i < arguments.size(); ++i) {
|
||||
message += fmt::format(" \"{}\"", arguments[i]);
|
||||
}
|
||||
Log::WriteToConsoleAndLog(message + "\n");
|
||||
// The new program writes to the same outputs, so everything written so far goes out first.
|
||||
// Neither exec nor quick_exit writes out buffered streams.
|
||||
Log::Flush();
|
||||
std::fflush(nullptr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
wchar_t executable[32768] {};
|
||||
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
|
||||
@@ -88,17 +98,57 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
wide.resize(size - 1);
|
||||
command += L" " + QuoteArgument(wide);
|
||||
}
|
||||
STARTUPINFOW startup {};
|
||||
startup.cb = sizeof(startup);
|
||||
// The new process writes where this one does: to its console, or to the files or pipes its
|
||||
// output was redirected to. Only the standard handles are inherited.
|
||||
HANDLE std_handles[3] = {GetStdHandle(STD_INPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE),
|
||||
GetStdHandle(STD_ERROR_HANDLE)};
|
||||
std::vector<HANDLE> inherited;
|
||||
for (auto& handle: std_handles) {
|
||||
if (handle == nullptr || handle == INVALID_HANDLE_VALUE ||
|
||||
!SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) {
|
||||
handle = nullptr;
|
||||
} else if (std::find(inherited.begin(), inherited.end(), handle) == inherited.end()) {
|
||||
inherited.push_back(handle);
|
||||
}
|
||||
}
|
||||
SIZE_T attributes_size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributes_size);
|
||||
std::vector<uint8_t> attributes_storage(attributes_size);
|
||||
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes_storage.data());
|
||||
const bool inherit =
|
||||
!inherited.empty() && InitializeProcThreadAttributeList(attributes, 1, 0, &attributes_size);
|
||||
const bool listed =
|
||||
inherit && UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherited.data(), inherited.size() * sizeof(HANDLE),
|
||||
nullptr, nullptr);
|
||||
STARTUPINFOEXW startup {};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
if (listed) {
|
||||
startup.lpAttributeList = attributes;
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = std_handles[0];
|
||||
startup.StartupInfo.hStdOutput = std_handles[1];
|
||||
startup.StartupInfo.hStdError = std_handles[2];
|
||||
}
|
||||
// A process with a console shares it with the new one; without one, none is created.
|
||||
const DWORD flags = (listed ? EXTENDED_STARTUPINFO_PRESENT : 0u) |
|
||||
(GetConsoleWindow() != nullptr ? 0u : static_cast<DWORD>(CREATE_NO_WINDOW));
|
||||
// The new process continues this one's log file; it inherits the environment.
|
||||
(void)SetEnvironmentVariableA(Log::RelaunchedVariable, "1");
|
||||
PROCESS_INFORMATION process {};
|
||||
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, nullptr, &startup, &process)) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
|
||||
const bool created =
|
||||
CreateProcessW(executable, command.data(), nullptr, nullptr, listed ? TRUE : FALSE, flags,
|
||||
nullptr, nullptr, &startup.StartupInfo, &process) != 0;
|
||||
const auto error = GetLastError();
|
||||
if (inherit) {
|
||||
DeleteProcThreadAttributeList(attributes);
|
||||
}
|
||||
if (!created) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", error);
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
std::fflush(nullptr);
|
||||
std::quick_exit(0);
|
||||
#else
|
||||
std::ifstream command("/proc/self/cmdline", std::ios::binary);
|
||||
@@ -116,8 +166,20 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
host_argv.push_back(argument.data());
|
||||
}
|
||||
host_argv.push_back(nullptr);
|
||||
// The new program continues this one's log file. It is given its environment explicitly:
|
||||
// setenv is not safe while other threads may read the environment.
|
||||
const std::string relaunched = std::string(Log::RelaunchedVariable) + "=";
|
||||
std::string relaunched_entry = relaunched + "1";
|
||||
std::vector<char*> environment;
|
||||
for (char** variable = environ; variable != nullptr && *variable != nullptr; ++variable) {
|
||||
if (!std::string_view(*variable).starts_with(relaunched)) {
|
||||
environment.push_back(*variable);
|
||||
}
|
||||
}
|
||||
environment.push_back(relaunched_entry.data());
|
||||
environment.push_back(nullptr);
|
||||
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
|
||||
execvp(host_argv[0], host_argv.data());
|
||||
execvpe(host_argv[0], host_argv.data(), environment.data());
|
||||
}
|
||||
LOGF("LoadExec failed: %s\n", std::strerror(errno));
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Exercise production host input with deterministic gamepads and guest output.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "HostInputTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Sint16, 2> physical_stick {};
|
||||
std::array<int, 2> guest_stick {128, 128};
|
||||
uint32_t guest_buttons = 0;
|
||||
bool guest_touch = false;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
Sint16 GetGamepadAxis(SDL_Gamepad*, SDL_GamepadAxis axis) {
|
||||
return physical_stick[axis == SDL_GAMEPAD_AXIS_LEFTY ? 1 : 0];
|
||||
}
|
||||
Uint64 GetTicks() {
|
||||
return 1000;
|
||||
}
|
||||
bool HideCursor() {
|
||||
return true;
|
||||
}
|
||||
bool ShowCursor() {
|
||||
return true;
|
||||
}
|
||||
SDL_MouseButtonFlags GetMouseState(float* x, float* y) {
|
||||
*x = *y = 0.0f;
|
||||
return 0;
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_GetGamepadAxis Fake::GetGamepadAxis
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_HideCursor Fake::HideCursor
|
||||
#define SDL_ShowCursor Fake::ShowCursor
|
||||
#define SDL_GetMouseState Fake::GetMouseState
|
||||
#include "graphics/presentation/window/hostInput.cpp"
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_GetGamepadAxis
|
||||
#undef SDL_GetTicks
|
||||
#undef SDL_HideCursor
|
||||
#undef SDL_ShowCursor
|
||||
#undef SDL_GetMouseState
|
||||
|
||||
namespace Libs::Controller {
|
||||
void SetButton(int id, uint32_t button, bool down) {
|
||||
if (id == 1) {
|
||||
guest_buttons = down ? guest_buttons | button : guest_buttons & ~button;
|
||||
}
|
||||
}
|
||||
void SetAxis(int id, Axis axis, int value) {
|
||||
if (id == 1 && (axis == Axis::LeftX || axis == Axis::LeftY)) {
|
||||
guest_stick[axis == Axis::LeftY ? 1 : 0] = value;
|
||||
}
|
||||
}
|
||||
void SetTouchPad(int id, int finger, bool down, float, float) {
|
||||
if (id == 1 && finger == 1) {
|
||||
guest_touch = down;
|
||||
}
|
||||
}
|
||||
void SetRightStick(int, int, int) {}
|
||||
void ToggleGyroEmulation(int) {}
|
||||
void CycleSetting(Setting) {}
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Graphics;
|
||||
using namespace Libs::Controller;
|
||||
|
||||
struct Inputs {
|
||||
Inputs() {
|
||||
physical_stick = {};
|
||||
guest_stick = {128, 128};
|
||||
guest_buttons = 0;
|
||||
guest_touch = false;
|
||||
HostInputInit(nullptr);
|
||||
}
|
||||
~Inputs() { HostInputShutdown(); }
|
||||
};
|
||||
|
||||
void TestTouchPadStickHandoff() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MAX, SDL_JOYSTICK_AXIS_MIN};
|
||||
guest_stick = {255, 0};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {128, 128}, "entering touchpad mode retained guest movement");
|
||||
PollGamepadTouchPad(1008);
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
Check(cursor.x > 0.5f && cursor.y < 0.5f,
|
||||
"entering touchpad mode did not sample an already-held stick");
|
||||
Check(HostInputGamepadAxis(1, true, 127) && HostInputGamepadAxis(1, false, 127),
|
||||
"touchpad mode did not capture stick releases");
|
||||
Check(guest_stick == std::array {128, 128}, "touchpad movement leaked to guest axes");
|
||||
// Sample current physical axes on exit even if an event was consumed elsewhere.
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {0, 255}, "leaving touchpad mode did not restore current axes");
|
||||
Check(!HostInputGamepadAxis(1, true, 127), "leaving touchpad mode retained stick capture");
|
||||
}
|
||||
|
||||
void TestKeyboardSwitchesFromGamepadTouchPad() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(HostInputGamepadDrawButton(1, true) && guest_touch, "gamepad drawing did not start");
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(GetHostInputTouchPadVisualState().enabled && !HostInputGamepadAxis(1, true, 127),
|
||||
"F6 failed to switch from gamepad to mouse touchpad mode");
|
||||
Check(!guest_touch && guest_stick == std::array {0, 255},
|
||||
"switching to mouse touchpad mode retained gamepad input capture");
|
||||
HostInputKey(SDLK_F6, false);
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(!GetHostInputTouchPadVisualState().enabled, "F6 failed to disable mouse touchpad mode");
|
||||
}
|
||||
|
||||
void TestIndependentTriggerEdges() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(2, true, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "an inactive pad suppressed the active trigger press");
|
||||
HostInputPadTrigger(2, true, 0);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == 0, "an inactive pad suppressed the active trigger release");
|
||||
HostInputPadTrigger(1, false, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == PAD_BUTTON_CIRCLE, "left and right trigger edges were mixed");
|
||||
HostInputPadTrigger(1, false, 0);
|
||||
Check(guest_buttons == 0, "right trigger release was lost");
|
||||
}
|
||||
|
||||
void TestTriggerLifecycleReset() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputDisconnectGamepad(1);
|
||||
guest_buttons = 0;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "disconnect retained a previous trigger press");
|
||||
HostInputShutdown();
|
||||
guest_buttons = 0;
|
||||
HostInputInit(nullptr);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "shutdown retained a previous trigger press");
|
||||
}
|
||||
|
||||
void TestDisabledMappingTabs() {
|
||||
Config::ConfigOptions options;
|
||||
const auto south = InputMap::SourceFromButton(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
Config::Load(options);
|
||||
const InputMap defaults;
|
||||
Check(!defaults.Custom() && defaults.PadControl(south) == ControlFromName("Cross"),
|
||||
"empty configuration lost its default mappings");
|
||||
options.keymap = {"Keyboard=Disabled"};
|
||||
Config::Load(options);
|
||||
const InputMap keyboard_disabled;
|
||||
Check(keyboard_disabled.Custom() && keyboard_disabled.FindKey(SDLK_J) == INVALID_CONTROL &&
|
||||
keyboard_disabled.PadControl(south) == ControlFromName("Cross"),
|
||||
"disabling keyboard mappings changed gamepad defaults or restored keyboard input");
|
||||
for (const auto& keymap: {Config::Keymap {"Pad=Disabled"},
|
||||
Config::Keymap {"Keyboard=Disabled", "Pad=Disabled"}}) {
|
||||
options.keymap = keymap;
|
||||
Config::Load(options);
|
||||
const InputMap disabled;
|
||||
Check(disabled.Custom() == (keymap.size() == 2),
|
||||
"disabling gamepad mappings changed keyboard defaults");
|
||||
for (std::size_t source = 0; source < PAD_SOURCES.size(); source++) {
|
||||
Check(disabled.PadControl(source) == INVALID_CONTROL,
|
||||
"a disabled gamepad mapping reverted to its default");
|
||||
}
|
||||
}
|
||||
options.keymap = {"Cross=J", "Pad:Circle=South"};
|
||||
Config::Load(options);
|
||||
const InputMap mapped;
|
||||
Check(mapped.Custom() && mapped.FindKey(SDLK_J) == ControlFromName("Cross") &&
|
||||
mapped.PadControl(south) == ControlFromName("Circle"),
|
||||
"explicit mappings no longer override defaults");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
Config::ConfigOptions options;
|
||||
options.keymap = {"Pad:Cross=LeftTrigger", "Pad:Circle=RightTrigger"};
|
||||
Config::Load(options);
|
||||
TestTouchPadStickHandoff();
|
||||
TestKeyboardSwitchesFromGamepadTouchPad();
|
||||
TestIndependentTriggerEdges();
|
||||
TestTriggerLifecycleReset();
|
||||
TestDisabledMappingTabs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -235,6 +235,9 @@ void TestSysmoduleReferences() {
|
||||
const auto unload = resolve("eR2bZFAAU0Q");
|
||||
const auto is_loaded = resolve("fMP5NHUOaMk");
|
||||
constexpr int unloaded = static_cast<int>(0x805a1001u);
|
||||
Check(load(0x115) == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded &&
|
||||
unload(0x115) == unloaded,
|
||||
"missing packaged NpCppWebApi fails without recording a loaded module");
|
||||
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
|
||||
"loading entitlement access does not mark AppContent loaded");
|
||||
Check(unload(0x113) == OK, "release independent module reference");
|
||||
@@ -255,6 +258,9 @@ void TestSysmoduleReferences() {
|
||||
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
|
||||
internal_symbol->vaddr);
|
||||
int result = -1;
|
||||
Check(internal_load(0x115, 0, 0, 0, &result) == Libs::LibKernel::KERNEL_ERROR_ENOENT &&
|
||||
result == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded,
|
||||
"internal module loading propagates missing native module failure");
|
||||
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
|
||||
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
|
||||
"internal and public module operations share load state");
|
||||
@@ -407,7 +413,9 @@ void TestAddContent(const std::filesystem::path& root) {
|
||||
Check(mount(0, nullptr, &point) == static_cast<int>(0x80d90002u) &&
|
||||
list(0, nullptr, 0, nullptr) == static_cast<int>(0x817d0002u),
|
||||
"null add-on parameters return parameter errors");
|
||||
Check(mount(0, &info[3].label, &point) == OK, "mount owned add-on");
|
||||
Check(mount(0, &info[3].label, &point) == OK &&
|
||||
!std::string_view(point.data).ends_with('/'),
|
||||
"mount owned add-on without trailing slash");
|
||||
const std::string mounted_file = std::string(point.data) + "/payload.txt";
|
||||
const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0);
|
||||
std::array<char, 11> payload {};
|
||||
@@ -416,8 +424,9 @@ void TestAddContent(const std::filesystem::path& root) {
|
||||
FileSystem::KernelClose(fd) == OK,
|
||||
"guest reads files from the selected title's add-on");
|
||||
MountPoint repeated {};
|
||||
Check(mount(0, &info[3].label, &repeated) == static_cast<int>(0x80d90003u),
|
||||
"duplicate mount returns BUSY");
|
||||
Check(mount(0, &info[3].label, &repeated) == OK &&
|
||||
std::strcmp(repeated.data, point.data) == 0,
|
||||
"duplicate mount returns the same mount point");
|
||||
Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() &&
|
||||
unmount(&point) == static_cast<int>(0x80d90004u),
|
||||
"unmount removes mapping and repeated unmount reports NOT_MOUNTED");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user