Compare commits

...

20 Commits

Author SHA1 Message Date
Felipe b95ea4bb5f Support mountable-content DLC (addcont with real content) (#1581)
Support mountable-content DLC (addcont) and fix PlayGo chunk count query
2026-10-17 04:44:28 +02:00
Nawid3333 e93b4b0378 shader: support returning 32-bit LDS compare-exchange (DS_CMPST_RTN_B32) (#1578)
* shader: support DS_CMPST_RTN_B32

Assassin's Creed Valhalla (PPSA01490, #281) stops on DS opcode 0x30 in a
compute shader. DS compares DATA and stores DATA2, the opposite of
BUFFER_ATOMIC_CMPSWAP, so the 32-bit atomic path swaps the two for this
opcode, as the 64-bit form does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: reject unsupported GDS compare-store variant

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-17 04:37:16 +02:00
nmzik 0fd41ae206 Fix Linux CI xxHash include path (#1580)
Fix xxHash include path capitalization for test builds
2026-10-17 04:37:00 +02:00
nmzik 9202bddad2 recognize verified pattern fills
Co-authored-by: Juan Pablo Farias <juanpablo.profissional.a.f@gmail.com>
2026-10-17 04:09:54 +02:00
nmzik 32587337b5 renderdoc: advance capture flips independently of logging 2026-10-17 04:09:54 +02:00
Çağatay Güçlü 02ead0e2e1 shader: 64-bit buffer atomic add (BUFFER_ATOMIC_ADD_X2) 2026-10-17 04:09:54 +02:00
NeySlim 4f1f3b1bb3 shader: make the f32 clamp modifier turn NaN into 0 2026-10-17 04:09:53 +02:00
Nawid3333 42bb0da520 loader: keep the caller's flags in the TLS stubs (#1571)
A patched TLS load calls TlsRegStub and SafeCall, which moved RSP past the
red zone with sub/add rsp,0x80. ADD and SUB write the arithmetic flags, so
the sub ran before SafeCall's pushfq saved them and the add ran after its
popfq restored them: guest code that compares, loads fs:[0] and then
branches on the flags could branch the wrong way. lea moves RSP without
touching the flags. The entry lea plus a two-byte nop keeps every patched
offset in both stubs; the exit lea is one byte longer and moves only ret.

TlsStubsPreserveFlags calls the stubs with two flag patterns and checks
the status flags, the output register and RAX; it fails with the old
bytes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:05:08 +02:00
nmzik 5dec523143 Stub RazorCpu profiling markers in a dedicated library 2026-10-16 19:16:42 +02:00
nmzik d01633e18e Revert "Fix scalar buffer reads across sparse guest mappings"
This reverts commit d7974dc763.
2026-10-16 19:13:36 +02:00
nmzik e7ac242039 Load packaged system modules through the native module lifecycle 2026-10-16 19:13:36 +02:00
nmzik d7974dc763 Fix scalar buffer reads across sparse guest mappings 2026-10-16 18:20:31 +02:00
shadowbeat070 b899717900 renderer: log unsupported depth feedback once 2026-10-16 17:24:45 +02:00
shadowbeat070 d58aefde49 renderer: reduce synchronization and allocation overhead
Performance changes from #1495:

- Sync changed BDA ranges instead of scanning all mappings.
- Let VMA suballocate BDA buffers.
- Remove the renderer lock from presentation.
- Keep compatible pipeline caches across builds.
- Fold masked shifts that always produce zero.
2026-10-16 17:18:49 +02:00
Pipe530 4c6be6cd1d Localize trophy notifications with embedded JSON catalogs 2026-10-16 16:19:43 +02:00
Felipe 584c2e8ca6 Play snd0.at9 during the startup splash screen (#1545)
launcher: play snd0.at9 on title selection
2026-10-16 15:59:03 +02:00
nmzik 717b50155a launcher: rename executable to kyty_launcher 2026-10-16 12:57:28 +02:00
shadowbeat070 57b161019d shader: honor front-face input format 2026-10-16 12:57:27 +02:00
Felipe 3ff6606fcb Input mapping tabs UI, emulated touchpad, and rumble for non-PS5 controllers (#1541)
* Revamp input mapping dialog with tabs, gamepad remapping and gyro emulation toggle

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add emulated touchpad (mouse/gamepad) with particle skins; widen input dialog

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Disable mouse touchpad mode when the cursor leaves the window

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add touchpad hand-cursor toggle; share touchpad options across both tabs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Hide touchpad trail and particles when the visual toggle is off

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Rename input mapping tabs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Rumble non-DualSense gamepads from haptic audio

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix input mapping, touchpad transitions, and rumble fallback

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-16 04:04:37 +02:00
Ali Almohaya 6a0a666a41 loader: keep the output and log of a game LoadExec relaunches (#1480)
* loader: keep the output of a process LoadExec relaunches

LoadExec on Windows starts a new emulator process and exits the current
one. The new process was created without a console and without the
standard handles, so everything it printed was lost: a redirected log of
a game that relaunches itself ended at the relaunch.

Hand the new process the standard input, output and error handles,
inheriting only those, and let it share the console when there is one.

* loader: flush output before LoadExec starts the new program

On Windows, LoadExec flushed the C streams only after the new process
had been created, so output the old process still held could appear
after the new one's first lines. On Linux they were not flushed at all
before exec, which discards them.

Flush the log and every stream before starting the new program.

* loader: continue the log file across LoadExec

With --printf-direction File, the program LoadExec starts is given the
same --printf-output-file and opened it anew, emptying it. Everything
the first program had logged, its LoadExec line included, was lost.

LoadExec now sets KYTY_RELAUNCHED for the program it starts, which then
appends to the log file instead of replacing it. On Linux the variable
goes into the environment passed to exec rather than this process's
own, which other threads may be reading.

* loader: show LoadExec switches on the console

LoadExec logged the program it switched to only when guest output was
logged, and then only into the log: on a console the new program's
startup output followed the old one's without anything marking where
one program ended and the next began.

Write the switch to the console as well as the log, with the program's
arguments.
2026-10-16 03:01:17 +02:00
106 changed files with 3713 additions and 358 deletions
+11 -11
View File
@@ -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 \
+2 -2
View File
@@ -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
View File
@@ -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)
+5 -5
View File
@@ -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

+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "تم كسب الكأس!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofej získána!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofæ optjent!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trophäe erhalten!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trophy earned!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trophy earned!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "¡Ganaste un trofeo!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Pokaali ansaittu!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trophée obtenu !"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trophée obtenu !"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trófea megszerezve!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofi diperoleh!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofeo ottenuto!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "トロフィーを獲得しました!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "트로피를 획득했습니다!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofee behaald!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofé opptjent!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Zdobyto trofeum!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Troféu conquistado!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Troféu conquistado!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofeu câștigat!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Трофей получен!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Trofé intjänad!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Kupa kazanıldı!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "Đã nhận cúp!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "已获得奖杯!"
}
+3
View File
@@ -0,0 +1,3 @@
{
"trophy.toast.earned": "已獲得獎盃!"
}
+20
View File
@@ -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.
+14
View File
@@ -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
)
+53
View File
@@ -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
+13
View File
@@ -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
+3 -2
View File
@@ -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));
}
+4
View File
@@ -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);
+2 -8
View File
@@ -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",
+62
View File
@@ -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")
+15
View File
@@ -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
+7
View File
@@ -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;
};
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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)) {
+2 -1
View File
@@ -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;
}
+504 -7
View File
@@ -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;
+567 -3
View File
@@ -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);
+12 -13
View File
@@ -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();
+24 -40
View File
@@ -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
+6 -3
View File
@@ -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");
+1
View File
@@ -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;
+8 -2
View File
@@ -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);
}
+24
View File
@@ -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_
+13 -2
View File
@@ -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)
+2
View File
@@ -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;
+21 -6
View File
@@ -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_ */
+36
View File
@@ -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;
};
+45 -2
View File
@@ -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;
+452 -146
View File
@@ -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);
}
}
+10 -1
View File
@@ -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);
+106
View File
@@ -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;
}
}
+2 -2
View File
@@ -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
View File
@@ -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();
}
+1
View File
@@ -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();
+27
View File
@@ -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);
+22
View File
@@ -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());
+20 -5
View File
@@ -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;
}
+7 -7
View File
@@ -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());
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+205
View File
@@ -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;
}
+12 -3
View File
@@ -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