Compare commits

...
Author SHA1 Message Date
nmzik 96eb681b90 shader: add V_MIN3_U16 opcode 2026-10-07 16:22:30 +04:00
nmzik 26cce9909a shader: add BUFFER_LOAD_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 5af370c257 shader: add BUFFER_STORE_FORMAT_D16_X opcode 2026-10-07 16:22:30 +04:00
nmzik 67d7db685a agc: new abi 2026-10-07 16:22:30 +04:00
nmzik bb98f26433 shader: add missing I64/U64 compare variants 2026-10-07 05:03:09 +04:00
cc44a43ebc systemservice: add an option to report the notice screen as skipped (#1127)
* systemservice: add an option to report the notice screen as skipped

sceSystemServiceGetNoticeScreenSkipFlag always returned false. Return the
new skip_notice_screen option instead. Games that check this flag skip
their boot logo movies when it is set; Marvel's Wolverine, for example,
then does not queue movies/BootSequence/BootSequence.movie.

The option defaults to off, so behaviour is unchanged unless it is
enabled with --skip-notice-screen true or the launcher's "Skip boot
logos" checkbox. The launcher stores it with the other per-game settings,
so it is included in per-game config import and export.

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

* Shorten skip notice screen wording

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 04:21:27 +04:00
nmzik 2a18e66a4c gpu: validate depth copy state when drawing instead of decoding 2026-10-07 04:14:49 +04:00
Henrique Reis Nevesandnmzik e7cfe5a2f7 libkernel: implement sceKernelAioWaitRequests (#1109)
kernel: implement batch AIO completion waits

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 04:13:06 +04:00
nmzikandnetwarex 7415febd93 SDL title fix
Co-authored-by: netwarex <mail@pety.me>
2026-10-07 03:15:16 +04:00
NeySlimandnmzik 7492cb9577 shader: emulate DS_ORDERED_COUNT as a GDS atomic (#1117)
* feat(shader): emulate DS_ORDERED_COUNT as a GDS atomic

* fix(shader): issue one ordered-count atomic per wave

* shader: correct ordered-count addressing and wave results

Use the M0 base and pixel packer offset for the GDS counter. Broadcast the
previous value to every lane and preserve the counter when EXEC is empty.

Add GPU regressions for add/exchange with full, partial, and empty EXEC in
wave32 and wave64. The targeted DS and buffer atomic suite passes 63 cases.

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 01:45:37 +04:00
NeySlim 60e53a39cc decoder: accept clamp on V_MIN3/MAX3/MED3_F16 (#1116)
fix(decoder): accept clamp on V_MIN3/MAX3/MED3_F16
2026-10-07 01:24:18 +04:00
1b21161aac kernel: fix the macOS build of host thread naming (#1122)
* kernel: apply clang-format to pthread.cpp

Output of the repository's pre-commit clang-format hook (v22.1.3) on
this file; no code change.

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

* kernel: fix the macOS build of host thread naming

199d0a9d (#1106) names guest threads on the host with the two-argument
glibc pthread_setname_np(thread, name). KYTY_PLATFORM is
KYTY_PLATFORM_LINUX on macOS too, so the macOS job now fails with "no
matching function for call to 'pthread_setname_np'": Apple's version
takes only the name and renames the calling thread. This code already
runs on the new thread itself (it passes pthread_self()), so use the
one-argument form on Apple with Apple's 63-character limit and keep the
Linux call unchanged.

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

* Remove formatting

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-07 01:18:48 +04:00
NeySlim 1195af2510 decoder: add V_CMPX_U_F32 (#1120)
feat(decoder): add V_CMPX_U_F32
2026-10-07 01:16:23 +04:00
nmzik 9a5658c038 launcher: add per-game config import and export 2026-10-07 01:02:33 +04:00
Oberd 199d0a9d92 kernel: name guest threads on the host (#1106)
PthreadCreate carries a guest thread name, but RunThread never handed it to
the host, so every guest thread showed up as the same generic host thread
name in core dumps, perf samples and htop.

Call pthread_setname_np() on Linux so host-side diagnostics can attribute
work to the guest thread that caused it.
2026-10-06 23:43:36 +04:00
nmzik e679486ed3 net: translate native UDP socket options and microsecond timeouts 2026-10-06 23:41:06 +04:00
Henrique Reis NevesandClaude Opus 5.5 6ef406542c tests: make kyty_tests build and run every test without LTO (#1110)
avplayer_file_tests compiles libs/avPlayer.cpp into the test, and its
PRINT_NAME() calls Loader::Timer::GetTimeMs(). Add loader/timer.cpp to the
target, like the other tests that compile HLE library sources. CI links the
test with LTO, which drops the unused reference, so the failure only shows
in non-LTO builds such as the README's no-Qt instructions.

Clang with libstdc++ rejects optional<IndirectDescriptor>::emplace() with
no arguments in ShaderRecompilerComputeTests.cpp. Assign the descriptor the
same way the neighbouring test does.

trophy_system is registered with ctest but kyty_tests did not build its
executable, so ctest reported it as Not Run.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:56:48 +04:00
Bartłomiej Wołyńczykandnmzik 35c6e8fff6 loader: enable the AMD CPU instruction patcher on macOS (#1014)
* loader: enable AMD CPU instruction patching on macOS

The guest instruction patcher was compiled out on macOS and runtimeLinker forced emulate_amd off,
so under Rosetta every SSE4a EXTRQ/INSERTQ took the SIGILL path (about 18 us each). Build the
patcher on all hosts and let --amd-cpu enable it on macOS like on Windows and Linux. The host
feature probe already reports no SSE4a under Rosetta, so EXTRQ becomes a native trampoline and the
remaining fallbacks keep using the x64 instruction emulator through the host exception handler.

* tests: macos_sse4a covers the guest instruction patcher on macOS

Patch EXTRQ/INSERTQ blobs in an RWX area with a trampoline page behind it, with the host features of
Rosetta (no SSE4a/RDPID/CLWB), and compare against the SIGILL emulator: EXTRQ must become a native
trampoline without SIGILL, INSERTQ stays a trap handled by the emulator, and nothing is touched
without emulate_amd.

* loader: emulate the patcher's VRSQRTPS trap and MWAITX on macOS

The AMD CPU patcher marks VRSQRTPS it cannot relocate by clearing a reserved
VEX.vvvv bit, which raises #UD on every host including Rosetta. The macOS build
left TryEmulateReciprocalSquareRoot and TryEmulateMonitorxMwaitx out, so the
guest died with an unhandled SIGILL at the first such site.

Both are plain SSE2/libc, so enable them on macOS. The Darwin context also
gains ClearUpperYmm so the VEX.128 result zeroes YMM[255:128] when the thread
has the AVX context. macos_sse4a covers the trapped VRSQRTPS and MWAITX.

* tests: macos_sse4a runs a plain VEX.128 VRSQRTPS through the patcher

The existing case started from an instruction that was already marked as a trap. The
new cases feed the ordinary encoding to PatchGuestInstructions and compare the result
with the emulated reference, both through a native trampoline and, when the instruction
cannot be relocated, through the trap mark and the signal emulator.

* loader: compact macOS instruction patcher coverage

Share the emulator dispatch chain while keeping SHA emulation limited to non-Apple hosts and preserving handler order. Remove a duplicate VRSQRTPS signal test already covered by both native and trapped patcher cases, plus blank lines left by removed platform guards.

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-06 21:55:11 +04:00
thenotsoquietninja44-collab 4852721005 net: export connect, getsockopt, sendto, and recvfrom 2026-10-06 21:32:46 +04:00
fb7ff7ec03 Map UDS events to trophies from dumped metadata; trophy viewer/overview improvements (#1088)
* Map UDS events to trophies via dumped metadata and redesign trophy UI

Correlate each UDS event to its stat ID (uds00.ucp) and trophy target (trophy00.ucp) instead of a hard-coded event, store trophy JSON with unlock date/time outside _SaveData, and add a PS5-style toast, viewer and overview.

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

* Fix trophy rule evaluation and share progress accounting

* Match trophy and reload toolbar styling

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-06 17:45:26 +04:00
nmzik 7d2422abbe add remote cheats launcher 2026-10-06 15:44:26 +04:00
nmzik b929bbe3e0 shader: limit broad image tables to compatible views 2026-10-06 14:59:05 +04:00
nmzik ae1e92621d shader: sample mixed bindless images through contiguous native arrays 2026-10-06 14:53:17 +04:00
nmzik 5a491abbda shader: remove redundant descriptor sorting and stale null check 2026-10-06 14:53:17 +04:00
Claxten afbb322b32 kernel: deliver persistent user events once per wait 2026-10-06 14:53:17 +04:00
Claxten 5601861d67 shader: implement RDNA2 scalar 16-bit sign extension 2026-10-06 14:53:17 +04:00
Claxten cd06601022 shader: replace CPU selector chains with direct image-table lookup 2026-10-06 14:53:17 +04:00
nmzik c23de62184 shader: resolve nested descriptor selector tables with exact scalar offsets 2026-10-06 14:53:17 +04:00
Claxten 9caec9f2c8 shader: preserve RDNA2 DLC coherence for flat address loads 2026-10-06 14:53:17 +04:00
nmzik cdb64bfd4f graphics: share indirect draw handling and honor indexed sources 2026-10-06 00:35:02 +04:00
nmzikandJoão Lobo e5dcfdb87b graphics: bound unbounded buffer reads and writes (#1066)
Co-authored-by: João Lobo <joaodlobo@gmail.com>
2026-10-06 00:35:02 +04:00
nmzikandTheCruZ 984010227f graphics: make depth bounds dynamic to reuse pipelines
Co-authored-by: TheCruZ <thecruzwtf@gmail.com>
2026-10-06 00:35:02 +04:00
TheCruZ 57c97ebd86 Decode 16-bit color clears (TheCruZ/KytyPS5-GTA@b8dcd826) 2026-10-06 00:35:02 +04:00
Rohit Mahtoandnmzik dc3cd2d2c9 libNet: honour the option mask in sceHttpUriBuild (#1069)
* libNet: honour the option mask in sceHttpUriBuild

sceHttpUriBuild's `option` argument (SCE_HTTP_URI_BUILD_WITH_*) selects which
components of the SceHttpUriElement end up in the output. HttpUriBuild logged
the value and ignored it, always returning the full URL.

Black Myth: Wukong calls it with SCHEME alone and requires "https://" (or
"http://") back before it starts an HTTP request. The full URL fails that
check, so the request starter returns false. The caller's "run on game thread
and wait" helper then returns early while its task stays queued, still holding
by-reference captures of a stack frame that is already gone. About 70 seconds
into boot the game thread runs that task against the dead stack and faults
(null read at eboot+0x59662f). With the mask honoured the game gets past that
point and reaches its title cinematic and menu.

Bits: SCHEME 0x01, HOSTNAME 0x02, PORT 0x04, PATH 0x08, USERNAME 0x10,
PASSWORD 0x20, QUERY 0x40, FRAGMENT 0x80. A mask with none of the eight bits
set keeps the previous "everything" behaviour for callers that never pass one.
A scheme-only build returns "https://", the authority marker belonging to the
scheme chunk.

Adds TestBuildHonoursOption to http_uri_parse covering scheme-only, hostname,
port, path, and the 0xff / 0 cases.

* libNet: simplify URI build code and tests

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-05 21:31:45 +04:00
86 changed files with 4720 additions and 1372 deletions

No files matched your search

+14 -2
View File
@@ -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

+4
View File
@@ -164,6 +164,10 @@ bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
bool SkipNoticeScreen() {
return g_config->skip_notice_screen;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled() {
return g_config->red_zone_protection_enabled;
+2
View File
@@ -76,6 +76,7 @@ struct ConfigOptions {
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool playgo_hack_enabled = false;
bool skip_notice_screen = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
#endif
@@ -126,6 +127,7 @@ bool ReadbackLinearImagesEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
bool PlayGoHackEnabled();
bool SkipNoticeScreen();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
#endif
+215 -26
View File
@@ -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
View File
@@ -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
@@ -184,7 +184,6 @@ static HW::RenderControl DecodeRenderControl(uint32_t value) {
KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_STENCIL_TO_COLOR) != 0;
r.copy_centroid = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_CENTROID) != 0;
r.copy_sample = KYTY_PM4_GET(value, DB_RENDER_CONTROL, COPY_SAMPLE);
EXIT_NOT_IMPLEMENTED(r.copy_depth_to_color || r.copy_stencil_to_color);
return r;
}
+3 -52
View File
@@ -876,64 +876,15 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
const auto* args_addr =
reinterpret_cast<const void*>(m_draw_indirect_args_base_addr + data_offset);
if (!indexed) {
DrawIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
m_num_instances = args.instance_count;
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
.instance_count = args.instance_count,
.first_vertex = args.start_vertex_location,
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
return;
}
DrawIndexedIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
uint64_t index_size = 0;
switch (m_index_type_and_size) {
case 0: index_size = 2; break;
case 1: index_size = 4; break;
case 2: index_size = 1; break;
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
}
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args.start_index_location) * index_size);
const uint32_t index_count =
(m_index_buffer_size != 0 ? std::min(args.index_count_per_instance, m_index_buffer_size)
: args.index_count_per_instance);
if (GraphicsRunDebugDumpEnabled() && index_count != args.index_count_per_instance) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
LOGF("\t DrawIndexIndirect: clamped index_count from %" PRIu32 " to %" PRIu32
" using INDEX_BUFFER_SIZE\n",
args.index_count_per_instance, index_count);
}
}
m_num_instances = args.instance_count;
DrawIndex({.index_count = index_count,
.index_addr = index_addr,
.instance_count = args.instance_count,
.base_vertex = static_cast<int32_t>(args.base_vertex_location),
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
}
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr,
uint32_t stride_in_bytes, uint32_t draw_initiator,
bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
uint32_t draw_count = max_count_or_count;
@@ -315,8 +315,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
r.depth_bounds_test_enable = dc.depth_bounds_enable;
r.depth_min_bounds = hw.GetDepthBoundsMin();
r.depth_max_bounds = hw.GetDepthBoundsMax();
r.stencil_clear_enable =
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
@@ -29,8 +29,6 @@ struct RenderDepthInfo {
bool depth_write_enable = false;
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
bool stencil_clear_enable = false;
uint8_t stencil_clear_value = 0;
bool stencil_test_enable = false;
@@ -390,7 +390,28 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
const auto mantissa = value & 0x3ffu;
if (exponent == 0) {
return std::copysign(static_cast<float>(mantissa) * 0x1p-24f,
std::bit_cast<float>(sign));
}
return std::bit_cast<float>(sign | ((exponent == 31 ? 255u : exponent + 112u) << 23u) |
(mantissa << 13u));
};
switch (format) {
case vk::Format::eR16G16Sfloat:
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
break;
// A single-plane float target carries its clear as raw float bits, the same encoding the
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
// contents.
@@ -443,6 +464,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -807,30 +807,52 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
}
}
void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
auto& cache = m_context.GetBufferCache();
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
return;
}
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
const auto requested_size = descriptor.GetSize();
if (address == 0 || requested_size == 0) {
prepared.buffer_sources.push_back({});
auto& cache = m_context.GetBufferCache();
for (auto* stage: stages) {
auto& prepared = *stage;
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
continue;
}
const auto size = Libs::LibKernel::Memory::ClampRangeSize(address, requested_size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
auto size = descriptor.GetSize();
if (address == 0 || size == 0) {
prepared.buffer_sources.push_back({});
continue;
}
if (descriptor.NumRecords() == UINT32_MAX) {
if (program.info.buffers[resource].written) {
// Sentinel write ranges end before tables captured by any bound stage.
for (const auto* reader: stages) {
for (const auto [read_address, read_size]: reader->runtime->resources->specialization_reads) {
if (read_size == 0) continue;
if (read_address > address) {
size = std::min(size, read_address - address);
} else if (address - read_address < read_size) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
}
} else {
const auto& graphics = m_context.GetGraphics();
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
(graphics.StorageMinAlignment() - 1);
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
}
}
}
@@ -919,8 +941,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
std::span<RenderColorInfo> colors) {
bool uses_dma = false;
FindBuffers(stages);
for (auto* stage: stages) {
FindBuffers(*stage);
uses_dma |= stage->runtime->program->info.uses_dma;
}
if (uses_dma) {
@@ -831,9 +831,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
}
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
static_params.depth_min_bounds = depth.depth_min_bounds;
static_params.depth_max_bounds = depth.depth_max_bounds;
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
@@ -38,9 +38,6 @@ struct PipelineStaticParameters {
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
@@ -64,7 +61,7 @@ struct PipelineStaticParameters {
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 125);
static_assert(sizeof(PipelineStaticParameters) == 116);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
@@ -471,14 +471,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
depth_stencil_info.depthBoundsTestEnable =
#if defined(__APPLE__)
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
#else
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
#endif
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
std::vector<vk::DynamicState> dynamic_states {
vk::DynamicState::eViewportWithCount,
@@ -497,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::DynamicState::eBlendConstants,
};
#if !defined(__APPLE__)
dynamic_states.push_back(vk::DynamicState::eDepthBoundsTestEnable);
dynamic_states.push_back(vk::DynamicState::eDepthBounds);
if (rendering.color_count != 0) {
dynamic_states.push_back(vk::DynamicState::eColorWriteEnableEXT);
}
+1 -1
View File
@@ -161,7 +161,7 @@ public:
uint32_t mode);
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
void FindBuffers(PreparedBindings& bindings);
void FindBuffers(std::span<PreparedBindings* const> stages);
void RebindBuffers(PreparedBindings& bindings);
void RebindImages(PreparedBindings& bindings);
void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point,
@@ -342,7 +342,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
std::copy(std::begin(input_info.dispatch_threads_num), std::end(input_info.dispatch_threads_num),
bindings.shader_data.begin() + program.bindings.dispatch_thread_dword);
}
FindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
if (program.info.uses_dma) {
m_context.PrepareBda();
}
@@ -351,7 +352,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
RebindBuffers(bindings);
auto vk_buffer = buffer.Handle();
PreparedBindings* descriptor_stage = &bindings;
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
std::span {&descriptor_stage, 1u});
bool has_storage_writes = bindings.shared_memory.buffer != nullptr ||
@@ -457,7 +457,8 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& bindings = m_compute_bindings;
PrepareBindings(input_info.stage, bindings);
FindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
const auto& program = *input_info.stage.program;
if (program.info.uses_dma) {
m_context.PrepareBda();
@@ -469,7 +470,6 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
args_addr, sizeof(vk::DispatchIndirectCommand), false);
EXIT_IF(args_buffer == nullptr || (args_offset & 3u) != 0);
RebindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
std::span {&descriptor_stage, 1u});
const auto vk_buffer = buffer.Handle();
@@ -379,6 +379,12 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
#if !defined(__APPLE__)
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
if (depth.depth_bounds_test_enable) {
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
}
#endif
const auto& mode = ctx.GetModeControl();
const auto& poly_offset = ctx.GetPolyOffset();
+49 -37
View File
@@ -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()) {
+1 -1
View File
@@ -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");
+20 -27
View File
@@ -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);
@@ -205,7 +205,8 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
if (memory_index >= program.memory_info.size()) {
Fail(program, "address operation has invalid memory metadata");
}
const auto kind = program.memory_info[memory_index].kind;
const auto& memory = program.memory_info[memory_index];
const auto kind = memory.kind;
if (kind == IR::ResourceKind::Scratch || kind == IR::ResourceKind::FlatLocal) {
if (program.scratch_dwords == 0) {
Fail(program, "scratch operation has no per-thread storage");
@@ -217,6 +218,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
}
} else if (address_access == IR::AddressAccess::Write) {
Fail(program, "writable FLAT/GLOBAL addresses require GPU ownership tracking");
} else {
// Descriptor stores can alias coherent physical-address loads.
requirements.coherent_buffers |= memory.coherent;
}
}
if (IR::BufferAccessOf(inst.GetOpcode()) != IR::BufferAccess::None) {
@@ -849,74 +849,76 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 || image.indirect_search_iterations == 0u ||
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
return;
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
const auto kind = *IR::DescriptorBindingForImage(image);
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
uint32_t first_child = 0;
bool homogeneous = true;
struct SampleRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<SampleRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
if (candidate.dimension != image.dimension || candidate.cube != image.cube ||
IR::DescriptorBindingForImage(candidate) != kind ||
candidate.mip_count != 1u) {
homogeneous = false;
break;
const auto kind = *IR::DescriptorBindingForImage(candidate);
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || (next_slot < binding.resources.size() &&
binding.resources[next_slot] == resource))) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = ResourceForDescriptor(state, kind, resource) - ordinal;
++run.count;
continue;
}
}
if (ordinal == 1u) first_child = ResourceForDescriptor(state, kind, resource);
const auto child_slot = first_child + ordinal - 1u;
if (ordinal > 1u && (child_slot >= binding.resources.size() ||
binding.resources[child_slot] != resource)) {
homogeneous = false;
break;
runs.push_back({ordinal, 1u, resource,
ResourceForDescriptor(state, kind, resource) - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return EmitSample(run.resource);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto kind = *IR::DescriptorBindingForImage(image);
const auto root_slot = ResourceForDescriptor(state, kind, mem.resource);
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return EmitSample(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
if (homogeneous) {
const auto child_index = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, first_child - 1u));
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto array_index = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), array_index, is_root,
ConstantU32(state, ResourceForDescriptor(state, kind, mem.resource)),
child_index);
const auto sample = EmitSample(mem.resource, array_index);
const auto result = dref ? sample : UnpackImageTexel(ctx, mem, sample);
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
return;
}
const auto default_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
std::vector<uint32_t> labels(image.indirect_resources.size() - 1u);
std::vector<uint32_t> switch_words {spv::OpSwitch, selected, default_label};
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
labels[candidate - 1u] = state.builder.AllocateId();
switch_words.push_back(candidate);
switch_words.push_back(labels[candidate - 1u]);
}
state.builder.AddFunction(spv::OpSelectionMerge, merge_label,
spv::SelectionControlMaskNone);
state.builder.AddFunction(switch_words);
std::vector<uint32_t> phi_words {spv::OpPhi, result_type, state.builder.AllocateId()};
EmitLabel(state, default_label);
phi_words.push_back(EmitSample(image.indirect_resources[0]));
phi_words.push_back(default_label);
state.builder.AddFunction(spv::OpBranch, merge_label);
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
EmitLabel(state, labels[candidate - 1u]);
phi_words.push_back(EmitSample(image.indirect_resources[candidate]));
phi_words.push_back(labels[candidate - 1u]);
state.builder.AddFunction(spv::OpBranch, merge_label);
}
EmitLabel(state, merge_label);
state.builder.AddFunction(phi_words);
auto result = phi_words[2];
auto result = runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
if (!dref) {
result = UnpackImageTexel(ctx, mem, result);
}
@@ -180,29 +180,46 @@ void RecordBdaFault(EmitterState& state, uint32_t page) {
Binary(state, spv::OpBitwiseOr, TypeU32(state), value, bit));
}
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address, bool coherent = false) {
auto& state = ctx.state;
const auto bda = GetBdaPointer(state, address);
const auto present =
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantU64(state, 0));
return EmitValueOrZeroIfCondition(state, present, [&]() {
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
bda);
auto pointer = state.builder.AllocateId();
if (coherent) {
const auto block = state.builder.DecoratedType(
spv::OpTypeStruct, {{spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}},
{spv::OpMemberDecorate, {0, spv::DecorationCoherent}},
{spv::OpDecorate, {spv::DecorationBlock}}}, TypeU32(state));
state.builder.AddFunction(
spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, block),
pointer, bda);
const auto base = pointer;
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypePhysicalU32Pointer(state), pointer,
base, ConstantU32(state, 0));
} else {
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
bda);
}
const auto value = state.builder.AllocateId();
constexpr uint32_t alignment = sizeof(uint32_t);
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer,
spv::MemoryAccessAlignedMask, alignment);
spv::MemoryAccessAlignedMask |
(coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone),
alignment);
return value;
});
}
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits,
bool coherent = false) {
auto& state = ctx.state;
return EmitValueOrZeroIfCondition(state, active, [&]() {
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
ConstantU64(state, ~uint64_t {3}));
const auto first = LoadBdaDword(ctx, aligned);
const auto first = LoadBdaDword(ctx, aligned, coherent);
const auto byte =
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
@@ -212,7 +229,7 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
TypeBool(state), byte, ConstantU32(state, bits == 16u ? 2u : 0u));
const auto second = EmitValueOrZeroIfCondition(state, crosses, [&]() {
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), aligned,
ConstantU64(state, sizeof(uint32_t))));
ConstantU64(state, sizeof(uint32_t))), coherent);
});
const auto shift =
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte, ConstantU32(state, 3));
@@ -699,18 +716,38 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
return LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, 0u), resource);
}
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info);
return EmitValueOrDefaultIfCondition(
const auto value = EmitValueOrDefaultIfCondition(
ctx.state, plan.in_bounds, TypeU32(ctx.state),
FormattedOutOfBoundsValue(ctx, mem, plan, 1u),
[&]() { return LoadFormattedInBounds(ctx, mem, plan, 0u); });
if (mem.data_bits != 16u) {
return value;
}
if (info.type == Format::ComponentType::Uint) {
return EmitUMin32(ctx.state, value, ConstantU32(ctx.state, 0xffffu));
}
if (info.type == Format::ComponentType::Sint) {
const auto lower = EmitSMax32(ctx.state, value, ConstantU32(ctx.state, 0xffff8000u));
return EmitSMin32(ctx.state, lower, ConstantU32(ctx.state, 0x7fffu));
}
return EmitConvertF16F32(ctx.state, EmitBitCastF32U32(ctx.state, value));
});
}
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data) {
uint32_t component, uint32_t data, uint32_t data_bits) {
auto& state = ctx.state;
const auto bits = info.component_bits[component];
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else if (info.type == Format::ComponentType::Sint) {
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else {
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
@@ -762,7 +799,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
value, data, component);
}
}
value = EncodeFormattedStoreComponent(ctx, info, component, value);
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
const auto bits = info.component_bits[component];
if (info.packed_bitfield) {
packed = EmitBitFieldInsert(
@@ -1359,7 +1396,7 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
else if (address_info.access == IR::AddressAccess::Read &&
mem.kind != IR::ResourceKind::Scratch)
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
address_info.data_bits);
address_info.data_bits, mem.coherent);
else if (op == IR::ValueOpcode::LoadBufferU32 && mem.formatted)
value = mem.kind == IR::ResourceKind::IndirectBuffer ? LoadIndirectFormattedX(ctx, inst)
: FormattedLoad(ctx, inst, mem);
@@ -44,6 +44,16 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
};
const auto mapping = ConstantU32(state, mapping_offset);
const auto count = LoadMapping(mapping);
if (search_iterations == 0u) {
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), key,
ConstantU32(state, 2u));
const auto valid = Binary(state, spv::OpULessThan, TypeBool(state), index, count);
const auto safe = Select(state, TypeU32(state), valid, index, ConstantU32(state, 0u));
const auto entry = Binary(state, spv::OpIAdd, TypeU32(state), safe,
ConstantU32(state, mapping_offset + 1u));
return Select(state, TypeU32(state), valid, LoadMapping(entry),
ConstantU32(state, default_resource));
}
const auto entry_at = [&](uint32_t index) {
return Binary(state, spv::OpIAdd, TypeU32(state), mapping,
Binary(state, spv::OpIAdd, TypeU32(state),
@@ -63,6 +63,8 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x80u, Opcode::BUFFER_LOAD_FORMAT_D16_X, 1, 16, false, false, true},
{0x84u, Opcode::BUFFER_STORE_FORMAT_D16_X, 1, 16, false, false, true},
};
constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
@@ -109,6 +111,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 +217,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;
}
}
@@ -294,6 +298,9 @@ void DecodeMubuf(uint32_t pc, std::span<const uint32_t> code, uint32_t word_inde
}
DecodeVectorGpr(vdata, inst.dst);
if (inst.opcode == Opcode::BUFFER_LOAD_FORMAT_D16_X) {
inst.dst.sdwa_sel = 4u;
}
DecodeVectorGpr(vaddr, inst.src0);
DecodeScalarSource(srsrc * 4u, pc, inst.src1);
DecodeScalarSource(soffset, pc, inst.src2);
@@ -343,7 +350,6 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const uint32_t word0 = code[word_index];
const uint32_t word1 = code[word_index + 1u];
const uint32_t offset = word0 & 0xfffu;
const uint32_t dlc = (word0 >> 12u) & 1u;
const uint32_t lds = (word0 >> 13u) & 1u;
const uint32_t seg = (word0 >> 14u) & 0x3u;
const uint32_t opcode = (word0 >> 18u) & 0x7fu;
@@ -355,6 +361,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.pc = pc;
inst.word_count = 2;
inst.offset = seg == 0u ? (offset & 0x7ffu) : SignExtendU32(offset, 12u);
inst.dlc = ((word0 >> 12u) & 1u) != 0;
inst.glc = ((word0 >> 16u) & 1u) != 0;
inst.slc = ((word0 >> 17u) & 1u) != 0;
inst.family = Family::FLAT;
@@ -364,7 +371,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
ApplyMemoryInfo(inst, info);
SetRawWords(inst, code, word_index, 2);
if (dlc != 0 || lds != 0 || inst.glc || inst.slc || seg == 3u) {
if (lds != 0 || inst.glc || inst.slc || seg == 3u) {
SetUnsupported(inst, Family::FLAT, opcode, "FLAT modifiers or segment are not implemented");
return;
}
@@ -412,6 +419,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");
}
@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
{0x16u, Opcode::S_FLBIT_I32_B64},
{0x1au, Opcode::S_SEXT_I32_I16},
{0x1bu, Opcode::S_BITSET0_B32},
{0x1cu, Opcode::S_BITSET0_B64},
{0x1du, Opcode::S_BITSET1_B32},
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.dst).c_str(),
OperandToString(inst.src0).c_str()));
case Opcode::S_ABS_I32:
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
case Opcode::S_BCNT1_I32_B32:
@@ -617,10 +618,12 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_LOAD_FORMAT_XY:
case Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Opcode::BUFFER_STORE_FORMAT_X:
case Opcode::BUFFER_STORE_FORMAT_XY:
case Opcode::BUFFER_STORE_FORMAT_XYZ:
case Opcode::BUFFER_STORE_FORMAT_XYZW:
case Opcode::BUFFER_STORE_FORMAT_D16_X:
case Opcode::BUFFER_LOAD_UBYTE:
case Opcode::BUFFER_LOAD_USHORT:
case Opcode::BUFFER_LOAD_DWORD:
@@ -45,6 +45,7 @@ enum class Opcode {
S_CMOV_B64,
S_MOVK_I32,
S_ABS_I32,
S_SEXT_I32_I16,
S_ABSDIFF_I32,
S_BREV_B32,
S_BREV_B64,
@@ -268,6 +269,7 @@ enum class Opcode {
V_MIN3_I32,
V_MIN3_U32,
V_MIN3_F16,
V_MIN3_U16,
V_MAX3_F32,
V_MAX3_I32,
V_MAX3_U32,
@@ -381,6 +383,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,
@@ -453,19 +456,34 @@ enum class Opcode {
V_CMP_NE_U32,
V_CMP_GE_U32,
V_CMP_T_U32,
V_CMP_F_I64,
V_CMP_EQ_I64,
V_CMP_LT_I64,
V_CMP_LE_I64,
V_CMP_NE_I64,
V_CMP_T_I64,
V_CMP_F_U64,
V_CMP_LT_U64,
V_CMP_EQ_U64,
V_CMP_LE_U64,
V_CMP_GT_U64,
V_CMP_NE_U64,
V_CMP_GE_U64,
V_CMP_T_U64,
V_CMPX_F_I64,
V_CMPX_LT_I64,
V_CMPX_EQ_I64,
V_CMPX_LE_I64,
V_CMPX_NE_I64,
V_CMPX_T_I64,
V_CMPX_F_U64,
V_CMPX_LT_U64,
V_CMPX_EQ_U64,
V_CMPX_LE_U64,
V_CMPX_GT_U64,
V_CMPX_NE_U64,
V_CMPX_GE_U64,
V_CMPX_T_U64,
V_CMPX_LT_U32,
V_CMPX_EQ_U32,
V_CMPX_LE_U32,
@@ -488,10 +506,12 @@ enum class Opcode {
BUFFER_LOAD_FORMAT_XY,
BUFFER_LOAD_FORMAT_XYZ,
BUFFER_LOAD_FORMAT_XYZW,
BUFFER_LOAD_FORMAT_D16_X,
BUFFER_STORE_FORMAT_X,
BUFFER_STORE_FORMAT_XY,
BUFFER_STORE_FORMAT_XYZ,
BUFFER_STORE_FORMAT_XYZW,
BUFFER_STORE_FORMAT_D16_X,
BUFFER_LOAD_UBYTE,
BUFFER_LOAD_SBYTE,
BUFFER_LOAD_USHORT,
@@ -578,6 +598,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},
@@ -265,10 +265,18 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false},
{0xa0u, Opcode::V_CMP_F_I64, false}, {0xa7u, Opcode::V_CMP_T_I64, false},
{0xb0u, Opcode::V_CMPX_F_I64, false}, {0xb1u, Opcode::V_CMPX_LT_I64, false},
{0xb2u, Opcode::V_CMPX_EQ_I64, false}, {0xb3u, Opcode::V_CMPX_LE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
{0xe0u, Opcode::V_CMP_F_U64, false}, {0xe7u, Opcode::V_CMP_T_U64, false},
{0xf0u, Opcode::V_CMPX_F_U64, false}, {0xf1u, Opcode::V_CMPX_LT_U64, false},
{0xf2u, Opcode::V_CMPX_EQ_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf6u, Opcode::V_CMPX_GE_U64, false}, {0xf7u, Opcode::V_CMPX_T_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
@@ -310,6 +318,7 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
{0x152u, Opcode::V_MIN3_I32},
{0x153u, Opcode::V_MIN3_U32},
{0x351u, Opcode::V_MIN3_F16},
{0x353u, Opcode::V_MIN3_U16},
{0x154u, Opcode::V_MAX3_F32},
{0x155u, Opcode::V_MAX3_I32},
{0x156u, Opcode::V_MAX3_U32},
@@ -460,8 +469,9 @@ bool IsNativeVop3F16TernaryOpcode(Opcode opcode) {
opcode == Opcode::V_MED3_F16 || opcode == Opcode::V_FMA_F16;
}
bool IsNativeVop3I16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MED3_I16 || opcode == Opcode::V_MAD_I16;
bool IsNativeVop3B16TernaryOpcode(Opcode opcode) {
return opcode == Opcode::V_MIN3_U16 || opcode == Opcode::V_MED3_I16 ||
opcode == Opcode::V_MAD_I16;
}
bool IsNativeVop3B16BinaryOpcode(Opcode opcode) {
@@ -892,6 +902,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:
@@ -1366,7 +1377,7 @@ void ApplyNativeVop3TernaryModifiers(Instruction& inst, uint32_t op_sel, uint32_
inst.dst.sdwa_sel = ((op_sel & 0x8u) != 0) ? 5u : 4u;
}
void ApplyNativeVop3I16TernarySelectors(Instruction& inst, uint32_t op_sel) {
void ApplyNativeVop3B16TernarySelectors(Instruction& inst, uint32_t op_sel) {
Operand* sources[] = {&inst.src0, &inst.src1, &inst.src2};
for (uint32_t i = 0; i < 3u; i++) {
sources[i]->op_sel = ((op_sel >> i) & 1u) != 0;
@@ -1449,6 +1460,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,9 +1493,10 @@ 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)) {
if (IsNativeVop3B16TernaryOpcode(opcode)) {
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
}
if (IsNativeVop3B16BinaryOpcode(opcode)) {
@@ -1529,6 +1544,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:
@@ -1549,9 +1565,20 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_GT_U32:
case Opcode::V_CMPX_NE_U32:
case Opcode::V_CMPX_GE_U32:
case Opcode::V_CMPX_F_I64:
case Opcode::V_CMPX_LT_I64:
case Opcode::V_CMPX_EQ_I64:
case Opcode::V_CMPX_LE_I64:
case Opcode::V_CMPX_NE_I64:
case Opcode::V_CMPX_T_I64:
case Opcode::V_CMPX_F_U64:
case Opcode::V_CMPX_LT_U64:
case Opcode::V_CMPX_EQ_U64:
case Opcode::V_CMPX_LE_U64:
case Opcode::V_CMPX_GT_U64:
case Opcode::V_CMPX_NE_U64:
case Opcode::V_CMPX_GE_U64:
case Opcode::V_CMPX_T_U64:
case Opcode::V_CMPX_LT_I16:
case Opcode::V_CMPX_EQ_I16:
case Opcode::V_CMPX_LE_I16:
@@ -1706,7 +1733,7 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const bool vop3b_uses_sdst = carry_in_out || vop3b_carry_out || vop3b_mad_u64;
const bool mad_mix = false;
const bool f16_ternary = IsNativeVop3F16TernaryOpcode(inst.opcode);
const bool i16_ternary = IsNativeVop3I16TernaryOpcode(inst.opcode);
const bool b16_ternary = IsNativeVop3B16TernaryOpcode(inst.opcode);
const bool b16_binary = IsNativeVop3B16BinaryOpcode(inst.opcode);
const bool pack_b32_f16 = inst.opcode == Opcode::V_PACK_B32_F16;
const bool permlane = IsPermlaneOpcode(inst.opcode);
@@ -1817,8 +1844,8 @@ void DecodeVop3(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (f16_ternary) {
ApplyNativeVop3TernaryModifiers(inst, op_sel, abs, neg);
} else if (i16_ternary) {
ApplyNativeVop3I16TernarySelectors(inst, op_sel);
} else if (b16_ternary) {
ApplyNativeVop3B16TernarySelectors(inst, op_sel);
} else if (b16_binary) {
ApplyNativeVop3B16BinaryModifiers(inst, op_sel);
} else if (pack_b32_f16) {
@@ -31,6 +31,7 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_EQ_I64 ||
inst.opcode == Decoder::Opcode::V_CMP_NE_I64 ||
inst.opcode == Decoder::Opcode::V_CMPX_NE_I64;
const auto read = [&](const Decoder::Operand& operand) {
@@ -31,11 +31,12 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
}
void Translator::V_MED3_I16(const Decoder::Instruction& inst) {
void Translator::Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool sign) {
const auto result = IR::U32(ir.Emit(
IR::ValueOpcode::SMedTri32, {ReadU16AsU32(inst.src0, true), ReadU16AsU32(inst.src1, true),
ReadU16AsU32(inst.src2, true)}));
Write16Bits(DestinationOperand(inst), ir.BitwiseAnd(result, IR::U32(IR::Value(0xffffu))));
opcode, {ReadU16AsU32(inst.src0, sign), ReadU16AsU32(inst.src1, sign),
ReadU16AsU32(inst.src2, sign)}));
Write16Bits(DestinationOperand(inst), result);
}
void Translator::PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
@@ -129,8 +129,9 @@ IR::MemoryInfo MemoryInfoFromDecoded(const Decoder::Instruction& decoded) {
// Vector loads use GLC/DLC to bypass L0/GL1; atomics use GLC only to return data.
const bool buffer_atomic = decoded.opcode >= Decoder::Opcode::BUFFER_ATOMIC_SWAP &&
decoded.opcode <= Decoder::Opcode::BUFFER_ATOMIC_FMAX;
memory.coherent = memory.kind == ResourceKind::Buffer && !buffer_atomic &&
(decoded.glc || decoded.dlc);
memory.coherent = (memory.kind == ResourceKind::Buffer || memory.kind == ResourceKind::Flat ||
memory.kind == ResourceKind::Global) &&
!buffer_atomic && (decoded.glc || decoded.dlc);
memory.resource = ResourceIndexFromOperand(decoded.src1);
memory.sampler = ResourceIndexFromOperand(decoded.src2);
if (memory.kind == ResourceKind::ScalarBuffer) {
@@ -186,7 +187,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
if (decoded.family == Decoder::Family::MUBUF || decoded.family == Decoder::Family::MTBUF) {
const bool store_or_atomic =
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_FORMAT_X &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW) ||
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X) ||
(decoded.opcode >= Decoder::Opcode::BUFFER_STORE_BYTE &&
decoded.opcode <= Decoder::Opcode::BUFFER_STORE_DWORDX4) ||
(decoded.opcode >= Decoder::Opcode::TBUFFER_STORE_FORMAT_X &&
@@ -426,7 +427,7 @@ void Translator::S_LOAD(const Decoder::Instruction& inst, bool raw) {
void Translator::BUFFER_LOAD(const Decoder::Instruction& inst) {
const auto memory = MemoryInfoFromDecoded(inst);
IR::ValueOpcode opcode;
const auto bits = memory.data_bits;
const auto bits = memory.formatted ? 32u : memory.data_bits;
const auto sign = memory.data_signed;
switch (bits) {
case 8u: opcode = IR::ValueOpcode::LoadBufferU8; break;
@@ -472,7 +473,8 @@ void Translator::BUFFER_STORE(const Decoder::Instruction& inst) {
const auto data = ReadU32(data_src);
IR::ValueOpcode opcode;
IR::Value value;
switch (memory.data_bits) {
// Formatted stores carry packed VGPR bits; the format determines their conversion.
switch (memory.formatted ? 32u : memory.data_bits) {
case 8u:
opcode = IR::ValueOpcode::StoreBufferU8;
value = NarrowSubdword(data, 8u);
@@ -882,6 +884,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
@@ -935,6 +981,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_LOAD_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_X:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XY:
case Decoder::Opcode::TBUFFER_LOAD_FORMAT_XYZ:
@@ -950,6 +997,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::BUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZ:
case Decoder::Opcode::BUFFER_STORE_FORMAT_XYZW:
case Decoder::Opcode::BUFFER_STORE_FORMAT_D16_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_X:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XY:
case Decoder::Opcode::TBUFFER_STORE_FORMAT_XYZ:
@@ -1113,6 +1161,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:
@@ -134,6 +134,10 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_ABS_I32:
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
case O::S_SEXT_I32_I16:
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
return;
case O::S_MUL_I32:
case O::S_MULK_I32:
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
@@ -889,9 +889,16 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
case Decoder::Opcode::V_CMP_GT_U64:
case Decoder::Opcode::V_CMP_NE_U64:
case Decoder::Opcode::V_CMP_GE_U64:
case Decoder::Opcode::V_CMPX_LT_I64:
case Decoder::Opcode::V_CMPX_EQ_I64:
case Decoder::Opcode::V_CMPX_LE_I64:
case Decoder::Opcode::V_CMPX_NE_I64:
case Decoder::Opcode::V_CMPX_LT_U64:
case Decoder::Opcode::V_CMPX_EQ_U64:
case Decoder::Opcode::V_CMPX_LE_U64:
case Decoder::Opcode::V_CMPX_GT_U64:
case Decoder::Opcode::V_CMPX_NE_U64:
case Decoder::Opcode::V_CMPX_GE_U64:
include_vector(inst.src0, 2u);
include_vector(inst.src1, 2u);
break;
@@ -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,
@@ -165,8 +166,8 @@ private:
void FloatCube(const Decoder::Instruction& inst, uint32_t result_kind);
void Integer16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool arithmetic);
void Integer16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void Integer16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool sign);
void V_MAD_I16(const Decoder::Instruction& inst);
void V_MED3_I16(const Decoder::Instruction& inst);
void PackedInteger16Shift(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool arithmetic);
void PackedInteger16Binary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
@@ -96,16 +96,34 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
@@ -118,9 +136,15 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
@@ -130,6 +154,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
@@ -344,6 +371,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;
@@ -394,7 +422,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_SUB_NC_I16: return Integer16Binary(inst, IR::ValueOpcode::ISub32, false);
case O::V_MUL_LO_U16: return Integer16Binary(inst, IR::ValueOpcode::IMul32, false);
case O::V_MAD_I16: return V_MAD_I16(inst);
case O::V_MED3_I16: return V_MED3_I16(inst);
case O::V_MED3_I16: return Integer16Ternary(inst, IR::ValueOpcode::SMedTri32, true);
case O::V_MIN3_U16: return Integer16Ternary(inst, IR::ValueOpcode::UMinTri32, false);
case O::V_MIN_I16: return Integer16Binary(inst, IR::ValueOpcode::SMin32, true);
case O::V_MAX_I16: return Integer16Binary(inst, IR::ValueOpcode::SMax32, true);
case O::V_MIN_U16: return Integer16Binary(inst, IR::ValueOpcode::UMin32, false);
+11 -5
View File
@@ -485,15 +485,21 @@ struct BlockInfo {
struct DescriptorSource {
struct IndirectDescriptor {
uint32_t material_source = UINT32_MAX;
struct SelectorRead {
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
bool operator==(const SelectorRead& other) const = default;
};
std::optional<SelectorRead> selector;
uint32_t table_source = 0;
uint32_t selector_stride = 0;
uint32_t selector_offset = 0;
uint32_t table_offset = 0;
uint32_t table_immediate = 0;
uint32_t table_stride = 0;
uint32_t table_record_bytes = 0;
bool table_scalar = false;
uint32_t workgroup_axis = UINT32_MAX;
uint32_t selector_shift = 0;
uint32_t selector_bits = UINT32_MAX;
Value key_count;
Value selector_first;
Value selector_mask;
@@ -101,7 +101,7 @@ SharedMemoryResources CollectMemoryResources(const Program& program, std::vector
bool UsesFlattenedSrt(const Program& program) {
const auto uses_mapping = [](const auto& resource) {
return resource.indirect_search_iterations != 0u;
return resource.indirect_root != UINT32_MAX;
};
return std::ranges::any_of(program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
@@ -238,6 +238,13 @@ bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
return true;
}
bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u;
}
template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect,
@@ -249,7 +256,24 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t maximum_resources, Normalize&& normalize) {
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
auto& keys = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) return false;
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
return true;
};
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
uint64_t step, uint64_t count) {
keys.resize(count);
@@ -269,8 +293,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.clear();
DescriptorValue material_value;
DescriptorValue table_value;
if ((indirect.material_source != UINT32_MAX &&
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
if ((selector != nullptr &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
}
@@ -283,7 +307,48 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
return false;
}
if (indirect.material_source == UINT32_MAX) {
if (IsBoundedDescriptorTable(program, indirect)) {
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
descriptors[resource_index] = {.dword_count = dword_count};
return true;
}
if (table_size > UINT32_MAX ||
(indirect.table_record_bytes != 0u &&
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
return false;
// Descriptor bytes are refreshed together. The GPU selects the DWORD address;
// no material selector or unused table field is interpreted on the CPU.
keys.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false;
const auto mapping_offset = snapshot.flattened_srt.size();
descriptors[resource_index] = {.dword_count = dword_count};
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
for (uint32_t word = 0; word < keys.size(); ++word) {
DescriptorValue candidate {.dword_count = dword_count};
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
candidate.dwords.begin());
if (!normalize(candidate)) return false;
uint32_t ordinal;
if (!intern_candidate(candidate, ordinal)) return false;
if (ordinal == 0u) continue;
snapshot.flattened_srt.resize(mapping_offset + 2u + word, 0u);
snapshot.flattened_srt[mapping_offset + 1u + word] = ordinal;
}
if (descriptors.size() == children_begin) {
snapshot.flattened_srt.resize(mapping_offset);
} else {
snapshot.flattened_srt[mapping_offset] =
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
auto& root = specializations[resource_index];
root.indirect_root = resource_index;
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
root.indirect_search_iterations = 0u;
}
return true;
}
if (selector == nullptr) {
uint32_t key_count = 0;
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
@@ -295,7 +360,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
}
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(key_count != 0u && uint64_t {indirect.table_offset} +
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
UINT32_MAX + 1ull)) {
return false;
@@ -306,20 +371,18 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
ShaderBufferResource material;
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) &&
(material.Base48() & 3u) != 0u) ||
material.SwizzleEnabled() ||
material.AddTid() || material.OutOfBounds() != 0u ||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
uint64_t {selector->offset} + 4u > material.Stride() ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
uint64_t {first} + count > material.NumRecords())
return false;
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
indirect.selector_offset + 4u >
selector->offset + 4u >
uint64_t {UINT32_MAX} + 1u)
return false;
if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset,
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
material.Stride(), count)) return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
@@ -335,8 +398,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
static_cast<uint64_t>(index) * indirect.selector_stride;
const auto offset = static_cast<uint64_t>(selector->offset) +
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
@@ -344,37 +407,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.push_back(key);
mask &= mask - 1u;
}
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) {
return false;
}
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectDescriptorProbes) {
return false;
}
if (!read_keys(material, first, step, probe_count)) return false;
if (indirect.selector_shift == 0u) keys.push_back(0u);
}
if (indirect.material_source != UINT32_MAX) {
for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits;
if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) {
keys.push_back(0u); // An out-of-range material load returns zero.
}
} else return false;
if (selector != nullptr) {
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
}
const auto children_begin = descriptors.size();
const auto mapping_offset = snapshot.flattened_srt.size();
const auto root_resource = specializations[resource_index];
const auto key_count = sources.empty() ? keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
@@ -383,8 +423,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
DescriptorValue candidate;
candidate.dword_count = dword_count;
if (sources.empty()) {
const auto table_offset =
static_cast<uint32_t>(key * indirect.table_stride) + indirect.table_offset;
const uint64_t table_offset =
uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(dword_count))) {
return false;
@@ -396,19 +437,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t ordinal = 0;
if (entry == 0) {
descriptors[resource_index] = candidate;
} else if (descriptors[resource_index] != candidate) {
const auto found =
std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) {
return false;
}
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
} else if (!intern_candidate(candidate, ordinal)) {
return false;
}
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
@@ -566,9 +596,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
? snapshot.flattened_srt[root.indirect_mapping_offset]
: 0u;
if (root.indirect_search_iterations == 0u || key_count < 2u ||
if (key_count == 0u ||
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
static_cast<size_t>(key_count) * 2u >
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
snapshot.flattened_srt.size()) {
return SpecializationFail("indirect image specialization has an invalid key mapping");
}
@@ -1010,9 +1040,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
plan.requires_specialization_memory = true;
capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty();
MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots,
indirect.selector_mask);
capture_indirect_reads = true;
if (indirect.selector)
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
if (indirect.sources.empty()) {
@@ -1156,10 +1187,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
continue;
}
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128))
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
value.dwords.fill(0u);
return true;
})) {
@@ -8,7 +8,6 @@
#include <algorithm>
#include <fmt/format.h>
#include <map>
#include <numeric>
#include <optional>
#include <span>
#include <tuple>
@@ -374,11 +373,68 @@ public:
}
}
for (const auto& plan: m_indirect_descriptors) {
auto key = plan.key;
if (plan.table_read != nullptr) {
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
auto* block = anchor->Parent();
const auto where = std::ranges::find_if(block->Instructions(),
[&](const Inst& inst) { return &inst == anchor; });
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
return Value(&*block->PrependNewInst(where, op, args));
};
const auto& read = *plan.table_read;
const auto& memory = m_program.memory_info[read.Flags<MemoryFlags>().index];
Value offset;
Value valid(true);
if (plan.table_indexed) {
const auto* table = read.Arg(0).ResolveInstruction();
const auto stride = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(1), Value(16u), Value(14u)});
const auto format = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(3), Value(12u), Value(7u)});
offset = emit(ValueOpcode::IAdd32,
{emit(ValueOpcode::IMul32, {read.Arg(1), stride}), Value(memory.offset)});
valid = emit(ValueOpcode::LogicalAnd,
{emit(ValueOpcode::ULessThan32, {read.Arg(1), table->Arg(2)}),
emit(ValueOpcode::INotEqual32, {format, Value(0u)})});
} else {
const auto dynamic = emit(ValueOpcode::BitwiseAnd32, {read.Arg(1), Value(~3u)});
offset = emit(ValueOpcode::IAdd32, {dynamic, Value(memory.offset & ~3u)});
valid = emit(ValueOpcode::ULessThanEqual32, {dynamic, offset});
}
key = emit(ValueOpcode::SelectU32, {valid, offset, Value(UINT32_MAX)});
if (!plan.table_indexed) {
// NativeDescriptorSource proved this read reaches the image. Preserve its
// SSA Phi edges while retaining descriptor DWORDs for other consumers.
std::map<const Inst*, Value> projected;
const auto project = [&](auto&& self, Value value) -> Value {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (phi == plan.table_read) return key;
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi) return Value(UINT32_MAX);
if (const auto found = projected.find(phi); found != projected.end()) return found->second;
auto* parent = phi->Parent();
const auto position = std::ranges::find_if(parent->Instructions(),
[&](const Inst& inst) { return &inst == phi; });
auto& selected = *parent->PrependNewInst(position, ValueOpcode::Phi);
selected.SetFlags(Type::U32);
projected.emplace(phi, Value(&selected));
for (uint32_t arm = 0; arm < phi->NumArgs(); ++arm)
selected.AddPhiOperand(phi->PhiBlock(arm), self(self, phi->Arg(arm)));
return Value(&selected);
};
key = project(project, plan.handle->Arg(0));
}
plan.handle->SetArg(0, key);
for (uint32_t word = 1; word < plan.handle->NumArgs(); ++word)
plan.handle->SetArg(word, Value(0u));
continue;
}
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
plan.handle->SetArg(0, plan.key);
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
} else {
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, plan.key);
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, key);
for (uint32_t dword = 0; dword < 4u; dword++) {
plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]);
}
@@ -437,6 +493,10 @@ private:
std::array<Value, 8> roots {};
std::array<uint32_t, 8> memory {};
std::array<const Inst*, 8> reads {};
bool split_offsets = false;
bool retain_reads = false;
bool table_indexed = false;
const Inst* table_read = nullptr;
};
[[noreturn]] void Fail(uint32_t pc, const std::string& reason) const {
@@ -834,7 +894,7 @@ private:
uint32_t InternSource(const DescriptorSource& descriptor) {
for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) {
const auto& current = m_sources[candidate];
auto& current = m_sources[candidate];
if (current.dword_count != descriptor.dword_count ||
current.indirect_descriptor.has_value() != descriptor.indirect_descriptor.has_value()) {
continue;
@@ -842,10 +902,9 @@ private:
if (current.indirect_descriptor.has_value()) {
const auto& a = *current.indirect_descriptor;
const auto& b = *descriptor.indirect_descriptor;
if (a.material_source != b.material_source || a.table_source != b.table_source ||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits ||
a.table_offset != b.table_offset || a.table_stride != b.table_stride ||
if (a.selector != b.selector || a.table_source != b.table_source ||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
a.table_stride != b.table_stride ||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
@@ -860,6 +919,12 @@ private:
same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]);
}
if (same) {
if (current.indirect_descriptor) {
current.indirect_descriptor->table_scalar |= descriptor.indirect_descriptor->table_scalar;
current.indirect_descriptor->table_record_bytes = std::max(
current.indirect_descriptor->table_record_bytes,
descriptor.indirect_descriptor->table_record_bytes);
}
return candidate;
}
}
@@ -949,54 +1014,6 @@ private:
return ValidateSource(descriptor, bad_dword);
}
bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const {
stride = 1u;
offset = 0u;
value = value.Resolve();
while (const auto* inst = value.TryInstruction()) {
if (inst->NumArgs() != 2u) break;
uint32_t constant = 0;
Value remaining;
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
ImmediateU32(inst->Arg(1), constant) && constant < 32u) {
stride *= 1u << constant;
remaining = inst->Arg(0);
} else if (inst->GetOpcode() == ValueOpcode::IAdd32 ||
inst->GetOpcode() == ValueOpcode::IMul32) {
if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1);
else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0);
else break;
if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant;
else stride *= constant;
} else {
break;
}
value = remaining.Resolve();
}
// Treat the remaining GPU-derived scalar as any U32 value.
return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate();
}
Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
indirect.selector_shift = 0u;
indirect.selector_bits = UINT32_MAX;
const auto* inst = key.Resolve().TryInstruction();
uint32_t immediate;
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) {
if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0);
else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1);
else return key;
indirect.selector_bits = immediate;
inst = key.Resolve().TryInstruction();
}
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) {
indirect.selector_shift = immediate;
key = inst->Arg(0);
}
return key.Resolve();
}
enum class LaneQuantifier { Any, All };
struct EdgePredicate {
Value condition;
@@ -1443,9 +1460,9 @@ private:
static_cast<uint64_t>(UINT32_MAX) + 1u) return false;
const auto mask = BoundedSetBitMask(offset.index, active);
if (mask.IsEmpty()) return false;
indirect.material_source = InternSource(material_source);
indirect.selector_stride = static_cast<uint32_t>(offset.stride);
indirect.selector_offset = static_cast<uint32_t>(offset.offset + memory.offset);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.stride = static_cast<uint32_t>(offset.stride),
.offset = static_cast<uint32_t>(offset.offset + memory.offset)});
indirect.key_count = Value(32u);
indirect.selector_mask = mask;
return true;
@@ -1606,8 +1623,9 @@ private:
bool MatchDescriptorTable(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan,
DescriptorSource& table_source, Value& key, uint32_t& table_offset,
uint32_t& table_stride) {
uint32_t& table_stride, uint32_t& table_immediate) {
Inst* table_handle = nullptr;
Value table_guard;
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
if (read == nullptr) {
@@ -1615,7 +1633,31 @@ private:
}
uint32_t memory_index = 0;
const auto* memory = ScalarReadMemory(*read, memory_index);
if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
uint32_t component = 0;
const bool indexed = memory == nullptr;
if (indexed) {
if (handle.NumArgs() != 8u) return false;
const auto* selected = UniformizedMaterialValue(descriptor.dwords[dword], handle);
if (selected == nullptr) return false;
const auto* extract = selected->Arg(1).Resolve().TryInstruction();
if (extract == nullptr || extract->GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!ImmediateU32(extract->Arg(1), component) || component >= 4u) return false;
read = extract->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x4) return false;
memory_index = read->Flags<MemoryFlags>().index;
if (memory_index >= m_program.memory_info.size()) return false;
memory = &m_program.memory_info[memory_index];
uint32_t offset = 1u, scalar = 1u;
if (memory->kind != ResourceKind::Buffer || memory->typed || memory->formatted ||
!memory->idxen || memory->offen || memory->data_bits != 32u ||
memory->data_dwords != 4u ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, read->Arg(4), selected->Arg(0)) ||
(dword != 0u && !EquivalentValue(m_program, table_guard, read->Arg(4)))) return false;
table_guard = read->Arg(4);
}
if (memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
!MemoryIndexBelongsTo(memory_index, *read)) {
return false;
}
@@ -1630,23 +1672,32 @@ private:
(memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) ||
(table_handle != nullptr &&
!EquivalentValue(m_program, Value(table_handle), Value(current_handle))) ||
!MatchTableOffset(read->Arg(1), current_key, offset, stride) ||
memory->offset > UINT32_MAX - offset) {
(!indexed && !MatchTableOffset(read->Arg(1), current_key, offset, stride))) {
return false;
}
offset += memory->offset;
if (indexed) {
current_key = read->Arg(1);
offset = component * 4u;
}
if (dword == 0u) {
if (component != 0u) return false;
plan.table_indexed = indexed;
key = current_key;
table_offset = offset;
table_immediate = memory->offset;
table_stride = stride;
} else if (table_stride != stride || !EquivalentValue(m_program, key, current_key) ||
static_cast<uint64_t>(table_offset) + dword * sizeof(uint32_t) != offset) {
} else if (plan.table_indexed != indexed || table_stride != stride ||
!EquivalentValue(m_program, key, current_key) ||
uint64_t {table_offset} + table_immediate + dword * sizeof(uint32_t) !=
uint64_t {offset} + memory->offset) {
return false;
}
plan.split_offsets |= !indexed && offset != table_offset;
table_handle = current_handle;
if (handle.NumArgs() == 8u
? WorkgroupAxis(key) == UINT32_MAX && !UsesOnlyImageDescriptors(*read) :
std::ranges::any_of(read->Uses(), [](const Use& use) {
if (handle.NumArgs() == 8u) {
if (memory->kind == ResourceKind::ScalarAddress)
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
} else if (std::ranges::any_of(read->Uses(), [](const Use& use) {
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
std::ranges::any_of(use.user->Uses(), [](const Use& consumer) {
return consumer.user->GetOpcode() != ValueOpcode::StoreBufferU32;
@@ -1664,34 +1715,6 @@ private:
return true;
}
bool MatchUniformizedBufferKey(Value key, const Inst& image,
DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
const auto* selected = UniformizedMaterialValue(key, image);
if (selected == nullptr) return false;
const auto value = MaterialKey(selected->Arg(1), indirect);
if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false;
const auto* loaded = value.TryInstruction();
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false;
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
uint32_t offset = 1u, scalar = 1u;
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) ||
!MakeRuntimeTableSource(*read, material_source)) return false;
// GPU index arithmetic may wrap. Every record in the bounded V# is a candidate.
indirect.material_source = InternSource(material_source);
indirect.selector_first = Value(0u);
indirect.key_count = material_source.dwords[2];
indirect.selector_offset = memory.offset;
return true;
}
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
@@ -1699,17 +1722,19 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride)) return false;
table_stride, table_immediate)) return false;
DescriptorSource material_source;
DescriptorSource::IndirectDescriptor indirect;
indirect.table_offset = table_offset;
indirect.table_immediate = table_immediate;
indirect.table_stride = table_stride;
indirect.workgroup_axis = WorkgroupAxis(key);
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
// only the image handle is projected onto the bounded workgroup key.
plan.reads.fill(nullptr);
plan.retain_reads = true;
} else if (table_source.dword_count == 2u) {
if (table_stride != 32u) return false;
const auto* selector = key.Resolve().TryInstruction();
@@ -1728,36 +1753,21 @@ private:
}
if ((table_offset & 3u) != 0u ||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
} else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) {
auto* material_read = MaterialKey(key, indirect).TryInstruction();
uint32_t material_memory_index = 0;
const auto* memory = material_read != nullptr
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
return false;
}
if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride,
indirect.selector_offset)) {
return false;
}
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
indirect.selector_offset =
(static_cast<uint32_t>(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u);
const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction();
if (material_handle == nullptr ||
material_handle->GetOpcode() != ValueOpcode::GetBufferResource ||
!MakeRuntimeTableSource(*material_read, material_source)) {
return false;
}
indirect.material_source = InternSource(material_source);
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
if (plan.split_offsets) return false;
indirect.table_stride = 0u;
indirect.table_offset = 0u;
indirect.table_immediate = 0u;
indirect.workgroup_axis = UINT32_MAX;
plan.table_read = plan.reads[0];
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
else indirect.table_scalar = true;
plan.retain_reads = true;
}
// This plan combines shader byte-offset additions with the scalar immediate.
// A nonzero combined offset is exact only when neither can cross U32 wrap.
const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits;
if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u &&
table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX)
return false;
if (plan.retain_reads) plan.reads.fill(nullptr);
indirect.table_source = InternSource(table_source);
DescriptorSource image_source;
image_source.dword_count = 8u;
@@ -1791,9 +1801,10 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u) return false;
table_stride, table_immediate) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u || table_immediate != 0u) return false;
const auto* lane_read = key.Resolve().TryInstruction();
if (lane_read == nullptr || lane_read->GetOpcode() != ValueOpcode::ReadFirstLane)
return false;
@@ -1825,9 +1836,9 @@ private:
indirect.table_stride = table_stride;
if (!MakeRuntimeTableSource(*read, material_source) ||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
indirect.material_source = InternSource(material_source);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.offset = memory.offset + lane * 4u});
indirect.table_source = InternSource(table_source);
indirect.selector_offset = memory.offset + lane * 4u;
DescriptorSource source;
source.dword_count = 4u;
source.dwords.fill(Value(0u));
+3
View File
@@ -121,6 +121,9 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) {
event.event = event.pending_events.front();
event.pending_events.pop_front();
event.triggered = true;
} else {
// A persistent level is delivered again by the next wait, not this batch.
break;
}
if (ret >= num) {
+9
View File
@@ -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>
@@ -179,6 +179,12 @@
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -400,6 +406,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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>Edit game config</string>
</property>
<property name="text">
<string/>
@@ -150,7 +150,7 @@
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delete configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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">
+6 -1
View File
@@ -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

+7
View File
@@ -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

+26
View File
@@ -0,0 +1,26 @@
#ifndef CHEAT_FILE_H
#define CHEAT_FILE_H
#include <QByteArray>
#include <QJsonDocument>
#include <QList>
#include <QString>
namespace Cheats {
[[nodiscard]] bool IsSupportedTitleId(const QString& title_id);
[[nodiscard]] QString PlanPath(const QString& title_id);
// Local files retain their existing defaults; remote imports receive additional validation.
[[nodiscard]] QString ParseDocument(const QByteArray& data, QJsonDocument* document);
[[nodiscard]] QString ValidateImport(const QJsonDocument& document, const QString& title_id,
const QString& version, const QString& process);
[[nodiscard]] QJsonDocument WithSelection(const QJsonDocument& document,
const QList<bool>& enabled);
// Refuse to replace a file changed since it was loaded; empty data also permits creation.
[[nodiscard]] QString SaveDocument(const QString& path, const QJsonDocument& document,
const QByteArray& expected_data);
} // namespace Cheats
#endif // CHEAT_FILE_H
+66
View File
@@ -0,0 +1,66 @@
#ifndef CHEAT_REPOSITORY_H
#define CHEAT_REPOSITORY_H
#include <QByteArray>
#include <QList>
#include <QNetworkAccessManager>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <functional>
class QNetworkReply;
namespace Cheats {
struct RemoteFile {
QString name;
QString title;
QString version;
QString format;
[[nodiscard]] bool IsSupported() const;
[[nodiscard]] QUrl Url() const;
};
struct CatalogResult {
QList<RemoteFile> files;
QString error;
};
[[nodiscard]] CatalogResult ParseIndex(const QByteArray& data, const QString& format,
const QString& title_id);
class CheatRepository final: public QObject {
Q_OBJECT
public:
explicit CheatRepository(QObject* parent = nullptr);
void Load(const QString& title_id);
void Fetch(const RemoteFile& file);
void CancelFetch();
signals:
void CatalogLoaded(QList<Cheats::RemoteFile> files, QString error);
void FileLoaded(QByteArray data, QString error);
private:
QNetworkReply* Request(const QUrl& url, qint64 limit,
std::function<void(QByteArray, QString)> complete);
QNetworkAccessManager m_network;
QList<QNetworkReply*> m_catalog_replies;
QPointer<QNetworkReply> m_file_reply;
QList<RemoteFile> m_files;
QStringList m_errors;
int m_catalog_generation = 0;
int m_fetch_generation = 0;
int m_pending = 0;
};
} // namespace Cheats
#endif // CHEAT_REPOSITORY_H
+13 -87
View File
@@ -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;
@@ -123,10 +121,11 @@ public:
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
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";
@@ -156,6 +155,7 @@ public:
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -188,90 +188,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;
+28 -7
View File
@@ -1,30 +1,51 @@
#ifndef PATCHES_DIALOG_H
#define PATCHES_DIALOG_H
#include "cheatRepository.h"
#include <QByteArray>
#include <QDialog>
#include <QJsonDocument>
#include <QString>
class Configuration;
class QLabel;
class QListWidget;
class QPushButton;
class QTabWidget;
class QTreeWidget;
class PatchesDialog final: public QDialog {
public:
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
~PatchesDialog() override = default;
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
private:
void Load();
void Save();
void LoadRemote();
void PreviewRemote();
void ImportRemote();
QString m_title_id;
QListWidget* m_patches = nullptr;
QLabel* m_status = nullptr;
QPushButton* m_apply = nullptr;
QString m_title_id;
QString m_version;
QString m_process;
QByteArray m_local_data;
QJsonDocument m_local_document;
QJsonDocument m_remote_document;
bool m_local_readable = false;
Cheats::CheatRepository* m_repository = nullptr;
QList<Cheats::RemoteFile> m_remote_files;
QTabWidget* m_tabs = nullptr;
QTreeWidget* m_files = nullptr;
QListWidget* m_preview = nullptr;
QLabel* m_remote_status = nullptr;
QLabel* m_preview_status = nullptr;
QPushButton* m_refresh = nullptr;
QPushButton* m_import = nullptr;
QListWidget* m_patches = nullptr;
QLabel* m_status = nullptr;
QPushButton* m_apply = nullptr;
};
#endif // PATCHES_DIALOG_H
@@ -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);
+2
View File
@@ -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>
+180
View File
@@ -0,0 +1,180 @@
#include "cheatFile.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSaveFile>
namespace Cheats {
namespace {
QString Tr(const char* text) {
return QCoreApplication::translate("Cheats", text);
}
QString ValidateDocument(const QJsonDocument& document) {
if (!document.isObject()) {
return Tr("Invalid cheat JSON.");
}
const auto mods = document.object().value(QStringLiteral("mods"));
if (!mods.isArray()) {
return Tr("Cheat JSON has no \"mods\" array.");
}
for (const auto& value: mods.toArray()) {
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!value.isObject() || !name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
return Tr("Invalid cheat entry.");
}
}
return {};
}
} // namespace
bool IsSupportedTitleId(const QString& title_id) {
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
return pattern.match(title_id.trimmed().toUpper()).hasMatch();
}
QString PlanPath(const QString& title_id) {
if (!IsSupportedTitleId(title_id)) {
return {};
}
return QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
}
QString ParseDocument(const QByteArray& data, QJsonDocument* document) {
const auto parsed = QJsonDocument::fromJson(data);
const auto error = ValidateDocument(parsed);
*document = error.isEmpty() ? parsed : QJsonDocument();
return error;
}
QString ValidateImport(const QJsonDocument& document, const QString& title_id,
const QString& version, const QString& process) {
const auto parse_error = ValidateDocument(document);
if (!parse_error.isEmpty()) {
return parse_error;
}
const auto root = document.object();
if (!IsSupportedTitleId(title_id) ||
root.value(QStringLiteral("id"))
.toString()
.compare(title_id.trimmed(), Qt::CaseInsensitive) != 0) {
return Tr("The cheat file does not match this game's title ID.");
}
if (version.isEmpty() || root.value(QStringLiteral("version")).toString() != version) {
return Tr("The cheat file does not match this game's version (%1).").arg(version);
}
if (process.isEmpty() ||
root.value(QStringLiteral("process")).toString().compare(process, Qt::CaseInsensitive) !=
0) {
return Tr("The cheat file does not match this game's executable (%1).").arg(process);
}
static const QRegularExpression offset_pattern(
QStringLiteral("\\A(?:0[xX])?[0-9a-fA-F]{1,16}\\z"));
static const QRegularExpression bytes_pattern(QStringLiteral("\\A(?:[0-9a-fA-F]{2})+\\z"));
const auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.isEmpty()) {
return Tr("The cheat file contains no cheats.");
}
if (root.contains(QStringLiteral("master"))) {
return Tr("This JSON uses master codes, which are not supported yet.");
}
for (const auto& value: mods) {
const auto mod = value.toObject();
const auto memory = mod.value(QStringLiteral("memory"));
if (mod.contains(QStringLiteral("module_name"))) {
return Tr("This JSON uses module-specific cheats, which are not supported yet.");
}
if (!memory.isArray() || memory.toArray().isEmpty()) {
return Tr("Unsupported memory entries in cheat \"%1\".")
.arg(mod.value(QStringLiteral("name")).toString());
}
for (const auto& entry: memory.toArray()) {
const auto write = entry.toObject();
const auto offset = write.value(QStringLiteral("offset")).toString();
const auto off = write.value(QStringLiteral("off")).toString();
const auto on = write.value(QStringLiteral("on")).toString();
const auto absolute = write.value(QStringLiteral("absolute"));
const auto expected = write.value(QStringLiteral("expected"));
if ((!absolute.isUndefined() && (!absolute.isBool() || absolute.toBool())) ||
(!expected.isUndefined() &&
(!expected.isString() ||
expected.toString().compare(off, Qt::CaseInsensitive) != 0)) ||
write.contains(QStringLiteral("section")) ||
write.contains(QStringLiteral("sectionName")) ||
write.contains(QStringLiteral("sectionProtection")) ||
write.contains(QStringLiteral("logicalOffset")) ||
write.contains(QStringLiteral("sectionOffset"))) {
return Tr("This JSON uses memory addressing features that are not supported yet.");
}
if (!entry.isObject() || !offset_pattern.match(offset).hasMatch() ||
!bytes_pattern.match(off).hasMatch() || !bytes_pattern.match(on).hasMatch() ||
off.size() != on.size()) {
return Tr("Unsupported memory entries in cheat \"%1\".")
.arg(mod.value(QStringLiteral("name")).toString());
}
}
}
return {};
}
QJsonDocument WithSelection(const QJsonDocument& document, const QList<bool>& enabled) {
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != enabled.size()) {
return {};
}
for (qsizetype index = 0; index < mods.size(); ++index) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), enabled[index]);
mods[index] = mod;
}
root.insert(QStringLiteral("mods"), mods);
return QJsonDocument(root);
}
QString SaveDocument(const QString& path, const QJsonDocument& document,
const QByteArray& expected_data) {
if (path.isEmpty() || !document.isObject()) {
return Tr("Invalid cheat file.");
}
QFile input(path);
if (input.exists()) {
if (!input.open(QIODevice::ReadOnly)) {
return Tr("Could not read cheat file: %1").arg(input.errorString());
}
const auto current = input.readAll();
if (input.error() != QFile::NoError) {
return Tr("Could not read cheat file: %1").arg(input.errorString());
}
if (current != expected_data) {
return Tr("Cheat file changed; reload the dialog.");
}
} else if (!expected_data.isEmpty()) {
return Tr("Cheat file changed; reload the dialog.");
}
input.close();
if (!QDir().mkpath(QFileInfo(path).absolutePath())) {
return Tr("Could not create the local cheats directory.");
}
QSaveFile output(path);
const auto data = document.toJson();
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() ||
!output.commit()) {
return Tr("Could not save cheat file: %1").arg(output.errorString());
}
return {};
}
} // namespace Cheats
+200
View File
@@ -0,0 +1,200 @@
#include "cheatRepository.h"
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QSet>
#include <QTimer>
#include <algorithm>
#include <utility>
namespace Cheats {
namespace {
constexpr qint64 kIndexLimit = 2 * 1024 * 1024;
constexpr qint64 kFileLimit = 8 * 1024 * 1024;
const QString kBaseUrl = QStringLiteral(
"https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/");
bool IsFormat(const QString& format) {
return format == QStringLiteral("json") || format == QStringLiteral("mc4") ||
format == QStringLiteral("shn");
}
QRegularExpressionMatch MatchName(const QString& name) {
static const QRegularExpression pattern(QStringLiteral(
"\\A([A-Z]{4}[0-9]{5})_([0-9]+(?:\\.[0-9]+)+)_[A-Za-z0-9_.-]+\\.(json|mc4|shn)\\z"));
return pattern.match(name);
}
} // namespace
bool RemoteFile::IsSupported() const {
return format == QStringLiteral("json") && !Url().isEmpty();
}
QUrl RemoteFile::Url() const {
const auto match = MatchName(name);
if (!IsFormat(format) || !match.hasMatch() || match.captured(3) != format) {
return {};
}
return QUrl(kBaseUrl + format + QLatin1Char('/') + name);
}
CatalogResult ParseIndex(const QByteArray& data, const QString& format, const QString& title_id) {
CatalogResult result;
if (!IsFormat(format) || data.isEmpty() || data.size() > kIndexLimit) {
result.error = CheatRepository::tr("Invalid cheat index.");
return result;
}
const auto id = title_id.trimmed().toUpper();
QSet<QString> seen;
int invalid = 0;
auto text = QString::fromUtf8(data);
if (text.startsWith(QChar(0xfeff))) {
text.remove(0, 1);
}
for (const auto& line: text.split(QLatin1Char('\n'))) {
const auto trimmed = line.trimmed();
if (trimmed.isEmpty()) {
continue;
}
const auto separator = trimmed.indexOf(QLatin1Char('='));
const auto name = trimmed.left(separator);
const auto match = MatchName(name);
if (separator < 0 || !match.hasMatch() || match.captured(3) != format) {
invalid++;
continue;
}
if (match.captured(1) != id || seen.contains(name)) {
continue;
}
seen.insert(name);
result.files.append(
{name, trimmed.mid(separator + 1).trimmed(), match.captured(2), format});
}
if (invalid != 0) {
result.error = CheatRepository::tr("Skipped %1 invalid cheat index entries.").arg(invalid);
}
return result;
}
CheatRepository::CheatRepository(QObject* parent): QObject(parent), m_network(this) {}
QNetworkReply* CheatRepository::Request(const QUrl& url, qint64 limit,
std::function<void(QByteArray, QString)> complete) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::SameOriginRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
reply->setReadBufferSize(limit + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply, limit]() {
if (reply->bytesAvailable() > limit) {
reply->setProperty("too_large", true);
reply->abort();
}
});
QTimer::singleShot(30000, reply, [reply]() {
if (!reply->isFinished()) {
reply->setProperty("timed_out", true);
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this,
[this, reply, limit, complete = std::move(complete)]() {
QString error;
if (reply->property("too_large").toBool() || reply->bytesAvailable() > limit) {
error = tr("Cheat download exceeds the size limit.");
} else if (reply->property("timed_out").toBool()) {
error = tr("Cheat download timed out.");
} else if (reply->error() != QNetworkReply::NoError) {
error = reply->errorString();
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) {
error = tr("Unexpected response from the cheat repository.");
}
const auto data = error.isEmpty() ? reply->readAll() : QByteArray();
m_catalog_replies.removeAll(reply);
if (m_file_reply == reply) {
m_file_reply.clear();
}
reply->deleteLater();
complete(data, error);
});
return reply;
}
void CheatRepository::Load(const QString& title_id) {
const auto generation = ++m_catalog_generation;
for (auto* reply: std::exchange(m_catalog_replies, {})) {
reply->abort();
}
CancelFetch();
m_files.clear();
m_errors.clear();
m_pending = 3;
for (const auto& format:
{QStringLiteral("json"), QStringLiteral("mc4"), QStringLiteral("shn")}) {
auto* reply =
Request(QUrl(kBaseUrl + format + QStringLiteral(".txt")), kIndexLimit,
[this, generation, format, title_id](const QByteArray& data, QString error) {
if (generation != m_catalog_generation) {
return;
}
if (error.isEmpty()) {
auto result = ParseIndex(data, format, title_id);
m_files.append(result.files);
error = result.error;
}
if (!error.isEmpty()) {
m_errors.append(format.toUpper() + QStringLiteral(": ") + error);
}
if (--m_pending == 0) {
std::sort(m_files.begin(), m_files.end(),
[](const RemoteFile& left, const RemoteFile& right) {
if (left.version != right.version) {
return left.version > right.version;
}
if (left.format != right.format) {
return left.format < right.format;
}
return left.name < right.name;
});
emit CatalogLoaded(m_files, m_errors.join(QLatin1Char('\n')));
}
});
m_catalog_replies.append(reply);
}
}
void CheatRepository::Fetch(const RemoteFile& file) {
CancelFetch();
if (!file.IsSupported()) {
emit FileLoaded({}, tr("Only JSON cheat files are supported."));
return;
}
const auto generation = m_fetch_generation;
m_file_reply = Request(file.Url(), kFileLimit,
[this, generation](const QByteArray& data, const QString& error) {
if (generation == m_fetch_generation) {
emit FileLoaded(data, error);
}
});
}
void CheatRepository::CancelFetch() {
m_fetch_generation++;
if (m_file_reply != nullptr) {
auto* reply = m_file_reply.data();
m_file_reply.clear();
reply->abort();
}
}
} // namespace Cheats
+147
View File
@@ -0,0 +1,147 @@
#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(skip_notice_screen);
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();
KYTY_CFG_GET(skip_notice_screen);
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);
@@ -274,6 +276,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -354,6 +357,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);
@@ -428,6 +436,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
+159 -7
View File
@@ -1,5 +1,6 @@
#include "configurationListWidget.h"
#include "cheatFile.h"
#include "common/archive.h"
#include "compatibilityDatabase.h"
#include "configuration.h"
@@ -23,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>
@@ -39,6 +42,7 @@
#include <QProcess>
#include <QPushButton>
#include <QRegularExpression>
#include <QSaveFile>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
@@ -55,6 +59,7 @@
#endif
#include <memory>
#include <vector>
#include "ui_configuration_list_widget.h"
@@ -240,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,
@@ -292,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() {
@@ -760,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);
@@ -784,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();
@@ -805,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);
@@ -817,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() {
@@ -839,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) {
@@ -1015,17 +1167,17 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
}
});
action_patches->setVisible(item != nullptr &&
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
Cheats::IsSupportedTitleId(item->GetInfo().title_id));
QAction* action_remove_save_data =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
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();
+3 -2
View File
@@ -1,11 +1,11 @@
#include "mainDialog.h"
#include "cheatFile.h"
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include "controllerLightbar.h"
#include "gameContent.h"
#include "patchesDialog.h"
#include "updateChecker.h"
#include <QApplication>
@@ -246,6 +246,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
if (info.tessellation_enabled) {
args << "--tessellation";
}
@@ -285,7 +286,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--game" << game;
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
const auto patch_plan = Cheats::PlanPath(info.title_id);
if (QFileInfo::exists(patch_plan)) {
args << "--game-patch" << patch_plan;
}
+262 -93
View File
@@ -1,140 +1,309 @@
#include "patchesDialog.h"
#include "cheatFile.h"
#include "configuration.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QSplitter>
#include <QStringList>
#include <QTabWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <algorithm>
#include <utility>
namespace {
void ShowMods(QListWidget* list, const QJsonDocument& document, bool checkable) {
list->clear();
for (const auto& value: document.object().value(QStringLiteral("mods")).toArray()) {
const auto mod = value.toObject();
auto* item = new QListWidgetItem(mod.value(QStringLiteral("name")).toString(), list);
if (checkable) {
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(mod.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
: Qt::Unchecked);
}
}
}
QLabel* StatusLabel(QWidget* parent) {
auto* label = new QLabel(parent);
label->setWordWrap(true);
label->setTextFormat(Qt::PlainText);
return label;
}
} // namespace
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_version(game.gameVersion),
m_process(QFileInfo(game.elf.isEmpty() ? QStringLiteral("eboot.bin") : game.elf).fileName()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
resize(640, 480);
resize(800, 640);
auto* layout = new QVBoxLayout(this);
m_patches = new QListWidget(this);
m_status = new QLabel(this);
m_apply = new QPushButton(tr("Apply selection"), this);
auto* close = new QPushButton(tr("Close"), this);
auto* layout = new QVBoxLayout(this);
auto* game_label = StatusLabel(this);
game_label->setText(tr("%1 · Installed version: %2")
.arg(m_title_id, m_version.isEmpty() ? tr("Unknown") : m_version));
layout->addWidget(game_label);
m_tabs = new QTabWidget(this);
layout->addWidget(m_tabs);
m_status->setWordWrap(true);
layout->addWidget(m_patches);
layout->addWidget(m_status);
auto* remote = new QWidget(m_tabs);
auto* remote_layout = new QVBoxLayout(remote);
m_tabs->addTab(remote, tr("Remote"));
auto* credits = new QLabel(
tr("Cheats by <a href=\"https://github.com/TeeKay87/HEN-Cheats-Collection\">TeeKay87 / HEN "
"Cheats Collection</a> and its contributors."),
remote);
credits->setOpenExternalLinks(true);
credits->setWordWrap(true);
remote_layout->addWidget(credits);
auto* support = StatusLabel(remote);
support->setText(tr("Only JSON cheats are supported. MC4 and SHN files are shown disabled. "
"Select a JSON file to preview its cheats."));
remote_layout->addWidget(support);
auto* split = new QSplitter(Qt::Vertical, remote);
m_files = new QTreeWidget(split);
m_files->setHeaderLabels({tr("Version"), tr("Format"), tr("File")});
m_files->setRootIsDecorated(false);
m_files->setSelectionMode(QAbstractItemView::SingleSelection);
m_files->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(2, QHeaderView::Stretch);
m_preview = new QListWidget(split);
remote_layout->addWidget(split, 1);
m_remote_status = StatusLabel(remote);
m_preview_status = StatusLabel(remote);
remote_layout->addWidget(m_remote_status);
remote_layout->addWidget(m_preview_status);
auto* remote_buttons = new QDialogButtonBox(remote);
m_refresh = remote_buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
m_import = remote_buttons->addButton(tr("Import to Local"), QDialogButtonBox::ActionRole);
m_import->setEnabled(false);
remote_layout->addWidget(remote_buttons);
auto* buttons = new QDialogButtonBox(this);
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
buttons->addButton(close, QDialogButtonBox::RejectRole);
auto* local = new QWidget(m_tabs);
auto* local_layout = new QVBoxLayout(local);
m_tabs->addTab(local, tr("Local"));
m_patches = new QListWidget(local);
m_status = StatusLabel(local);
local_layout->addWidget(m_patches, 1);
local_layout->addWidget(m_status);
auto* local_buttons = new QDialogButtonBox(local);
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
local_layout->addWidget(local_buttons);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
connect(reload, &QPushButton::clicked, this, &PatchesDialog::Load);
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
connect(close, &QPushButton::clicked, this, &QDialog::close);
connect(m_refresh, &QPushButton::clicked, this, &PatchesDialog::LoadRemote);
connect(m_import, &QPushButton::clicked, this, &PatchesDialog::ImportRemote);
connect(m_files, &QTreeWidget::currentItemChanged, this, &PatchesDialog::PreviewRemote);
m_repository = new Cheats::CheatRepository(this);
connect(
m_repository, &Cheats::CheatRepository::CatalogLoaded, this,
[this](QList<Cheats::RemoteFile> files, const QString& error) {
std::stable_sort(files.begin(), files.end(), [this](const auto& a, const auto& b) {
return (a.version == m_version) > (b.version == m_version);
});
m_remote_files = std::move(files);
for (qsizetype index = 0; index < m_remote_files.size(); ++index) {
const auto& file = m_remote_files[index];
auto* item = new QTreeWidgetItem(
m_files, {file.version == m_version ? tr("%1 (installed)").arg(file.version)
: file.version,
file.format.toUpper(), file.name});
item->setData(0, Qt::UserRole, index);
for (int column = 0; column < 3; ++column) {
item->setToolTip(column, file.IsSupported()
? file.title
: tr("%1 — %2 is not supported.")
.arg(file.title, file.format.toUpper()));
}
if (!file.IsSupported()) {
item->setFlags(item->flags() & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
}
}
auto status =
m_remote_files.isEmpty() && error.isEmpty()
? tr("No remote cheats found for %1.").arg(m_title_id)
: tr("%1 remote file(s) for %2.").arg(m_remote_files.size()).arg(m_title_id);
if (!error.isEmpty()) {
status = tr("%1\nCould not load the complete collection: %2\nUse Refresh to retry.")
.arg(status, error);
}
m_remote_status->setText(status);
m_refresh->setEnabled(true);
});
connect(
m_repository, &Cheats::CheatRepository::FileLoaded, this,
[this](const QByteArray& data, QString error) {
if (error.isEmpty()) {
error = Cheats::ParseDocument(data, &m_remote_document);
}
if (!error.isEmpty()) {
m_preview_status->setText(
tr("Could not preview this file: %1\nSelect another file or refresh to retry.")
.arg(error));
return;
}
ShowMods(m_preview, m_remote_document, false);
error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
const auto root = m_remote_document.object();
QStringList creators;
for (const auto& value: root.value(QStringLiteral("credits")).toArray()) {
if (value.isString()) {
creators.append(value.toString());
}
}
const auto metadata =
tr("%1\nCredits: %2 · Version: %3")
.arg(root.value(QStringLiteral("name")).toString(),
creators.isEmpty() ? root.value(QStringLiteral("author")).toString()
: creators.join(QStringLiteral(", ")),
root.value(QStringLiteral("version")).toString());
m_preview_status->setText(
metadata + QStringLiteral("\n\n") +
(error.isEmpty() ? tr("Import to Local, then select the cheats to enable. Changes "
"take effect on the next game launch.")
: error));
m_import->setEnabled(error.isEmpty());
});
Load();
}
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
}
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
return QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
LoadRemote();
}
void PatchesDialog::Load() {
QFile file(PatchPlanPath(m_title_id));
m_patches->clear();
m_local_data.clear();
m_local_document = {};
m_local_readable = false;
m_apply->setEnabled(false);
const auto path = Cheats::PlanPath(m_title_id);
if (path.isEmpty()) {
m_status->setText(tr("Unsupported title ID."));
return;
}
QFile file(path);
if (!file.exists()) {
m_local_readable = true;
m_status->setText(tr("No local cheat file: %1").arg(path));
return;
}
if (!file.open(QIODevice::ReadOnly)) {
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
m_apply->setEnabled(false);
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_data = file.readAll();
if (file.error() != QFile::NoError) {
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto root = document.object();
if (!root.value(QStringLiteral("mods")).isArray()) {
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_readable = true;
const auto error = Cheats::ParseDocument(m_local_data, &m_local_document);
if (!error.isEmpty()) {
m_status->setText(tr("%1\n%2").arg(error, path));
return;
}
const auto mods = root.value(QStringLiteral("mods")).toArray();
for (const auto& value: mods) {
if (!value.isObject()) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
auto* item = new QListWidgetItem(name.toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
}
m_apply->setEnabled(!mods.isEmpty());
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
ShowMods(m_patches, m_local_document, true);
m_apply->setEnabled(m_patches->count() > 0);
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(m_patches->count()).arg(path));
}
void PatchesDialog::Save() {
const auto path = PatchPlanPath(m_title_id);
QFile input(path);
if (!input.open(QIODevice::ReadOnly)) {
QList<bool> enabled;
for (int index = 0; index < m_patches->count(); ++index) {
enabled.append(m_patches->item(index)->checkState() == Qt::Checked);
}
const auto document = Cheats::WithSelection(m_local_document, enabled);
const auto error = Cheats::SaveDocument(Cheats::PlanPath(m_title_id), document, m_local_data);
if (!error.isEmpty()) {
m_status->setText(error);
return;
}
m_local_document = document;
m_local_data = document.toJson();
m_status->setText(tr("Cheat selection saved. Changes take effect on the next game launch."));
}
auto document = QJsonDocument::fromJson(input.readAll());
input.close();
const auto changed_message = tr("Cheat file changed; reload the dialog.");
if (!document.isObject()) {
m_status->setText(changed_message);
void PatchesDialog::LoadRemote() {
m_repository->CancelFetch();
m_files->clear();
m_remote_files.clear();
m_preview->clear();
m_remote_document = {};
m_import->setEnabled(false);
m_refresh->setEnabled(false);
m_preview_status->clear();
m_remote_status->setText(tr("Loading remote cheats…"));
m_repository->Load(m_title_id);
}
void PatchesDialog::PreviewRemote() {
m_repository->CancelFetch();
m_preview->clear();
m_remote_document = {};
m_import->setEnabled(false);
m_preview_status->clear();
const auto* item = m_files->currentItem();
if (item == nullptr) {
return;
}
const auto index = item->data(0, Qt::UserRole).toInt();
if (index < 0 || index >= m_remote_files.size() || !m_remote_files[index].IsSupported()) {
return;
}
m_preview_status->setText(tr("Loading cheat preview…"));
m_repository->Fetch(m_remote_files[index]);
}
void PatchesDialog::ImportRemote() {
const auto error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
if (!error.isEmpty()) {
m_preview_status->setText(error);
return;
}
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != m_patches->count()) {
m_status->setText(changed_message);
if (!m_local_readable) {
m_preview_status->setText(
tr("Could not read the local cheat file. Open Local and reload it before importing."));
return;
}
for (int index = 0; index < mods.size(); index++) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
mods[index] = mod;
const auto path = Cheats::PlanPath(m_title_id);
if (QFile::exists(path) &&
QMessageBox::question(this, tr("Replace local cheats?"),
tr("Replace the local cheat file and selections for %1? Imported "
"cheats will start unchecked.")
.arg(m_title_id),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes) {
return;
}
root.insert(QStringLiteral("mods"), mods);
document.setObject(root);
QSaveFile output(path);
if (!output.open(QIODevice::WriteOnly) || output.write(document.toJson()) < 0 ||
!output.commit()) {
qWarning() << "Could not save cheat file:" << output.errorString();
m_status->setText(tr("Could not save cheat file: %1").arg(output.errorString()));
const auto mods = m_remote_document.object().value(QStringLiteral("mods")).toArray();
const auto document = Cheats::WithSelection(m_remote_document, QList<bool>(mods.size(), false));
const auto save_error = Cheats::SaveDocument(path, document, m_local_data);
if (!save_error.isEmpty()) {
m_preview_status->setText(save_error);
return;
}
m_status->setText(tr("Cheat selection saved."));
Load();
m_status->setText(
tr("Imported %1 cheat(s). Select cheats and apply your selection for the next game launch.")
.arg(mods.size()));
m_tabs->setCurrentIndex(1);
}
+350 -66
View File
@@ -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;
+6
View File
@@ -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);
}
+23 -5
View File
@@ -2388,6 +2388,12 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize() {
PRINT_NAME();
return 5u * sizeof(uint32_t);
}
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs) {
@@ -2529,7 +2535,6 @@ static uint64_t decode_indirect_modifier_patch_offsets(uint64_t modifier, bool i
if (indexed && (low & 0x2u) != 0) {
base_vtx_loc |= static_cast<uint64_t>(sgpr_base + extract_modifier_bits(low, 14u, 5u))
<< 16u;
base_vtx_loc |= 1ull << 59u;
}
return base_vtx_loc | (start_inst_loc << 32u);
@@ -2787,13 +2792,15 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t dat
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true);
// Single indexed draws store the index-offset enable in word 3, bit 28.
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true) |
((modifier & 0x2ull) << 59u);
cmd[0] = KYTY_PM4(5, Pm4::IT_DRAW_INDEX_INDIRECT, 0u);
cmd[1] = data_offset_in_bytes;
cmd[2] = static_cast<uint32_t>(patch_offsets);
cmd[3] = static_cast<uint32_t>(patch_offsets >> 32u);
cmd[4] = decode_indirect_draw_initiator(modifier);
cmd[4] = decode_draw_index_initiator(modifier);
return cmd;
}
@@ -2914,19 +2921,30 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirectMulti(CommandBuffer* buf,
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
const auto low = static_cast<uint32_t>(modifier);
const auto sgpr_base = indirect_modifier_sgpr_base(low);
const auto patch_offsets = decode_indirect_modifier_patch_offsets(modifier, true);
const auto count_vaddr = reinterpret_cast<uint64_t>(count_addr);
uint32_t draw_index_location = 0x280u;
if ((low & 0x8u) != 0) {
draw_index_location = sgpr_base + extract_modifier_bits(low, 24u, 5u);
}
// Multi indexed draws store the index-offset enable in word 4, bit 28.
const auto draw_control = draw_index_location | ((low & 0x10u) << 23u) |
((low & 0x2u) << 27u) | ((count_indirect & 0x1u) << 30u) |
((low & 0x8u) << 28u);
cmd[0] = KYTY_PM4(10, Pm4::IT_DRAW_INDEX_INDIRECT_MULTI, 0u);
cmd[1] = data_offset_in_bytes;
cmd[2] = static_cast<uint32_t>(patch_offsets);
cmd[3] = static_cast<uint32_t>(patch_offsets >> 32u);
cmd[4] = (count_indirect & 1u) << 30u;
cmd[4] = draw_control;
cmd[5] = max_count_or_count;
cmd[6] = static_cast<uint32_t>(count_vaddr) & ~0x3u;
cmd[7] = static_cast<uint32_t>(count_vaddr >> 32u);
cmd[8] = stride_in_bytes;
cmd[9] = decode_indirect_draw_initiator(modifier);
cmd[9] = decode_draw_index_initiator(modifier);
return cmd;
}
+1
View File
@@ -137,6 +137,7 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
uint64_t KYTY_SYSV_ABI AgcDcbSetShRegistersIndirectGetSize();
uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer* buf,
const volatile ShaderRegister* regs,
uint32_t num_regs);
+1
View File
@@ -95,6 +95,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("w4-d0n60hdo", Gen5::AgcDcbSetUcRegisterDirect);
LIB_FUNC("ZvwO9euwYzc", Gen5::AgcDcbSetCxRegistersIndirect);
LIB_FUNC("-HOOCn0JY48", Gen5::AgcDcbSetShRegistersIndirect);
LIB_FUNC("nNlUtdDDvZ0", Gen5::AgcDcbSetShRegistersIndirectGetSize);
LIB_FUNC("hvUfkUIQcOE", Gen5::AgcDcbSetUcRegistersIndirect);
LIB_FUNC("GIIW2J37e70", Gen5::AgcDcbSetIndexSize);
LIB_FUNC("l4fM9K-Lyks", Gen5::AgcDcbSetIndexBuffer);
+56 -29
View File
@@ -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);
+81 -25
View File
@@ -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);
@@ -599,34 +623,66 @@ static int KYTY_SYSV_ABI HttpUriBuild(char* out, size_t* require, size_t prepare
return HTTP_ERROR_INVALID_VALUE;
}
std::string uri;
if (src_element->scheme != nullptr) {
uri.append(src_element->scheme);
uri.push_back(':');
}
constexpr uint32_t BUILD_WITH_SCHEME = 0x01;
constexpr uint32_t BUILD_WITH_HOSTNAME = 0x02;
constexpr uint32_t BUILD_WITH_PORT = 0x04;
constexpr uint32_t BUILD_WITH_PATH = 0x08;
constexpr uint32_t BUILD_WITH_USERNAME = 0x10;
constexpr uint32_t BUILD_WITH_PASSWORD = 0x20;
constexpr uint32_t BUILD_WITH_QUERY = 0x40;
constexpr uint32_t BUILD_WITH_FRAGMENT = 0x80;
constexpr uint32_t BUILD_WITH_ALL = 0xff;
if (src_element->opaque == 0) {
if (src_element->hostname != nullptr) {
// No component bits preserves the legacy full-URI build.
const uint32_t parts = (option & BUILD_WITH_ALL) == 0 ? BUILD_WITH_ALL : option;
const bool hierarchical = src_element->opaque == 0 && src_element->hostname != nullptr;
const bool with_hostname = (parts & BUILD_WITH_HOSTNAME) != 0 && hierarchical;
std::string uri;
if ((parts & BUILD_WITH_SCHEME) != 0) {
if (src_element->scheme != nullptr) {
uri.append(src_element->scheme);
uri.push_back(':');
}
// Scheme-only builds include the authority marker.
if (hierarchical) {
uri.append("//");
if (src_element->username != nullptr) {
uri.append(src_element->username);
if (src_element->password != nullptr) {
uri.push_back(':');
uri.append(src_element->password);
}
uri.push_back('@');
}
uri.append(src_element->hostname);
if (src_element->port != 0) {
uri.push_back(':');
uri.append(std::to_string(src_element->port));
}
}
}
AppendUriPart(&uri, src_element->path);
AppendUriPart(&uri, src_element->query);
AppendUriPart(&uri, src_element->fragment);
const bool with_username = (parts & BUILD_WITH_USERNAME) != 0 && src_element->username != nullptr;
const bool with_password = (parts & BUILD_WITH_PASSWORD) != 0 && src_element->password != nullptr;
if (with_username) {
uri.append(src_element->username);
}
if (with_password) {
if (with_username) {
uri.push_back(':');
}
uri.append(src_element->password);
}
if (with_hostname) {
if (with_username || with_password) {
uri.push_back('@');
}
uri.append(src_element->hostname);
}
if ((parts & BUILD_WITH_PORT) != 0 && hierarchical && src_element->port != 0) {
if (with_hostname) {
uri.push_back(':');
}
uri.append(std::to_string(src_element->port));
}
if ((parts & BUILD_WITH_PATH) != 0) {
AppendUriPart(&uri, src_element->path);
}
if ((parts & BUILD_WITH_QUERY) != 0) {
AppendUriPart(&uri, src_element->query);
}
if ((parts & BUILD_WITH_FRAGMENT) != 0) {
AppendUriPart(&uri, src_element->fragment);
}
const auto needed = uri.size() + 1;
if (require != nullptr) {
+1 -1
View File
@@ -206,7 +206,7 @@ static int KYTY_SYSV_ABI SystemServiceGetNoticeScreenSkipFlag(bool* value) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
*value = false;
*value = Config::SkipNoticeScreen();
return OK;
}
+86 -49
View File
@@ -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
View File
@@ -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) {
-30
View File
@@ -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
-4
View File
@@ -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
+15 -8
View File
@@ -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
+8
View File
@@ -87,6 +87,9 @@ static void PrintUsage() {
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
::printf(
" --skip-notice-screen <true|false> Skip startup logos and notices in supported games.\n"
" Default: false.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
#endif
@@ -428,6 +431,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--skip-notice-screen") {
if (!ParseBool(value, options.config.skip_notice_screen)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+47
View File
@@ -28,6 +28,46 @@ void CheckConcurrentResult(int result, const char* text) {
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
}
void TestLevelUserEventDelivery() {
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
Check(EventQueue::KernelCreateEqueue(&queue, "level-user-events") == OK, "create user event queue");
Check(EventQueue::KernelAddUserEvent(queue, 1) == OK &&
EventQueue::KernelAddUserEvent(queue, 2) == OK &&
EventQueue::KernelAddUserEventEdge(queue, 3) == OK, "add level and edge events");
for (int id = 1; id <= 3; ++id)
Check(EventQueue::KernelTriggerUserEvent(queue, id, reinterpret_cast<void*>(uintptr_t(id))) == OK,
"trigger distinct user events");
EventQueue::KernelEvent events[16] {};
Libs::LibKernel::KernelUseconds timeout = 0;
int out = 0;
const auto find_event = [&](uintptr_t id) {
return std::find_if(events, events + out, [=](const auto& event) { return event.ident == id; });
};
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 3,
"level events are returned once per wait without starving following events");
for (uintptr_t id = 1; id <= 3; ++id) {
const auto* event = find_event(id);
Check(event != events + out && event->data == static_cast<intptr_t>(id) &&
event->udata == reinterpret_cast<void*>(id), "distinct event payloads");
}
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 2 &&
find_event(1) != events + out && find_event(2) != events + out,
"level persists while edge clears");
Check(EventQueue::KernelDeleteUserEvent(queue, 1) == OK, "delete first level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 2, "remaining level event is delivered only once");
Check(EventQueue::KernelDeleteUserEvent(queue, 2) == OK, "delete remaining level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"deleted levels and consumed edge do not reappear");
Check(EventQueue::KernelTriggerUserEvent(queue, 3, nullptr) == OK, "retrigger edge event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 3, "retriggered edge is delivered once");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"retriggered edge clears after delivery");
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete user event queue");
}
void TestPeriodicTimerEvents() {
using namespace std::chrono_literals;
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
@@ -188,6 +228,12 @@ void TestDuplicateAddPreservesEventState() {
"duplicate add updates deadline metadata");
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
"deadline trigger retains updated duplicate metadata");
for (uintptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
reinterpret_cast<void*>(value)) == OK, "queue bounded read payload");
for (intptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelWaitEqueue(queue, events, 1, &out, &timeout) == OK && out == 1 &&
events[0].data == value, "capacity-one reads preserve pending event order");
auto retained_owner = weak_original.lock();
Check(retained_owner != nullptr, "original owner alive before delete");
@@ -495,6 +541,7 @@ void TestConcurrentDelete() {
} // namespace
int main() {
TestLevelUserEventDelivery();
TestPeriodicTimerEvents();
TestDuplicateAddPreservesEventState();
TestCallbackStateOutlivesPort();
+58 -9
View File
@@ -6,6 +6,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
namespace Libs::LibHttp {
void InitNet_1_Http(Loader::SymbolDatabase *symbols);
@@ -40,14 +41,14 @@ int failures = 0;
using HttpUriParse = int(KYTY_SYSV_ABI *)(SceHttpUriElement *, const char *,
void *, size_t *, size_t);
HttpUriParse GetHttpUriParse() {
Loader::SymbolDatabase symbols;
Libs::LibHttp::InitNet_1_Http(&symbols);
const auto *record =
symbols.FindByNid("IWalAn-guFs", Loader::SymbolType::Func);
using HttpUriBuild = int(KYTY_SYSV_ABI *)(char *, size_t *, size_t,
const SceHttpUriElement *, uint32_t);
template <typename T>
T GetHttpFunction(const Loader::SymbolDatabase &symbols, const char *nid) {
const auto *record = symbols.FindByNid(nid, Loader::SymbolType::Func);
CHECK(record != nullptr);
return record != nullptr ? reinterpret_cast<HttpUriParse>(record->vaddr)
: nullptr;
return record != nullptr ? reinterpret_cast<T>(record->vaddr) : nullptr;
}
template <size_t N>
@@ -117,13 +118,61 @@ void TestPresentQuery(HttpUriParse parse) {
}
}
std::string BuildWith(HttpUriBuild build, const SceHttpUriElement &element,
uint32_t option) {
size_t required = 0;
CHECK(build(nullptr, &required, 0, &element, option) == 0);
std::array<char, 256> out{};
size_t filled = 0;
CHECK(required <= out.size());
CHECK(build(out.data(), &filled, required, &element, option) == 0);
CHECK(filled == required);
return std::string(out.data());
}
void TestBuildHonoursOption(HttpUriParse parse, HttpUriBuild build) {
constexpr char url[] =
"https://user:pw@gssdk1.gamesci.com.cn:8443/VersionServerImpl?x=1#frag";
size_t required = 0;
CHECK(parse(nullptr, url, nullptr, &required, 0) == 0);
std::array<char, 256> pool{};
SceHttpUriElement element{};
CHECK(parse(&element, url, pool.data(), &required, required) == 0);
constexpr uint32_t scheme = 0x01, hostname = 0x02, port = 0x04, path = 0x08,
username = 0x10, password = 0x20, query = 0x40,
fragment = 0x80;
CHECK(BuildWith(build, element, scheme) == "https://");
CHECK(BuildWith(build, element, hostname) == "gssdk1.gamesci.com.cn");
CHECK(BuildWith(build, element, port) == "8443");
CHECK(BuildWith(build, element, path) == "/VersionServerImpl");
CHECK(BuildWith(build, element, username) == "user");
CHECK(BuildWith(build, element, password) == "pw");
CHECK(BuildWith(build, element, query) == "?x=1");
CHECK(BuildWith(build, element, fragment) == "#frag");
CHECK(BuildWith(build, element, scheme | hostname) ==
"https://gssdk1.gamesci.com.cn");
CHECK(BuildWith(build, element, hostname | path) ==
"gssdk1.gamesci.com.cn/VersionServerImpl");
CHECK(BuildWith(build, element, hostname | port) ==
"gssdk1.gamesci.com.cn:8443");
CHECK(BuildWith(build, element, 0xff) == url);
CHECK(BuildWith(build, element, 0) == url);
}
} // namespace
int main() {
const auto parse = GetHttpUriParse();
if (parse == nullptr) {
Loader::SymbolDatabase symbols;
Libs::LibHttp::InitNet_1_Http(&symbols);
const auto parse = GetHttpFunction<HttpUriParse>(symbols, "IWalAn-guFs");
const auto build = GetHttpFunction<HttpUriBuild>(symbols, "5LZA+KPISVA");
if (parse == nullptr || build == nullptr) {
return 1;
}
TestBuildHonoursOption(parse, build);
TestAbsentQuery(parse);
TestEmptyUri(parse);
TestPresentQuery(parse);
+191 -7
View File
@@ -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)
+289 -352
View File
@@ -206,10 +206,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
}
std::unique_ptr<Fixture>
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
bool memory_backed_material = false, uint32_t member_offset = 0,
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
uint32_t table_stride = 32, uint32_t table_offset = 0) {
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
uint32_t table_shader_offset = 0, bool nested = false) {
auto fixture = std::make_unique<Fixture>();
std::array<Value, 4> material_words;
std::array<Value, 4> heap_words;
@@ -217,54 +215,38 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
material_words[dword] = fixture->UserData(dword);
heap_words[dword] = fixture->UserData(dword + 4u);
}
if (memory_backed_material) {
const auto pointer_address =
fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0);
MemoryInfo pointer_word;
pointer_word.kind = ResourceKind::ScalarAddress;
const auto pointer =
fixture->Emit(ValueOpcode::LoadAddressU32,
{pointer_address, Value(0u), Value(0u), Value(true)},
fixture->AddMemory(pointer_word, 0x10b0));
const auto address = fixture->Address(pointer, Value(0u), 0x10c0);
MemoryInfo descriptor_word;
descriptor_word.kind = ResourceKind::ScalarAddress;
material_words[0] =
fixture->Emit(ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)},
fixture->AddMemory(descriptor_word, 0x10c0));
}
const auto material = fixture->Buffer(material_words, 0x10d8);
const auto heap = fixture->Buffer(heap_words, 0x10d8);
if (memory_backed_material) {
MemoryInfo shared_buffer;
shared_buffer.kind = ResourceKind::Buffer;
const auto load =
fixture->Emit(ValueOpcode::LoadBufferU32,
{material, Value(0u), Value(0u), Value(0u), Value(true)},
fixture->AddMemory(shared_buffer, 0x10d8));
fixture->Emit(ValueOpcode::ReferenceU32, {load});
}
const auto invocation = fixture->Emit(
ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
{invocation, Value(true)});
const auto record =
fixture->Emit(ValueOpcode::IMul32, {selector, Value(material_stride)});
const auto member = member_offset == 0 ? record :
fixture->Emit(ValueOpcode::IAdd32, {record, Value(member_offset)});
Value member;
if (nested) {
std::array<Value, 4> root_words;
for (uint32_t word = 0; word < 4u; ++word) root_words[word] = fixture->UserData(12u + word);
const auto root = fixture->Buffer(root_words);
MemoryInfo root_memory;
root_memory.kind = ResourceKind::ScalarBuffer;
const auto root_offset = fixture->Emit(ValueOpcode::IMul32, {selector, Value(32u)});
const auto pointer = fixture->Emit(ValueOpcode::ReadConstBuffer, {root, root_offset},
fixture->AddMemory(root_memory, 0x1000));
const auto masked = fixture->Emit(ValueOpcode::BitwiseAnd32, {pointer, Value(0xfffffff1u)});
member = fixture->Emit(ValueOpcode::IAdd32, {masked, Value(16u)});
} else {
member = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
}
fixture->Emit(ValueOpcode::ReferenceU32, {member});
MemoryInfo material_scalar;
material_scalar.kind = ResourceKind::ScalarBuffer;
material_scalar.offset = material_immediate;
auto key =
material_scalar.offset = 4u;
const auto key =
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
fixture->AddMemory(material_scalar, 0x10d8));
if (selector_bits != UINT32_MAX)
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
const auto heap_offset =
fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
if (table_shader_offset != 0u)
heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)});
std::array<Value, 8> image_words;
MemoryInfo heap_scalar;
heap_scalar.kind = ResourceKind::ScalarBuffer;
@@ -278,6 +260,10 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset},
fixture->AddMemory(component, 0x10d8));
}
if (nested) {
const auto field = fixture->Emit(ValueOpcode::BitwiseAnd32, {image_words[4], Value(7u)});
fixture->Emit(ValueOpcode::ReferenceU32, {field});
}
const auto image = fixture->Image(image_words, 0x10f0);
const auto sampler =
fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}, 0x10f0);
@@ -294,331 +280,280 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
}
void TestInvariantIndirectImageMaterialization() {
auto fixture = MakeIndirectImageFixture(false);
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u, true);
fixture->PlanAndTrack();
auto resource_plan = ExtractResourcePlan(fixture->program);
auto plan = ExtractResourcePlan(fixture->program);
EliminateDeadCode(fixture->program.blocks);
ValidateProgram(fixture->program, true);
Check(fixture->program.info.buffers.size() == 1 &&
fixture->program.info.images.size() == 1 &&
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
}),
"indirect image key was not retained as a scalar-buffer read");
const auto source = fixture->program.info.images[0].source;
Check(source < fixture->program.descriptor_sources.size() &&
fixture->program.descriptor_sources[source]
.indirect_descriptor.has_value(),
"indirect image source was not retained for runtime proof");
const auto image_handle =
std::ranges::find_if(*fixture->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
Check(image_handle != fixture->block->end() &&
image_handle->Arg(0).ResolveInstruction() != nullptr &&
image_handle->Arg(0).ResolveInstruction()->GetOpcode() ==
ValueOpcode::ReadConstBuffer,
"indirect image handle discarded the live material key");
std::array<uint32_t, 9> user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u,
16u << 16u, 4u, 0u, 7u};
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(!source.selector && fixture->program.info.buffers.size() == 3u,
"bounded image table retained CPU selector traversal or lost live loads");
std::array<uint32_t, 8> descriptor{};
descriptor[0] = 0x20u;
descriptor[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
descriptor[2] = 3u | (3u << 14u);
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
std::array<uint32_t, 16> data{0x3000u, 16u << 16u, 0x200000u, 0u,
0x2000u, 48u << 16u, 3u, 0u};
std::copy_n(data.begin(), 4u, data.begin() + 12u);
LinearTestMemory memory;
std::array<uint32_t, 8> image_descriptor{};
image_descriptor[0] = 0x20u;
image_descriptor[1] =
static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
<< 20u;
image_descriptor[2] = 3u | (3u << 14u);
image_descriptor[3] =
Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
<< 28u);
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
SrtRuntime runtime{.user_data = user_data,
.userdata = &memory,
memory.fail_address = 0x3000u; // GPU-only material backing must never be read on the CPU.
memory.watched_address = 0x2000u;
const auto put = [&](uint32_t offset, uint32_t identity) {
auto value = descriptor;
value[0] += identity;
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1000u + offset) / 4u);
};
put(16u, 0u);
put(64u, 1u);
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1 &&
std::equal(image_descriptor.begin(), image_descriptor.end(),
snapshot.images[0].dwords.begin()),
"invariant indirect image table did not materialize");
user_data[5] = 0u;
user_data[6] = 19u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 3u && memory.watched_reads == 1u &&
memory.watched_dwords == 36u && memory.reads == 1u,
"image table was not read once independently of its GPU selector chain");
const auto root = specialization.images[0];
const auto mapping = root.indirect_mapping_offset;
Check(root.indirect_search_iterations == 0u && snapshot.flattened_srt[mapping] == 17u &&
snapshot.flattened_srt[mapping + 1u] == 0u &&
snapshot.flattened_srt[mapping + 5u] == 1u &&
snapshot.flattened_srt[mapping + 17u] == 2u &&
snapshot.specialization_reads == std::vector<std::pair<uint64_t, uint64_t>>{{0x2000u, 144u}},
"dense image mapping lost its null slot, trimmed length or coherent read dependency");
const auto original_specialization = specialization;
std::fill_n(memory.words.begin() + 0x1040u / 4u, 8u, 0u);
put(112u, 1u);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
specialization == original_specialization && memory.watched_reads == 2u &&
snapshot.flattened_srt[mapping] == 29u,
"moving a table entry changed shader specialization or reused stale mapping bytes");
put(112u, 7u);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images[2].dwords[0] == descriptor[0] + 7u,
"changed table descriptor was not refreshed");
// Scalar reads align each DWORD and return zero beyond the V# byte size.
data[5] = 0u;
data[6] = 19u;
std::fill(memory.words.begin() + 0x1000u / 4u, memory.words.end(), 0u);
put(0u, 0u);
memory.fail_address = 0x2010u;
memory.watched_address = 0x2000u;
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
memory.watched_dwords == 4u &&
std::equal(image_descriptor.begin(), image_descriptor.end(),
snapshot.images[0].dwords.begin()),
"partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail");
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.watched_dwords == 4u && snapshot.images.size() == 2u &&
snapshot.images[1].dwords == descriptor,
"partial descriptor prefix did not zero its out-of-bounds DWORDs");
memory.fail_address = 0x2008u;
Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization),
"unreadable memory inside the descriptor prefix was accepted");
user_data[5] = 16u << 16u;
user_data[6] = 4u;
memory.watched_address = UINT64_MAX;
memory.fail_address = 0x1004u;
Check(!MaterializeResources(resource_plan, runtime, snapshot,
specialization),
"unreadable material-table selector was accepted");
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable in-range table bytes were accepted");
memory.fail_address = UINT64_MAX;
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] = 0u;
memory.words[(0x2020u - memory.base) / 4u + dword] = 0u;
for (uint32_t size : {0u, 3u, 4u}) {
data[6] = size;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1u &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage &&
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }),
"empty image table did not specialize as a safe null image");
}
memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1];
memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3];
memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1];
memory.words[(0x2020u - memory.base) / 4u + 3u] =
image_descriptor[3] ^ (1u << 28u);
ResourceSnapshot null_snapshot;
ResourceSpecialization null_specialization;
Check(MaterializeResources(resource_plan, runtime, null_snapshot,
null_specialization) &&
std::ranges::all_of(null_snapshot.images[0].dwords,
[](uint32_t dword) { return dword == 0u; }),
"stale typed null image descriptors were not canonicalized");
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
// The GPU computes these offsets; host materialization must not enumerate the key.
for (const auto [shader, immediate, key, expected] :
{std::array{16u, 0u, 0x55555555u, 0u},
std::array{0u, 16u, 0x55555555u, UINT32_MAX},
std::array{0xfffffff0u, 32u, 0u, UINT32_MAX}}) {
auto wrap = MakeIndirectImageFixture(false, 48u, immediate, shader);
wrap->PlanAndTrack();
for (auto &inst : *wrap->block) {
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
inst.ReplaceUsesWith(Value(key));
break;
}
}
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
SrtWalker walker(wrap->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
}
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
ResourceSnapshot dynamic_snapshot;
ResourceSpecialization dynamic_specialization;
Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot,
dynamic_specialization) &&
dynamic_snapshot.images.size() == 2 &&
dynamic_specialization.images.size() == 2,
"dynamic indirect image table did not materialize");
const auto second_image = (0x2020u - memory.base) / 4u;
memory.words[second_image + 1u] |= 3u << 30u;
memory.words[second_image + 2u] = 3u << 14u;
memory.words[second_image + 3u] =
Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
memory.words[second_image + 4u] = 11u;
ResourceSnapshot mixed_snapshot;
ResourceSpecialization mixed_specialization;
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization) &&
mixed_snapshot.images.size() == 2 &&
mixed_specialization.images.size() == 2 &&
mixed_specialization.images[0].dimension ==
Decoder::ImageDimension::Dim2D &&
!mixed_specialization.images[0].cube &&
mixed_specialization.images[1].dimension ==
Decoder::ImageDimension::Dim2DArray &&
mixed_specialization.images[1].cube &&
std::equal(mixed_snapshot.images[1].dwords.begin(),
mixed_snapshot.images[1].dwords.end(),
memory.words.begin() + second_image),
"mixed 2D and cube candidates were rejected or discarded");
memory.words[second_image + 1u] =
static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
<< 20u |
(3u << 30u);
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization),
"indirect images with different numeric classes were accepted");
for (const auto dword : {1u, 2u, 3u, 4u}) {
memory.words[second_image + dword] = image_descriptor[dword];
}
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
Check(fixture->program.info.images.size() == 2 &&
fixture->program.info.images[0].indirect_root == 0 &&
fixture->program.info.images[0].indirect_search_iterations != 0 &&
fixture->program.info.images[0].indirect_resources.size() == 2 &&
dynamic_snapshot.images.size() == 2,
"dynamic indirect image table was not specialized transactionally");
const auto &mapping = dynamic_specialization.images[0];
const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset];
Check(mapping.indirect_search_iterations == std::bit_width(key_count) &&
mapping.indirect_mapping_offset + 1u + key_count * 2u ==
dynamic_snapshot.flattened_srt.size(),
"indirect image mapping retained worst-case padding");
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
memory.words[(0x2020u - memory.base) / 4u] += 0x101u;
ResourceSnapshot rebound_snapshot;
ResourceSpecialization rebound_specialization;
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization == dynamic_specialization,
"stable indirect key mapping did not accept changed image addresses");
memory.words[(0x2020u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u];
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization != dynamic_specialization,
"collapsed indirect candidates did not select a new specialization");
const auto collapsed_specialization = rebound_specialization;
ResourceSnapshot capacity_snapshot;
ResourceSpecialization capacity_specialization;
for (const uint32_t records : {1u, 3u}) {
user_data[2] = records;
Check(MaterializeResources(resource_plan, runtime, capacity_snapshot,
capacity_specialization),
"runtime indirect key mapping rejected a valid material-table size");
}
user_data[2] = 2u;
memory.words[(0x2020u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u] + 1u;
memory.words[(0x2040u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u] + 2u;
for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) {
memory.words[(0x2040u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u;
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization != collapsed_specialization,
"larger indirect candidate topology reused the old specialization");
auto memory_backed = MakeIndirectImageFixture(false, 4u, true);
memory_backed->PlanAndTrack();
auto memory_backed_plan = ExtractResourcePlan(memory_backed->program);
EliminateDeadCode(memory_backed->program.blocks);
std::array<uint32_t, 11> memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u,
0x2000u, 16u << 16u, 4u, 0u,
7u, 0x3100u, 0u};
memory.words[(0x3100u - memory.base) / 4u] = 0x3000u;
memory.words[(0x3000u - memory.base) / 4u] = 0x1000u;
memory.fail_address = 0x3100u;
SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data,
.userdata = &memory,
.read_specialization_memory =
ReadLinearTestMemory};
Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization),
"unreadable indirect table descriptor was accepted");
memory.fail_address = UINT64_MAX;
memory.watched_address = 0x3100u;
memory.watched_reads = 0;
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization) && memory.watched_reads == 1u,
"descriptor, flattened SRT and indirect-table roots repeated a clean pointer read");
const auto* buffer_storage = snapshot.buffers.data();
const auto* image_storage = snapshot.images.data();
const auto* flat_storage = snapshot.flattened_srt.data();
const auto* user_data_storage = snapshot.user_data.data();
const auto* buffer_specialization_storage = specialization.buffers.data();
const auto* image_specialization_storage = specialization.images.data();
const auto old_address = snapshot.images[0].dwords[0];
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
memory.watched_reads = 0;
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization) && memory.watched_reads == 1u &&
snapshot.images[0].dwords[0] == old_address + 0x100u,
"cache refresh reused stale table contents or repeated its clean pointer read");
Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage &&
snapshot.flattened_srt.data() == flat_storage &&
snapshot.user_data.data() == user_data_storage &&
specialization.buffers.data() == buffer_specialization_storage &&
specialization.images.data() == image_specialization_storage,
"a same-shape refresh discarded the runtime output storage");
auto malformed = MakeIndirectImageFixture(true);
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
"malformed indirect image pattern was accepted");
Check(!malformed->program.resource_tracking_complete &&
malformed->program.info.images.empty() &&
malformed->program.descriptor_sources.empty(),
"malformed indirect image pattern was partially accepted");
"noncontiguous descriptor DWORDs entered the table plan");
Check(!malformed->program.resource_tracking_complete && malformed->program.descriptor_sources.empty(),
"malformed image table tracking was not transactional");
}
auto negative_immediate = MakeIndirectImageFixture(false, 0xfffffffcu);
CheckFatal([&] { negative_immediate->PlanAndTrack(); },
"not a valid runtime value",
"negative scalar immediate entered the indirect image proof");
for (const auto [immediate, member, first, stride] :
{std::array{0u, 4u, 4u, 224u}, std::array{4u, 4u, 8u, 224u},
std::array{36u, 0u, 36u, 224u}, std::array{1u, 3u, 0u, 224u},
std::array{1u, 3u, 0u, 1u}, std::array{1u, 3u, 0u, 2u}}) {
auto split_offset = MakeIndirectImageFixture(false, immediate, false, member, stride);
split_offset->PlanAndTrack();
const auto split_plan = ExtractResourcePlan(split_offset->program);
LinearTestMemory split_memory;
std::copy(image_descriptor.begin(), image_descriptor.end(),
split_memory.words.begin() + 0x1000u / 4u);
split_memory.fail_address = first == 36u ? 0x1004u : UINT64_MAX;
split_memory.watched_address = split_memory.base + first;
user_data[1] = stride << 16u;
user_data[2] = stride < 4u ? 8u / stride : 1u;
SrtRuntime split_runtime{.user_data = user_data,
.userdata = &split_memory,
.read_specialization_memory = ReadLinearTestMemory};
Check(MaterializeResources(split_plan, split_runtime, snapshot, specialization) &&
snapshot.images.size() == 1 &&
snapshot.images[0].dwords == image_descriptor &&
split_memory.watched_reads == 1u,
"indirect scalar offsets did not align and bound their components independently");
void TestBoundedImageViewEligibility() {
using Type = Libs::Graphics::Prospero::ImageType;
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
fixture->PlanAndTrack();
auto plan = ExtractResourcePlan(fixture->program);
std::array<uint32_t, 8> data{0x3000u, 16u << 16u, 1u, 0u,
0x2000u, 48u << 16u, 5u, 0u};
LinearTestMemory memory;
memory.fail_address = 0x3000u;
const auto descriptor = [](uint32_t identity, Type type) {
return std::array<uint32_t, 8>{identity, 75u << 20u, 3u | (3u << 14u),
Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast<uint32_t>(type) << 28u),
0u, 0u, 0u, 0u};
};
const std::array types{Type::kColor2D, Type::kCube, Type::kColor3D,
Type::kColor2DArray, Type::kColor2D};
for (uint32_t row = 0; row < types.size(); ++row) {
const auto value = descriptor(0x20u + row, types[row]);
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1010u + row * 48u) / 4u);
}
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 4u && memory.reads == 1u,
"unrelated cube/3D heap entries prevented a typed 2D table view");
const auto mapping = specialization.images[0].indirect_mapping_offset;
Check(snapshot.flattened_srt[mapping] == 53u &&
snapshot.flattened_srt[mapping + 5u] == 1u &&
snapshot.flattened_srt[mapping + 17u] == 0u &&
snapshot.flattened_srt[mapping + 29u] == 0u &&
snapshot.flattened_srt[mapping + 41u] == 2u &&
snapshot.flattened_srt[mapping + 53u] == 3u &&
snapshot.images[1].dwords == descriptor(0x20u, Type::kColor2D) &&
snapshot.images[2].dwords == descriptor(0x23u, Type::kColor2DArray) &&
snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D),
"typed table eligibility changed byte offsets or discarded compatible views");
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
// each 64-byte material record; the image occupies bytes 16..47 of its record.
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
packed->PlanAndTrack();
const auto packed_plan = ExtractResourcePlan(packed->program);
LinearTestMemory packed_memory;
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
0x2000u, 48u << 16u, 2u, 0u};
for (uint32_t i = 0; i < 3u; ++i)
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
for (uint32_t i = 0; i < 2u; ++i) {
auto descriptor = image_descriptor;
descriptor[0] += i;
std::copy(descriptor.begin(), descriptor.end(),
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
// An explicitly selected descriptor is not an unrelated entry in a broad heap.
const auto image_source = plan.info.images[0].source;
std::vector<uint32_t> selected;
for (auto type : {Type::kColor2D, Type::kColor3D}) {
DescriptorSource source;
source.dword_count = 8u;
const auto value = descriptor(0x30u, type);
for (uint32_t word = 0; word < value.size(); ++word) source.dwords[word] = Value(value[word]);
selected.push_back(static_cast<uint32_t>(plan.descriptor_sources.size()));
plan.descriptor_sources.push_back(source);
}
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
.read_specialization_memory = ReadLinearTestMemory};
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
"packed material flags changed the table key or 48-byte descriptor addressing");
packed_memory.words[60u / 4u] = 0xffff8002u;
packed_memory.fail_address = 0x2070u;
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
snapshot.images.size() == 3u &&
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
"masked out-of-range descriptor did not return zero without reading past table bounds");
packed_memory.fail_address = 0x205cu;
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
UINT32_MAX, 48u, 16u);
CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value",
"unbounded table offset conflated shader U32 wrap with scalar immediate addition");
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicitly selected incompatible descriptor was silently normalized to null");
}
void TestWaterfallImageTable() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
const auto make = [](bool equality, bool scalar) {
auto fixture = std::make_unique<Fixture>();
auto *entry = fixture->block;
auto *body = fixture->AddBlock();
auto *exit = fixture->AddBlock();
entry->AddBranch(body);
entry->AddBranch(exit);
body->AddBranch(exit);
std::array<Value, 4> words;
for (uint32_t i = 0; i < 4u; ++i) words[i] = fixture->UserData(i);
const auto table = fixture->Buffer(words);
const auto lane_value = fixture->Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)});
const auto active = fixture->Emit(ValueOpcode::INotEqual32, {lane_value, Value(0u)});
const auto index = fixture->UserData(4u);
std::array<Value, 8> image_words;
auto guard = active;
for (uint32_t group = 0; group < 2u; ++group) {
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
memory.idxen = true;
memory.data_dwords = 4u;
memory.offset = 16u + group * 16u;
const auto loaded = fixture->Emit(ValueOpcode::LoadBufferU32x4,
{table, index, Value(0u), Value(0u), active}, fixture->AddMemory(memory, 0x850u));
for (uint32_t component = 0; component < 4u; ++component) {
const auto word = group * 4u + component;
const auto local = fixture->Emit(ValueOpcode::SelectU32,
{active, fixture->Emit(ValueOpcode::CompositeExtractU32x4,
{loaded, Value(component)}), Value(0xdeadu)});
image_words[word] = fixture->Emit(ValueOpcode::ReadLane, {local, Value(0u)});
if (equality || word != 7u)
guard = fixture->Emit(ValueOpcode::LogicalAnd,
{guard, fixture->Emit(ValueOpcode::IEqual32, {image_words[word], local})});
}
}
fixture->program.block_info[0].condition = guard;
fixture->program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u};
const auto sample = [&](Value image, Block *block) {
const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
fixture->Emit(ValueOpcode::ReferenceU32,
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
};
if (scalar) {
const auto key = fixture->Emit(ValueOpcode::ReadFirstLane, {lane_value, active});
const auto offset = fixture->Emit(ValueOpcode::IAdd32,
{fixture->Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(16u)});
std::array<Value, 8> scalar_words;
for (uint32_t word = 0; word < 8u; ++word) {
MemoryInfo memory;
memory.kind = ResourceKind::ScalarBuffer;
memory.offset = word * 4u;
scalar_words[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
fixture->AddMemory(memory, 0x7bcu));
}
sample(fixture->Image(scalar_words), entry);
}
const auto image = fixture->Emit(ValueOpcode::GetImageResource,
{image_words[0], image_words[1], image_words[2], image_words[3],
image_words[4], image_words[5], image_words[6], image_words[7]}, 0, body);
sample(image, body);
return fixture;
};
auto fixture = make(true, true);
fixture->PlanAndTrack();
auto plan = ExtractResourcePlan(fixture->program);
Check(plan.info.images.size() == 1u &&
plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor->table_record_bytes == 48u,
"scalar and waterfall uses did not share their native image table");
std::array<uint32_t, 5> data{0x2000u, 48u << 16u, 2u, 0x5204u, 1u};
LinearTestMemory memory;
const std::array<uint32_t, 8> descriptor{0x20u, 75u << 20u, 3u | (3u << 14u),
Libs::Graphics::DstSel(4, 5, 6, 7) | (9u << 28u), 0u, 0u, 0u, 0u};
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1u &&
snapshot.images.size() == 2u,
"shared scalar/waterfall table repeated its coherent bulk read");
const auto handle = std::ranges::find_if(*fixture->program.blocks[1], [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
for (const auto [index, format, expected] :
{std::array{1u, 0x5204u, 64u}, std::array{2u, 0x5204u, UINT32_MAX},
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
data[4] = index;
data[3] = format;
SrtWalker walker(fixture->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
"VMEM table projection lost record bounds, format disable or multiplication carry");
}
auto disabled = make(true, false);
disabled->PlanAndTrack();
const auto disabled_plan = ExtractResourcePlan(disabled->program);
data[3] = 0u;
memory.fail_address = 0x2000u;
Check(MaterializeResources(disabled_plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1u &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
"disabled VMEM descriptor table read its unavailable backing");
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"VMEM format disable incorrectly disabled a shared scalar descriptor read");
auto malformed = make(false, false);
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
"waterfall without all descriptor equality witnesses was accepted");
}
void TestGuardedDirectImageTable() {
@@ -709,8 +644,8 @@ void TestGuardedDirectImageTable() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->material_source == UINT32_MAX &&
indirect->selector_stride == 0u && indirect->table_offset == 344u &&
Check(indirect && !indirect->selector.has_value() &&
indirect->table_offset == 344u &&
indirect->key_count.Resolve().IsImmediate() &&
indirect->key_count.Resolve().U32() == 32u &&
fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u,
@@ -1007,7 +942,7 @@ void TestBoundedComputeImageLoop() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->material_source == UINT32_MAX &&
Check(indirect && !indirect->selector.has_value() &&
indirect->table_offset == 0x6b0u &&
indirect->key_count.Resolve() == count.Resolve(),
"bounded compute loop lost its runtime image count");
@@ -1362,8 +1297,8 @@ void TestUniformizedMaterialImageKeys() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->selector_stride == 0x90u &&
indirect->selector_offset == 0xc00u &&
Check(indirect && indirect->selector->stride == 0x90u &&
indirect->selector->offset == 0xc00u &&
indirect->table_offset == 0x20e0u &&
!indirect->selector_mask.IsEmpty() &&
indirect->key_count.Resolve().IsImmediate() &&
@@ -3637,6 +3572,8 @@ int main() {
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
Run("dynamic storage mips", TestDynamicStorageMipTracking);
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
Run("waterfall image table", TestWaterfallImageTable);
Run("guarded direct image table", TestGuardedDirectImageTable);
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
File diff suppressed because it is too large. Load diff
+261
View File
@@ -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;
}
+227
View File
@@ -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;
}