mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-29 00:33:06 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415387f00d | ||
|
|
890393fbe6 | ||
|
|
9f88043ebe | ||
|
|
c2173801d4 | ||
|
|
56d03f16e9 | ||
|
|
3ecc6b7d1b | ||
|
|
5bf37f52ad | ||
|
|
57791645e4 | ||
|
|
d1fe1324c8 | ||
|
|
b485dd24be | ||
|
|
ae25558321 | ||
|
|
ec08ea197b | ||
|
|
f6f1ac8b40 | ||
|
|
b82512be06 | ||
|
|
ba8b0c85b8 | ||
|
|
b6bd9ecae4 | ||
|
|
b95ea4bb5f | ||
|
|
e93b4b0378 | ||
|
|
0fd41ae206 | ||
|
|
9202bddad2 | ||
|
|
32587337b5 | ||
|
|
02ead0e2e1 | ||
|
|
4f1f3b1bb3 | ||
|
|
42bb0da520 | ||
|
|
5dec523143 | ||
|
|
d01633e18e | ||
|
|
e7ac242039 | ||
|
|
d7974dc763 | ||
|
|
b899717900 | ||
|
|
d58aefde49 | ||
|
|
4c6be6cd1d |
No files matched your search
+1
-1
@@ -350,7 +350,7 @@ set(inc_headers
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Tools/include
|
||||
${KYTY_THIRD_PARTY_DIR}/SPIRV-Headers/include
|
||||
${KYTY_THIRD_PARTY_DIR}/fmt/include
|
||||
${KYTY_THIRD_PARTY_DIR}/xxhash
|
||||
${KYTY_THIRD_PARTY_DIR}/xxHash
|
||||
${KYTY_THIRD_PARTY_DIR}/cpuinfo/include
|
||||
${KYTY_THIRD_PARTY_DIR}/renderdoc
|
||||
${KYTY_THIRD_PARTY_DIR}/stb
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "تم كسب الكأس!",
|
||||
"user.guest": "ضيف"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofej získána!",
|
||||
"user.guest": "Host"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofæ optjent!",
|
||||
"user.guest": "Gæst"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophäe erhalten!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Κερδίσατε ένα τρόπαιο!",
|
||||
"user.guest": "Επισκ."
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophy earned!",
|
||||
"user.guest": "Guest"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "¡Ganaste un trofeo!",
|
||||
"user.guest": "Invitado"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Pokaali ansaittu!",
|
||||
"user.guest": "Vieras"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trophée obtenu !",
|
||||
"user.guest": "Invité"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trófea megszerezve!",
|
||||
"user.guest": "Vendég"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofi diperoleh!",
|
||||
"user.guest": "Tamu"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeo ottenuto!",
|
||||
"user.guest": "Ospite"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "トロフィーを獲得しました!",
|
||||
"user.guest": "ゲスト"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "트로피를 획득했습니다!",
|
||||
"user.guest": "게스트"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofee behaald!",
|
||||
"user.guest": "Gast"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé opptjent!",
|
||||
"user.guest": "Gjest"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Zdobyto trofeum!",
|
||||
"user.guest": "Gość"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Troféu conquistado!",
|
||||
"user.guest": "Convidado"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofeu câștigat!",
|
||||
"user.guest": "Invitat"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Трофей получен!",
|
||||
"user.guest": "Гость"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Trofé intjänad!",
|
||||
"user.guest": "Gäst"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "ได้รับถ้วยรางวัลแล้ว!",
|
||||
"user.guest": "แขก"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Kupa kazanıldı!",
|
||||
"user.guest": "Misafir"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "Đã nhận cúp!",
|
||||
"user.guest": "Khách"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已获得奖杯!",
|
||||
"user.guest": "访客"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trophy.toast.earned": "已獲得獎盃!",
|
||||
"user.guest": "訪客"
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ void Load(const ConfigOptions& cfg) {
|
||||
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
|
||||
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
|
||||
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
|
||||
EXIT_IF(cfg.keyboard_player > static_cast<uint32_t>(MAX_PLAYERS));
|
||||
|
||||
*g_config = cfg;
|
||||
}
|
||||
@@ -60,6 +61,27 @@ uint32_t GetControllerVibrationIntensity() {
|
||||
return g_config->controller_vibration_intensity;
|
||||
}
|
||||
|
||||
uint32_t GetKeyboardPlayer() {
|
||||
return g_config->keyboard_player;
|
||||
}
|
||||
|
||||
int32_t GetPlayerUserId(int player) {
|
||||
return g_config->user_id + player * PLAYER_USER_ID_STEP;
|
||||
}
|
||||
|
||||
int GetPlayerFromUserId(int32_t user_id) {
|
||||
for (int player = 0; player < MAX_PLAYERS; player++) {
|
||||
if (user_id == GetPlayerUserId(player)) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool IsGuestUserId(int32_t user_id) {
|
||||
return GetPlayerFromUserId(user_id) > 0;
|
||||
}
|
||||
|
||||
PresentMode GetPresentMode() {
|
||||
return g_config->present_mode;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -35,10 +36,16 @@ constexpr uint32_t MAX_CONSOLE_LANGUAGE = 29;
|
||||
constexpr std::size_t MAX_USER_NAME_LENGTH = 16;
|
||||
constexpr int32_t DEFAULT_USER_ID = 1000;
|
||||
|
||||
constexpr int MAX_PLAYERS = 4;
|
||||
constexpr int32_t PLAYER_USER_ID_STEP = 0x100;
|
||||
constexpr int32_t MAX_USER_ID =
|
||||
std::numeric_limits<int32_t>::max() - (MAX_PLAYERS - 1) * PLAYER_USER_ID_STEP;
|
||||
|
||||
constexpr bool IsConfiguredUserIdValid(int32_t user_id) {
|
||||
constexpr int32_t USER_ID_EVERYONE = 0xfe;
|
||||
constexpr int32_t USER_ID_SYSTEM = 0xff;
|
||||
return user_id >= 0 && user_id != USER_ID_EVERYONE && user_id != USER_ID_SYSTEM;
|
||||
return user_id >= 0 && user_id <= MAX_USER_ID && user_id != USER_ID_EVERYONE &&
|
||||
user_id != USER_ID_SYSTEM;
|
||||
}
|
||||
|
||||
struct ConfigOptions {
|
||||
@@ -50,6 +57,8 @@ struct ConfigOptions {
|
||||
std::optional<ControllerColor> controller_color;
|
||||
uint32_t controller_speaker_volume = 50;
|
||||
uint32_t controller_vibration_intensity = 100;
|
||||
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
|
||||
uint32_t keyboard_player = 0;
|
||||
PresentMode present_mode = PresentMode::Mailbox;
|
||||
int32_t gpu_index = -1;
|
||||
bool fullscreen_enabled = false;
|
||||
@@ -100,6 +109,13 @@ const std::string& GetAudioInputDevice();
|
||||
const std::optional<ControllerColor>& GetControllerColor();
|
||||
uint32_t GetControllerSpeakerVolume();
|
||||
uint32_t GetControllerVibrationIntensity();
|
||||
uint32_t GetKeyboardPlayer();
|
||||
// Player 0 is the configured user; later players derive their IDs from it.
|
||||
int32_t GetPlayerUserId(int player);
|
||||
// Returns -1 when the user ID does not belong to any player.
|
||||
int GetPlayerFromUserId(int32_t user_id);
|
||||
// Players after the first are guests: they have no profile, trophies or NP account.
|
||||
bool IsGuestUserId(int32_t user_id);
|
||||
PresentMode GetPresentMode();
|
||||
int32_t GetGpuIndex();
|
||||
bool FullscreenEnabled();
|
||||
|
||||
@@ -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
|
||||
@@ -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 {}
|
||||
|
||||
@@ -311,7 +311,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
||||
}
|
||||
r.desc = MakeDepthTargetDesc(buffer, z);
|
||||
r.depth_clear_enable = rc.depth_clear_enable;
|
||||
r.depth_load_clear_enable = r.depth_clear_enable;
|
||||
r.depth_clear_value = hw.GetDepthClearValue();
|
||||
r.depth_test_enable = dc.z_enable;
|
||||
r.depth_write_enable = r.depth_test_enable && dc.z_write_enable &&
|
||||
@@ -421,9 +420,9 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
}
|
||||
|
||||
const auto available = ImageViewOps::DepthAspectMask(format);
|
||||
vk::ImageAspectFlags writes {};
|
||||
vk::ImageAspectFlags writes = load_clear_aspects & available;
|
||||
if ((available & vk::ImageAspectFlagBits::eDepth) &&
|
||||
(depth_load_clear_enable || depth_write_enable)) {
|
||||
depth_write_enable) {
|
||||
writes |= vk::ImageAspectFlagBits::eDepth;
|
||||
}
|
||||
if (!(available & vk::ImageAspectFlagBits::eStencil)) {
|
||||
@@ -461,8 +460,7 @@ vk::ImageAspectFlags RenderDepthInfo::AttachmentWriteAspects() const {
|
||||
(can_pass && depth_pass && state.passOp != vk::StencilOp::eKeep) ||
|
||||
(can_pass && depth_fail && state.depthFailOp != vk::StencilOp::eKeep);
|
||||
};
|
||||
if (stencil_clear_enable ||
|
||||
(stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back)))) {
|
||||
if (stencil_test_enable && (face_writes(stencil_front) || face_writes(stencil_back))) {
|
||||
writes |= vk::ImageAspectFlagBits::eStencil;
|
||||
}
|
||||
return writes;
|
||||
|
||||
@@ -22,7 +22,7 @@ struct RenderDepthInfo {
|
||||
// Discovery keeps guest image information but can remap the view into a larger cache image.
|
||||
TextureCache::ImageDesc desc;
|
||||
bool depth_clear_enable = false;
|
||||
bool depth_load_clear_enable = false;
|
||||
vk::ImageAspectFlags load_clear_aspects;
|
||||
float depth_clear_value = 0.0f;
|
||||
bool depth_test_enable = false;
|
||||
// Effective draw writes; discovery applies test, target-write and clear controls.
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "graphics/shader/recompiler/ShaderRecompiler.h"
|
||||
#include "graphics/shader/shaderCompiler.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kytyGitVersion.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -81,7 +80,8 @@ std::string DriverCacheSignature(const vk::PhysicalDeviceProperties& properties)
|
||||
uuid[i * 2] = hex[properties.pipelineCacheUUID[i] >> 4u];
|
||||
uuid[i * 2 + 1] = hex[properties.pipelineCacheUUID[i] & 0xfu];
|
||||
}
|
||||
return fmt::format("KytyPC1:{}:{:08x}:{:08x}:{:08x}:{}\n", KYTY_GIT_REVISION,
|
||||
// Driver cache entries already distinguish shader code and pipeline state across builds.
|
||||
return fmt::format("KytyPC2:{:08x}:{:08x}:{:08x}:{}\n",
|
||||
properties.vendorID, properties.deviceID, properties.driverVersion, uuid);
|
||||
}
|
||||
|
||||
@@ -516,17 +516,6 @@ void PipelineCache::InitializeDriverCache() {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (non-Release build)");
|
||||
return;
|
||||
}
|
||||
const std::string_view git_hash = KYTY_GIT_HASH;
|
||||
const std::string_view git_revision = KYTY_GIT_REVISION;
|
||||
if (git_hash == "unknown" || git_revision == "unknown") {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (unknown git revision)");
|
||||
return;
|
||||
}
|
||||
if (git_hash.ends_with("-dirty")) {
|
||||
PipelineCacheLog("Vulkan pipeline cache: disabled (dirty build)");
|
||||
return;
|
||||
}
|
||||
|
||||
m_driver_cache_path = std::filesystem::path("_PipelineCache") / (title_id + ".bin");
|
||||
const auto path = Common::PathToString(m_driver_cache_path);
|
||||
const bool cache_exists = Common::File::IsFileExisting(m_driver_cache_path);
|
||||
@@ -559,7 +548,7 @@ void PipelineCache::InitializeDriverCache() {
|
||||
XXH3_64bits(initial_data.data(), initial_data.size()) != payload_hash) {
|
||||
initial_data.clear();
|
||||
PipelineCacheLog(
|
||||
"Vulkan pipeline cache: invalidating {} (driver, emulator, or data mismatch)",
|
||||
"Vulkan pipeline cache: invalidating {} (driver or data mismatch)",
|
||||
path);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -94,9 +94,10 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
constexpr std::array formats {
|
||||
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
|
||||
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
|
||||
// Offset-or-swizzle bounds do not describe the element range proved by the shader.
|
||||
if (descriptor.Stride() != element_size || descriptor.Format() != formats[fill.words - 1] ||
|
||||
descriptor.SwizzleEnabled() || descriptor.IndexStride() != 0 || descriptor.AddTid() ||
|
||||
descriptor.Base48() == 0) {
|
||||
descriptor.OutOfBounds() == 3 || descriptor.Base48() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
|
||||
@@ -108,7 +109,11 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
|
||||
? group_x
|
||||
: static_cast<uint64_t>(group_x) * input.threads_num[0];
|
||||
const auto size = descriptor.GetSize();
|
||||
if (invocations != descriptor.NumRecords() || size == 0 || size > UINT32_MAX ||
|
||||
const auto elements = descriptor.NumRecords();
|
||||
const bool covered = fill.bounded_elements != 0
|
||||
? fill.bounded_elements == elements && invocations >= elements
|
||||
: invocations == elements;
|
||||
if (!covered || invocations > uint64_t {UINT32_MAX} + 1 || size == 0 || size > UINT32_MAX ||
|
||||
(input.dispatch_thread_dimensions &&
|
||||
(group_x % input.threads_num[0] != 0 || input.dispatch_threads_num[0] != group_x ||
|
||||
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
|
||||
@@ -252,10 +257,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
return;
|
||||
}
|
||||
|
||||
if (sh_ctx.GetCs().cs_regs.data_addr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr uint32_t DISPATCH_INITIATOR_USE_THREAD_DIMENSIONS = 1u << 5u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_BASE_BITS = 0x41u;
|
||||
constexpr uint32_t DISPATCH_INITIATOR_MODIFIER_BITS = 0xa038u;
|
||||
|
||||
@@ -90,6 +90,7 @@ bool RenderContext::IsMapped(uint64_t vaddr, uint64_t size) const noexcept {
|
||||
void RenderContext::MapMemory(uint64_t vaddr, uint64_t size) {
|
||||
std::lock_guard lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.Add(vaddr, size);
|
||||
m_buffer_cache.QueueBdaSync(vaddr, size);
|
||||
}
|
||||
|
||||
void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
||||
@@ -129,9 +130,7 @@ void RenderContext::PrepareBda() {
|
||||
m_bda_logged = true;
|
||||
}
|
||||
std::shared_lock lock(m_mapped_ranges_mutex);
|
||||
m_mapped_ranges.ForEach([this](uint64_t start, uint64_t end) {
|
||||
m_buffer_cache.SynchronizeBuffersInRange(start, end - start);
|
||||
});
|
||||
m_buffer_cache.SynchronizeBda(m_mapped_ranges);
|
||||
m_fault_process_pending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -507,14 +507,15 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
}
|
||||
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
|
||||
const auto& metadata = depth.desc.info.metadata;
|
||||
if (metadata.kind == ImageMetadataKind::Htile && depth.depth_clear_enable &&
|
||||
!cache.ClearMeta(metadata.range.address)) {
|
||||
EXIT("failed to acquire HTile metadata for a depth clear\n");
|
||||
}
|
||||
const bool meta_clear =
|
||||
metadata.kind == ImageMetadataKind::Htile &&
|
||||
cache.IsMetaCleared(metadata.range.address, depth.desc.view_info.base_layer);
|
||||
depth.depth_load_clear_enable = depth.depth_clear_enable || meta_clear;
|
||||
depth.load_clear_aspects = {};
|
||||
if (depth.depth_clear_enable || meta_clear)
|
||||
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eDepth;
|
||||
// Apply pending clears to compressed stencil as well as depth.
|
||||
if (depth.stencil_clear_enable || (meta_clear && metadata.stencil_compressed))
|
||||
depth.load_clear_aspects |= vk::ImageAspectFlagBits::eStencil;
|
||||
if (meta_clear &&
|
||||
!cache.TouchMeta(metadata.range.address, depth.desc.view_info.base_layer, false)) {
|
||||
EXIT("failed to consume HTile clear state\n");
|
||||
@@ -536,7 +537,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)) {
|
||||
@@ -564,9 +568,11 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
attachment.clear_value[0] = std::bit_cast<uint32_t>(depth.depth_clear_value);
|
||||
attachment.clear_value[1] = depth.stencil_clear_value;
|
||||
attachment.has_depth = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eDepth);
|
||||
attachment.depth_clear = depth.depth_load_clear_enable;
|
||||
attachment.depth_clear =
|
||||
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eDepth);
|
||||
attachment.has_stencil = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eStencil);
|
||||
attachment.stencil_clear = depth.stencil_clear_enable;
|
||||
attachment.stencil_clear =
|
||||
static_cast<bool>(depth.load_clear_aspects & vk::ImageAspectFlagBits::eStencil);
|
||||
}
|
||||
if (color_count == 0 && !depth.image_id) {
|
||||
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
|
||||
@@ -1057,7 +1063,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
|
||||
state.ps_input_info.ps_sample_mask_export_enable);
|
||||
vk::ImageAspectFlags feedback;
|
||||
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
|
||||
if (depth.depth_load_clear_enable) {
|
||||
if (depth.load_clear_aspects) {
|
||||
m_context.GetCommandScheduler().BeginRendering(rendering);
|
||||
}
|
||||
auto& cache = m_context.GetTextureCache();
|
||||
|
||||
@@ -162,7 +162,8 @@ void RenderDocOnGuestFlip(RenderContext& renderer) {
|
||||
return;
|
||||
}
|
||||
if (state == RenderDocState::Capturing) {
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", ++g_captured_flips);
|
||||
++g_captured_flips;
|
||||
LOGF("RenderDoc: captured guest flip %u/2\n", g_captured_flips);
|
||||
if (g_captured_flips < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#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"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <fmt/format.h>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -280,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();
|
||||
@@ -441,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;
|
||||
@@ -794,6 +908,7 @@ 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;
|
||||
@@ -1578,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();
|
||||
@@ -1663,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();
|
||||
}
|
||||
|
||||
@@ -1782,6 +1907,7 @@ struct SystemOverlay::Impl {
|
||||
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;
|
||||
|
||||
@@ -254,7 +254,8 @@ public:
|
||||
}
|
||||
|
||||
const bool reserved = binding.key == SDLK_ESCAPE || binding.key == SDLK_F1 ||
|
||||
binding.key == SDLK_F7 || binding.key == SDLK_F11;
|
||||
binding.key == SDLK_F7 || binding.key == SDLK_F8 ||
|
||||
binding.key == SDLK_F11;
|
||||
if (binding.control == INVALID_CONTROL || reserved ||
|
||||
(binding.key == SDLK_UNKNOWN && binding.mouse_button == 0)) {
|
||||
EXIT("Invalid input mapping: %s\n", value.c_str());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -266,7 +266,6 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
|
||||
#endif
|
||||
#if !defined(__APPLE__)
|
||||
check_feature(device_features2.features.depthClamp, "depthClamp");
|
||||
check_feature(fragment_barycentric.fragmentShaderBarycentric, "fragmentShaderBarycentric");
|
||||
#endif
|
||||
|
||||
check_feature(features12.samplerMirrorClampToEdge, "samplerMirrorClampToEdge",
|
||||
@@ -498,7 +497,21 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
image_atomic_int64.pNext = supported_features2.pNext;
|
||||
supported_features2.pNext = &image_atomic_int64;
|
||||
}
|
||||
const bool barycentric_extension =
|
||||
HasExtension(device_extensions, VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
|
||||
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR supported_barycentric {};
|
||||
if (barycentric_extension) {
|
||||
supported_barycentric.pNext = supported_features2.pNext;
|
||||
supported_features2.pNext = &supported_barycentric;
|
||||
}
|
||||
physical_device.getFeatures2(&supported_features2);
|
||||
const bool barycentric_supported =
|
||||
barycentric_extension && supported_barycentric.fragmentShaderBarycentric == VK_TRUE;
|
||||
if (!barycentric_supported) {
|
||||
Log::WriteToConsoleAndLog(
|
||||
"WARNING: Your GPU is not fully supported (fragmentShaderBarycentric is unavailable). "
|
||||
"Some games may render incorrectly or crash.\n");
|
||||
}
|
||||
graphics.shader_image_int64_atomics_enabled = image_atomic_int64.shaderImageInt64Atomics;
|
||||
|
||||
auto features12 = WindowContext::RequiredVulkan12Features();
|
||||
@@ -610,9 +623,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
||||
robustness2.pNext = &features12;
|
||||
#else
|
||||
vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR fragment_barycentric {};
|
||||
fragment_barycentric.pNext = &features12;
|
||||
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
|
||||
robustness2.pNext = &fragment_barycentric;
|
||||
if (barycentric_supported) {
|
||||
fragment_barycentric.pNext = &features12;
|
||||
fragment_barycentric.fragmentShaderBarycentric = VK_TRUE;
|
||||
robustness2.pNext = &fragment_barycentric;
|
||||
} else {
|
||||
robustness2.pNext = &features12;
|
||||
}
|
||||
#endif
|
||||
robustness2.robustBufferAccess2 = supported_robustness2.robustBufferAccess2;
|
||||
robustness2.robustImageAccess2 = supported_robustness2.robustImageAccess2;
|
||||
@@ -966,7 +983,6 @@ void WindowContext::CreateVulkan() {
|
||||
#else
|
||||
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
|
||||
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
|
||||
device_extensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
|
||||
#endif
|
||||
|
||||
#ifdef KYTY_ENABLE_DEBUG_PRINTF
|
||||
@@ -1024,7 +1040,8 @@ void WindowContext::CreateVulkan() {
|
||||
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
|
||||
VK_EXT_MESH_SHADER_EXTENSION_NAME,
|
||||
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
|
||||
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME}) {
|
||||
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
|
||||
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME}) {
|
||||
if (HasExtension(available_extensions, extension)) {
|
||||
device_extensions.push_back(extension);
|
||||
}
|
||||
|
||||
@@ -466,6 +466,13 @@ void WindowContext::ProcessEvent(double time_s) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
|
||||
event->key.key == SDLK_F8) {
|
||||
if (event->type == SDL_EVENT_KEY_DOWN && event->key.repeat == 0) {
|
||||
Controller::CycleKeyboardPlayer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ProcessSystemOverlayInput(*event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,44 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
|
||||
// Native half conversion causes regressions; use compact integer RTZ conversion.
|
||||
const auto bits = EmitBitcastF32ToU32(state, value);
|
||||
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
const auto sign = EmitAndConstant(
|
||||
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
|
||||
0x8000u);
|
||||
const auto exp = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
|
||||
ConstantU32(state, 23));
|
||||
const auto mant = EmitAndConstant(state, abs, 0x007fffffu);
|
||||
|
||||
// Align the hidden bit to the half subnormal mantissa. Even unused shifts must be < 32.
|
||||
const auto shift = EmitUMin32(
|
||||
state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 126), exp),
|
||||
ConstantU32(state, 31));
|
||||
const auto sub = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
|
||||
EmitOrU32(state, mant, ConstantU32(state, 0x00800000u)), shift);
|
||||
|
||||
// Drop 13 mantissa bits and rebias the exponent; finite overflow saturates to max half.
|
||||
const auto truncated = Binary(state, spv::OpShiftRightLogical, TypeU32(state), abs,
|
||||
ConstantU32(state, 13));
|
||||
const auto normal = EmitUMin32(
|
||||
state, Binary(state, spv::OpISub, TypeU32(state), truncated, ConstantU32(state, 0x1c000u)),
|
||||
ConstantU32(state, 0x7bffu));
|
||||
const auto finite = Select(state, TypeU32(state),
|
||||
EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x38800000u),
|
||||
sub, normal);
|
||||
const auto nan = EmitOrU32(state, EmitAndConstant(state, truncated, 0x03ffu),
|
||||
ConstantU32(state, 0x7e00u));
|
||||
const auto special = Select(state, TypeU32(state),
|
||||
EmitCompareU32Constant(state, spv::OpIEqual, mant, 0),
|
||||
ConstantU32(state, 0x7c00u), nan);
|
||||
const auto magnitude = Select(
|
||||
state, TypeU32(state),
|
||||
EmitCompareU32Constant(state, spv::OpUGreaterThanEqual, abs, 0x7f800000u), special, finite);
|
||||
return EmitOrU32(state, sign, magnitude);
|
||||
}
|
||||
|
||||
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
const auto trunc = EmitTruncF32Value(state, src);
|
||||
const auto converted_raw = state.builder.AllocateId();
|
||||
@@ -210,28 +248,18 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
|
||||
}
|
||||
|
||||
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
const auto pair = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 2), pair, arg0, arg1);
|
||||
const auto packed = EmitPackHalf2x16(state, pair);
|
||||
const auto back = EmitExt(state, TypeF32Vector(state, 2), GLSLstd450UnpackHalf2x16, {packed});
|
||||
auto result = packed;
|
||||
for (uint32_t i = 0; i < 2; i++) {
|
||||
const auto rounded = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), rounded, back, i);
|
||||
const auto above =
|
||||
Binary(state, spv::OpFOrdGreaterThan, TypeBool(state),
|
||||
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {rounded}),
|
||||
EmitExt(state, TypeF32(state), GLSLstd450FAbs, {i == 0 ? arg0 : arg1}));
|
||||
result = Binary(state, spv::OpISub, TypeU32(state), result,
|
||||
Select(state, TypeU32(state), above,
|
||||
ConstantU32(state, i == 0 ? 1u : 0x10000u), ConstantU32(state, 0)));
|
||||
}
|
||||
return result;
|
||||
const auto low = EmitF32ToF16RtzBits(state, arg0);
|
||||
const auto high = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
EmitF32ToF16RtzBits(state, arg1), ConstantU32(state, 16));
|
||||
return EmitOrU32(state, low, high);
|
||||
}
|
||||
|
||||
uint32_t EmitFPSaturate32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
// The clamp modifier runs in DX10 clamp mode: NaN becomes 0. FClamp leaves NaN undefined.
|
||||
const auto clamped = EmitExt(state, TypeF32(state), GLSLstd450FClamp,
|
||||
{arg0, ConstantF32(state, 0), ConstantF32(state, 0x3f800000u)});
|
||||
return Select(state, TypeF32(state), EmitClassifyF32(state, arg0).nan, ConstantF32(state, 0),
|
||||
clamped);
|
||||
}
|
||||
|
||||
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
|
||||
@@ -296,6 +296,7 @@ inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicAnd64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicIAdd64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicOr32 = EmitAtomic32;
|
||||
inline constexpr auto EmitBufferAtomicOr64 = EmitBufferAtomic64;
|
||||
inline constexpr auto EmitBufferAtomicXor32 = EmitAtomic32;
|
||||
@@ -325,6 +326,7 @@ inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
|
||||
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
|
||||
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
|
||||
|
||||
@@ -420,6 +420,7 @@ void EnsureLdsStorage(EmitterState& state);
|
||||
struct MemoryResourceAccess {
|
||||
IR::ResourceKind kind = IR::ResourceKind::None;
|
||||
uint32_t object_pointer = 0;
|
||||
uint32_t descriptor_index = 0;
|
||||
uint32_t length = 0;
|
||||
uint32_t byte_offset = 0;
|
||||
uint32_t element_bits = 32;
|
||||
|
||||
@@ -1075,6 +1075,63 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadRawBuffer(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
|
||||
const MemoryResourceAccess& resource, uint32_t components) {
|
||||
auto& state = ctx.state;
|
||||
const auto scalar = [&] {
|
||||
std::array<uint32_t, 4> values {};
|
||||
for (uint32_t component = 0; component < components; component++) {
|
||||
values[component] =
|
||||
LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, component), resource);
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
};
|
||||
if (!mem.IsCoherentBufferPairLoad(inst.GetOpcode())) return scalar();
|
||||
|
||||
const auto first = ByteAddress(ctx, inst, mem, resource.byte_offset);
|
||||
const auto second = ByteAddress(ctx, inst, RebaseRawComponent(mem, 1), resource.byte_offset);
|
||||
const auto wide =
|
||||
PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[3],
|
||||
resource.non_uniform ? resource.descriptor_index : 0u);
|
||||
const auto index =
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), first, ConstantU32(state, 3));
|
||||
auto atomic =
|
||||
Binary(state, spv::OpIEqual, TypeBool(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 7)),
|
||||
ConstantU32(state, 0));
|
||||
atomic = AndCondition(state, atomic,
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), second,
|
||||
EmitAddU32(state, first, ConstantU32(state, 4))));
|
||||
// The 64-bit alias requires the entire pair; partial and swizzled reads retain DWORD bounds.
|
||||
atomic = AndCondition(state, atomic, EmitMemoryElementInBounds(state, wide, index));
|
||||
const auto atomic_label = state.builder.AllocateId(), scalar_label = state.builder.AllocateId();
|
||||
const auto merge = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
|
||||
state.builder.AddFunction(spv::OpBranchConditional, atomic, atomic_label, scalar_label);
|
||||
EmitLabel(state, atomic_label);
|
||||
const auto pointer = EmitStorageBufferElementPointer(
|
||||
state, wide, index, TypeStorageBufferElementPointer(state, 64));
|
||||
const auto snapshot = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAtomicLoad, TypeU64(state), snapshot, pointer,
|
||||
ConstantU32(state, spv::ScopeDevice),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireMask |
|
||||
spv::MemorySemanticsUniformMemoryMask));
|
||||
const std::array<uint32_t, 4> words {
|
||||
EmitCompositeExtractU64(state, snapshot, IR::Value(0u)),
|
||||
EmitCompositeExtractU64(state, snapshot, IR::Value(1u))};
|
||||
const auto pair = ConstructU32Composite(state, 2, words);
|
||||
state.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(state, scalar_label);
|
||||
const auto fallback = scalar();
|
||||
const auto scalar_exit = state.current_label;
|
||||
state.builder.AddFunction(spv::OpBranch, merge);
|
||||
EmitLabel(state, merge);
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpPhi, TypeU32Composite(state, 2), result, pair, atomic_label,
|
||||
fallback, scalar_exit);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t components) {
|
||||
auto& state = ctx.state;
|
||||
const auto mem = ctx.Memory(inst);
|
||||
@@ -1100,12 +1157,7 @@ uint32_t LoadBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compon
|
||||
return LoadFormattedPrepared(ctx, inst, selected, resource, info, components);
|
||||
}
|
||||
if (resource.object_pointer == 0u) return ConstantU32CompositeZero(state, components);
|
||||
std::array<uint32_t, 4> values {};
|
||||
for (uint32_t component = 0; component < components; component++) {
|
||||
values[component] =
|
||||
LoadWordPrepared(ctx, inst, RebaseRawComponent(selected, component), resource);
|
||||
}
|
||||
return ConstructU32Composite(state, components, values);
|
||||
return LoadRawBuffer(ctx, inst, selected, resource, components);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,10 +207,11 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
|
||||
const bool non_uniform = array_index != 0u;
|
||||
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
|
||||
if (!non_uniform) array_index = ConstantU32(state, static_index);
|
||||
MemoryResourceAccess access {
|
||||
.kind = mem.kind,
|
||||
.non_uniform = non_uniform,
|
||||
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
|
||||
MemoryResourceAccess access {.kind = mem.kind,
|
||||
.descriptor_index = array_index,
|
||||
.non_uniform = non_uniform,
|
||||
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask
|
||||
: spv::MemoryAccessMaskNone};
|
||||
access.object_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
|
||||
buffer.variable, array_index);
|
||||
@@ -504,6 +505,7 @@ uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFor
|
||||
|
||||
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap32:
|
||||
case IR::ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
@@ -516,6 +518,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicIAdd64:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd32:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd64:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd64:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
case IR::ValueOpcode::BufferAtomicISub32:
|
||||
case IR::ValueOpcode::SharedAtomicISub32: return spv::OpAtomicISub;
|
||||
@@ -560,8 +563,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
|
||||
const bool wide = inst.GetType() == IR::Type::U64;
|
||||
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
if (inst.GetOpcode() == IR::ValueOpcode::SharedAtomicCmpSwap32) {
|
||||
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
|
||||
std::swap(desired, comparator);
|
||||
}
|
||||
ctx.state.builder.AddFunction(
|
||||
spv::OpAtomicCompareExchange, TypeU32(ctx.state), old, pointer,
|
||||
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
|
||||
|
||||
@@ -67,6 +67,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
|
||||
{0x3fu, Opcode::BUFFER_ATOMIC_FMIN, 1, 32},
|
||||
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
|
||||
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
|
||||
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
|
||||
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
|
||||
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
|
||||
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
|
||||
@@ -114,6 +115,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
{0x27u, Opcode::DS_MIN_RTN_U32, 1, 32}, {0x28u, Opcode::DS_MAX_RTN_U32, 1, 32},
|
||||
{0x29u, Opcode::DS_AND_RTN_B32, 1, 32}, {0x2au, Opcode::DS_OR_RTN_B32, 1, 32},
|
||||
{0x2bu, Opcode::DS_XOR_RTN_B32, 1, 32}, {0x2du, Opcode::DS_WRXCHG_RTN_B32, 1, 32},
|
||||
{0x30u, Opcode::DS_CMPST_RTN_B32, 1, 32},
|
||||
{0x35u, Opcode::DS_SWIZZLE_B32, 1, 32}, {0x36u, Opcode::DS_READ_B32, 1, 32},
|
||||
{0x37u, Opcode::DS_READ2_B32, 2, 32}, {0x38u, Opcode::DS_READ2ST64_B32, 2, 32},
|
||||
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
|
||||
@@ -200,6 +202,7 @@ bool IsDsAtomicOpcode(Opcode opcode) {
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
@@ -220,6 +223,7 @@ uint32_t DsSourceCount(Opcode opcode) {
|
||||
case Opcode::DS_WRITE2ST64_B32:
|
||||
case Opcode::DS_WRITE2_B64:
|
||||
case Opcode::DS_WRITE2ST64_B64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_MSKOR_B32: return 3u;
|
||||
case Opcode::DS_MIN_F32:
|
||||
@@ -454,6 +458,9 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
|
||||
}
|
||||
if (inst.gds && inst.opcode == Opcode::DS_CMPST_RTN_B32) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS compare-store return B32 GDS variant is not implemented");
|
||||
}
|
||||
if (inst.gds &&
|
||||
(inst.opcode == Opcode::DS_SWIZZLE_B32 || inst.opcode == Opcode::DS_PERMUTE_B32 ||
|
||||
inst.opcode == Opcode::DS_BPERMUTE_B32 ||
|
||||
|
||||
@@ -670,6 +670,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::BUFFER_ATOMIC_CMPSWAP:
|
||||
case Opcode::BUFFER_ATOMIC_SWAP_X2:
|
||||
case Opcode::BUFFER_ATOMIC_ADD:
|
||||
case Opcode::BUFFER_ATOMIC_ADD_X2:
|
||||
case Opcode::BUFFER_ATOMIC_SUB:
|
||||
case Opcode::BUFFER_ATOMIC_SMIN:
|
||||
case Opcode::BUFFER_ATOMIC_UMIN:
|
||||
@@ -717,6 +718,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::DS_MAX_U32:
|
||||
case Opcode::DS_MAX_RTN_U32:
|
||||
case Opcode::DS_MAX_RTN_U64:
|
||||
case Opcode::DS_CMPST_RTN_B32:
|
||||
case Opcode::DS_CMPST_RTN_B64:
|
||||
case Opcode::DS_AND_B32:
|
||||
case Opcode::DS_AND_RTN_B32:
|
||||
|
||||
@@ -586,6 +586,7 @@ enum class Opcode {
|
||||
BUFFER_ATOMIC_CMPSWAP,
|
||||
BUFFER_ATOMIC_SWAP_X2,
|
||||
BUFFER_ATOMIC_ADD,
|
||||
BUFFER_ATOMIC_ADD_X2,
|
||||
BUFFER_ATOMIC_SUB,
|
||||
BUFFER_ATOMIC_SMIN,
|
||||
BUFFER_ATOMIC_UMIN,
|
||||
@@ -631,6 +632,7 @@ enum class Opcode {
|
||||
DS_MAX_U32,
|
||||
DS_MAX_RTN_U32,
|
||||
DS_MAX_RTN_U64,
|
||||
DS_CMPST_RTN_B32,
|
||||
DS_CMPST_RTN_B64,
|
||||
DS_AND_B32,
|
||||
DS_AND_RTN_B32,
|
||||
|
||||
@@ -27,7 +27,7 @@ void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode
|
||||
if (quiet_snan) {
|
||||
const auto quiet_snan_lane = [&](const Decoder::Operand& operand, bool high) {
|
||||
const auto bits = ReadU16LaneAsU32(operand, high, false);
|
||||
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7c00u)));
|
||||
const auto exponent = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x7e00u)));
|
||||
const auto payload = ir.BitwiseAnd(bits, IR::U32(IR::Value(0x01ffu)));
|
||||
const auto snan = ir.LogicalAnd(ir.IEqual(exponent, IR::U32(IR::Value(0x7c00u))),
|
||||
ir.INotEqual(payload, IR::U32(IR::Value(0u))));
|
||||
|
||||
@@ -1046,6 +1046,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMin32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_FMAX:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicFMax32);
|
||||
case Decoder::Opcode::BUFFER_ATOMIC_ADD_X2:
|
||||
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicIAdd64);
|
||||
|
||||
case Decoder::Opcode::DS_ADD_U32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicIAdd32, false);
|
||||
@@ -1083,6 +1085,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
|
||||
case Decoder::Opcode::DS_MAX_RTN_U64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B32:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap32, true);
|
||||
case Decoder::Opcode::DS_CMPST_RTN_B64:
|
||||
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
|
||||
case Decoder::Opcode::DS_AND_B32:
|
||||
|
||||
@@ -233,13 +233,6 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
|
||||
if (type == IR::Type::F64) {
|
||||
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
|
||||
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
|
||||
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
const auto bits = operand.value == 0x3e22f983u
|
||||
? 0x3fc45f306dc9c882ull
|
||||
: std::bit_cast<uint64_t>(static_cast<double>(
|
||||
std::bit_cast<float>(operand.value)));
|
||||
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
|
||||
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
|
||||
}
|
||||
if (operand.absolute) {
|
||||
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
|
||||
@@ -488,6 +481,14 @@ std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand)
|
||||
if (operand.kind == Decoder::OperandKind::VccLo) {
|
||||
return {ir.GetVccLo(), ir.GetVccHi()};
|
||||
}
|
||||
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
const auto bits =
|
||||
operand.value == 0x3e22f983u
|
||||
? 0x3fc45f306dc9c882ull
|
||||
: std::bit_cast<uint64_t>(static_cast<double>(std::bit_cast<float>(operand.value)));
|
||||
return {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
|
||||
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
|
||||
}
|
||||
const auto low = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
|
||||
IR::U32 high(IR::Value(0u));
|
||||
if (operand.kind == Decoder::OperandKind::Sgpr || operand.kind == Decoder::OperandKind::Vgpr) {
|
||||
@@ -546,9 +547,11 @@ IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_la
|
||||
auto bits = ReadRawU32(operand);
|
||||
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
|
||||
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
|
||||
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
|
||||
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
|
||||
const auto value = operand.value;
|
||||
bits = IR::U32(IR::Value(value == 0x3e22f983u
|
||||
? 0x3118u
|
||||
: ((value >> 16u) & 0x8000u) |
|
||||
((((value >> 23u) & 0xffu) - 112u) << 10u)));
|
||||
}
|
||||
bits = ApplyBitSourceModifiers(raw_operand, bits);
|
||||
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
|
||||
|
||||
@@ -25,6 +25,7 @@ struct UniformFill {
|
||||
std::array<uint32_t, 3> group_stride {};
|
||||
uint32_t words = 0;
|
||||
uint32_t value = 0;
|
||||
uint32_t bounded_elements = 0;
|
||||
|
||||
bool operator==(const UniformFill&) const = default;
|
||||
};
|
||||
|
||||
@@ -82,6 +82,11 @@ struct MemoryInfo {
|
||||
bool coherent = false;
|
||||
bool planning_only = false;
|
||||
|
||||
[[nodiscard]] bool IsCoherentBufferPairLoad(ValueOpcode opcode) const {
|
||||
return kind == ResourceKind::Buffer && opcode == ValueOpcode::LoadBufferU32x2 && coherent &&
|
||||
!typed && !formatted && data_bits == 32u;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SupportsIndirectBufferLoad(ValueOpcode opcode) const {
|
||||
if (typed) return false;
|
||||
if (data_bits == 8u) return !formatted && opcode == ValueOpcode::LoadBufferU8;
|
||||
@@ -622,6 +627,8 @@ struct CompiledShaderInfo {
|
||||
struct UniformFillPlan {
|
||||
UniformFill fill;
|
||||
std::array<Value, 4> values;
|
||||
Value pattern_size;
|
||||
Value element_count;
|
||||
};
|
||||
|
||||
// Resource analysis retained by the shader cache. It owns immutable descriptor/SRT,
|
||||
|
||||
@@ -118,6 +118,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::BufferAtomicCmpSwap32:
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicIAdd32:
|
||||
case ValueOpcode::BufferAtomicIAdd64:
|
||||
case ValueOpcode::BufferAtomicISub32:
|
||||
case ValueOpcode::BufferAtomicSMin32:
|
||||
case ValueOpcode::BufferAtomicUMin32:
|
||||
@@ -137,6 +138,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
|
||||
uint32_t BufferComponentCount(ValueOpcode opcode) {
|
||||
switch (opcode) {
|
||||
case ValueOpcode::BufferAtomicSwap64:
|
||||
case ValueOpcode::BufferAtomicIAdd64:
|
||||
case ValueOpcode::BufferAtomicAnd64:
|
||||
case ValueOpcode::BufferAtomicOr64:
|
||||
case ValueOpcode::LoadBufferU32x2:
|
||||
@@ -176,6 +178,7 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
|
||||
case ValueOpcode::SharedAtomicSMax32:
|
||||
case ValueOpcode::SharedAtomicUMax32:
|
||||
case ValueOpcode::SharedAtomicUMax64:
|
||||
case ValueOpcode::SharedAtomicCmpSwap32:
|
||||
case ValueOpcode::SharedAtomicCmpSwap64:
|
||||
case ValueOpcode::SharedAtomicAnd32:
|
||||
case ValueOpcode::SharedAtomicOr32:
|
||||
|
||||
@@ -268,6 +268,7 @@ VALUE_OPCODE(BufferAtomicSwap32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicCmpSwap32, U32, BufferResource, U32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSwap64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicIAdd32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
|
||||
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
|
||||
@@ -306,6 +307,7 @@ VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
|
||||
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
|
||||
|
||||
@@ -51,11 +51,13 @@ public:
|
||||
}
|
||||
|
||||
void QueueUsers(const std::vector<Use>& users) {
|
||||
++generation;
|
||||
affected.clear();
|
||||
for (const auto& use: users) affected.push_back(use.user);
|
||||
}
|
||||
|
||||
void FlushUsers() {
|
||||
++generation;
|
||||
// Pattern folds inspect nested operands even when intermediate operations
|
||||
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
|
||||
// stay dynamic. Visit their union once per drain, including cyclic Phi edges.
|
||||
for (size_t i = 0; i < affected.size(); ++i) {
|
||||
auto* inst = affected[i];
|
||||
const auto found = locations.find(inst);
|
||||
@@ -64,6 +66,7 @@ public:
|
||||
Queue(inst);
|
||||
for (const auto& use: inst->Uses()) affected.push_back(use.user);
|
||||
}
|
||||
affected.clear();
|
||||
}
|
||||
|
||||
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
|
||||
@@ -608,13 +611,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;
|
||||
@@ -967,27 +980,30 @@ void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
|
||||
std::vector<Use> users;
|
||||
std::vector<Value> operands;
|
||||
const auto drain = [&] {
|
||||
for (size_t i = 0; i < worklist.pending.size(); ++i) {
|
||||
auto* inst = worklist.pending[i];
|
||||
auto& location = worklist.locations.at(inst);
|
||||
location.queued = false;
|
||||
const auto opcode = inst->GetOpcode();
|
||||
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
|
||||
const auto where = location.instruction;
|
||||
users.assign(inst->Uses().begin(), inst->Uses().end());
|
||||
operands.clear();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
|
||||
mask_projection.Fold(*inst, wave_size);
|
||||
FoldInstruction(*inst->Parent(), where, worklist);
|
||||
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
|
||||
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
|
||||
changed = operands[arg] != inst->Arg(arg);
|
||||
if (changed) {
|
||||
worklist.QueueUsers(users);
|
||||
worklist.Queue(inst);
|
||||
do {
|
||||
for (size_t i = 0; i < worklist.pending.size(); ++i) {
|
||||
auto* inst = worklist.pending[i];
|
||||
auto& location = worklist.locations.at(inst);
|
||||
location.queued = false;
|
||||
const auto opcode = inst->GetOpcode();
|
||||
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
|
||||
const auto where = location.instruction;
|
||||
users.assign(inst->Uses().begin(), inst->Uses().end());
|
||||
operands.clear();
|
||||
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
|
||||
mask_projection.Fold(*inst, wave_size);
|
||||
FoldInstruction(*inst->Parent(), where, worklist);
|
||||
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
|
||||
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
|
||||
changed = operands[arg] != inst->Arg(arg);
|
||||
if (changed) {
|
||||
worklist.QueueUsers(users);
|
||||
worklist.Queue(inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
worklist.pending.clear();
|
||||
worklist.pending.clear();
|
||||
worklist.FlushUsers();
|
||||
} while (!worklist.pending.empty());
|
||||
};
|
||||
drain();
|
||||
// Normalize retained PHI/select values only after every supported field read has
|
||||
|
||||
@@ -1409,7 +1409,8 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
}
|
||||
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
|
||||
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
|
||||
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
|
||||
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThan32 &&
|
||||
op != ValueOpcode::ULessThanEqual32 &&
|
||||
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
|
||||
auto& node = plan.value_storage.emplace_back(op);
|
||||
for (size_t index = 0; index < inst->NumArgs(); ++index)
|
||||
@@ -1424,10 +1425,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
|
||||
plan.value_storage.erase(first, plan.value_storage.end());
|
||||
}
|
||||
plan.uniform_fill = AnalyzeUniformFill(program);
|
||||
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
|
||||
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
|
||||
}
|
||||
plan.uniform_fill = program.uniform_fill.fill.kind != UniformFillKind::None
|
||||
? program.uniform_fill : AnalyzeUniformFill(program);
|
||||
for (auto& value: plan.uniform_fill.values) value = Clone(value);
|
||||
plan.uniform_fill.pattern_size = Clone(plan.uniform_fill.pattern_size);
|
||||
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
|
||||
plan.capture_specialization_reads |=
|
||||
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
|
||||
if (plan.capture_specialization_reads) {
|
||||
@@ -1513,15 +1515,21 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
}
|
||||
snapshot.uniform_fill = {};
|
||||
const auto& fill = program.uniform_fill;
|
||||
const auto words = fill.fill.words;
|
||||
auto words = fill.fill.words;
|
||||
std::array<uint32_t, 4> stored {};
|
||||
bool uniform_fill = words != 0;
|
||||
if (uniform_fill && !fill.pattern_size.IsEmpty())
|
||||
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
|
||||
uint32_t elements = 0;
|
||||
if (uniform_fill && !fill.element_count.IsEmpty())
|
||||
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
|
||||
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
|
||||
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
|
||||
}
|
||||
if (uniform_fill) {
|
||||
snapshot.uniform_fill = fill.fill;
|
||||
snapshot.uniform_fill.value = stored[0];
|
||||
snapshot.uniform_fill.bounded_elements = elements;
|
||||
}
|
||||
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
|
||||
if (source >= program.descriptor_sources.size()) {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#define XXH_INLINE_ALL
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace Libs::Graphics::ShaderRecompiler::IR {
|
||||
namespace {
|
||||
@@ -4040,11 +4042,71 @@ private:
|
||||
bool m_shader_writes = false;
|
||||
};
|
||||
|
||||
UniformFillPlan RecognizePatternFill(Program& program, const Decoder::Program& decoded,
|
||||
ShaderStageInputInfo input) {
|
||||
struct Pattern {
|
||||
size_t code_words;
|
||||
uint64_t hash;
|
||||
uint32_t wave_size_mask;
|
||||
bool bounded;
|
||||
};
|
||||
constexpr std::array patterns {
|
||||
// Bounded 64-lane variant.
|
||||
Pattern {.code_words = 69, .hash = 0x0704b9818e3cba06ull, .wave_size_mask = 64, .bounded = true},
|
||||
// Dispatch thread dimensions, 64-lane variants.
|
||||
Pattern {.code_words = 42, .hash = 0x92ce57f21310ef02ull, .wave_size_mask = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x374161027f58bee7ull, .wave_size_mask = 64, .bounded = false},
|
||||
Pattern {.code_words = 40, .hash = 0x7d7172c56c2e296dull, .wave_size_mask = 64, .bounded = false},
|
||||
// Dispatch thread dimensions, valid in both wave sizes.
|
||||
Pattern {.code_words = 38, .hash = 0xacbda1a1ccdd4198ull, .wave_size_mask = 32 | 64, .bounded = false},
|
||||
};
|
||||
if (program.stage != ShaderType::Compute || input.compute == nullptr ||
|
||||
decoded.instructions.empty() || program.user_data_base != 0 || program.user_data_count < 10 ||
|
||||
program.info.buffers.size() != 1 || !program.info.images.empty()) return {};
|
||||
const auto& cs = *input.compute;
|
||||
const bool matching_inputs = cs.workgroup_register == 10 && cs.thread_ids_num == 1 &&
|
||||
cs.group_id[0] && !cs.group_id[1] && !cs.group_id[2] && cs.threads_num[0] == 64 &&
|
||||
cs.threads_num[1] == 1 && cs.threads_num[2] == 1 && cs.float_mode == 0xc0;
|
||||
if (!matching_inputs) return {};
|
||||
const auto& last = decoded.instructions.back();
|
||||
const size_t words = last.pc / sizeof(uint32_t) + last.word_count;
|
||||
if (words > decoded.code.size() ||
|
||||
std::ranges::find(patterns, words, &Pattern::code_words) == patterns.end()) return {};
|
||||
// Hash executable instructions through S_ENDPGM, excluding binary metadata.
|
||||
// Verified variants implement Destination[id] = value[id % pattern_size].
|
||||
const auto hash = XXH3_64bits(decoded.code.data(), words * sizeof(uint32_t));
|
||||
const auto pattern = std::ranges::find_if(patterns, [&](const Pattern& candidate) {
|
||||
return candidate.code_words == words && candidate.hash == hash;
|
||||
});
|
||||
if (pattern == patterns.end() || (program.wave_size & pattern->wave_size_mask) == 0 ||
|
||||
cs.dispatch_thread_dimensions != !pattern->bounded) return {};
|
||||
std::array<Value, 10> userdata;
|
||||
for (auto* block: program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
if (inst.GetOpcode() != ValueOpcode::GetUserData) continue;
|
||||
const auto reg = RegIndex(inst.Arg(0).ScalarRegister());
|
||||
if (reg < userdata.size()) userdata[reg] = Value(&inst);
|
||||
}
|
||||
}
|
||||
for (uint32_t reg = 4; reg < 10; ++reg)
|
||||
if ((reg != 8 || pattern->bounded) && userdata[reg].IsEmpty()) return {};
|
||||
UniformFillPlan result;
|
||||
result.fill.kind = UniformFillKind::Buffer;
|
||||
result.fill.words = 1;
|
||||
result.fill.group_stride[0] = 64;
|
||||
std::copy_n(userdata.begin() + 4, 4, result.values.begin());
|
||||
result.pattern_size = userdata[9];
|
||||
// Only the bounded variant reads an explicit count; the other uses thread dimensions.
|
||||
if (pattern->bounded) result.element_count = userdata[8];
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
|
||||
ShaderStageInputInfo input) {
|
||||
Tracker(program, decoded, native_cfg, input).Run();
|
||||
program.uniform_fill = RecognizePatternFill(program, decoded, input);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::IR
|
||||
@@ -340,6 +340,7 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
|
||||
}
|
||||
if (kind == ResourceKind::Buffer) {
|
||||
info.coherent_buffers |= memory.coherent;
|
||||
info.buffer_int64_atomics |= memory.IsCoherentBufferPairLoad(op);
|
||||
if (memory.resource >= info.buffers.size()) {
|
||||
return Fail("buffer operation has invalid resource metadata");
|
||||
}
|
||||
|
||||
@@ -1054,7 +1054,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
|
||||
}
|
||||
return false;
|
||||
case ValueOpcode::ULessThan32:
|
||||
if (binary()) {
|
||||
// No U32 is below zero, including an unknown loop-carried value.
|
||||
if (!Arg(inst, 1, b)) return false;
|
||||
if (static_cast<uint32_t>(b) == 0u) {
|
||||
result = 0u;
|
||||
return true;
|
||||
}
|
||||
if (Arg(inst, 0, a)) {
|
||||
result = static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -583,16 +583,16 @@ ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context&
|
||||
mesh.input_primitive, sh.m_vgtGsOutPrimType, sh.m_vgtGsMaxVertOut,
|
||||
group.primitive_group_size, group.vertex_group_size, mesh.max_vertices);
|
||||
}
|
||||
mesh.max_primitives = mesh.fast_launch ? mesh.max_vertices :
|
||||
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
|
||||
group.primitive_group_size * (sh.m_vgtGsMaxVertOut - 2u);
|
||||
mesh.primitives_per_group = std::min({static_cast<uint32_t>(group.primitive_group_size),
|
||||
mesh.InputPrimitiveCount(group.vertex_group_size),
|
||||
native_ngg ? mesh.max_primitives :
|
||||
native_ngg ? mesh.InputPrimitiveCount(mesh.max_vertices) :
|
||||
mesh.max_vertices / sh.m_vgtGsMaxVertOut});
|
||||
EXIT_IF(mesh.primitives_per_group == 0u);
|
||||
mesh.vertices_per_group = mesh.InputVertexCount(mesh.primitives_per_group);
|
||||
const auto threads = native_ngg && !mesh.fast_launch ? mesh.vertices_per_group : mesh.max_vertices;
|
||||
mesh.max_primitives = mesh.primitives_per_group * (sh.m_geNggSubgrpCntl & 0x1ffu);
|
||||
const auto threads = std::max(native_ngg && !mesh.fast_launch ? mesh.vertices_per_group
|
||||
: mesh.max_vertices,
|
||||
mesh.max_primitives);
|
||||
mesh.threads_num[0] = ((threads + mesh.wave_size - 1u) / mesh.wave_size) * mesh.wave_size;
|
||||
mesh.threads_num[1] = mesh.threads_num[2] = 1u;
|
||||
return params;
|
||||
|
||||
@@ -338,14 +338,20 @@ static bool HasWindowsForbiddenFilenameCharacter(const std::string& relative_pat
|
||||
std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
// Guests join mount points and names with extra slashes (e.g. "/addcont0//file").
|
||||
std::string normalized = Common::FixFilenameSlash(mounted_name);
|
||||
normalized.erase(std::unique(normalized.begin(), normalized.end(),
|
||||
[](char a, char b) { return a == '/' && b == '/'; }),
|
||||
normalized.end());
|
||||
|
||||
// Match the entire guest path so a mount root works with or without a trailing slash.
|
||||
const auto mounted_path = Common::FixDirectorySlash(mounted_name);
|
||||
const auto mounted_path = Common::FixDirectorySlash(normalized);
|
||||
const auto it = std::find_if(
|
||||
m_mount_pairs.begin(), m_mount_pairs.end(),
|
||||
[&mounted_path](const MountPair& p) { return mounted_path.starts_with(p.point); });
|
||||
if (it != m_mount_pairs.end()) {
|
||||
const auto& p = *it;
|
||||
auto rel_path = Common::RemoveFirst(Common::FixFilenameSlash(mounted_name), p.point.size());
|
||||
auto rel_path = Common::RemoveFirst(normalized, p.point.size());
|
||||
while (rel_path.starts_with('/')) {
|
||||
rel_path = Common::RemoveFirst(rel_path, 1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
#define EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
|
||||
#include "common/abi.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
|
||||
using KernelModule = int32_t;
|
||||
struct KernelLoadModuleOpt;
|
||||
struct KernelUnloadModuleOpt;
|
||||
|
||||
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res);
|
||||
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res);
|
||||
|
||||
} // namespace Libs::LibKernel
|
||||
|
||||
#endif // EMULATOR_SRC_KERNEL_MODULE_H_
|
||||
@@ -66,7 +66,6 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_user_id">
|
||||
<property name="toolTip"><string>Local user ID exposed to games</string></property>
|
||||
<property name="maximum"><number>2147483647</number></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
@@ -347,6 +346,16 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_keyboard_player">
|
||||
<property name="text"><string>Keyboard player:</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="comboBox_keyboard_player">
|
||||
<property name="toolTip"><string>Gamepads fill the remaining players in connection order. Every player uses the same input mapping.</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -48,11 +48,14 @@ struct ControllerSettings {
|
||||
QString color;
|
||||
int speaker_volume = 50;
|
||||
int vibration_intensity = 100;
|
||||
// 0 shares player 1 with the first gamepad; 1..MAX_PLAYERS dedicates that player to the keyboard.
|
||||
int keyboard_player = 0;
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
s->setValue("controller_color", color);
|
||||
s->setValue("controller_speaker_volume", speaker_volume);
|
||||
s->setValue("controller_vibration_intensity", vibration_intensity);
|
||||
s->setValue("controller_keyboard_player", keyboard_player);
|
||||
}
|
||||
|
||||
void ReadSettings(QSettings* s) {
|
||||
@@ -65,6 +68,8 @@ struct ControllerSettings {
|
||||
};
|
||||
speaker_volume = read_percent("controller_speaker_volume", 50);
|
||||
vibration_intensity = read_percent("controller_vibration_intensity", 100);
|
||||
keyboard_player = qBound(0, s->value("controller_keyboard_player", 0).toInt(),
|
||||
Config::MAX_PLAYERS);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -209,10 +209,19 @@ static void ListInit(QComboBox* combo, T value) {
|
||||
void ConfigurationEditDialog::Init(const Configuration& info) {
|
||||
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
|
||||
m_ui->lineEdit_user_name->setText(info.user_name);
|
||||
m_ui->spinBox_user_id->setMaximum(Config::MAX_USER_ID);
|
||||
m_ui->spinBox_user_id->setValue(info.user_id);
|
||||
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
|
||||
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
|
||||
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
|
||||
auto* keyboard_player = m_ui->comboBox_keyboard_player;
|
||||
keyboard_player->clear();
|
||||
keyboard_player->addItem(tr("Shared with player 1 (default)"), 0);
|
||||
for (int player = 1; player <= Config::MAX_PLAYERS; player++) {
|
||||
keyboard_player->addItem(tr("Player %1").arg(player), player);
|
||||
}
|
||||
keyboard_player->setCurrentIndex(
|
||||
qMax(0, keyboard_player->findData(info.controller.keyboard_player)));
|
||||
auto* microphone = m_ui->comboBox_audio_input_device;
|
||||
microphone->clear();
|
||||
microphone->addItem(tr("None"), QString {});
|
||||
@@ -458,6 +467,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
|
||||
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
|
||||
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
|
||||
info.controller.speaker_volume = ui.slider_controller_volume->value();
|
||||
info.controller.keyboard_player = ui.comboBox_keyboard_player->currentData().toInt();
|
||||
}
|
||||
info.screen_resolution =
|
||||
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
|
||||
|
||||
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
|
||||
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
|
||||
args << "--keyboard-player" << QString::number(info.controller.keyboard_player);
|
||||
args << "--present-mode" << EnumToText(info.present_mode);
|
||||
if (info.gpu_index >= 0) {
|
||||
args << "--gpu" << QString::number(info.gpu_index);
|
||||
|
||||
+245
-124
@@ -18,6 +18,7 @@
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Controller {
|
||||
@@ -107,18 +108,19 @@ struct ControllerState {
|
||||
|
||||
class GameController {
|
||||
public:
|
||||
GameController() = default;
|
||||
explicit GameController(int player): m_player(player) {}
|
||||
|
||||
KYTY_CLASS_NO_COPY(GameController);
|
||||
|
||||
[[nodiscard]] int Handle() const { return PAD_HANDLE + m_player; }
|
||||
|
||||
int InitPort();
|
||||
int OpenPort(int user_id, int type, int index);
|
||||
int GetPortHandle(int user_id, int type, int index);
|
||||
int ClosePort(int handle);
|
||||
int CheckPortHandle(int handle);
|
||||
|
||||
void Connect(int id);
|
||||
void Disconnect(int id);
|
||||
void SetDevices(int active_id);
|
||||
void Button(int id, uint32_t button, bool down);
|
||||
void Axis(int id, Axis axis, int value);
|
||||
void RightStick(int id, int x, int y);
|
||||
@@ -128,7 +130,6 @@ public:
|
||||
void ResetOrientation();
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
void ReleaseHostPads();
|
||||
void GetConnectionInfo(bool* flag, int* count);
|
||||
void SetVibration(uint8_t large_motor, uint8_t small_motor);
|
||||
int GetActiveControllerId();
|
||||
@@ -145,8 +146,10 @@ private:
|
||||
enum class PortState { Uninitialized, Closed, Open };
|
||||
|
||||
int CheckPortHandleLocked(int handle) const;
|
||||
[[nodiscard]] bool PadOpenArgsAreValid(int user_id, int type, int index) const;
|
||||
|
||||
const int m_player;
|
||||
|
||||
void CheckActive();
|
||||
void AddState();
|
||||
// Emulated tilt: the right stick drives pitch and roll instead of the stick axes.
|
||||
void UpdateEmulatedTilt();
|
||||
@@ -157,7 +160,6 @@ private:
|
||||
|
||||
Common::Mutex m_mutex;
|
||||
PortState m_port_state = PortState::Uninitialized;
|
||||
std::vector<int> m_connected_ids;
|
||||
int m_active_id = -1;
|
||||
bool m_connected = false;
|
||||
int m_connected_count = 0;
|
||||
@@ -184,7 +186,39 @@ private:
|
||||
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
|
||||
};
|
||||
|
||||
static GameController* g_controller = nullptr;
|
||||
static std::array<std::unique_ptr<GameController>, Config::MAX_PLAYERS> g_players;
|
||||
|
||||
// Maps host gamepads to players; the keyboard follows the configured keyboard player.
|
||||
static Common::Mutex g_router_mutex;
|
||||
static std::vector<std::pair<int, int>> g_pad_players;
|
||||
|
||||
static uint32_t g_keyboard_player = 0;
|
||||
|
||||
static void RefreshPlayers();
|
||||
static void StopHostPad(SDL_Gamepad* pad);
|
||||
|
||||
static int KeyboardPlayer() {
|
||||
return g_keyboard_player == 0 ? 0 : static_cast<int>(g_keyboard_player) - 1;
|
||||
}
|
||||
|
||||
// Caller holds g_router_mutex.
|
||||
static GameController* PlayerForDevice(int id) {
|
||||
if (id == HOST_INPUT_CONTROLLER_ID) {
|
||||
return g_players[KeyboardPlayer()].get();
|
||||
}
|
||||
for (const auto& [pad, player]: g_pad_players) {
|
||||
if (pad == id) {
|
||||
return player >= 0 ? g_players[player].get() : nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static GameController* PlayerForHandle(int handle) {
|
||||
return handle >= PAD_HANDLE && handle < PAD_HANDLE + Config::MAX_PLAYERS
|
||||
? g_players[handle - PAD_HANDLE].get()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
static void pad_fill_data(PadData* data, const ControllerState& state, bool connected,
|
||||
int connected_count) {
|
||||
@@ -228,13 +262,15 @@ constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
|
||||
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
|
||||
|
||||
void CycleSetting(Setting setting) {
|
||||
if (g_controller != nullptr) {
|
||||
g_controller->CycleSetting(setting);
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->CycleSetting(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float GetSettingScale(Setting setting) {
|
||||
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
|
||||
return g_players[0] != nullptr ? g_players[0]->GetSettingScale(setting) : 1.0f;
|
||||
}
|
||||
|
||||
void GameController::CycleSetting(Setting setting) {
|
||||
@@ -404,35 +440,53 @@ static int32_t TriggerEffectState(const PadTriggerEffectCommand& command, int va
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
EXIT_IF(g_controller != nullptr);
|
||||
EXIT_IF(g_players[0] != nullptr);
|
||||
|
||||
g_controller = new GameController;
|
||||
g_controller->Connect(HOST_INPUT_CONTROLLER_ID);
|
||||
for (int i = 0; i < Config::MAX_PLAYERS; i++) {
|
||||
g_players[i] = std::make_unique<GameController>(i);
|
||||
}
|
||||
g_keyboard_player = Config::GetKeyboardPlayer();
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
EmergencyShutdown();
|
||||
delete g_controller;
|
||||
g_controller = nullptr;
|
||||
for (auto& player: g_players) {
|
||||
player.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void EmergencyShutdown() {
|
||||
if (g_controller != nullptr) {
|
||||
g_controller->ReleaseHostPads();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
DualSenseHaptics::Shutdown();
|
||||
std::vector<SDL_Gamepad*> pads;
|
||||
for (const auto& [id, player]: g_pad_players) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id))) {
|
||||
StopHostPad(pad);
|
||||
pads.push_back(pad);
|
||||
}
|
||||
}
|
||||
if (!pads.empty()) {
|
||||
SDL_Delay(RELEASE_FLUSH_MS);
|
||||
for (auto* pad: pads) {
|
||||
SDL_CloseGamepad(pad);
|
||||
}
|
||||
}
|
||||
g_pad_players.clear();
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->SetDevices(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameController::Connect(int id) {
|
||||
void GameController::SetDevices(int active_id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
if (std::find(m_connected_ids.begin(), m_connected_ids.end(), id) != m_connected_ids.end()) {
|
||||
if (m_active_id == active_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_connected_ids.push_back(id);
|
||||
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
|
||||
if (active_id != HOST_INPUT_CONTROLLER_ID && active_id != -1) {
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(active_id));
|
||||
pad != nullptr) {
|
||||
if (const auto& color = Config::GetControllerColor()) {
|
||||
(void)SDL_SetGamepadLED(pad, (*color)[0], (*color)[1], (*color)[2]);
|
||||
@@ -446,42 +500,11 @@ void GameController::Connect(int id) {
|
||||
}
|
||||
}
|
||||
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::Disconnect(int id) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
const auto it = std::find(m_connected_ids.begin(), m_connected_ids.end(), id);
|
||||
EXIT_IF(it == m_connected_ids.end());
|
||||
|
||||
m_connected_ids.erase(it);
|
||||
|
||||
CheckActive();
|
||||
}
|
||||
|
||||
void GameController::CheckActive() {
|
||||
int new_active_id = -1;
|
||||
bool new_connected = false;
|
||||
|
||||
if (!m_connected_ids.empty()) {
|
||||
new_active_id = m_connected_ids[0];
|
||||
for (const auto id: m_connected_ids) {
|
||||
if (id != HOST_INPUT_CONTROLLER_ID) {
|
||||
new_active_id = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_connected = true;
|
||||
}
|
||||
|
||||
if (m_connected == new_connected && m_active_id == new_active_id) {
|
||||
return;
|
||||
}
|
||||
const bool new_connected = active_id != -1;
|
||||
if (!m_connected && new_connected) {
|
||||
m_connected_count++;
|
||||
}
|
||||
m_active_id = new_active_id;
|
||||
m_active_id = active_id;
|
||||
m_connected = new_connected;
|
||||
m_state = {};
|
||||
m_gyro_time = 0;
|
||||
@@ -513,7 +536,7 @@ void GameController::AddState() {
|
||||
void GameController::Button(int id, uint32_t button, bool down) {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
// The keyboard shares the player-1 pad with the active gamepad.
|
||||
// Keyboard events are routed to the configured player's port.
|
||||
if (m_active_id == id || id == HOST_INPUT_CONTROLLER_ID) {
|
||||
m_state.time = LibKernel::KernelGetProcessTime();
|
||||
|
||||
@@ -798,39 +821,16 @@ void GameController::ResetInputState() {
|
||||
AddState();
|
||||
}
|
||||
|
||||
void GameController::ReleaseHostPads() {
|
||||
Common::LockGuard lock(m_mutex);
|
||||
DualSenseHaptics::Shutdown();
|
||||
|
||||
std::vector<SDL_Gamepad*> pads;
|
||||
for (const auto id: m_connected_ids) {
|
||||
if (id == HOST_INPUT_CONTROLLER_ID) {
|
||||
continue;
|
||||
}
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(id));
|
||||
pad != nullptr) {
|
||||
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
DualSenseEffects effect {};
|
||||
effect.enable_bits = 0x0c;
|
||||
effect.right_trigger[0] = 0x05;
|
||||
effect.left_trigger[0] = 0x05;
|
||||
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
|
||||
}
|
||||
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
|
||||
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
|
||||
pads.push_back(pad);
|
||||
}
|
||||
static void StopHostPad(SDL_Gamepad* pad) {
|
||||
if (SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
DualSenseEffects effect {};
|
||||
effect.enable_bits = 0x0c;
|
||||
effect.right_trigger[0] = 0x05;
|
||||
effect.left_trigger[0] = 0x05;
|
||||
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
|
||||
}
|
||||
|
||||
if (!pads.empty()) {
|
||||
SDL_Delay(RELEASE_FLUSH_MS);
|
||||
for (auto* pad: pads) {
|
||||
SDL_CloseGamepad(pad);
|
||||
}
|
||||
}
|
||||
|
||||
m_connected_ids.clear();
|
||||
CheckActive();
|
||||
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
|
||||
(void)SDL_SetGamepadLED(pad, 0, 0, 0);
|
||||
}
|
||||
|
||||
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
|
||||
@@ -847,7 +847,9 @@ void GameController::ApplyVibration() {
|
||||
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
|
||||
const auto large_motor = Scale(m_vibration[0], scale);
|
||||
const auto small_motor = Scale(m_vibration[1], scale);
|
||||
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
|
||||
// The DualSense haptics backend drives a single pad, so only player 1 uses it.
|
||||
if (m_player == 0 &&
|
||||
DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -999,49 +1001,149 @@ int GameController::ReadStates(ControllerState* states, int states_num, bool* fl
|
||||
return ret_num;
|
||||
}
|
||||
|
||||
// Keep every opened pad, including those waiting for a free player slot.
|
||||
// Caller holds g_router_mutex.
|
||||
static void RefreshPlayers() {
|
||||
std::array<bool, Config::MAX_PLAYERS> taken {};
|
||||
const int keyboard = KeyboardPlayer();
|
||||
if (g_keyboard_player != 0) {
|
||||
taken[keyboard] = true;
|
||||
}
|
||||
for (auto& [id, player]: g_pad_players) {
|
||||
if (g_keyboard_player != 0 && player == keyboard) {
|
||||
player = -1;
|
||||
}
|
||||
if (player >= 0) {
|
||||
taken[player] = true;
|
||||
}
|
||||
}
|
||||
for (auto& [id, player]: g_pad_players) {
|
||||
if (player >= 0) {
|
||||
continue;
|
||||
}
|
||||
const auto free = std::find(taken.begin(), taken.end(), false);
|
||||
if (free != taken.end()) {
|
||||
player = static_cast<int>(free - taken.begin());
|
||||
*free = true;
|
||||
LOGF("Controller %d assigned to player %d\n", id, player + 1);
|
||||
}
|
||||
}
|
||||
std::array<int, Config::MAX_PLAYERS> active;
|
||||
active.fill(-1);
|
||||
active[keyboard] = HOST_INPUT_CONTROLLER_ID;
|
||||
for (const auto& [id, player]: g_pad_players) {
|
||||
if (player >= 0) {
|
||||
active[player] = id;
|
||||
}
|
||||
}
|
||||
// Stop old owners before configuring new owners of the same physical pad.
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
const int previous = g_players[player]->GetActiveControllerId();
|
||||
if (previous != active[player]) {
|
||||
if (player == 0) {
|
||||
(void)DualSenseHaptics::SetVibration(previous, 0, 0, 0);
|
||||
}
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(previous))) {
|
||||
StopHostPad(pad);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
g_players[player]->SetDevices(active[player]);
|
||||
}
|
||||
}
|
||||
|
||||
void Connect(int id) {
|
||||
g_controller->Connect(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (std::any_of(g_pad_players.begin(), g_pad_players.end(),
|
||||
[id](const auto& pad) { return pad.first == id; })) {
|
||||
return;
|
||||
}
|
||||
g_pad_players.emplace_back(id, -1);
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void Disconnect(int id) {
|
||||
g_controller->Disconnect(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
std::erase_if(g_pad_players, [id](const auto& pad) { return pad.first == id; });
|
||||
RefreshPlayers();
|
||||
}
|
||||
|
||||
void CycleKeyboardPlayer() {
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
g_keyboard_player = (g_keyboard_player + 1) % (Config::MAX_PLAYERS + 1);
|
||||
RefreshPlayers();
|
||||
if (g_keyboard_player == 0) {
|
||||
LOGF("Keyboard shared with player 1\n");
|
||||
} else {
|
||||
LOGF("Keyboard assigned to player %u\n", g_keyboard_player);
|
||||
}
|
||||
}
|
||||
|
||||
void SetButton(int id, uint32_t button, bool down) {
|
||||
g_controller->Button(id, button, down);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Button(id, button, down);
|
||||
}
|
||||
}
|
||||
|
||||
void SetAxis(int id, Axis axis, int value) {
|
||||
g_controller->Axis(id, axis, value);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Axis(id, axis, value);
|
||||
}
|
||||
}
|
||||
|
||||
void SetRightStick(int id, int x, int y) {
|
||||
g_controller->RightStick(id, x, y);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->RightStick(id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void SetTouchPad(int id, int finger, bool down, float x, float y) {
|
||||
g_controller->TouchPad(id, finger, down, x, y);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->TouchPad(id, finger, down, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us) {
|
||||
g_controller->Motion(id, sensor, data, time_us);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->Motion(id, sensor, data, time_us);
|
||||
}
|
||||
}
|
||||
|
||||
void ToggleGyroEmulation(int id) {
|
||||
g_controller->ToggleGyroEmulation(id);
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
if (auto* player = PlayerForDevice(id)) {
|
||||
player->ToggleGyroEmulation(id);
|
||||
}
|
||||
}
|
||||
|
||||
void ResetInputState() {
|
||||
g_controller->ResetInputState();
|
||||
for (const auto& player: g_players) {
|
||||
if (player != nullptr) {
|
||||
player->ResetInputState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GetActiveControllerId() {
|
||||
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
|
||||
return g_players[0] != nullptr ? g_players[0]->GetActiveControllerId() : -1;
|
||||
}
|
||||
|
||||
static bool PadOpenArgsAreValid(int user_id, int type, int index) {
|
||||
// The host keyboard and gamepad feed one standard port for the configured user.
|
||||
return user_id == Config::GetUserId() && type == 0 && index == 0;
|
||||
bool IsPlayerConnected(int player) {
|
||||
if (player < 0 || player >= Config::MAX_PLAYERS || g_players[player] == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return g_players[player]->GetActiveControllerId() != -1;
|
||||
}
|
||||
|
||||
bool GameController::PadOpenArgsAreValid(int user_id, int type, int index) const {
|
||||
// The host keyboard and gamepads of a player feed one standard port for that player's user.
|
||||
return user_id == Config::GetPlayerUserId(m_player) && type == 0 && index == 0;
|
||||
}
|
||||
|
||||
int GameController::InitPort() {
|
||||
@@ -1064,7 +1166,7 @@ int GameController::OpenPort(int user_id, int type, int index) {
|
||||
return PAD_ERROR_ALREADY_OPENED;
|
||||
}
|
||||
m_port_state = PortState::Open;
|
||||
return PAD_HANDLE;
|
||||
return Handle();
|
||||
}
|
||||
|
||||
int GameController::GetPortHandle(int user_id, int type, int index) {
|
||||
@@ -1072,15 +1174,16 @@ int GameController::GetPortHandle(int user_id, int type, int index) {
|
||||
if (m_port_state == PortState::Uninitialized) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index) ? PAD_HANDLE
|
||||
: PAD_ERROR_NO_HANDLE;
|
||||
return m_port_state == PortState::Open && PadOpenArgsAreValid(user_id, type, index)
|
||||
? Handle()
|
||||
: PAD_ERROR_NO_HANDLE;
|
||||
}
|
||||
|
||||
int GameController::CheckPortHandleLocked(int handle) const {
|
||||
if (m_port_state == PortState::Uninitialized) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
return m_port_state == PortState::Open && handle == PAD_HANDLE ? OK : PAD_ERROR_INVALID_HANDLE;
|
||||
return m_port_state == PortState::Open && handle == Handle() ? OK : PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int GameController::CheckPortHandle(int handle) {
|
||||
@@ -1098,12 +1201,19 @@ int GameController::ClosePort(int handle) {
|
||||
}
|
||||
|
||||
int PadCheckHandle(int handle) {
|
||||
return g_controller->CheckPortHandle(handle);
|
||||
if (g_players[0] == nullptr) {
|
||||
return PAD_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
auto* player = PlayerForHandle(handle);
|
||||
return player != nullptr ? player->CheckPortHandle(handle) : PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadInit() {
|
||||
PRINT_NAME();
|
||||
return g_controller->InitPort();
|
||||
for (const auto& player: g_players) {
|
||||
(void)player->InitPort();
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
|
||||
@@ -1115,7 +1225,8 @@ int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param) {
|
||||
"\t param = 0x%016" PRIx64 "\n",
|
||||
user_id, type, index, reinterpret_cast<uint64_t>(param));
|
||||
|
||||
return g_controller->OpenPort(user_id, type, index);
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return player >= 0 ? g_players[player]->OpenPort(user_id, type, index) : PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
|
||||
@@ -1126,13 +1237,19 @@ int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index) {
|
||||
"\t index = %d\n",
|
||||
user_id, type, index);
|
||||
|
||||
return g_controller->GetPortHandle(user_id, type, index);
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return player >= 0 ? g_players[player]->GetPortHandle(user_id, type, index)
|
||||
: PAD_ERROR_NO_HANDLE;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadClose(int handle) {
|
||||
PRINT_NAME();
|
||||
LOGF("\t handle = %d\n", handle);
|
||||
return g_controller->ClosePort(handle);
|
||||
auto* player = PlayerForHandle(handle);
|
||||
if (player == nullptr) {
|
||||
return PAD_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
return player->ClosePort(handle);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
|
||||
@@ -1143,7 +1260,7 @@ int KYTY_SYSV_ABI PadSetMotionSensorState(int handle, bool enable) {
|
||||
}
|
||||
|
||||
LOGF("\t enable = %s\n", (enable ? "true" : "false"));
|
||||
g_controller->SetMotionSensorState(enable);
|
||||
PlayerForHandle(handle)->SetMotionSensorState(enable);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -1167,7 +1284,7 @@ int KYTY_SYSV_ABI PadResetOrientation(int handle) {
|
||||
return error;
|
||||
}
|
||||
|
||||
g_controller->ResetOrientation();
|
||||
PlayerForHandle(handle)->ResetOrientation();
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -1183,7 +1300,7 @@ int KYTY_SYSV_ABI PadGetControllerInformation(int handle, PadControllerInformati
|
||||
|
||||
int connected_count = 0;
|
||||
bool connected = false;
|
||||
g_controller->GetConnectionInfo(&connected, &connected_count);
|
||||
PlayerForHandle(handle)->GetConnectionInfo(&connected, &connected_count);
|
||||
|
||||
std::memset(info, 0, sizeof(*info));
|
||||
|
||||
@@ -1228,7 +1345,7 @@ int KYTY_SYSV_ABI PadReadState(int handle, PadData* data) {
|
||||
bool connected = false;
|
||||
ControllerState state;
|
||||
|
||||
g_controller->ReadState(&state, &connected, &connected_count);
|
||||
PlayerForHandle(handle)->ReadState(&state, &connected, &connected_count);
|
||||
|
||||
pad_fill_data(data, state, connected, connected_count);
|
||||
|
||||
@@ -1252,11 +1369,12 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
|
||||
bool connected = false;
|
||||
ControllerState states[64] = {};
|
||||
|
||||
int ret_num = g_controller->ReadStates(states, num, &connected, &connected_count);
|
||||
auto* player = PlayerForHandle(handle);
|
||||
int ret_num = player->ReadStates(states, num, &connected, &connected_count);
|
||||
|
||||
if (!connected || ret_num == 0) {
|
||||
if (connected) {
|
||||
g_controller->ReadState(&states[0], &connected, &connected_count);
|
||||
player->ReadState(&states[0], &connected, &connected_count);
|
||||
}
|
||||
ret_num = 1;
|
||||
}
|
||||
@@ -1270,6 +1388,7 @@ int KYTY_SYSV_ABI PadRead(int handle, PadData* data, int num) {
|
||||
|
||||
int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1282,7 +1401,7 @@ int KYTY_SYSV_ABI PadSetVibration(int handle, const PadVibrationParam* param) {
|
||||
"\t small_motor = %d\n",
|
||||
static_cast<int>(param->large_motor), static_cast<int>(param->small_motor));
|
||||
|
||||
g_controller->SetVibration(param->large_motor, param->small_motor);
|
||||
PlayerForHandle(handle)->SetVibration(param->large_motor, param->small_motor);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -1299,6 +1418,7 @@ int KYTY_SYSV_ABI PadResetLightBar(int handle) {
|
||||
|
||||
int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1307,13 +1427,14 @@ int KYTY_SYSV_ABI PadSetLightBar(int handle, const PadLightBarParam* param) {
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
g_controller->SetLightBar(param->r, param->g, param->b);
|
||||
PlayerForHandle(handle)->SetLightBar(param->r, param->g, param->b);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* param) {
|
||||
PRINT_NAME();
|
||||
Common::LockGuard lock(g_router_mutex);
|
||||
|
||||
if (const auto error = PadCheckHandle(handle); error != OK) {
|
||||
return error;
|
||||
@@ -1322,7 +1443,7 @@ int KYTY_SYSV_ABI PadSetTriggerEffect(int handle, const PadTriggerEffectParam* p
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
return g_controller->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
|
||||
return PlayerForHandle(handle)->SetTriggerEffect(*param) ? OK : PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInformation* info) {
|
||||
@@ -1337,7 +1458,7 @@ int KYTY_SYSV_ABI PadGetTriggerEffectState(int handle, PadTriggerEffectStateInfo
|
||||
return PAD_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
g_controller->GetTriggerEffectState(info);
|
||||
PlayerForHandle(handle)->GetTriggerEffectState(info);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ inline int controller_get_axis(int min, int max, int value) {
|
||||
|
||||
void Connect(int id);
|
||||
void Disconnect(int id);
|
||||
// Moves the keyboard to the next player (shared with player 1, then 1-4) while the game runs.
|
||||
void CycleKeyboardPlayer();
|
||||
void SetButton(int id, uint32_t button, bool down);
|
||||
void SetAxis(int id, Axis axis, int value);
|
||||
void SetRightStick(int id, int x, int y);
|
||||
@@ -76,6 +78,8 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
|
||||
void ToggleGyroEmulation(int id);
|
||||
void ResetInputState();
|
||||
int GetActiveControllerId();
|
||||
// True when the player has a keyboard or gamepad assigned.
|
||||
bool IsPlayerConnected(int player);
|
||||
|
||||
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <atomic>
|
||||
#include <cinttypes>
|
||||
#include <iterator>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
@@ -176,6 +179,14 @@ static int GetHostPathStat(const std::filesystem::path& host_path, LibKernel::Fi
|
||||
return OK;
|
||||
}
|
||||
|
||||
// Mounts can appear after a failed lookup (e.g. additional content), so forget negative results.
|
||||
void InvalidateFailedResolves() {
|
||||
std::scoped_lock lock(g_mutex);
|
||||
for (auto it = g_resolved_paths.begin(); it != g_resolved_paths.end();) {
|
||||
it = it->second.result != OK ? g_resolved_paths.erase(it) : std::next(it);
|
||||
}
|
||||
}
|
||||
|
||||
static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size) {
|
||||
if (guest_path == nullptr || guest_path[0] == '\0') {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
@@ -225,6 +236,10 @@ static int ResolveOnePath(const char* guest_path, uint32_t* id, uint64_t* size)
|
||||
log_missing = true;
|
||||
}
|
||||
}
|
||||
if (!log_missing && info.result == OK && path.starts_with("/addcont")) {
|
||||
LOGF("\tAPR resolve: %s -> %s (%" PRIu64 " bytes)\n", guest_path,
|
||||
Common::PathToString(info.host_path).c_str(), info.file_size);
|
||||
}
|
||||
if (log_missing) {
|
||||
LOGF("\tAPR resolve missing path: %s -> %s\n", guest_path, Common::PathToString(info.host_path).c_str());
|
||||
}
|
||||
@@ -1181,6 +1196,13 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
|
||||
uint64_t bytes_read = 0;
|
||||
auto result = ReadHostFileToGuest(host_path, command.file_offset, command.destination,
|
||||
command.size, &bytes_read);
|
||||
static std::atomic<uint32_t> s_logged_dlc_reads {0};
|
||||
if (host_path.filename() == "re_dlc_000.pak" && s_logged_dlc_reads.fetch_add(1) < 32) {
|
||||
LOGF("\tAPR read dlc: id=0x%08" PRIx32 ", offset=0x%016" PRIx64 ", size=0x%016" PRIx64
|
||||
", read=0x%016" PRIx64 ", result=0x%08" PRIx32 "\n",
|
||||
command.file_id, command.file_offset, command.size, bytes_read,
|
||||
static_cast<uint32_t>(result));
|
||||
}
|
||||
if (result != OK) {
|
||||
LOGF("\tAPR submit read failed: id=0x%08" PRIx32 ", result=0x%08" PRIx32 ", path=%s\n",
|
||||
command.file_id, static_cast<uint32_t>(result), Common::PathToString(host_path).c_str());
|
||||
|
||||
@@ -22,13 +22,16 @@
|
||||
|
||||
namespace Libs {
|
||||
|
||||
namespace AprShared {
|
||||
void InvalidateFailedResolves();
|
||||
}
|
||||
|
||||
LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
|
||||
|
||||
namespace AppContent {
|
||||
|
||||
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
|
||||
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
|
||||
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
|
||||
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
|
||||
@@ -207,7 +210,10 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
|
||||
|
||||
for (const auto& m: g_addcont_mounts) {
|
||||
if (m.label == entry->entitlement_label) {
|
||||
return APP_CONTENT_ERROR_BUSY;
|
||||
const auto guest_point = m.point.substr(0, m.point.size() - 1);
|
||||
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
|
||||
LOGF("\t already mounted %s\n", guest_point.c_str());
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +238,15 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
|
||||
LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
|
||||
Common::PathToString(entry->mount_dir).c_str());
|
||||
|
||||
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1);
|
||||
AprShared::InvalidateFailedResolves();
|
||||
|
||||
// Titles scan /download0/pdlc for downloaded content; it must exist even when empty.
|
||||
if (const auto pdlc = LibKernel::FileSystem::GetRealFilename("/download0/pdlc"); !pdlc.empty()) {
|
||||
std::filesystem::create_directories(pdlc, ec);
|
||||
}
|
||||
|
||||
const auto guest_point = mount.point.substr(0, mount.point.size() - 1);
|
||||
std::strncpy(mount_point->data, guest_point.c_str(), sizeof(mount_point->data) - 1);
|
||||
g_addcont_mounts.push_back(std::move(mount));
|
||||
|
||||
return OK;
|
||||
@@ -251,8 +265,8 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
|
||||
return APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
}
|
||||
|
||||
const std::string point(mount_point->data,
|
||||
strnlen(mount_point->data, sizeof(mount_point->data)));
|
||||
const std::string point = Common::FixDirectorySlash(
|
||||
std::string_view(mount_point->data, strnlen(mount_point->data, sizeof(mount_point->data))));
|
||||
|
||||
std::lock_guard lock(g_addcont_mutex);
|
||||
|
||||
@@ -263,6 +277,7 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
|
||||
}
|
||||
LibKernel::FileSystem::Umount(point);
|
||||
g_addcont_mounts.erase(it);
|
||||
AprShared::InvalidateFailedResolves();
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
+13
-7
@@ -11,6 +11,7 @@
|
||||
#include "kernel/eventQueue.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "kernel/memory.h"
|
||||
#include "kernel/module.h"
|
||||
#include "kernel/pthread.h"
|
||||
#include "kernel/semaphore.h"
|
||||
#include "kernel/syncOnAddress.h"
|
||||
@@ -65,7 +66,6 @@ LIB_DEFINE(InitLibKernel_1_Apr);
|
||||
|
||||
namespace LibKernel {
|
||||
|
||||
using KernelModule = int32_t;
|
||||
using get_thread_atexit_count_func_t = KYTY_SYSV_ABI int (*)(KernelModule);
|
||||
using thread_atexit_report_func_t = KYTY_SYSV_ABI void (*)(KernelModule);
|
||||
|
||||
@@ -1201,9 +1201,9 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res) {
|
||||
KernelModule KYTY_SYSV_ABI KernelLoadStartModule(const char* module_file_name, size_t args,
|
||||
const void* argp, uint32_t flags,
|
||||
const KernelLoadModuleOpt* opt, int* res) {
|
||||
PRINT_NAME();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
@@ -1255,9 +1255,9 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
|
||||
return static_cast<KernelModule>(handle);
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res) {
|
||||
int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args, const void* argp,
|
||||
uint32_t flags, const KernelUnloadModuleOpt* opt,
|
||||
int* res) {
|
||||
PRINT_NAME();
|
||||
|
||||
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||
@@ -2006,6 +2006,11 @@ int KYTY_SYSV_ABI listen(int s, int backlog) {
|
||||
return Network::Net::Listen(s, backlog);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI shutdown(int s, int how) {
|
||||
PRINT_NAME();
|
||||
return Network::Net::Shutdown(s, how);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI accept(int s, void* addr, uint32_t* addrlen) {
|
||||
PRINT_NAME();
|
||||
return Network::Net::Accept(s, addr, addrlen);
|
||||
@@ -2242,6 +2247,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
|
||||
LIB_FUNC("KuOmgKoqCdY", Posix::bind);
|
||||
LIB_FUNC("XVL8So3QJUk", Posix::connect);
|
||||
LIB_FUNC("pxnCmagrtao", Posix::listen);
|
||||
LIB_FUNC("TUuiYS2kE8s", Posix::shutdown);
|
||||
LIB_FUNC("3e+4Iv7IJ8U", Posix::accept);
|
||||
LIB_FUNC("RenI1lL1WFk", Posix::getsockname);
|
||||
LIB_FUNC("6O8EwYOgH9Y", Posix::getsockopt);
|
||||
|
||||
@@ -221,11 +221,17 @@ int KYTY_SYSV_ABI PlayGoGetChunkId(int handle, uint16_t* out_chunk_id_list,
|
||||
if (out_entries == nullptr) {
|
||||
return PLAYGO_ERROR_BAD_POINTER;
|
||||
}
|
||||
if (number_of_entries != 0 && out_chunk_id_list == nullptr) {
|
||||
return PLAYGO_ERROR_BAD_POINTER;
|
||||
if (out_chunk_id_list != nullptr && number_of_entries == 0) {
|
||||
return PLAYGO_ERROR_BAD_SIZE;
|
||||
}
|
||||
ensure_chunks_loaded();
|
||||
|
||||
// A null output buffer asks for the total chunk count.
|
||||
if (out_chunk_id_list == nullptr) {
|
||||
*out_entries = g_chunks_num;
|
||||
return OK;
|
||||
}
|
||||
|
||||
const uint32_t entries = std::min(number_of_entries, g_chunks_num);
|
||||
for (uint32_t i = 0; i < entries; i++) {
|
||||
out_chunk_id_list[i] = static_cast<uint16_t>(i);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/common.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
namespace LibRazorCpu {
|
||||
@@ -16,8 +16,18 @@ static KYTY_SYSV_ABI uint32_t RazorCpuIsCapturing() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI RazorCpuPopMarker() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI RazorCpuPushMarker(const char*, uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LIB_DEFINE(InitRazorCpu_1) {
|
||||
LIB_FUNC("EboejOQvLL4", LibRazorCpu::RazorCpuIsCapturing);
|
||||
LIB_FUNC("YpkGsMXP3ew", LibRazorCpu::RazorCpuPopMarker);
|
||||
LIB_FUNC("zw+celG7zSI", LibRazorCpu::RazorCpuPushMarker);
|
||||
}
|
||||
|
||||
} // namespace LibRazorCpu
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
@@ -300,15 +301,23 @@ static std::string memory_dir_name(uint32_t slot_id) {
|
||||
return "sce_sdmemory" + (slot_id == 0 ? std::string() : std::to_string(slot_id));
|
||||
}
|
||||
|
||||
static std::filesystem::path save_user_directory(std::string_view title_id, int32_t user_id) {
|
||||
auto directory = std::filesystem::path(SAVE_DATA_DIR);
|
||||
if (Config::IsGuestUserId(user_id)) {
|
||||
directory /= std::filesystem::path("[users]") / std::to_string(user_id);
|
||||
}
|
||||
return directory / title_id;
|
||||
}
|
||||
|
||||
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
|
||||
int32_t user_id) {
|
||||
const auto directory = std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name;
|
||||
for (uint32_t slot = 0; slot < 4; slot++) {
|
||||
if (dir_name == memory_dir_name(slot)) {
|
||||
return directory / std::to_string(user_id);
|
||||
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
|
||||
std::to_string(user_id);
|
||||
}
|
||||
}
|
||||
return directory;
|
||||
return save_user_directory(title_id, user_id) / dir_name;
|
||||
}
|
||||
|
||||
static std::filesystem::path memory_directory(int32_t user_id, uint32_t slot_id) {
|
||||
@@ -590,7 +599,7 @@ int KYTY_SYSV_ABI SaveDataDirNameSearch(const SaveDataDirNameSearchCond* cond,
|
||||
|
||||
Common::LockGuard lock(g_mount_mutex);
|
||||
const std::string title_id = cond->title_id != nullptr ? cond->title_id->data : get_title_id();
|
||||
const auto root = std::filesystem::path(SAVE_DATA_DIR) / title_id;
|
||||
const auto root = save_user_directory(title_id, cond->user_id);
|
||||
std::vector<std::string> dir_list;
|
||||
|
||||
if (Common::File::IsDirectoryExisting(root)) {
|
||||
|
||||
+47
-16
@@ -3,6 +3,7 @@
|
||||
#include "common/common.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "kernel/module.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
@@ -20,14 +21,40 @@ int KYTY_SYSV_ABI KernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleI
|
||||
|
||||
namespace Sysmodule {
|
||||
|
||||
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
|
||||
constexpr int SYSMODULE_ERROR_UNLOADED = static_cast<int>(0x805a1001u);
|
||||
constexpr int SYSMODULE_ERROR_FATAL = static_cast<int>(0x805a10ffu);
|
||||
|
||||
static std::mutex g_module_mutex;
|
||||
static std::unordered_map<uint16_t, uint32_t> g_module_refs;
|
||||
static constexpr struct NativeModule {
|
||||
uint16_t id;
|
||||
const char* path;
|
||||
} g_native_modules[] = {{0x0115, "/app0/sce_module/libSceNpCppWebApi.prx"}};
|
||||
|
||||
static void LoadModule(uint16_t id) {
|
||||
struct Module {
|
||||
uint32_t references = 0;
|
||||
LibKernel::KernelModule handle = -1;
|
||||
};
|
||||
|
||||
static std::mutex g_module_mutex;
|
||||
static std::unordered_map<uint16_t, Module> g_modules;
|
||||
|
||||
static int LoadModule(uint16_t id) {
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
++g_module_refs[id];
|
||||
if (const auto module = g_modules.find(id); module != g_modules.end()) {
|
||||
++module->second.references;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LibKernel::KernelModule handle = -1;
|
||||
for (const auto& module: g_native_modules) {
|
||||
if (module.id != id) continue;
|
||||
handle = LibKernel::KernelLoadStartModule(module.path, 0, nullptr, 0, nullptr, nullptr);
|
||||
if (handle < 0) {
|
||||
return handle;
|
||||
}
|
||||
break;
|
||||
}
|
||||
g_modules.emplace(id, Module {1, handle});
|
||||
return 0;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleGetModuleInfoForUnwind(uint64_t addr, int flags,
|
||||
@@ -40,8 +67,7 @@ static KYTY_SYSV_ABI int SysmoduleLoadModule(uint16_t id) {
|
||||
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
LoadModule(id);
|
||||
return 0;
|
||||
return LoadModule(id);
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
|
||||
@@ -50,12 +76,19 @@ static KYTY_SYSV_ABI int SysmoduleUnloadModule(uint16_t id) {
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
const auto module = g_module_refs.find(id);
|
||||
if (module == g_module_refs.end()) {
|
||||
const auto module = g_modules.find(id);
|
||||
if (module == g_modules.end()) {
|
||||
return SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
if (--module->second == 0) {
|
||||
g_module_refs.erase(module);
|
||||
if (module->second.references == 1) {
|
||||
if (module->second.handle >= 0 &&
|
||||
LibKernel::KernelStopUnloadModule(module->second.handle, 0, nullptr, 0, nullptr,
|
||||
nullptr) < 0) {
|
||||
return SYSMODULE_ERROR_FATAL;
|
||||
}
|
||||
g_modules.erase(id);
|
||||
} else {
|
||||
--module->second.references;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -71,10 +104,8 @@ static KYTY_SYSV_ABI int SysmoduleLoadModuleInternalWithArg(uint16_t id, int arg
|
||||
EXIT_IF(arg3 != 0);
|
||||
EXIT_IF(ret == nullptr);
|
||||
|
||||
LoadModule(id);
|
||||
*ret = 0;
|
||||
|
||||
return 0;
|
||||
*ret = LoadModule(id);
|
||||
return *ret;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
|
||||
@@ -83,7 +114,7 @@ static KYTY_SYSV_ABI int SysmoduleIsLoaded(uint16_t id) {
|
||||
LOGF("\t id = %d\n", static_cast<int>(id));
|
||||
|
||||
std::scoped_lock lock(g_module_mutex);
|
||||
return g_module_refs.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
|
||||
return g_modules.contains(id) ? 0 : SYSMODULE_ERROR_UNLOADED;
|
||||
}
|
||||
|
||||
} // namespace Sysmodule
|
||||
|
||||
@@ -258,6 +258,10 @@ static int KYTY_SYSV_ABI SystemServicePowerTick() {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceDisableMusicPlayer() {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI SystemServiceReportAbnormalTermination(const void* info) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -577,6 +581,7 @@ LIB_DEFINE(InitSystemService_1) {
|
||||
LIB_FUNC("8Lo6Zv94aho", SystemService::SystemServiceDisableNoticeScreenSkipFlagAutoSet);
|
||||
LIB_FUNC("Q3utJvma4Mo", SystemService::SystemServiceSetNoticeScreenSkipFlag);
|
||||
LIB_FUNC("XbbJC3E+L5M", SystemService::SystemServicePowerTick);
|
||||
LIB_FUNC("x1UB9bwDSOw", SystemService::SystemServiceDisableMusicPlayer);
|
||||
LIB_FUNC("3s8cHiCBKBE", SystemService::SystemServiceReportAbnormalTermination);
|
||||
SystemGesture::InitSystemGesture_1(s);
|
||||
}
|
||||
|
||||
+12
-3
@@ -105,6 +105,8 @@ struct TrophyState {
|
||||
const Trophies::Package* package = nullptr;
|
||||
Trophies::UnlockData unlocks;
|
||||
std::filesystem::path path;
|
||||
// Guests (players 2+) have no profile: nothing is loaded, saved or announced.
|
||||
bool guest = false;
|
||||
};
|
||||
|
||||
struct TrophyContext {
|
||||
@@ -153,9 +155,12 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
|
||||
package.event_rules.size(), uds_filename.c_str());
|
||||
}
|
||||
}
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = Trophies::LoadUnlockData(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
|
||||
const bool guest = Config::IsGuestUserId(key.first);
|
||||
auto path = guest ? std::filesystem::path {}
|
||||
: Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = guest ? Trophies::UnlockData {} : Trophies::LoadUnlockData(path);
|
||||
return &g_states
|
||||
.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path), guest})
|
||||
.first->second;
|
||||
}
|
||||
|
||||
@@ -240,6 +245,10 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
|
||||
}
|
||||
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
|
||||
if (state.guest) {
|
||||
LOGF("[Trophy] guest users cannot earn trophies (trophy %d)\n", trophy_id);
|
||||
return;
|
||||
}
|
||||
const auto found = state.package->trophies.find(trophy_id);
|
||||
if (found == state.package->trophies.end()) {
|
||||
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
|
||||
|
||||
+54
-27
@@ -1,12 +1,14 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/localization.h"
|
||||
#include "libs/controller.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
@@ -39,6 +41,15 @@ struct UserServiceGamePresets {
|
||||
|
||||
static_assert(sizeof(UserServiceGamePresets) == 40);
|
||||
|
||||
static bool IsPlayerLoggedIn(int player) {
|
||||
return player == 0 || (player > 0 && Controller::IsPlayerConnected(player));
|
||||
}
|
||||
|
||||
static int GetLoggedInPlayer(int user_id) {
|
||||
const int player = Config::GetPlayerFromUserId(user_id);
|
||||
return IsPlayerLoggedIn(player) ? player : -1;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceInitialize(const void* /*params*/) {
|
||||
PRINT_NAME();
|
||||
|
||||
@@ -66,13 +77,16 @@ static KYTY_SYSV_ABI int UserServiceGetEvent(SceUserServiceEvent* event) {
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(event == nullptr);
|
||||
|
||||
static bool logged_in = false;
|
||||
static bool logged_in[Config::MAX_PLAYERS] = {};
|
||||
|
||||
if (!logged_in) {
|
||||
logged_in = true;
|
||||
event->event_type = UserServiceEventTypeLogin;
|
||||
event->user_id = Config::GetUserId();
|
||||
return OK;
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
const bool connected = IsPlayerLoggedIn(player);
|
||||
if (connected != logged_in[player]) {
|
||||
logged_in[player] = connected;
|
||||
event->event_type = connected ? UserServiceEventTypeLogin : UserServiceEventTypeLogout;
|
||||
event->user_id = Config::GetPlayerUserId(player);
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
return USER_SERVICE_ERROR_NO_EVENT;
|
||||
@@ -83,24 +97,36 @@ static KYTY_SYSV_ABI int UserServiceGetLoginUserIdList(UserServiceLoginUserIdLis
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(user_id_list == nullptr);
|
||||
|
||||
user_id_list->user_id[0] = Config::GetUserId();
|
||||
user_id_list->user_id[1] = -1;
|
||||
user_id_list->user_id[2] = -1;
|
||||
user_id_list->user_id[3] = -1;
|
||||
int count = 0;
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
if (IsPlayerLoggedIn(player)) {
|
||||
user_id_list->user_id[count++] = Config::GetPlayerUserId(player);
|
||||
}
|
||||
}
|
||||
while (count < Config::MAX_PLAYERS) {
|
||||
user_id_list->user_id[count++] = -1;
|
||||
}
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
static KYTY_SYSV_ABI int UserServiceGetUserName(int user_id, char* name, size_t size) {
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (name == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(size < 5);
|
||||
|
||||
const auto& user_name = Config::GetUserName();
|
||||
int s = snprintf(name, size, "%s", user_name.c_str());
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(static_cast<size_t>(s) >= size);
|
||||
const auto user_name =
|
||||
player > 0 ? std::string(Common::Localization::Get(Config::GetConsoleLanguage(), "user.guest")) +
|
||||
" " + std::to_string(player + 1)
|
||||
: Config::GetUserName();
|
||||
if (size <= user_name.size()) {
|
||||
return USER_SERVICE_ERROR_BUFFER_TOO_SHORT;
|
||||
}
|
||||
std::memcpy(name, user_name.c_str(), user_name.size() + 1);
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -111,11 +137,12 @@ static KYTY_SYSV_ABI int UserServiceGetUserNumber(int user_id, int32_t* number)
|
||||
if (number == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
const int player = GetLoggedInPlayer(user_id);
|
||||
if (player < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
*number = 1;
|
||||
*number = player + 1;
|
||||
|
||||
return OK;
|
||||
}
|
||||
@@ -126,7 +153,7 @@ static KYTY_SYSV_ABI int UserServiceGetGamePresets(int user_id, UserServiceGameP
|
||||
if (presets == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -147,7 +174,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityVibration(int user_id, int32
|
||||
if (vibration == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -163,7 +190,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityTriggerEffect(int user_
|
||||
if (trigger_effect == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -178,7 +205,7 @@ static KYTY_SYSV_ABI int UserServiceGetAgeLevel(int user_id, uint32_t* age_level
|
||||
if (age_level == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -194,7 +221,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityChatTranscription(int u
|
||||
if (chat_transcription == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -210,7 +237,7 @@ UserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hol
|
||||
if (press_and_hold_delay == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -226,7 +253,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomEnabled(int user_id
|
||||
if (zoom_enabled == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
@@ -242,7 +269,7 @@ static KYTY_SYSV_ABI int UserServiceGetAccessibilityZoomFollowFocus(int use
|
||||
if (zoom_follow_focus == nullptr) {
|
||||
return USER_SERVICE_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (user_id != Config::GetUserId()) {
|
||||
if (GetLoggedInPlayer(user_id) < 0) {
|
||||
return USER_SERVICE_ERROR_NOT_LOGGED_IN;
|
||||
}
|
||||
|
||||
|
||||
+26
-13
@@ -891,6 +891,7 @@ struct P2pEndpoint {
|
||||
uint16_t virtual_port = 0;
|
||||
int nonblocking = 0;
|
||||
int broadcast = 0;
|
||||
int use_crypto = 0;
|
||||
int send_buffer = 9216;
|
||||
int receive_buffer = 40 * 1024;
|
||||
};
|
||||
@@ -1095,6 +1096,7 @@ static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||
case 0x1001: return &endpoint.send_buffer;
|
||||
case 0x1002: return &endpoint.receive_buffer;
|
||||
case 0x1200: return &endpoint.nonblocking;
|
||||
case 0x20000: return &endpoint.use_crypto;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -2096,19 +2098,15 @@ int KYTY_SYSV_ABI Shutdown(int s, int how) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (::shutdown(socket, how) == SOCKET_ERROR) {
|
||||
if (::shutdown(socket, how) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (how == SD_RECEIVE || how == SD_BOTH) {
|
||||
transport->receive_shutdown.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
return 0;
|
||||
#else
|
||||
*Posix::GetErrorAddr() = Posix::POSIX_ENOSYS;
|
||||
return -1;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI Getsockname(int s, void* addr, uint32_t* addrlen) {
|
||||
@@ -2463,9 +2461,9 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
return -1;
|
||||
}
|
||||
|
||||
NativeSocket socket = INVALID_NATIVE_SOCKET;
|
||||
std::shared_ptr<SocketTransport> transport;
|
||||
if (!GetSocketBackend(s, &socket, nullptr, &transport)) {
|
||||
NativeSocket socket = INVALID_NATIVE_SOCKET;
|
||||
SocketSlot state;
|
||||
if (!GetSocketBackend(s, &socket, &state)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -2473,6 +2471,21 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
if (host_flags < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (state.p2p) {
|
||||
if (!state.p2p->bound || (!state.p2p->nonblocking && (flags & 0x80) == 0)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
|
||||
}
|
||||
char pending;
|
||||
if (::recv(socket, &pending, sizeof(pending), MSG_PEEK) < 0) {
|
||||
SetHostSocketError();
|
||||
// Winsock reports a peeked datagram's truncation as EMSGSIZE.
|
||||
if (*Posix::GetErrorAddr() != Posix::POSIX_EMSGSIZE) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Pending UDPP2P packets still require framing and optional decryption.
|
||||
return SetGuestSocketError(Posix::POSIX_EOPNOTSUPP);
|
||||
}
|
||||
|
||||
const auto host_len = static_cast<SocketIoLength>(
|
||||
std::min<uint64_t>(len, std::numeric_limits<SocketIoLength>::max()));
|
||||
@@ -2480,12 +2493,12 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
SocketLength host_addrlen = sizeof(host_addr);
|
||||
int64_t result = 0;
|
||||
#if defined(_WIN32)
|
||||
if (transport->type == SOCK_STREAM) {
|
||||
if (state.transport->type == SOCK_STREAM) {
|
||||
if (addr != nullptr &&
|
||||
::getpeername(socket, reinterpret_cast<sockaddr*>(&host_addr), &host_addrlen) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
result = ReceiveStream(*transport, static_cast<char*>(buf), host_len, host_flags,
|
||||
result = ReceiveStream(*state.transport, static_cast<char*>(buf), host_len, host_flags,
|
||||
(flags & 0x80) != 0);
|
||||
if (result < 0) {
|
||||
return result;
|
||||
@@ -2493,7 +2506,7 @@ int64_t KYTY_SYSV_ABI Recvfrom(int s, void* buf, uint64_t len, int flags, void*
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
transport.reset();
|
||||
state.transport.reset();
|
||||
if (addr == nullptr) {
|
||||
result = ::recv(socket, static_cast<char*>(buf), host_len, host_flags);
|
||||
} else {
|
||||
|
||||
+11
-9
@@ -58,16 +58,17 @@ struct TlsRegStub {
|
||||
}
|
||||
static uint64_t GetSize() { return 32; }
|
||||
|
||||
// sub rsp,0x80
|
||||
// lea rsp,[rsp-0x80]
|
||||
// nop
|
||||
// push rax
|
||||
// call safe_call
|
||||
// mov <reg>,rax
|
||||
// pop rax
|
||||
// add rsp,0x80
|
||||
// lea rsp,[rsp+0x80]
|
||||
// ret
|
||||
uint8_t code[32] = {0x48, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00, 0x50, 0xE8, 0x00, 0x00,
|
||||
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x81, 0xC4, 0x80, 0x00,
|
||||
0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
|
||||
uint8_t code[32] = {0x48, 0x8D, 0x64, 0x24, 0x80, 0x66, 0x90, 0x50, 0xE8, 0x00, 0x00,
|
||||
0x00, 0x00, 0x48, 0x89, 0xC0, 0x58, 0x48, 0x8D, 0xA4, 0x24, 0x80,
|
||||
0x00, 0x00, 0x00, 0xC3, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
|
||||
};
|
||||
|
||||
struct SafeCall {
|
||||
@@ -78,7 +79,8 @@ struct SafeCall {
|
||||
static uint64_t GetSize() { return 0x1000; }
|
||||
|
||||
uint8_t code[0x90] = {
|
||||
/*00*/ 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, // sub rsp,0x80
|
||||
/*00*/ 0x48, 0x8d, 0x64, 0x24, 0x80, // lea rsp,[rsp-0x80]
|
||||
/*05*/ 0x66, 0x90, // nop
|
||||
/*07*/ 0x9c, // pushfq
|
||||
/*08*/ 0x51, // push rcx
|
||||
/*09*/ 0x52, // push rdx
|
||||
@@ -108,9 +110,9 @@ struct SafeCall {
|
||||
/*40*/ 0x5a, // pop rdx
|
||||
/*41*/ 0x59, // pop rcx
|
||||
/*42*/ 0x9d, // popfq
|
||||
/*43*/ 0x48, 0x81, 0xc4, 0x80, 0x00, 0x00,
|
||||
0x00, // add rsp,0x80
|
||||
/*4a*/ 0xc3, // ret
|
||||
/*43*/ 0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00,
|
||||
0x00, // lea rsp,[rsp+0x80]
|
||||
/*4b*/ 0xc3, // ret
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ static void PrintUsage() {
|
||||
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
|
||||
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 50.\n");
|
||||
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
|
||||
::printf(" --keyboard-player <0-4> Keyboard player; 0 shares player 1 with the gamepad. Default: 0.\n");
|
||||
::printf(
|
||||
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
|
||||
::printf(
|
||||
@@ -378,6 +379,12 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
::printf("invalid controller vibration intensity: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--keyboard-player") {
|
||||
if (!ParseUint32(value, options.config.keyboard_player) ||
|
||||
options.config.keyboard_player > static_cast<uint32_t>(Config::MAX_PLAYERS)) {
|
||||
::printf("invalid keyboard player: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--present-mode") {
|
||||
if (!ParseEnum(value, options.config.present_mode)) {
|
||||
::printf("invalid present mode: %s\n", value.c_str());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -23,6 +24,9 @@ std::vector<Rumble> rumble;
|
||||
std::vector<Rumble> haptics;
|
||||
std::vector<std::array<Uint8, 32>> effects;
|
||||
bool haptics_handles_rumble = false;
|
||||
std::array<Rumble, 6> pad_rumble {};
|
||||
std::vector<int> closed_pads;
|
||||
std::array<std::array<Uint8, 3>, 6> pad_colors {};
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -30,14 +34,15 @@ Uint64 GetTicks() {
|
||||
return now;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
return id >= 1 && id <= 5 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
|
||||
return SDL_GAMEPAD_TYPE_PS5;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
rumble.push_back({large, small, duration});
|
||||
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = rumble.back();
|
||||
return true;
|
||||
}
|
||||
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
@@ -47,7 +52,8 @@ bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
|
||||
effects.push_back(effect);
|
||||
return true;
|
||||
}
|
||||
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
|
||||
bool SetGamepadLED(SDL_Gamepad* pad, Uint8 r, Uint8 g, Uint8 b) {
|
||||
pad_colors[reinterpret_cast<uintptr_t>(pad)] = {r, g, b};
|
||||
return true;
|
||||
}
|
||||
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
@@ -56,7 +62,9 @@ bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
|
||||
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
|
||||
return true;
|
||||
}
|
||||
void CloseGamepad(SDL_Gamepad*) {}
|
||||
void CloseGamepad(SDL_Gamepad* pad) {
|
||||
closed_pads.push_back(static_cast<int>(reinterpret_cast<uintptr_t>(pad)));
|
||||
}
|
||||
void Delay(Uint32) {}
|
||||
} // namespace Fake
|
||||
|
||||
@@ -368,6 +376,10 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
"speaker volume resent controller effects");
|
||||
Connect(2);
|
||||
Disconnect(1);
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID,
|
||||
"disconnecting player 1 stole player 2's pad");
|
||||
Disconnect(2);
|
||||
Connect(2);
|
||||
Check(GetActiveControllerId() == 2, "active controller did not change");
|
||||
haptics.clear();
|
||||
effects.clear();
|
||||
@@ -394,6 +406,126 @@ void TestIndependentOutputsAndPadSwitch() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestPlayerUserIds() {
|
||||
Check(Config::IsConfiguredUserIdValid(Config::MAX_USER_ID) &&
|
||||
!Config::IsConfiguredUserIdValid(Config::MAX_USER_ID + 1) &&
|
||||
!Config::IsConfiguredUserIdValid(254) && !Config::IsConfiguredUserIdValid(255),
|
||||
"configured user IDs did not reserve a valid range for guests");
|
||||
Config::ConfigOptions options;
|
||||
options.user_id = Config::MAX_USER_ID;
|
||||
Config::Load(options);
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(Config::GetPlayerFromUserId(Config::GetPlayerUserId(player)) == player,
|
||||
"player user ID did not round-trip at the upper limit");
|
||||
}
|
||||
Check(Config::GetPlayerUserId(3) == std::numeric_limits<int32_t>::max() &&
|
||||
Config::GetPlayerFromUserId(-1) == -1,
|
||||
"guest user ID overflowed or accepted an unknown user");
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestMultiplayerRouting() {
|
||||
Controller controller;
|
||||
for (int player = 1; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"a guest could not open its own pad port");
|
||||
}
|
||||
for (int id = 2; id <= 5; id++) {
|
||||
Connect(id);
|
||||
}
|
||||
Connect(2); // Duplicate device reports must not consume another port.
|
||||
const auto read = [](int handle) {
|
||||
PadData data {};
|
||||
Check(PadReadState(handle, &data) == OK, "player pad read failed");
|
||||
return data;
|
||||
};
|
||||
SetButton(1, PAD_BUTTON_CROSS, true);
|
||||
SetButton(2, PAD_BUTTON_CIRCLE, true);
|
||||
SetButton(5, PAD_BUTTON_TRIANGLE, true);
|
||||
Check(read(1).buttons == PAD_BUTTON_CROSS && read(2).buttons == PAD_BUTTON_CIRCLE &&
|
||||
read(3).buttons == 0 && read(4).buttons == 0,
|
||||
"physical pads did not route independently or overflow reached a player");
|
||||
|
||||
SetRumble(100, 50);
|
||||
CycleKeyboardPlayer(); // Dedicated player 1; physical pad 1 waits.
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID &&
|
||||
read(2).buttons == PAD_BUTTON_CIRCLE && pad_rumble[1].large == 0 &&
|
||||
pad_rumble[1].small == 0,
|
||||
"keyboard reassignment changed an unaffected player or left pad output "
|
||||
"running");
|
||||
for (int mode = 2; mode <= Config::MAX_PLAYERS; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
CycleKeyboardPlayer(); // Shared player 1; all original pads must return.
|
||||
ResetInputState();
|
||||
for (int id = 1; id <= 5; id++) {
|
||||
SetButton(id, 1u << id, true);
|
||||
}
|
||||
for (int handle = 1; handle <= Config::MAX_PLAYERS; handle++) {
|
||||
Check(read(handle).buttons == (1u << handle),
|
||||
"cycling keyboard players lost or duplicated a connected pad");
|
||||
}
|
||||
|
||||
// Moving the keyboard clears its held input from the old player.
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
SetButton(HOST_INPUT_CONTROLLER_ID, PAD_BUTTON_TRIANGLE, true);
|
||||
CycleKeyboardPlayer(); // Keyboard now moves from player 2 to player 3.
|
||||
Check((read(2).buttons & PAD_BUTTON_TRIANGLE) == 0,
|
||||
"moving the keyboard retained a held button on its old player");
|
||||
CycleKeyboardPlayer();
|
||||
CycleKeyboardPlayer();
|
||||
Disconnect(2);
|
||||
SetButton(5, PAD_BUTTON_SQUARE, true);
|
||||
Check(read(2).buttons == PAD_BUTTON_SQUARE && GetActiveControllerId() == 1,
|
||||
"waiting pad did not fill a free slot or an unaffected pad moved");
|
||||
Disconnect(99);
|
||||
Check(PadCheckHandle(std::numeric_limits<int>::min()) == PAD_ERROR_INVALID_HANDLE &&
|
||||
PadClose(std::numeric_limits<int>::max()) == PAD_ERROR_INVALID_HANDLE,
|
||||
"an extreme handle aliased a player port");
|
||||
closed_pads.clear();
|
||||
EmergencyShutdown();
|
||||
std::sort(closed_pads.begin(), closed_pads.end());
|
||||
Check(closed_pads == std::vector<int> {1, 3, 4, 5},
|
||||
"shutdown did not close each registered host pad exactly once");
|
||||
}
|
||||
|
||||
void TestDedicatedKeyboardOutputs() {
|
||||
Config::ConfigOptions options;
|
||||
options.keyboard_player = 1;
|
||||
options.controller_color = Config::ControllerColor {30, 40, 50};
|
||||
Config::Load(options);
|
||||
Initialize();
|
||||
PadInit();
|
||||
for (int player = 0; player < Config::MAX_PLAYERS; player++) {
|
||||
Check(PadOpen(Config::GetPlayerUserId(player), 0, 0, nullptr) == player + 1,
|
||||
"dedicated keyboard port did not open");
|
||||
}
|
||||
Connect(1);
|
||||
Connect(2);
|
||||
const PadVibrationParam request {100, 50};
|
||||
Check(PadSetVibration(3, &request) == OK, "player three vibration failed");
|
||||
SetButton(2, PAD_BUTTON_CROSS, true);
|
||||
CycleKeyboardPlayer(); // Pad 1 moves from player 2 to player 1.
|
||||
PadData data {};
|
||||
Check(GetActiveControllerId() == 1 && pad_colors[1] == *options.controller_color,
|
||||
"moving a pad to an earlier slot cleared its configured lightbar");
|
||||
Check(PadReadState(3, &data) == OK && data.buttons == PAD_BUTTON_CROSS &&
|
||||
pad_rumble[2].large == 100 * 257 && pad_rumble[2].small == 50 * 257,
|
||||
"keyboard reassignment reset an unaffected player's input or output");
|
||||
haptics_handles_rumble = true;
|
||||
SetRumble(200, 100);
|
||||
for (int mode = 0; mode < 4; mode++) {
|
||||
CycleKeyboardPlayer();
|
||||
}
|
||||
Check(GetActiveControllerId() == HOST_INPUT_CONTROLLER_ID && haptics.back().large == 0 &&
|
||||
haptics.back().small == 0 && haptics.back().duration == 0,
|
||||
"moving player one's pad retained its DualSense vibration request");
|
||||
Shutdown();
|
||||
Config::Load(Config::ConfigOptions {});
|
||||
}
|
||||
|
||||
void TestTriggerEffectState() {
|
||||
Controller controller;
|
||||
PadTriggerEffectStateInformation info {{-1, -1}};
|
||||
@@ -553,6 +685,7 @@ void TestTriggerEffectState() {
|
||||
|
||||
int main() {
|
||||
Config::Initialize();
|
||||
TestPlayerUserIds();
|
||||
TestPadPortOwnership();
|
||||
TestSettingCycles();
|
||||
TestGlobalControllerLevels();
|
||||
@@ -561,6 +694,8 @@ int main() {
|
||||
TestMaskedTriggersAndValidation();
|
||||
TestIndependentOutputsAndPadSwitch();
|
||||
TestTriggerEffectState();
|
||||
TestMultiplayerRouting();
|
||||
TestDedicatedKeyboardOutputs();
|
||||
Config::Shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -235,6 +235,9 @@ void TestSysmoduleReferences() {
|
||||
const auto unload = resolve("eR2bZFAAU0Q");
|
||||
const auto is_loaded = resolve("fMP5NHUOaMk");
|
||||
constexpr int unloaded = static_cast<int>(0x805a1001u);
|
||||
Check(load(0x115) == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded &&
|
||||
unload(0x115) == unloaded,
|
||||
"missing packaged NpCppWebApi fails without recording a loaded module");
|
||||
Check(load(0x113) == OK && is_loaded(0xb4) == unloaded,
|
||||
"loading entitlement access does not mark AppContent loaded");
|
||||
Check(unload(0x113) == OK, "release independent module reference");
|
||||
@@ -255,6 +258,9 @@ void TestSysmoduleReferences() {
|
||||
reinterpret_cast<int (KYTY_SYSV_ABI *)(uint16_t, int, int, int, int*)>(
|
||||
internal_symbol->vaddr);
|
||||
int result = -1;
|
||||
Check(internal_load(0x115, 0, 0, 0, &result) == Libs::LibKernel::KERNEL_ERROR_ENOENT &&
|
||||
result == Libs::LibKernel::KERNEL_ERROR_ENOENT && is_loaded(0x115) == unloaded,
|
||||
"internal module loading propagates missing native module failure");
|
||||
Check(internal_load(0xb4, 0, 0, 0, &result) == OK && result == OK &&
|
||||
is_loaded(0xb4) == OK && unload(0xb4) == OK && is_loaded(0xb4) == unloaded,
|
||||
"internal and public module operations share load state");
|
||||
@@ -407,7 +413,9 @@ void TestAddContent(const std::filesystem::path& root) {
|
||||
Check(mount(0, nullptr, &point) == static_cast<int>(0x80d90002u) &&
|
||||
list(0, nullptr, 0, nullptr) == static_cast<int>(0x817d0002u),
|
||||
"null add-on parameters return parameter errors");
|
||||
Check(mount(0, &info[3].label, &point) == OK, "mount owned add-on");
|
||||
Check(mount(0, &info[3].label, &point) == OK &&
|
||||
!std::string_view(point.data).ends_with('/'),
|
||||
"mount owned add-on without trailing slash");
|
||||
const std::string mounted_file = std::string(point.data) + "/payload.txt";
|
||||
const int fd = FileSystem::KernelOpen(mounted_file.c_str(), 0, 0);
|
||||
std::array<char, 11> payload {};
|
||||
@@ -416,8 +424,9 @@ void TestAddContent(const std::filesystem::path& root) {
|
||||
FileSystem::KernelClose(fd) == OK,
|
||||
"guest reads files from the selected title's add-on");
|
||||
MountPoint repeated {};
|
||||
Check(mount(0, &info[3].label, &repeated) == static_cast<int>(0x80d90003u),
|
||||
"duplicate mount returns BUSY");
|
||||
Check(mount(0, &info[3].label, &repeated) == OK &&
|
||||
std::strcmp(repeated.data, point.data) == 0,
|
||||
"duplicate mount returns the same mount point");
|
||||
Check(unmount(&point) == OK && FileSystem::GetRealFilename(mounted_file).empty() &&
|
||||
unmount(&point) == static_cast<int>(0x80d90004u),
|
||||
"unmount removes mapping and repeated unmount reports NOT_MOUNTED");
|
||||
@@ -1896,6 +1905,11 @@ void CheckSocketWakeup() {
|
||||
namespace Net = Libs::Network::Net;
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
Libs::InitLibKernel_1(&symbols);
|
||||
const auto *shutdown_symbol = symbols.Find(
|
||||
{"TUuiYS2kE8s", "Posix", 1, "libkernel", 1, 1, Loader::SymbolType::Func});
|
||||
Check(shutdown_symbol != nullptr, "POSIX shutdown export resolves");
|
||||
const auto shutdown = reinterpret_cast<int (KYTY_SYSV_ABI *)(int, int)>(shutdown_symbol->vaddr);
|
||||
const auto *connect_symbol = symbols.Find(
|
||||
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *getsockopt_symbol = symbols.Find(
|
||||
@@ -2136,6 +2150,76 @@ void CheckSocketWakeup() {
|
||||
CheckSocketReceiveBuffer(reader, writer);
|
||||
CheckSocketReceiveConcurrency();
|
||||
#endif
|
||||
const auto [shutdown_reader, shutdown_writer] = CreateTcpPair();
|
||||
const auto ready_to_read = [](int fd) {
|
||||
std::array<uint64_t, 16> readable {};
|
||||
readable[fd / 64] = uint64_t {1} << (fd % 64);
|
||||
const std::array<int64_t, 2> timeout {2, 0};
|
||||
return Net::Select(fd + 1, readable.data(), nullptr, nullptr, timeout.data()) == 1;
|
||||
};
|
||||
Check(shutdown(-1, 1) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF &&
|
||||
shutdown(shutdown_writer, 3) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"shutdown rejects invalid descriptors and directions");
|
||||
Check(Net::Send(shutdown_writer, "x", 1, 0) == 1 && shutdown(shutdown_writer, 1) == 0 &&
|
||||
ready_to_read(shutdown_reader) &&
|
||||
Net::Recv(shutdown_reader, received.data(), 1, 0) == 1 && received[0] == 'x' &&
|
||||
ready_to_read(shutdown_reader) &&
|
||||
Net::Recv(shutdown_reader, received.data(), 1, 0) == 0 &&
|
||||
Net::Send(shutdown_reader, "y", 1, 0) == 1 &&
|
||||
ready_to_read(shutdown_writer) &&
|
||||
Net::Recv(shutdown_writer, received.data(), 1, 0) == 1 && received[0] == 'y',
|
||||
"write shutdown drains queued bytes, signals EOF, and preserves reverse traffic");
|
||||
Check(Net::SocketClose(shutdown_reader) == 0 && Net::SocketClose(shutdown_writer) == 0,
|
||||
"close half-shutdown sockets");
|
||||
const int encrypted = Net::Socket(2, 6, 0);
|
||||
int crypto = -1;
|
||||
uint32_t crypto_size = sizeof(crypto);
|
||||
Check(encrypted >= 0 &&
|
||||
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
|
||||
crypto == 0,
|
||||
"UDPP2P encryption defaults to disabled");
|
||||
for (const int setting : {7, 0}) {
|
||||
Check(Net::Setsockopt(encrypted, 0xffff, 0x20000, &setting, sizeof(setting)) == 0 &&
|
||||
Net::Getsockopt(encrypted, 0xffff, 0x20000, &crypto, &crypto_size) == 0 &&
|
||||
crypto == (setting != 0),
|
||||
"UDPP2P encryption configuration retains normalized enable and disable values");
|
||||
Check(Net::Send(encrypted, "x", 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP,
|
||||
"UDPP2P configuration never exposes unframed plaintext transport");
|
||||
}
|
||||
std::array<uint8_t, 16> p2p_address {16, 2, 0x0e, 0x4a, 127, 0, 0, 1, 0x1e, 0x61};
|
||||
Check(Net::Bind(encrypted, p2p_address.data(), p2p_address.size()) == 0,
|
||||
"bind UDPP2P physical and virtual ports");
|
||||
char untouched = '!';
|
||||
auto source = p2p_address;
|
||||
uint32_t source_size = source.size();
|
||||
Check(Net::Recvfrom(encrypted, &untouched, 1, 0x80, source.data(), &source_size) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"per-call nonblocking UDPP2P receive reports an actually empty queue");
|
||||
Check(Net::Setsockopt(encrypted, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0 &&
|
||||
Net::Setsockopt(encrypted, 0xffff, 0x20000, &enabled, sizeof(enabled)) == 0 &&
|
||||
Net::Recvfrom(encrypted, &untouched, 1, 0, source.data(), &source_size) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
|
||||
untouched == '!' && source == p2p_address && source_size == source.size(),
|
||||
"empty encrypted nonblocking receive preserves all caller outputs");
|
||||
const int raw_sender = Net::Socket(2, 2, 0);
|
||||
Check(raw_sender >= 0 &&
|
||||
Net::Sendto(raw_sender, "pending", 7, 0, p2p_address.data(), p2p_address.size()) == 7,
|
||||
"queue a datagram requiring unsupported UDPP2P processing");
|
||||
const auto pending_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK &&
|
||||
std::chrono::steady_clock::now() < pending_deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
Check(*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP &&
|
||||
Net::Recv(encrypted, &untouched, 1, 0) == -1 &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EOPNOTSUPP && untouched == '!',
|
||||
"queued packets remain queued and unsupported instead of masquerading as empty");
|
||||
Check(Net::SocketClose(encrypted) == 0 && Net::SocketClose(raw_sender) == 0,
|
||||
"close UDPP2P readiness fixtures");
|
||||
Check(Net::SocketClose(writer) == 0, "close wake writer");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_recv(reader, received.data(), received.size(), 0) == 0 &&
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user