mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 19:36:55 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7974dc763 | |||
| b899717900 | |||
| d58aefde49 | |||
| 4c6be6cd1d | |||
| 584c2e8ca6 | |||
| 717b50155a | |||
| 57b161019d | |||
| 3ff6606fcb | |||
| 6a0a666a41 |
+11
-11
@@ -113,7 +113,7 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
shader_recompiler_compute_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -388,7 +388,7 @@ jobs:
|
||||
run: |
|
||||
cmake --build _Build/linux \
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests host_input_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
@@ -397,7 +397,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|host_input|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -407,12 +407,12 @@ jobs:
|
||||
- name: Verify artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
file _Build/linux/install/launcher
|
||||
file _Build/linux/install/kyty_launcher
|
||||
file _Build/linux/install/kyty_emulator
|
||||
test -x _Build/linux/install/kyty_updater
|
||||
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
|
||||
ldd _Build/linux/install/kyty_emulator > /dev/null
|
||||
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
readelf -d _Build/linux/install/kyty_launcher | grep -q 'RPATH.*\$ORIGIN/lib'
|
||||
while IFS= read -r -d '' binary; do
|
||||
while read -r dependency; do
|
||||
test -e "_Build/linux/install/lib/$dependency"
|
||||
@@ -421,8 +421,8 @@ jobs:
|
||||
sed -n 's/.*Shared library: \[\(libQt6[^]]*\|libicu[^]]*\)\].*/\1/p'
|
||||
)
|
||||
done < <(
|
||||
find _Build/linux/install/launcher _Build/linux/install/plugins \
|
||||
-type f \( -name launcher -o -name '*.so' \) -print0
|
||||
find _Build/linux/install/kyty_launcher _Build/linux/install/plugins \
|
||||
-type f \( -name kyty_launcher -o -name '*.so' \) -print0
|
||||
)
|
||||
_Build/linux/install/kyty_emulator --help > /dev/null
|
||||
|
||||
@@ -497,7 +497,7 @@ jobs:
|
||||
linux_dir="artifacts/KytyPS5-Linux-x86_64"
|
||||
test -d "$linux_dir"
|
||||
chmod a+x \
|
||||
"$linux_dir/launcher" \
|
||||
"$linux_dir/kyty_launcher" \
|
||||
"$linux_dir/kyty_emulator" \
|
||||
"$linux_dir/kyty_updater"
|
||||
tar -C "$linux_dir" -czf \
|
||||
|
||||
Vendored
+2
-2
@@ -5,7 +5,7 @@
|
||||
"name": "Debug launcher UI",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\launcher.exe",
|
||||
"program": "${workspaceFolder}\\_Build\\vscode-clang\\launcher\\kyty_launcher.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\_Build\\vscode-clang\\launcher",
|
||||
@@ -13,7 +13,7 @@
|
||||
"externalConsole": false,
|
||||
"linux": {
|
||||
"type": "cppdbg",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/launcher",
|
||||
"program": "${workspaceFolder}/_Build/vscode-clang/launcher/kyty_launcher",
|
||||
"cwd": "${workspaceFolder}/_Build/vscode-clang/launcher",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/gdb"
|
||||
|
||||
@@ -526,6 +526,10 @@ add_executable(controller_settings_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(host_input_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/HostInputTests.cpp")
|
||||
target_link_libraries(host_input_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_include_directories(host_input_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
|
||||
)
|
||||
@@ -644,6 +648,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
||||
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
|
||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
|
||||
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
|
||||
@@ -723,6 +728,7 @@ if(BUILD_TESTING)
|
||||
ngs2_sampler_tests
|
||||
pad_haptics_tests
|
||||
controller_settings_tests
|
||||
host_input_tests
|
||||
save_data_memory_tests
|
||||
png_enc_tests
|
||||
ces_tests
|
||||
@@ -760,6 +766,14 @@ add_custom_target(kyty_trophy_sound
|
||||
add_dependencies(kyty_emulator kyty_trophy_sound)
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
|
||||
|
||||
add_custom_target(kyty_touch_cursors
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors"
|
||||
"$<TARGET_FILE_DIR:kyty_emulator>/assets/cursors"
|
||||
VERBATIM)
|
||||
add_dependencies(kyty_emulator kyty_touch_cursors)
|
||||
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
|
||||
|
||||
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||
if (WIN32)
|
||||
target_link_libraries(kyty_emulator opengl32)
|
||||
|
||||
@@ -275,11 +275,11 @@ From the repository root, follow the launcher configure, build and install steps
|
||||
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
|
||||
Fedora's packaged Qt 6 automatically.
|
||||
|
||||
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
|
||||
Run `./_Build/linux/install/kyty_launcher` on regular Fedora or inside the container. To launch a
|
||||
container build from the host, leave the container with `exit`, then run from the repository root:
|
||||
|
||||
```bash
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/launcher
|
||||
distrobox enter kyty-build -- ./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
|
||||
@@ -345,7 +345,7 @@ Use `_Build/linux` instead of `_Build/windows` for a Linux build.
|
||||
|
||||
A ready-made Visual Studio Code setup is included in [`.vscode`](.vscode). It configures CMake
|
||||
Tools to build the project with Ninja and `clang-cl` and provides launch profiles for both
|
||||
`launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
`kyty_launcher.exe` and `kyty_emulator.exe`. It is Windows-only: VS Code settings cannot select a
|
||||
compiler per platform, so on Linux configure from the command line as shown above.
|
||||
|
||||
Before using it:
|
||||
@@ -365,11 +365,11 @@ Update your graphics driver before reporting rendering problems.
|
||||
To use the graphical launcher:
|
||||
|
||||
```powershell
|
||||
.\_Build\windows\install\launcher.exe
|
||||
.\_Build\windows\install\kyty_launcher.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
./_Build/linux/install/launcher
|
||||
./_Build/linux/install/kyty_launcher
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Notification localization
|
||||
|
||||
Trophy notification subtitles use the configured console language. Trophy names still come
|
||||
from the game's metadata. The launcher trophy dialogs are unchanged.
|
||||
|
||||
Edit `assets/localization/<locale>.json`, using flat UTF-8 string values:
|
||||
|
||||
```json
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!"
|
||||
}
|
||||
```
|
||||
|
||||
CMake converts the catalogs into C++ byte arrays in the build directory. The emulator parses
|
||||
the embedded catalogs once and falls back to `en-US.json` for missing translations. No language
|
||||
files or translation service are needed at runtime. Rebuild after editing a catalog.
|
||||
|
||||
For local testing, rebuild `kyty_emulator`, choose a game's Console Language in the launcher,
|
||||
and unlock a trophy with notifications enabled. Installed system fonts provide non-Latin
|
||||
glyphs; the selected language needs an appropriate font on the host.
|
||||
@@ -12,9 +12,23 @@ endif()
|
||||
|
||||
add_library(common STATIC ${common_src} ${common_headers})
|
||||
|
||||
set(localization_dir "${PROJECT_SOURCE_DIR}/assets/localization")
|
||||
file(GLOB localization_catalogs CONFIGURE_DEPENDS "${localization_dir}/*.json")
|
||||
set(localization_data "${CMAKE_CURRENT_BINARY_DIR}/localizationData.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${localization_data}"
|
||||
COMMAND ${CMAKE_COMMAND} "-DINPUTS=${localization_catalogs}" "-DOUTPUT=${localization_data}"
|
||||
-P "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
DEPENDS ${localization_catalogs} "${KYTY_SOURCE_DIR}/embed_localization.cmake"
|
||||
COMMENT "Embed notification localization catalogs"
|
||||
VERBATIM
|
||||
)
|
||||
target_sources(common PRIVATE "${localization_data}")
|
||||
|
||||
target_include_directories(common
|
||||
PUBLIC ${KYTY_SOURCE_DIR}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "localizationData.h"
|
||||
|
||||
namespace Common::Localization {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 30> Locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
|
||||
size_t LanguageIndex(int console_language) {
|
||||
return console_language >= 0 && console_language < static_cast<int>(Locales.size())
|
||||
? static_cast<size_t>(console_language)
|
||||
: 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view LocaleName(int console_language) {
|
||||
return Locales[LanguageIndex(console_language)];
|
||||
}
|
||||
|
||||
std::string_view Get(int console_language, std::string_view key) {
|
||||
static const auto catalogs = [] {
|
||||
std::array<nlohmann::json, Locales.size()> result;
|
||||
for (size_t i = 0; i < Locales.size(); ++i) {
|
||||
const auto bytes = EmbeddedCatalog(Locales[i]);
|
||||
result[i] = nlohmann::json::parse(bytes.begin(), bytes.end(), nullptr, false);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
for (const auto index: std::array {LanguageIndex(console_language), size_t {1}}) {
|
||||
const auto& catalog = catalogs[index];
|
||||
const auto found = catalog.find(key);
|
||||
if (found != catalog.end() && found->is_string()) {
|
||||
const auto& text = found->get_ref<const std::string&>();
|
||||
if (!text.empty()) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace Common::Localization {
|
||||
|
||||
// Console language indices follow the system configuration; invalid indices use English.
|
||||
[[nodiscard]] std::string_view LocaleName(int console_language);
|
||||
|
||||
// Translations have static lifetime. Missing translations use English, then the supplied key.
|
||||
[[nodiscard]] std::string_view Get(int console_language, std::string_view key);
|
||||
|
||||
} // namespace Common::Localization
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fmt/format.h>
|
||||
#include <memory>
|
||||
@@ -45,8 +46,8 @@ std::shared_ptr<spdlog::logger> MakeFileLogger(std::string name
|
||||
std::filesystem::create_directories(parent);
|
||||
}
|
||||
|
||||
auto sink =
|
||||
std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), true);
|
||||
const bool truncate = std::getenv(Log::RelaunchedVariable) == nullptr;
|
||||
auto sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(path.native(), truncate);
|
||||
return MakeLogger(std::move(name), std::move(sink));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ struct Lifecycle {
|
||||
|
||||
enum class Direction { Silent, Console, File };
|
||||
|
||||
// Set by LoadExec for the program it starts, which continues the log file of the one it replaces
|
||||
// instead of starting it over.
|
||||
inline constexpr const char* RelaunchedVariable = "KYTY_RELAUNCHED";
|
||||
|
||||
Direction GetDirection();
|
||||
bool IsSilent();
|
||||
void Write(std::string_view text);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -105,13 +105,7 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
!conf["defaultLanguage"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
static constexpr std::array<std::string_view, 30> locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
const auto locale =
|
||||
locales[console_language >= 0 && console_language < locales.size() ? console_language : 1];
|
||||
const auto locale = Localization::LocaleName(console_language);
|
||||
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
|
||||
if (meta.is_null()) {
|
||||
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
if(NOT DEFINED INPUTS OR NOT DEFINED OUTPUT)
|
||||
message(FATAL_ERROR "embed_localization.cmake requires INPUTS and OUTPUT")
|
||||
endif()
|
||||
|
||||
set(contents "// Generated from assets/localization; do not edit.\n#pragma once\n#include <span>\n#include <string_view>\n\nnamespace {\n")
|
||||
set(lookup "std::span<const unsigned char> EmbeddedCatalog(std::string_view locale) {\n")
|
||||
set(continuation "[89ab][0-9a-f]")
|
||||
string(CONCAT utf8_character
|
||||
"[0-7][0-9a-f]|(c[2-9a-f]|d[0-9a-f])${continuation}"
|
||||
"|e0[ab][0-9a-f]${continuation}|e[1-9abcef]${continuation}${continuation}"
|
||||
"|ed[89][0-9a-f]${continuation}|f0[9ab][0-9a-f]${continuation}${continuation}"
|
||||
"|f[123]${continuation}${continuation}${continuation}|f48[0-9a-f]${continuation}${continuation}")
|
||||
foreach(catalog IN LISTS INPUTS)
|
||||
file(READ "${catalog}" bytes HEX)
|
||||
# Validate UTF-8, excluding overlong sequences, surrogates, and values above U+10FFFF.
|
||||
if(NOT bytes MATCHES "^(${utf8_character})*$")
|
||||
message(FATAL_ERROR "Localization must be UTF-8: ${catalog}")
|
||||
endif()
|
||||
file(READ "${catalog}" json)
|
||||
string(JSON type ERROR_VARIABLE error TYPE "${json}")
|
||||
if(error OR NOT type STREQUAL "OBJECT")
|
||||
message(FATAL_ERROR "Invalid localization object in ${catalog}: ${error}")
|
||||
endif()
|
||||
string(JSON count LENGTH "${json}")
|
||||
if(count GREATER 0)
|
||||
math(EXPR last "${count} - 1")
|
||||
foreach(index RANGE ${last})
|
||||
string(JSON key MEMBER "${json}" ${index})
|
||||
string(JSON type TYPE "${json}" "${key}")
|
||||
if(NOT type STREQUAL "STRING")
|
||||
message(FATAL_ERROR "Localization value '${key}' must be a string: ${catalog}")
|
||||
endif()
|
||||
string(JSON text GET "${json}" "${key}")
|
||||
if(key STREQUAL "" OR text STREQUAL "")
|
||||
message(FATAL_ERROR "Localization keys and values must not be empty: ${catalog}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
get_filename_component(locale "${catalog}" NAME_WE)
|
||||
if(locale STREQUAL "en-US")
|
||||
string(JSON earned ERROR_VARIABLE error GET "${json}" "trophy.toast.earned")
|
||||
if(error OR earned STREQUAL "")
|
||||
message(FATAL_ERROR "English localization requires trophy.toast.earned: ${catalog}")
|
||||
endif()
|
||||
set(english_found TRUE)
|
||||
endif()
|
||||
string(MAKE_C_IDENTIFIER "${locale}" symbol)
|
||||
string(REGEX REPLACE "(..)" "0x\\1," bytes "${bytes}")
|
||||
string(APPEND contents "constexpr unsigned char catalog_${symbol}[] = {${bytes}};\n")
|
||||
string(APPEND lookup "\tif (locale == \"${locale}\") return catalog_${symbol};\n")
|
||||
endforeach()
|
||||
if(NOT english_found)
|
||||
message(FATAL_ERROR "Localization requires an en-US.json fallback")
|
||||
endif()
|
||||
string(APPEND contents "\n${lookup}\treturn {};\n}\n} // namespace\n")
|
||||
|
||||
get_filename_component(output_dir "${OUTPUT}" DIRECTORY)
|
||||
file(MAKE_DIRECTORY "${output_dir}")
|
||||
file(WRITE "${OUTPUT}.tmp" "${contents}")
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${OUTPUT}.tmp" "${OUTPUT}"
|
||||
COMMAND_ERROR_IS_FATAL ANY)
|
||||
file(REMOVE "${OUTPUT}.tmp")
|
||||
@@ -66,6 +66,18 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
|
||||
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
m_bda_sync.Add(begin, end - begin);
|
||||
}
|
||||
|
||||
RangeSet MemoryTracker::TakeBdaSync() {
|
||||
std::lock_guard lock(m_bda_sync_mutex);
|
||||
return std::exchange(m_bda_sync, RangeSet {});
|
||||
}
|
||||
|
||||
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
CheckNotInUploadCallback();
|
||||
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
@@ -88,6 +100,7 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
|
||||
std::scoped_lock lock(manager->lock);
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
|
||||
@@ -128,6 +141,8 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
|
||||
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
|
||||
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
|
||||
});
|
||||
locks.clear();
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -29,6 +29,10 @@ public:
|
||||
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void UntrackMemory(uint64_t vaddr, uint64_t size);
|
||||
// Queue after changing ownership; a concurrent drain must not consume an event
|
||||
// before the dirty pages it describes become visible.
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] RangeSet TakeBdaSync();
|
||||
// Removes protection from a range and flushes GPU-owned data when required.
|
||||
template <typename Flush>
|
||||
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
|
||||
@@ -51,6 +55,7 @@ public:
|
||||
on_flush();
|
||||
}
|
||||
});
|
||||
QueueBdaSync(vaddr, size);
|
||||
}
|
||||
#if KYTY_BUILD == KYTY_BUILD_DEBUG
|
||||
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
|
||||
@@ -149,6 +154,8 @@ private:
|
||||
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
|
||||
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
|
||||
std::mutex m_region_mutex;
|
||||
std::mutex m_bda_sync_mutex;
|
||||
RangeSet m_bda_sync;
|
||||
PageManager& m_page_manager;
|
||||
};
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ void BufferCache::ChangeRegister(BufferId id) {
|
||||
}
|
||||
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
|
||||
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
|
||||
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
|
||||
} else {
|
||||
const auto found = m_buffers.find(buffer.CpuAddress());
|
||||
EXIT_IF(found == m_buffers.end() || found->second != id);
|
||||
@@ -685,6 +686,16 @@ void BufferCache::ProcessFaultBuffer() {
|
||||
m_fault_manager.ProcessFaultBuffer();
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
|
||||
// Take before walking: writes queued during synchronization remain pending.
|
||||
const auto pending = m_memory_tracker.TakeBdaSync();
|
||||
pending.ForEach([&](uint64_t begin, uint64_t end) {
|
||||
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
|
||||
SynchronizeBuffersInRange(start, finish - start);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
|
||||
const auto end = vaddr + size;
|
||||
auto it = m_buffers.upper_bound(vaddr);
|
||||
|
||||
+5
-1
@@ -80,7 +80,10 @@ public:
|
||||
[[nodiscard]] bool IsRegionCpuModified(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] bool IsRegionGpuModified(uint64_t vaddr, uint64_t size);
|
||||
void ProcessFaultBuffer();
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
void SynchronizeBda(const RangeSet& mapped);
|
||||
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
|
||||
m_memory_tracker.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
void RunGarbageCollector();
|
||||
|
||||
private:
|
||||
@@ -114,6 +117,7 @@ private:
|
||||
void DeleteBuffer(BufferId id);
|
||||
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
|
||||
bool is_written, bool is_texel_buffer);
|
||||
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
|
||||
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
|
||||
uint64_t total_size);
|
||||
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
|
||||
|
||||
+1
-3
@@ -67,11 +67,9 @@ Buffer::Buffer(GraphicContext& graphics, CommandScheduler& scheduler, MemoryUsag
|
||||
buffer_info.usage = flags;
|
||||
|
||||
const bool with_bda = bool(flags & vk::BufferUsageFlagBits::eShaderDeviceAddress);
|
||||
const VmaAllocationCreateFlags bda_flag =
|
||||
with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0;
|
||||
VmaAllocationCreateInfo allocation_info {};
|
||||
allocation_info.flags =
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | bda_flag | AllocationFlags(usage);
|
||||
VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | AllocationFlags(usage);
|
||||
allocation_info.usage = AllocationUsage(usage);
|
||||
allocation_info.preferredFlags = usage == MemoryUsage::DeviceLocal
|
||||
? VkMemoryPropertyFlags {}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kytyGitVersion.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -81,7 +80,8 @@ std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties)
|
||||
uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u];
|
||||
uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu];
|
||||
}
|
||||
return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION,
|
||||
// Driver cache entries already distinguish shader code and pipeline state across builds.
|
||||
return fmt::format("KytyPC2:{:08x}:{:08x}:{:08x}:{}\n",
|
||||
properties.vendorID, properties.deviceID, properties.driverVersion, uuid);
|
||||
}
|
||||
|
||||
@@ -516,17 +516,6 @@ void PipelineCache::InitializeDriverCache() {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)");
|
||||
return;
|
||||
}
|
||||
const std::string_view git_hash = KYTY_GIT_HASH;
|
||||
const std::string_view git_revision = KYTY_GIT_REVISION;
|
||||
if (git_hash == "unknown" || git_revision == "unknown") {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)");
|
||||
return;
|
||||
}
|
||||
if (git_hash.ends_with("-dirty")) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)");
|
||||
return;
|
||||
}
|
||||
|
||||
m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin");
|
||||
const auto path = Common::PathToString(m_driver_cache_path);
|
||||
const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path);
|
||||
@@ -559,7 +548,7 @@ void PipelineCache::InitializeDriverCache() {
|
||||
XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) {
|
||||
initial_data.clear();
|
||||
PipelineCacheLog(
|
||||
"Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)",
|
||||
"Vulkan pipeline cache: invalidating {} (driver or data mismatch)",
|
||||
path);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/localization.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "graphics/presentation/window/hostInput.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@@ -29,6 +31,7 @@
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -149,6 +152,47 @@ bool GetOverlaySnapshot(OverlaySnapshot* snapshot) {
|
||||
}
|
||||
|
||||
constexpr size_t INPUT_QUEUE_CAPACITY = 128;
|
||||
constexpr int MAX_TOUCH_CURSOR_IMAGE_DIMENSION = 2048;
|
||||
constexpr float TOUCH_CURSOR_IMAGE_SIZE = 48.0f;
|
||||
|
||||
enum class TouchParticleSkin : uint8_t { Classic, BlueNeon, Monochrome, Crimson, RedBlue };
|
||||
|
||||
TouchParticleSkin GetTouchParticleSkin() {
|
||||
static const auto skin = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
constexpr std::string_view key = "TouchParticleSkin=";
|
||||
if (std::string_view(value).starts_with(key)) {
|
||||
const auto name = std::string_view(value).substr(key.size());
|
||||
if (name == "BlueNeon") {
|
||||
return TouchParticleSkin::BlueNeon;
|
||||
}
|
||||
if (name == "Monochrome") {
|
||||
return TouchParticleSkin::Monochrome;
|
||||
}
|
||||
if (name == "Crimson") {
|
||||
return TouchParticleSkin::Crimson;
|
||||
}
|
||||
if (name == "RedBlue") {
|
||||
return TouchParticleSkin::RedBlue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TouchParticleSkin::Classic;
|
||||
}();
|
||||
return skin;
|
||||
}
|
||||
|
||||
bool TouchCursorImageEnabled() {
|
||||
static const bool enabled = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
if (std::string_view(value) == "TouchCursorImage=Off") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
enum class InputKind : uint8_t {
|
||||
Button,
|
||||
@@ -238,6 +282,61 @@ std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_
|
||||
}
|
||||
}
|
||||
|
||||
// ImGui's built-in font covers Latin only. Merge installed fonts for the selected console language.
|
||||
void AddSystemFontFallbacks(ImGuiIO& io) {
|
||||
io.Fonts->AddFontDefault();
|
||||
const auto add = [&](std::initializer_list<const char*> paths) {
|
||||
for (const char* path: paths) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::is_regular_file(path, error)) {
|
||||
ImFontConfig config;
|
||||
config.MergeMode = true;
|
||||
config.Flags = ImFontFlags_NoLoadError;
|
||||
if (io.Fonts->AddFontFromFileTTF(path, 0.0f, &config) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool found =
|
||||
add({"C:/Windows/Fonts/segoeui.ttf", "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/Library/Fonts/Arial Unicode.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/noto/NotoSans-Regular.ttf"});
|
||||
const auto locale = Common::Localization::LocaleName(Config::GetConsoleLanguage());
|
||||
const char* noto_cjk[] = {"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc"};
|
||||
if (locale == "ja-JP") {
|
||||
found = add({"C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc",
|
||||
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc", noto_cjk[0], noto_cjk[1],
|
||||
noto_cjk[2]});
|
||||
} else if (locale == "ko-KR") {
|
||||
found = add({"C:/Windows/Fonts/malgun.ttf", "/System/Library/Fonts/AppleSDGothicNeo.ttc",
|
||||
noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "zh-Hans" || locale == "zh-Hant") {
|
||||
found =
|
||||
add({locale == "zh-Hans" ? "C:/Windows/Fonts/msyh.ttc" : "C:/Windows/Fonts/msjh.ttc",
|
||||
"/System/Library/Fonts/PingFang.ttc", noto_cjk[0], noto_cjk[1], noto_cjk[2]});
|
||||
} else if (locale == "th-TH") {
|
||||
found = add({"C:/Windows/Fonts/leelawui.ttf", "C:/Windows/Fonts/tahoma.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Ayuthaya.ttf",
|
||||
"/usr/share/fonts/truetype/tlwg/Garuda.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansThai-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansThai-Regular.ttf"});
|
||||
} else if (locale == "ar-AE") {
|
||||
found = add({"C:/Windows/Fonts/tahoma.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansArabic-Regular.ttf"});
|
||||
}
|
||||
if (!found) {
|
||||
LOGF("[Trophy] no installed fallback font for %.*s\n", static_cast<int>(locale.size()),
|
||||
locale.data());
|
||||
}
|
||||
}
|
||||
|
||||
void ClearInputEvents() {
|
||||
std::scoped_lock lock(g_input_mutex);
|
||||
g_input_events.clear();
|
||||
@@ -399,6 +498,63 @@ std::u16string Utf8ToUtf16(std::string_view text) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// The Arabic notification is a single RTL run. Use presentation forms because ImGui does not
|
||||
// shape Arabic. This intentionally applies only to the catalog subtitle, never game-provided names.
|
||||
std::string PrepareTrophySubtitle(int language) {
|
||||
const auto text = Common::Localization::Get(language, "trophy.toast.earned");
|
||||
if (Common::Localization::LocaleName(language) != "ar-AE") {
|
||||
return std::string(text);
|
||||
}
|
||||
struct ArabicForm {
|
||||
char16_t letter;
|
||||
char16_t isolated;
|
||||
bool joins_next;
|
||||
};
|
||||
static constexpr ArabicForm FORMS[] = {
|
||||
{0x0621, 0xfe80, false}, {0x0622, 0xfe81, false}, {0x0623, 0xfe83, false},
|
||||
{0x0624, 0xfe85, false}, {0x0625, 0xfe87, false}, {0x0626, 0xfe89, true},
|
||||
{0x0627, 0xfe8d, false}, {0x0628, 0xfe8f, true}, {0x0629, 0xfe93, false},
|
||||
{0x062a, 0xfe95, true}, {0x062b, 0xfe99, true}, {0x062c, 0xfe9d, true},
|
||||
{0x062d, 0xfea1, true}, {0x062e, 0xfea5, true}, {0x062f, 0xfea9, false},
|
||||
{0x0630, 0xfeab, false}, {0x0631, 0xfead, false}, {0x0632, 0xfeaf, false},
|
||||
{0x0633, 0xfeb1, true}, {0x0634, 0xfeb5, true}, {0x0635, 0xfeb9, true},
|
||||
{0x0636, 0xfebd, true}, {0x0637, 0xfec1, true}, {0x0638, 0xfec5, true},
|
||||
{0x0639, 0xfec9, true}, {0x063a, 0xfecd, true}, {0x0641, 0xfed1, true},
|
||||
{0x0642, 0xfed5, true}, {0x0643, 0xfed9, true}, {0x0644, 0xfedd, true},
|
||||
{0x0645, 0xfee1, true}, {0x0646, 0xfee5, true}, {0x0647, 0xfee9, true},
|
||||
{0x0648, 0xfeed, false}, {0x0649, 0xfeef, false}, {0x064a, 0xfef1, true},
|
||||
};
|
||||
const auto form = [](char16_t c) -> const ArabicForm* {
|
||||
for (const auto& value: FORMS) {
|
||||
if (value.letter == c) {
|
||||
return &value;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const auto logical = Utf8ToUtf16(text);
|
||||
if (std::none_of(logical.begin(), logical.end(),
|
||||
[](char16_t c) { return c >= 0x0600 && c <= 0x06ff; })) {
|
||||
return std::string(text);
|
||||
}
|
||||
auto visual = logical;
|
||||
for (size_t i = 0; i < logical.size(); ++i) {
|
||||
const auto* current = form(logical[i]);
|
||||
if (current == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto* previous = i > 0 ? form(logical[i - 1]) : nullptr;
|
||||
const auto* next = i + 1 < logical.size() ? form(logical[i + 1]) : nullptr;
|
||||
const bool joins_previous =
|
||||
previous != nullptr && previous->joins_next && current->letter != 0x0621;
|
||||
const bool joins_next = current->joins_next && next != nullptr && next->letter != 0x0621;
|
||||
visual[i] = static_cast<char16_t>(current->isolated + (joins_next ? 2 : 0) +
|
||||
(joins_previous ? 1 : 0));
|
||||
}
|
||||
std::reverse(visual.begin(), visual.end());
|
||||
return Common::Utf16ToUtf8(visual);
|
||||
}
|
||||
|
||||
std::string VisibleText(const Ime::HostSnapshot& snapshot, size_t max_units) {
|
||||
const size_t cursor = std::min<size_t>(snapshot.cursor, snapshot.text.size());
|
||||
const size_t payload_units = max_units > 4 ? max_units - 4 : 1;
|
||||
@@ -552,9 +708,11 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
const bool splash = g_splash_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash,
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash ||
|
||||
(cursor.enabled && cursor.cursor_visible),
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u)};
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -694,6 +852,20 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
enum class TouchParticleIcon : uint8_t { Square, Triangle, Cross, Circle };
|
||||
|
||||
struct TouchParticle {
|
||||
ImVec2 position;
|
||||
Clock::time_point born;
|
||||
TouchParticleIcon icon;
|
||||
};
|
||||
|
||||
struct TouchTrailSegment {
|
||||
ImVec2 from;
|
||||
ImVec2 to;
|
||||
Clock::time_point born;
|
||||
};
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
@@ -705,6 +877,12 @@ struct SystemOverlay::Impl {
|
||||
ReleaseVulkan();
|
||||
if (imgui_context != nullptr) {
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
if (hand_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(hand_cursor_image.get());
|
||||
}
|
||||
if (drawing_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(drawing_cursor_image.get());
|
||||
}
|
||||
if (trophy_notification_image) {
|
||||
ImGui::UnregisterUserTexture(trophy_notification_image.get());
|
||||
}
|
||||
@@ -730,11 +908,14 @@ struct SystemOverlay::Impl {
|
||||
io.ConfigNavCursorVisibleAlways = true;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
io.BackendPlatformName = "Kyty system overlay input";
|
||||
AddSystemFontFallbacks(io);
|
||||
ImGui::StyleColorsDark();
|
||||
auto& style = ImGui::GetStyle();
|
||||
style.WindowRounding = 10.0f;
|
||||
style.FrameRounding = 6.0f;
|
||||
style.ItemSpacing = {8.0f, 8.0f};
|
||||
hand_cursor_image = LoadTouchCursorImage("hand.png");
|
||||
drawing_cursor_image = LoadTouchCursorImage("drawing.png");
|
||||
}
|
||||
|
||||
void PlayTrophySound() {
|
||||
@@ -775,6 +956,55 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTouchCursorImage(std::string_view filename) {
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
if (base_path == nullptr) {
|
||||
LOGF("Could not locate the emulator folder for touchpad cursor images: %s\n",
|
||||
SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path(base_path) / "assets" / "cursors" / std::string(filename);
|
||||
|
||||
Common::File file;
|
||||
if (!file.Open(path, Common::File::Mode::Read)) {
|
||||
LOGF("Touchpad cursor image not found: %s\n", Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
const auto encoded = file.ReadWholeBuffer();
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
LOGF("Touchpad cursor image is empty or too large: %s\n",
|
||||
Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
auto* pixels = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(encoded.data()), static_cast<int>(encoded.size()),
|
||||
&width, &height, nullptr, 4);
|
||||
if (pixels == nullptr || width <= 0 || height <= 0 ||
|
||||
width > MAX_TOUCH_CURSOR_IMAGE_DIMENSION ||
|
||||
height > MAX_TOUCH_CURSOR_IMAGE_DIMENSION) {
|
||||
LOGF("Could not load touchpad cursor image %s: %s\n",
|
||||
Common::PathToString(path).c_str(),
|
||||
pixels == nullptr ? stbi_failure_reason() : "image dimensions exceed 2048x2048");
|
||||
stbi_image_free(pixels);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<ImTextureData>();
|
||||
texture->Create(ImTextureFormat_RGBA32, width, height);
|
||||
std::memcpy(texture->Pixels, pixels,
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
|
||||
texture->UseColors = true;
|
||||
stbi_image_free(pixels);
|
||||
ImGui::RegisterUserTexture(texture.get());
|
||||
LOGF("Loaded touchpad cursor image: %s\n", Common::PathToString(path).c_str());
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
@@ -1191,6 +1421,248 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void EmitTouchParticles(ImVec2 from, ImVec2 to, Clock::time_point now) {
|
||||
const float dx = to.x - from.x;
|
||||
const float dy = to.y - from.y;
|
||||
const float distance = std::hypot(dx, dy);
|
||||
if (distance < 0.01f) {
|
||||
return;
|
||||
}
|
||||
if (distance >= 1.0f) {
|
||||
trail_segments.push_back({from, to, now});
|
||||
if (trail_segments.size() > MAX_TOUCH_TRAIL_SEGMENTS) {
|
||||
trail_segments.erase(trail_segments.begin());
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float PARTICLE_SPACING = 24.0f;
|
||||
float traveled = 0.0f;
|
||||
while (particle_distance + distance - traveled >= PARTICLE_SPACING) {
|
||||
traveled += PARTICLE_SPACING - particle_distance;
|
||||
const float fraction = std::clamp(traveled / distance, 0.0f, 1.0f);
|
||||
particles.push_back(
|
||||
{{from.x + dx * fraction, from.y + dy * fraction}, now, next_particle_icon});
|
||||
next_particle_icon = static_cast<TouchParticleIcon>(
|
||||
(static_cast<uint8_t>(next_particle_icon) + 1) % 4);
|
||||
if (particles.size() > MAX_TOUCH_PARTICLES) {
|
||||
particles.erase(particles.begin());
|
||||
}
|
||||
particle_distance = 0.0f;
|
||||
}
|
||||
particle_distance += distance - traveled;
|
||||
}
|
||||
|
||||
void DrawTouchParticles(ImDrawList* draw, Clock::time_point now) {
|
||||
const auto skin = GetTouchParticleSkin();
|
||||
constexpr float PARTICLE_LIFETIME = 1.0f;
|
||||
constexpr float TRAIL_LIFETIME = 0.35f;
|
||||
std::erase_if(trail_segments, [now](const auto& segment) {
|
||||
return std::chrono::duration<float>(now - segment.born).count() >= TRAIL_LIFETIME;
|
||||
});
|
||||
std::erase_if(particles, [now](const auto& particle) {
|
||||
return std::chrono::duration<float>(now - particle.born).count() >=
|
||||
PARTICLE_LIFETIME;
|
||||
});
|
||||
for (const auto& segment: trail_segments) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - segment.born).count() / TRAIL_LIFETIME;
|
||||
const float fade = 1.0f - age;
|
||||
if (skin == TouchParticleSkin::RedBlue) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(20, 75, 220, static_cast<int>(105.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 45, 65, static_cast<int>(230.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(110, 0, 8, static_cast<int>(130.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 35, 20, static_cast<int>(235.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(35, 145, 255, static_cast<int>(180.0f * fade)),
|
||||
5.0f * (1.0f - age * 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& particle: particles) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - particle.born).count() / PARTICLE_LIFETIME;
|
||||
const float alpha = 1.0f - age;
|
||||
const float size = 9.0f * (1.0f - age * 0.35f);
|
||||
const float drift = (static_cast<uint8_t>(particle.icon) % 2 == 0 ? -1.0f : 1.0f) *
|
||||
8.0f * age;
|
||||
const ImVec2 center {particle.position.x + drift, particle.position.y - 18.0f * age};
|
||||
const auto faded = [alpha, skin, icon = particle.icon](int r, int g, int b) {
|
||||
if (skin == TouchParticleSkin::BlueNeon) {
|
||||
r = 75;
|
||||
g = 220;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::RedBlue) {
|
||||
const bool red = icon == TouchParticleIcon::Square ||
|
||||
icon == TouchParticleIcon::Cross;
|
||||
r = red ? 255 : 75;
|
||||
g = red ? 65 : 175;
|
||||
b = red ? 85 : 255;
|
||||
} else if (skin == TouchParticleSkin::Monochrome) {
|
||||
r = 235;
|
||||
g = 245;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
const bool ember = icon == TouchParticleIcon::Triangle ||
|
||||
icon == TouchParticleIcon::Circle;
|
||||
r = 255;
|
||||
g = ember ? 205 : 115;
|
||||
b = ember ? 70 : 15;
|
||||
}
|
||||
return IM_COL32(r, g, b, static_cast<int>(255.0f * alpha));
|
||||
};
|
||||
|
||||
if (skin != TouchParticleSkin::Classic) {
|
||||
const ImU32 glow =
|
||||
skin == TouchParticleSkin::BlueNeon
|
||||
? IM_COL32(0, 115, 255, static_cast<int>(38.0f * alpha))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? IM_COL32(75, 100, 255, static_cast<int>(55.0f * alpha))
|
||||
: (skin == TouchParticleSkin::Crimson
|
||||
? IM_COL32(255, 70, 0, static_cast<int>(70.0f * alpha))
|
||||
: IM_COL32(115, 165, 255, static_cast<int>(30.0f * alpha))));
|
||||
draw->AddCircleFilled(center, size + 6.0f, glow);
|
||||
}
|
||||
const ImU32 particle_glow =
|
||||
skin == TouchParticleSkin::Crimson
|
||||
? (particle.icon == TouchParticleIcon::Triangle ||
|
||||
particle.icon == TouchParticleIcon::Circle
|
||||
? IM_COL32(255, 195, 30, static_cast<int>(135.0f * alpha))
|
||||
: IM_COL32(255, 95, 0, static_cast<int>(135.0f * alpha)))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? (particle.icon == TouchParticleIcon::Square ||
|
||||
particle.icon == TouchParticleIcon::Cross
|
||||
? IM_COL32(255, 40, 65, static_cast<int>(105.0f * alpha))
|
||||
: IM_COL32(35, 125, 255, static_cast<int>(105.0f * alpha)))
|
||||
: IM_COL32(35, 145, 255,
|
||||
static_cast<int>((skin == TouchParticleSkin::BlueNeon
|
||||
? 100.0f
|
||||
: 65.0f) *
|
||||
alpha)));
|
||||
draw->AddCircleFilled(center, size + 2.0f, particle_glow);
|
||||
switch (particle.icon) {
|
||||
case TouchParticleIcon::Square:
|
||||
draw->AddRect({center.x - size * 0.65f, center.y - size * 0.65f},
|
||||
{center.x + size * 0.65f, center.y + size * 0.65f},
|
||||
faded(255, 105, 180), 2.0f, 0, 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Triangle:
|
||||
draw->AddLine({center.x, center.y - size}, {center.x - size, center.y + size},
|
||||
faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x - size, center.y + size},
|
||||
{center.x + size, center.y + size}, faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x + size, center.y + size},
|
||||
{center.x, center.y - size}, faded(80, 230, 130), 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Cross:
|
||||
draw->AddLine({center.x - size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x + size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
draw->AddLine({center.x + size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x - size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
break;
|
||||
case TouchParticleIcon::Circle:
|
||||
draw->AddCircle(center, size * 0.8f, faded(255, 105, 105), 0, 2.2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTouchPadCursor(const HostInputTouchPadVisualState& state) {
|
||||
if (!TouchCursorImageEnabled()) {
|
||||
particles.clear();
|
||||
trail_segments.clear();
|
||||
return;
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (state.enabled && state.position_valid) {
|
||||
const ImVec2 target {state.x * extent.width, state.y * extent.height};
|
||||
if (!cursor_initialized || state.gesture != cursor_gesture) {
|
||||
cursor_position = target;
|
||||
cursor_initialized = true;
|
||||
cursor_gesture = state.gesture;
|
||||
particle_distance = 0.0f;
|
||||
} else {
|
||||
const float dt = last_cursor_update == Clock::time_point {}
|
||||
? 1.0f / 60.0f
|
||||
: std::clamp(std::chrono::duration<float>(now -
|
||||
last_cursor_update)
|
||||
.count(),
|
||||
1.0f / 1000.0f, 0.05f);
|
||||
const ImVec2 previous = cursor_position;
|
||||
const float follow = 1.0f - std::exp(-18.0f * dt);
|
||||
cursor_position.x += (target.x - cursor_position.x) * follow;
|
||||
cursor_position.y += (target.y - cursor_position.y) * follow;
|
||||
if (state.drawing) {
|
||||
EmitTouchParticles(previous, cursor_position, now);
|
||||
}
|
||||
}
|
||||
last_cursor_update = now;
|
||||
}
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
DrawTouchParticles(draw, now);
|
||||
if (!state.enabled || !state.cursor_visible ||
|
||||
!state.position_valid || !cursor_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 position = cursor_position;
|
||||
const auto& cursor_image =
|
||||
state.drawing ? drawing_cursor_image : hand_cursor_image;
|
||||
if (cursor_image != nullptr) {
|
||||
const float scale = TOUCH_CURSOR_IMAGE_SIZE /
|
||||
static_cast<float>(std::max(cursor_image->Height, 1));
|
||||
const ImVec2 size {static_cast<float>(cursor_image->Width) * scale,
|
||||
static_cast<float>(cursor_image->Height) * scale};
|
||||
draw->AddImage(cursor_image->GetTexRef(), position,
|
||||
{position.x + size.x, position.y + size.y});
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 blue = IM_COL32(35, 145, 255, 220);
|
||||
const ImVec2 hand[] = {
|
||||
{position.x, position.y}, {position.x, position.y + 20.0f},
|
||||
{position.x + 4.0f, position.y + 16.0f},
|
||||
{position.x + 9.0f, position.y + 27.0f},
|
||||
{position.x + 13.0f, position.y + 25.0f},
|
||||
{position.x + 10.0f, position.y + 17.0f},
|
||||
{position.x + 18.0f, position.y + 18.0f},
|
||||
{position.x + 20.0f, position.y + 15.0f},
|
||||
{position.x + 19.0f, position.y + 12.0f},
|
||||
{position.x + 12.0f, position.y + 10.0f},
|
||||
{position.x + 12.0f, position.y + 4.0f},
|
||||
{position.x + 10.0f, position.y + 2.0f},
|
||||
{position.x + 8.0f, position.y + 3.0f},
|
||||
{position.x + 8.0f, position.y + 10.0f},
|
||||
{position.x + 6.0f, position.y + 2.0f},
|
||||
{position.x + 4.0f, position.y + 2.0f},
|
||||
{position.x + 3.0f, position.y + 10.0f},
|
||||
{position.x + 2.0f, position.y + 1.0f},
|
||||
};
|
||||
draw->AddConvexPolyFilled(hand, IM_ARRAYSIZE(hand), IM_COL32(250, 250, 250, 255));
|
||||
for (std::size_t i = 0; i < IM_ARRAYSIZE(hand); i++) {
|
||||
draw->AddLine(hand[i], hand[(i + 1) % IM_ARRAYSIZE(hand)],
|
||||
IM_COL32(20, 25, 35, 255), 1.5f);
|
||||
}
|
||||
if (state.drawing) {
|
||||
const ImVec2 pen_start {position.x + 12.0f, position.y + 8.0f};
|
||||
const ImVec2 pen_end {position.x + 23.0f, position.y - 3.0f};
|
||||
draw->AddLine(pen_start, pen_end, IM_COL32(10, 45, 95, 255), 5.0f);
|
||||
draw->AddLine(pen_start, pen_end, blue, 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
@@ -1221,8 +1693,9 @@ struct SystemOverlay::Impl {
|
||||
trophy_notification = std::move(g_trophy_notifications.front());
|
||||
g_trophy_notifications.pop_front();
|
||||
}
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
trophy_notification_subtitle = PrepareTrophySubtitle(Config::GetConsoleLanguage());
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
PlayTrophySound();
|
||||
}
|
||||
const float age = std::chrono::duration<float>(now - trophy_notification_born).count();
|
||||
@@ -1306,8 +1779,17 @@ struct SystemOverlay::Impl {
|
||||
line.data(), line.data() + line.size());
|
||||
line_y += line_height;
|
||||
}
|
||||
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), "Trophy earned!");
|
||||
const float subtitle_size = 20.0f * screen_scale;
|
||||
const float subtitle_width =
|
||||
ImGui::GetFont()
|
||||
->CalcTextSizeA(subtitle_size, std::numeric_limits<float>::max(), 0.0f,
|
||||
trophy_notification_subtitle.c_str())
|
||||
.x;
|
||||
const float available_width = std::max(1.0f, width - text_offset - text_padding);
|
||||
const float fitted_size = std::max(
|
||||
1.0f, subtitle_size * std::min(1.0f, available_width / std::max(1.0f, subtitle_width)));
|
||||
draw->AddText(nullptr, fitted_size, {text_left, line_y + 8.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), trophy_notification_subtitle.c_str());
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
@@ -1316,7 +1798,8 @@ struct SystemOverlay::Impl {
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire)) {
|
||||
!g_splash_active.load(std::memory_order_acquire) &&
|
||||
!GetHostInputTouchPadVisualState().enabled) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -1356,6 +1839,7 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
return true;
|
||||
@@ -1410,7 +1894,20 @@ struct SystemOverlay::Impl {
|
||||
ImVec2 right_stick {};
|
||||
OverlaySession session;
|
||||
std::unique_ptr<ImTextureData> trophy_notification_image;
|
||||
std::unique_ptr<ImTextureData> hand_cursor_image;
|
||||
std::unique_ptr<ImTextureData> drawing_cursor_image;
|
||||
bool cursor_initialized = false;
|
||||
ImVec2 cursor_position {};
|
||||
float particle_distance = 0.0f;
|
||||
TouchParticleIcon next_particle_icon = TouchParticleIcon::Square;
|
||||
Clock::time_point last_cursor_update;
|
||||
uint64_t cursor_gesture = 0;
|
||||
std::vector<TouchParticle> particles;
|
||||
std::vector<TouchTrailSegment> trail_segments;
|
||||
static constexpr std::size_t MAX_TOUCH_PARTICLES = 64;
|
||||
static constexpr std::size_t MAX_TOUCH_TRAIL_SEGMENTS = 128;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
std::string trophy_notification_subtitle;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -27,6 +29,8 @@ struct ControlInfo {
|
||||
float touch_x = 0.0f;
|
||||
|
||||
Controller::Setting setting {};
|
||||
bool gyro = false;
|
||||
bool touchpad_mode = false;
|
||||
};
|
||||
|
||||
static constexpr std::array CONTROL_INFO = {
|
||||
@@ -61,10 +65,66 @@ static constexpr std::array CONTROL_INFO = {
|
||||
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
|
||||
ControlInfo {.name = "TriggerEffectIntensity",
|
||||
.setting = Controller::Setting::TriggerEffectIntensity},
|
||||
ControlInfo {.name = "GyroToggle", .gyro = true},
|
||||
ControlInfo {.name = "TouchPadMode", .touchpad_mode = true},
|
||||
};
|
||||
|
||||
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
|
||||
|
||||
struct PadSource {
|
||||
std::string_view name;
|
||||
int button; // SDL_GamepadButton, or -1 for an analog trigger
|
||||
};
|
||||
|
||||
constexpr std::array PAD_SOURCES = {
|
||||
PadSource {"South", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
PadSource {"East", SDL_GAMEPAD_BUTTON_EAST},
|
||||
PadSource {"West", SDL_GAMEPAD_BUTTON_WEST},
|
||||
PadSource {"North", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
PadSource {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||
PadSource {"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
PadSource {"Start", SDL_GAMEPAD_BUTTON_START},
|
||||
PadSource {"LeftStick", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
PadSource {"RightStick", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
PadSource {"LeftShoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
PadSource {"RightShoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
PadSource {"DpadUp", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
PadSource {"DpadDown", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
PadSource {"DpadLeft", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
PadSource {"DpadRight", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
PadSource {"Misc1", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
PadSource {"RightPaddle1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
PadSource {"LeftPaddle1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
PadSource {"RightPaddle2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
PadSource {"LeftPaddle2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
PadSource {"Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
PadSource {"LeftTrigger", -1},
|
||||
PadSource {"RightTrigger", -1},
|
||||
};
|
||||
|
||||
constexpr std::size_t LEFT_TRIGGER_SOURCE = PAD_SOURCES.size() - 2;
|
||||
constexpr std::size_t RIGHT_TRIGGER_SOURCE = PAD_SOURCES.size() - 1;
|
||||
constexpr std::size_t INVALID_SOURCE = PAD_SOURCES.size();
|
||||
|
||||
struct PadDefault {
|
||||
std::string_view control;
|
||||
std::string_view source;
|
||||
};
|
||||
|
||||
constexpr std::array PAD_DEFAULTS = {
|
||||
PadDefault {"Cross", "South"}, PadDefault {"Circle", "East"},
|
||||
PadDefault {"Square", "West"}, PadDefault {"Triangle", "North"},
|
||||
PadDefault {"Options", "Start"}, PadDefault {"L3", "LeftStick"},
|
||||
PadDefault {"R3", "RightStick"}, PadDefault {"L1", "LeftShoulder"},
|
||||
PadDefault {"R1", "RightShoulder"}, PadDefault {"L2", "LeftTrigger"},
|
||||
PadDefault {"R2", "RightTrigger"}, PadDefault {"Up", "DpadUp"},
|
||||
PadDefault {"Down", "DpadDown"}, PadDefault {"Left", "DpadLeft"},
|
||||
PadDefault {"Right", "DpadRight"}, PadDefault {"TouchPad", "Touchpad"},
|
||||
PadDefault {"GyroToggle", "Back"},
|
||||
};
|
||||
|
||||
constexpr std::string_view PAD_PREFIX = "Pad:";
|
||||
|
||||
struct Binding {
|
||||
SDL_Keycode key = SDLK_UNKNOWN;
|
||||
uint8_t mouse_button = 0;
|
||||
@@ -74,6 +134,7 @@ struct Binding {
|
||||
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
|
||||
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
|
||||
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
|
||||
constexpr std::string_view TOUCH_PARTICLE_SKIN = "TouchParticleSkin=";
|
||||
|
||||
struct MouseJoystickState {
|
||||
bool enabled = false;
|
||||
@@ -85,6 +146,38 @@ MouseJoystickState g_mouse;
|
||||
SDL_Window* g_mouse_window = nullptr;
|
||||
uint64_t g_cursor_hide_at = 0;
|
||||
|
||||
struct MouseTouchPadState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
struct GamepadTouchPadState {
|
||||
bool enabled = false;
|
||||
bool drawing = false;
|
||||
int id = -1;
|
||||
int x_axis = 128;
|
||||
int y_axis = 128;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t next_poll = 0;
|
||||
uint64_t last_poll = 0;
|
||||
};
|
||||
|
||||
constexpr int GAMEPAD_TOUCHPAD_POLL_MS = 8;
|
||||
constexpr int GAMEPAD_TOUCHPAD_DEADZONE = 18;
|
||||
constexpr float GAMEPAD_TOUCHPAD_SPEED = 0.8f;
|
||||
|
||||
MouseTouchPadState g_mouse_touchpad;
|
||||
GamepadTouchPadState g_gamepad_touchpad;
|
||||
std::mutex g_touchpad_mutex;
|
||||
std::unordered_map<int, std::array<bool, 2>> g_pad_trigger_pressed;
|
||||
|
||||
std::size_t ControlFromName(std::string_view name) {
|
||||
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
|
||||
[name](const auto& item) { return item.name == name; });
|
||||
@@ -126,6 +219,14 @@ public:
|
||||
InputMap() {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
const std::string_view entry = value;
|
||||
if (entry == "Keyboard=Disabled") {
|
||||
m_custom = true;
|
||||
continue;
|
||||
}
|
||||
if (entry == "Pad=Disabled") {
|
||||
m_custom_pad = true;
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(MOUSE_SENSITIVITY)) {
|
||||
const float sensitivity =
|
||||
std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr);
|
||||
@@ -133,6 +234,13 @@ public:
|
||||
std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f);
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(TOUCH_PARTICLE_SKIN) || entry.starts_with("TouchCursorImage=")) {
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(PAD_PREFIX)) {
|
||||
AddPadEntry(entry.substr(PAD_PREFIX.size()), value);
|
||||
continue;
|
||||
}
|
||||
const auto split = entry.find('=');
|
||||
|
||||
Binding binding;
|
||||
@@ -153,10 +261,25 @@ public:
|
||||
}
|
||||
Add(binding);
|
||||
}
|
||||
if (!m_custom_pad) {
|
||||
for (const auto& item: PAD_DEFAULTS) {
|
||||
AssignPad(ControlFromName(item.control), SourceFromName(item.source));
|
||||
}
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool Custom() const { return m_size != 0; }
|
||||
[[nodiscard]] bool Custom() const { return m_custom; }
|
||||
[[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; }
|
||||
|
||||
[[nodiscard]] std::size_t PadControl(std::size_t source) const {
|
||||
return source < m_pad_controls.size() ? m_pad_controls[source] : INVALID_CONTROL;
|
||||
}
|
||||
|
||||
static std::size_t SourceFromButton(int button) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[button](const auto& s) { return s.button == button; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t FindKey(int key_code) const {
|
||||
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
|
||||
const auto binding =
|
||||
@@ -173,7 +296,39 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t SourceFromName(std::string_view name) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[name](const auto& s) { return s.name == name; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
void AssignPad(std::size_t control, std::size_t source) {
|
||||
for (auto& existing: m_pad_controls) {
|
||||
if (existing == control) {
|
||||
existing = INVALID_CONTROL;
|
||||
}
|
||||
}
|
||||
m_pad_controls[source] = control;
|
||||
}
|
||||
|
||||
void AddPadEntry(std::string_view entry, const std::string& original) {
|
||||
const auto split = entry.find('=');
|
||||
const auto control = split != std::string_view::npos ? ControlFromName(entry.substr(0, split))
|
||||
: INVALID_CONTROL;
|
||||
const auto source = split != std::string_view::npos ? SourceFromName(entry.substr(split + 1))
|
||||
: INVALID_SOURCE;
|
||||
if (control == INVALID_CONTROL || source == INVALID_SOURCE) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == 0 && !info.gyro && info.axis != Controller::Axis::AxisMax) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
m_custom_pad = true;
|
||||
AssignPad(control, source);
|
||||
}
|
||||
void Add(const Binding& binding) {
|
||||
m_custom = true;
|
||||
for (std::size_t index = 0; index < m_size;) {
|
||||
if (Conflicts(m_bindings[index], binding)) {
|
||||
m_bindings[index] = m_bindings[--m_size];
|
||||
@@ -186,6 +341,13 @@ private:
|
||||
}
|
||||
|
||||
std::array<Binding, CONTROL_INFO.size()> m_bindings {};
|
||||
std::array<std::size_t, PAD_SOURCES.size()> m_pad_controls = [] {
|
||||
std::array<std::size_t, PAD_SOURCES.size()> a {};
|
||||
a.fill(INVALID_CONTROL);
|
||||
return a;
|
||||
}();
|
||||
bool m_custom = false;
|
||||
bool m_custom_pad = false;
|
||||
std::size_t m_size = 0;
|
||||
float m_mouse_sensitivity = 1.0f;
|
||||
};
|
||||
@@ -211,6 +373,39 @@ void SetTouchPad(float x, bool down) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 0, down, x, 0.5f);
|
||||
}
|
||||
|
||||
void SetSwipeTouchPad(bool down, float x, float y) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 1, down, x, y);
|
||||
}
|
||||
|
||||
void RecordMouseTouchPadPosition(float x, float y) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (g_mouse_window != nullptr) {
|
||||
SDL_GetWindowSize(g_mouse_window, &width, &height);
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
x = std::clamp(x / static_cast<float>(width), 0.0f, 1.0f);
|
||||
y = std::clamp(y / static_cast<float>(height), 0.0f, 1.0f);
|
||||
} else {
|
||||
x = 0.5f;
|
||||
y = 0.5f;
|
||||
}
|
||||
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
g_mouse_touchpad.x = x;
|
||||
g_mouse_touchpad.y = y;
|
||||
g_mouse_touchpad.position_valid = true;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
|
||||
bool TouchPadModeActive() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
return g_mouse_touchpad.enabled || g_gamepad_touchpad.enabled;
|
||||
}
|
||||
|
||||
uint32_t DefaultKeyboardButton(int key_code) {
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_UP: return Controller::PAD_BUTTON_UP;
|
||||
@@ -270,6 +465,18 @@ void SetControl(std::size_t control, bool down) {
|
||||
SetButton(info.button, down);
|
||||
return;
|
||||
}
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(Controller::HOST_INPUT_CONTROLLER_ID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
return;
|
||||
@@ -297,6 +504,11 @@ void DefaultKeyboardInput(int key_code, bool down) {
|
||||
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
|
||||
case SDLK_F6:
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
case SDLK_TAB: SetTouchPad(0.75f, down); return;
|
||||
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
|
||||
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
|
||||
@@ -399,8 +611,134 @@ bool IsCursorActivity(const SDL_Event& event) {
|
||||
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
|
||||
}
|
||||
|
||||
std::array<int, 2> GamepadLeftStick(int id) {
|
||||
auto* pad = SDL_GetGamepadFromID(id);
|
||||
if (pad == nullptr) {
|
||||
return {128, 128};
|
||||
}
|
||||
const auto value = [pad](SDL_GamepadAxis axis) {
|
||||
return Controller::controller_get_axis(SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX,
|
||||
SDL_GetGamepadAxis(pad, axis));
|
||||
};
|
||||
return {value(SDL_GAMEPAD_AXIS_LEFTX), value(SDL_GAMEPAD_AXIS_LEFTY)};
|
||||
}
|
||||
|
||||
void ApplyPadControl(int id, std::size_t control, std::size_t source, bool down) {
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(id);
|
||||
}
|
||||
} else if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleGamepadTouchPad(id);
|
||||
}
|
||||
} else if (info.button == Controller::PAD_BUTTON_CROSS && HostInputGamepadDrawButton(id, down)) {
|
||||
return;
|
||||
} else if (info.button == Controller::PAD_BUTTON_TOUCH_PAD) {
|
||||
// Real touchpads report their own finger position; other inputs emulate a click.
|
||||
if (source != PAD_SOURCES.size() && PAD_SOURCES[source].button != SDL_GAMEPAD_BUTTON_TOUCHPAD) {
|
||||
Controller::SetTouchPad(id, 0, down, info.touch_x != 0.0f ? info.touch_x : 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(id, Controller::PAD_BUTTON_TOUCH_PAD, down);
|
||||
} else if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, down ? 255 : 0);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, down ? 255 : 0);
|
||||
} else if (info.button != 0) {
|
||||
Controller::SetButton(id, info.button, down);
|
||||
} else if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
}
|
||||
}
|
||||
|
||||
int PollGamepadTouchPad(uint64_t now_ms) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
int id = -1;
|
||||
bool drawing = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled) {
|
||||
return -1;
|
||||
}
|
||||
if (now_ms < g_gamepad_touchpad.next_poll) {
|
||||
return static_cast<int>(g_gamepad_touchpad.next_poll - now_ms);
|
||||
}
|
||||
|
||||
const float dt = g_gamepad_touchpad.last_poll == 0 || now_ms <= g_gamepad_touchpad.last_poll
|
||||
? 0.008f
|
||||
: std::min(static_cast<float>(now_ms - g_gamepad_touchpad.last_poll) *
|
||||
0.001f,
|
||||
0.05f);
|
||||
g_gamepad_touchpad.last_poll = now_ms;
|
||||
g_gamepad_touchpad.next_poll = now_ms + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
const auto velocity = [](int axis) {
|
||||
const int offset = axis - 128;
|
||||
if (std::abs(offset) <= GAMEPAD_TOUCHPAD_DEADZONE) {
|
||||
return 0.0f;
|
||||
}
|
||||
const int adjusted =
|
||||
offset > 0 ? offset - GAMEPAD_TOUCHPAD_DEADZONE : offset + GAMEPAD_TOUCHPAD_DEADZONE;
|
||||
return static_cast<float>(adjusted) / static_cast<float>(127 - GAMEPAD_TOUCHPAD_DEADZONE);
|
||||
};
|
||||
const float old_x = g_gamepad_touchpad.x;
|
||||
const float old_y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad.x = std::clamp(
|
||||
old_x + velocity(g_gamepad_touchpad.x_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
g_gamepad_touchpad.y = std::clamp(
|
||||
old_y + velocity(g_gamepad_touchpad.y_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
if (old_x != g_gamepad_touchpad.x || old_y != g_gamepad_touchpad.y) {
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
id = g_gamepad_touchpad.id;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
drawing = g_gamepad_touchpad.drawing;
|
||||
}
|
||||
if (drawing) {
|
||||
Controller::SetTouchPad(id, 1, true, x, y);
|
||||
}
|
||||
return GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HostInputPadButton(int id, int button, bool down) {
|
||||
const auto source = InputMap::SourceFromButton(button);
|
||||
const auto& map = GetInputMap();
|
||||
ApplyPadControl(id, map.PadControl(source), source, down);
|
||||
}
|
||||
|
||||
void HostInputPadTrigger(int id, bool left, int value) {
|
||||
const auto source = left ? LEFT_TRIGGER_SOURCE : RIGHT_TRIGGER_SOURCE;
|
||||
const auto control = GetInputMap().PadControl(source);
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, value);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, value);
|
||||
} else {
|
||||
auto& state = g_pad_trigger_pressed[id][left ? 0 : 1];
|
||||
const bool down = state ? value > 32 : value > 96;
|
||||
if (down != state) {
|
||||
state = down;
|
||||
ApplyPadControl(id, control, source, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepad(int id) {
|
||||
g_pad_trigger_pressed.erase(id);
|
||||
HostInputDisconnectGamepadTouchPad(id);
|
||||
}
|
||||
|
||||
void HostInputInit(SDL_Window* window) {
|
||||
GetInputMap();
|
||||
g_mouse_window = window;
|
||||
@@ -410,6 +748,16 @@ void HostInputInit(SDL_Window* window) {
|
||||
}
|
||||
|
||||
void HostInputShutdown() {
|
||||
g_pad_trigger_pressed.clear();
|
||||
HostInputSetTouchPadMode(false);
|
||||
int gamepad_touch_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
gamepad_touch_id = g_gamepad_touchpad.enabled ? g_gamepad_touchpad.id : -1;
|
||||
}
|
||||
if (gamepad_touch_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_touch_id);
|
||||
}
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
CenterMouseStick();
|
||||
@@ -438,6 +786,219 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputMouseTouchPad(bool down, float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
if (down && !g_mouse_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_mouse_touchpad.drawing = down;
|
||||
g_mouse_touchpad.revision++;
|
||||
x = g_mouse_touchpad.x;
|
||||
y = g_mouse_touchpad.y;
|
||||
}
|
||||
SetSwipeTouchPad(down, x, y);
|
||||
}
|
||||
|
||||
void HostInputMouseMotion(float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled && g_mouse_touchpad.drawing) {
|
||||
SetSwipeTouchPad(true, g_mouse_touchpad.x, g_mouse_touchpad.y);
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputSetTouchPadMode(bool enabled) {
|
||||
if (enabled) {
|
||||
int gamepad_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
gamepad_id = g_gamepad_touchpad.id;
|
||||
}
|
||||
}
|
||||
if (gamepad_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_id);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled == enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
(void) SDL_GetMouseState(&x, &y);
|
||||
HostInputMouseTouchPad(false, x, y);
|
||||
}
|
||||
bool show_system_cursor = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
g_mouse_touchpad.enabled = enabled;
|
||||
g_mouse_touchpad.cursor_visible = true;
|
||||
g_mouse_touchpad.drawing = false;
|
||||
g_mouse_touchpad.position_valid = false;
|
||||
g_mouse_touchpad.gesture++;
|
||||
g_mouse_touchpad.revision++;
|
||||
show_system_cursor = !enabled && !g_gamepad_touchpad.enabled;
|
||||
}
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else if (show_system_cursor && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
LOGF("Mouse touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void HostInputToggleTouchPadMode() {
|
||||
bool enabled = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
enabled = !g_mouse_touchpad.enabled;
|
||||
}
|
||||
HostInputSetTouchPadMode(enabled);
|
||||
}
|
||||
|
||||
void HostInputToggleGamepadTouchPad(int id) {
|
||||
bool mouse_mode = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
mouse_mode = g_mouse_touchpad.enabled;
|
||||
}
|
||||
if (mouse_mode) {
|
||||
HostInputSetTouchPadMode(false);
|
||||
}
|
||||
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
bool enabled = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled && g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
enabled = !g_gamepad_touchpad.enabled;
|
||||
if (enabled) {
|
||||
g_gamepad_touchpad = {};
|
||||
g_gamepad_touchpad.enabled = true;
|
||||
g_gamepad_touchpad.id = id;
|
||||
g_gamepad_touchpad.x_axis = stick[0];
|
||||
g_gamepad_touchpad.y_axis = stick[1];
|
||||
g_gamepad_touchpad.last_poll = SDL_GetTicks();
|
||||
g_gamepad_touchpad.next_poll = g_gamepad_touchpad.last_poll + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
g_mouse_touchpad.gesture++;
|
||||
} else {
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
}
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, enabled ? 128 : stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, enabled ? 128 : stick[1]);
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for gamepad touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
if (!Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad touchpad mode: %s\n",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
LOGF("Gamepad touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool HostInputGamepadDrawButton(int id, bool down) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
if (!down && !g_gamepad_touchpad.drawing) {
|
||||
return false;
|
||||
}
|
||||
if (down && !g_gamepad_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_gamepad_touchpad.drawing = down;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, down, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HostInputGamepadAxis(int id, bool horizontal, int value) {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
(horizontal ? g_gamepad_touchpad.x_axis : g_gamepad_touchpad.y_axis) = std::clamp(value, 0, 255);
|
||||
g_gamepad_touchpad.next_poll = SDL_GetTicks();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepadTouchPad(int id) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, stick[1]);
|
||||
if (!TouchPadModeActive() && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad disconnect: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
HostInputTouchPadVisualState GetHostInputTouchPadVisualState() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
HostInputTouchPadVisualState state {};
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
state.enabled = true;
|
||||
state.cursor_visible = true;
|
||||
state.drawing = g_gamepad_touchpad.drawing;
|
||||
state.x = g_gamepad_touchpad.x;
|
||||
state.y = g_gamepad_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = true;
|
||||
return state;
|
||||
}
|
||||
state.enabled = g_mouse_touchpad.enabled;
|
||||
state.cursor_visible = g_mouse_touchpad.cursor_visible;
|
||||
state.drawing = g_mouse_touchpad.drawing;
|
||||
state.x = g_mouse_touchpad.x;
|
||||
state.y = g_mouse_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = g_mouse_touchpad.position_valid;
|
||||
return state;
|
||||
}
|
||||
|
||||
void HostInputToggleMouseToJoystick() {
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
@@ -467,16 +1028,19 @@ bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
timeout = PollMouse(SDL_GetTicks());
|
||||
}
|
||||
|
||||
if (g_cursor_hide_at != 0) {
|
||||
if (g_cursor_hide_at != 0 && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
const int cursor_timeout =
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
if (const int touch_timeout = PollGamepadTouchPad(SDL_GetTicks()); touch_timeout >= 0) {
|
||||
timeout = timeout < 0 ? touch_timeout : std::min(timeout, touch_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
if (Config::HideCursorEnabled() && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
|
||||
SDL_GetWindowFromEvent(event) == g_mouse_window) {
|
||||
|
||||
@@ -8,10 +8,33 @@ union SDL_Event;
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct HostInputTouchPadVisualState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
void HostInputInit(SDL_Window* window);
|
||||
void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputMouseTouchPad(bool down, float x, float y);
|
||||
void HostInputMouseMotion(float x, float y);
|
||||
void HostInputPadButton(int id, int sdl_button, bool down);
|
||||
void HostInputPadTrigger(int id, bool left, int value);
|
||||
void HostInputDisconnectGamepad(int id);
|
||||
void HostInputSetTouchPadMode(bool enabled);
|
||||
void HostInputToggleTouchPadMode();
|
||||
void HostInputToggleGamepadTouchPad(int id);
|
||||
[[nodiscard]] bool HostInputGamepadDrawButton(int id, bool down);
|
||||
[[nodiscard]] bool HostInputGamepadAxis(int id, bool horizontal, int value);
|
||||
void HostInputDisconnectGamepadTouchPad(int id);
|
||||
[[nodiscard]] HostInputTouchPadVisualState GetHostInputTouchPadVisualState();
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
|
||||
@@ -945,7 +945,6 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
auto* frame = m_impl->frames.Acquire({width, height}, format);
|
||||
frame->hdr = false;
|
||||
frame->blank = true;
|
||||
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
|
||||
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
|
||||
vk::ClearColorValue clear {};
|
||||
clear.float32[3] = opaque ? 1.0f : 0.0f;
|
||||
@@ -953,6 +952,7 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
|
||||
EXIT_IF(producer->IsInvalid());
|
||||
frame->Clear(*producer, clear);
|
||||
} else {
|
||||
Common::LockGuard lock(m_impl->present_mutex);
|
||||
auto& command = m_impl->present_scheduler.BeginCommand();
|
||||
frame->Clear(command, clear);
|
||||
frame->present_tick = m_impl->present_scheduler.Submit();
|
||||
@@ -1036,18 +1036,17 @@ void Presenter::Impl::Present() {
|
||||
RecoverSwapchain(status);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
Common::LockGuard render_lock(renderer.GetMutex());
|
||||
auto& command = present_scheduler.BeginCommand();
|
||||
const bool draw_system_overlay =
|
||||
overlay_visual.active && swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
// present_mutex owns the prepared frames and this scheduler. Only queue submission
|
||||
// is shared with rendering, and it has its own queue_mutex.
|
||||
auto& command = present_scheduler.BeginCommand();
|
||||
const bool draw_system_overlay =
|
||||
overlay_visual.active && swapchain.PrepareSystemOverlay();
|
||||
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
|
||||
draw_system_overlay);
|
||||
const auto tick = swapchain.Submit(present_scheduler);
|
||||
for (const auto& layer: layers) {
|
||||
if (layer.frame != nullptr) {
|
||||
layer.frame->present_tick = tick;
|
||||
}
|
||||
}
|
||||
status = swapchain.Present();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "graphics/presentation/window.h"
|
||||
#include "graphics/presentation/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -54,25 +54,6 @@ struct EventKeyboard {
|
||||
double timestamp_seconds;
|
||||
};
|
||||
|
||||
static uint32_t ControllerButtonToPadButton(int button) {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH: return Controller::PAD_BUTTON_CROSS;
|
||||
case SDL_GAMEPAD_BUTTON_EAST: return Controller::PAD_BUTTON_CIRCLE;
|
||||
case SDL_GAMEPAD_BUTTON_WEST: return Controller::PAD_BUTTON_SQUARE;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH: return Controller::PAD_BUTTON_TRIANGLE;
|
||||
case SDL_GAMEPAD_BUTTON_START: return Controller::PAD_BUTTON_OPTIONS;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_STICK: return Controller::PAD_BUTTON_L3;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return Controller::PAD_BUTTON_R3;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return Controller::PAD_BUTTON_L1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Controller::PAD_BUTTON_R1;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP: return Controller::PAD_BUTTON_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return Controller::PAD_BUTTON_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return Controller::PAD_BUTTON_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return Controller::PAD_BUTTON_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_TOUCHPAD: return Controller::PAD_BUTTON_TOUCH_PAD;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static Controller::Axis ControllerAxisFromSdl(int axis_id) {
|
||||
switch (axis_id) {
|
||||
@@ -268,6 +249,15 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
|
||||
}
|
||||
|
||||
HostInputMouseButton(mouse_button, mb.down);
|
||||
if (!mb.touch && mb.left) {
|
||||
HostInputMouseTouchPad(mb.down, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
} else if (mb.motion && !mb.touch) {
|
||||
if (mb.left) {
|
||||
HostInputMouseTouchPad(true, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
} else {
|
||||
HostInputMouseMotion(static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +306,7 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.removed) {
|
||||
HostInputDisconnectGamepad(f.id);
|
||||
if (auto* pad = SDL_GetGamepadFromID(f.id); pad != nullptr) {
|
||||
Controller::Disconnect(f.id);
|
||||
SDL_CloseGamepad(pad);
|
||||
@@ -323,30 +314,22 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.down || f.up) {
|
||||
const auto button = ControllerButtonToPadButton(f.button);
|
||||
if (button != 0) {
|
||||
Controller::SetButton(f.id, button, f.down);
|
||||
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
|
||||
// blocks games that require it. Use their Back/Select button as a centered click.
|
||||
auto* pad = SDL_GetGamepadFromID(f.id);
|
||||
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
|
||||
if (f.down) {
|
||||
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
|
||||
if (f.up) {
|
||||
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
HostInputPadButton(f.id, f.button, f.down);
|
||||
}
|
||||
|
||||
if (f.axis) {
|
||||
const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
if (axis != Controller::Axis::AxisMax) {
|
||||
Controller::SetAxis(f.id, axis,
|
||||
if (f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ||
|
||||
f.axis_id == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
HostInputPadTrigger(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
ControllerAxisValueFromSdl(f.axis_id, f.axis_value));
|
||||
} else if (const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
axis != Controller::Axis::AxisMax) {
|
||||
const int value = ControllerAxisValueFromSdl(f.axis_id, f.axis_value);
|
||||
if ((f.axis_id == SDL_GAMEPAD_AXIS_LEFTX || f.axis_id == SDL_GAMEPAD_AXIS_LEFTY) &&
|
||||
HostInputGamepadAxis(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFTX, value)) {
|
||||
return;
|
||||
}
|
||||
Controller::SetAxis(f.id, axis, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +414,7 @@ void WindowContext::ProcessWindowEvent(const SDL_WindowEvent& event) {
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
LOGF("Mouse left window %" PRIu32 "\n", window_event.windowID);
|
||||
HostInputSetTouchPadMode(false);
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
LOGF("Window %" PRIu32 " gained keyboard focus\n", window_event.windowID);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -344,6 +344,8 @@ static void ShaderGetStaticInputInfoPS(
|
||||
ps_info.ps_pos_z = (active_inputs & 0x00000400u) != 0;
|
||||
ps_info.ps_pos_w = (active_inputs & 0x00000800u) != 0;
|
||||
ps_info.ps_front_face = (active_inputs & 0x00001000u) != 0;
|
||||
// SPI_BARYC_CNTL.FRONT_FACE_ALL_BITS selects integer masks instead of float +/-1.
|
||||
ps_info.ps_front_face_all_bits = (sh.baryc_cntl & 0x01000000u) != 0;
|
||||
ps_info.ps_ancillary = (active_inputs & 0x00002000u) != 0;
|
||||
ps_info.ps_sample_shading = (active_inputs & 0x00000011u) != 0;
|
||||
ps_info.ps_no_perspective = (sh.ps_input_ena & sh.ps_input_addr & 0x00000020u) != 0;
|
||||
@@ -464,6 +466,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_z));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_pos_w));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_front_face_all_bits));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_ancillary));
|
||||
key.push_back(static_cast<uint32_t>(info.ps_no_perspective));
|
||||
key.push_back(static_cast<uint32_t>(info.parameter_mode));
|
||||
@@ -737,6 +740,7 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_pos_z = %s\n"
|
||||
"\t ps_pos_w = %s\n"
|
||||
"\t ps_front_face = %s\n"
|
||||
"\t ps_front_face_all_bits = %s\n"
|
||||
"\t ps_ancillary = %s\n"
|
||||
"\t ps_sample_shading = %s\n"
|
||||
"\t ps_no_perspective = %s\n"
|
||||
@@ -745,11 +749,10 @@ void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info) {
|
||||
"\t ps_execute_on_noop = %s\n",
|
||||
info.input_num, info.ps_system_input_base, info.custom_interpolation_mask,
|
||||
info.ps_perspective_center_vgpr, info.ps_perspective_sample_vgpr,
|
||||
info.ps_perspective_centroid_vgpr,
|
||||
info.ps_pos_x ? "true" : "false",
|
||||
info.ps_perspective_centroid_vgpr, info.ps_pos_x ? "true" : "false",
|
||||
info.ps_pos_y ? "true" : "false", info.ps_pos_z ? "true" : "false",
|
||||
info.ps_pos_w ? "true" : "false", info.ps_front_face ? "true" : "false",
|
||||
info.ps_ancillary ? "true" : "false",
|
||||
info.ps_front_face_all_bits ? "true" : "false", info.ps_ancillary ? "true" : "false",
|
||||
info.ps_sample_shading ? "true" : "false", info.ps_no_perspective ? "true" : "false",
|
||||
info.ps_pixel_kill_enable ? "true" : "false", info.ps_early_z ? "true" : "false",
|
||||
info.ps_execute_on_noop ? "true" : "false");
|
||||
|
||||
@@ -196,6 +196,7 @@ struct ShaderPixelInputInfo {
|
||||
bool ps_pos_z = false;
|
||||
bool ps_pos_w = false;
|
||||
bool ps_front_face = false;
|
||||
bool ps_front_face_all_bits = false;
|
||||
bool ps_ancillary = false;
|
||||
bool ps_no_perspective = false;
|
||||
ShaderPixelParameterMode parameter_mode = ShaderPixelParameterMode::FirstVertex;
|
||||
|
||||
@@ -21,7 +21,8 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static
|
||||
LibAtrac9 kyty_git_version common)
|
||||
if(LINUX)
|
||||
find_package(Qt6 COMPONENTS DBus REQUIRED)
|
||||
target_link_libraries(launcher Qt6::DBus)
|
||||
@@ -76,7 +77,7 @@ if(WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(launcher_name "launcher")
|
||||
set(launcher_name "kyty_launcher")
|
||||
if(APPLE)
|
||||
set(launcher_name "KytyPS5")
|
||||
endif()
|
||||
@@ -101,10 +102,20 @@ install(TARGETS launcher
|
||||
BUNDLE DESTINATION .
|
||||
RUNTIME DESTINATION .)
|
||||
|
||||
if(NOT APPLE)
|
||||
# Older releases require the previous executable name when validating updates.
|
||||
install(CODE "
|
||||
file(INSTALL DESTINATION \"\${CMAKE_INSTALL_PREFIX}\"
|
||||
TYPE PROGRAM RENAME \"launcher${CMAKE_EXECUTABLE_SUFFIX}\"
|
||||
FILES \"\${CMAKE_INSTALL_PREFIX}/${launcher_name}${CMAKE_EXECUTABLE_SUFFIX}\")
|
||||
")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/cursors" DESTINATION "KytyPS5.app/Contents/Resources/assets")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
if(NOT KYTY_MACDEPLOYQT)
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
void SetSuspended(bool suspended);
|
||||
|
||||
[[nodiscard]] const QSet<QString>& Pressed() const { return m_pressed; }
|
||||
[[nodiscard]] const QSet<QString>& RawPressed() const { return m_raw_pressed; }
|
||||
[[nodiscard]] QPointF LeftStick() const { return m_left_stick; }
|
||||
[[nodiscard]] QPointF RightStick() const { return m_right_stick; }
|
||||
[[nodiscard]] const QString& DeviceName() const { return m_device_name; }
|
||||
@@ -44,6 +45,7 @@ private:
|
||||
bool m_preview_enabled = false;
|
||||
bool m_suspended = false;
|
||||
QSet<QString> m_pressed;
|
||||
QSet<QString> m_raw_pressed;
|
||||
QPointF m_left_stick;
|
||||
QPointF m_right_stick;
|
||||
QString m_device_name;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#ifndef LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
#define LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_
|
||||
|
||||
#include <QDialog>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class ControllerPreview;
|
||||
class DualSenseWidget;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QDoubleSpinBox;
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
class InputMappingDialog final: public QDialog {
|
||||
public:
|
||||
@@ -20,20 +25,30 @@ public:
|
||||
[[nodiscard]] QStringList Mapping() const;
|
||||
|
||||
private:
|
||||
void ChangeBinding(const QString& id);
|
||||
void RestoreDefaults();
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
void UpdateHighlights();
|
||||
QWidget* BuildTab(bool pad);
|
||||
void ChangeKeyboardBinding(const QString& id);
|
||||
void ChangePadBinding(const QString& id);
|
||||
void ClearBinding(bool pad, const QString& id);
|
||||
void RestoreDefaults();
|
||||
void UpdateButtons();
|
||||
void UpdatePreview();
|
||||
void UpdateHighlights();
|
||||
|
||||
ControllerPreview* m_preview;
|
||||
DualSenseWidget* m_controller = nullptr;
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
QHash<QString, QString> m_bindings;
|
||||
QHash<QString, QString> m_pad_bindings;
|
||||
QHash<QString, QPushButton*> m_buttons;
|
||||
QHash<QString, QPushButton*> m_pad_buttons;
|
||||
QSet<QString> m_pad_pressed;
|
||||
QString m_selected;
|
||||
QLabel* m_status = nullptr;
|
||||
QDoubleSpinBox* m_sensitivity = nullptr;
|
||||
QComboBox* m_particle_skin = nullptr;
|
||||
QCheckBox* m_touch_cursor = nullptr;
|
||||
bool m_custom_bindings = false;
|
||||
bool m_custom_pad = false;
|
||||
};
|
||||
|
||||
#endif /* LAUNCHER_INCLUDE_INPUT_MAPPING_DIALOG_H_ */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFutureWatcher>
|
||||
#include <QObject>
|
||||
#include <QPromise>
|
||||
#include <QString>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct SDL_AudioStream;
|
||||
|
||||
class TitleSound final: public QObject {
|
||||
public:
|
||||
explicit TitleSound(QObject* parent = nullptr);
|
||||
~TitleSound() override;
|
||||
|
||||
void SetTitle(const QString& base);
|
||||
|
||||
private:
|
||||
struct Sound {
|
||||
std::vector<float> pcm;
|
||||
int channels = 0;
|
||||
int rate = 0;
|
||||
};
|
||||
|
||||
static Sound Decode(const QByteArray& data, const QPromise<Sound>& promise);
|
||||
void Stop();
|
||||
|
||||
QThreadPool m_decode_pool;
|
||||
QFutureWatcher<Sound> m_decode;
|
||||
QString m_base;
|
||||
SDL_AudioStream* m_stream = nullptr;
|
||||
bool m_audio_initialized = false;
|
||||
};
|
||||
@@ -20,6 +20,34 @@ constexpr struct {
|
||||
{SDL_GAMEPAD_BUTTON_START, "Options"},
|
||||
};
|
||||
|
||||
// Host-side names; they must match PAD_SOURCES in hostInput.cpp.
|
||||
constexpr struct {
|
||||
SDL_GamepadButton button;
|
||||
const char* name;
|
||||
} RAW_BUTTONS[] = {
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, "South"},
|
||||
{SDL_GAMEPAD_BUTTON_EAST, "East"},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, "West"},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, "North"},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, "Back"},
|
||||
{SDL_GAMEPAD_BUTTON_GUIDE, "Guide"},
|
||||
{SDL_GAMEPAD_BUTTON_START, "Start"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_STICK, "LeftStick"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_STICK, "RightStick"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LeftShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RightShoulder"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DpadUp"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DpadDown"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DpadLeft"},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DpadRight"},
|
||||
{SDL_GAMEPAD_BUTTON_MISC1, "Misc1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, "RightPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, "LeftPaddle1"},
|
||||
{SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, "RightPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, "LeftPaddle2"},
|
||||
{SDL_GAMEPAD_BUTTON_TOUCHPAD, "Touchpad"},
|
||||
};
|
||||
|
||||
double Axis(SDL_Gamepad* gamepad, SDL_GamepadAxis axis) {
|
||||
const int value = SDL_GetGamepadAxis(gamepad, axis);
|
||||
return value / (value < 0 ? 32768.0 : 32767.0);
|
||||
@@ -100,6 +128,7 @@ void ControllerPreview::UpdateRunning() {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
m_pressed.clear();
|
||||
m_raw_pressed.clear();
|
||||
m_left_stick = {};
|
||||
m_right_stick = {};
|
||||
m_device_name.clear();
|
||||
@@ -131,12 +160,25 @@ void ControllerPreview::Refresh() {
|
||||
}
|
||||
|
||||
QSet<QString> pressed;
|
||||
QSet<QString> raw;
|
||||
QPointF left;
|
||||
QPointF right;
|
||||
QString name;
|
||||
if (!m_gamepads.isEmpty()) {
|
||||
auto* gamepad = m_gamepads.first();
|
||||
name = QString::fromUtf8(SDL_GetGamepadName(gamepad));
|
||||
for (const auto& binding: RAW_BUTTONS) {
|
||||
if (SDL_GamepadHasButton(gamepad, binding.button) &&
|
||||
SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
raw.insert(QString::fromLatin1(binding.name));
|
||||
}
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("LeftTrigger"));
|
||||
}
|
||||
if (Axis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0.5) {
|
||||
raw.insert(QStringLiteral("RightTrigger"));
|
||||
}
|
||||
for (const auto& binding: BUTTONS) {
|
||||
if (SDL_GetGamepadButton(gamepad, binding.button)) {
|
||||
pressed.insert(QString::fromLatin1(binding.control));
|
||||
@@ -180,9 +222,10 @@ void ControllerPreview::Refresh() {
|
||||
pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
}
|
||||
if (pressed != m_pressed || left != m_left_stick || right != m_right_stick ||
|
||||
name != m_device_name) {
|
||||
if (pressed != m_pressed || raw != m_raw_pressed || left != m_left_stick ||
|
||||
right != m_right_stick || name != m_device_name) {
|
||||
m_pressed = pressed;
|
||||
m_raw_pressed = raw;
|
||||
m_left_stick = left;
|
||||
m_right_stick = right;
|
||||
m_device_name = name;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "inputMappingDialog.h"
|
||||
#include "inputMappingDialog.h"
|
||||
|
||||
#include "controllerPreview.h"
|
||||
#include "dualsenseWidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QGridLayout>
|
||||
@@ -16,56 +18,113 @@
|
||||
#include <QPushButton>
|
||||
#include <QScreen>
|
||||
#include <QScrollArea>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto DEFAULT_MOUSE_SENSITIVITY = 1.0;
|
||||
constexpr char MOUSE_SENSITIVITY[] = "MouseSensitivity=";
|
||||
constexpr char TOUCH_PARTICLE_SKIN[] = "TouchParticleSkin=";
|
||||
constexpr char TOUCH_CURSOR_OFF[] = "TouchCursorImage=Off";
|
||||
constexpr char KEYBOARD_DISABLED[] = "Keyboard=Disabled";
|
||||
constexpr char PAD_DISABLED[] = "Pad=Disabled";
|
||||
|
||||
enum Group { Dpad, LeftStick, RightStick, Face, Shoulders, Center, Shortcuts, GroupCount };
|
||||
constexpr char PAD_PREFIX[] = "Pad:";
|
||||
|
||||
enum Section { Face, Dpad, Shoulders, Sticks, System, LeftStick, RightStick, Shortcuts, SectionCount };
|
||||
|
||||
const char* const SECTION_TITLES[SectionCount] = {
|
||||
QT_TR_NOOP("Face buttons"), QT_TR_NOOP("D-pad"),
|
||||
QT_TR_NOOP("Shoulders && triggers"), QT_TR_NOOP("Stick clicks"),
|
||||
QT_TR_NOOP("System"), QT_TR_NOOP("Left stick"),
|
||||
QT_TR_NOOP("Right stick"), QT_TR_NOOP("In-game shortcuts (cycle)"),
|
||||
};
|
||||
|
||||
// pad_default is null for controls that have no gamepad equivalent.
|
||||
struct PadControl {
|
||||
const char* id;
|
||||
const char* label;
|
||||
const char* default_binding;
|
||||
Group group;
|
||||
int row;
|
||||
int column;
|
||||
const char* keyboard_default;
|
||||
const char* pad_default;
|
||||
Section section;
|
||||
};
|
||||
|
||||
constexpr PadControl PAD_CONTROLS[] = {
|
||||
{"Up", "Up", "Up", Dpad, 0, 1},
|
||||
{"Down", "Down", "Down", Dpad, 2, 1},
|
||||
{"Left", "Left", "Left", Dpad, 1, 0},
|
||||
{"Right", "Right", "Right", Dpad, 1, 2},
|
||||
{"LeftStickUp", "Up", "W", LeftStick, 0, 1},
|
||||
{"LeftStickDown", "Down", "S", LeftStick, 2, 1},
|
||||
{"LeftStickLeft", "Left", "A", LeftStick, 1, 0},
|
||||
{"LeftStickRight", "Right", "D", LeftStick, 1, 2},
|
||||
{"RightStickUp", "Up", "T", RightStick, 0, 1},
|
||||
{"RightStickDown", "Down", "G", RightStick, 2, 1},
|
||||
{"RightStickLeft", "Left", "F", RightStick, 1, 0},
|
||||
{"RightStickRight", "Right", "H", RightStick, 1, 2},
|
||||
{"Triangle", "Triangle", "I", Face, 0, 1},
|
||||
{"Circle", "Circle", "L", Face, 1, 2},
|
||||
{"Cross", "Cross", "J", Face, 2, 1},
|
||||
{"Square", "Square", "K", Face, 1, 0},
|
||||
{"L1", "L1", "Q", Shoulders, 0, 1},
|
||||
{"R1", "R1", "E", Shoulders, 0, 2},
|
||||
{"L2", "L2", "Z", Shoulders, 0, 0},
|
||||
{"R2", "R2", "C", Shoulders, 0, 3},
|
||||
{"L3", "Press / L3", "Left Shift", LeftStick, 1, 1},
|
||||
{"R3", "Press / R3", "Left Ctrl", RightStick, 1, 1},
|
||||
{"Options", "Options", "Return", Center, 0, 1},
|
||||
{"TouchPad", "Touch pad left", "Backspace", Center, 0, 0},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", Center, 0, 2},
|
||||
{"SpeakerVolume", "Speaker volume", "1", Shortcuts, 0, 0},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", Shortcuts, 0, 1},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", Shortcuts, 0, 2},
|
||||
{"Cross", "Cross", "J", "South", Face},
|
||||
{"Circle", "Circle", "L", "East", Face},
|
||||
{"Square", "Square", "K", "West", Face},
|
||||
{"Triangle", "Triangle", "I", "North", Face},
|
||||
{"Up", "Up", "Up", "DpadUp", Dpad},
|
||||
{"Down", "Down", "Down", "DpadDown", Dpad},
|
||||
{"Left", "Left", "Left", "DpadLeft", Dpad},
|
||||
{"Right", "Right", "Right", "DpadRight", Dpad},
|
||||
{"L1", "L1", "Q", "LeftShoulder", Shoulders},
|
||||
{"R1", "R1", "E", "RightShoulder", Shoulders},
|
||||
{"L2", "L2", "Z", "LeftTrigger", Shoulders},
|
||||
{"R2", "R2", "C", "RightTrigger", Shoulders},
|
||||
{"L3", "L3 (left stick click)", "Left Shift", "LeftStick", Sticks},
|
||||
{"R3", "R3 (right stick click)", "Left Ctrl", "RightStick", Sticks},
|
||||
{"Options", "Options", "Return", "Start", System},
|
||||
{"TouchPad", "Touch pad click", "Backspace", "Touchpad", System},
|
||||
{"TouchPadRight", "Touch pad right", "Tab", nullptr, System},
|
||||
{"GyroToggle", "Gyro emulation toggle", "", "Back", System},
|
||||
{"TouchPadMode", "Emulated touch pad mode", "F6", "", System},
|
||||
{"LeftStickUp", "Up", "W", nullptr, LeftStick},
|
||||
{"LeftStickDown", "Down", "S", nullptr, LeftStick},
|
||||
{"LeftStickLeft", "Left", "A", nullptr, LeftStick},
|
||||
{"LeftStickRight", "Right", "D", nullptr, LeftStick},
|
||||
{"RightStickUp", "Up", "T", nullptr, RightStick},
|
||||
{"RightStickDown", "Down", "G", nullptr, RightStick},
|
||||
{"RightStickLeft", "Left", "F", nullptr, RightStick},
|
||||
{"RightStickRight", "Right", "H", nullptr, RightStick},
|
||||
{"SpeakerVolume", "Speaker volume", "1", nullptr, Shortcuts},
|
||||
{"VibrationIntensity", "Vibration intensity", "2", nullptr, Shortcuts},
|
||||
{"TriggerEffectIntensity", "Trigger intensity", "3", nullptr, Shortcuts},
|
||||
};
|
||||
|
||||
struct PadSourceName {
|
||||
const char* id;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
constexpr PadSourceName PAD_SOURCES[] = {
|
||||
{"South", "A / Cross"},
|
||||
{"East", "B / Circle"},
|
||||
{"West", "X / Square"},
|
||||
{"North", "Y / Triangle"},
|
||||
{"Back", "View / Back / Share"},
|
||||
{"Guide", "Guide / Home"},
|
||||
{"Start", "Menu / Start"},
|
||||
{"LeftStick", "Left stick click"},
|
||||
{"RightStick", "Right stick click"},
|
||||
{"LeftShoulder", "LB / L1"},
|
||||
{"RightShoulder", "RB / R1"},
|
||||
{"DpadUp", "D-pad Up"},
|
||||
{"DpadDown", "D-pad Down"},
|
||||
{"DpadLeft", "D-pad Left"},
|
||||
{"DpadRight", "D-pad Right"},
|
||||
{"Misc1", "Share / Misc"},
|
||||
{"RightPaddle1", "Right paddle 1"},
|
||||
{"LeftPaddle1", "Left paddle 1"},
|
||||
{"RightPaddle2", "Right paddle 2"},
|
||||
{"LeftPaddle2", "Left paddle 2"},
|
||||
{"Touchpad", "Touch pad click"},
|
||||
{"LeftTrigger", "LT / L2"},
|
||||
{"RightTrigger", "RT / R2"},
|
||||
};
|
||||
|
||||
QString PadSourceLabel(const QString& id) {
|
||||
for (const auto& source: PAD_SOURCES) {
|
||||
if (id == QLatin1String(source.id)) {
|
||||
return QObject::tr(source.label);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
QString KeypadName(int key) {
|
||||
if (key >= Qt::Key_0 && key <= Qt::Key_9) {
|
||||
return QStringLiteral("Keypad %1").arg(key - Qt::Key_0);
|
||||
@@ -191,6 +250,54 @@ private:
|
||||
QString m_binding;
|
||||
};
|
||||
|
||||
class PadCaptureDialog final: public QDialog {
|
||||
public:
|
||||
PadCaptureDialog(ControllerPreview* preview, QWidget* parent): QDialog(parent) {
|
||||
setWindowTitle(tr("Set Binding"));
|
||||
setModal(true);
|
||||
setMinimumWidth(360);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
m_label = new QLabel(this);
|
||||
m_label->setAlignment(Qt::AlignCenter);
|
||||
m_label->setWordWrap(true);
|
||||
layout->addWidget(m_label);
|
||||
|
||||
if (preview == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Inputs already held when the dialog opens must be released before they count.
|
||||
m_held = preview->RawPressed();
|
||||
connect(preview, &ControllerPreview::StateChanged, this, [this, preview] {
|
||||
const auto& pressed = preview->RawPressed();
|
||||
m_held.intersect(pressed);
|
||||
for (const auto& name: pressed) {
|
||||
if (!m_held.contains(name)) {
|
||||
m_binding = name;
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
UpdateText(preview);
|
||||
});
|
||||
UpdateText(preview);
|
||||
}
|
||||
|
||||
[[nodiscard]] const QString& Binding() const { return m_binding; }
|
||||
|
||||
private:
|
||||
void UpdateText(const ControllerPreview* preview) {
|
||||
m_label->setText(preview->DeviceName().isEmpty()
|
||||
? tr("No controller connected.\nConnect one, or press Esc to cancel.")
|
||||
: tr("Press a button or trigger on %1.\nEsc cancels.")
|
||||
.arg(preview->DeviceName()));
|
||||
}
|
||||
|
||||
QLabel* m_label = nullptr;
|
||||
QString m_binding;
|
||||
QSet<QString> m_held;
|
||||
};
|
||||
|
||||
void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const QString& binding) {
|
||||
for (auto item = bindings.begin(); item != bindings.end();) {
|
||||
if (item.value().compare(binding, Qt::CaseInsensitive) == 0) {
|
||||
@@ -205,7 +312,11 @@ void AssignBinding(QHash<QString, QString>& bindings, const QString& id, const Q
|
||||
QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
QHash<QString, QString> result;
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
if (entry == QLatin1String(KEYBOARD_DISABLED) || entry == QLatin1String(PAD_DISABLED) ||
|
||||
entry.startsWith(QLatin1String(MOUSE_SENSITIVITY)) ||
|
||||
entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN)) ||
|
||||
entry.startsWith(QLatin1String("TouchCursorImage=")) ||
|
||||
entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto separator = entry.indexOf(QLatin1Char('='));
|
||||
@@ -216,6 +327,15 @@ QHash<QString, QString> ParseMapping(const QStringList& mapping) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QString ParseTouchParticleSkin(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(TOUCH_PARTICLE_SKIN))) {
|
||||
return entry.mid(sizeof(TOUCH_PARTICLE_SKIN) - 1);
|
||||
}
|
||||
}
|
||||
return QStringLiteral("Classic");
|
||||
}
|
||||
|
||||
double ParseMouseSensitivity(const QStringList& mapping) {
|
||||
for (const auto& entry: mapping) {
|
||||
if (entry.startsWith(QLatin1String(MOUSE_SENSITIVITY))) {
|
||||
@@ -231,106 +351,67 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
QWidget* parent)
|
||||
: QDialog(parent), m_preview(preview) {
|
||||
setWindowTitle(tr("Controls"));
|
||||
resize(QSize(1100, 820).boundedTo(screen()->availableGeometry().size()));
|
||||
resize(QSize(1360, 780).boundedTo(screen()->availableGeometry().size()));
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty() || mapping.contains(QLatin1String(KEYBOARD_DISABLED));
|
||||
m_custom_pad = mapping.contains(QLatin1String(PAD_DISABLED));
|
||||
for (const auto& entry: mapping) {
|
||||
if (!entry.startsWith(QLatin1String(PAD_PREFIX))) {
|
||||
continue;
|
||||
}
|
||||
const auto body = entry.mid(static_cast<int>(sizeof(PAD_PREFIX)) - 1);
|
||||
const auto separator = body.indexOf(QLatin1Char('='));
|
||||
if (separator > 0 && separator + 1 < body.size()) {
|
||||
AssignBinding(m_pad_bindings, body.left(separator), body.mid(separator + 1));
|
||||
m_custom_pad = true;
|
||||
}
|
||||
}
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
if (!m_custom_bindings && control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
if (!m_custom_pad && control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(id, QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setSpacing(10);
|
||||
auto* title = new QLabel(tr("Controller setup"), this);
|
||||
auto font = title->font();
|
||||
font.setPointSize(font.pointSize() + 7);
|
||||
font.setBold(true);
|
||||
title->setFont(font);
|
||||
layout->addWidget(title);
|
||||
auto* hint = new QLabel(
|
||||
tr("Select a button to remap. Press controller buttons to test."), this);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
m_status = new QLabel(this);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
|
||||
auto* scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* grid = new QGridLayout(content);
|
||||
grid->setContentsMargins(0, 0, 0, 0);
|
||||
grid->setSpacing(12);
|
||||
QGroupBox* groups[GroupCount];
|
||||
QGridLayout* cells[GroupCount];
|
||||
const char* titles[] = {"D-Pad",
|
||||
"Left stick",
|
||||
"Right stick",
|
||||
"Face buttons",
|
||||
"Shoulders and triggers",
|
||||
"Touch pad and options",
|
||||
"In-game shortcuts (cycle)"};
|
||||
for (int index = 0; index < GroupCount; ++index) {
|
||||
groups[index] = new QGroupBox(tr(titles[index]), content);
|
||||
cells[index] = new QGridLayout(groups[index]);
|
||||
cells[index]->setSpacing(5);
|
||||
}
|
||||
|
||||
m_bindings = ParseMapping(mapping);
|
||||
m_custom_bindings = !m_bindings.isEmpty();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
auto* cell = new QGroupBox(tr(control.label), groups[control.group]);
|
||||
auto* cell_layout = new QVBoxLayout(cell);
|
||||
cell_layout->setContentsMargins(3, 5, 3, 3);
|
||||
auto* button = new QPushButton(cell);
|
||||
button->setObjectName(QStringLiteral("binding_") + id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(64);
|
||||
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
button->setAccessibleName(tr(titles[control.group]) + QStringLiteral(" / ") +
|
||||
tr(control.label));
|
||||
cell_layout->addWidget(button);
|
||||
cells[control.group]->addWidget(cell, control.row, control.column);
|
||||
m_buttons.insert(id, button);
|
||||
if (!m_custom_bindings) {
|
||||
m_bindings.insert(id, QString::fromLatin1(control.default_binding));
|
||||
}
|
||||
connect(button, &QPushButton::clicked, this, [this, id] { ChangeBinding(id); });
|
||||
}
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
left->addWidget(groups[Dpad]);
|
||||
left->addWidget(groups[LeftStick]);
|
||||
left->addStretch();
|
||||
grid->addLayout(left, 0, 0);
|
||||
auto* center = new QVBoxLayout;
|
||||
center->addWidget(groups[Shoulders]);
|
||||
m_controller = new DualSenseWidget(content);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
center->addWidget(m_controller, 1);
|
||||
center->addWidget(groups[Center]);
|
||||
grid->addLayout(center, 0, 1);
|
||||
auto* right = new QVBoxLayout;
|
||||
right->addWidget(groups[Face]);
|
||||
right->addWidget(groups[RightStick]);
|
||||
right->addStretch();
|
||||
grid->addLayout(right, 0, 2);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->addWidget(groups[Shortcuts], 1, 0, 1, 3);
|
||||
scroll->setWidget(content);
|
||||
layout->addWidget(scroll, 1);
|
||||
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), this));
|
||||
m_sensitivity = new QDoubleSpinBox(this);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_tabs = new QTabWidget(this);
|
||||
m_tabs->setObjectName(QStringLiteral("inputTabs"));
|
||||
m_tabs->setDocumentMode(true);
|
||||
m_tabs->addTab(BuildTab(true), tr("Gamepad"));
|
||||
m_tabs->addTab(BuildTab(false), tr("Keyboard && Mouse"));
|
||||
layout->addWidget(m_tabs, 1);
|
||||
m_sensitivity->setValue(ParseMouseSensitivity(mapping));
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), this));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
auto* touch_group = new QGroupBox(tr("Emulated touch pad (shared by both tabs)"), this);
|
||||
auto* touch_layout = new QHBoxLayout(touch_group);
|
||||
touch_layout->addWidget(new QLabel(tr("Particle skin"), touch_group));
|
||||
m_particle_skin = new QComboBox(touch_group);
|
||||
m_particle_skin->setObjectName(QStringLiteral("touchParticleSkin"));
|
||||
m_particle_skin->addItem(tr("Classic"), QStringLiteral("Classic"));
|
||||
m_particle_skin->addItem(tr("Blue neon"), QStringLiteral("BlueNeon"));
|
||||
m_particle_skin->addItem(tr("Monochrome"), QStringLiteral("Monochrome"));
|
||||
m_particle_skin->addItem(tr("Crimson"), QStringLiteral("Crimson"));
|
||||
m_particle_skin->addItem(tr("Red/Blue"), QStringLiteral("RedBlue"));
|
||||
touch_layout->addWidget(m_particle_skin);
|
||||
m_touch_cursor = new QCheckBox(tr("Show hand cursor and trail"), touch_group);
|
||||
m_touch_cursor->setObjectName(QStringLiteral("touchCursorImage"));
|
||||
m_touch_cursor->setToolTip(tr("When off, the emulated touch pad still works but the hand "
|
||||
"image and drawing trail are not shown."));
|
||||
m_touch_cursor->setChecked(!mapping.contains(QLatin1String(TOUCH_CURSOR_OFF)));
|
||||
touch_layout->addWidget(m_touch_cursor);
|
||||
touch_layout->addStretch();
|
||||
touch_layout->addWidget(new QLabel(
|
||||
tr("Toggle with the touch pad mode key (F6 by default); draw with left mouse / Cross."),
|
||||
touch_group));
|
||||
layout->addWidget(touch_group);
|
||||
const auto skin_index = m_particle_skin->findData(ParseTouchParticleSkin(mapping));
|
||||
m_particle_skin->setCurrentIndex(skin_index >= 0 ? skin_index : 0);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults |
|
||||
QDialogButtonBox::Save | QDialogButtonBox::Cancel,
|
||||
@@ -338,9 +419,14 @@ InputMappingDialog::InputMappingDialog(const QStringList& mapping, ControllerPre
|
||||
auto* defaults = buttons->button(QDialogButtonBox::RestoreDefaults);
|
||||
defaults->setObjectName(QStringLiteral("defaults"));
|
||||
defaults->setAutoDefault(false);
|
||||
layout->addWidget(buttons);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
&InputMappingDialog::ChangeBinding);
|
||||
defaults->setToolTip(tr("Restore the defaults of the current tab."));
|
||||
auto* tip = new QLabel(tr("Tip: right-click a binding to unmap it."), this);
|
||||
auto* footer = new QHBoxLayout;
|
||||
footer->addWidget(tip);
|
||||
footer->addStretch();
|
||||
footer->addWidget(buttons);
|
||||
layout->addLayout(footer);
|
||||
|
||||
connect(defaults, &QPushButton::clicked, this, &InputMappingDialog::RestoreDefaults);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
@@ -359,6 +445,128 @@ InputMappingDialog::~InputMappingDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
QWidget* InputMappingDialog::BuildTab(bool pad) {
|
||||
auto* page = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(page);
|
||||
layout->setSpacing(10);
|
||||
|
||||
auto* hint = new QLabel(
|
||||
pad ? tr("Choose which button on your gamepad triggers each PS5 control. Click a "
|
||||
"binding, then press the button or trigger you want to use.")
|
||||
: tr("Choose which keyboard key or mouse button triggers each PS5 control. Click a "
|
||||
"binding, then press the key or mouse button."),
|
||||
page);
|
||||
hint->setWordWrap(true);
|
||||
layout->addWidget(hint);
|
||||
if (pad) {
|
||||
m_status = new QLabel(page);
|
||||
m_status->setObjectName(QStringLiteral("controllerStatus"));
|
||||
m_status->setWordWrap(true);
|
||||
layout->addWidget(m_status);
|
||||
}
|
||||
|
||||
auto* body = new QHBoxLayout;
|
||||
body->setSpacing(14);
|
||||
layout->addLayout(body, 1);
|
||||
|
||||
auto* scroll = new QScrollArea(page);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
auto* content = new QWidget(scroll);
|
||||
auto* columns = new QGridLayout(content);
|
||||
columns->setContentsMargins(0, 0, 8, 0);
|
||||
columns->setSpacing(12);
|
||||
columns->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Section order and the column each one lives in.
|
||||
static constexpr Section ORDER[] = {Face, Dpad, Shoulders, Sticks, System, LeftStick,
|
||||
RightStick, Shortcuts};
|
||||
int column_rows[2] = {0, 0};
|
||||
for (const auto section: ORDER) {
|
||||
QGroupBox* group = nullptr;
|
||||
QGridLayout* rows = nullptr;
|
||||
int row = 0;
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.section != section || (pad && control.pad_default == nullptr)) {
|
||||
continue;
|
||||
}
|
||||
if (group == nullptr) {
|
||||
group = new QGroupBox(tr(SECTION_TITLES[section]), content);
|
||||
rows = new QGridLayout(group);
|
||||
rows->setHorizontalSpacing(12);
|
||||
rows->setVerticalSpacing(6);
|
||||
rows->setColumnStretch(0, 1);
|
||||
rows->setColumnMinimumWidth(1, 190);
|
||||
}
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto label = !pad && id == QLatin1String("TouchPad") ? tr("Touch pad left")
|
||||
: tr(control.label);
|
||||
auto* name = new QLabel(label, group);
|
||||
auto* button = new QPushButton(group);
|
||||
button->setObjectName((pad ? QStringLiteral("padBinding_") : QStringLiteral("binding_")) +
|
||||
id);
|
||||
button->setAutoDefault(false);
|
||||
button->setMinimumWidth(190);
|
||||
button->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
button->setAccessibleName(tr(SECTION_TITLES[section]) + QStringLiteral(" / ") + label);
|
||||
rows->addWidget(name, row, 0);
|
||||
rows->addWidget(button, row, 1);
|
||||
++row;
|
||||
(pad ? m_pad_buttons : m_buttons).insert(id, button);
|
||||
connect(button, &QPushButton::clicked, this, [this, pad, id] {
|
||||
pad ? ChangePadBinding(id) : ChangeKeyboardBinding(id);
|
||||
});
|
||||
connect(button, &QPushButton::customContextMenuRequested, this,
|
||||
[this, pad, id] { ClearBinding(pad, id); });
|
||||
}
|
||||
if (group != nullptr) {
|
||||
const int target = column_rows[0] <= column_rows[1] ? 0 : 1;
|
||||
columns->addWidget(group, column_rows[target]++, target);
|
||||
}
|
||||
}
|
||||
columns->setColumnStretch(0, 1);
|
||||
columns->setColumnStretch(1, 1);
|
||||
scroll->setWidget(content);
|
||||
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll->setMinimumWidth(pad ? 640 : 700);
|
||||
body->addWidget(scroll, 3);
|
||||
|
||||
if (pad) {
|
||||
m_controller = new DualSenseWidget(page);
|
||||
m_controller->setObjectName(QStringLiteral("controllerDiagram"));
|
||||
m_controller->setMinimumWidth(320);
|
||||
body->addWidget(m_controller, 2);
|
||||
connect(m_controller, &DualSenseWidget::ControlClicked, this,
|
||||
[this](const QString& id) {
|
||||
ChangePadBinding(id == QLatin1String("TouchPadRight") ? QStringLiteral("TouchPad")
|
||||
: id);
|
||||
});
|
||||
auto* note = new QLabel(tr("The gyro emulation toggle activates tilt mode on gamepads "
|
||||
"without a gyroscope: the right stick then controls pitch and "
|
||||
"roll. It defaults to the View/Back button."),
|
||||
page);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
} else {
|
||||
auto* sensitivity_layout = new QHBoxLayout;
|
||||
sensitivity_layout->addWidget(new QLabel(tr("Mouse sensitivity"), page));
|
||||
m_sensitivity = new QDoubleSpinBox(page);
|
||||
m_sensitivity->setObjectName(QStringLiteral("mouseSensitivity"));
|
||||
m_sensitivity->setRange(0.1, 5.0);
|
||||
m_sensitivity->setSingleStep(0.1);
|
||||
m_sensitivity->setDecimals(1);
|
||||
m_sensitivity->setSuffix(QStringLiteral("×"));
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
sensitivity_layout->addWidget(m_sensitivity);
|
||||
sensitivity_layout->addWidget(
|
||||
new QLabel(tr("F7 in-game toggles mouse movement on the right stick."), page));
|
||||
sensitivity_layout->addStretch();
|
||||
layout->addLayout(sensitivity_layout);
|
||||
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
QStringList InputMappingDialog::Mapping() const {
|
||||
QStringList result;
|
||||
if (m_custom_bindings) {
|
||||
@@ -369,15 +577,38 @@ QStringList InputMappingDialog::Mapping() const {
|
||||
result.append(id + QLatin1Char('=') + binding);
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.append(QLatin1String(KEYBOARD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_custom_pad) {
|
||||
const auto start = result.size();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
const auto id = QString::fromLatin1(control.id);
|
||||
const auto source = m_pad_bindings.value(id);
|
||||
if (!source.isEmpty()) {
|
||||
result.append(QLatin1String(PAD_PREFIX) + id + QLatin1Char('=') + source);
|
||||
}
|
||||
}
|
||||
if (result.size() == start) {
|
||||
result.append(QLatin1String(PAD_DISABLED));
|
||||
}
|
||||
}
|
||||
if (m_sensitivity->value() != DEFAULT_MOUSE_SENSITIVITY) {
|
||||
result.append(QLatin1String(MOUSE_SENSITIVITY) +
|
||||
QString::number(m_sensitivity->value(), 'f', 1));
|
||||
}
|
||||
const auto skin = m_particle_skin->currentData().toString();
|
||||
if (skin != QLatin1String("Classic")) {
|
||||
result.append(QLatin1String(TOUCH_PARTICLE_SKIN) + skin);
|
||||
}
|
||||
if (!m_touch_cursor->isChecked()) {
|
||||
result.append(QLatin1String(TOUCH_CURSOR_OFF));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
void InputMappingDialog::ChangeKeyboardBinding(const QString& id) {
|
||||
if (!m_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
@@ -393,14 +624,54 @@ void InputMappingDialog::ChangeBinding(const QString& id) {
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.default_binding));
|
||||
void InputMappingDialog::ChangePadBinding(const QString& id) {
|
||||
if (!m_pad_buttons.contains(id)) {
|
||||
return;
|
||||
}
|
||||
m_selected = id;
|
||||
UpdateHighlights();
|
||||
PadCaptureDialog dialog(m_preview, this);
|
||||
dialog.setWindowTitle(tr("Set binding — %1").arg(m_pad_buttons.value(id)->accessibleName()));
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
AssignBinding(m_pad_bindings, id, dialog.Binding());
|
||||
m_custom_pad = true;
|
||||
}
|
||||
m_selected.clear();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::ClearBinding(bool pad, const QString& id) {
|
||||
if (pad) {
|
||||
m_pad_bindings.remove(id);
|
||||
m_custom_pad = true;
|
||||
} else {
|
||||
m_bindings.remove(id);
|
||||
m_custom_bindings = true;
|
||||
}
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
void InputMappingDialog::RestoreDefaults() {
|
||||
if (m_tabs->currentIndex() == 0) {
|
||||
m_pad_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.pad_default != nullptr) {
|
||||
m_pad_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.pad_default));
|
||||
}
|
||||
}
|
||||
m_custom_pad = false;
|
||||
} else {
|
||||
m_bindings.clear();
|
||||
for (const auto& control: PAD_CONTROLS) {
|
||||
if (control.keyboard_default[0] != '\0') {
|
||||
m_bindings.insert(QString::fromLatin1(control.id),
|
||||
QString::fromLatin1(control.keyboard_default));
|
||||
}
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
}
|
||||
m_sensitivity->setValue(DEFAULT_MOUSE_SENSITIVITY);
|
||||
m_custom_bindings = false;
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
@@ -410,14 +681,42 @@ void InputMappingDialog::UpdateButtons() {
|
||||
const auto binding = m_bindings.value(item.key());
|
||||
const auto label = binding.isEmpty() ? tr("None") : binding;
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(tr("%1: %2. Click to change.").arg(button->accessibleName(), label));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
auto* button = item.value();
|
||||
const auto source = m_pad_bindings.value(item.key());
|
||||
const auto label = source.isEmpty() ? tr("None") : PadSourceLabel(source);
|
||||
button->setText(QString(label).replace(QLatin1Char('&'), QLatin1String("&&")));
|
||||
button->setToolTip(
|
||||
tr("%1: %2. Click to change, right-click to clear.").arg(button->accessibleName(), label));
|
||||
}
|
||||
UpdateHighlights();
|
||||
}
|
||||
|
||||
void InputMappingDialog::UpdatePreview() {
|
||||
const auto pressed = m_preview != nullptr ? m_preview->Pressed() : QSet<QString>();
|
||||
m_controller->SetPressed(pressed);
|
||||
m_pad_pressed.clear();
|
||||
if (m_preview != nullptr) {
|
||||
const auto& raw = m_preview->RawPressed();
|
||||
for (auto item = m_pad_bindings.begin(); item != m_pad_bindings.end(); ++item) {
|
||||
if (raw.contains(item.value())) {
|
||||
m_pad_pressed.insert(item.key());
|
||||
}
|
||||
}
|
||||
if (m_pad_pressed.contains(QStringLiteral("TouchPad"))) {
|
||||
m_pad_pressed.insert(QStringLiteral("TouchPadRight"));
|
||||
}
|
||||
for (const auto& name: m_preview->Pressed()) {
|
||||
// Stick directions and touch pad fingers are shown as the pad reports them.
|
||||
if (name.startsWith(QLatin1String("LeftStick")) ||
|
||||
name.startsWith(QLatin1String("RightStick")) ||
|
||||
name.startsWith(QLatin1String("TouchPad"))) {
|
||||
m_pad_pressed.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_controller->SetPressed(m_pad_pressed);
|
||||
m_controller->SetSticks(m_preview != nullptr ? m_preview->LeftStick() : QPointF(),
|
||||
m_preview != nullptr ? m_preview->RightStick() : QPointF());
|
||||
UpdateHighlights();
|
||||
@@ -432,8 +731,8 @@ void InputMappingDialog::UpdatePreview() {
|
||||
|
||||
void InputMappingDialog::UpdateHighlights() {
|
||||
m_controller->SetSelected(m_selected);
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
const bool down = m_preview != nullptr && m_preview->Pressed().contains(item.key());
|
||||
for (auto item = m_pad_buttons.begin(); item != m_pad_buttons.end(); ++item) {
|
||||
const bool down = m_pad_pressed.contains(item.key());
|
||||
auto colors = palette();
|
||||
if (down) {
|
||||
colors.setColor(QPalette::Button, colors.color(QPalette::Highlight));
|
||||
@@ -446,4 +745,11 @@ void InputMappingDialog::UpdateHighlights() {
|
||||
item.value()->setPalette(colors);
|
||||
item.value()->setFont(label_font);
|
||||
}
|
||||
for (auto item = m_buttons.begin(); item != m_buttons.end(); ++item) {
|
||||
auto colors = palette();
|
||||
if (item.key() == m_selected) {
|
||||
colors.setColor(QPalette::ButtonText, colors.color(QPalette::Highlight));
|
||||
}
|
||||
item.value()->setPalette(colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "configurationListWidget.h"
|
||||
#include "controllerPreview.h"
|
||||
#include "gameContent.h"
|
||||
#include "titleSound.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -90,6 +91,7 @@ private:
|
||||
MainDialog* m_main_dialog = nullptr;
|
||||
UpdateChecker* m_update_checker = nullptr;
|
||||
ControllerPreview m_controller_preview;
|
||||
TitleSound m_title_sound;
|
||||
QString m_interpreter;
|
||||
|
||||
QProcess m_process;
|
||||
@@ -124,7 +126,13 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
|
||||
|
||||
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
|
||||
Qt::QueuedConnection);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
|
||||
connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() {
|
||||
Update();
|
||||
const auto* item = m_ui->widget->GetSelectedItem();
|
||||
m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr
|
||||
? item->GetInfo().basedir
|
||||
: QString());
|
||||
});
|
||||
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
|
||||
[this](const QString& color) {
|
||||
if (m_process.state() == QProcess::NotRunning) {
|
||||
@@ -530,6 +538,7 @@ void MainDialogPrivate::Run() {
|
||||
|
||||
m_running_item->SetRunning(true);
|
||||
m_controller_preview.SetSuspended(true);
|
||||
m_title_sound.SetTitle({});
|
||||
|
||||
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
|
||||
m_main_dialog->RunInterpreter(&m_process, *info);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "titleSound.h"
|
||||
|
||||
#include "gameContent.h"
|
||||
#include "libs/ajm/atrac9_decoder.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
|
||||
TitleSound::Sound TitleSound::Decode(const QByteArray& data, const QPromise<Sound>& promise) {
|
||||
using namespace Libs::Audio::Ajm;
|
||||
AjmAt9Decoder decoder(0, 0, AjmSampleEncoding::Float,
|
||||
AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER);
|
||||
AjmGaplessState gapless;
|
||||
std::array<float, 16 * 1024> batch;
|
||||
Sound sound;
|
||||
size_t offset = 0;
|
||||
while (offset < static_cast<size_t>(data.size()) && !gapless.IsEnd()) {
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto result = decoder.Decode(data.constData() + offset, data.size() - offset,
|
||||
batch.data(), sizeof(batch), true, &gapless);
|
||||
if ((result.result != 0 && result.result != AJM_RESULT_NOT_ENOUGH_ROOM) ||
|
||||
result.input_consumed == 0) {
|
||||
return {};
|
||||
}
|
||||
const auto samples = result.output_written / sizeof(float);
|
||||
constexpr size_t MaxSamples = (128u << 20u) / sizeof(float);
|
||||
if (samples > MaxSamples - sound.pcm.size()) {
|
||||
return {};
|
||||
}
|
||||
sound.pcm.insert(sound.pcm.end(), batch.begin(), batch.begin() + samples);
|
||||
offset += result.input_consumed;
|
||||
}
|
||||
if (promise.isCanceled()) {
|
||||
return {};
|
||||
}
|
||||
const auto format = decoder.GetFormat();
|
||||
sound.channels = static_cast<int>(format.channel_num);
|
||||
sound.rate = static_cast<int>(format.sampling_frequency);
|
||||
return sound;
|
||||
}
|
||||
|
||||
TitleSound::TitleSound(QObject* parent): QObject(parent) {
|
||||
m_decode_pool.setMaxThreadCount(1);
|
||||
connect(&m_decode, &QFutureWatcher<Sound>::finished, this, [this] {
|
||||
if (m_decode.isCanceled() || m_decode.future().resultCount() == 0) {
|
||||
return;
|
||||
}
|
||||
auto sound = m_decode.future().takeResult();
|
||||
if (sound.pcm.empty()) {
|
||||
qWarning() << "Cannot decode title sound:" << m_base;
|
||||
return;
|
||||
}
|
||||
m_audio_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
if (m_audio_initialized) {
|
||||
const SDL_AudioSpec spec {SDL_AUDIO_F32, sound.channels, sound.rate};
|
||||
m_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr,
|
||||
nullptr);
|
||||
}
|
||||
if (m_stream == nullptr ||
|
||||
!SDL_PutAudioStreamData(m_stream, sound.pcm.data(),
|
||||
static_cast<int>(sound.pcm.size() * sizeof(float))) ||
|
||||
!SDL_FlushAudioStream(m_stream) || !SDL_ResumeAudioStreamDevice(m_stream)) {
|
||||
qWarning() << "Cannot play title sound:" << SDL_GetError();
|
||||
Stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TitleSound::~TitleSound() {
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
}
|
||||
|
||||
void TitleSound::SetTitle(const QString& base) {
|
||||
if (base == m_base) {
|
||||
return;
|
||||
}
|
||||
m_decode.cancel();
|
||||
Stop();
|
||||
m_base = base;
|
||||
if (!base.isEmpty()) {
|
||||
m_decode.setFuture(QtConcurrent::run(&m_decode_pool, [base](QPromise<Sound>& promise) {
|
||||
const auto data =
|
||||
GameContent::ReadFile(base, QStringLiteral("sce_sys/snd0.at9"), 32u << 20u);
|
||||
if (!data.isEmpty() && !promise.isCanceled()) {
|
||||
promise.addResult(Decode(data, promise));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void TitleSound::Stop() {
|
||||
if (m_stream != nullptr) {
|
||||
SDL_DestroyAudioStream(m_stream);
|
||||
m_stream = nullptr;
|
||||
}
|
||||
if (m_audio_initialized) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
m_audio_initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,11 @@ constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
QString LauncherRelativePath() {
|
||||
#if defined(_WIN32)
|
||||
return QStringLiteral("launcher.exe");
|
||||
return QStringLiteral("kyty_launcher.exe");
|
||||
#elif defined(__APPLE__)
|
||||
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
|
||||
#else
|
||||
return QStringLiteral("launcher");
|
||||
return QStringLiteral("kyty_launcher");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+101
-1
@@ -126,6 +126,7 @@ public:
|
||||
void Motion(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void SetMotionSensorState(bool enable);
|
||||
void ResetOrientation();
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
@@ -147,6 +148,8 @@ private:
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
|
||||
void UpdateEmulatedTilt();
|
||||
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
|
||||
void ApplyVibration();
|
||||
bool SendTriggerEffect(const PadTriggerEffectParam& param);
|
||||
@@ -160,6 +163,9 @@ private:
|
||||
int m_connected_count = 0;
|
||||
bool m_motion_enabled = true;
|
||||
uint64_t m_gyro_time = 0;
|
||||
bool m_gyro_emulation = false;
|
||||
uint64_t m_emulated_time = 0;
|
||||
std::array<int, 2> m_emulated_stick {128, 128};
|
||||
// Accelerometer gravity direction in the reset frame.
|
||||
std::array<float, 3> m_up_reference {0.0f, 1.0f, 0.0f};
|
||||
bool m_up_reference_valid = false;
|
||||
@@ -479,6 +485,9 @@ void GameController::CheckActive() {
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
m_gyro_emulation = false;
|
||||
m_emulated_time = 0;
|
||||
m_emulated_stick = {128, 128};
|
||||
m_up_reference_valid = false;
|
||||
m_states_num = 0;
|
||||
m_first_state = 0;
|
||||
@@ -524,6 +533,12 @@ void GameController::Axis(int id, Controller::Axis axis, int value) {
|
||||
|
||||
EXIT_IF(axis_id < 0 || axis_id >= static_cast<int>(Controller::Axis::AxisMax));
|
||||
|
||||
if (m_gyro_emulation && (axis == Controller::Axis::RightX || axis == Controller::Axis::RightY)) {
|
||||
m_emulated_stick[axis == Controller::Axis::RightY ? 1 : 0] = value;
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.axes[axis_id] = value;
|
||||
|
||||
uint32_t trigger = 0;
|
||||
@@ -545,7 +560,12 @@ void GameController::RightStick(int id, int x, int y) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {x, y};
|
||||
UpdateEmulatedTilt();
|
||||
return;
|
||||
}
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = x;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = y;
|
||||
AddState();
|
||||
@@ -677,6 +697,82 @@ void GameController::SetMotionSensorState(bool enable) {
|
||||
}
|
||||
}
|
||||
|
||||
static Quat QuatMultiply(const Quat& a, const Quat& b) {
|
||||
return {a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
||||
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
||||
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
||||
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]};
|
||||
}
|
||||
|
||||
void GameController::UpdateEmulatedTilt() {
|
||||
constexpr float max_angle = 0.7853982f; // 45 degrees at full stick deflection
|
||||
const auto angle = [](int value) {
|
||||
return std::clamp((value - 128) / 127.0f, -1.0f, 1.0f) * max_angle;
|
||||
};
|
||||
const float roll = angle(m_emulated_stick[0]);
|
||||
const float pitch = angle(m_emulated_stick[1]);
|
||||
|
||||
const Quat q_pitch {std::sin(pitch * 0.5f), 0.0f, 0.0f, std::cos(pitch * 0.5f)};
|
||||
const Quat q_roll {0.0f, 0.0f, std::sin(-roll * 0.5f), std::cos(-roll * 0.5f)};
|
||||
Quat q = QuatMultiply(q_roll, q_pitch);
|
||||
|
||||
const uint64_t now = LibKernel::KernelGetProcessTime();
|
||||
Vec3 rate {};
|
||||
if (m_emulated_time != 0 && now > m_emulated_time) {
|
||||
const float dt = std::clamp(static_cast<float>(now - m_emulated_time) * 0.000001f,
|
||||
0.001f, 0.1f);
|
||||
auto delta = QuatMultiply(QuatConjugate(m_state.orientation), q);
|
||||
if (delta[3] < 0.0f) {
|
||||
for (auto& v: delta) {
|
||||
v = -v;
|
||||
}
|
||||
}
|
||||
rate = {2.0f * delta[0] / dt, 2.0f * delta[1] / dt, 2.0f * delta[2] / dt};
|
||||
}
|
||||
m_emulated_time = now;
|
||||
|
||||
m_state.time = now;
|
||||
m_state.orientation = q;
|
||||
m_state.accel = QuatRotate(QuatConjugate(q), {0.0f, 1.0f, 0.0f});
|
||||
m_state.gyro = rate;
|
||||
AddState();
|
||||
// Angular velocity is only nonzero while the tilt changes.
|
||||
m_state.gyro = {};
|
||||
}
|
||||
|
||||
void GameController::ToggleGyroEmulation(int id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
if (id != HOST_INPUT_CONTROLLER_ID && id != m_active_id) {
|
||||
return;
|
||||
}
|
||||
// Pads with a real gyroscope report it directly.
|
||||
if (m_active_id != HOST_INPUT_CONTROLLER_ID) {
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
|
||||
if (pad != nullptr && SDL_GamepadHasSensor(pad, SDL_SENSOR_GYRO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_gyro_emulation = !m_gyro_emulation;
|
||||
m_emulated_time = 0;
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
if (m_gyro_emulation) {
|
||||
m_emulated_stick = {m_state.axes[static_cast<int>(Axis::RightX)],
|
||||
m_state.axes[static_cast<int>(Axis::RightY)]};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = 128;
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = 128;
|
||||
UpdateEmulatedTilt();
|
||||
} else {
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
m_state.accel = {0.0f, 1.0f, 0.0f};
|
||||
m_state.gyro = {};
|
||||
m_state.axes[static_cast<int>(Axis::RightX)] = m_emulated_stick[0];
|
||||
m_state.axes[static_cast<int>(Axis::RightY)] = m_emulated_stick[1];
|
||||
AddState();
|
||||
}
|
||||
LOGF("Emulated gyro tilt: %s\n", m_gyro_emulation ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void GameController::ResetOrientation() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
m_state.orientation = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
@@ -931,6 +1027,10 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
|
||||
void ToggleGyroEmulation(int id) {
|
||||
g_controller->ToggleGyroEmulation(id);
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y);
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
|
||||
|
||||
@@ -326,6 +326,33 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
|
||||
if (frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (!stream->speaker &&
|
||||
SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
|
||||
CloseDevice(stream);
|
||||
stream->next_check = 0;
|
||||
// Non-DualSense pads have no haptic audio path: approximate it with the rumble motors.
|
||||
float peak[2] = {0.0f, 0.0f};
|
||||
for (uint32_t frame = 0; frame < frames; frame++) {
|
||||
for (uint32_t ch = 0; ch < 2; ch++) {
|
||||
const auto src_ch = channels == 1 ? 0 : ch;
|
||||
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
|
||||
float value = is_float ? static_cast<const float*>(data)[index]
|
||||
: static_cast<const int16_t*>(data)[index] / 32768.0f;
|
||||
value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f);
|
||||
peak[ch] = std::max(peak[ch], std::fabs(value));
|
||||
}
|
||||
}
|
||||
const auto to_motor = [](float v) {
|
||||
return static_cast<uint16_t>(std::clamp(v * 2.0f, 0.0f, 1.0f) * 65535.0f);
|
||||
};
|
||||
SDL_LockJoysticks();
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
pad != nullptr) {
|
||||
(void) SDL_RumbleGamepad(pad, to_motor(peak[0]), to_motor(peak[1]), 100);
|
||||
}
|
||||
SDL_UnlockJoysticks();
|
||||
return static_cast<uint64_t>(frames) * 1000000 / stream->freq;
|
||||
}
|
||||
const bool wireless = IsWireless(controller);
|
||||
if (!wireless && !CanUseDevice(controller)) {
|
||||
CloseDevice(stream);
|
||||
|
||||
+70
-8
@@ -4,7 +4,9 @@
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
@@ -56,8 +58,16 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
bytes += size;
|
||||
arguments.emplace_back(argv[i], size - 1);
|
||||
}
|
||||
LOGF("LoadExec: %s\n", path);
|
||||
// Shown on the console too: the output that follows comes from the new program.
|
||||
auto message = fmt::format("LoadExec: switching to {}", path);
|
||||
for (size_t i = 2; i < arguments.size(); ++i) {
|
||||
message += fmt::format(" \"{}\"", arguments[i]);
|
||||
}
|
||||
Log::WriteToConsoleAndLog(message + "\n");
|
||||
// The new program writes to the same outputs, so everything written so far goes out first.
|
||||
// Neither exec nor quick_exit writes out buffered streams.
|
||||
Log::Flush();
|
||||
std::fflush(nullptr);
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
wchar_t executable[32768] {};
|
||||
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
|
||||
@@ -88,17 +98,57 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
wide.resize(size - 1);
|
||||
command += L" " + QuoteArgument(wide);
|
||||
}
|
||||
STARTUPINFOW startup {};
|
||||
startup.cb = sizeof(startup);
|
||||
// The new process writes where this one does: to its console, or to the files or pipes its
|
||||
// output was redirected to. Only the standard handles are inherited.
|
||||
HANDLE std_handles[3] = {GetStdHandle(STD_INPUT_HANDLE), GetStdHandle(STD_OUTPUT_HANDLE),
|
||||
GetStdHandle(STD_ERROR_HANDLE)};
|
||||
std::vector<HANDLE> inherited;
|
||||
for (auto& handle: std_handles) {
|
||||
if (handle == nullptr || handle == INVALID_HANDLE_VALUE ||
|
||||
!SetHandleInformation(handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) {
|
||||
handle = nullptr;
|
||||
} else if (std::find(inherited.begin(), inherited.end(), handle) == inherited.end()) {
|
||||
inherited.push_back(handle);
|
||||
}
|
||||
}
|
||||
SIZE_T attributes_size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributes_size);
|
||||
std::vector<uint8_t> attributes_storage(attributes_size);
|
||||
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes_storage.data());
|
||||
const bool inherit =
|
||||
!inherited.empty() && InitializeProcThreadAttributeList(attributes, 1, 0, &attributes_size);
|
||||
const bool listed =
|
||||
inherit && UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherited.data(), inherited.size() * sizeof(HANDLE),
|
||||
nullptr, nullptr);
|
||||
STARTUPINFOEXW startup {};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
if (listed) {
|
||||
startup.lpAttributeList = attributes;
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = std_handles[0];
|
||||
startup.StartupInfo.hStdOutput = std_handles[1];
|
||||
startup.StartupInfo.hStdError = std_handles[2];
|
||||
}
|
||||
// A process with a console shares it with the new one; without one, none is created.
|
||||
const DWORD flags = (listed ? EXTENDED_STARTUPINFO_PRESENT : 0u) |
|
||||
(GetConsoleWindow() != nullptr ? 0u : static_cast<DWORD>(CREATE_NO_WINDOW));
|
||||
// The new process continues this one's log file; it inherits the environment.
|
||||
(void)SetEnvironmentVariableA(Log::RelaunchedVariable, "1");
|
||||
PROCESS_INFORMATION process {};
|
||||
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, nullptr, &startup, &process)) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
|
||||
const bool created =
|
||||
CreateProcessW(executable, command.data(), nullptr, nullptr, listed ? TRUE : FALSE, flags,
|
||||
nullptr, nullptr, &startup.StartupInfo, &process) != 0;
|
||||
const auto error = GetLastError();
|
||||
if (inherit) {
|
||||
DeleteProcThreadAttributeList(attributes);
|
||||
}
|
||||
if (!created) {
|
||||
LOGF("LoadExec: process creation failed: %lu\n", error);
|
||||
return SYSTEM_SERVICE_ERROR_INTERNAL;
|
||||
}
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
std::fflush(nullptr);
|
||||
std::quick_exit(0);
|
||||
#else
|
||||
std::ifstream command("/proc/self/cmdline", std::ios::binary);
|
||||
@@ -116,8 +166,20 @@ int LoadExec(const char* path, const char* const argv[]) {
|
||||
host_argv.push_back(argument.data());
|
||||
}
|
||||
host_argv.push_back(nullptr);
|
||||
// The new program continues this one's log file. It is given its environment explicitly:
|
||||
// setenv is not safe while other threads may read the environment.
|
||||
const std::string relaunched = std::string(Log::RelaunchedVariable) + "=";
|
||||
std::string relaunched_entry = relaunched + "1";
|
||||
std::vector<char*> environment;
|
||||
for (char** variable = environ; variable != nullptr && *variable != nullptr; ++variable) {
|
||||
if (!std::string_view(*variable).starts_with(relaunched)) {
|
||||
environment.push_back(*variable);
|
||||
}
|
||||
}
|
||||
environment.push_back(relaunched_entry.data());
|
||||
environment.push_back(nullptr);
|
||||
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
|
||||
execvp(host_argv[0], host_argv.data());
|
||||
execvpe(host_argv[0], host_argv.data(), environment.data());
|
||||
}
|
||||
LOGF("LoadExec failed: %s\n", std::strerror(errno));
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
// Exercise production host input with deterministic gamepads and guest output.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "HostInputTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Sint16, 2> physical_stick {};
|
||||
std::array<int, 2> guest_stick {128, 128};
|
||||
uint32_t guest_buttons = 0;
|
||||
bool guest_touch = false;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
Sint16 GetGamepadAxis(SDL_Gamepad*, SDL_GamepadAxis axis) {
|
||||
return physical_stick[axis == SDL_GAMEPAD_AXIS_LEFTY ? 1 : 0];
|
||||
}
|
||||
Uint64 GetTicks() {
|
||||
return 1000;
|
||||
}
|
||||
bool HideCursor() {
|
||||
return true;
|
||||
}
|
||||
bool ShowCursor() {
|
||||
return true;
|
||||
}
|
||||
SDL_MouseButtonFlags GetMouseState(float* x, float* y) {
|
||||
*x = *y = 0.0f;
|
||||
return 0;
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_GetGamepadAxis Fake::GetGamepadAxis
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_HideCursor Fake::HideCursor
|
||||
#define SDL_ShowCursor Fake::ShowCursor
|
||||
#define SDL_GetMouseState Fake::GetMouseState
|
||||
#include "graphics/presentation/window/hostInput.cpp"
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_GetGamepadAxis
|
||||
#undef SDL_GetTicks
|
||||
#undef SDL_HideCursor
|
||||
#undef SDL_ShowCursor
|
||||
#undef SDL_GetMouseState
|
||||
|
||||
namespace Libs::Controller {
|
||||
void SetButton(int id, uint32_t button, bool down) {
|
||||
if (id == 1) {
|
||||
guest_buttons = down ? guest_buttons | button : guest_buttons & ~button;
|
||||
}
|
||||
}
|
||||
void SetAxis(int id, Axis axis, int value) {
|
||||
if (id == 1 && (axis == Axis::LeftX || axis == Axis::LeftY)) {
|
||||
guest_stick[axis == Axis::LeftY ? 1 : 0] = value;
|
||||
}
|
||||
}
|
||||
void SetTouchPad(int id, int finger, bool down, float, float) {
|
||||
if (id == 1 && finger == 1) {
|
||||
guest_touch = down;
|
||||
}
|
||||
}
|
||||
void SetRightStick(int, int, int) {}
|
||||
void ToggleGyroEmulation(int) {}
|
||||
void CycleSetting(Setting) {}
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Graphics;
|
||||
using namespace Libs::Controller;
|
||||
|
||||
struct Inputs {
|
||||
Inputs() {
|
||||
physical_stick = {};
|
||||
guest_stick = {128, 128};
|
||||
guest_buttons = 0;
|
||||
guest_touch = false;
|
||||
HostInputInit(nullptr);
|
||||
}
|
||||
~Inputs() { HostInputShutdown(); }
|
||||
};
|
||||
|
||||
void TestTouchPadStickHandoff() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MAX, SDL_JOYSTICK_AXIS_MIN};
|
||||
guest_stick = {255, 0};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {128, 128}, "entering touchpad mode retained guest movement");
|
||||
PollGamepadTouchPad(1008);
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
Check(cursor.x > 0.5f && cursor.y < 0.5f,
|
||||
"entering touchpad mode did not sample an already-held stick");
|
||||
Check(HostInputGamepadAxis(1, true, 127) && HostInputGamepadAxis(1, false, 127),
|
||||
"touchpad mode did not capture stick releases");
|
||||
Check(guest_stick == std::array {128, 128}, "touchpad movement leaked to guest axes");
|
||||
// Sample current physical axes on exit even if an event was consumed elsewhere.
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(guest_stick == std::array {0, 255}, "leaving touchpad mode did not restore current axes");
|
||||
Check(!HostInputGamepadAxis(1, true, 127), "leaving touchpad mode retained stick capture");
|
||||
}
|
||||
|
||||
void TestKeyboardSwitchesFromGamepadTouchPad() {
|
||||
Inputs inputs;
|
||||
physical_stick = {SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX};
|
||||
HostInputToggleGamepadTouchPad(1);
|
||||
Check(HostInputGamepadDrawButton(1, true) && guest_touch, "gamepad drawing did not start");
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(GetHostInputTouchPadVisualState().enabled && !HostInputGamepadAxis(1, true, 127),
|
||||
"F6 failed to switch from gamepad to mouse touchpad mode");
|
||||
Check(!guest_touch && guest_stick == std::array {0, 255},
|
||||
"switching to mouse touchpad mode retained gamepad input capture");
|
||||
HostInputKey(SDLK_F6, false);
|
||||
HostInputKey(SDLK_F6, true);
|
||||
Check(!GetHostInputTouchPadVisualState().enabled, "F6 failed to disable mouse touchpad mode");
|
||||
}
|
||||
|
||||
void TestIndependentTriggerEdges() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(2, true, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "an inactive pad suppressed the active trigger press");
|
||||
HostInputPadTrigger(2, true, 0);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == 0, "an inactive pad suppressed the active trigger release");
|
||||
HostInputPadTrigger(1, false, 255);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputPadTrigger(1, true, 0);
|
||||
Check(guest_buttons == PAD_BUTTON_CIRCLE, "left and right trigger edges were mixed");
|
||||
HostInputPadTrigger(1, false, 0);
|
||||
Check(guest_buttons == 0, "right trigger release was lost");
|
||||
}
|
||||
|
||||
void TestTriggerLifecycleReset() {
|
||||
Inputs inputs;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
HostInputDisconnectGamepad(1);
|
||||
guest_buttons = 0;
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "disconnect retained a previous trigger press");
|
||||
HostInputShutdown();
|
||||
guest_buttons = 0;
|
||||
HostInputInit(nullptr);
|
||||
HostInputPadTrigger(1, true, 255);
|
||||
Check(guest_buttons == PAD_BUTTON_CROSS, "shutdown retained a previous trigger press");
|
||||
}
|
||||
|
||||
void TestDisabledMappingTabs() {
|
||||
Config::ConfigOptions options;
|
||||
const auto south = InputMap::SourceFromButton(SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
Config::Load(options);
|
||||
const InputMap defaults;
|
||||
Check(!defaults.Custom() && defaults.PadControl(south) == ControlFromName("Cross"),
|
||||
"empty configuration lost its default mappings");
|
||||
options.keymap = {"Keyboard=Disabled"};
|
||||
Config::Load(options);
|
||||
const InputMap keyboard_disabled;
|
||||
Check(keyboard_disabled.Custom() && keyboard_disabled.FindKey(SDLK_J) == INVALID_CONTROL &&
|
||||
keyboard_disabled.PadControl(south) == ControlFromName("Cross"),
|
||||
"disabling keyboard mappings changed gamepad defaults or restored keyboard input");
|
||||
for (const auto& keymap: {Config::Keymap {"Pad=Disabled"},
|
||||
Config::Keymap {"Keyboard=Disabled", "Pad=Disabled"}}) {
|
||||
options.keymap = keymap;
|
||||
Config::Load(options);
|
||||
const InputMap disabled;
|
||||
Check(disabled.Custom() == (keymap.size() == 2),
|
||||
"disabling gamepad mappings changed keyboard defaults");
|
||||
for (std::size_t source = 0; source < PAD_SOURCES.size(); source++) {
|
||||
Check(disabled.PadControl(source) == INVALID_CONTROL,
|
||||
"a disabled gamepad mapping reverted to its default");
|
||||
}
|
||||
}
|
||||
options.keymap = {"Cross=J", "Pad:Circle=South"};
|
||||
Config::Load(options);
|
||||
const InputMap mapped;
|
||||
Check(mapped.Custom() && mapped.FindKey(SDLK_J) == ControlFromName("Cross") &&
|
||||
mapped.PadControl(south) == ControlFromName("Circle"),
|
||||
"explicit mappings no longer override defaults");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
Config::ConfigOptions options;
|
||||
options.keymap = {"Pad:Cross=LeftTrigger", "Pad:Circle=RightTrigger"};
|
||||
Config::Load(options);
|
||||
TestTouchPadStickHandoff();
|
||||
TestKeyboardSwitchesFromGamepadTouchPad();
|
||||
TestIndependentTriggerEdges();
|
||||
TestTriggerLifecycleReset();
|
||||
TestDisabledMappingTabs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ©);
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user