mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 10:56:58 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7cfe5a2f7 | |||
| 7415febd93 | |||
| 7492cb9577 | |||
| 60e53a39cc | |||
| 1b21161aac | |||
| 1195af2510 | |||
| 9a5658c038 | |||
| 199d0a9d92 | |||
| e679486ed3 | |||
| 6ef406542c | |||
| 35c6e8fff6 | |||
| 4852721005 | |||
| fb7ff7ec03 |
+14
-2
@@ -413,7 +413,16 @@ add_executable(archive_file_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(archive_file_tests common ZArchive::zarchive)
|
||||
target_include_directories(archive_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp")
|
||||
add_executable(trophy_system_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/TrophySystemTests.cpp"
|
||||
)
|
||||
target_link_libraries(trophy_system_tests common)
|
||||
target_include_directories(trophy_system_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
)
|
||||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
@@ -606,6 +615,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
|
||||
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
|
||||
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
|
||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
|
||||
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
|
||||
@@ -682,6 +692,7 @@ if(BUILD_TESTING)
|
||||
memory_tracker_tests
|
||||
page_manager_tests
|
||||
archive_file_tests
|
||||
trophy_system_tests
|
||||
avplayer_file_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
@@ -712,8 +723,9 @@ if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR
|
||||
add_executable(macos_sse4a_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/macosSse4aTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/x64InstructionEmulator.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/guestInstructionPatcher.cpp"
|
||||
)
|
||||
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis)
|
||||
target_link_libraries(macos_sse4a_tests common fmt::fmt Zydis::Zydis xbyak::xbyak)
|
||||
target_include_directories(macos_sse4a_tests PRIVATE ${inc_headers})
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME macos_sse4a COMMAND $<TARGET_FILE:macos_sse4a_tests>)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
+215
-26
@@ -5,9 +5,9 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <span>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <span>
|
||||
|
||||
namespace Common::Trophies {
|
||||
namespace {
|
||||
@@ -40,6 +40,25 @@ int ReadId(const Json& value) {
|
||||
return error == std::errc {} && end == text.data() + text.size() && id >= 0 ? id : -1;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> ReadUnsigned(const Json& value) {
|
||||
if (value.is_number_unsigned()) {
|
||||
return value.get<uint64_t>();
|
||||
}
|
||||
if (value.is_number_integer()) {
|
||||
const auto number = value.get<int64_t>();
|
||||
return number >= 0 ? std::optional<uint64_t>(static_cast<uint64_t>(number)) : std::nullopt;
|
||||
}
|
||||
if (!value.is_string()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& text = value.get_ref<const std::string&>();
|
||||
uint64_t number = 0;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number);
|
||||
return error == std::errc {} && end == text.data() + text.size()
|
||||
? std::optional<uint64_t>(number)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
Json ReadJson(const Files& files, std::string_view name) {
|
||||
const auto entry = files.find(name);
|
||||
if (entry == files.end()) {
|
||||
@@ -48,19 +67,18 @@ Json ReadJson(const Files& files, std::string_view name) {
|
||||
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
|
||||
}
|
||||
|
||||
Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
bool ReadFiles(std::span<const std::byte> data, Files& files, uint64_t& size) {
|
||||
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
|
||||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
const auto size = ReadBigEndian(data.data() + 8, 8);
|
||||
size = ReadBigEndian(data.data() + 8, 8);
|
||||
const auto count = ReadBigEndian(data.data() + 0x10, 4);
|
||||
const auto toc = ReadBigEndian(data.data() + 0x14, 4);
|
||||
if (size < 0x40 || size > data.size() || count > 4096 || toc > size ||
|
||||
0x20 + count * 0x40 > size - toc) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
Files files;
|
||||
for (uint64_t i = 0; i < count; ++i) {
|
||||
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
|
||||
const auto* name = reinterpret_cast<const char*>(entry);
|
||||
@@ -68,10 +86,19 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
const auto offset = ReadBigEndian(entry + 0x20, 8);
|
||||
const auto length = ReadBigEndian(entry + 0x28, 8);
|
||||
if (offset > size || length > size - offset) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
files.emplace(filename, data.subspan(offset, length));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
Files files;
|
||||
uint64_t size = 0;
|
||||
if (!ReadFiles(data, files, size)) {
|
||||
return {};
|
||||
}
|
||||
const auto conf = ReadJson(files, "tropconf.json");
|
||||
if (!conf.is_object() || !conf.contains("trophies") || !conf["trophies"].is_array() ||
|
||||
conf["trophies"].size() > 1000 || !conf.contains("defaultLanguage") ||
|
||||
@@ -129,19 +156,29 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
|
||||
if (definition.contains("unlockCondition")) {
|
||||
const auto& condition = definition["unlockCondition"];
|
||||
if (condition.is_object() && condition.contains("progressive") &&
|
||||
condition["progressive"] == true) {
|
||||
if (!condition.contains("targetValue") || !condition["targetValue"].is_string()) {
|
||||
return {};
|
||||
if (condition.is_object()) {
|
||||
trophy.progressive =
|
||||
condition.contains("progressive") && condition["progressive"] == true;
|
||||
if (condition.contains("udsStatId")) {
|
||||
trophy.uds_stat_id = ReadUnsigned(condition["udsStatId"]);
|
||||
if (!trophy.uds_stat_id) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const auto& value = condition["targetValue"].get_ref<const std::string&>();
|
||||
uint64_t target = 0;
|
||||
const auto [end, error] =
|
||||
std::from_chars(value.data(), value.data() + value.size(), target);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return {};
|
||||
if (condition.contains("targetValue")) {
|
||||
trophy.target = ReadUnsigned(condition["targetValue"]);
|
||||
}
|
||||
const auto comparison = condition.find("comparator");
|
||||
if (comparison != condition.end()) {
|
||||
if (*comparison == "ge")
|
||||
trophy.comparison = Comparison::GreaterEqual;
|
||||
else if (*comparison == "gt")
|
||||
trophy.comparison = Comparison::Greater;
|
||||
else if (*comparison == "le")
|
||||
trophy.comparison = Comparison::LessEqual;
|
||||
else if (*comparison == "lt")
|
||||
trophy.comparison = Comparison::Less;
|
||||
}
|
||||
trophy.target = target;
|
||||
}
|
||||
}
|
||||
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
|
||||
@@ -210,11 +247,11 @@ std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
return root / "_SaveData" / title_id /
|
||||
return root / "_Trophies" / title_id /
|
||||
fmt::format("trophies_{}_{}.json", user_id, service_label);
|
||||
}
|
||||
|
||||
std::set<int> LoadUnlocks(const std::filesystem::path& path) {
|
||||
UnlockData LoadUnlockData(const std::filesystem::path& path) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
|
||||
return {};
|
||||
@@ -225,22 +262,40 @@ std::set<int> LoadUnlocks(const std::filesystem::path& path) {
|
||||
!json["unlockedTrophies"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
std::set<int> unlocked;
|
||||
UnlockData unlocks;
|
||||
for (const auto& value: json["unlockedTrophies"]) {
|
||||
const auto id = ReadId(value);
|
||||
if (id >= 0) {
|
||||
unlocked.insert(id);
|
||||
unlocks.unlocked.insert(id);
|
||||
}
|
||||
}
|
||||
return unlocked;
|
||||
if (json.contains("unlockedAt") && json["unlockedAt"].is_object()) {
|
||||
for (auto it = json["unlockedAt"].begin(); it != json["unlockedAt"].end(); ++it) {
|
||||
const auto id = ReadId(Json(it.key()));
|
||||
if (id >= 0 && unlocks.unlocked.contains(id)) {
|
||||
const auto tick = ReadUnsigned(it.value());
|
||||
if (tick && *tick >= UnixEpochTick && *tick <= 315537897599999999ULL) {
|
||||
unlocks.timestamps.emplace(id, *tick);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return unlocks;
|
||||
}
|
||||
|
||||
bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked) {
|
||||
bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
|
||||
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
|
||||
return false;
|
||||
}
|
||||
const auto text = Json {{"unlockedTrophies", unlocked}}.dump();
|
||||
auto temporary = path;
|
||||
Json timestamps = Json::object();
|
||||
for (const auto& [id, tick]: unlocks.timestamps) {
|
||||
if (unlocks.unlocked.contains(id)) {
|
||||
timestamps[std::to_string(id)] = tick;
|
||||
}
|
||||
}
|
||||
const auto text =
|
||||
Json {{"unlockedTrophies", unlocks.unlocked}, {"unlockedAt", timestamps}}.dump();
|
||||
auto temporary = path;
|
||||
temporary += ".tmp";
|
||||
File file;
|
||||
if (text.size() > MaxUnlocksSize || !file.Create(temporary)) {
|
||||
@@ -261,4 +316,138 @@ bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocke
|
||||
return false;
|
||||
}
|
||||
|
||||
UdsRules LoadUdsRules(const std::filesystem::path& path) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
|
||||
return {};
|
||||
}
|
||||
const auto bytes = file.ReadWholeBuffer();
|
||||
Files files;
|
||||
uint64_t size = 0;
|
||||
if (!ReadFiles(bytes, files, size)) {
|
||||
return {};
|
||||
}
|
||||
const auto definitions = ReadJson(files, "stats_definition.json");
|
||||
const auto extraction = ReadJson(files, "stats_extraction.json");
|
||||
if (!definitions.is_object() || !definitions.contains("statDefinitionArray") ||
|
||||
!definitions["statDefinitionArray"].is_array() || !extraction.is_object() ||
|
||||
!extraction.contains("statsExtractionRuleArray") ||
|
||||
!extraction["statsExtractionRuleArray"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
// Only direct unsigned latest-value stats are evaluated here. Aggregated stats
|
||||
// and filtered/nested extraction require UDS state and event types we do not implement.
|
||||
std::map<uint64_t, bool> stats;
|
||||
for (const auto& definition: definitions["statDefinitionArray"]) {
|
||||
if (!definition.is_object() || !definition.contains("statId") ||
|
||||
!definition.contains("aggregation") || definition["aggregation"] != "latest" ||
|
||||
!definition.contains("dataType")) {
|
||||
continue;
|
||||
}
|
||||
const auto id = ReadUnsigned(definition["statId"]);
|
||||
const auto& type = definition["dataType"];
|
||||
if (!id || (type != "uint64" && type != "uint32")) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t maximum = type == "uint32" ? UINT32_MAX : UINT64_MAX;
|
||||
if ((definition.contains("minValue") && ReadUnsigned(definition["minValue"]) != 0) ||
|
||||
(definition.contains("maxValue") && ReadUnsigned(definition["maxValue"]) != maximum)) {
|
||||
continue;
|
||||
}
|
||||
stats.emplace(*id, type == "uint32");
|
||||
}
|
||||
UdsRules events;
|
||||
for (const auto& rule: extraction["statsExtractionRuleArray"]) {
|
||||
if (!rule.is_object() || !rule.contains("condition") || !rule.contains("action")) {
|
||||
continue;
|
||||
}
|
||||
const auto& condition = rule["condition"];
|
||||
const auto& action = rule["action"];
|
||||
if (!condition.is_object() || condition.size() != 1 || !condition.contains("eventName") ||
|
||||
!condition["eventName"].is_string() || !action.is_object() ||
|
||||
!action.contains("input") || !action["input"].is_string() ||
|
||||
!action.contains("output") || !action["output"].is_object() ||
|
||||
!action["output"].contains("statId")) {
|
||||
continue;
|
||||
}
|
||||
const auto id = ReadUnsigned(action["output"]["statId"]);
|
||||
const auto& input = action["input"].get_ref<const std::string&>();
|
||||
if (!id || !stats.contains(*id) || !input.starts_with("$.") || input.size() == 2 ||
|
||||
input.find_first_not_of(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", 2) !=
|
||||
std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
events[condition["eventName"].get<std::string>()].push_back(
|
||||
{*id, input.substr(2), stats.at(*id)});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
std::filesystem::path UdsPackagePath(uint32_t service_label) {
|
||||
return std::filesystem::path("sce_sys/uds") / fmt::format("uds{:02}.ucp", service_label);
|
||||
}
|
||||
|
||||
std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
|
||||
const std::map<std::string, UdsInteger>& properties) {
|
||||
const auto rules = package.event_rules.find(std::string(event_name));
|
||||
if (rules == package.event_rules.end()) {
|
||||
return {};
|
||||
}
|
||||
std::vector<int> ids;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
if (trophy.grade == 1 || !trophy.uds_stat_id || !trophy.target) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& rule: rules->second) {
|
||||
const auto value = properties.find(rule.input);
|
||||
if (rule.stat_id != *trophy.uds_stat_id || value == properties.end()) {
|
||||
continue;
|
||||
}
|
||||
std::optional<uint64_t> number;
|
||||
if (rule.is_uint32) {
|
||||
if (const auto* input = std::get_if<uint32_t>(&value->second)) number = *input;
|
||||
} else if (const auto* input = std::get_if<uint64_t>(&value->second)) {
|
||||
number = *input;
|
||||
}
|
||||
if (!number) continue;
|
||||
bool matched = false;
|
||||
switch (trophy.comparison) {
|
||||
case Comparison::GreaterEqual: matched = *number >= *trophy.target; break;
|
||||
case Comparison::Greater: matched = *number > *trophy.target; break;
|
||||
case Comparison::LessEqual: matched = *number <= *trophy.target; break;
|
||||
case Comparison::Less: matched = *number < *trophy.target; break;
|
||||
case Comparison::None: break;
|
||||
}
|
||||
if (matched) {
|
||||
ids.push_back(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
uint32_t Progress::Percentage() const {
|
||||
const auto points = total_grade[2] * 6 + total_grade[3] * 2 + total_grade[4];
|
||||
const auto earned_points = earned_grade[2] * 6 + earned_grade[3] * 2 + earned_grade[4];
|
||||
return points == 0 ? 0 : earned_points * 100 / points;
|
||||
}
|
||||
|
||||
Progress GetProgress(const Package& package, const UnlockData& unlocks, std::optional<int> group) {
|
||||
Progress progress;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
if (group && trophy.group_id != *group) {
|
||||
continue;
|
||||
}
|
||||
++progress.total;
|
||||
++progress.total_grade[trophy.grade];
|
||||
if (unlocks.unlocked.contains(id)) {
|
||||
++progress.earned;
|
||||
++progress.earned_grade[trophy.grade];
|
||||
}
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
+42
-3
@@ -1,6 +1,7 @@
|
||||
#ifndef KYTY_COMMON_TROPHIES_H_
|
||||
#define KYTY_COMMON_TROPHIES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -9,11 +10,15 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace Common::Trophies {
|
||||
|
||||
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
|
||||
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
|
||||
inline constexpr uint64_t UnixEpochTick = 62135596800000000ULL;
|
||||
|
||||
enum class Comparison { None, GreaterEqual, Greater, LessEqual, Less };
|
||||
|
||||
struct Trophy {
|
||||
int id = 0;
|
||||
@@ -21,6 +26,9 @@ struct Trophy {
|
||||
int platinum_id = -1;
|
||||
int grade = 0;
|
||||
std::optional<uint64_t> target;
|
||||
std::optional<uint64_t> uds_stat_id;
|
||||
Comparison comparison = Comparison::None;
|
||||
bool progressive = false;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string reward;
|
||||
@@ -29,19 +37,50 @@ struct Trophy {
|
||||
bool has_reward = false;
|
||||
};
|
||||
|
||||
struct UdsRule {
|
||||
uint64_t stat_id;
|
||||
std::string input;
|
||||
bool is_uint32;
|
||||
};
|
||||
|
||||
using UdsRules = std::map<std::string, std::vector<UdsRule>>;
|
||||
using UdsInteger = std::variant<int32_t, uint32_t, uint64_t>;
|
||||
|
||||
struct Package {
|
||||
std::string title;
|
||||
std::map<int, std::string> groups;
|
||||
std::map<int, Trophy> trophies;
|
||||
UdsRules event_rules;
|
||||
};
|
||||
|
||||
struct UnlockData {
|
||||
std::set<int> unlocked;
|
||||
std::map<int, uint64_t> timestamps;
|
||||
};
|
||||
|
||||
struct Progress {
|
||||
std::array<uint32_t, 5> total_grade {};
|
||||
std::array<uint32_t, 5> earned_grade {};
|
||||
uint32_t total = 0;
|
||||
uint32_t earned = 0;
|
||||
|
||||
[[nodiscard]] uint32_t Percentage() const;
|
||||
};
|
||||
|
||||
[[nodiscard]] Progress GetProgress(const Package& package, const UnlockData& unlocks,
|
||||
std::optional<int> group = std::nullopt);
|
||||
|
||||
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
|
||||
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
|
||||
[[nodiscard]] std::filesystem::path UdsPackagePath(uint32_t service_label);
|
||||
[[nodiscard]] UdsRules LoadUdsRules(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
|
||||
const std::map<std::string, UdsInteger>& properties);
|
||||
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
|
||||
std::string_view title_id, int user_id,
|
||||
uint32_t service_label);
|
||||
[[nodiscard]] std::set<int> LoadUnlocks(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked);
|
||||
[[nodiscard]] UnlockData LoadUnlockData(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
|
||||
@@ -1100,56 +1100,68 @@ struct SystemOverlay::Impl {
|
||||
static_cast<float>(frame_extent.height) / 1080.0f),
|
||||
0.65f, 1.25f);
|
||||
const float width = std::max(
|
||||
0.0f, std::min(440.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
|
||||
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
|
||||
const float height = 104.0f * screen_scale;
|
||||
const float rounding = 14.0f * screen_scale;
|
||||
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
|
||||
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
|
||||
const float alpha = std::min(fade_in, fade_out);
|
||||
const float slide = (1.0f - fade_in) * (width + 24.0f);
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 24.0f - width + slide,
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
|
||||
32.0f};
|
||||
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
draw->AddRectFilled({top_left.x + 5.0f, top_left.y + 6.0f},
|
||||
{bottom_right.x + 5.0f, bottom_right.y + 6.0f},
|
||||
IM_COL32(0, 0, 0, static_cast<int>(95.0f * alpha)), 10.0f);
|
||||
draw->AddRectFilled(top_left, bottom_right,
|
||||
IM_COL32(18, 21, 29, static_cast<int>(242.0f * alpha)), 10.0f);
|
||||
draw->AddRect(top_left, bottom_right,
|
||||
IM_COL32(190, 154, 78, static_cast<int>(200.0f * alpha)), 10.0f,
|
||||
ImDrawFlags_RoundCornersAll, 1.5f);
|
||||
const auto fade = [alpha](int r, int g, int b, int a) {
|
||||
return IM_COL32(r, g, b, static_cast<int>(static_cast<float>(a) * alpha));
|
||||
};
|
||||
draw->AddRectFilled({top_left.x + 3.0f, top_left.y + 5.0f},
|
||||
{bottom_right.x + 3.0f, bottom_right.y + 5.0f}, fade(0, 0, 0, 90),
|
||||
rounding);
|
||||
draw->AddRectFilled(top_left, bottom_right, fade(24, 26, 32, 235), rounding);
|
||||
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
|
||||
ImDrawFlags_RoundCornersAll, 1.0f);
|
||||
|
||||
const float pad = 10.0f * screen_scale;
|
||||
const float icon_size = height - 2.0f * pad;
|
||||
float text_left = top_left.x + 20.0f * screen_scale;
|
||||
if (trophy_notification_image != nullptr) {
|
||||
constexpr float ICON_SIZE = 88.0f;
|
||||
const ImVec2 icon_min {top_left.x + 8.0f * screen_scale,
|
||||
top_left.y + (height - ICON_SIZE * screen_scale) * 0.5f};
|
||||
const ImVec2 icon_max {icon_min.x + ICON_SIZE * screen_scale,
|
||||
icon_min.y + ICON_SIZE * screen_scale};
|
||||
draw->AddImage(trophy_notification_image->GetTexRef(), icon_min, icon_max, {0, 0},
|
||||
{1, 1}, IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)));
|
||||
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
|
||||
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
|
||||
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
|
||||
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
|
||||
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
|
||||
ImDrawFlags_RoundCornersAll, 1.0f);
|
||||
text_left = icon_max.x + 18.0f * screen_scale;
|
||||
}
|
||||
|
||||
const ImVec2 text_position {top_left.x +
|
||||
(trophy_notification_image ? 112.0f : 18.0f) * screen_scale,
|
||||
top_left.y + 18.0f * screen_scale};
|
||||
draw->PushClipRect(top_left, {bottom_right.x - 12.0f * screen_scale, bottom_right.y}, true);
|
||||
draw->AddText(nullptr, 13.0f * screen_scale, text_position,
|
||||
IM_COL32(232, 203, 139, static_cast<int>(255.0f * alpha)), "Trophy earned");
|
||||
draw->AddText(nullptr, 21.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 22.0f * screen_scale},
|
||||
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)),
|
||||
trophy_notification->name.c_str());
|
||||
constexpr std::array GRADE_LABELS {"Platinum", "Gold", "Silver", "Bronze"};
|
||||
constexpr std::array GRADE_COLORS {
|
||||
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
|
||||
IM_COL32(211, 142, 91, 255)};
|
||||
const int grade = trophy_notification->grade;
|
||||
if (grade > 0 && grade <= static_cast<int>(GRADE_LABELS.size())) {
|
||||
draw->AddText(nullptr, 13.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 50.0f * screen_scale},
|
||||
ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha),
|
||||
GRADE_LABELS[grade - 1]);
|
||||
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
|
||||
IM_COL32(211, 220, 232, 255),
|
||||
IM_COL32(211, 142, 91, 255)};
|
||||
const int grade = trophy_notification->grade;
|
||||
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
|
||||
float title_x = text_left;
|
||||
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
|
||||
// Small cup glyph tinted with the trophy grade.
|
||||
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
|
||||
const float u = screen_scale;
|
||||
const float cx = text_left + 9.0f * u;
|
||||
const float cy = title_y + 15.0f * u;
|
||||
draw->AddRectFilled({cx - 8.0f * u, cy - 11.0f * u}, {cx + 8.0f * u, cy + 2.0f * u},
|
||||
cup, 2.0f * u, ImDrawFlags_RoundCornersBottom);
|
||||
draw->AddRect({cx - 11.0f * u, cy - 9.0f * u}, {cx - 6.0f * u, cy - 3.0f * u}, cup,
|
||||
2.0f * u, ImDrawFlags_RoundCornersLeft, 1.8f * u);
|
||||
draw->AddRect({cx + 6.0f * u, cy - 9.0f * u}, {cx + 11.0f * u, cy - 3.0f * u}, cup,
|
||||
2.0f * u, ImDrawFlags_RoundCornersRight, 1.8f * u);
|
||||
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
|
||||
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
|
||||
1.5f * u);
|
||||
title_x += 30.0f * u;
|
||||
}
|
||||
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
|
||||
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
|
||||
fade(255, 255, 255, 255), trophy_notification->name.c_str());
|
||||
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), "Trophy earned!");
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
@@ -454,7 +454,7 @@ void HostInputToggleMouseToJoystick() {
|
||||
LOGF("Mouse to right stick: enabled (F7 to release)\n");
|
||||
}
|
||||
|
||||
bool HostInputWaitEvent(SDL_Event* event) {
|
||||
bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
int timeout = -1;
|
||||
if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) {
|
||||
g_mouse.next_poll = 0;
|
||||
@@ -473,6 +473,7 @@ bool HostInputWaitEvent(SDL_Event* event) {
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
|
||||
@@ -13,7 +13,7 @@ void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event);
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
|
||||
@@ -998,7 +998,7 @@ void Presenter::Impl::Present() {
|
||||
}
|
||||
|
||||
presented_overlay_revision.store(overlay_visual.revision, std::memory_order_release);
|
||||
window.UpdateTitle();
|
||||
window.loop.presented_frames.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
LOGF("Vulkan presentation retry exhausted; dropping frame\n");
|
||||
|
||||
@@ -722,6 +722,10 @@ void WindowContext::Run() {
|
||||
loop.need_exit = false;
|
||||
loop.paused.store(false, std::memory_order_release);
|
||||
|
||||
constexpr uint64_t title_interval_ms = 1000;
|
||||
auto title_time = SDL_GetTicks();
|
||||
auto title_frames = loop.presented_frames.load(std::memory_order_relaxed);
|
||||
|
||||
while (!loop.need_exit) {
|
||||
if (loop.paused.load(std::memory_order_acquire)) {
|
||||
if (!timer.IsPaused()) {
|
||||
@@ -731,7 +735,20 @@ void WindowContext::Run() {
|
||||
timer.Resume();
|
||||
}
|
||||
|
||||
if (!HostInputWaitEvent(&loop.event)) {
|
||||
// Refresh the title on the main thread without making presentation wait for it.
|
||||
const auto now = SDL_GetTicks();
|
||||
const auto elapsed = now - title_time;
|
||||
if (elapsed >= title_interval_ms) {
|
||||
const auto frames = loop.presented_frames.load(std::memory_order_relaxed);
|
||||
if (frames != 0) {
|
||||
UpdateTitle(frames, static_cast<double>(frames - title_frames) * 1000.0 /
|
||||
static_cast<double>(elapsed));
|
||||
}
|
||||
title_time = now;
|
||||
title_frames = frames;
|
||||
}
|
||||
const auto wait_ms = static_cast<int>(title_interval_ms - (now - title_time));
|
||||
if (!HostInputWaitEvent(&loop.event, wait_ms)) {
|
||||
continue;
|
||||
}
|
||||
ProcessEvent(timer.GetTimeS());
|
||||
@@ -913,7 +930,7 @@ void WindowContext::UpdateIcon() {
|
||||
}
|
||||
}
|
||||
|
||||
void WindowContext::UpdateTitle() {
|
||||
void WindowContext::UpdateTitle(uint64_t frame_num, double current_fps) {
|
||||
static char title[128];
|
||||
static char title_id[12];
|
||||
static char app_ver[12];
|
||||
@@ -923,10 +940,6 @@ void WindowContext::UpdateTitle() {
|
||||
static bool has_app_ver =
|
||||
Loader::SystemContentParamSfoGetString("APP_VER", app_ver, sizeof(app_ver));
|
||||
static const std::string processor_name = Common::GetSystemInfo().ProcessorName;
|
||||
static uint64_t fps_start = Common::Timer::QueryPerformanceCounter();
|
||||
static uint64_t frame_num = 0;
|
||||
static uint64_t fps_frames = 0;
|
||||
static double current_fps = 0.0;
|
||||
|
||||
#if KYTY_BUILD == KYTY_BUILD_DEBUG
|
||||
static constexpr auto build_type = "Debug";
|
||||
@@ -936,17 +949,6 @@ void WindowContext::UpdateTitle() {
|
||||
static constexpr auto build_type = "Unknown";
|
||||
#endif
|
||||
|
||||
const auto now = Common::Timer::QueryPerformanceCounter();
|
||||
const auto frequency = Common::Timer::QueryPerformanceFrequency();
|
||||
frame_num++;
|
||||
fps_frames++;
|
||||
if (now - fps_start >= frequency) {
|
||||
current_fps = static_cast<double>(fps_frames) * static_cast<double>(frequency) /
|
||||
static_cast<double>(now - fps_start);
|
||||
fps_start = now;
|
||||
fps_frames = 0;
|
||||
}
|
||||
|
||||
const auto* device_name = graphic_ctx.GetPhysicalDeviceProperties().deviceName.data();
|
||||
auto text = fmt::format(
|
||||
"[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:.0f}", KYTY_BUILD_LABEL, build_type,
|
||||
@@ -954,16 +956,7 @@ void WindowContext::UpdateTitle() {
|
||||
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""),
|
||||
device_name, processor_name, frame_num, current_fps);
|
||||
|
||||
struct TitleUpdate {
|
||||
SDL_Window* window;
|
||||
std::string* text;
|
||||
} update {window, &text};
|
||||
EXIT_IF(!SDL_RunOnMainThread(
|
||||
[](void* data) {
|
||||
auto& title = *static_cast<TitleUpdate*>(data);
|
||||
SDL_SetWindowTitle(title.window, title.text->c_str());
|
||||
},
|
||||
&update, true));
|
||||
SDL_SetWindowTitle(window, text.c_str());
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
@@ -27,6 +27,7 @@ struct WindowLoopState {
|
||||
SDL_Event event {};
|
||||
bool need_exit = false;
|
||||
std::atomic_bool paused = false;
|
||||
std::atomic<uint64_t> presented_frames {0};
|
||||
};
|
||||
|
||||
struct WindowContext {
|
||||
@@ -42,7 +43,7 @@ struct WindowContext {
|
||||
void RecreateSurface();
|
||||
void RefreshSurfaceCapabilities();
|
||||
void UpdateIcon();
|
||||
void UpdateTitle();
|
||||
void UpdateTitle(uint64_t frame_num, double current_fps);
|
||||
void Resize(int width, int height);
|
||||
void ProcessWindowEvent(const SDL_WindowEvent& event);
|
||||
void ProcessDisplayEvent(const SDL_DisplayEvent& event);
|
||||
|
||||
@@ -109,6 +109,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
|
||||
{0x3bu, Opcode::DS_READ_I16, 1, 16, true}, {0x3cu, Opcode::DS_READ_U16, 1, 16},
|
||||
{0x3du, Opcode::DS_CONSUME, 1, 32}, {0x3eu, Opcode::DS_APPEND, 1, 32},
|
||||
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
|
||||
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
@@ -214,6 +215,7 @@ uint32_t DsSourceCount(Opcode opcode) {
|
||||
case Opcode::DS_READ_ADDTID_B32:
|
||||
case Opcode::DS_CONSUME:
|
||||
case Opcode::DS_APPEND: return 0u;
|
||||
case Opcode::DS_ORDERED_COUNT: return 1u;
|
||||
default: return IsDsWriteOpcode(opcode) || IsDsAtomicOpcode(opcode) ? 2u : 1u;
|
||||
}
|
||||
}
|
||||
@@ -412,6 +414,14 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS opcode is not implemented");
|
||||
}
|
||||
if (inst.opcode == Opcode::DS_ORDERED_COUNT) {
|
||||
// Keep OFFSET1's wave type as well as its add/swap selector for GDS addressing.
|
||||
inst.offset = offset0 & 0xfcu;
|
||||
inst.secondary_offset = offset1;
|
||||
if (!inst.gds || ((offset1 >> 4u) & 3u) > 1u || ((offset1 >> 6u) & 3u) != 0u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS ordered count variant is not implemented");
|
||||
}
|
||||
}
|
||||
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
|
||||
}
|
||||
|
||||
@@ -382,6 +382,7 @@ enum class Opcode {
|
||||
V_CMPX_LG_F32,
|
||||
V_CMPX_GE_F32,
|
||||
V_CMPX_O_F32,
|
||||
V_CMPX_U_F32,
|
||||
V_CMPX_NGE_F32,
|
||||
V_CMPX_NLG_F32,
|
||||
V_CMPX_NGT_F32,
|
||||
@@ -579,6 +580,7 @@ enum class Opcode {
|
||||
DS_BPERMUTE_B32,
|
||||
DS_CONSUME,
|
||||
DS_APPEND,
|
||||
DS_ORDERED_COUNT,
|
||||
DS_READ_I8,
|
||||
DS_READ_U8,
|
||||
DS_READ_I16,
|
||||
|
||||
@@ -232,7 +232,7 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x11u, Opcode::V_CMPX_LT_F32}, {0x12u, Opcode::V_CMPX_EQ_F32},
|
||||
{0x13u, Opcode::V_CMPX_LE_F32}, {0x14u, Opcode::V_CMPX_GT_F32},
|
||||
{0x15u, Opcode::V_CMPX_LG_F32}, {0x16u, Opcode::V_CMPX_GE_F32},
|
||||
{0x17u, Opcode::V_CMPX_O_F32},
|
||||
{0x17u, Opcode::V_CMPX_O_F32}, {0x18u, Opcode::V_CMPX_U_F32},
|
||||
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
|
||||
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
|
||||
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
|
||||
@@ -892,6 +892,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
@@ -1449,6 +1450,9 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
|
||||
case Opcode::V_MAD_F32:
|
||||
case Opcode::V_FMA_F32:
|
||||
case Opcode::V_FMA_F16:
|
||||
case Opcode::V_MIN3_F16:
|
||||
case Opcode::V_MAX3_F16:
|
||||
case Opcode::V_MED3_F16:
|
||||
case Opcode::V_CUBEID_F32:
|
||||
case Opcode::V_CVT_PKRTZ_F16_F32:
|
||||
case Opcode::V_LDEXP_F32:
|
||||
@@ -1479,7 +1483,8 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
|
||||
return clamp != 0u || omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3F16TernaryOpcode(opcode)) {
|
||||
return opcode != Opcode::V_FMA_F16 && (clamp != 0u || omod != 0u);
|
||||
// Clamp is applied by WriteF16 for every f16 ternary; omod only for V_FMA_F16.
|
||||
return opcode != Opcode::V_FMA_F16 && omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3I16TernaryOpcode(opcode)) {
|
||||
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
|
||||
@@ -1529,6 +1534,7 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
|
||||
@@ -883,6 +883,50 @@ void Translator::DS_SWIZZLE_B32(const Decoder::Instruction& inst) {
|
||||
ReadU32(MemorySourceAt(inst, 1)), ir.GetExec()}));
|
||||
}
|
||||
|
||||
void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
|
||||
// Approximate ordered counting with one GDS atomic per wave. Wave-launch ordering and
|
||||
// release/done synchronization are not emulated.
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto m0 = ir.GetM0();
|
||||
auto address = ir.BitwiseAnd(ir.ShiftRightLogical(m0, IR::U32(IR::Value(16u))),
|
||||
IR::U32(IR::Value(0xfffcu)));
|
||||
if (((inst.secondary_offset >> 2u) & 3u) == 1u) {
|
||||
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
|
||||
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
|
||||
}
|
||||
const auto exec = ir.GetExec();
|
||||
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
|
||||
const auto low = ir.CompositeExtract(ballot, 0u);
|
||||
const auto high = ir.CompositeExtract(ballot, 1u);
|
||||
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
|
||||
const auto first = ir.Select(
|
||||
ir.INotEqual(low, IR::U32(IR::Value(0u))),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {low})),
|
||||
ir.IAdd(IR::U32(IR::Value(32u)), IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {high}))));
|
||||
// An empty EXEC still returns the counter. Elect lane zero and perform an add of zero.
|
||||
const auto selected = ir.Select(active, first, IR::U32(IR::Value(0u)));
|
||||
const auto lane = IR::U32(ir.Emit(IR::ValueOpcode::LaneId));
|
||||
const auto is_first = ir.IEqual(lane, selected);
|
||||
const auto source = IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane,
|
||||
{ReadU32(inst.src0), is_first}));
|
||||
const auto value = ir.Select(active, source, IR::U32(IR::Value(0u)));
|
||||
const auto flags = AddMemoryInfo(memory, inst.pc);
|
||||
IR::U32 result;
|
||||
if (((inst.secondary_offset >> 4u) & 3u) == 1u) {
|
||||
const auto swapped = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicSwap32,
|
||||
{address, value, ir.LogicalAnd(is_first, active)}, flags));
|
||||
const auto unchanged = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
|
||||
{address, IR::Value(0u), ir.LogicalAnd(is_first, ir.LogicalNot(active))}, flags));
|
||||
result = ir.Select(active, swapped, unchanged);
|
||||
} else {
|
||||
result = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
|
||||
{address, value, is_first}, flags));
|
||||
}
|
||||
// DS_ORDERED_COUNT writes every destination lane, regardless of EXEC.
|
||||
ir.SetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane, {result, is_first})));
|
||||
}
|
||||
|
||||
void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
|
||||
const auto address = ir.IAdd(ReadU32(inst.src0), IR::U32(IR::Value(inst.offset)));
|
||||
WriteOperand(inst.dst, ir.Emit(backward ? IR::ValueOpcode::BpermuteU32
|
||||
@@ -1114,6 +1158,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::DS_SWIZZLE_B32: return DS_SWIZZLE_B32(inst);
|
||||
case Decoder::Opcode::DS_PERMUTE_B32: return DS_PERMUTE(inst, false);
|
||||
case Decoder::Opcode::DS_BPERMUTE_B32: return DS_PERMUTE(inst, true);
|
||||
case Decoder::Opcode::DS_ORDERED_COUNT: return DS_ORDERED_COUNT(inst);
|
||||
case Decoder::Opcode::DS_CONSUME:
|
||||
return DS_APPEND_CONSUME(inst, IR::ValueOpcode::DataConsume);
|
||||
case Decoder::Opcode::DS_APPEND:
|
||||
|
||||
@@ -109,6 +109,7 @@ private:
|
||||
void DS_ADDTID(const Decoder::Instruction& inst, bool write);
|
||||
void DS_SWIZZLE_B32(const Decoder::Instruction& inst);
|
||||
void DS_PERMUTE(const Decoder::Instruction& inst, bool backward);
|
||||
void DS_ORDERED_COUNT(const Decoder::Instruction& inst);
|
||||
|
||||
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
|
||||
IR::U32 ConvertF32ToU32Saturated(IR::F32 value, float upper_bound, float safe_upper,
|
||||
|
||||
@@ -344,6 +344,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
|
||||
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
|
||||
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
|
||||
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
|
||||
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
|
||||
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
|
||||
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
|
||||
|
||||
@@ -3200,6 +3200,15 @@ static void* RunThread(void* arg) {
|
||||
os_thread_id = GetHostThreadId();
|
||||
#endif
|
||||
thread->host_thread_id = os_thread_id;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX
|
||||
if (!thread->name.empty()) {
|
||||
#if defined(__APPLE__)
|
||||
pthread_setname_np(thread->name.substr(0, 63).c_str());
|
||||
#else
|
||||
pthread_setname_np(pthread_self(), thread->name.substr(0, 15).c_str());
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
LOGF("\tPthread run begin: %s, id = %d, os_thread_id = %" PRIu64 ", entry = 0x%016" PRIx64
|
||||
", arg = 0x%016" PRIx64 ", stack_addr = 0x%016" PRIx64 ", stack_size = %" PRIu64 "\n",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>840</width>
|
||||
<width>900</width>
|
||||
<height>620</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -400,6 +400,13 @@ To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav wi
|
||||
<property name="toolTip"><string>Restore default settings</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="settings_button">
|
||||
<property name="minimumSize"><size><width>80</width><height>25</height></size></property>
|
||||
<property name="text"><string>Game Configs</string></property>
|
||||
<property name="toolTip"><string>Import or export game configs</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="buttonSpacer">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Edit configuration</p></body></html></string>
|
||||
<string>Edit game config</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
@@ -150,7 +150,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Delete configuration</p></body></html></string>
|
||||
<string>Clear game config</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
@@ -169,6 +169,28 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="trophy_overview_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>36</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>View saved trophies</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#000" d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M26 16a10 10 0 1 1-2.93-7.07L27 13"/>
|
||||
<path d="M27 5v8h-8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 252 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M9 5h14v8a7 7 0 0 1-14 0Z"/>
|
||||
<path d="M9 8H5v3a4 4 0 0 0 4 4M23 8h4v3a4 4 0 0 1-4 4"/>
|
||||
<path d="M16 20v4M12 28v-4h8v4M11 28h10"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -6,6 +6,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QColor>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaEnum>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
@@ -14,9 +15,6 @@
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
|
||||
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
|
||||
|
||||
template <class T>
|
||||
inline QStringList EnumToList() {
|
||||
QStringList ret;
|
||||
@@ -126,7 +124,7 @@ public:
|
||||
int vblank_frequency = 60;
|
||||
int console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
bool vulkan_validation_enabled = false;
|
||||
bool shader_validation_enabled = true;
|
||||
bool shader_validation_enabled = false;
|
||||
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
|
||||
LogDirection shader_log_direction = LogDirection::Silent;
|
||||
QString shader_log_folder = "_Shaders";
|
||||
@@ -188,90 +186,16 @@ public:
|
||||
game_comment = other.game_comment;
|
||||
}
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
KYTY_CFG_SET(name);
|
||||
KYTY_CFG_SET(basedir);
|
||||
KYTY_CFG_SET(game_path);
|
||||
KYTY_CFG_SET(custom_settings);
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
s->setValue("host_input_mapping", host_input_mapping);
|
||||
KYTY_CFG_SET(elf);
|
||||
}
|
||||
void WriteSettings(QSettings* s) const;
|
||||
void ReadSettings(QSettings* s);
|
||||
|
||||
void ReadSettings(QSettings* s) {
|
||||
KYTY_CFG_GET(name);
|
||||
KYTY_CFG_GET(basedir);
|
||||
KYTY_CFG_GET(game_path);
|
||||
KYTY_CFG_GET(custom_settings);
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
user_name = s->value("user_name", user_name).toString();
|
||||
bool user_id_ok = false;
|
||||
auto saved_user_id = s->value("user_id", user_id).toInt(&user_id_ok);
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s->value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
|
||||
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s->value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
}
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s->value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
#endif
|
||||
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
|
||||
elf = s->value("elf", elf).toString();
|
||||
}
|
||||
[[nodiscard]] QVariantMap GameSettings() const;
|
||||
// Call on a temporary configuration: validation can fail after loading values.
|
||||
bool SetGameSettings(const QJsonObject& settings, QString& error);
|
||||
|
||||
private:
|
||||
template <class Settings>
|
||||
void ReadGameSettingsValues(const Settings& s);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Configuration*)
|
||||
|
||||
@@ -33,6 +33,8 @@ public:
|
||||
|
||||
signals:
|
||||
void PreviewControllerColor(const QString& color);
|
||||
void ImportGameSettings();
|
||||
void ExportGameSettings();
|
||||
|
||||
private:
|
||||
Ui::ConfigurationEditDialog* m_ui = nullptr;
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
bool EnsureGameDirectory();
|
||||
void ScanGameDirectory();
|
||||
void ViewTrophies();
|
||||
void ViewTrophyOverview();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -73,6 +74,8 @@ private:
|
||||
void SelectItem(QTreeWidgetItem* witem);
|
||||
void ApplyCompatibility();
|
||||
void UpdateToolbarIcons();
|
||||
void ImportGameSettings(QWidget* parent);
|
||||
void ExportGameSettings(QWidget* parent) const;
|
||||
[[nodiscard]] bool HasValidGameDirectory() const;
|
||||
|
||||
ConfigurationItem* m_selected_item = nullptr;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
@@ -14,6 +16,8 @@ public:
|
||||
static bool HasTrophyData(const Configuration* info);
|
||||
static void ShowForGame(const Configuration* info, const QString& runtime_directory,
|
||||
QWidget* parent);
|
||||
static void ShowOverview(const std::vector<const Configuration*>& games,
|
||||
const QString& runtime_directory, QWidget* parent);
|
||||
|
||||
private:
|
||||
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
|
||||
<file alias="refresh.svg">icons/refresh.svg</file>
|
||||
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
|
||||
<file alias="trophy.svg">icons/trophy.svg</file>
|
||||
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="/styles">
|
||||
<file alias="launcher.qss">styles/launcher.qss</file>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "configuration.h"
|
||||
|
||||
QVariantMap Configuration::GameSettings() const {
|
||||
QVariantMap values;
|
||||
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
KYTY_CFG_SET(elf);
|
||||
#undef KYTY_CFG_SET
|
||||
return values;
|
||||
}
|
||||
|
||||
template <class Settings>
|
||||
void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
user_name = s.value("user_name", user_name).toString();
|
||||
bool user_id_ok = false;
|
||||
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s.value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s.value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s.value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
}
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s.value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s.value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
#endif
|
||||
elf = s.value("elf", elf).toString();
|
||||
#undef KYTY_CFG_GET
|
||||
}
|
||||
|
||||
void Configuration::WriteSettings(QSettings* s) const {
|
||||
const auto values = GameSettings();
|
||||
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
|
||||
s->setValue(it.key(), it.value());
|
||||
}
|
||||
s->setValue("name", name);
|
||||
s->setValue("basedir", basedir);
|
||||
s->setValue("game_path", game_path);
|
||||
s->setValue("custom_settings", QVariant::fromValue(custom_settings).toString());
|
||||
s->setValue("host_input_mapping", host_input_mapping);
|
||||
}
|
||||
|
||||
void Configuration::ReadSettings(QSettings* s) {
|
||||
ReadGameSettingsValues(*s);
|
||||
name = s->value("name").toString();
|
||||
basedir = s->value("basedir").toString();
|
||||
game_path = s->value("game_path").toString();
|
||||
custom_settings = s->value("custom_settings").toBool();
|
||||
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
|
||||
}
|
||||
|
||||
bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) {
|
||||
const auto fail = [&error](const QString& message) {
|
||||
error = message;
|
||||
return false;
|
||||
};
|
||||
auto values = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (!values.contains(it.key())) {
|
||||
return fail(tr("Unknown game setting: %1").arg(it.key()));
|
||||
}
|
||||
if (!it.value().isString()) {
|
||||
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
|
||||
}
|
||||
values.insert(it.key(), it.value().toString());
|
||||
}
|
||||
|
||||
ReadGameSettingsValues(values);
|
||||
const auto canonical = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (canonical.value(it.key()) != values.value(it.key())) {
|
||||
return fail(tr("Invalid value for game setting: %1").arg(it.key()));
|
||||
}
|
||||
}
|
||||
if (user_name.trimmed().isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
|
||||
return fail(tr("User name must contain 1-16 UTF-8 bytes."));
|
||||
}
|
||||
if (gpu_index < -1) {
|
||||
return fail(tr("GPU index must be -1 or greater."));
|
||||
}
|
||||
if (vblank_frequency < 30 || vblank_frequency > 360) {
|
||||
return fail(tr("Vblank frequency must be between 30 and 360."));
|
||||
}
|
||||
if ((shader_log_direction == LogDirection::File && shader_log_folder.isEmpty()) ||
|
||||
(printf_direction == LogDirection::File && printf_output_file.isEmpty()) ||
|
||||
(command_buffer_dump_enabled && command_buffer_dump_folder.isEmpty())) {
|
||||
return fail(tr("Enabled logging and command buffer dumps require an output path."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
@@ -126,6 +127,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
setMinimumWidth(width());
|
||||
InitGameDirectories();
|
||||
m_ui->controller_group->setVisible(false);
|
||||
m_ui->settings_button->setVisible(false);
|
||||
|
||||
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
|
||||
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
@@ -354,6 +356,11 @@ void ConfigurationEditDialog::InitGameDirectories() {
|
||||
|
||||
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
|
||||
m_global_settings = true;
|
||||
auto* menu = new QMenu(m_ui->settings_button);
|
||||
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
|
||||
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
|
||||
m_ui->settings_button->setMenu(menu);
|
||||
m_ui->settings_button->setVisible(true);
|
||||
m_game_dirs_list->clear();
|
||||
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QInputDialog>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
@@ -40,6 +42,7 @@
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
@@ -56,6 +59,7 @@
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ui_configuration_list_widget.h"
|
||||
|
||||
@@ -241,6 +245,8 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
||||
&ConfigurationListWidget::edit_configuration);
|
||||
connect(m_ui->delete_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::delete_configuartion);
|
||||
connect(m_ui->trophy_overview_button, &QToolButton::clicked, this,
|
||||
&ConfigurationListWidget::ViewTrophyOverview);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::currentItemChanged, this,
|
||||
&ConfigurationListWidget::SelectItem);
|
||||
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
|
||||
@@ -293,6 +299,7 @@ void ConfigurationListWidget::UpdateToolbarIcons() {
|
||||
set_icon(m_ui->input_mapping_button, QStringLiteral(":/icons/input-mapping.svg"));
|
||||
set_icon(m_ui->edit_button, QStringLiteral(":/icons/edit-configuration.svg"));
|
||||
set_icon(m_ui->delete_button, QStringLiteral(":/icons/remove-configuration.svg"));
|
||||
set_icon(m_ui->trophy_overview_button, QStringLiteral(":/icons/trophy.svg"));
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::WriteSettings() {
|
||||
@@ -761,7 +768,7 @@ void ConfigurationListWidget::edit_configuration() {
|
||||
|
||||
auto info = CreateConfiguration(*item);
|
||||
ConfigurationEditDialog dlg(*info, this);
|
||||
dlg.setWindowTitle(tr("Edit game settings"));
|
||||
dlg.setWindowTitle(tr("Edit game config"));
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
|
||||
@@ -785,8 +792,8 @@ void ConfigurationListWidget::delete_configuartion() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
|
||||
tr("Do you want to clear custom settings?"))) {
|
||||
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear game config"),
|
||||
tr("Clear this game's config and use global settings?"))) {
|
||||
delete m_custom_infos.take(item->GetInfo().game_path);
|
||||
item->GetInfo().custom_settings = false;
|
||||
WriteSettings();
|
||||
@@ -806,6 +813,10 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
dlg.SetGlobalSettings(m_game_dirs);
|
||||
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
|
||||
&ConfigurationListWidget::PreviewControllerColor);
|
||||
connect(&dlg, &ConfigurationEditDialog::ImportGameSettings, this,
|
||||
[this, &dlg]() { ImportGameSettings(&dlg); });
|
||||
connect(&dlg, &ConfigurationEditDialog::ExportGameSettings, this,
|
||||
[this, &dlg]() { ExportGameSettings(&dlg); });
|
||||
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_global_info.CopyEmulatorSettingsFrom(info);
|
||||
@@ -818,7 +829,133 @@ void ConfigurationListWidget::edit_global_settings() {
|
||||
ScanGameDirectory();
|
||||
}
|
||||
}
|
||||
emit Select();
|
||||
SelectItem(m_ui->cfgs_list->currentItem());
|
||||
}
|
||||
|
||||
static bool IsGameSettingsTitleId(const QString& title_id) {
|
||||
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
|
||||
return pattern.match(title_id).hasMatch();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ImportGameSettings(QWidget* parent) {
|
||||
const auto path = QFileDialog::getOpenFileName(parent, tr("Import game configs"), {},
|
||||
tr("JSON files (*.json)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"), file.errorString());
|
||||
return;
|
||||
}
|
||||
const auto document = QJsonDocument::fromJson(file.readAll());
|
||||
if (!document.isObject() || document.object().isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
parent, tr("Import failed"),
|
||||
tr("Expected a JSON object containing game configs keyed by PPSA code."));
|
||||
return;
|
||||
}
|
||||
const auto settings = document.object();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
Configuration info;
|
||||
QString error;
|
||||
if (!IsGameSettingsTitleId(it.key()) || !it.value().isObject() ||
|
||||
it.value().toObject().isEmpty() ||
|
||||
!info.SetGameSettings(it.value().toObject(), error)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"),
|
||||
tr("Invalid game config for %1. %2").arg(it.key(), error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
struct ImportedGame {
|
||||
ConfigurationItem* item;
|
||||
std::unique_ptr<Configuration> info;
|
||||
};
|
||||
std::vector<ImportedGame> imported;
|
||||
auto unmatched = settings.keys();
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
|
||||
if (!settings.contains(title_id)) {
|
||||
continue;
|
||||
}
|
||||
auto info = CreateConfiguration(*item);
|
||||
QString error;
|
||||
if (!info->SetGameSettings(settings.value(title_id).toObject(), error)) {
|
||||
QMessageBox::warning(parent, tr("Import failed"),
|
||||
tr("Invalid game config for %1. %2").arg(title_id, error));
|
||||
return;
|
||||
}
|
||||
info->custom_settings = true;
|
||||
imported.push_back({item, std::move(info)});
|
||||
unmatched.removeAll(title_id);
|
||||
}
|
||||
auto message = imported.empty() ? tr("No matching games found in the library.")
|
||||
: tr("Configs will be updated for the following games:\n");
|
||||
for (const auto& game: imported) {
|
||||
message += tr("\n%1 — %2").arg(game.info->title_id.trimmed().toUpper(), game.info->name);
|
||||
}
|
||||
if (!unmatched.isEmpty()) {
|
||||
message += tr("\nGames not found: %1").arg(unmatched.join(QStringLiteral(", ")));
|
||||
}
|
||||
if (imported.empty()) {
|
||||
QMessageBox::information(parent, tr("Import game configs"), message);
|
||||
return;
|
||||
}
|
||||
QMessageBox confirmation(QMessageBox::Question, tr("Import game configs"), message,
|
||||
QMessageBox::Apply | QMessageBox::Cancel, parent);
|
||||
confirmation.setTextFormat(Qt::PlainText);
|
||||
confirmation.setDefaultButton(QMessageBox::Cancel);
|
||||
if (confirmation.exec() != QMessageBox::Apply) {
|
||||
return;
|
||||
}
|
||||
for (auto& [item, info]: imported) {
|
||||
const auto game_path = info->game_path;
|
||||
delete m_custom_infos.take(game_path);
|
||||
m_custom_infos.insert(game_path, info.release());
|
||||
item->GetInfo().custom_settings = true;
|
||||
item->Update();
|
||||
}
|
||||
WriteSettings();
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ExportGameSettings(QWidget* parent) const {
|
||||
QMap<QString, const ConfigurationItem*> games;
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
const auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
const auto& game = item->GetInfo();
|
||||
const auto title_id = game.title_id.trimmed().toUpper();
|
||||
if (m_custom_infos.contains(game.game_path) && IsGameSettingsTitleId(title_id)) {
|
||||
games.insert(tr("%1 — %2 (%3)").arg(title_id, game.name, game.game_path), item);
|
||||
}
|
||||
}
|
||||
if (games.isEmpty()) {
|
||||
QMessageBox::information(parent, tr("Export game config"),
|
||||
tr("No game configs with a PPSA code were found."));
|
||||
return;
|
||||
}
|
||||
bool accepted = false;
|
||||
const auto selected = QInputDialog::getItem(parent, tr("Export game config"), tr("Game:"),
|
||||
games.keys(), 0, false, &accepted);
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
const auto& game = games.value(selected)->GetInfo();
|
||||
const auto title_id = game.title_id.trimmed().toUpper();
|
||||
const auto path =
|
||||
QFileDialog::getSaveFileName(parent, tr("Export game config"),
|
||||
title_id + QStringLiteral(".json"), tr("JSON files (*.json)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto settings =
|
||||
QJsonObject::fromVariantMap(m_custom_infos.value(game.game_path)->GameSettings());
|
||||
const auto data =
|
||||
QJsonDocument(QJsonObject {{title_id, settings}}).toJson(QJsonDocument::Indented);
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
|
||||
QMessageBox::warning(parent, tr("Export failed"), file.errorString());
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::edit_input_mapping() {
|
||||
@@ -840,6 +977,20 @@ void ConfigurationListWidget::ViewTrophies() {
|
||||
TrophyViewerDialog::ShowForGame(config.get(), m_runtime_directory, this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ViewTrophyOverview() {
|
||||
std::vector<std::unique_ptr<Configuration>> configurations;
|
||||
std::vector<const Configuration*> games;
|
||||
configurations.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
|
||||
games.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
const auto* item =
|
||||
static_cast<const ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
configurations.push_back(CreateConfiguration(*item));
|
||||
games.push_back(configurations.back().get());
|
||||
}
|
||||
TrophyViewerDialog::ShowOverview(games, m_runtime_directory, this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::open_game_folder() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr) {
|
||||
@@ -1022,11 +1173,11 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||
menu.addSeparator();
|
||||
QAction* action_edit =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game config..."),
|
||||
this, SLOT(edit_configuration()));
|
||||
QAction* action_delete =
|
||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
|
||||
tr("Clear game config"), this, SLOT(delete_configuartion()));
|
||||
|
||||
if (item == nullptr) {
|
||||
menu.addSeparator();
|
||||
|
||||
@@ -6,18 +6,23 @@
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QHBoxLayout>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QRegularExpression>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QTabBar>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
@@ -49,26 +54,6 @@ static QString GradeToText(int grade) {
|
||||
}
|
||||
}
|
||||
|
||||
static void PrepareTable(QTableWidget* table) {
|
||||
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
|
||||
QObject::tr("Name"), QObject::tr("Description")});
|
||||
table->setAlternatingRowColors(true);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setIconSize(QSize(96, 96));
|
||||
table->setShowGrid(false);
|
||||
table->setWordWrap(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->verticalHeader()->setDefaultSectionSize(112);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
table->horizontalHeader()->setHighlightSections(false);
|
||||
table->setColumnWidth(0, 100);
|
||||
table->setColumnWidth(1, 132);
|
||||
table->setColumnWidth(2, 260);
|
||||
table->setColumnWidth(3, 480);
|
||||
}
|
||||
|
||||
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
|
||||
QStringList lines {QString::fromStdString(trophy.name),
|
||||
QString::fromStdString(trophy.description),
|
||||
@@ -102,6 +87,225 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
|
||||
return !FindTrophyFiles(info).isEmpty();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Common::Trophies::Progress;
|
||||
|
||||
Common::Trophies::UnlockData LoadUnlocks(const Configuration& info,
|
||||
const QString& runtime_directory, const QString& file) {
|
||||
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
|
||||
return Common::Trophies::LoadUnlockData(Common::Trophies::UnlocksPath(
|
||||
GameContent::ToPath(runtime_directory), info.title_id.toStdString(), info.user_id, label));
|
||||
}
|
||||
|
||||
Progress GetGameProgress(const Configuration* info, const QString& runtime_directory) {
|
||||
Progress result;
|
||||
if (info == nullptr) {
|
||||
return result;
|
||||
}
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info->basedir));
|
||||
for (const auto& file: FindTrophyFiles(info)) {
|
||||
const auto package =
|
||||
Common::Trophies::LoadPackage(GameContent::ToPath(file), info->console_language);
|
||||
const auto progress =
|
||||
Common::Trophies::GetProgress(package, LoadUnlocks(*info, runtime_directory, file));
|
||||
result.total += progress.total;
|
||||
result.earned += progress.earned;
|
||||
for (size_t grade = 1; grade < result.total_grade.size(); ++grade) {
|
||||
result.total_grade[grade] += progress.total_grade[grade];
|
||||
result.earned_grade[grade] += progress.earned_grade[grade];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap MakeCupIcon(const QColor& color, int size) {
|
||||
QPixmap pixmap(size, size);
|
||||
pixmap.fill(Qt::transparent);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
const double u = size / 20.0;
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
QPainterPath cup;
|
||||
cup.moveTo(5 * u, 2 * u);
|
||||
cup.lineTo(15 * u, 2 * u);
|
||||
cup.lineTo(14 * u, 9 * u);
|
||||
cup.quadTo(10 * u, 13 * u, 6 * u, 9 * u);
|
||||
cup.closeSubpath();
|
||||
painter.drawPath(cup);
|
||||
painter.setPen(QPen(color, 1.6 * u));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawArc(QRectF(1.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, 180 * 16);
|
||||
painter.drawArc(QRectF(13.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, -180 * 16);
|
||||
painter.drawLine(QPointF(10 * u, 12 * u), QPointF(10 * u, 16 * u));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
painter.drawRoundedRect(QRectF(6 * u, 16 * u, 8 * u, 2.5 * u), u, u);
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QColor GradeColor(int grade) {
|
||||
switch (grade) {
|
||||
case 1: return QColor(0x7fc8ff);
|
||||
case 2: return QColor(0xf5c542);
|
||||
case 3: return QColor(0xc4cad2);
|
||||
default: return QColor(0xcd7f4f);
|
||||
}
|
||||
}
|
||||
|
||||
QWidget* MakeGradeCount(QWidget* parent, int grade, int value, int icon_size, int font_size) {
|
||||
auto* box = new QWidget(parent);
|
||||
auto* row = new QHBoxLayout(box);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
auto* icon = new QLabel(box);
|
||||
icon->setPixmap(MakeCupIcon(GradeColor(grade), icon_size));
|
||||
auto* text = new QLabel(QString::number(value), box);
|
||||
text->setStyleSheet(QStringLiteral("font-size: %1px;").arg(font_size));
|
||||
row->addWidget(icon);
|
||||
row->addWidget(text);
|
||||
return box;
|
||||
}
|
||||
|
||||
QPixmap LoadGameArt(const Configuration* game, int height) {
|
||||
QPixmap art;
|
||||
const auto data = GameContent::ReadFile(game->basedir, QStringLiteral("sce_sys/icon0.png"),
|
||||
GameContent::MaxImageSize);
|
||||
if (!data.isEmpty() && art.loadFromData(data)) {
|
||||
return art.scaledToHeight(height, Qt::SmoothTransformation);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void TrophyViewerDialog::ShowOverview(const std::vector<const Configuration*>& games,
|
||||
const QString& runtime_directory, QWidget* parent) {
|
||||
QDialog dialog(parent);
|
||||
dialog.setWindowTitle(QObject::tr("Trophies"));
|
||||
dialog.resize(820, 640);
|
||||
dialog.setStyleSheet(QStringLiteral(
|
||||
"QDialog { background: palette(window); }"
|
||||
"QLabel { color: palette(text); }"
|
||||
"QListWidget { background: transparent; border: none; outline: none; }"
|
||||
"QListWidget::item { border-bottom: 1px solid palette(mid); padding: 4px; }"
|
||||
"QListWidget::item:selected, QListWidget::item:hover { background: palette(midlight); "
|
||||
"border: 1px solid palette(highlight); }"
|
||||
"QProgressBar { background: palette(mid); border: none; height: 4px; }"
|
||||
"QProgressBar::chunk { background: palette(highlight); }"));
|
||||
auto* layout = new QVBoxLayout(&dialog);
|
||||
|
||||
struct GameProgress {
|
||||
const Configuration* game;
|
||||
Progress progress;
|
||||
};
|
||||
std::vector<GameProgress> trophy_games;
|
||||
Progress totals;
|
||||
for (const auto* game: games) {
|
||||
const auto progress = GetGameProgress(game, runtime_directory);
|
||||
if (progress.total == 0) {
|
||||
continue;
|
||||
}
|
||||
trophy_games.push_back({game, progress});
|
||||
totals.earned += progress.earned;
|
||||
for (size_t grade = 1; grade < totals.earned_grade.size(); ++grade) {
|
||||
totals.earned_grade[grade] += progress.earned_grade[grade];
|
||||
}
|
||||
}
|
||||
|
||||
auto* header = new QHBoxLayout;
|
||||
auto* title = new QLabel(QObject::tr("Trophies"), &dialog);
|
||||
title->setStyleSheet(QStringLiteral("font-size: 30px;"));
|
||||
header->addWidget(title);
|
||||
header->addStretch(1);
|
||||
auto* total = new QLabel(QObject::tr("Total %1").arg(totals.earned), &dialog);
|
||||
total->setAlignment(Qt::AlignCenter);
|
||||
total->setStyleSheet(QStringLiteral("font-size: 16px;"));
|
||||
header->addWidget(total);
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
header->addWidget(MakeGradeCount(&dialog, grade, totals.earned_grade[grade], 32, 18));
|
||||
}
|
||||
layout->addLayout(header);
|
||||
|
||||
auto* list = new QListWidget(&dialog);
|
||||
list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
layout->addWidget(list, 1);
|
||||
|
||||
for (size_t index = 0; index < trophy_games.size(); ++index) {
|
||||
const auto& [game, progress] = trophy_games[index];
|
||||
auto* item = new QListWidgetItem(list);
|
||||
item->setSizeHint(QSize(760, 112));
|
||||
item->setData(Qt::UserRole, static_cast<int>(index));
|
||||
|
||||
auto* row_widget = new QWidget(list);
|
||||
row_widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
auto* row = new QHBoxLayout(row_widget);
|
||||
auto* art = new QLabel(row_widget);
|
||||
art->setFixedSize(80, 80);
|
||||
art->setAlignment(Qt::AlignCenter);
|
||||
const auto pixmap = LoadGameArt(game, 80);
|
||||
if (!pixmap.isNull()) {
|
||||
art->setPixmap(pixmap);
|
||||
} else {
|
||||
art->setStyleSheet(QStringLiteral("background: palette(mid);"));
|
||||
}
|
||||
row->addWidget(art);
|
||||
const auto name = !game->name.isEmpty() ? game->name : game->title_id;
|
||||
auto* text = new QLabel(QStringLiteral("%1\n%2").arg(name, game->title_id), row_widget);
|
||||
text->setStyleSheet(QStringLiteral("font-size: 18px;"));
|
||||
row->addWidget(text, 1);
|
||||
|
||||
auto* percent_box = new QVBoxLayout;
|
||||
percent_box->setSpacing(2);
|
||||
percent_box->setContentsMargins(0, 0, 0, 0);
|
||||
auto* percent = new QLabel(QStringLiteral("%1%").arg(progress.Percentage()), row_widget);
|
||||
percent->setAlignment(Qt::AlignRight);
|
||||
percent->setStyleSheet(QStringLiteral("font-size: 24px;"));
|
||||
percent->setMinimumHeight(34);
|
||||
auto* earned = new QLabel(
|
||||
QObject::tr("Earned %1/%2").arg(progress.earned).arg(progress.total), row_widget);
|
||||
earned->setAlignment(Qt::AlignRight);
|
||||
earned->setMinimumHeight(18);
|
||||
auto* bar = new QProgressBar(row_widget);
|
||||
bar->setRange(0, 100);
|
||||
bar->setValue(progress.Percentage());
|
||||
bar->setTextVisible(false);
|
||||
bar->setFixedWidth(130);
|
||||
percent_box->addWidget(percent);
|
||||
percent_box->addWidget(earned);
|
||||
percent_box->addWidget(bar);
|
||||
row->addLayout(percent_box);
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
if (grade == 1 && progress.total_grade[1] == 0) {
|
||||
continue;
|
||||
}
|
||||
row->addWidget(MakeGradeCount(row_widget, grade, progress.earned_grade[grade], 24, 18));
|
||||
}
|
||||
list->setItemWidget(item, row_widget);
|
||||
}
|
||||
if (trophy_games.empty()) {
|
||||
auto* empty =
|
||||
new QLabel(QObject::tr("No games with trophies found in the game folders."), &dialog);
|
||||
empty->setAlignment(Qt::AlignCenter);
|
||||
layout->insertWidget(1, empty);
|
||||
list->hide();
|
||||
}
|
||||
|
||||
connect(list, &QListWidget::itemClicked, &dialog,
|
||||
[&dialog, &trophy_games, &runtime_directory](QListWidgetItem* item) {
|
||||
const auto index = item->data(Qt::UserRole).toInt();
|
||||
if (index >= 0 && static_cast<size_t>(index) < trophy_games.size()) {
|
||||
ShowForGame(trophy_games[static_cast<size_t>(index)].game, runtime_directory,
|
||||
&dialog);
|
||||
}
|
||||
});
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
layout->addWidget(buttons);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, const QString& runtime_directory,
|
||||
QWidget* parent) {
|
||||
if (info == nullptr) {
|
||||
@@ -139,51 +343,131 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
|
||||
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
|
||||
continue;
|
||||
}
|
||||
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
|
||||
const auto unlocked = Common::Trophies::LoadUnlocks(
|
||||
Common::Trophies::UnlocksPath(GameContent::ToPath(runtime_directory),
|
||||
info.title_id.toStdString(), info.user_id, label));
|
||||
auto* table = new QTableWidget(static_cast<int>(package.trophies.size()), 4, m_tabs);
|
||||
PrepareTable(table);
|
||||
int row_index = 0;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
const bool trophy_unlocked = unlocked.contains(id);
|
||||
auto* status = new QTableWidgetItem(trophy_unlocked ? tr("UNLOCKED") : tr("LOCKED"));
|
||||
status->setTextAlignment(Qt::AlignCenter);
|
||||
auto status_font = status->font();
|
||||
status_font.setBold(true);
|
||||
status->setFont(status_font);
|
||||
table->setItem(row_index, 0, status);
|
||||
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
|
||||
const auto counts = Common::Trophies::GetProgress(package, unlocks);
|
||||
auto* page = new QWidget(m_tabs);
|
||||
auto* page_layout = new QVBoxLayout(page);
|
||||
|
||||
auto* icon_item = new QTableWidgetItem;
|
||||
icon_item->setTextAlignment(Qt::AlignCenter);
|
||||
QPixmap icon;
|
||||
if (icon.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
|
||||
static_cast<uint>(trophy.icon_png.size()))) {
|
||||
if (!trophy_unlocked) {
|
||||
icon = QPixmap::fromImage(
|
||||
icon.toImage().convertToFormat(QImage::Format_Grayscale8));
|
||||
}
|
||||
icon_item->setIcon(QIcon(icon));
|
||||
}
|
||||
table->setItem(row_index, 1, icon_item);
|
||||
|
||||
const auto name_text = trophy.name.empty() ? (trophy.hidden ? tr("Hidden Trophy")
|
||||
: tr("Trophy %1").arg(id))
|
||||
: QString::fromStdString(trophy.name);
|
||||
auto* name = new QTableWidgetItem(name_text);
|
||||
auto font = name->font();
|
||||
font.setBold(true);
|
||||
name->setFont(font);
|
||||
name->setToolTip(TrophyTooltip(trophy));
|
||||
auto* detail = new QTableWidgetItem(QString::fromStdString(trophy.description));
|
||||
detail->setToolTip(name->toolTip());
|
||||
table->setItem(row_index, 2, name);
|
||||
table->setItem(row_index, 3, detail);
|
||||
++row_index;
|
||||
auto* header = new QHBoxLayout;
|
||||
auto* art = new QLabel(page);
|
||||
if (const auto pixmap = LoadGameArt(&info, 56); !pixmap.isNull()) {
|
||||
art->setPixmap(pixmap);
|
||||
header->addWidget(art);
|
||||
}
|
||||
m_tabs->addTab(table, tr("Trophy %1").arg(label));
|
||||
auto* title = new QLabel(QString::fromStdString(package.title), page);
|
||||
title->setStyleSheet(QStringLiteral("font-size: 28px;"));
|
||||
header->addWidget(title, 1);
|
||||
const auto make_stat = [page](const QString& label, const QString& value) {
|
||||
auto* box = new QVBoxLayout;
|
||||
auto* name = new QLabel(label, page);
|
||||
auto* number = new QLabel(value, page);
|
||||
name->setStyleSheet(QStringLiteral("font-size: 14px;"));
|
||||
number->setStyleSheet(QStringLiteral("font-size: 22px;"));
|
||||
box->addWidget(name);
|
||||
box->addWidget(number);
|
||||
return box;
|
||||
};
|
||||
header->addLayout(
|
||||
make_stat(tr("Progress"), QStringLiteral("%1%").arg(counts.Percentage())));
|
||||
header->addLayout(
|
||||
make_stat(tr("Earned"), QStringLiteral("%1/%2").arg(counts.earned).arg(counts.total)));
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
if (counts.total_grade[grade] > 0) {
|
||||
header->addWidget(MakeGradeCount(page, grade, counts.earned_grade[grade], 30, 20));
|
||||
}
|
||||
}
|
||||
page_layout->addLayout(header);
|
||||
auto* progress = new QProgressBar(page);
|
||||
progress->setRange(0, 100);
|
||||
progress->setValue(counts.Percentage());
|
||||
progress->setTextVisible(false);
|
||||
page_layout->addWidget(progress);
|
||||
auto* count_label = new QLabel(tr("All trophies: %1").arg(counts.total), page);
|
||||
page_layout->addWidget(count_label);
|
||||
|
||||
auto* list = new QListWidget(page);
|
||||
list->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
list->setSpacing(4);
|
||||
page_layout->addWidget(list, 1);
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
const bool trophy_unlocked = unlocks.unlocked.contains(id);
|
||||
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
|
||||
auto* item = new QListWidgetItem(list);
|
||||
item->setSizeHint(QSize(900, 96));
|
||||
|
||||
auto* card = new QWidget(list);
|
||||
card->setStyleSheet(
|
||||
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
|
||||
card->setObjectName(QStringLiteral("card"));
|
||||
auto* row = new QHBoxLayout(card);
|
||||
auto* icon = new QLabel(card);
|
||||
icon->setFixedSize(76, 76);
|
||||
icon->setAlignment(Qt::AlignCenter);
|
||||
QPixmap pixmap;
|
||||
if (hidden_locked) {
|
||||
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
|
||||
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
|
||||
static_cast<uint>(trophy.icon_png.size())) &&
|
||||
!trophy_unlocked) {
|
||||
pixmap =
|
||||
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
|
||||
}
|
||||
if (!pixmap.isNull()) {
|
||||
icon->setPixmap(
|
||||
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
row->addWidget(icon);
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
const auto name_text = hidden_locked ? tr("Hidden trophy")
|
||||
: trophy.name.empty() ? tr("Trophy %1").arg(id)
|
||||
: QString::fromStdString(trophy.name);
|
||||
auto* name = new QLabel(name_text, card);
|
||||
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
|
||||
left->addWidget(name);
|
||||
if (!hidden_locked) {
|
||||
auto* grade_row = new QHBoxLayout;
|
||||
auto* cup = new QLabel(card);
|
||||
cup->setPixmap(MakeCupIcon(GradeColor(trophy.grade), 18));
|
||||
auto* grade_text = new QLabel(GradeToText(trophy.grade), card);
|
||||
grade_text->setStyleSheet(QStringLiteral("font-size: 15px;"));
|
||||
grade_row->addWidget(cup);
|
||||
grade_row->addWidget(grade_text);
|
||||
grade_row->addStretch(1);
|
||||
left->addLayout(grade_row);
|
||||
}
|
||||
|
||||
row->addLayout(left, 2);
|
||||
|
||||
auto* right = new QVBoxLayout;
|
||||
QString date_text;
|
||||
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
|
||||
date_text = QDateTime::fromMSecsSinceEpoch(
|
||||
static_cast<qint64>(
|
||||
(found->second - Common::Trophies::UnixEpochTick) / 1000))
|
||||
.toLocalTime()
|
||||
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
||||
} else if (trophy_unlocked) {
|
||||
date_text = tr("Earned");
|
||||
}
|
||||
auto* date = new QLabel(date_text, card);
|
||||
date->setAlignment(Qt::AlignRight);
|
||||
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
|
||||
auto* detail = new QLabel(
|
||||
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
|
||||
detail->setWordWrap(true);
|
||||
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
|
||||
right->addWidget(date);
|
||||
right->addWidget(detail, 1);
|
||||
row->addLayout(right, 3);
|
||||
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
|
||||
list->setItemWidget(item, card);
|
||||
}
|
||||
m_tabs->addTab(page, QString::fromStdString(package.title));
|
||||
}
|
||||
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
|
||||
if (m_tabs->count() == 0) {
|
||||
error = errors.join(QLatin1Char('\n'));
|
||||
return false;
|
||||
|
||||
@@ -39,9 +39,11 @@ QLineEdit#search_line_edit:focus {
|
||||
border-color: palette(highlight);
|
||||
}
|
||||
|
||||
QToolButton#refresh_button,
|
||||
QToolButton#global_settings_button,
|
||||
QToolButton#input_mapping_button,
|
||||
QToolButton#edit_button,
|
||||
QToolButton#trophy_overview_button,
|
||||
QToolButton#delete_button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
@@ -49,17 +51,21 @@ QToolButton#delete_button {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
QToolButton#refresh_button:hover,
|
||||
QToolButton#global_settings_button:hover,
|
||||
QToolButton#input_mapping_button:hover,
|
||||
QToolButton#edit_button:hover,
|
||||
QToolButton#trophy_overview_button:hover,
|
||||
QToolButton#delete_button:hover {
|
||||
background: palette(button);
|
||||
border-color: palette(mid);
|
||||
}
|
||||
|
||||
QToolButton#refresh_button:pressed,
|
||||
QToolButton#global_settings_button:pressed,
|
||||
QToolButton#input_mapping_button:pressed,
|
||||
QToolButton#edit_button:pressed,
|
||||
QToolButton#trophy_overview_button:pressed,
|
||||
QToolButton#delete_button:pressed {
|
||||
background: palette(midlight);
|
||||
}
|
||||
|
||||
+56
-29
@@ -2831,6 +2831,8 @@ constexpr int32_t KERNEL_AIO_STATE_ABORTED = 4;
|
||||
constexpr int32_t KERNEL_AIO_STATE_NOTIFIED = 0x10000;
|
||||
constexpr int32_t KERNEL_AIO_MAX_QUEUE = 512;
|
||||
constexpr int32_t KERNEL_AIO_MAX_REQUESTS = 128;
|
||||
constexpr uint32_t KERNEL_AIO_WAIT_AND = 1;
|
||||
constexpr uint32_t KERNEL_AIO_WAIT_OR = 2;
|
||||
|
||||
struct KernelAioResult {
|
||||
int64_t return_value;
|
||||
@@ -2933,26 +2935,28 @@ int KYTY_SYSV_ABI KernelAioSubmitWriteCommands(KernelAioRwRequest* req, int32_t
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int32_t kernel_aio_get_state(int32_t id, bool notify) {
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
return LibKernel::KERNEL_ERROR_ESRCH;
|
||||
}
|
||||
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (notify && (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED)) {
|
||||
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
|
||||
std::memory_order_acq_rel)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioPollRequest(int32_t id, int32_t* state) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (state == nullptr) {
|
||||
return LibKernel::KERNEL_ERROR_EFAULT;
|
||||
}
|
||||
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
*state = LibKernel::KERNEL_ERROR_ESRCH;
|
||||
return OK;
|
||||
}
|
||||
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED) {
|
||||
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
|
||||
std::memory_order_acq_rel)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*state = current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
|
||||
*state = kernel_aio_get_state(id, true);
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -2969,31 +2973,53 @@ int KYTY_SYSV_ABI KernelAioPollRequests(int32_t* ids, int32_t num, int32_t* stat
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequests(int32_t* ids, int32_t num, int32_t* states, uint32_t mode,
|
||||
uint32_t* usec) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (state == nullptr) {
|
||||
if (ids == nullptr || states == nullptr) {
|
||||
return LibKernel::KERNEL_ERROR_EFAULT;
|
||||
}
|
||||
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
return KernelAioPollRequest(id, state);
|
||||
if (num <= 0 || num > KERNEL_AIO_MAX_REQUESTS ||
|
||||
(num != 1 && mode != KERNEL_AIO_WAIT_AND && mode != KERNEL_AIO_WAIT_OR)) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
uint32_t waited = 0;
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (current == KERNEL_AIO_STATE_PROCESSING) {
|
||||
if (usec != nullptr && *usec != 0 && waited >= *usec) {
|
||||
*state = current;
|
||||
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
const auto collect_states = [&](bool notify) {
|
||||
int32_t finished = 0;
|
||||
bool newly_complete = false;
|
||||
for (int32_t i = 0; i < num; i++) {
|
||||
const auto current = kernel_aio_get_state(ids[i], notify);
|
||||
states[i] = current;
|
||||
finished += current != KERNEL_AIO_STATE_PROCESSING;
|
||||
newly_complete |= current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED;
|
||||
}
|
||||
return finished == num || (mode == KERNEL_AIO_WAIT_OR && newly_complete);
|
||||
};
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
const bool complete = collect_states(false);
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
const bool timed_out = usec != nullptr && elapsed >= *usec;
|
||||
if (complete || timed_out) {
|
||||
// A competing waiter can claim the newly completed request after the first scan.
|
||||
if (collect_states(true)) {
|
||||
if (usec != nullptr) {
|
||||
*usec = elapsed < *usec ? *usec - static_cast<uint32_t>(elapsed) : 0;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
if (timed_out) {
|
||||
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Thread::SleepMicro(10);
|
||||
waited += 10;
|
||||
current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
}
|
||||
}
|
||||
|
||||
return KernelAioPollRequest(id, state);
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
|
||||
return KernelAioWaitRequests(&id, 1, state, KERNEL_AIO_WAIT_AND, usec);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioDeleteRequest(int32_t id, int32_t* ret) {
|
||||
@@ -3376,6 +3402,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("2pOuoWoCxdk", KernelAioPollRequest);
|
||||
LIB_FUNC("o7O4z3jwKzo", KernelAioPollRequests);
|
||||
LIB_FUNC("KOF-oJbQVvc", KernelAioWaitRequest);
|
||||
LIB_FUNC("lgK+oIWkJyA", KernelAioWaitRequests);
|
||||
LIB_FUNC("XQ8C8y+de+E", KernelAioSubmitWriteCommands);
|
||||
LIB_FUNC("nu4a0-arQis", KernelAioInitializeParam);
|
||||
LIB_FUNC("il03nluKfMk", LibKernel::KernelRaiseException);
|
||||
|
||||
+26
-2
@@ -89,6 +89,10 @@ int KYTY_SYSV_ABI NetBind(int s, const void* addr, uint32_t addrlen) {
|
||||
return FinishSocketCall(Net::Bind(s, addr, addrlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetConnect(int s, const void* addr, uint32_t addrlen) {
|
||||
return FinishSocketCall(Net::Connect(s, addr, addrlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetListen(int s, int backlog) {
|
||||
return FinishSocketCall(Net::Listen(s, backlog));
|
||||
}
|
||||
@@ -170,14 +174,24 @@ int KYTY_SYSV_ABI NetSocketClose(int s) {
|
||||
return FinishNetCall(Net::SocketClose(s));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetGetsockopt(int s, int level, int optname, void* optval, uint32_t* optlen) {
|
||||
return FinishSocketCall(Net::Getsockopt(s, level, optname, optval, optlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optval,
|
||||
uint32_t optlen) {
|
||||
return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
|
||||
int KYTY_SYSV_ABI NetSendto(int s, const void* buf, size_t len, int flags, const void* addr,
|
||||
uint32_t addrlen) {
|
||||
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
|
||||
return FinishSocketCall(static_cast<int>(Net::Send(s, buf, size, flags | 0x20000)));
|
||||
return FinishSocketCall(
|
||||
static_cast<int>(Net::Sendto(s, buf, size, flags | 0x20000, addr, addrlen)));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
|
||||
return NetSendto(s, buf, len, flags, nullptr, 0);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
|
||||
@@ -185,6 +199,12 @@ int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
|
||||
return FinishSocketCall(static_cast<int>(Net::Recv(s, buf, size, flags)));
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI NetRecvfrom(int s, void* buf, size_t len, int flags, void* addr,
|
||||
uint32_t* addrlen) {
|
||||
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
|
||||
return FinishSocketCall(static_cast<int>(Net::Recvfrom(s, buf, size, flags, addr, addrlen)));
|
||||
}
|
||||
|
||||
uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) {
|
||||
return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) |
|
||||
((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u);
|
||||
@@ -209,6 +229,7 @@ LIB_DEFINE(InitNet_1_Net) {
|
||||
LIB_FUNC("HQOwnfMGipQ", LibNet::GetNetErrorAddr);
|
||||
LIB_FUNC("PIWqhn9oSxc", LibNet::NetAccept);
|
||||
LIB_FUNC("bErx49PgxyY", LibNet::NetBind);
|
||||
LIB_FUNC("OXXX4mUk3uk", LibNet::NetConnect);
|
||||
LIB_FUNC("dgJBaeJnGpo", LibNet::NetPoolCreate);
|
||||
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
|
||||
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
|
||||
@@ -229,9 +250,12 @@ LIB_DEFINE(InitNet_1_Net) {
|
||||
LIB_FUNC("TSM6whtekok", LibNet::NetShutdown);
|
||||
LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket);
|
||||
LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose);
|
||||
LIB_FUNC("xphrZusl78E", LibNet::NetGetsockopt);
|
||||
LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt);
|
||||
LIB_FUNC("beRjXBn-z+o", LibNet::NetSend);
|
||||
LIB_FUNC("gvD1greCu0A", LibNet::NetSendto);
|
||||
LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv);
|
||||
LIB_FUNC("304ooNZxWDY", LibNet::NetRecvfrom);
|
||||
LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl);
|
||||
LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons);
|
||||
LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl);
|
||||
|
||||
+86
-49
@@ -7,13 +7,12 @@
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -104,7 +103,7 @@ using TrophyKey = std::pair<int, uint32_t>;
|
||||
|
||||
struct TrophyState {
|
||||
const Trophies::Package* package = nullptr;
|
||||
std::set<int> unlocked;
|
||||
Trophies::UnlockData unlocks;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
@@ -130,20 +129,33 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
|
||||
if (const auto it = g_states.find(key); it != g_states.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
std::string title_id;
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
auto& package = g_packages[key.second];
|
||||
if (package.trophies.empty()) {
|
||||
const auto filename = "/app0/" + Trophies::PackagePath(key.second).generic_string();
|
||||
package = Trophies::LoadPackage(LibKernel::FileSystem::GetRealFilename(filename),
|
||||
Config::GetConsoleLanguage());
|
||||
if (package.trophies.empty()) {
|
||||
LOGF("[Trophy] could not load trophy package %s for %s\n", filename.c_str(),
|
||||
title_id.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
LOGF("[Trophy] loaded %zu trophy definitions for %s (service %u)\n",
|
||||
package.trophies.size(), title_id.c_str(), key.second);
|
||||
const auto uds_filename = "/app0/" + Trophies::UdsPackagePath(key.second).generic_string();
|
||||
package.event_rules =
|
||||
Trophies::LoadUdsRules(LibKernel::FileSystem::GetRealFilename(uds_filename));
|
||||
if (package.event_rules.empty()) {
|
||||
LOGF("[Trophy] no supported UDS extraction rules in %s\n", uds_filename.c_str());
|
||||
} else {
|
||||
LOGF("[Trophy] loaded UDS extraction rules for %zu events from %s\n",
|
||||
package.event_rules.size(), uds_filename.c_str());
|
||||
}
|
||||
}
|
||||
std::string title_id;
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocked = Trophies::LoadUnlocks(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocked), std::move(path)})
|
||||
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)})
|
||||
.first->second;
|
||||
}
|
||||
|
||||
@@ -165,7 +177,7 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
details->group_id = trophy.group_id;
|
||||
details->hidden = trophy.hidden;
|
||||
details->has_reward = trophy.has_reward;
|
||||
if (trophy.target) {
|
||||
if (trophy.progressive && trophy.target) {
|
||||
details->target.type = 1;
|
||||
details->target.value = *trophy.target;
|
||||
}
|
||||
@@ -177,8 +189,12 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
if (data != nullptr) {
|
||||
*data = {};
|
||||
data->trophy_id = trophy.id;
|
||||
data->unlocked = state.unlocked.contains(trophy.id);
|
||||
if (trophy.target) {
|
||||
data->unlocked = state.unlocks.unlocked.contains(trophy.id);
|
||||
if (const auto timestamp = state.unlocks.timestamps.find(trophy.id);
|
||||
timestamp != state.unlocks.timestamps.end()) {
|
||||
data->timestamp_tick = timestamp->second;
|
||||
}
|
||||
if (trophy.progressive && trophy.target) {
|
||||
data->progress.type = 1;
|
||||
data->progress.value = data->unlocked ? *trophy.target : 0;
|
||||
}
|
||||
@@ -188,19 +204,13 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
// Game and group structures share the same count fields.
|
||||
template <typename Details, typename Data>
|
||||
static void FillTrophyCounts(const TrophyState& state, int group, Details* details, Data* data) {
|
||||
std::array<uint32_t, 5> total {}, unlocked {};
|
||||
for (const auto& [id, trophy]: state.package->trophies) {
|
||||
if (group != -2 && trophy.group_id != group) {
|
||||
continue;
|
||||
}
|
||||
++total[trophy.grade];
|
||||
if (state.unlocked.contains(id)) {
|
||||
++unlocked[trophy.grade];
|
||||
}
|
||||
}
|
||||
const auto progress = Trophies::GetProgress(
|
||||
*state.package, state.unlocks, group == -2 ? std::nullopt : std::optional<int>(group));
|
||||
const auto& total = progress.total_grade;
|
||||
const auto& unlocked = progress.earned_grade;
|
||||
if (details != nullptr) {
|
||||
*details = {};
|
||||
details->num_trophies = total[1] + total[2] + total[3] + total[4];
|
||||
details->num_trophies = progress.total;
|
||||
details->num_platinum = total[1];
|
||||
details->num_gold = total[2];
|
||||
details->num_silver = total[3];
|
||||
@@ -208,14 +218,12 @@ static void FillTrophyCounts(const TrophyState& state, int group, Details* detai
|
||||
}
|
||||
if (data != nullptr) {
|
||||
*data = {};
|
||||
data->unlocked_trophies = unlocked[1] + unlocked[2] + unlocked[3] + unlocked[4];
|
||||
data->unlocked_trophies = progress.earned;
|
||||
data->unlocked_platinum = unlocked[1];
|
||||
data->unlocked_gold = unlocked[2];
|
||||
data->unlocked_silver = unlocked[3];
|
||||
data->unlocked_bronze = unlocked[4];
|
||||
const auto points = total[2] * 6 + total[3] * 2 + total[4];
|
||||
const auto earned = unlocked[2] * 6 + unlocked[3] * 2 + unlocked[4];
|
||||
data->progress_percentage = points == 0 ? 0 : earned * 100 / points;
|
||||
data->progress_percentage = progress.Percentage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,31 +239,45 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
|
||||
}
|
||||
}
|
||||
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id) {
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
|
||||
const auto found = state.package->trophies.find(trophy_id);
|
||||
if (found == state.package->trophies.end() || found->second.grade == 1 ||
|
||||
found->second.target || !state.unlocked.insert(trophy_id).second) {
|
||||
if (found == state.package->trophies.end()) {
|
||||
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
|
||||
return;
|
||||
}
|
||||
if (found->second.grade == 1 || (!from_stat && found->second.progressive) ||
|
||||
!state.unlocks.unlocked.insert(trophy_id).second) {
|
||||
return;
|
||||
}
|
||||
const auto timestamp =
|
||||
Trophies::UnixEpochTick + std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
state.unlocks.timestamps.emplace(trophy_id, timestamp);
|
||||
std::vector<int> earned {trophy_id};
|
||||
const auto platinum_id = found->second.platinum_id;
|
||||
if (platinum_id >= 0 && state.package->trophies.contains(platinum_id) &&
|
||||
std::all_of(state.package->trophies.begin(), state.package->trophies.end(),
|
||||
[&](const auto& entry) {
|
||||
return entry.second.platinum_id != platinum_id ||
|
||||
state.unlocked.contains(entry.first);
|
||||
state.unlocks.unlocked.contains(entry.first);
|
||||
}) &&
|
||||
state.unlocked.insert(platinum_id).second) {
|
||||
state.unlocks.unlocked.insert(platinum_id).second) {
|
||||
state.unlocks.timestamps.emplace(platinum_id, timestamp);
|
||||
earned.push_back(platinum_id);
|
||||
}
|
||||
if (!Trophies::SaveUnlocks(state.path, state.unlocked)) {
|
||||
if (!Trophies::SaveUnlockData(state.path, state.unlocks)) {
|
||||
LOGF("[Trophy] could not save unlocks to %s\n", state.path.string().c_str());
|
||||
} else {
|
||||
LOGF("[Trophy] recorded unlock %d: %s\n", trophy_id, found->second.name.c_str());
|
||||
}
|
||||
if (Config::TrophyEnabled()) {
|
||||
for (const auto id: earned) {
|
||||
const auto& trophy = state.package->trophies.at(id);
|
||||
Graphics::NotifyTrophyUnlocked(trophy.name, trophy.grade, trophy.icon_png);
|
||||
}
|
||||
} else {
|
||||
LOGF("[Trophy] notification disabled by configuration\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,11 +563,13 @@ struct NpUniversalDataSystemMemoryStat {
|
||||
};
|
||||
|
||||
struct NpUniversalDataSystemEventPropertyObject {
|
||||
std::optional<int32_t> trophy_id;
|
||||
std::map<std::string, Common::Trophies::UdsInteger> integers;
|
||||
|
||||
void SetTrophyId(const char* key, std::optional<int32_t> value) {
|
||||
if (std::strcmp(key, "_trophy_id") == 0) {
|
||||
trophy_id = value;
|
||||
void SetInteger(const char* key, std::optional<Common::Trophies::UdsInteger> value) {
|
||||
if (value) {
|
||||
integers.insert_or_assign(key, *value);
|
||||
} else {
|
||||
integers.erase(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -647,6 +671,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemCreateEvent(
|
||||
}
|
||||
*new_event = new NpUniversalDataSystemEvent {
|
||||
event_name, prop != nullptr ? *prop : NpUniversalDataSystemEventPropertyObject {}};
|
||||
LOGF("[UDS] created event: %s\n", event_name);
|
||||
if (prop_ptr != nullptr) {
|
||||
*prop_ptr = &(*new_event)->properties;
|
||||
}
|
||||
@@ -668,14 +693,26 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemPostEvent(int context, int handle,
|
||||
return static_cast<int>(0x80553120); // NOT_REGISTERED
|
||||
}
|
||||
const auto& uds_event = *static_cast<const NpUniversalDataSystemEvent*>(event);
|
||||
LOGF("[UDS] posted event: %s\n", uds_event.name.c_str());
|
||||
if (uds_event.name == "_UnlockTrophy") {
|
||||
const auto trophy_id = uds_event.properties.trophy_id;
|
||||
if (!trophy_id || *trophy_id < 0) {
|
||||
const auto property = uds_event.properties.integers.find("_trophy_id");
|
||||
const auto* trophy_id = property == uds_event.properties.integers.end()
|
||||
? nullptr
|
||||
: std::get_if<int32_t>(&property->second);
|
||||
if (trophy_id == nullptr || *trophy_id < 0) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
|
||||
LibNpTrophy2::RecordTrophyUnlock(*state, *trophy_id);
|
||||
}
|
||||
} else if (!uds_event.properties.integers.empty()) {
|
||||
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
|
||||
const auto ids = LibNpTrophy2::Trophies::FindUdsTrophies(
|
||||
*state->package, uds_event.name, uds_event.properties.integers);
|
||||
for (const auto trophy_id: ids) {
|
||||
LibNpTrophy2::RecordTrophyUnlock(*state, trophy_id, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -809,7 +846,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetString(
|
||||
if (object == nullptr || key == nullptr || value == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -826,7 +863,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, value);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -838,7 +875,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -855,7 +892,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -872,7 +909,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -889,7 +926,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -906,7 +943,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -923,7 +960,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBool(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -941,7 +978,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBinary(
|
||||
if (object == nullptr || key == nullptr || (value == nullptr && value_size != 0)) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -962,7 +999,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetObject(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
if (value_ptr != nullptr) {
|
||||
*value_ptr =
|
||||
@@ -996,7 +1033,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetArray(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
if (value_ptr != nullptr) {
|
||||
*value_ptr = (value != nullptr ? const_cast<NpUniversalDataSystemEventPropertyArray*>(value)
|
||||
|
||||
+84
-12
@@ -992,6 +992,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case WSAENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
||||
@@ -1028,6 +1029,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case ENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
||||
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
@@ -1066,6 +1068,26 @@ static int ConvertSocketOptionLevel(int level) {
|
||||
return (level == 0xffff ? SOL_SOCKET : level);
|
||||
}
|
||||
|
||||
static int ConvertSocketOptionName(int level, int option) {
|
||||
if (level == 0xffff) {
|
||||
switch (option) {
|
||||
case 0x0020: return SO_BROADCAST;
|
||||
case 0x1001: return SO_SNDBUF;
|
||||
case 0x1002: return SO_RCVBUF;
|
||||
case 0x1007: return SO_ERROR;
|
||||
case 0x1105: return SO_SNDTIMEO;
|
||||
default: break;
|
||||
}
|
||||
} else if (level == 6 && option == 1) {
|
||||
return TCP_NODELAY;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return option;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||
switch (option) {
|
||||
case 0x0020: return &endpoint.broadcast;
|
||||
@@ -2149,21 +2171,41 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Guest socket options: SOL_SOCKET=0xffff, SO_ERROR=0x1007.
|
||||
const bool socket_error = (level == 0xffff && optname == 0x1007);
|
||||
#if !defined(_WIN32)
|
||||
if (!socket_error) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (socket_error) {
|
||||
optname = SO_ERROR;
|
||||
}
|
||||
SocketLength len = static_cast<SocketLength>(*optlen);
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
|
||||
void* value = optval;
|
||||
if (send_timeout) {
|
||||
if (*optlen < sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
value = &timeout;
|
||||
len = sizeof(timeout);
|
||||
}
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(value),
|
||||
&len) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
if (send_timeout) {
|
||||
#if defined(_WIN32)
|
||||
const int64_t usec = static_cast<int64_t>(timeout) * 1000;
|
||||
#else
|
||||
const int64_t usec = static_cast<int64_t>(timeout.tv_sec) * 1'000'000 + timeout.tv_usec;
|
||||
#endif
|
||||
const int guest_timeout =
|
||||
static_cast<int>(std::min<int64_t>(usec, std::numeric_limits<int>::max()));
|
||||
std::memcpy(optval, &guest_timeout, sizeof(guest_timeout));
|
||||
len = sizeof(guest_timeout);
|
||||
}
|
||||
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
|
||||
auto* error = static_cast<int*>(optval);
|
||||
*error = ConvertHostSocketError(*error);
|
||||
@@ -2215,6 +2257,18 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
if (level == 0xffff && optname == 0x10000) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int enabled = 0;
|
||||
std::memcpy(&enabled, optval, sizeof(enabled));
|
||||
// Native sends preserve 255.255.255.255 without changing broadcast permission.
|
||||
return enabled != 0 ? 0 : SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int ORBIS_SO_NBIO = 0x1200;
|
||||
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
||||
optlen >= sizeof(int)) {
|
||||
@@ -2230,14 +2284,32 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
#endif
|
||||
return failed ? SetHostSocketError() : 0;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
|
||||
if (level != 6 || optname != 1) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
level = IPPROTO_TCP;
|
||||
optname = TCP_NODELAY;
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (send_timeout) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int usec = 0;
|
||||
std::memcpy(&usec, optval, sizeof(usec));
|
||||
usec = std::max(usec, 0);
|
||||
#if defined(_WIN32)
|
||||
timeout = static_cast<DWORD>((static_cast<int64_t>(usec) + 999) / 1000);
|
||||
#else
|
||||
timeout.tv_sec = usec / 1'000'000;
|
||||
timeout.tv_usec = usec % 1'000'000;
|
||||
#endif
|
||||
optval = &timeout;
|
||||
optlen = sizeof(timeout);
|
||||
}
|
||||
|
||||
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
|
||||
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#if !defined(__APPLE__)
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <xbyak/xbyak_util.h>
|
||||
#endif
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
@@ -33,9 +31,7 @@
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
using namespace Xbyak::util;
|
||||
#endif
|
||||
|
||||
namespace Loader {
|
||||
|
||||
@@ -52,8 +48,6 @@ using s64 = int64_t;
|
||||
|
||||
constexpr size_t NearJumpSize = 5;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
struct PatchModule {
|
||||
std::mutex mutex {};
|
||||
u8* start = nullptr;
|
||||
@@ -1573,19 +1567,6 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
|
||||
return {};
|
||||
}
|
||||
|
||||
GuestInstructionPatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool,
|
||||
GuestInstructionHostFeatures) {
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f;
|
||||
@@ -1763,7 +1744,6 @@ bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_fram
|
||||
|
||||
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
void* trampoline_area_ptr, uint64_t trampoline_area_size) {
|
||||
#if !defined(__APPLE__)
|
||||
EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr ||
|
||||
trampoline_area_size == 0);
|
||||
const auto module_addr = reinterpret_cast<u64>(module_ptr);
|
||||
@@ -1772,20 +1752,10 @@ void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
std::forward_as_tuple(static_cast<u8*>(module_ptr), module_size,
|
||||
static_cast<u8*>(trampoline_area_ptr),
|
||||
trampoline_area_size));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
(void)module_size;
|
||||
(void)trampoline_area_ptr;
|
||||
(void)trampoline_area_size;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
|
||||
#if !defined(__APPLE__)
|
||||
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
#undef ASSERT
|
||||
|
||||
@@ -1670,11 +1670,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
uint64_t tls_handler_size = is_shared ? 0 : Jit::SafeCall::GetSize();
|
||||
EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned);
|
||||
program->mapped_size = program->base_size_aligned + tls_handler_size;
|
||||
#if !defined(__APPLE__)
|
||||
const bool emulate_amd = Config::AmdCpuEnabled();
|
||||
#else
|
||||
const bool emulate_amd = false;
|
||||
#endif
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
|
||||
#else
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif defined(__APPLE__)
|
||||
#include <sched.h>
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
@@ -468,6 +469,16 @@ struct Context {
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
// The kernel hands AVX threads the longer context that carries YMM[255:128]; a plain SSE
|
||||
// context has no such state to clear.
|
||||
void ClearUpperYmm(uint8_t index) const {
|
||||
if (index >= 16 || native->uc_mcsize < sizeof(_STRUCT_MCONTEXT_AVX64)) {
|
||||
return;
|
||||
}
|
||||
auto* avx = reinterpret_cast<_STRUCT_MCONTEXT_AVX64*>(native->uc_mcontext);
|
||||
auto* ymmh = &avx->__fs.__fpu_ymmh0;
|
||||
ymmh[index] = {};
|
||||
}
|
||||
#else
|
||||
ucontext_t* native;
|
||||
|
||||
@@ -703,8 +714,6 @@ static bool TryEmulateCpuExtensions(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
@@ -790,8 +799,6 @@ static bool TryEmulateReciprocalSquareRoot(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool TryEmulate(void* native_context) {
|
||||
if (native_context == nullptr) {
|
||||
return false;
|
||||
@@ -807,12 +814,12 @@ bool TryEmulate(void* native_context) {
|
||||
#else
|
||||
Context context {static_cast<ucontext_t*>(native_context)};
|
||||
#endif
|
||||
#if !defined(__APPLE__)
|
||||
return TryEmulateReciprocalSquareRoot(context) || TryEmulateMonitorxMwaitx(context) ||
|
||||
TryEmulateSse4a(context) || TryEmulateShaNi(context) || TryEmulateCpuExtensions(context);
|
||||
#else
|
||||
return TryEmulateSse4a(context) || TryEmulateCpuExtensions(context);
|
||||
TryEmulateSse4a(context) ||
|
||||
#if !defined(__APPLE__)
|
||||
TryEmulateShaNi(context) ||
|
||||
#endif
|
||||
TryEmulateCpuExtensions(context);
|
||||
}
|
||||
|
||||
} // namespace Loader::X64InstructionEmulator
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Libs::LibKernelApr {
|
||||
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
|
||||
}
|
||||
@@ -164,12 +169,14 @@ void TestAioBatches() {
|
||||
using Batch = int (KYTY_SYSV_ABI *)(int32_t *, int32_t, int32_t *);
|
||||
using Single = int (KYTY_SYSV_ABI *)(int32_t, int32_t *);
|
||||
using Wait = int (KYTY_SYSV_ABI *)(int32_t, int32_t *, uint32_t *);
|
||||
using WaitBatch = int (KYTY_SYSV_ABI *)(int32_t *, int32_t, int32_t *, uint32_t, uint32_t *);
|
||||
const auto submit = reinterpret_cast<Submit>(find("HgX7+AORI58"));
|
||||
const auto poll = reinterpret_cast<Batch>(find("o7O4z3jwKzo"));
|
||||
const auto erase = reinterpret_cast<Batch>(find("Ft3EtsZzAoY"));
|
||||
const auto poll_one = reinterpret_cast<Single>(find("2pOuoWoCxdk"));
|
||||
const auto erase_one = reinterpret_cast<Single>(find("5TgME6AYty4"));
|
||||
const auto wait = reinterpret_cast<Wait>(find("KOF-oJbQVvc"));
|
||||
const auto wait_batch = reinterpret_cast<WaitBatch>(find("lgK+oIWkJyA"));
|
||||
constexpr char Payload[] = "AIO payload";
|
||||
const int fd = FileSystem::KernelOpen("/savedata0/aio.dat", 0x602, 0777);
|
||||
Check(fd >= 3 && FileSystem::KernelWrite(fd, Payload, sizeof(Payload)) == sizeof(Payload),
|
||||
@@ -186,14 +193,22 @@ void TestAioBatches() {
|
||||
}
|
||||
ids[2] = -1;
|
||||
std::array<int32_t, 3> states {-1, -1, -1};
|
||||
Check(poll(ids.data(), ids.size(), states.data()) == OK &&
|
||||
uint32_t timeout = 0;
|
||||
Check(wait_batch(ids.data(), ids.size(), states.data(), 2, &timeout) == OK &&
|
||||
states[0] == 3 && states[1] == 3 && states[2] == Kernel::KERNEL_ERROR_ESRCH,
|
||||
"batch poll writes every state and per-request invalid-ID error");
|
||||
"OR wait observes completed reads and invalid-ID errors even with a zero timeout");
|
||||
Check(poll_one(ids[0], &states[0]) == OK && states[0] == (3 | 0x10000) &&
|
||||
poll(ids.data(), 2, states.data()) == OK &&
|
||||
states[0] == (3 | 0x10000) && states[1] == (3 | 0x10000) &&
|
||||
wait(ids[0], &states[0], nullptr) == OK && states[0] == (3 | 0x10000),
|
||||
"single and batch polls and waits share completion notification state");
|
||||
timeout = 1000000;
|
||||
Check(wait_batch(ids.data(), ids.size(), states.data(), 1, &timeout) == OK &&
|
||||
states[0] == (3 | 0x10000) && states[1] == (3 | 0x10000) &&
|
||||
states[2] == Kernel::KERNEL_ERROR_ESRCH && timeout <= 1000000 &&
|
||||
wait_batch(ids.data(), ids.size(), states.data(), 2, nullptr) == OK &&
|
||||
wait_batch(ids.data(), 1, states.data(), 0, nullptr) == OK,
|
||||
"AND and OR waits return when all IDs are terminal and one-ID waits ignore mode");
|
||||
Check(erase(ids.data(), ids.size(), states.data()) == OK &&
|
||||
states[0] == OK && states[1] == OK && states[2] == Kernel::KERNEL_ERROR_ESRCH,
|
||||
"batch deletion writes per-request results");
|
||||
@@ -216,6 +231,22 @@ void TestAioBatches() {
|
||||
}),
|
||||
"maximum-sized batch returns all invalid-ID errors");
|
||||
}
|
||||
states.fill(42);
|
||||
Check(wait_batch(nullptr, 1, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EFAULT &&
|
||||
wait_batch(ids.data(), 1, nullptr, 1, nullptr) == Kernel::KERNEL_ERROR_EFAULT &&
|
||||
wait_batch(ids.data(), 0, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
|
||||
wait_batch(ids.data(), 129, states.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
|
||||
wait_batch(ids.data(), 2, states.data(), 0, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
|
||||
wait_batch(ids.data(), 2, states.data(), 3, nullptr) == Kernel::KERNEL_ERROR_EINVAL &&
|
||||
states == std::array<int32_t, 3> {42, 42, 42},
|
||||
"invalid wait arguments do not modify outputs");
|
||||
std::array<int32_t, 128> invalid_ids {};
|
||||
std::array<int32_t, 128> errors {};
|
||||
Check(wait_batch(invalid_ids.data(), invalid_ids.size(), errors.data(), 1, nullptr) == OK &&
|
||||
std::all_of(errors.begin(), errors.end(), [](int32_t error) {
|
||||
return error == Kernel::KERNEL_ERROR_ESRCH;
|
||||
}),
|
||||
"maximum-sized wait returns every invalid-ID error");
|
||||
Check(FileSystem::KernelClose(fd) == OK, "close AIO read fixture");
|
||||
}
|
||||
|
||||
@@ -1061,19 +1092,40 @@ void CheckSocketWakeup() {
|
||||
namespace Net = Libs::Network::Net;
|
||||
Loader::SymbolDatabase symbols;
|
||||
Libs::LibNet::InitNet_1_Net(&symbols);
|
||||
const auto *connect_symbol = symbols.Find(
|
||||
{"OXXX4mUk3uk", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *getsockopt_symbol = symbols.Find(
|
||||
{"xphrZusl78E", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *setsockopt_symbol = symbols.Find(
|
||||
{"2mKX2Spso7I", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *send_symbol = symbols.Find(
|
||||
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *sendto_symbol = symbols.Find(
|
||||
{"gvD1greCu0A", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *recv_symbol = symbols.Find(
|
||||
{"9wO9XrMsNhc", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *recvfrom_symbol = symbols.Find(
|
||||
{"304ooNZxWDY", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
const auto *errno_symbol = symbols.Find(
|
||||
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
|
||||
Check(send_symbol && recv_symbol && errno_symbol,
|
||||
"Net send, receive and errno exports resolve with the guest ABI versions");
|
||||
Check(connect_symbol && getsockopt_symbol && setsockopt_symbol && send_symbol && sendto_symbol &&
|
||||
recv_symbol && recvfrom_symbol && errno_symbol,
|
||||
"Net socket and errno exports resolve with the guest ABI versions");
|
||||
using Connect = int (KYTY_SYSV_ABI *)(int, const void *, uint32_t);
|
||||
using Getsockopt = int (KYTY_SYSV_ABI *)(int, int, int, void *, uint32_t *);
|
||||
using Setsockopt = int (KYTY_SYSV_ABI *)(int, int, int, const void *, uint32_t);
|
||||
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
|
||||
using Sendto = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int, const void *, uint32_t);
|
||||
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
|
||||
using Recvfrom = int (KYTY_SYSV_ABI *)(int, void *, size_t, int, void *, uint32_t *);
|
||||
using Errno = int *(KYTY_SYSV_ABI *)();
|
||||
const auto net_connect = reinterpret_cast<Connect>(connect_symbol->vaddr);
|
||||
const auto net_getsockopt = reinterpret_cast<Getsockopt>(getsockopt_symbol->vaddr);
|
||||
const auto net_setsockopt = reinterpret_cast<Setsockopt>(setsockopt_symbol->vaddr);
|
||||
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
|
||||
const auto net_sendto = reinterpret_cast<Sendto>(sendto_symbol->vaddr);
|
||||
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
|
||||
const auto net_recvfrom = reinterpret_cast<Recvfrom>(recvfrom_symbol->vaddr);
|
||||
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
|
||||
const auto [reader, writer] = CreateTcpPair();
|
||||
const int enabled = 1;
|
||||
@@ -1082,10 +1134,15 @@ void CheckSocketWakeup() {
|
||||
int socket_error = -1;
|
||||
uint32_t error_size = sizeof(socket_error);
|
||||
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
||||
Check(Net::Getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
|
||||
socket_error == 0 && error_size == sizeof(socket_error) &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"SO_ERROR reports socket status without changing guest errno");
|
||||
"Net SO_ERROR reports socket status without changing either guest errno");
|
||||
Check(net_getsockopt(writer, 0xffff, 0x1007, nullptr, &error_size) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
|
||||
"Net getsockopt translates an invalid output buffer");
|
||||
|
||||
std::array<uint64_t, 16> readable {};
|
||||
const auto bit = uint64_t {1} << (reader % 64);
|
||||
@@ -1122,6 +1179,131 @@ void CheckSocketWakeup() {
|
||||
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"Net nonblocking receive translates POSIX failure and Net errno");
|
||||
#endif
|
||||
const int datagram = Net::Socket(2, 2, 0);
|
||||
const int datagram_writer = Net::Socket(2, 2, 0);
|
||||
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
|
||||
std::array<uint8_t, 16> peer {}, expected_peer {};
|
||||
uint32_t address_size = address.size(), peer_size = peer.size();
|
||||
uint32_t expected_peer_size = expected_peer.size();
|
||||
Check(datagram >= 0 && datagram_writer >= 0 &&
|
||||
Net::Bind(datagram, address.data(), address.size()) == 0 &&
|
||||
Net::Bind(datagram_writer, address.data(), address.size()) == 0 &&
|
||||
Net::Getsockname(datagram, address.data(), &address_size) == 0 &&
|
||||
Net::Getsockname(datagram_writer, expected_peer.data(), &expected_peer_size) == 0 &&
|
||||
Net::Setsockopt(datagram, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0,
|
||||
"create nonblocking loopback datagrams for Net ABI verification");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
|
||||
for (const int option : {0x1001, 0x1002}) {
|
||||
constexpr int requested = 16384;
|
||||
int actual = 0;
|
||||
uint32_t size = sizeof(actual);
|
||||
Check(net_setsockopt(datagram, 0xffff, option, &requested, sizeof(requested)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, option, &actual, &size) == 0 &&
|
||||
actual >= requested && size == sizeof(actual),
|
||||
"Net UDP send and receive buffers accept guest option numbers");
|
||||
}
|
||||
for (const int value : {1, 0}) {
|
||||
int actual = -1;
|
||||
uint32_t size = sizeof(actual);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x20, &value, sizeof(value)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, 0x20, &actual, &size) == 0 &&
|
||||
actual == value && size == sizeof(actual),
|
||||
"Net UDP broadcast option can be enabled and disabled");
|
||||
}
|
||||
int timeout = 0;
|
||||
int *timeout_value = &timeout;
|
||||
#if defined(__linux__)
|
||||
int broadcast = -1;
|
||||
uint32_t broadcast_size = sizeof(broadcast);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x10000, &enabled, sizeof(enabled)) == 0 &&
|
||||
net_getsockopt(datagram, 0xffff, 0x20, &broadcast, &broadcast_size) == 0 &&
|
||||
broadcast == 0,
|
||||
"preserving the all-ones destination does not enable broadcast permission");
|
||||
const long page_size = sysconf(_SC_PAGESIZE);
|
||||
Check(page_size > 0, "get host page size for socket timeout boundary");
|
||||
void *timeout_pages = mmap(nullptr, page_size * 2, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Check(timeout_pages != MAP_FAILED &&
|
||||
mprotect(static_cast<char *>(timeout_pages) + page_size, page_size,
|
||||
PROT_NONE) == 0,
|
||||
"guard memory after the four-byte socket timeout");
|
||||
timeout_value = reinterpret_cast<int *>(static_cast<char *>(timeout_pages) +
|
||||
page_size - sizeof(int));
|
||||
#endif
|
||||
for (const int value : {1500000, 0, -1}) {
|
||||
*timeout_value = value;
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, timeout_value, sizeof(int)) == 0,
|
||||
"Net send timeout reads a four-byte microsecond value");
|
||||
*timeout_value = -2;
|
||||
uint32_t size = sizeof(int);
|
||||
Check(net_getsockopt(datagram, 0xffff, 0x1105, timeout_value, &size) == 0 &&
|
||||
*timeout_value == std::max(value, 0) && size == sizeof(int),
|
||||
"Net send timeout returns four-byte microseconds and disables nonpositive values");
|
||||
}
|
||||
#if defined(__linux__)
|
||||
Check(munmap(timeout_pages, page_size * 2) == 0, "free socket timeout guard pages");
|
||||
#endif
|
||||
Check(*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
|
||||
"successful Net socket option calls preserve both guest errno values");
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, nullptr, sizeof(timeout)) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT &&
|
||||
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, nullptr) ==
|
||||
Libs::Network::NET_ERROR_EFAULT && *net_errno == Libs::Posix::POSIX_EFAULT,
|
||||
"Net socket options reject null value and length pointers");
|
||||
uint32_t short_size = sizeof(timeout) - 1;
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1105, &timeout, short_size) ==
|
||||
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_getsockopt(datagram, 0xffff, 0x1105, &timeout, &short_size) ==
|
||||
Libs::Network::NET_ERROR_EINVAL && *net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net send timeout rejects undersized values");
|
||||
uint32_t option_size = sizeof(timeout);
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x7fffffff, &timeout, option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT &&
|
||||
net_getsockopt(datagram, 0xffff, 0x7fffffff, &timeout, &option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||
"Net unknown socket options return protocol-option errors");
|
||||
#if defined(__linux__)
|
||||
Check(net_setsockopt(datagram, 0xffff, 0x1007, &timeout, option_size) ==
|
||||
Libs::Network::NET_ERROR_ENOPROTOOPT &&
|
||||
*net_errno == Libs::Posix::POSIX_ENOPROTOOPT,
|
||||
"Net native protocol-option errors retain their guest error code");
|
||||
#endif
|
||||
received.fill(0);
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_sendto(datagram_writer, payload, sizeof(payload), 0,
|
||||
address.data(), address_size) == sizeof(payload) &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net sendto delivers to a guest sockaddr and preserves errno on success");
|
||||
Check(net_recvfrom(datagram, received.data(), received.size(), 0,
|
||||
peer.data(), &peer_size) == sizeof(payload) &&
|
||||
std::memcmp(received.data(), payload, sizeof(payload)) == 0 &&
|
||||
peer_size == expected_peer_size && peer == expected_peer &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL,
|
||||
"Net recvfrom returns datagram bytes and sender address while preserving errno");
|
||||
Check(net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
|
||||
Libs::Network::NET_ERROR_EWOULDBLOCK &&
|
||||
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
|
||||
"Net recvfrom translates nonblocking failure with an omitted sender address");
|
||||
*net_errno = Libs::Posix::POSIX_EINVAL;
|
||||
Check(net_connect(datagram_writer, address.data(), address_size) == 0 &&
|
||||
*net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_sendto(datagram_writer, payload, sizeof(payload), 0, nullptr, 0) ==
|
||||
sizeof(payload) && *net_errno == Libs::Posix::POSIX_EINVAL &&
|
||||
net_recvfrom(datagram, received.data(), received.size(), 0, nullptr, nullptr) ==
|
||||
sizeof(payload) && std::memcmp(received.data(), payload, sizeof(payload)) == 0,
|
||||
"Net connect selects the peer used by sendto with an omitted destination");
|
||||
Check(net_connect(-1, address.data(), address_size) == Libs::Network::NET_ERROR_EBADF &&
|
||||
*net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net connect translates an invalid socket");
|
||||
Check(net_sendto(-1, payload, sizeof(payload), 0, address.data(), address_size) ==
|
||||
Libs::Network::NET_ERROR_EBADF && *net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net sendto translates an invalid socket");
|
||||
Check(Net::SocketClose(datagram) == 0 && Net::SocketClose(datagram_writer) == 0,
|
||||
"close Net loopback datagrams");
|
||||
Check(net_send(-1, payload, sizeof(payload), 0) == Libs::Network::NET_ERROR_EBADF &&
|
||||
*net_errno == Libs::Posix::POSIX_EBADF,
|
||||
"Net send translates an invalid socket instead of returning POSIX minus one");
|
||||
@@ -1138,10 +1320,12 @@ void CheckSocketWakeup() {
|
||||
const auto previous_sigpipe = std::signal(SIGPIPE, SIG_DFL);
|
||||
Check(previous_sigpipe != SIG_ERR, "set default SIGPIPE disposition for Net send");
|
||||
const auto broken_send = net_send(disconnected, payload, sizeof(payload), 0);
|
||||
const auto broken_sendto = net_sendto(disconnected, payload, sizeof(payload), 0, nullptr, 0);
|
||||
std::signal(SIGPIPE, previous_sigpipe);
|
||||
Check(broken_send == Libs::Network::NET_ERROR_EPIPE &&
|
||||
broken_sendto == Libs::Network::NET_ERROR_EPIPE &&
|
||||
*net_errno == Libs::Posix::POSIX_EPIPE,
|
||||
"Net send reports a broken pipe without raising host SIGPIPE");
|
||||
"Net send and sendto report a broken pipe without raising host SIGPIPE");
|
||||
Check(Net::SocketClose(disconnected) == 0, "close unconnected socket");
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
|
||||
@@ -32243,6 +32243,66 @@ TestCase DsAppendUsesEncodedGdsSelector() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase DsOrderedCountAddressAndExec(u32 wave_size, bool pixel_counter) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = pixel_counter
|
||||
? (wave_size == 64 ? "DsOrderedCountPixelAddressWave64"
|
||||
: "DsOrderedCountPixelAddressWave32")
|
||||
: (wave_size == 64 ? "DsOrderedCountAddressAndExecWave64"
|
||||
: "DsOrderedCountAddressAndExecWave32");
|
||||
auto &code = test.code;
|
||||
// Both the base and OFFSET0 are aligned independently; M0's low bits are
|
||||
// a wave index, or a pixel packer ID, rather than a byte address or size.
|
||||
AppendSMovLiteral(&code, 124, 0x01030003u);
|
||||
code.push_back(EncodeSop1(0x04, 10, 126)); // Save the full EXEC mask.
|
||||
code.push_back(EncodeVop2(0x25, 2, InlineU32(1), 0)); // ADDR value = lane + 1.
|
||||
AppendVMovU32(&code, 9, 99); // DATA0 must not supply the count.
|
||||
const u32 selected_lane = wave_size == 64 ? 47u : 7u;
|
||||
const std::array<uint64_t, 3> masks = {
|
||||
wave_size == 64 ? UINT64_MAX : uint64_t{UINT32_MAX},
|
||||
uint64_t{1} << selected_lane, 0};
|
||||
u32 counter = 1000;
|
||||
for (bool exchange : {false, true}) {
|
||||
for (size_t mode = 0; mode < masks.size(); ++mode) {
|
||||
AppendVMovU32(&code, 3, 0xdeadbeefu);
|
||||
AppendSMovLiteral(&code, 126, static_cast<u32>(masks[mode]));
|
||||
AppendSMovLiteral(&code, 127, static_cast<u32>(masks[mode] >> 32u));
|
||||
const u32 release_done = exchange && mode == 2 ? 3u : 0u;
|
||||
const u32 offset1 = release_done | (pixel_counter ? 4u : 0u) |
|
||||
(exchange ? 0x10u : 0u);
|
||||
code.push_back(EncodeDs0(0x3f, (offset1 << 8u) | 7u, true));
|
||||
code.push_back(EncodeDs1(3, 9, 2));
|
||||
code.push_back(EncodeSopp(0x0c, 0)); // Wait for the returned GDS value.
|
||||
code.push_back(EncodeSop1(0x04, 126, 10)); // Restore EXEC before reading all lanes.
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, 3, 0,
|
||||
static_cast<u32>(test.expected.size()));
|
||||
test.expected.insert(test.expected.end(), wave_size, counter);
|
||||
if (mode != 2) {
|
||||
const u32 value = mode == 0 ? 1u : selected_lane + 1u;
|
||||
counter = exchange ? value : counter + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.initial.assign(test.expected.size(), 0xdeadbeefu);
|
||||
const u32 counter_address = pixel_counter ? 0x110u : 0x104u;
|
||||
test.gds_initial.assign(0x120u / sizeof(u32), 100);
|
||||
test.gds_initial[counter_address / sizeof(u32)] = 1000;
|
||||
test.expected_gds = test.gds_initial;
|
||||
test.expected_gds[counter_address / sizeof(u32)] = counter;
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::V_ADD_NC_U32, O::V_MOV_B32,
|
||||
O::DS_ORDERED_COUNT, O::S_WAITCNT, O::V_LSHLREV_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase DsAppendConsumeGdsRegionBounds() {
|
||||
using O = ShaderOpcode;
|
||||
struct Access {
|
||||
@@ -33541,7 +33601,8 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
|
||||
candidate.indirect_search_iterations = 0;
|
||||
candidate.indirect_resources.clear();
|
||||
program.descriptor_sources.resize(1);
|
||||
program.descriptor_sources[0].indirect_descriptor.emplace().table_stride = 16;
|
||||
program.descriptor_sources[0].indirect_descriptor =
|
||||
DescriptorSource::IndirectDescriptor{.table_stride = 16};
|
||||
ShaderComputeInputInfo compute;
|
||||
compute.wave_size = 32;
|
||||
compute.host_subgroup_size = vulkan.SubgroupSize();
|
||||
@@ -35537,6 +35598,10 @@ std::vector<TestCase> MakeCases() {
|
||||
cases.push_back(DsAppendAllocatesAcrossWaves(32));
|
||||
cases.push_back(DsAppendAllocatesAcrossWaves(64));
|
||||
AddCase(DsAppendUsesEncodedGdsSelector);
|
||||
for (u32 wave_size : {32, 64}) {
|
||||
cases.push_back(DsOrderedCountAddressAndExec(wave_size, false));
|
||||
cases.push_back(DsOrderedCountAddressAndExec(wave_size, true));
|
||||
}
|
||||
AddCase(DsAppendConsumeGdsRegionBounds);
|
||||
AddCase(DsGdsSubdwordAndAtomicWrites);
|
||||
AddCase(DsReadWrite2Variants);
|
||||
@@ -41058,6 +41123,14 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--ds-ordered-count-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
for (u32 wave_size : {32, 64}) {
|
||||
RunCase(&vulkan, DsOrderedCountAddressAndExec(wave_size, false));
|
||||
RunCase(&vulkan, DsOrderedCountAddressAndExec(wave_size, true));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--ds-atomics-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
for (const auto &test : MakeCases()) {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "common/file.h"
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
namespace Trophies = Common::Trophies;
|
||||
|
||||
void Check(bool value, const char *text) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "TrophySystemTests: failed: %s\n", text);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
const auto unique =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_path = std::filesystem::temp_directory_path() /
|
||||
("kyty_trophy_test_" + std::to_string(unique));
|
||||
Check(std::filesystem::create_directories(m_path),
|
||||
"create temporary directory");
|
||||
}
|
||||
~TempDirectory() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(m_path, error);
|
||||
}
|
||||
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
|
||||
|
||||
private:
|
||||
std::filesystem::path m_path;
|
||||
};
|
||||
|
||||
void WriteBigEndian(std::vector<char> &bytes, size_t offset, uint64_t value,
|
||||
size_t length) {
|
||||
Check(offset <= bytes.size() && length <= bytes.size() - offset,
|
||||
"write within package");
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
bytes[offset + length - 1 - i] = static_cast<char>(value & 0xff);
|
||||
value >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
void WritePackage(const std::filesystem::path &path,
|
||||
const std::map<std::string, std::string> &files) {
|
||||
size_t offset = 0x40 + files.size() * 0x40;
|
||||
size_t size = offset;
|
||||
for (const auto &[name, contents] : files)
|
||||
size += contents.size();
|
||||
std::vector<char> bytes(size);
|
||||
WriteBigEndian(bytes, 0, 0xb228c60a, 4);
|
||||
WriteBigEndian(bytes, 4, 1, 4);
|
||||
WriteBigEndian(bytes, 8, bytes.size(), 8);
|
||||
WriteBigEndian(bytes, 0x10, files.size(), 4);
|
||||
WriteBigEndian(bytes, 0x14, 0x20, 4);
|
||||
size_t entry = 0x40;
|
||||
for (const auto &[name, contents] : files) {
|
||||
Check(name.size() < 0x20, "fixture file name fits");
|
||||
std::memcpy(bytes.data() + entry, name.data(), name.size());
|
||||
WriteBigEndian(bytes, entry + 0x20, offset, 8);
|
||||
WriteBigEndian(bytes, entry + 0x28, contents.size(), 8);
|
||||
std::memcpy(bytes.data() + offset, contents.data(), contents.size());
|
||||
offset += contents.size();
|
||||
entry += 0x40;
|
||||
}
|
||||
Common::File file;
|
||||
Check(file.Create(path), "create package fixture");
|
||||
uint32_t written = 0;
|
||||
file.Write(bytes.data(), static_cast<uint32_t>(bytes.size()), &written);
|
||||
Check(written == bytes.size(), "write package fixture");
|
||||
}
|
||||
|
||||
Trophies::Package MakePackage(const std::filesystem::path &directory) {
|
||||
const auto path = directory / "trophy00.ucp";
|
||||
WritePackage(
|
||||
path,
|
||||
{{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[
|
||||
{"id":"0","grade":"P"},
|
||||
{"id":"1","grade":"G","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
|
||||
{"id":"2","grade":"S","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100","progressive":false}},
|
||||
{"id":"3","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5","progressive":false}},
|
||||
{"id":"4","grade":"B","unlockCondition":{"udsStatId":"10","comparator":"lt","targetValue":"-1","progressive":false}}
|
||||
]})"},
|
||||
{"tropmeta_en-US.json",
|
||||
R"({"metadata":{"titleMetadata":{"name":"Test game"},"trophyMetadata":[
|
||||
{"id":"1","name":"Find All Coins","detail":"Find all coins in the level."}
|
||||
]}})"}});
|
||||
return Trophies::LoadPackage(path, 1);
|
||||
}
|
||||
|
||||
void TestEventExtraction(const std::filesystem::path &directory) {
|
||||
auto package = MakePackage(directory);
|
||||
Check(package.trophies.size() == 5,
|
||||
"unsupported signed target does not discard trophy metadata");
|
||||
Check(package.trophies.at(1).uds_stat_id == 10 &&
|
||||
package.trophies.at(1).target == 10 &&
|
||||
!package.trophies.at(1).progressive,
|
||||
"non-progressive trophy retains its stat target");
|
||||
Check(!package.trophies.at(4).target,
|
||||
"negative target is not converted to unsigned");
|
||||
Check(Trophies::FindUdsTrophies(package, "FIND_ALL_COINS",
|
||||
{{"counter", uint64_t{100}}})
|
||||
.empty(),
|
||||
"missing mapping never guesses from trophy text");
|
||||
const auto path = directory / "uds00.ucp";
|
||||
WritePackage(path, {{"stats_definition.json", R"({"statDefinitionArray":[
|
||||
{"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"},
|
||||
{"statId":20,"dataType":"uint64","aggregation":"latest"},
|
||||
{"statId":30,"dataType":"uint32","aggregation":"latest"},
|
||||
{"statId":40,"dataType":"uint64","aggregation":"sum"},
|
||||
{"statId":50,"dataType":"uint64","aggregation":"count"},
|
||||
{"statId":60,"dataType":"uint64","aggregation":"max"},
|
||||
{"statId":70,"dataType":"uint64","aggregation":"latest","maxValue":"3"}
|
||||
]})"},
|
||||
{"stats_extraction.json", R"({"statsExtractionRuleArray":[
|
||||
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counterBase","output":{"statId":20}}},
|
||||
{"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}},
|
||||
{"condition":{"eventName":"FILTERED","property":{"path":"$.level","comparator":"==","value":1}},"action":{"input":"$.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"NESTED"},"action":{"input":"$.player.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":40}}},
|
||||
{"condition":{"eventName":"COUNT"},"action":{"output":{"statId":50}}},
|
||||
{"condition":{"eventName":"MAX"},"action":{"input":"$.counter","output":{"statId":60}}},
|
||||
{"condition":{"eventName":"BOUNDED"},"action":{"input":"$.counter","output":{"statId":70}}},
|
||||
{"bad":1}
|
||||
]})"}});
|
||||
package.event_rules = Trophies::LoadUdsRules(path);
|
||||
Check(package.event_rules.size() == 2,
|
||||
"skip unsupported extraction and aggregation");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{9}}, {"counterBase", uint64_t{100}}})
|
||||
.empty(),
|
||||
"do not add independent counters or treat gt as ge");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{100}}}) ==
|
||||
std::vector<int>{1},
|
||||
"each stat reads its configured property");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{0}}, {"counterBase", uint64_t{101}}}) ==
|
||||
std::vector<int>{2},
|
||||
"strict comparison requires greater value");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{101}}}) ==
|
||||
std::vector<int>({1, 2}),
|
||||
"one event can unlock multiple mapped trophies");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "TIME", {{"seconds", uint32_t{5}}}) == std::vector<int>{3},
|
||||
"less-equal condition accepts its boundary");
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{6}}})
|
||||
.empty(),
|
||||
"less-equal condition rejects larger input");
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"counter", uint64_t{0}}})
|
||||
.empty(),
|
||||
"missing property is not zero");
|
||||
package.trophies.at(3).comparison = Trophies::Comparison::Less;
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{5}}})
|
||||
.empty(),
|
||||
"strict less rejects its boundary");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "TIME", {{"seconds", uint32_t{4}}}) == std::vector<int>{3},
|
||||
"strict less accepts lower input");
|
||||
package.trophies.at(3).comparison = Trophies::Comparison::GreaterEqual;
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME",
|
||||
{{"seconds", uint64_t{UINT32_MAX} + 1}})
|
||||
.empty(),
|
||||
"uint64 input cannot update a uint32 stat");
|
||||
Check(
|
||||
Trophies::FindUdsTrophies(package, "COLLECT", {{"counter", int32_t{100}}})
|
||||
.empty(),
|
||||
"signed inputs cannot update unsigned stats");
|
||||
Check(Trophies::FindUdsTrophies(package, "COLLECT",
|
||||
{{"counter", uint32_t{100}}})
|
||||
.empty(),
|
||||
"unsigned input width must match the stat");
|
||||
package.trophies.at(1).comparison = Trophies::Comparison::None;
|
||||
Check(Trophies::FindUdsTrophies(package, "COLLECT",
|
||||
{{"counter", uint64_t{100}}})
|
||||
.empty(),
|
||||
"unknown comparator cannot unlock");
|
||||
WritePackage(path, {{"stats_extraction.json", "{}"}});
|
||||
Check(Trophies::LoadUdsRules(path).empty(),
|
||||
"missing stat definitions cannot infer aggregation");
|
||||
Check(Trophies::LoadUdsRules(directory / "missing.ucp").empty(),
|
||||
"missing UDS package has no rules");
|
||||
}
|
||||
|
||||
void TestProgress(const std::filesystem::path &directory) {
|
||||
const auto package = MakePackage(directory);
|
||||
Trophies::UnlockData unlocks;
|
||||
unlocks.unlocked = {0, 1, 999};
|
||||
const auto progress = Trophies::GetProgress(package, unlocks);
|
||||
Check(progress.total == 5 && progress.earned == 2,
|
||||
"ignore unlock IDs absent from package");
|
||||
Check(progress.earned_grade[1] == 1 && progress.earned_grade[2] == 1 &&
|
||||
progress.Percentage() == 60,
|
||||
"percentage uses 6:2:1 grade points and excludes platinum");
|
||||
const auto group = Trophies::GetProgress(package, unlocks, 1);
|
||||
Check(group.total == 1 && group.earned == 0 && group.Percentage() == 0,
|
||||
"group counts filter trophies");
|
||||
Check(Trophies::GetProgress({}, {}).Percentage() == 0,
|
||||
"empty package progress is zero");
|
||||
}
|
||||
|
||||
void TestUnlockPersistence(const std::filesystem::path &directory) {
|
||||
const auto path = Trophies::UnlocksPath(directory, "PPSA00001", 1000, 0);
|
||||
Check(path == directory / "_Trophies/PPSA00001/trophies_1000_0.json",
|
||||
"store trophies outside save data");
|
||||
Check(Trophies::UnlocksPath(directory, "../bad", 1000, 0).empty(),
|
||||
"reject unsafe title IDs");
|
||||
Check(Trophies::LoadUnlockData(path).unlocked.empty(),
|
||||
"missing unlock file loads as empty");
|
||||
Trophies::UnlockData unlocks;
|
||||
unlocks.unlocked = {4, 42};
|
||||
const auto tick = Trophies::UnixEpochTick + 1791288000123456ULL;
|
||||
unlocks.timestamps.emplace(42, tick);
|
||||
Check(Trophies::SaveUnlockData(path, unlocks), "save timestamped unlocks");
|
||||
auto loaded = Trophies::LoadUnlockData(path);
|
||||
Check(loaded.unlocked == unlocks.unlocked &&
|
||||
loaded.timestamps == unlocks.timestamps,
|
||||
"persist full UTC microsecond timestamp");
|
||||
loaded.unlocked.insert(43);
|
||||
loaded.timestamps.emplace(43, tick + 1000000);
|
||||
Check(Trophies::SaveUnlockData(path, loaded), "replace existing unlock file");
|
||||
Check(Trophies::LoadUnlockData(path).timestamps.at(42) == tick,
|
||||
"retain original unlock time");
|
||||
|
||||
const auto blocked = directory / "blocked.json";
|
||||
std::filesystem::create_directory(blocked);
|
||||
Common::File sentinel;
|
||||
Check(sentinel.Create(blocked / "keep"),
|
||||
"create replacement failure fixture");
|
||||
sentinel.Close();
|
||||
Check(!Trophies::SaveUnlockData(blocked, loaded) &&
|
||||
std::filesystem::exists(blocked / "keep"),
|
||||
"failed replacement leaves destination intact");
|
||||
Check(!std::filesystem::exists(directory / "blocked.json.tmp"),
|
||||
"failed replacement removes temporary file");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TempDirectory directory;
|
||||
TestEventExtraction(directory.Path());
|
||||
TestProgress(directory.Path());
|
||||
TestUnlockPersistence(directory.Path());
|
||||
std::printf("TrophySystemTests: all cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "common/hostException.h"
|
||||
#include "loader/guestInstructionPatcher.h"
|
||||
#include "loader/x64InstructionEmulator.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
@@ -82,6 +84,53 @@ constexpr uint8_t kUd2[] = {
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// VRSQRTPS xmm1, xmm0 as the AMD CPU patcher leaves it when it cannot relocate the instruction: a
|
||||
// reserved VEX.vvvv bit is cleared (c5 f8 -> c5 f0), which raises #UD on every host. Rosetta aborted
|
||||
// the guest on exactly this encoding until the emulator handled it there too.
|
||||
constexpr uint8_t kTrappedVrsqrtpsXmm1Xmm0[] = {
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf0, 0x52, 0xc8, // vrsqrtps xmm1, xmm0 with the vvvv trap mark
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// The same sequence with the ordinary VEX.128 VRSQRTPS encoding (vvvv = 1111), as it appears in guest
|
||||
// code before the patcher marks it as a trap.
|
||||
constexpr uint8_t kPlainVrsqrtpsXmm1Xmm0[] = {
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
// The same again behind a never taken indirect branch. A function with an indirect branch does not
|
||||
// let the patcher relocate neighbours, and the 4-byte VRSQRTPS cannot hold a jump on its own, so it
|
||||
// is left in place with the trap mark.
|
||||
constexpr uint8_t kPlainVrsqrtpsIndirectBranch[] = {
|
||||
0x31, 0xc0, // xor %eax, %eax
|
||||
0x85, 0xc0, // test %eax, %eax
|
||||
0x74, 0x02, // jz over
|
||||
0xff, 0xe0, // jmp *%rax (never taken)
|
||||
0x0f, 0x10, 0x06, // movups (%rsi), %xmm0
|
||||
0xc5, 0xfc, 0x10, 0x0e, // vmovups ymm1, [rsi]: dirty the upper half of ymm1
|
||||
0xc5, 0xf8, 0x52, 0xc8, // vrsqrtps xmm1, xmm0
|
||||
0x0f, 0x11, 0x0f, // movups %xmm1, (%rdi)
|
||||
0xc4, 0xe3, 0x7d, 0x19, 0x4f, 0x10, 0x01, // vextractf128 [rdi + 16], ymm1, 1
|
||||
0xc5, 0xf8, 0x77, // vzeroupper
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
constexpr uint8_t kMwaitx[] = {
|
||||
0x0f, 0x01, 0xfb, // mwaitx
|
||||
0xc3, // ret
|
||||
};
|
||||
|
||||
constexpr size_t kPageSize = 4096;
|
||||
|
||||
std::atomic<int> g_illegal_hits {0};
|
||||
@@ -233,6 +282,13 @@ void TestInsertqRegister() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestMwaitxEmulated() {
|
||||
auto fn = MapCode<VoidBlobFn>(kMwaitx);
|
||||
const int before = IllegalHits();
|
||||
fn();
|
||||
Check(IllegalHits() > before, "host raises SIGILL for MWAITX");
|
||||
}
|
||||
|
||||
void TestUnknownInstructionRefused() {
|
||||
g_refused.store(false, std::memory_order_relaxed);
|
||||
g_refuse_skip.store(2, std::memory_order_relaxed);
|
||||
@@ -244,6 +300,170 @@ void TestUnknownInstructionRefused() {
|
||||
g_refuse_skip.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// The loader rewrites SSE4a instructions into native trampolines when the patcher is enabled. The
|
||||
// trampoline area lives right behind the code page, like it does behind a mapped guest module.
|
||||
struct PatchedCode {
|
||||
void *code = nullptr;
|
||||
Loader::GuestInstructionPatchResult result {};
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
PatchedCode MapPatchedCode(const uint8_t (&blob)[N], bool emulate_amd) {
|
||||
void *area = ::mmap(nullptr, 2 * kPageSize, PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
Check(area != MAP_FAILED, "map RWX code and trampoline area");
|
||||
std::memcpy(area, blob, N);
|
||||
auto *trampoline = static_cast<uint8_t *>(area) + kPageSize;
|
||||
Loader::RegisterGuestInstructionPatchModule(area, kPageSize, trampoline, kPageSize);
|
||||
const std::array<uintptr_t, 1> function_starts {reinterpret_cast<uintptr_t>(area)};
|
||||
// A host without SSE4a, RDPID and CLWB, which is what Rosetta presents.
|
||||
const Loader::GuestInstructionHostFeatures host {false, false, false};
|
||||
PatchedCode patched;
|
||||
patched.code = area;
|
||||
patched.result = Loader::PatchGuestInstructions(reinterpret_cast<uintptr_t>(area), N, function_starts,
|
||||
false, emulate_amd, host);
|
||||
Loader::UnregisterGuestInstructionPatchModule(area);
|
||||
return patched;
|
||||
}
|
||||
|
||||
using PatchCounts = Loader::InstructionPatchCounts Loader::GuestInstructionPatchResult::*;
|
||||
|
||||
// Runs the blob through the SIGILL emulator (reference) and through the patched copy, which must
|
||||
// produce the same bytes. EXTRQ becomes a native trampoline without a single SIGILL. INSERTQ is
|
||||
// marked as a trap by the patcher and keeps going through the signal emulator.
|
||||
template <typename Fn, size_t N, typename Invoke>
|
||||
void CheckPatchedMatchesEmulator(const uint8_t (&blob)[N], PatchCounts counts, bool native,
|
||||
Invoke invoke, const char *text) {
|
||||
uint64_t reference[2] {};
|
||||
const int before_reference = IllegalHits();
|
||||
invoke(MapCode<Fn>(blob), reference);
|
||||
Check(IllegalHits() > before_reference, text);
|
||||
|
||||
const auto unpatched = MapPatchedCode(blob, false);
|
||||
Check((unpatched.result.*counts).found == 0, "patcher stays off without emulate_amd");
|
||||
uint64_t unpatched_out[2] {};
|
||||
const int before_off = IllegalHits();
|
||||
invoke(reinterpret_cast<Fn>(unpatched.code), unpatched_out);
|
||||
Check(IllegalHits() > before_off, "unpatched copy still raises SIGILL");
|
||||
|
||||
const auto patched = MapPatchedCode(blob, true);
|
||||
Check((patched.result.*counts).found == 1, text);
|
||||
Check((patched.result.*counts).native == (native ? 1u : 0u), "native trampoline count");
|
||||
Check((patched.result.*counts).trapped == (native ? 0u : 1u), "trapped (signal path) count");
|
||||
uint64_t out[2] {};
|
||||
const int before = IllegalHits();
|
||||
invoke(reinterpret_cast<Fn>(patched.code), out);
|
||||
if (native) {
|
||||
Check(IllegalHits() == before, "patched code raises no SIGILL");
|
||||
} else {
|
||||
Check(IllegalHits() > before, "trapped instruction still goes through the signal emulator");
|
||||
}
|
||||
Check(out[0] == reference[0] && out[1] == reference[1], "patched result equals emulated result");
|
||||
}
|
||||
|
||||
void TestPatchedExtrqImmediate() {
|
||||
CheckPatchedMatchesEmulator<BlobFn>(
|
||||
kExtrqXmm2Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
|
||||
[](BlobFn fn, uint64_t *out) { fn(out, 0.0, 0.0, std::bit_cast<double>(0xaabbccddeeff1122ull)); },
|
||||
"EXTRQ immediate found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedExtrqRexHigh() {
|
||||
CheckPatchedMatchesEmulator<LoadBlobFn>(
|
||||
kExtrqXmm10Len40Index0, &Loader::GuestInstructionPatchResult::extrq, true,
|
||||
[](LoadBlobFn fn, uint64_t *out) {
|
||||
const uint64_t in[2] {0xaabbccddeeff1122ull, 0x5555555555555555ull};
|
||||
fn(out, in);
|
||||
},
|
||||
"REX EXTRQ found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqImmediate() {
|
||||
CheckPatchedMatchesEmulator<BlobFn>(
|
||||
kInsertqXmm2Xmm0Len32Index0, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](BlobFn fn, uint64_t *out) {
|
||||
fn(out, std::bit_cast<double>(0x1122334455667788ull), 0.0,
|
||||
std::bit_cast<double>(0x0123456789abcdefull));
|
||||
},
|
||||
"INSERTQ immediate found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqIndexedRexHigh() {
|
||||
CheckPatchedMatchesEmulator<PairBlobFn>(
|
||||
kInsertqXmm10Xmm9Len16Index8, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](PairBlobFn fn, uint64_t *out) {
|
||||
const uint64_t src[2] {0x8877665544332211ull, 0xdeadbeefcafebabeull};
|
||||
const uint64_t dest[2] {0xaaaaaaaaaaaaaaaaull, 0xbbbbbbbbbbbbbbbbull};
|
||||
fn(out, src, dest);
|
||||
},
|
||||
"indexed REX INSERTQ found by the patcher");
|
||||
}
|
||||
|
||||
void TestPatchedInsertqRegister() {
|
||||
CheckPatchedMatchesEmulator<PairBlobFn>(
|
||||
kInsertqXmm10Xmm9, &Loader::GuestInstructionPatchResult::insertq, false,
|
||||
[](PairBlobFn fn, uint64_t *out) {
|
||||
const uint64_t src[2] {0x0123456789abcdefull, 0xdeadbeef0000c4c8ull};
|
||||
const uint64_t dest[2] {0x8877665544332211ull, 0xfedcba9876543210ull};
|
||||
fn(out, src, dest);
|
||||
},
|
||||
"register INSERTQ found by the patcher");
|
||||
}
|
||||
|
||||
// The ordinary VEX.128 VRSQRTPS must be found by the patcher and give the reference result, either
|
||||
// through a native trampoline (no SIGILL) or, when it cannot be relocated, marked as a trap (the
|
||||
// vvvv bit is cleared in place) and emulated from the signal handler.
|
||||
template <size_t N>
|
||||
void CheckPatchedReciprocalSquareRoot(const uint8_t (&blob)[N], bool native) {
|
||||
const uint32_t in_bits[8] {std::bit_cast<uint32_t>(4.0f), 0x00000000u, 0xbf800000u, 0x7f800000u,
|
||||
0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u};
|
||||
const auto Run = [&](LoadBlobFn fn, uint32_t (&out_bits)[8]) {
|
||||
fn(reinterpret_cast<uint64_t *>(out_bits), reinterpret_cast<const uint64_t *>(in_bits));
|
||||
};
|
||||
uint32_t reference[8] {};
|
||||
const int before_reference = IllegalHits();
|
||||
Run(MapCode<LoadBlobFn>(kTrappedVrsqrtpsXmm1Xmm0), reference);
|
||||
Check(IllegalHits() > before_reference, "host raises SIGILL for the pre-marked VRSQRTPS");
|
||||
|
||||
const auto unpatched = MapPatchedCode(blob, false);
|
||||
Check(unpatched.result.reciprocal_sqrt.found == 0, "patcher leaves VRSQRTPS alone without emulate_amd");
|
||||
|
||||
const auto patched = MapPatchedCode(blob, true);
|
||||
const auto &counts = patched.result.reciprocal_sqrt;
|
||||
Check(counts.found == 1, "patcher finds the ordinary VEX.128 VRSQRTPS");
|
||||
Check(counts.native == (native ? 1u : 0u) && counts.trapped == (native ? 0u : 1u),
|
||||
"VRSQRTPS native trampoline / trap count");
|
||||
const auto *code = static_cast<const uint8_t *>(patched.code);
|
||||
size_t site = 0;
|
||||
while (site + 2 < N && !(blob[site] == 0xc5 && blob[site + 1] == 0xf8 && blob[site + 2] == 0x52)) {
|
||||
++site;
|
||||
}
|
||||
Check(site + 2 < N, "locate VRSQRTPS in the test blob");
|
||||
if (!native) {
|
||||
Check(code[site] == 0xc5 && code[site + 1] == 0xf0 && code[site + 2] == 0x52,
|
||||
"the trap mark clears a reserved VEX.vvvv bit (c5 f8 -> c5 f0)");
|
||||
} else {
|
||||
Check(code[site] != 0xc5 || code[site + 1] != 0xf8, "native trampoline replaced the VRSQRTPS");
|
||||
}
|
||||
uint32_t out[8] {};
|
||||
const int before = IllegalHits();
|
||||
Run(reinterpret_cast<LoadBlobFn>(patched.code), out);
|
||||
if (native) {
|
||||
Check(IllegalHits() == before, "natively patched VRSQRTPS raises no SIGILL");
|
||||
} else {
|
||||
Check(IllegalHits() > before, "marked VRSQRTPS goes through the signal emulator");
|
||||
}
|
||||
Check(std::memcmp(out, reference, sizeof(out)) == 0, "patched VRSQRTPS result equals the emulated reference");
|
||||
Check(std::bit_cast<float>(out[0]) == 0.5f && out[1] == 0x7f800000u && out[2] == 0xffc00000u && out[3] == 0,
|
||||
"patched VRSQRTPS produces the exact reciprocal square roots");
|
||||
Check(out[4] == 0 && out[5] == 0 && out[6] == 0 && out[7] == 0, "patched VEX.128 form zeroes the upper half");
|
||||
}
|
||||
|
||||
void TestPatchedReciprocalSquareRoot() {
|
||||
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsXmm1Xmm0, true);
|
||||
CheckPatchedReciprocalSquareRoot(kPlainVrsqrtpsIndirectBranch, false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -255,7 +475,14 @@ int main() {
|
||||
TestRexHighRegisterExtrq();
|
||||
TestRexHighRegisterInsertqWithIndex();
|
||||
TestInsertqRegister();
|
||||
TestMwaitxEmulated();
|
||||
TestUnknownInstructionRefused();
|
||||
TestPatchedExtrqImmediate();
|
||||
TestPatchedExtrqRexHigh();
|
||||
TestPatchedInsertqImmediate();
|
||||
TestPatchedInsertqIndexedRexHigh();
|
||||
TestPatchedInsertqRegister();
|
||||
TestPatchedReciprocalSquareRoot();
|
||||
std::printf("macosSse4aTests: all passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user