Compare commits

...

9 Commits

Author SHA1 Message Date
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
91 changed files with 3249 additions and 584 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"
+14
View File
@@ -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 {
@@ -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)) {
+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);
@@ -81,8 +81,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
}
uses_gds |= memory.kind == IR::ResourceKind::Gds;
uses_lds |= memory.kind == IR::ResourceKind::Lds;
} else if (memory.kind == IR::ResourceKind::Buffer ||
memory.kind == IR::ResourceKind::ScalarBuffer) {
} else if (memory.kind == IR::ResourceKind::Buffer) {
if (memory.resource >= program.info.buffers.size()) {
Fail(program, "typed shader contains an invalid buffer resource");
}
@@ -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 ||
@@ -867,7 +867,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
});
}
uint32_t LoadIndirectScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
uint32_t LoadScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto& handle = *inst.Arg(0).ResolveInstruction();
const auto word1 = ctx.Arg(handle, 1);
@@ -1545,29 +1545,8 @@ uint32_t EmitReadConst(ValueEmitContext& ctx, const IR::Inst& inst) {
}
void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
auto mem = ctx.Memory(inst);
if (mem.planning_only) return;
if (mem.kind == IR::ResourceKind::IndirectBuffer) {
ctx.Define(inst, LoadIndirectScalarBuffer(ctx, inst));
return;
}
auto& state = ctx.state;
mem.kind = IR::ResourceKind::ScalarBuffer;
auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2));
if (mem.offset >= 4u) {
index = Binary(state, spv::OpIAdd, TypeU32(state), index, ConstantU32(state, mem.offset >> 2u));
}
const auto access = PrepareMemoryResourceAccess(state, mem);
// Scalar buffer addressing aligns the base and each offset independently.
const auto element = Binary(
state, spv::OpIAdd, TypeU32(state), index,
Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.byte_offset, ConstantU32(state, 2)));
const auto condition = EmitMemoryElementInBounds(state, access, element);
ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() {
return EmitNative<spv::OpLoad, IR::Type::U32>(
state, EmitMemoryElementPointer(state, access, element));
}));
if (ctx.Memory(inst).planning_only) return;
ctx.Define(inst, LoadScalarBuffer(ctx, inst));
}
uint32_t EmitGetBufferResourceIndex(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -269,10 +269,8 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
case IR::ResourceKind::Flat:
case IR::ResourceKind::Global:
EXIT("physical address memory must use the BDA emitter\n");
case IR::ResourceKind::ScalarBuffer:
case IR::ResourceKind::Buffer: {
const auto bits = mem.kind == IR::ResourceKind::Buffer
? IR::StorageBufferElementBits(state.program, mem) : 32u;
const auto bits = IR::StorageBufferElementBits(state.program, mem);
auto* buffer = &state.storage_buffers[2];
if (bits == 8u) {
buffer = &state.storage_buffers[0];
@@ -128,7 +128,6 @@ struct BufferResource {
bool written = false;
bool atomic = false;
bool formatted = false;
bool scalar = false;
bool live = false;
uint32_t indirect_root = NoIndirectBuffer;
uint32_t indirect_mapping_offset = 0;
@@ -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;
@@ -1178,7 +1178,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
}
if (store == nullptr || visited.size() != program.blocks.size()) return {};
for (const auto& buffer: program.info.buffers) {
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
if (buffer.read) return {};
}
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
UniformFillPlan result;
@@ -1589,7 +1589,6 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
base.formatted ? descriptor.Format() : Prospero::BufferFormat::kInvalid;
buffer.descriptor_swizzle = base.formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7);
buffer.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u;
buffer.scalar_empty = base.scalar && descriptor.GetSize() == 0u;
}
snapshot.images.resize(program.info.images.size());
specialization.images.resize(program.info.images.size());
@@ -1787,17 +1786,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
auto& memory_info = program.memory_info;
// Resolve empty scalar-table words before folding the indirect descriptors that use them.
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() != ValueOpcode::ReadConstBuffer) continue;
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::ScalarBuffer &&
specialization.buffers[memory.resource].scalar_empty &&
buffers[memory.resource].indirect_root == BufferResource::NoIndirectBuffer)
inst.ReplaceUsesWith(Value(0u));
}
}
const ImageRemap image_remap(specialization);
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
@@ -13,7 +13,6 @@ struct ResourceSpecialization {
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
bool zero_stride_oob = false;
bool scalar_empty = false;
uint32_t indirect_root = BufferResource::NoIndirectBuffer;
uint32_t indirect_mapping_offset = 0;
uint32_t indirect_search_iterations = 0;
@@ -3735,8 +3735,6 @@ private:
resource.written = resource.written || write;
resource.atomic = resource.atomic || atomic;
resource.formatted = resource.formatted || memory.formatted;
resource.scalar = resource.scalar || op == ValueOpcode::ReadConstBuffer ||
memory.kind == ResourceKind::ScalarBuffer;
}
uint32_t AddImage(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
@@ -3883,6 +3881,24 @@ private:
if (memory.planning_only || IsIndirectPlanningMemory(flags.index)) {
return;
}
if (op == ValueOpcode::ReadConstBuffer) {
const auto* handle = inst.Arg(0).Resolve().TryInstruction();
if (memory.kind != ResourceKind::ScalarBuffer || handle == nullptr ||
handle->GetOpcode() != ValueOpcode::GetBufferResource || handle->NumArgs() != 4u) {
Fail(flags.pc, "scalar buffer read requires a four-DWORD buffer descriptor");
}
// Scalar descriptors can span disjoint guest mappings. Keep their raw GPU operands
// separate from handles that vector-resource planning may rewrite.
auto* block = inst.Parent();
const auto where = std::ranges::find_if(block->Instructions(),
[&](const Inst& candidate) { return &candidate == &inst; });
auto& scalar = *block->PrependNewInst(where, ValueOpcode::GetBufferResource,
{handle->Arg(0), handle->Arg(1), handle->Arg(2), handle->Arg(3)});
inst.SetArg(0, Value(&scalar));
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
m_info.uses_dma = true;
return;
}
Inst* handle = nullptr;
uint32_t source = 0;
uint32_t resource = 0;
@@ -3894,12 +3910,11 @@ private:
source = indirect->source;
} else if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc,
memory.resource * 4u, handle, source)) {
if (memory.kind != (op == ValueOpcode::ReadConstBuffer ? ResourceKind::ScalarBuffer
: ResourceKind::Buffer) ||
if (memory.kind != ResourceKind::Buffer ||
!memory.SupportsIndirectBufferLoad(op)) {
Fail(flags.pc,
"buffer descriptor is not a valid runtime value; GPU-selected access "
"requires a scalar, raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
"requires a raw 8/16-bit, raw DWORD x1/x2/x3/x4, or formatted X load");
}
m_program.memory_info[flags.index].kind = ResourceKind::IndirectBuffer;
m_info.uses_dma = true;
@@ -307,7 +307,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
if (shared_access != SharedAccess::None) {
info.uses_lds |= kind == ResourceKind::Lds;
info.uses_gds |= kind == ResourceKind::Gds;
} else if (kind == ResourceKind::Buffer || kind == ResourceKind::ScalarBuffer) {
} else if (kind == ResourceKind::Buffer) {
if (memory.resource >= info.buffers.size()) {
return Fail("buffer operation has invalid resource metadata");
}
@@ -89,7 +89,8 @@ bool IsRawRead(const ResourcePlan& values, const Inst& inst) {
const auto& memory = values.memory_info[index];
if (op != ValueOpcode::LoadBufferU32) {
return (op == ValueOpcode::LoadAddressU32 && memory.kind == ResourceKind::ScalarAddress) ||
(op == ValueOpcode::ReadConstBuffer && memory.kind == ResourceKind::ScalarBuffer);
(op == ValueOpcode::ReadConstBuffer &&
(memory.kind == ResourceKind::ScalarBuffer || memory.kind == ResourceKind::IndirectBuffer));
}
if (inst.NumArgs() != 5u || memory.kind != ResourceKind::Buffer || memory.typed ||
memory.formatted || memory.coherent || memory.data_bits != 32u ||
+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;
+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);
+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;
}
+80
View File
@@ -213,6 +213,12 @@ struct ProtectionCall {
std::vector<ProtectionCall> g_protection_log;
std::mutex g_protection_log_mutex;
struct ProtectionGate {
std::binary_semaphore entered{0};
std::binary_semaphore resume{0};
};
ProtectionGate *g_protection_gate = nullptr;
void ResetProtectionLog() {
g_protection_calls = 0;
g_protection_log.clear();
@@ -220,6 +226,10 @@ void ResetProtectionLog() {
bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
Common::VirtualMemory::Mode mode) {
if (g_protection_gate != nullptr && mode == Common::VirtualMemory::Mode::ReadWrite) {
g_protection_gate->entered.release();
g_protection_gate->resume.acquire();
}
uint32_t protection = PAGE_NOACCESS;
if (mode == Common::VirtualMemory::Mode::Read) {
protection = PAGE_READONLY;
@@ -407,6 +417,74 @@ void TestCpuDirtyUpload() {
Release(memory);
}
void TestBdaSyncDirtiness() {
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = Allocate(harness.page_manager, 3);
const auto address = reinterpret_cast<uint64_t>(memory);
Check(tracker.TakeBdaSync().Empty(), "new tracker queued unowned memory");
tracker.QueueBdaSync(address, 3 * page);
Check(tracker.IsRegionCpuModified(address, page), "new region was clean");
auto pending = tracker.TakeBdaSync();
Check(pending.Contains(address, 3 * page) && !pending.Intersects(address + 3 * page, page),
"mapped or registered range did not bound initial BDA synchronization");
tracker.ForEachUploadRange(address, 3 * page, false,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
Check(tracker.TakeBdaSync().Empty(), "clean upload queued redundant BDA work");
tracker.MarkRegionAsCpuModified(address + 17, 1);
pending = tracker.TakeBdaSync();
Check(pending.Contains(address, page) && !pending.Intersects(address + page, page) &&
tracker.TakeBdaSync().Empty(),
"byte dirtiness did not drain exactly its whole tracker page");
// A write after Take belongs to the next drain, even when its page was just taken.
tracker.MarkRegionAsCpuModified(address + 31, 1);
Check(tracker.TakeBdaSync().Contains(address, page), "re-dirty event was lost");
tracker.ForEachUploadRange(address, page, true,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
Check(tracker.TakeBdaSync().Empty(), "GPU ownership queued an unnecessary upload");
tracker.InvalidateRegion(address + 7, 1, [&] {
tracker.UnmarkRegionAsGpuModified(address, page);
tracker.MarkRegionAsCpuModified(address, page);
});
Check(tracker.TakeBdaSync().Contains(address, page),
"GPU-to-CPU invalidation did not queue BDA synchronization");
tracker.InvalidateRegion(address + page + 7, 1, [] {});
Check(tracker.TakeBdaSync().Contains(address + page, page),
"CPU invalidation did not queue BDA synchronization");
tracker.UntrackMemory(address, 3 * page);
Check(tracker.TakeBdaSync().Contains(address, 3 * page),
"untracked memory did not queue its newly dirty pages");
Release(memory);
}
void TestBdaSyncConcurrentDrain() {
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = Allocate(harness.page_manager, 1);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(address, page, false,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
(void)tracker.TakeBdaSync();
ProtectionGate gate;
g_protection_gate = &gate;
std::jthread writer([&] { tracker.MarkRegionAsCpuModified(address + 17, 1); });
gate.entered.acquire();
// Ownership is still changing. Publishing its event before this point would let
// a drain consume it before the writer has finished releasing page protection.
Check(tracker.TakeBdaSync().Empty(), "BDA event preceded its state transition");
gate.resume.release();
writer.join();
g_protection_gate = nullptr;
Check(tracker.TakeBdaSync().Contains(address, page) &&
tracker.IsRegionCpuModified(address, page),
"concurrent drain lost a completed CPU dirty transition");
tracker.UntrackMemory(address, page);
Release(memory);
}
void TestCleanUploadPreservesOwnership() {
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
@@ -1202,6 +1280,8 @@ int main(int argc, char **argv) {
TestQueriesDoNotRequireMappedOwnership();
TestConcurrentRegionPublication();
TestCpuDirtyUpload();
TestBdaSyncDirtiness();
TestBdaSyncConcurrentDrain();
TestCleanUploadPreservesOwnership();
TestRangeInvalidation();
TestGpuReacquisitionAfterInvalidation();
+46 -3
View File
@@ -143,13 +143,13 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
SDL_AudioDeviceID GetAudioStreamDevice(SDL_AudioStream*) {
return 999;
}
// Pads 1 and 3 are DualSenses.
// Pads 1 and 3 are DualSenses; pad 2 only supports rumble.
SDL_GamepadType GetGamepadTypeForID(SDL_JoystickID id) {
return id == 1 || id == 3 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN;
}
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
: nullptr;
return id >= 1 && id <= 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
: nullptr;
}
SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) {
return GetGamepadTypeForID(static_cast<SDL_JoystickID>(reinterpret_cast<uintptr_t>(pad)));
@@ -458,6 +458,47 @@ void TestDiscoveryAndHotplug() {
Check(streams.empty(), "disconnected endpoint retained its stream");
}
void TestRumbleFallbackGains() {
Fixture f;
auto port = Open();
const auto queue = [&](std::array<float, 2> gains) {
Check(Haptics::Queue(port.get(), 2, pcm.data(), 2, 2, true, unity.data(), 1.0f,
gains.data()) == 2 * 1000000 / 48000,
"rumble fallback playback time is wrong");
};
queue({0.0f, 0.25f});
Check(pad_rumble[2].large == 0 && pad_rumble[2].small == 16383,
"rumble fallback ignored per-channel gains");
queue({0.5f, 0.0f});
Check(pad_rumble[2].large == 32767 && pad_rumble[2].small == 0,
"rumble fallback swapped per-channel gains");
queue({0.0f, 0.0f});
Check(pad_rumble[2].large == 0 && pad_rumble[2].small == 0,
"muted fallback channels still rumbled");
Check(streams.empty() && hid_opens == 0, "rumble fallback opened an audio device");
}
void TestRumbleFallbackSwitch() {
for (bool bluetooth: {false, true}) {
Fixture f;
wireless = hid_available = bluetooth;
auto port = Open();
Queue(port, pcm.data());
Check(bluetooth ? Haptics::UsesBluetooth(port.get()) : streams.size() == 1,
"DualSense transport did not open before fallback");
Haptics::Queue(port.get(), 2, pcm.data(), 2, 2, true, unity.data());
Check(streams.empty() && !Haptics::UsesBluetooth(port.get()) &&
(!bluetooth || hid_closes == 1),
"switching to rumble retained the DualSense transport");
Check(pad_rumble[2].large == 65535 && pad_rumble[2].small == 65535,
"switching from DualSense did not start rumble fallback");
Queue(port, pcm.data());
Check(bluetooth ? Haptics::UsesBluetooth(port.get()) && hid_opens == 2
: streams.size() == 1 && opens == 2,
"switching back delayed reopening the DualSense transport");
}
}
void TestSpeaker() {
Fixture f;
devices = {{10, "DualSense Wireless Controller", 2}};
@@ -1279,6 +1320,8 @@ int main() {
TestAudioDefaultResumeFailure();
TestFormatsAndVolume();
TestDiscoveryAndHotplug();
TestRumbleFallbackGains();
TestRumbleFallbackSwitch();
TestSpeaker();
TestSpeakerUnplug();
TestSpeakerSwitchAndFailures();
-110
View File
@@ -1024,115 +1024,6 @@ void TestCyclicControlPredicatesRemainUnknown() {
"cyclic control lost a possible branch during reachability");
}
void TestEmptyScalarBufferSpecialization() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
struct Fixture {
Program program;
std::vector<std::pair<Inst *, Type>> uses;
Inst *nonnull = nullptr;
};
const auto make_fixture = [] {
Fixture fixture;
auto &program = fixture.program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = program.resource_tracking_complete = true;
program.user_data_count = 4u;
auto &block = AddValueBlock(program);
DescriptorSource source;
source.dword_count = 4u;
for (uint32_t word = 0; word < 4u; ++word)
source.dwords[word] = Value(&block.AppendNewInst(ValueOpcode::GetUserData,
{Value(static_cast<ScalarReg>(word))}));
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0u, .read = true, .scalar = true});
const auto table = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]}));
std::array<Value, 4> words;
for (uint32_t word = 0; word < 4u; ++word) {
program.memory_info.push_back({.kind = ResourceKind::ScalarBuffer, .resource = 0u,
.offset = word * 4u});
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {table, Value(0u)});
read.SetFlags(MemoryFlags{.index = word});
words[word] = Value(&read);
}
const auto handle = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
{words[0], words[1], words[2], words[3]}));
for (const auto opcode : {ValueOpcode::ReadConstBuffer, ValueOpcode::LoadBufferU8,
ValueOpcode::LoadBufferU16, ValueOpcode::LoadBufferU32,
ValueOpcode::LoadBufferU32x2, ValueOpcode::LoadBufferU32x3,
ValueOpcode::LoadBufferU32x4}) {
const auto index = static_cast<uint32_t>(program.memory_info.size());
program.memory_info.push_back({.kind = ResourceKind::IndirectBuffer, .resource = UINT32_MAX,
.data_dwords = BufferComponentCount(opcode),
.data_bits = opcode == ValueOpcode::LoadBufferU8 ? 8u : opcode == ValueOpcode::LoadBufferU16 ? 16u : 32u,
.formatted = opcode == ValueOpcode::LoadBufferU32});
auto &read = opcode == ValueOpcode::ReadConstBuffer
? block.AppendNewInst(opcode, {handle, Value(0u)})
: block.AppendNewInst(opcode, {handle, Value(0u), Value(0u), Value(0u), Value(true)});
read.SetFlags(MemoryFlags{.index = index});
fixture.uses.emplace_back(&block.AppendNewInst(ValueOpcode::Identity, {Value(&read)}), read.GetType());
}
const auto nonnull = Value(&block.AppendNewInst(ValueOpcode::GetBufferResource,
{words[0], words[1], words[2], Value(1u)}));
auto &read = block.AppendNewInst(ValueOpcode::ReadConstBuffer, {nonnull, Value(0u)});
read.SetFlags(MemoryFlags{.index = 4u});
fixture.nonnull = &block.AppendNewInst(ValueOpcode::Identity, {Value(&read)});
return fixture;
};
auto fixture = make_fixture();
auto plan = ExtractResourcePlan(fixture.program);
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
uint32_t reads = 0u;
const SrtRuntime runtime{.user_data = descriptor, .read_memory = RejectSpecializationRead,
.userdata = &reads, .read_specialization_memory = RejectSpecializationRead};
ResourceSnapshot snapshot;
ResourceSpecialization specialization, empty, populated;
for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) {
descriptor[2] = records;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && reads == 0u &&
specialization.buffers[0].scalar_empty == (records == 0u),
"scalar emptiness read table contents or truncated its 64-bit byte size");
if (records == 0u) {
if (empty.buffers.empty()) empty = specialization;
else Check(specialization == empty, "returning to an empty scalar table changed its specialization");
}
else if (records == 1u) populated = specialization;
else Check(specialization == populated, "nonempty scalar size created a shader permutation");
auto current = make_fixture();
ApplyResourceSpecialization(current.program, specialization);
for (const auto &[use, type] : current.uses) {
const auto value = use->Arg(0).Resolve();
Check(value.GetType() == type, "empty scalar-table folding changed the load result type");
if (records != 0u) {
Check(value.TryInstruction() != nullptr && BufferAccessOf(value.Instruction()->GetOpcode()) == BufferAccess::Read,
"nonempty scalar table lost a runtime buffer load");
} else if (value.IsImmediate()) {
Check(value == Value(uint8_t{0}) || value == Value(uint16_t{0}) || value == Value(0u),
"empty scalar table did not produce a typed zero");
} else {
for (size_t component = 0; component < value.Instruction()->NumArgs(); ++component)
Check(value.Instruction()->Arg(component) == Value(0u),
"empty scalar table retained a nonzero vector component");
}
}
Check(current.nonnull->Arg(0).Resolve().TryInstruction() != nullptr,
"a partly nonzero indirect descriptor was treated as unbound");
}
Check(empty != populated, "empty-to-populated scalar transition reused the folded specialization");
plan.info.buffers[0].scalar = false;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
!specialization.buffers[0].scalar_empty,
"vector-only resource acquired scalar-empty specialization");
empty.buffers[0].indirect_root = 0u;
empty.buffers.push_back(empty.buffers[0]);
empty.buffers[1].scalar_empty = false;
ApplyResourceSpecialization(fixture.program, empty);
for (const auto &[use, type] : fixture.uses)
Check(use->Arg(0).GetType() == type && use->Arg(0).Resolve().TryInstruction() != nullptr &&
BufferAccessOf(use->Arg(0).ResolveInstruction()->GetOpcode()) == BufferAccess::Read,
"empty root folded a scalar read with nonempty candidates");
}
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = MixedSamplerProgram();
@@ -1332,7 +1223,6 @@ int main() {
for (const bool independent_mask : {false, true})
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
TestCyclicControlPredicatesRemainUnknown();
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
+48 -58
View File
@@ -312,7 +312,8 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
EliminateDeadCode(fixture->program.blocks);
ValidateProgram(fixture->program, true);
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(!source.selector && fixture->program.info.buffers.size() == (nested ? 3u : 2u) &&
Check(!source.selector && fixture->program.info.buffers.empty() &&
fixture->program.info.uses_dma &&
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
}),
@@ -3313,9 +3314,12 @@ void TestUniformScalarBufferImage() {
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
Check(std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) {
return source.indirect_descriptor.has_value();
}), "draw-uniform scalar image was expanded to GPU-selected candidates");
Check(plan.info.uses_dma && plan.info.buffers.empty() &&
fixture.program.memory_info[key.Instruction()->Flags<MemoryFlags>().index].kind ==
ResourceKind::IndirectBuffer &&
std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) {
return source.indirect_descriptor.has_value();
}), "draw-uniform scalar image lost its BDA read or expanded GPU-selected candidates");
std::array<uint32_t, 9> user_data{0x1000u, 0u, 0x100u, 0u,
0x2000u, 0u, 0x100u, 0u, 0u};
LinearTestMemory memory;
@@ -4642,7 +4646,6 @@ void TestComputeBufferFill() {
bool conditional = false;
bool shifted = false;
bool extra_store = false;
bool clean = false;
bool branch = false;
};
const auto Run = [](Options options) {
@@ -4670,12 +4673,9 @@ void TestComputeBufferFill() {
if (options.shifted)
index = fixture.Emit(ValueOpcode::IAdd32, {index, Value(1u)});
Value value(0u);
TestMemory memory;
memory.words[0] = 0x40404040u;
if (options.scalar) {
const auto input =
fixture.Buffer({Value(static_cast<uint32_t>(memory.base)),
Value(4u << 16), Value(1u), Value(0x14204u)});
fixture.Buffer({Value(0x1000u), Value(4u << 16), Value(1u), Value(0x14204u)});
MemoryInfo load;
load.kind = ResourceKind::ScalarBuffer;
value = fixture.Emit(ValueOpcode::ReadConstBuffer, {input, Value(0u)},
@@ -4703,48 +4703,26 @@ void TestComputeBufferFill() {
std::array<uint32_t, 4> userdata{0x200000u, 4u << 16, 0x4000u, 0x14204u};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto Read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &memory = *static_cast<TestMemory *>(data);
if (address != memory.base || words.size() != 1u)
return false;
++memory.reads;
words[0] = memory.words[0];
return true;
};
Check(MaterializeResources(
plan,
{.user_data = userdata,
.read_memory = Read,
.userdata = &memory,
.read_specialization_memory = options.clean ? Read : nullptr},
snapshot, specialization),
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
"fill fixture did not materialize");
const bool expected = !options.conditional && !options.shifted &&
!options.extra_store && !options.branch &&
(!options.scalar || options.clean);
!options.scalar;
Check((snapshot.uniform_fill.words != 0) == expected,
"fill proof accepted an unsafe store or missed the real GTA3 clear");
if (expected) {
Check(snapshot.uniform_fill.words == 1 &&
snapshot.uniform_fill.group_stride[0] == 64 &&
snapshot.uniform_fill.value ==
(options.scalar ? 0x40404040u : 0u),
snapshot.uniform_fill.value == 0u,
"fill proof lost address coverage or the actual stored scalar");
if (options.scalar && options.clean) {
Check(MaterializeResources(plan,
{.user_data = userdata, .read_memory = Read, .userdata = &memory},
snapshot, specialization) && snapshot.uniform_fill.words == 0,
"an unavailable clean value retained a previous uniform fill");
}
}
};
Run({});
Run({.scalar = true, .clean = true});
Run({.scalar = true});
Run({.conditional = true, .clean = true});
Run({.shifted = true, .clean = true});
Run({.extra_store = true, .clean = true});
Run({.clean = true, .branch = true});
Run({.conditional = true});
Run({.shifted = true});
Run({.extra_store = true});
Run({.branch = true});
}
void TestDenseBufferTracking() {
@@ -4819,8 +4797,8 @@ void TestScalarAndVectorBufferAlias() {
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
const auto scalar_flags = fixture.AddMemory(scalar, 4);
fixture.Emit(ValueOpcode::ReadConstBuffer, {descriptor, fixture.UserData(4)},
scalar_flags);
const auto scalar_read = fixture.Emit(ValueOpcode::ReadConstBuffer,
{descriptor, fixture.UserData(4)}, scalar_flags);
MemoryInfo vector;
vector.kind = ResourceKind::Buffer;
const auto vector_flags = fixture.AddMemory(vector, 8);
@@ -4829,12 +4807,18 @@ void TestScalarAndVectorBufferAlias() {
vector_flags);
fixture.PlanAndTrack();
Check(fixture.program.info.buffers.size() == 1 &&
fixture.program.info.buffers[0].scalar,
"typed scalar and vector uses of one descriptor were split");
Check(fixture.program.memory_info[scalar_flags.index].resource == 0 &&
Check(fixture.program.info.buffers.size() == 1 && fixture.program.info.uses_dma &&
fixture.program.memory_info[scalar_flags.index].kind == ResourceKind::IndirectBuffer &&
fixture.program.memory_info[vector_flags.index].resource == 0,
"scalar/vector alias did not share a dense index");
"scalar BDA access changed the shared vector descriptor binding");
TestMemory memory;
memory.words[1] = 0x12345678u;
const std::array<uint32_t, 5> user_data{0x1000u, 0u, 32u, 0u, 4u};
SrtWalker walker(fixture.program, {.user_data = user_data, .read_memory = ReadTestMemory,
.userdata = &memory});
uint32_t loaded = 0;
Check(walker.Evaluate(scalar_read, loaded) && loaded == memory.words[1] && memory.reads == 1u,
"scalar BDA read lost its raw descriptor or dynamic uniform offset");
}
void TestRuntimeUnsignedMinDescriptor() {
@@ -6333,7 +6317,7 @@ void TestGpuSelectedFormattedRecordStore() {
}
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
const auto& memory = fixture->program.memory_info[inst.Flags<MemoryFlags>().index];
Check(memory.kind == ResourceKind::ScalarBuffer && !memory.planning_only,
Check(memory.kind == ResourceKind::IndirectBuffer && !memory.planning_only,
"store planning consumed descriptor words still used by the BDA load");
++scalar_reads;
}
@@ -7765,9 +7749,9 @@ void TestConditionalBufferMaterialization() {
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
snapshot.buffers[1].dword_count == 4 &&
snapshot.buffers[1].dwords == std::array<uint32_t, 8>{},
snapshot.buffers.size() == 1 &&
snapshot.buffers[0].dword_count == 4 &&
snapshot.buffers[0].dwords == std::array<uint32_t, 8>{},
"untaken scalar branch materialized stale buffer words");
Check(snapshot.user_data == std::vector<uint32_t>(user_data.begin(), user_data.end()),
"resource reachability changed native shader user data");
@@ -7782,9 +7766,9 @@ void TestConditionalBufferMaterialization() {
runtime.user_data = user_data;
const auto CheckActive = [&] {
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
snapshot.buffers.size() == 1 &&
std::equal(user_data.begin() + 4, user_data.end(),
snapshot.buffers[1].dwords.begin()),
snapshot.buffers[0].dwords.begin()),
"potentially executed buffer descriptor was discarded");
};
CheckActive();
@@ -7871,6 +7855,15 @@ void TestGuardedScalarDescriptorReads() {
memory.data.words[0xb0 / 4] = 4u;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
if (!exec) {
for (const uint32_t enabled : {0u, 1u, 0u}) {
memory.data.words[7] = enabled;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.null_reads == 0 && snapshot.flattened_srt.empty(),
"disabled GPU address loads introduced CPU descriptor reads");
}
continue;
}
if (!shared) {
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 0 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
@@ -7883,11 +7876,8 @@ void TestGuardedScalarDescriptorReads() {
memory.data.watched_address = 0x10a8u;
memory.data.watched_reads = 0;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
std::ranges::any_of(snapshot.buffers, [](const auto& descriptor) {
return descriptor.dwords[0] == 0x2000u;
}) &&
(!exec || (memory.data.watched_reads == 1 &&
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end())),
memory.data.watched_reads == 1 &&
std::ranges::find(snapshot.flattened_srt, 0x2000u) != snapshot.flattened_srt.end(),
"active or shared descriptor read was omitted after refreshing the cached plan");
if (!shared) {
memory.data.words[7] = 0;
@@ -7916,9 +7906,9 @@ void TestConservativeBufferReachability() {
if (use != ConditionalBufferUse::Writable)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
snapshot.buffers.size() == (use == ConditionalBufferUse::Writable ? 2u : 1u) &&
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
snapshot.buffers.back().dwords.begin()),
"shared or loop-dependent resource was pruned, or inactive scalar branch retained");
}
}
+240 -9
View File
@@ -1383,6 +1383,7 @@ struct GraphicsCase {
std::vector<u32> vertices;
bool pixel_ancillary = false;
bool pixel_front_face = false;
bool pixel_front_face_all_bits = false;
u32 layers = 1;
vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1;
bool pixel_position_w = false;
@@ -1834,6 +1835,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) {
: ShaderPixelParameterMode::FirstVertex;
pixel_info.ps_ancillary = test.pixel_ancillary;
pixel_info.ps_front_face = test.pixel_front_face;
pixel_info.ps_front_face_all_bits = test.pixel_front_face_all_bits;
pixel_info.ps_pos_w = test.pixel_position_w;
pixel_info.ps_depth_export_enable = test.pixel_depth_export;
pixel_info.ps_system_input_base = 2;
@@ -5279,6 +5281,85 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
void CheckBdaSyncUploads() {
constexpr const char *name = "BdaSyncUploads";
constexpr auto page = BufferCache::CACHING_PAGESIZE;
EnsureRuntimeContext();
const auto base = LibKernel::Memory::AllocateRuntimeMemory(
0, 3 * page, Common::VirtualMemory::Mode::ReadWrite, "bda_sync_uploads");
Require(name, "allocation", base != 0, "guest allocation failed");
auto *words = reinterpret_cast<u32 *>(base);
std::fill_n(words, 3 * page / sizeof(u32), 0x12345678u);
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
auto &cache = context.GetBufferCache();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, page);
context.MapMemory(base + 2 * page, page);
const auto id = cache.FindBuffer(base, 3 * page);
auto &buffer = cache.GetBuffer(id);
auto readback = CreateHostBuffer(name, 6 * sizeof(u32),
vk::BufferUsageFlagBits::eTransferDst, {});
const auto snapshot = [&](uint64_t offset, uint32_t index) {
const vk::BufferCopy copy{offset, index * sizeof(u32), sizeof(u32)};
scheduler.Current().Handle().copyBuffer(buffer.Handle(), readback.buffer, 1, &copy);
};
context.PrepareBda();
Require(name, "mapped intersection",
!cache.IsRegionCpuModified(base, page) &&
cache.IsRegionCpuModified(base + page, page) &&
!cache.IsRegionCpuModified(base + 2 * page, page),
"BDA upload missed a mapped range or uploaded an unmapped hole");
snapshot(0, 0);
snapshot(2 * page, 1);
cache.InvalidateMemory(base + 17, 1);
words[4] = 0x87654321u;
words[TRACKER_PAGE_SIZE / sizeof(u32) - 1] = 0xabcdef01u;
context.PrepareBda();
snapshot(16, 2);
snapshot(TRACKER_PAGE_SIZE - sizeof(u32), 3);
Require(name, "whole-page upload", !cache.IsRegionCpuModified(base, TRACKER_PAGE_SIZE),
"one-byte invalidation did not upload and consume its whole page");
context.MapMemory(base + page, page);
context.PrepareBda();
snapshot(page, 4);
Require(name, "new mapping", !cache.IsRegionCpuModified(base + page, page),
"mapping into an existing buffer did not queue its dirty pages");
context.UnmapMemory(base + 2 * page, page);
words[2 * page / sizeof(u32)] = 0xfedcba98u;
context.PrepareBda();
Require(name, "unmapped dirty page", cache.IsRegionCpuModified(base + 2 * page, page),
"BDA synchronization consumed dirtiness outside mapped memory");
context.MapMemory(base + 2 * page, page);
context.PrepareBda();
snapshot(2 * page, 5);
vk::BufferMemoryBarrier barrier{};
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eHostRead;
barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.buffer = readback.buffer;
barrier.size = readback.size;
scheduler.Current().Handle().pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eHost, {}, 0, nullptr, 1, &barrier, 0, nullptr);
scheduler.Finish();
Require(name, "GPU contents", ReadBuffer(name, readback, 6) ==
std::vector<u32>{0x12345678u, 0x12345678u, 0x87654321u, 0xabcdef01u,
0x12345678u, 0xfedcba98u},
"initial, re-dirtied, newly mapped or remapped bytes were stale on the GPU");
context.UnmapMemory(base, 3 * page);
scheduler.Finish();
DestroyBuffer(&readback);
Require(name, "release", LibKernel::Memory::FreeGuestMemory(base, 3 * page),
"guest allocation release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckBdaPageTableUploads() {
constexpr const char *name = "BdaPageTableUploads";
constexpr uintptr_t base = 0x0000000207b00000ull;
@@ -30436,7 +30517,7 @@ TestCase ScalarMemoryLoadVariants() {
std::vector<u32> expected = initial;
expected.insert(expected.end(), initial.begin(), initial.end());
return {"ScalarMemoryLoadVariants",
TestCase test{"ScalarMemoryLoadVariants",
code,
initial,
expected,
@@ -30445,6 +30526,8 @@ TestCase ScalarMemoryLoadVariants() {
O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4,
O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.bda_mappings = {{0u, 0u}};
return test;
}
TestCase ScalarBufferOffsetAlignmentAndCarry() {
@@ -30467,6 +30550,7 @@ TestCase ScalarBufferOffsetAlignmentAndCarry() {
{O::S_MOV_B32, O::S_ADD_U32, O::S_BUFFER_LOAD_DWORD,
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
test.forbidden_spirv = {"flattened_srt"};
test.bda_mappings = {{0u, 0u}};
test.compute_info.threads_num[0] = 1;
test.compute_info.threads_num[1] = 1;
test.compute_info.threads_num[2] = 1;
@@ -31100,7 +31184,7 @@ TestCase BufferLoadsGpuSelectedDescriptors(u32 width, u32 wave = 32u) {
}
}
AppendEnd(&test.code);
test.bda_mappings = {{GuestBase, 0}};
test.bda_mappings = {{0u, 0u}, {GuestBase, 0}};
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
O::V_READFIRSTLANE_B32, O::S_MUL_I32, O::S_BUFFER_LOAD_DWORDX4,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
@@ -31284,7 +31368,7 @@ TestCase BufferLoadGpuSelectedDescriptors(u32 wave, ShaderOpcode load) {
code.push_back(EncodeSopc(0x0a, 28, 29));
code.push_back(EncodeSopp(0x05, loop - static_cast<u32>(code.size()) - 1u));
AppendEnd(&code);
test.bda_mappings = {{GuestBase, 0}};
test.bda_mappings = {{0u, 0u}, {GuestBase, 0}};
test.fault_free_address = GuestBase + 0x100000u;
test.compute_info.threads_num[0] = 1;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
@@ -31336,11 +31420,17 @@ void CheckSharedIndirectBufferLoads(VulkanHarness &vulkan) {
auto compiled = CompileCase(test, vulkan.SubgroupSize());
struct Profile { u32 components = 0, uses = 0, definitions = 0, calls = 0, reads = 0; };
std::map<std::string, Profile> profiles;
const u32 halves = wave == 64u && vulkan.SubgroupSize() == 32u ? 2u : 1u;
u32 scalar_reads = 0;
for (const auto *block : compiled.program.blocks)
for (const auto &inst : *block) {
const auto components = BufferComponentCount(inst.GetOpcode());
if (components == 0u || inst.GetOpcode() == ValueOpcode::ReadConstBuffer) continue;
if (components == 0u) continue;
const auto &memory = compiled.program.memory_info[inst.Flags<MemoryFlags>().index];
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
scalar_reads += !memory.planning_only;
continue;
}
if (memory.kind != ResourceKind::IndirectBuffer || memory.formatted) continue;
const auto name = "load_indirect_buffer_" + std::to_string(memory.data_bits) + "x" +
std::to_string(components) + (memory.coherent ? "_coherent" : "");
@@ -31371,11 +31461,11 @@ void CheckSharedIndirectBufferLoads(VulkanHarness &vulkan) {
}
offset += words.size();
}
Require(test.name, "raw load function scope", functions.size() == profiles.size() && inline_reads == 0u,
Require(test.name, "raw load function scope", functions.size() == profiles.size() && inline_reads == scalar_reads * halves,
"a raw load profile was unused, missing or still emitted inline");
for (const auto &[name, profile] : profiles)
Require(test.name, name.c_str(), profile.uses != 0u && profile.definitions == 1u &&
profile.calls == profile.uses * (wave == 64u && vulkan.SubgroupSize() == 32u ? 2u : 1u) &&
profile.calls == profile.uses * halves &&
profile.reads == profile.components,
"raw load helpers duplicated definitions, calls or component reads");
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true);
@@ -33679,6 +33769,48 @@ TestCase ScalarBufferFromLoopReadlane(u32 wave_size) {
return test;
}
TestCase ScalarBufferLoadsDisjointPages() {
using O = ShaderOpcode;
constexpr u32 GuestBase = 0x10000, PageSize = 0x10000, PageStride = 0x400000;
TestCase test;
test.name = "ScalarBufferLoadsDisjointPages";
auto &code = test.code;
code.push_back(EncodeSop2(0x1e, 12, 4, InlineU32(22)));
code.push_back(EncodeSmem0(0x0a, 20, 4));
code.push_back(EncodeSmem1(256, 12));
code.push_back(EncodeVop1(0x01, 0, 4));
code.push_back(EncodeVop2(0x1a, 0, InlineU32(2), 0));
for (u32 component = 0; component < 4; ++component)
AppendStoreSgprAtLaneDwordOffset(&code, 20 + component, 0, component);
AppendEnd(&code);
test.initial.resize(2 * PageSize / sizeof(u32));
for (u32 page = 0; page < 2; ++page) {
for (u32 component = 0; component < 4; ++component) {
const u32 value = 0x12340000u + page * 16 + component;
test.initial[(page * PageSize + 256) / sizeof(u32) + component] = value;
test.expected.push_back(value);
}
}
test.user_data = MakeNativeUserData(nullptr);
test.user_data[8] = GuestBase;
test.user_data[10] = 32 * 1024 * 1024;
test.has_user_data = true;
// A large guest descriptor covers separate allocations, while its SSBO view ends at the first.
test.storage_buffer_range_bytes = PageSize;
test.bda_mappings = {{GuestBase, 0}, {GuestBase + PageStride, PageSize}};
test.opcodes = {O::S_LSHL_B32, O::S_BUFFER_LOAD_DWORDX4, O::V_MOV_B32,
O::V_LSHLREV_B32, O::V_ADD_NC_U32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.compute_info.wave_size = 32;
test.compute_info.threads_num[0] = 1;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
test.compute_info.group_id[0] = true;
test.compute_info.workgroup_register = 4;
test.has_compute_info = true;
test.dispatch_x = 2;
return test;
}
TestCase FlatVirtualAddressRebasesGuestAllocation() {
using O = ShaderOpcode;
@@ -37950,7 +38082,7 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
}
// Compile against poison selectors. A separate GPU dispatch writes the actual keys.
auto compiled = CompileCase(test, vulkan.SubgroupSize());
if (table_bytes == 0u || components != 0u)
if (components != 0u)
test.forbidden_spirv = {"convert_indirect_buffer_format", "load_indirect_buffer_",
"indirect_buffer_format_geometry"};
else
@@ -37966,8 +38098,8 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) {
if (opcode == spv::OpFunctionCall && words[3] == bda_function) ++bda_calls;
offset += words.size();
}
Require(test.name, "empty descriptor table specialization", (bda_calls == 0u) == (table_bytes == 0u || components != 0u),
"empty table retained BDA reads or nonempty table lost GPU descriptor loads");
Require(test.name, "GPU descriptor reads", (bda_calls == 0u) == (components != 0u),
"CPU-planned descriptors retained BDA reads or runtime descriptors lost them");
for (const auto &descriptor : compiled.resources.buffers) {
ShaderBufferResource resource{};
std::copy_n(descriptor.dwords.begin(), 4u, resource.fields);
@@ -41299,6 +41431,83 @@ GraphicsCase GraphicsAncillarySampleId() {
return test;
}
// SPI_BARYC_CNTL[24] selects all-bits mode for uint/bool S_FRONT_FACE;
// float S_FRONT_FACE and __getSpecialVgprFloat(__v_front_face) leave it clear.
GraphicsCase GraphicsFrontFaceBits(bool reversed, bool all_bits) {
GraphicsCase test;
test.name = all_bits ? (reversed ? "GraphicsFrontFaceUintFront" : "GraphicsFrontFaceUintBack")
: (reversed ? "GraphicsFrontFaceFloatFront" : "GraphicsFrontFaceFloatBack");
test.pixel_front_face = true;
test.pixel_front_face_all_bits = all_bits;
// Preserve every raw bit as two finite, exactly representable integer-to-float
// results. Directly exporting the integer front value would export a NaN.
AppendVop3(&test.fragment_code, 0x148, 4, Vgpr(2), InlineU32(0), InlineU32(16));
AppendVop3(&test.fragment_code, 0x148, 5, Vgpr(2), InlineU32(16), InlineU32(16));
test.fragment_code.push_back(EncodeVop1(0x06, 0, Vgpr(4)));
test.fragment_code.push_back(EncodeVop1(0x06, 1, Vgpr(5)));
// Integer inputs test raw zero; float inputs test the sign.
test.fragment_code.push_back(EncodeVopc(all_bits ? 0xc2 : 0x04, InlineU32(0), 2));
AppendVMovLiteral(&test.fragment_code, 3, 0x3f800000u);
test.fragment_code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 3));
test.fragment_code.push_back(EncodeExp0(0x00, 0xf));
test.fragment_code.push_back(EncodeExp1(0, 1, 2, 3));
AppendEnd(&test.fragment_code);
test.opcodes = {ShaderOpcode::V_BFE_U32, ShaderOpcode::V_CVT_F32_U32,
all_bits ? ShaderOpcode::V_CMP_EQ_U32 : ShaderOpcode::V_CMP_GT_F32,
ShaderOpcode::V_CNDMASK_B32, ShaderOpcode::V_MOV_B32,
ShaderOpcode::EXP, ShaderOpcode::S_ENDPGM};
test.vertices = {
0xbf800000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u,
0x40400000u, 0xbf800000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u,
0xbf800000u, 0x40400000u, 0x3e800000u, 0x3f000000u, 0x3f400000u, 0x3f800000u};
if (reversed) {
std::swap_ranges(test.vertices.begin() + 6, test.vertices.begin() + 12,
test.vertices.begin() + 12);
}
// With the harness's positive-height viewport, the default triangle faces back.
const u32 bits = all_bits ? (reversed ? UINT32_MAX : 0u)
: (reversed ? 0x3f800000u : 0xbf800000u);
test.expected_pixel = {
std::bit_cast<u32>(static_cast<float>(bits & 0xffffu)),
std::bit_cast<u32>(static_cast<float>(bits >> 16u)),
reversed ? 0u : 0x3f800000u, 0x3f800000u};
return test;
}
void CheckPixelFrontFaceModes() {
constexpr const char *name = "PixelFrontFaceModes";
static const std::array<u32, 1> code = {EncodeSopp(0x01)};
HW::PixelShaderInfo regs{};
regs.ps_regs.data_addr = reinterpret_cast<uint64_t>(code.data());
ShaderMapUserData(regs.ps_regs.data_addr,
{.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<u32>(sizeof(code))});
HW::ShaderRegisters sh{};
sh.ps_input_ena = sh.ps_input_addr = 0x1002u;
const std::array<Prospero::ColorComponentMapping, 8> export_mapping{};
std::vector<u32> float_key;
for (bool all_bits : {false, true}) {
for (u32 other_bits : {0u, 0x120u}) {
sh.baryc_cntl = (all_bits ? 0x01000000u : 0u) | other_bits;
ShaderPixelInputInfo pixel{};
(void)PrepareProgram(regs, sh, export_mapping, pixel);
Require(name, "register format decoding",
pixel.ps_front_face && pixel.ps_front_face_all_bits == all_bits &&
pixel.ps_system_input_base == 2u,
"SPI_BARYC_CNTL[24] did not select the front-face format");
std::vector<u32> key;
BuildStageStaticKey(pixel, key);
if (!all_bits && other_bits == 0u) {
float_key = key;
}
Require(name, "format specialization",
(key != float_key) == all_bits,
"front-face formats must have distinct shader keys and ignore unrelated bits");
}
}
std::printf("[host] %-32s ok\n", name);
}
GraphicsCase GraphicsFlatInterpolatorExport() {
using O = ShaderOpcode;
@@ -41865,6 +42074,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ScalarLoadAlignsDynamicBase);
cases.push_back(ScalarBufferFromLoopReadlane(32));
cases.push_back(ScalarBufferFromLoopReadlane(64));
AddCase(ScalarBufferLoadsDisjointPages);
AddCase(BufferLoadStore);
AddCase(BufferLoadDwordOffenIdxenUsesVaddrPlusOneOffset);
AddCase(BufferStoreDwordOffenIdxenUsesVaddrPlusOneOffset);
@@ -42179,6 +42389,10 @@ std::vector<GraphicsCase> MakeGraphicsCases() {
GraphicsAncillaryLayer(false),
GraphicsAncillaryLayer(true),
GraphicsAncillarySampleId(),
GraphicsFrontFaceBits(false, false),
GraphicsFrontFaceBits(true, false),
GraphicsFrontFaceBits(false, true),
GraphicsFrontFaceBits(true, true),
GraphicsFlatInterpolatorExport(),
GraphicsDsAddtidScratchExport(),
GraphicsDirectSgprPushConstantExport(),
@@ -47858,6 +48072,9 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--indirect-buffer-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ScalarBufferLoadsDisjointPages());
RunCase(&vulkan, ScalarMemoryLoadVariants());
RunCase(&vulkan, ScalarBufferOffsetAlignmentAndCarry());
RunCase(&vulkan, ScalarBufferFromLoopReadlane(32));
RunCase(&vulkan, ScalarBufferFromLoopReadlane(64));
CheckBoundedFormattedBufferCopy(vulkan);
@@ -47959,6 +48176,17 @@ int main(int argc, char **argv) {
RunCase(&vulkan, SiblingSharedExitKeepsCapturedConditions());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--front-face-only") == 0) {
CheckPixelFrontFaceModes();
VulkanHarness vulkan;
for (bool all_bits : {false, true}) {
for (bool reversed : {false, true}) {
RunGraphicsCase(&vulkan, GraphicsFrontFaceBits(reversed, all_bits));
}
}
RunGraphicsCase(&vulkan, GraphicsAncillaryLayer(true));
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--position-w-only") == 0) {
VulkanHarness vulkan;
RunGraphicsCase(&vulkan, GraphicsPositionWExport());
@@ -48378,6 +48606,7 @@ int main(int argc, char **argv) {
if (argc == 2 && std::strcmp(argv[1], "--bda-page-table-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckBdaPageTableUploads();
vulkan.CheckBdaSyncUploads();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--buffer-cache-range-only") == 0) {
@@ -48574,6 +48803,7 @@ int main(int argc, char **argv) {
CheckEmbeddedFetchLaneSpill();
CheckTessellationPrograms();
CheckPixelParameterAliases();
CheckPixelFrontFaceModes();
CheckRectListShaders();
CheckSigned16ImageStore(vulkan);
CheckBoundedFormattedBufferCopy(vulkan);
@@ -48607,6 +48837,7 @@ int main(int argc, char **argv) {
CheckComputeLdsLimit(vulkan);
vulkan.CheckNullTextureDescriptors();
vulkan.CheckBdaPageTableUploads();
vulkan.CheckBdaSyncUploads();
vulkan.CheckLargeSparseImageUpload();
vulkan.CheckUnifiedTextureCacheFlow();
vulkan.CheckUnifiedImageViewCache();
+62
View File
@@ -1,4 +1,5 @@
#include "common/file.h"
#include "common/localization.h"
#include "common/trophies.h"
#include <chrono>
@@ -8,6 +9,7 @@
#include <filesystem>
#include <map>
#include <string>
#include <string_view>
#include <vector>
namespace {
@@ -20,6 +22,65 @@ void Check(bool value, const char *text) {
}
}
void TestLocalization() {
struct Translation {
std::string_view locale;
std::string_view earned;
};
static constexpr Translation translations[] = {
{"ja-JP", "トロフィーを獲得しました!"},
{"en-US", "Trophy earned!"},
{"fr-FR", "Trophée obtenu !"},
{"es-ES", "¡Ganaste un trofeo!"},
{"de-DE", "Trophäe erhalten!"},
{"it-IT", "Trofeo ottenuto!"},
{"nl-NL", "Trofee behaald!"},
{"pt-PT", "Troféu conquistado!"},
{"ru-RU", "Трофей получен!"},
{"ko-KR", "트로피를 획득했습니다!"},
{"zh-Hant", "已獲得獎盃!"},
{"zh-Hans", "已获得奖杯!"},
{"fi-FI", "Pokaali ansaittu!"},
{"sv-SE", "Trofé intjänad!"},
{"da-DK", "Trofæ optjent!"},
{"no-NO", "Trofé opptjent!"},
{"pl-PL", "Zdobyto trofeum!"},
{"pt-BR", "Troféu conquistado!"},
{"en-GB", "Trophy earned!"},
{"tr-TR", "Kupa kazanıldı!"},
{"es-419", "¡Ganaste un trofeo!"},
{"ar-AE", "تم كسب الكأس!"},
{"fr-CA", "Trophée obtenu !"},
{"cs-CZ", "Trofej získána!"},
{"hu-HU", "Trófea megszerezve!"},
{"el-GR", "Κερδίσατε ένα τρόπαιο!"},
{"ro-RO", "Trofeu câștigat!"},
{"th-TH", "ได้รับถ้วยรางวัลแล้ว!"},
{"vi-VN", "Đã nhận cúp!"},
{"id-ID", "Trofi diperoleh!"},
};
constexpr std::string_view key = "trophy.toast.earned";
const auto japanese = Common::Localization::Get(0, key);
int language = 0;
for (const auto &translation : translations) {
Check(Common::Localization::LocaleName(language) == translation.locale,
"console language selects its locale");
Check(Common::Localization::Get(language, key) == translation.earned,
"embedded notification preserves translated UTF-8 bytes");
++language;
}
Check(japanese == translations[0].earned,
"translation view survives other locale lookups");
for (const int invalid : {-1, 30, 1000}) {
Check(Common::Localization::LocaleName(invalid) == "en-US" &&
Common::Localization::Get(invalid, key) == "Trophy earned!",
"invalid console language falls back to English");
}
Check(Common::Localization::Get(0, "missing.translation") ==
"missing.translation",
"unknown translation returns its key");
}
class TempDirectory {
public:
TempDirectory() {
@@ -252,6 +313,7 @@ void TestUnlockPersistence(const std::filesystem::path &directory) {
} // namespace
int main() {
TestLocalization();
TempDirectory directory;
TestEventExtraction(directory.Path());
TestProgress(directory.Path());
+155 -62
View File
@@ -5417,8 +5417,7 @@ void TestNewShaderRecompilerScalarMemoryBindingDomains() {
Check(index < program.memory_info.size(),
"typed scalar memory operation has invalid metadata");
const auto &memory = program.memory_info[index];
Check(memory.kind == kind && memory.resource == 0u &&
!memory.planning_only,
Check(memory.kind == kind && !memory.planning_only,
"typed scalar memory operation has the wrong live domain");
count++;
}
@@ -5478,17 +5477,15 @@ void TestNewShaderRecompilerScalarMemoryBindingDomains() {
auto buffer = RecompileForTest(buffer_shader, buffer_options);
const auto& buffer_counts = buffer.program.bindings.descriptor_counts;
Check(buffer.program.info.buffers.size() == 1u &&
buffer.program.info.buffers[0].scalar &&
buffer.program.info.buffers[0].descriptor_index == 0 &&
!buffer.program.info.uses_dma &&
buffer_counts[static_cast<size_t>(BindingKind::Buffers)] == 1 &&
buffer_counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 0 &&
Check(buffer.program.info.buffers.empty() && buffer.program.info.uses_dma &&
buffer_counts[static_cast<size_t>(BindingKind::Buffers)] == 0 &&
buffer_counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 1 &&
buffer_counts[static_cast<size_t>(BindingKind::FaultBuffer)] == 1 &&
buffer_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] == 0,
"descriptor scalar load did not use only the buffer domain");
"descriptor scalar load did not use only the DMA domain");
Check(count_live_memory_ops(
buffer.program, ShaderRecompiler::IR::ValueOpcode::ReadConstBuffer,
ShaderRecompiler::IR::ResourceKind::ScalarBuffer) == 1u,
ShaderRecompiler::IR::ResourceKind::IndirectBuffer) == 1u,
"descriptor scalar load did not remain a live typed buffer operation");
CheckSpirvBinaryValidates(buffer.spirv);
}
@@ -9027,6 +9024,55 @@ void TestNewShaderRecompilerCfgExecSccSharedArm() {
CheckSpirvBinaryValidates(result.spirv);
}
std::vector<std::pair<uint32_t, std::array<uint32_t, 4>>> ScalarDescriptorsOnUniformPath(
TestCompileResult& result, std::span<const uint32_t> user_data) {
using namespace ShaderRecompiler::IR;
using ShaderRecompiler::CFG::TerminatorKind;
// These acyclic fixtures have uniform branches and descriptor loads already flattened.
for (auto* block : result.program.blocks) {
for (auto& inst : *block) {
if (inst.GetOpcode() != ValueOpcode::ReadConst) continue;
const auto slot = inst.Arg(1).Resolve().U32();
Check(slot < result.resources.flattened_srt.size(), "scalar descriptor has an invalid SRT slot");
inst.ReplaceUsesWith(Value(result.resources.flattened_srt[slot]));
}
}
SrtWalker walker(result.program, {.user_data = user_data});
std::vector<std::pair<uint32_t, std::array<uint32_t, 4>>> descriptors;
Block* previous = nullptr;
auto* block = result.program.blocks.front();
for (size_t step = 0; step < result.program.blocks.size(); ++step) {
for (auto& inst : *block) {
if (inst.GetOpcode() == ValueOpcode::Phi) {
size_t incoming = 0;
while (incoming < inst.NumArgs() && inst.PhiBlock(incoming) != previous) ++incoming;
Check(incoming < inst.NumArgs(), "descriptor Phi lost the executed predecessor");
inst.ReplaceUsesWith(inst.Arg(incoming));
} else if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
const auto* handle = inst.Arg(0).ResolveInstruction();
Check(handle && handle->GetOpcode() == ValueOpcode::GetBufferResource && handle->NumArgs() == 4u,
"scalar read lost its raw buffer descriptor");
auto& [pc, words] = descriptors.emplace_back();
pc = inst.Flags<MemoryFlags>().pc;
for (uint32_t word = 0; word < words.size(); ++word)
Check(walker.Evaluate(handle->Arg(word), words[word]), "scalar descriptor word is not evaluable");
}
}
const auto& term = block->terminator;
if (term.kind == TerminatorKind::Return) return descriptors;
Check(term.kind == TerminatorKind::Branch || term.kind == TerminatorKind::ConditionalBranch,
"descriptor fixture has unsupported control flow");
uint32_t condition = 1u;
if (term.kind == TerminatorKind::ConditionalBranch)
Check(walker.Evaluate(block->condition, condition), "descriptor branch is not uniform");
previous = block;
block = condition != 0u ? term.true_block : term.false_block;
Check(block != nullptr, "descriptor fixture lost its branch target");
}
Check(false, "descriptor fixture unexpectedly contains a loop");
return descriptors;
}
void TestSharedReturnPreservesDescriptorDominance() {
const uint32_t shader[] = {
EncodeSmem0(0x02, 32, 14), (125u << 25u) | 0x60u,
@@ -9072,9 +9118,12 @@ void TestSharedReturnPreservesDescriptorDominance() {
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback &&
result.resources.buffers.size() == 1u &&
result.resources.buffers[0].dwords[0] == 0x2000u,
Check(!result.program.dispatcher_fallback && result.resources.buffers.empty() &&
result.program.info.uses_dma,
"surviving scalar operation did not retain structured BDA access");
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
Check(descriptors.size() == 1u && descriptors[0].first == 0x20u &&
std::equal(descriptors[0].second.begin(), descriptors[0].second.end(), table.begin() + 0x50u / 4u),
"return-only descriptor reached the surviving buffer operation");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -9131,24 +9180,18 @@ void TestNestedSelectionPreservesDescriptorSources() {
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 4u,
"nested descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
const auto pc = result.program.info.buffers[i].first_use_pc;
Check(pc == 8u * 4u || pc == 14u * 4u || pc == direct_arm * 4u || pc == join * 4u,
"nested descriptor selection lost a native source location");
const auto selected_pc = (mode == 0u ? 8u : mode == 1u ? 14u : direct_arm) * 4u;
const auto *expected = pc == 8u * 4u ? table.data()
: pc == 14u * 4u ? table.data() + 4
: pc == direct_arm * 4u ? user_data.data() + 8 : user_data.data() + 4;
const auto &descriptor = result.resources.buffers[i];
const bool unused = pc != selected_pc && pc != join * 4u;
Check(descriptor.dword_count == 4 &&
(std::equal(expected, expected + 4, descriptor.dwords.begin()) ||
(unused && std::ranges::all_of(descriptor.dwords, [](auto word) { return word == 0; }))),
"nested descriptor selection chose another arm's descriptor");
}
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
result.program.info.buffers[0].first_use_pc == join * 4u &&
std::equal(user_data.begin() + 4, user_data.begin() + 8,
result.resources.buffers[0].dwords.begin()),
"nested descriptor selection lost its structured output binding");
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
const auto selected_pc = (mode == 0u ? 8u : mode == 1u ? 14u : direct_arm) * 4u;
const auto* expected = mode == 0u ? table.data() : mode == 1u ? table.data() + 4 : user_data.data() + 8;
Check(descriptors.size() == 1u && descriptors[0].first == selected_pc &&
std::equal(expected, expected + 4, descriptors[0].second.begin()),
"nested descriptor selection chose another arm's descriptor");
CheckSpirvBinaryValidates(result.spirv);
}
}
@@ -9207,16 +9250,16 @@ void TestSharedExitPreservesNativeDescriptorSources() {
user_data[2] = arm;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 2u,
"shared-exit descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
const auto pc = result.program.info.buffers[i].first_use_pc;
const uint32_t expected = pc == 14u * 4u ? table[8]
: pc == 22u * 4u ? table[12] : 0u;
Check(expected != 0u && result.resources.buffers[i].dwords[0] == expected,
"shared-exit descriptor selection retained an overwritten native source");
}
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
Check(!result.program.dispatcher_fallback && result.resources.buffers.empty() &&
result.program.info.uses_dma,
"shared-exit descriptor selection lost its structured scalar reads");
const auto descriptors = ScalarDescriptorsOnUniformPath(result, user_data);
const auto selected_pc = (arm == 0u ? 22u : 14u) * 4u;
const auto* expected = table.data() + (arm == 0u ? 12u : 8u);
Check(descriptors.size() == 1u && descriptors[0].first == selected_pc &&
std::equal(expected, expected + 4, descriptors[0].second.begin()),
"shared-exit descriptor selection retained an overwritten native source");
CheckSpirvBinaryValidates(result.spirv);
}
}
@@ -12874,6 +12917,51 @@ void TestLateConstantPropagation() {
"an ungrounded Phi cycle acquired a constant");
}
void TestMaskedShiftConstantPropagation() {
using namespace ShaderRecompiler::IR;
for (const uint32_t count : {0u, 1u, 8u, 31u, 32u, 33u, 40u, 63u, 64u, UINT32_MAX}) {
for (const uint32_t mask : {0u, 1u, 0xffu, 0x7fffffffu, 0x80000000u, UINT32_MAX}) {
for (const bool swapped : {false, true}) {
for (const bool shared : {false, true}) {
Block block;
IREmitter ir(&block);
const auto input = ir.GetUserData(static_cast<ScalarReg>(0));
const auto shifted = ir.Emit(ValueOpcode::ShiftLeftLogical32, {input, Value(count)});
const auto result = ir.Emit(ValueOpcode::BitwiseAnd32,
swapped ? std::initializer_list<Value>{Value(mask), shifted}
: std::initializer_list<Value>{shifted, Value(mask)});
const auto other = shared ? ir.Emit(ValueOpcode::IAdd32, {shifted, Value(1u)}) : Value{};
ConstantPropagationPass({&block});
const bool zero = ((UINT32_MAX << (count & 31u)) & mask) == 0u;
Check(zero ? result.Resolve() == Value(0u) : !result.Resolve().IsImmediate(),
"masked shift retained impossible bits or discarded possible bits");
if (shared) {
Check(other.Resolve().Instruction()->Arg(0).Resolve() == shifted.Resolve(),
"folding a masked shift changed its other consumer");
}
constexpr uint32_t source = 0xa5f03c69u;
input.Instruction()->ReplaceUsesWith(Value(source));
ConstantPropagationPass({&block});
const uint32_t expected = source << (count & 31u);
Check(result.Resolve() == Value(expected & mask) &&
(!shared || other.Resolve() == Value(expected + 1u)),
"masked shift folding changed a runtime result");
}
}
}
}
Block block;
IREmitter ir(&block);
const auto input = ir.GetUserData(static_cast<ScalarReg>(0));
const auto count = ir.GetUserData(static_cast<ScalarReg>(1));
const auto shifted = ir.Emit(ValueOpcode::ShiftLeftLogical32, {input, count});
const auto result = ir.Emit(ValueOpcode::BitwiseAnd32, {shifted, Value(0xffu)});
ConstantPropagationPass({&block});
Check(result.Resolve().Instruction()->GetOpcode() == ValueOpcode::BitwiseAnd32,
"a variable shift count was treated as a constant");
}
void TestU64ShiftConstantPropagation() {
using namespace ShaderRecompiler::IR;
@@ -14076,10 +14164,12 @@ void TestNewShaderRecompilerNativeBindingPlan() {
storage = kind;
}
}
Check(counts[static_cast<size_t>(BindingKind::Buffers)] == 2 &&
Check(counts[static_cast<size_t>(BindingKind::Buffers)] == 1 &&
result.program.info.buffers.size() == 1u &&
result.program.info.buffers[0].descriptor_index == 0 &&
result.program.info.buffers[1].descriptor_index == 1,
"native emission did not allocate scalar/vector buffer slots");
counts[static_cast<size_t>(BindingKind::BdaPagetable)] == 1 &&
counts[static_cast<size_t>(BindingKind::FaultBuffer)] == 1,
"native emission did not allocate the vector buffer and scalar BDA slots");
Check(sampled && counts[static_cast<size_t>(*sampled)] == 1,
"native emission did not allocate the sampled image");
Check(storage && counts[static_cast<size_t>(*storage)] == 1,
@@ -14090,7 +14180,8 @@ void TestNewShaderRecompilerNativeBindingPlan() {
"native emission did not allocate the sampler");
Check(SpirvContainsOpcode(result.spirv, 86),
"SPIR-V binary does not combine separate image/sampler descriptors");
for (const auto kind : {BindingKind::Buffers, *sampled, BindingKind::Samplers}) {
for (const auto kind : {BindingKind::Buffers, BindingKind::BdaPagetable,
BindingKind::FaultBuffer, *sampled, BindingKind::Samplers}) {
Check(SpirvHasDecorationValue(result.spirv, 33u,
ShaderRecompiler::IR::NativeBinding(result.program.stage, kind)),
"SPIR-V lacks a resource Binding decoration");
@@ -14113,10 +14204,10 @@ void TestNewShaderRecompilerNativeBindingPlan() {
for (auto *block : result.program.blocks) {
const auto found = std::ranges::find_if(*block, [](const auto &inst) {
return inst.GetOpcode() ==
ShaderRecompiler::IR::ValueOpcode::GetBufferResource;
ShaderRecompiler::IR::ValueOpcode::StoreBufferU32;
});
if (found != block->end()) {
buffer_handle = &*found;
buffer_handle = found->Arg(0).ResolveInstruction();
break;
}
}
@@ -14290,13 +14381,11 @@ void TestComputeImageFill() {
0xbf8cc07fu, 0x7e06026au, 0xd7460001u, 0x0405060du,
0x7e04020eu, 0xf0200128u, 0x00000300u, 0xbf810000u,
};
uint32_t value = 0x7bu;
const auto address = reinterpret_cast<uint64_t>(&value);
constexpr uint32_t value = 0x7bu;
const std::array<uint32_t, 12> userdata = {
0x20021000u, 0xc0500000u, 0x01d8c347u, 0xd1800204u,
0u, 0x00700000u, 0u, 0u,
static_cast<uint32_t>(address),
static_cast<uint32_t>(address >> 32) | (4u << 16), 1u, 0x14204u,
0x1000u, 4u << 16u, 1u, 0x14204u,
};
ShaderComputeInputInfo compute{};
compute.threads_num[0] = compute.threads_num[1] = 8;
@@ -14307,9 +14396,14 @@ void TestComputeImageFill() {
auto options = MakeCompileOptions(ShaderType::Compute);
options.input_info.compute = &compute;
options.user_data = userdata;
enum class Mutation { None, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
const auto Run = [&](Mutation mutation, bool clean = true) {
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
enum class Mutation { None, RuntimeScalar, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
const auto Run = [&](Mutation mutation) {
auto code = std::to_array(shader);
if (mutation != Mutation::RuntimeScalar) {
code[2] = EncodeSMovB32(106, 255);
code[3] = value;
}
auto translated = ShaderRecompiler::TranslateProgram(code, options);
auto &program = translated.program;
for (auto *block : program.blocks) {
const auto found = std::ranges::find(*block, ValueOpcode::ImageWrite, &Inst::GetOpcode);
@@ -14320,7 +14414,8 @@ void TestComputeImageFill() {
Check(coordinates && values, "stencil fill fixture lost its image operands");
const auto x = coordinates->Arg(0);
switch (mutation) {
case Mutation::None: break;
case Mutation::None:
case Mutation::RuntimeScalar: break;
case Mutation::Offset:
coordinates->SetArg(0, Value(&*block->PrependNewInst(found, ValueOpcode::IAdd32,
{x, Value(1u)})));
@@ -14345,15 +14440,12 @@ void TestComputeImageFill() {
auto plan = ExtractResourcePlan(program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan,
{.user_data = userdata, .read_memory = ReadHostTestMemory,
.read_specialization_memory = clean ? ReadHostTestMemory : nullptr},
snapshot, specialization),
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
"captured stencil clear did not materialize");
const auto &fill = snapshot.uniform_fill;
const bool expected = mutation == Mutation::None && clean;
const bool expected = mutation == Mutation::None;
Check((fill.kind == UniformFillKind::Image) == expected,
"image fill proof accepted an unsafe store or missed the captured clear");
"image fill proof accepted a runtime read or unsafe store, or missed a constant clear");
if (expected) {
Check(fill.resource == 0 && fill.words == 1 && fill.value == value &&
fill.group_stride == std::array<uint32_t, 3>{8, 8, 1},
@@ -14361,7 +14453,7 @@ void TestComputeImageFill() {
}
};
Run(Mutation::None);
Run(Mutation::None, false);
Run(Mutation::RuntimeScalar);
for (const auto mutation : {Mutation::Offset, Mutation::WrongAxis, Mutation::VaryingValue,
Mutation::Predicate, Mutation::ExtraStore, Mutation::A16})
Run(mutation);
@@ -16364,6 +16456,7 @@ int main() {
TestValuePhiValidation();
TestLaneMaskProjection();
TestLateConstantPropagation();
TestMaskedShiftConstantPropagation();
TestU64ShiftConstantPropagation();
TestU64RegisterPairRoundTrip();
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS