mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 16:02:37 +00:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cfe5a2f7 | ||
|
|
7415febd93 | ||
|
|
7492cb9577 | ||
|
|
60e53a39cc | ||
|
|
1b21161aac | ||
|
|
1195af2510 | ||
|
|
9a5658c038 | ||
|
|
199d0a9d92 | ||
|
|
e679486ed3 | ||
|
|
6ef406542c | ||
|
|
35c6e8fff6 | ||
|
|
4852721005 | ||
|
|
fb7ff7ec03 | ||
|
|
7d2422abbe | ||
|
|
b929bbe3e0 | ||
|
|
ae1e92621d | ||
|
|
5a491abbda | ||
|
|
afbb322b32 | ||
|
|
5601861d67 | ||
|
|
cd06601022 | ||
|
|
c23de62184 | ||
|
|
9caec9f2c8 | ||
|
|
cdb64bfd4f | ||
|
|
e5dcfdb87b | ||
|
|
984010227f | ||
|
|
57c97ebd86 | ||
|
|
dc3cd2d2c9 | ||
|
|
e6fce45d3a | ||
|
|
5b826c6e4a | ||
|
|
77d7722173 | ||
|
|
266c12cec6 | ||
|
|
7a0e1dafd7 | ||
|
|
1d41ddb378 | ||
|
|
5c83060481 | ||
|
|
72e4989b1e | ||
|
|
1d55272467 | ||
|
|
2b1b7ed0e4 | ||
|
|
af3011cd69 | ||
|
|
de9c15fa79 | ||
|
|
e85279ea7b | ||
|
|
d8be245c22 |
No files matched your search
+24
-3
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.18...4.3)
|
||||
cmake_minimum_required(VERSION 3.19...4.3)
|
||||
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "Minimum policy version for bundled dependencies")
|
||||
@@ -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>)
|
||||
@@ -725,6 +737,15 @@ endif()
|
||||
add_executable(kyty_emulator "${KYTY_SOURCE_DIR}/main.cpp" ${kyty_emulator_src})
|
||||
configure_macos_guest_address_space(kyty_emulator)
|
||||
|
||||
set(KYTY_TROPHY_UNLOCK_SOUND "${CMAKE_CURRENT_SOURCE_DIR}/assets/sounds/trophy-unlock.wav")
|
||||
add_custom_target(kyty_trophy_sound
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_TROPHY_UNLOCK_SOUND}"
|
||||
"$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds/trophy-unlock.wav"
|
||||
VERBATIM)
|
||||
add_dependencies(kyty_emulator kyty_trophy_sound)
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
|
||||
|
||||
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
|
||||
if (WIN32)
|
||||
target_link_libraries(kyty_emulator opengl32)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
@@ -27,6 +27,7 @@ target_link_libraries(common
|
||||
kyty_git_version
|
||||
spdlog::spdlog
|
||||
ZArchive::zarchive
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
@@ -156,6 +156,10 @@ bool TessellationEnabled() {
|
||||
return g_config->tessellation_enabled;
|
||||
}
|
||||
|
||||
bool TrophyEnabled() {
|
||||
return g_config->trophy_enabled;
|
||||
}
|
||||
|
||||
bool PlayGoHackEnabled() {
|
||||
return g_config->playgo_hack_enabled;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ struct ConfigOptions {
|
||||
bool renderdoc_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
bool playgo_hack_enabled = false;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool red_zone_protection_enabled = false;
|
||||
@@ -123,6 +124,7 @@ bool GpuAssistedValidationEnabled();
|
||||
bool RenderDocEnabled();
|
||||
bool ReadbackLinearImagesEnabled();
|
||||
bool TessellationEnabled();
|
||||
bool TrophyEnabled();
|
||||
bool PlayGoHackEnabled();
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool RedZoneProtectionEnabled();
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include "common/file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <span>
|
||||
|
||||
namespace Common::Trophies {
|
||||
namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
using Files = std::map<std::string_view, std::span<const std::byte>>;
|
||||
|
||||
constexpr uint64_t MaxUnlocksSize = uint64_t {1} << 20u;
|
||||
constexpr uint64_t MaxPackageSize = uint64_t {1} << 30u;
|
||||
|
||||
uint64_t ReadBigEndian(const std::byte* data, size_t size) {
|
||||
uint64_t value = 0;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
value = (value << 8u) | std::to_integer<uint8_t>(data[i]);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int ReadId(const Json& value) {
|
||||
if (value.is_number_integer()) {
|
||||
const auto number = value.get<int64_t>();
|
||||
return number >= 0 && number <= INT32_MAX ? static_cast<int>(number) : -1;
|
||||
}
|
||||
if (!value.is_string()) {
|
||||
return -1;
|
||||
}
|
||||
const auto& text = value.get_ref<const std::string&>();
|
||||
int id = -1;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id);
|
||||
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()) {
|
||||
return {};
|
||||
}
|
||||
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
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 false;
|
||||
}
|
||||
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);
|
||||
const std::string_view filename(name, std::find(name, name + 0x20, '\0') - name);
|
||||
const auto offset = ReadBigEndian(entry + 0x20, 8);
|
||||
const auto length = ReadBigEndian(entry + 0x28, 8);
|
||||
if (offset > size || length > size - offset) {
|
||||
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") ||
|
||||
!conf["defaultLanguage"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
static constexpr std::array<std::string_view, 30> locales = {
|
||||
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
|
||||
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
|
||||
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
|
||||
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
|
||||
const auto locale =
|
||||
locales[console_language >= 0 && console_language < locales.size() ? console_language : 1];
|
||||
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
|
||||
if (meta.is_null()) {
|
||||
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
|
||||
conf["defaultLanguage"].get_ref<const std::string&>()));
|
||||
}
|
||||
if (!meta.is_object() || !meta.contains("metadata") || !meta["metadata"].is_object()) {
|
||||
return {};
|
||||
}
|
||||
const auto& texts = meta["metadata"];
|
||||
if (!texts.contains("titleMetadata") || !texts["titleMetadata"].is_object() ||
|
||||
!texts["titleMetadata"].contains("name") || !texts["titleMetadata"]["name"].is_string() ||
|
||||
!texts.contains("trophyMetadata") || !texts["trophyMetadata"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
Package package;
|
||||
package.title = texts["titleMetadata"]["name"].get<std::string>();
|
||||
package.groups.emplace(-1, package.title);
|
||||
uint64_t icon_bytes = 0;
|
||||
for (const auto& definition: conf["trophies"]) {
|
||||
if (!definition.is_object() || !definition.contains("id") ||
|
||||
!definition.contains("grade") || !definition["grade"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
Trophy trophy;
|
||||
trophy.id = ReadId(definition["id"]);
|
||||
const auto grade = definition["grade"].get<std::string>();
|
||||
const auto grade_index = std::string_view("PGSB").find(grade);
|
||||
if (trophy.id < 0 || grade.size() != 1 || grade_index == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
trophy.grade = static_cast<int>(grade_index) + 1;
|
||||
if (definition.contains("groupId")) {
|
||||
trophy.group_id = ReadId(definition["groupId"]);
|
||||
if (trophy.group_id < 0) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (definition.contains("platinumTrophyId")) {
|
||||
trophy.platinum_id = ReadId(definition["platinumTrophyId"]);
|
||||
}
|
||||
trophy.hidden = definition.contains("hidden") && definition["hidden"] == true;
|
||||
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
|
||||
if (definition.contains("unlockCondition")) {
|
||||
const auto& condition = definition["unlockCondition"];
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
|
||||
if (icon != files.end()) {
|
||||
if (icon->second.size() > size - icon_bytes) {
|
||||
return {};
|
||||
}
|
||||
trophy.icon_png.assign(icon->second.begin(), icon->second.end());
|
||||
icon_bytes += icon->second.size();
|
||||
}
|
||||
package.groups.try_emplace(trophy.group_id);
|
||||
if (package.groups.size() > 50 ||
|
||||
!package.trophies.emplace(trophy.id, std::move(trophy)).second) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (texts.contains("groupMetadata") && texts["groupMetadata"].is_array()) {
|
||||
for (const auto& group: texts["groupMetadata"]) {
|
||||
if (group.is_object() && group.contains("id") && group.contains("name") &&
|
||||
group["name"].is_string()) {
|
||||
const auto found = package.groups.find(ReadId(group["id"]));
|
||||
if (found != package.groups.end()) {
|
||||
found->second = group["name"].get<std::string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& text: texts["trophyMetadata"]) {
|
||||
if (!text.is_object() || !text.contains("id")) {
|
||||
continue;
|
||||
}
|
||||
const auto found = package.trophies.find(ReadId(text["id"]));
|
||||
if (found == package.trophies.end()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& [key, field]: {std::pair {"name", &found->second.name},
|
||||
{"detail", &found->second.description},
|
||||
{"reward", &found->second.reward}}) {
|
||||
if (text.contains(key) && text[key].is_string()) {
|
||||
*field = text[key].get<std::string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
return package;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Package LoadPackage(const std::filesystem::path& path, int console_language) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
|
||||
return {};
|
||||
}
|
||||
return ParsePackage(file.ReadWholeBuffer(), console_language);
|
||||
}
|
||||
|
||||
std::filesystem::path PackagePath(uint32_t service_label) {
|
||||
return std::filesystem::path(PackageDirectory) / fmt::format("trophy{:02}.ucp", service_label);
|
||||
}
|
||||
|
||||
std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string_view title_id,
|
||||
int user_id, uint32_t service_label) {
|
||||
if (title_id.empty() || !std::all_of(title_id.begin(), title_id.end(), [](char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
|
||||
c == '_' || c == '-';
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
return root / "_Trophies" / title_id /
|
||||
fmt::format("trophies_{}_{}.json", user_id, service_label);
|
||||
}
|
||||
|
||||
UnlockData LoadUnlockData(const std::filesystem::path& path) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
|
||||
return {};
|
||||
}
|
||||
const auto bytes = file.ReadWholeBuffer();
|
||||
const auto json = Json::parse(bytes.begin(), bytes.end(), nullptr, false);
|
||||
if (!json.is_object() || !json.contains("unlockedTrophies") ||
|
||||
!json["unlockedTrophies"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
UnlockData unlocks;
|
||||
for (const auto& value: json["unlockedTrophies"]) {
|
||||
const auto id = ReadId(value);
|
||||
if (id >= 0) {
|
||||
unlocks.unlocked.insert(id);
|
||||
}
|
||||
}
|
||||
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 SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
|
||||
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
|
||||
return false;
|
||||
}
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t written = 0;
|
||||
file.Write(text.data(), static_cast<uint32_t>(text.size()), &written);
|
||||
const bool complete = written == text.size() && file.Flush();
|
||||
file.Close();
|
||||
std::error_code error;
|
||||
if (complete) {
|
||||
std::filesystem::rename(temporary, path, error);
|
||||
if (!error) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(temporary, error);
|
||||
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
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef KYTY_COMMON_TROPHIES_H_
|
||||
#define KYTY_COMMON_TROPHIES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace Common::Trophies {
|
||||
|
||||
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;
|
||||
int group_id = -1;
|
||||
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;
|
||||
std::vector<std::byte> icon_png;
|
||||
bool hidden = false;
|
||||
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]] UnlockData LoadUnlockData(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
#endif // KYTY_COMMON_TROPHIES_H_
|
||||
@@ -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;
|
||||
|
||||
@@ -1962,6 +1962,11 @@ bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextureCache::IsRegionRegistered(uint64_t address, uint64_t size) {
|
||||
std::scoped_lock lock {m_lock};
|
||||
return !FindImagesInRegion(address, size, false).empty();
|
||||
}
|
||||
|
||||
void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
|
||||
if (!GuestRange {address, size}.Valid()) {
|
||||
EXIT("TextureCache: invalid unmap range\n");
|
||||
|
||||
@@ -61,6 +61,7 @@ public:
|
||||
uint32_t packed_clear);
|
||||
void InvalidateMemory(uint64_t address, uint64_t size);
|
||||
void InvalidateMemoryFromGPU(uint64_t address, uint64_t size);
|
||||
[[nodiscard]] bool IsRegionRegistered(uint64_t address, uint64_t size);
|
||||
|
||||
[[nodiscard]] bool IsMeta(uint64_t address);
|
||||
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice);
|
||||
|
||||
@@ -226,43 +226,37 @@ void CommandScheduler::PopPendingOperations() {
|
||||
}
|
||||
|
||||
void CommandScheduler::DeferOperation(Common::UniqueFunction<void>&& operation) {
|
||||
QueueOperation(std::move(operation), false);
|
||||
}
|
||||
|
||||
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
|
||||
QueueOperation(std::move(operation), true);
|
||||
}
|
||||
|
||||
void CommandScheduler::QueueOperation(Common::UniqueFunction<void>&& operation, bool priority) {
|
||||
CheckActive();
|
||||
EXIT_IF(!operation);
|
||||
std::unique_lock lock(m_operation_mutex);
|
||||
if (m_operation_state == OperationState::Open) {
|
||||
m_pending_operations.push({std::move(operation), CurrentTick()});
|
||||
return;
|
||||
}
|
||||
if (g_deferred_callback_scheduler == this) {
|
||||
auto& queue = priority ? m_priority_operations : m_pending_operations;
|
||||
queue.push({std::move(operation), CurrentTick()});
|
||||
lock.unlock();
|
||||
operation();
|
||||
if (priority) {
|
||||
m_operation_available.notify_one();
|
||||
}
|
||||
return;
|
||||
}
|
||||
m_operation_available.wait(lock,
|
||||
[this] { return m_operation_state == OperationState::Closed; });
|
||||
if (g_deferred_callback_scheduler != this) {
|
||||
m_operation_available.wait(lock,
|
||||
[this] { return m_operation_state == OperationState::Closed; });
|
||||
}
|
||||
lock.unlock();
|
||||
operation();
|
||||
}
|
||||
|
||||
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
|
||||
CheckActive();
|
||||
EXIT_IF(!operation);
|
||||
std::unique_lock lock(m_operation_mutex);
|
||||
if (m_operation_state == OperationState::Open) {
|
||||
m_priority_operations.push({std::move(operation), CurrentTick()});
|
||||
lock.unlock();
|
||||
m_operation_available.notify_one();
|
||||
return;
|
||||
}
|
||||
if (g_deferred_callback_scheduler == this) {
|
||||
lock.unlock();
|
||||
operation();
|
||||
return;
|
||||
}
|
||||
m_operation_available.wait(lock,
|
||||
[this] { return m_operation_state == OperationState::Closed; });
|
||||
lock.unlock();
|
||||
operation();
|
||||
bool CommandScheduler::HasPendingPriorityOperations() {
|
||||
std::lock_guard lock(m_operation_mutex);
|
||||
return !m_priority_operations.empty() || m_priority_active;
|
||||
}
|
||||
|
||||
void CommandScheduler::PriorityOperationsThread(std::stop_token stop) {
|
||||
|
||||
@@ -38,8 +38,10 @@ public:
|
||||
void PopPendingOperations();
|
||||
void DrainPriorityOperations();
|
||||
void WaitPriorityOperations(uint64_t tick);
|
||||
// Guest-memory completions use the priority queue; normal callbacks maintain GPU resources.
|
||||
void DeferOperation(Common::UniqueFunction<void>&& operation);
|
||||
void DeferPriorityOperation(Common::UniqueFunction<void>&& operation);
|
||||
[[nodiscard]] bool HasPendingPriorityOperations();
|
||||
[[nodiscard]] static bool InDeferredOperation() noexcept;
|
||||
|
||||
[[nodiscard]] bool Active() const noexcept { return m_command.m_registers != nullptr; }
|
||||
@@ -82,6 +84,7 @@ private:
|
||||
|
||||
void BeginNext();
|
||||
void PriorityOperationsThread(std::stop_token stop);
|
||||
void QueueOperation(Common::UniqueFunction<void>&& operation, bool priority);
|
||||
void RunOperation(Common::UniqueFunction<void>&& operation);
|
||||
|
||||
MasterSemaphore m_master;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -99,7 +99,12 @@ void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
|
||||
vaddr, size);
|
||||
}
|
||||
const auto unmap = [this, vaddr, size] {
|
||||
if (m_command_scheduler.Active()) {
|
||||
// Check cache ownership on the GPU thread. Guest-memory callbacks can still
|
||||
// access a range with no cached data, so they must finish before it is unmapped.
|
||||
if (m_command_scheduler.Active() &&
|
||||
(m_buffer_cache.IsRegionRegistered(vaddr, size) ||
|
||||
m_texture_cache.IsRegionRegistered(vaddr, size) ||
|
||||
m_command_scheduler.HasPendingPriorityOperations())) {
|
||||
const auto tick = m_command_scheduler.CurrentTick();
|
||||
m_command_scheduler.Finish();
|
||||
m_command_scheduler.WaitPriorityOperations(tick);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
#include "libs/controller.h"
|
||||
#include "libs/dialog.h"
|
||||
#include "libs/ime.h"
|
||||
#include "libs/imeDialog.h"
|
||||
#include "stb_image.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -18,12 +21,17 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -178,6 +186,14 @@ bool g_input_lifecycle_active = false;
|
||||
bool g_controller_captured = false;
|
||||
OverlaySession g_session;
|
||||
SDL_Window* g_input_window = nullptr;
|
||||
struct TrophyNotification {
|
||||
std::string name;
|
||||
int32_t grade;
|
||||
std::vector<std::byte> icon_png;
|
||||
};
|
||||
std::mutex g_trophy_notification_mutex;
|
||||
std::deque<TrophyNotification> g_trophy_notifications;
|
||||
std::atomic<bool> g_trophy_notification_active {false};
|
||||
|
||||
void ClearInputEvents() {
|
||||
std::scoped_lock lock(g_input_mutex);
|
||||
@@ -467,8 +483,9 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
const auto core = CoreIme::GetVisualState();
|
||||
const auto dialog = DialogIme::GetVisualState();
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
return {core.active || dialog.active || system.active,
|
||||
core.revision + dialog.revision + system.revision};
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active,
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -599,12 +616,34 @@ bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
std::span<const std::byte> icon_png) {
|
||||
std::scoped_lock lock(g_trophy_notification_mutex);
|
||||
g_trophy_notifications.push_back(
|
||||
{std::string(name), grade, std::vector<std::byte>(icon_png.begin(), icon_png.end())});
|
||||
g_trophy_notification_active.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
SDL_DestroyAudioStream(trophy_sound_stream);
|
||||
SDL_free(trophy_sound_data);
|
||||
if (trophy_sound_initialized) {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
ReleaseVulkan();
|
||||
if (imgui_context != nullptr) {
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
if (trophy_notification_image) {
|
||||
ImGui::UnregisterUserTexture(trophy_notification_image.get());
|
||||
}
|
||||
for (const auto& texture: retired_trophy_images) {
|
||||
ImGui::UnregisterUserTexture(texture.get());
|
||||
}
|
||||
ImGui::DestroyContext(imgui_context);
|
||||
}
|
||||
}
|
||||
@@ -631,6 +670,75 @@ struct SystemOverlay::Impl {
|
||||
style.ItemSpacing = {8.0f, 8.0f};
|
||||
}
|
||||
|
||||
void PlayTrophySound() {
|
||||
if (!trophy_sound_attempted) {
|
||||
trophy_sound_attempted = true;
|
||||
trophy_sound_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
if (!trophy_sound_initialized) {
|
||||
LOGF("[Trophy] could not initialize audio: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
if (base_path == nullptr) {
|
||||
LOGF("[Trophy] could not locate sound: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
const std::string path = std::string(base_path) + "assets/sounds/trophy-unlock.wav";
|
||||
SDL_AudioSpec spec {};
|
||||
if (!SDL_LoadWAV(path.c_str(), &spec, &trophy_sound_data, &trophy_sound_size)) {
|
||||
LOGF("[Trophy] could not load %s: %s\n", path.c_str(), SDL_GetError());
|
||||
return;
|
||||
}
|
||||
if (trophy_sound_size > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
|
||||
LOGF("[Trophy] sound is too large: %s\n", path.c_str());
|
||||
return;
|
||||
}
|
||||
trophy_sound_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&spec, nullptr, nullptr);
|
||||
if (trophy_sound_stream == nullptr) {
|
||||
LOGF("[Trophy] could not open audio stream: %s\n", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (trophy_sound_stream != nullptr &&
|
||||
(!SDL_PutAudioStreamData(trophy_sound_stream, trophy_sound_data,
|
||||
static_cast<int>(trophy_sound_size)) ||
|
||||
!SDL_ResumeAudioStreamDevice(trophy_sound_stream))) {
|
||||
LOGF("[Trophy] could not play sound: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto* data = reinterpret_cast<const stbi_uc*>(encoded.data());
|
||||
const int size = static_cast<int>(encoded.size());
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (!stbi_info_from_memory(data, size, &width, &height, nullptr) || width <= 0 ||
|
||||
height <= 0 || width > 512 || height > 512) {
|
||||
LOGF("[Trophy] invalid icon or dimensions exceed 512x512\n");
|
||||
return nullptr;
|
||||
}
|
||||
auto* pixels = stbi_load_from_memory(data, size, &width, &height, nullptr, 4);
|
||||
if (pixels == nullptr) {
|
||||
LOGF("[Trophy] could not decode trophy icon: %s\n", stbi_failure_reason());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<ImTextureData>();
|
||||
texture->Create(ImTextureFormat_RGBA32, width, height);
|
||||
std::memcpy(texture->Pixels, pixels,
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
|
||||
texture->UseColors = true;
|
||||
stbi_image_free(pixels);
|
||||
ImGui::RegisterUserTexture(texture.get());
|
||||
return texture;
|
||||
}
|
||||
|
||||
void EnsureVulkan(vk::Format format, uint32_t image_count) {
|
||||
EnsureContext();
|
||||
if (vulkan_initialized) {
|
||||
@@ -952,9 +1060,116 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
return false;
|
||||
}
|
||||
ImGui::UnregisterUserTexture(texture.get());
|
||||
return true;
|
||||
});
|
||||
constexpr float TOAST_LIFETIME = 5.0f;
|
||||
if (trophy_notification &&
|
||||
std::chrono::duration<float>(now - trophy_notification_born).count() >=
|
||||
TOAST_LIFETIME) {
|
||||
trophy_notification.reset();
|
||||
if (trophy_notification_image) {
|
||||
// Keep the texture registered until the backend retires in-flight frames.
|
||||
trophy_notification_image->WantDestroyNextFrame = true;
|
||||
retired_trophy_images.push_back(std::move(trophy_notification_image));
|
||||
}
|
||||
}
|
||||
if (!trophy_notification) {
|
||||
{
|
||||
std::scoped_lock lock(g_trophy_notification_mutex);
|
||||
if (g_trophy_notifications.empty()) {
|
||||
g_trophy_notification_active.store(!retired_trophy_images.empty(),
|
||||
std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
trophy_notification = std::move(g_trophy_notifications.front());
|
||||
g_trophy_notifications.pop_front();
|
||||
}
|
||||
trophy_notification_born = now;
|
||||
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
|
||||
PlayTrophySound();
|
||||
}
|
||||
const float age = std::chrono::duration<float>(now - trophy_notification_born).count();
|
||||
const float screen_scale =
|
||||
std::clamp(std::min(static_cast<float>(frame_extent.width) / 1920.0f,
|
||||
static_cast<float>(frame_extent.height) / 1080.0f),
|
||||
0.65f, 1.25f);
|
||||
const float width = std::max(
|
||||
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) - 32.0f - width + slide,
|
||||
32.0f};
|
||||
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
bool PrepareFrame(vk::Extent2D frame_extent, vk::Format format, uint32_t image_count) {
|
||||
OverlaySnapshot snapshot;
|
||||
if (!GetOverlaySnapshot(&snapshot)) {
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -977,7 +1192,6 @@ struct SystemOverlay::Impl {
|
||||
auto& io = ImGui::GetIO();
|
||||
io.DisplaySize = {static_cast<float>(frame_extent.width),
|
||||
static_cast<float>(frame_extent.height)};
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
io.DeltaTime = last_frame == std::chrono::steady_clock::time_point {}
|
||||
? 1.0f / 60.0f
|
||||
: std::clamp(std::chrono::duration<float>(now - last_frame).count(),
|
||||
@@ -985,15 +1199,19 @@ struct SystemOverlay::Impl {
|
||||
last_frame = now;
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
if (!GetOverlaySnapshot(&snapshot) || snapshot.session != prepared_session) {
|
||||
GetOverlaySnapshot(&snapshot);
|
||||
if (snapshot.session != prepared_session) {
|
||||
ImGui::EndFrame();
|
||||
return false;
|
||||
}
|
||||
if (snapshot.session.kind == OverlayKind::Dialog) {
|
||||
DrawDialog(snapshot.dialog, frame_extent);
|
||||
} else {
|
||||
DrawIme(snapshot.ime, frame_extent);
|
||||
if (has_overlay) {
|
||||
if (snapshot.session.kind == OverlayKind::Dialog) {
|
||||
DrawDialog(snapshot.dialog, frame_extent);
|
||||
} else {
|
||||
DrawIme(snapshot.ime, frame_extent);
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
return true;
|
||||
@@ -1040,9 +1258,18 @@ struct SystemOverlay::Impl {
|
||||
float button_height = 42.0f;
|
||||
ImVec2 panel_offset {};
|
||||
ImVec2 right_stick {};
|
||||
OverlaySession session;
|
||||
vk::Extent2D extent {};
|
||||
std::chrono::steady_clock::time_point last_frame;
|
||||
OverlaySession session;
|
||||
std::unique_ptr<ImTextureData> trophy_notification_image;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
SDL_AudioStream* trophy_sound_stream = nullptr;
|
||||
uint8_t* trophy_sound_data = nullptr;
|
||||
uint32_t trophy_sound_size = 0;
|
||||
bool trophy_sound_attempted = false;
|
||||
bool trophy_sound_initialized = false;
|
||||
vk::Extent2D extent {};
|
||||
std::chrono::steady_clock::time_point last_frame;
|
||||
};
|
||||
|
||||
SystemOverlay::SystemOverlay(GraphicContext& graphics): m_impl(std::make_unique<Impl>(graphics)) {}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#include "common/common.h"
|
||||
#include "graphics/host_gpu/vulkanCommon.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
union SDL_Event;
|
||||
struct SDL_Window;
|
||||
@@ -22,6 +25,8 @@ void InitializeSystemOverlayInput(SDL_Window* window);
|
||||
void ShutdownSystemOverlayInput();
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event);
|
||||
SystemOverlayVisualState GetSystemOverlayVisualState() noexcept;
|
||||
void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
std::span<const std::byte> icon_png);
|
||||
|
||||
class SystemOverlay final {
|
||||
public:
|
||||
|
||||
@@ -454,7 +454,7 @@ void HostInputToggleMouseToJoystick() {
|
||||
LOGF("Mouse to right stick: enabled (F7 to release)\n");
|
||||
}
|
||||
|
||||
bool HostInputWaitEvent(SDL_Event* event) {
|
||||
bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
int timeout = -1;
|
||||
if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) {
|
||||
g_mouse.next_poll = 0;
|
||||
@@ -473,6 +473,7 @@ bool HostInputWaitEvent(SDL_Event* event) {
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
|
||||
@@ -13,7 +13,7 @@ void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event);
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
|
||||
|
||||
@@ -998,7 +998,7 @@ void Presenter::Impl::Present() {
|
||||
}
|
||||
|
||||
presented_overlay_revision.store(overlay_visual.revision, std::memory_order_release);
|
||||
window.UpdateTitle();
|
||||
window.loop.presented_frames.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
LOGF("Vulkan presentation retry exhausted; dropping frame\n");
|
||||
|
||||
@@ -722,6 +722,10 @@ void WindowContext::Run() {
|
||||
loop.need_exit = false;
|
||||
loop.paused.store(false, std::memory_order_release);
|
||||
|
||||
constexpr uint64_t title_interval_ms = 1000;
|
||||
auto title_time = SDL_GetTicks();
|
||||
auto title_frames = loop.presented_frames.load(std::memory_order_relaxed);
|
||||
|
||||
while (!loop.need_exit) {
|
||||
if (loop.paused.load(std::memory_order_acquire)) {
|
||||
if (!timer.IsPaused()) {
|
||||
@@ -731,7 +735,20 @@ void WindowContext::Run() {
|
||||
timer.Resume();
|
||||
}
|
||||
|
||||
if (!HostInputWaitEvent(&loop.event)) {
|
||||
// Refresh the title on the main thread without making presentation wait for it.
|
||||
const auto now = SDL_GetTicks();
|
||||
const auto elapsed = now - title_time;
|
||||
if (elapsed >= title_interval_ms) {
|
||||
const auto frames = loop.presented_frames.load(std::memory_order_relaxed);
|
||||
if (frames != 0) {
|
||||
UpdateTitle(frames, static_cast<double>(frames - title_frames) * 1000.0 /
|
||||
static_cast<double>(elapsed));
|
||||
}
|
||||
title_time = now;
|
||||
title_frames = frames;
|
||||
}
|
||||
const auto wait_ms = static_cast<int>(title_interval_ms - (now - title_time));
|
||||
if (!HostInputWaitEvent(&loop.event, wait_ms)) {
|
||||
continue;
|
||||
}
|
||||
ProcessEvent(timer.GetTimeS());
|
||||
@@ -913,7 +930,7 @@ void WindowContext::UpdateIcon() {
|
||||
}
|
||||
}
|
||||
|
||||
void WindowContext::UpdateTitle() {
|
||||
void WindowContext::UpdateTitle(uint64_t frame_num, double current_fps) {
|
||||
static char title[128];
|
||||
static char title_id[12];
|
||||
static char app_ver[12];
|
||||
@@ -923,10 +940,6 @@ void WindowContext::UpdateTitle() {
|
||||
static bool has_app_ver =
|
||||
Loader::SystemContentParamSfoGetString("APP_VER", app_ver, sizeof(app_ver));
|
||||
static const std::string processor_name = Common::GetSystemInfo().ProcessorName;
|
||||
static uint64_t fps_start = Common::Timer::QueryPerformanceCounter();
|
||||
static uint64_t frame_num = 0;
|
||||
static uint64_t fps_frames = 0;
|
||||
static double current_fps = 0.0;
|
||||
|
||||
#if KYTY_BUILD == KYTY_BUILD_DEBUG
|
||||
static constexpr auto build_type = "Debug";
|
||||
@@ -936,17 +949,6 @@ void WindowContext::UpdateTitle() {
|
||||
static constexpr auto build_type = "Unknown";
|
||||
#endif
|
||||
|
||||
const auto now = Common::Timer::QueryPerformanceCounter();
|
||||
const auto frequency = Common::Timer::QueryPerformanceFrequency();
|
||||
frame_num++;
|
||||
fps_frames++;
|
||||
if (now - fps_start >= frequency) {
|
||||
current_fps = static_cast<double>(fps_frames) * static_cast<double>(frequency) /
|
||||
static_cast<double>(now - fps_start);
|
||||
fps_start = now;
|
||||
fps_frames = 0;
|
||||
}
|
||||
|
||||
const auto* device_name = graphic_ctx.GetPhysicalDeviceProperties().deviceName.data();
|
||||
auto text = fmt::format(
|
||||
"[{} | {}] {}{}{}{}{}{}[{}] [{}], frame: {}, fps: {:.0f}", KYTY_BUILD_LABEL, build_type,
|
||||
@@ -954,16 +956,7 @@ void WindowContext::UpdateTitle() {
|
||||
(has_title_id ? ", " : ""), (has_app_ver ? app_ver : ""), (has_app_ver ? " " : ""),
|
||||
device_name, processor_name, frame_num, current_fps);
|
||||
|
||||
struct TitleUpdate {
|
||||
SDL_Window* window;
|
||||
std::string* text;
|
||||
} update {window, &text};
|
||||
EXIT_IF(!SDL_RunOnMainThread(
|
||||
[](void* data) {
|
||||
auto& title = *static_cast<TitleUpdate*>(data);
|
||||
SDL_SetWindowTitle(title.window, title.text->c_str());
|
||||
},
|
||||
&update, true));
|
||||
SDL_SetWindowTitle(window, text.c_str());
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics
|
||||
@@ -27,6 +27,7 @@ struct WindowLoopState {
|
||||
SDL_Event event {};
|
||||
bool need_exit = false;
|
||||
std::atomic_bool paused = false;
|
||||
std::atomic<uint64_t> presented_frames {0};
|
||||
};
|
||||
|
||||
struct WindowContext {
|
||||
@@ -42,7 +43,7 @@ struct WindowContext {
|
||||
void RecreateSurface();
|
||||
void RefreshSurfaceCapabilities();
|
||||
void UpdateIcon();
|
||||
void UpdateTitle();
|
||||
void UpdateTitle(uint64_t frame_num, double current_fps);
|
||||
void Resize(int width, int height);
|
||||
void ProcessWindowEvent(const SDL_WindowEvent& event);
|
||||
void ProcessDisplayEvent(const SDL_DisplayEvent& event);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,21 +17,8 @@ Pair ExtractPair(EmitterState& state, uint32_t value) {
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t MakePair(EmitterState& state, uint32_t low, uint32_t high) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU64(state), result, low, high);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t CompareEqual64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value,
|
||||
bool not_equal) {
|
||||
const auto compare = Binary(state, not_equal ? spv::OpINotEqual : spv::OpIEqual,
|
||||
TypeBoolVector(state, 2), lhs_value, rhs_value);
|
||||
return Unary(state, not_equal ? spv::OpAny : spv::OpAll, TypeBool(state), compare);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto bits_type = TypeU64(state);
|
||||
const auto bits_type = TypeU32Vector(state, 2);
|
||||
const auto lhs_bits = Unary(state, spv::OpBitcast, bits_type, lhs);
|
||||
const auto rhs_bits = Unary(state, spv::OpBitcast, bits_type, rhs);
|
||||
const auto ordered = Binary(state, max_value ? spv::OpFOrdGreaterThan : spv::OpFOrdLessThan,
|
||||
@@ -52,17 +39,6 @@ uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max
|
||||
rhs, result);
|
||||
}
|
||||
|
||||
uint32_t CompareOrdered64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value,
|
||||
spv::Op high_compare, spv::Op low_compare) {
|
||||
const auto lhs = ExtractPair(state, lhs_value);
|
||||
const auto rhs = ExtractPair(state, rhs_value);
|
||||
const auto high_equal = Binary(state, spv::OpIEqual, TypeBool(state), lhs.high, rhs.high);
|
||||
const auto high_result = Binary(state, high_compare, TypeBool(state), lhs.high, rhs.high);
|
||||
const auto low_result = Binary(state, low_compare, TypeBool(state), lhs.low, rhs.low);
|
||||
const auto low_path = Binary(state, spv::OpLogicalAnd, TypeBool(state), high_equal, low_result);
|
||||
return Binary(state, spv::OpLogicalOr, TypeBool(state), high_result, low_path);
|
||||
}
|
||||
|
||||
uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signed_value) {
|
||||
const auto operand_type = signed_value ? TypeI32(state) : TypeU32(state);
|
||||
const auto pair_type = signed_value ? TypeI32Pair(state) : TypeU32Pair(state);
|
||||
@@ -80,86 +56,6 @@ uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signe
|
||||
return signed_value ? Unary(state, spv::OpBitcast, TypeU32(state), high) : high;
|
||||
}
|
||||
|
||||
uint32_t EmitShift64(EmitterState& state, spv::Op opcode, uint32_t value, uint32_t shift) {
|
||||
const auto pair = ExtractPair(state, value);
|
||||
const auto amount = EmitAndConstant(state, shift, 63u);
|
||||
const auto word_shift = EmitAndConstant(state, amount, 31u);
|
||||
const auto at_least_32 =
|
||||
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), amount, ConstantU32(state, 32));
|
||||
const auto nonzero =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), amount, ConstantU32(state, 0));
|
||||
const auto carry_count = EmitAndConstant(
|
||||
state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32), word_shift), 31u);
|
||||
if (opcode == spv::OpShiftLeftLogical) {
|
||||
const auto low = Binary(state, opcode, TypeU32(state), pair.low, word_shift);
|
||||
const auto carry =
|
||||
Select(state, TypeU32(state), nonzero,
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low, carry_count),
|
||||
ConstantU32(state, 0));
|
||||
const auto high =
|
||||
Binary(state, spv::OpBitwiseOr, TypeU32(state),
|
||||
Binary(state, opcode, TypeU32(state), pair.high, word_shift), carry);
|
||||
return MakePair(state,
|
||||
Select(state, TypeU32(state), at_least_32, ConstantU32(state, 0), low),
|
||||
Select(state, TypeU32(state), at_least_32, low, high));
|
||||
}
|
||||
const auto high = Binary(state, opcode, TypeU32(state), pair.high, word_shift);
|
||||
const auto carry =
|
||||
Select(state, TypeU32(state), nonzero,
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.high, carry_count),
|
||||
ConstantU32(state, 0));
|
||||
const auto low = Binary(
|
||||
state, spv::OpBitwiseOr, TypeU32(state),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low, word_shift), carry);
|
||||
const auto fill = opcode == spv::OpShiftRightArithmetic
|
||||
? Binary(state, opcode, TypeU32(state), pair.high, ConstantU32(state, 31))
|
||||
: ConstantU32(state, 0);
|
||||
return MakePair(state, Select(state, TypeU32(state), at_least_32, high, low),
|
||||
Select(state, TypeU32(state), at_least_32, fill, high));
|
||||
}
|
||||
|
||||
uint32_t EmitConstantShift64(EmitterState& state, spv::Op opcode, uint32_t value, uint32_t shift) {
|
||||
shift &= 63u;
|
||||
if (shift == 0u) {
|
||||
return value;
|
||||
}
|
||||
const auto pair = ExtractPair(state, value);
|
||||
if (opcode == spv::OpShiftLeftLogical) {
|
||||
if (shift < 32u) {
|
||||
const auto low = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.low,
|
||||
ConstantU32(state, shift));
|
||||
const auto high = Binary(state, spv::OpBitwiseOr, TypeU32(state),
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
pair.high, ConstantU32(state, shift)),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state),
|
||||
pair.low, ConstantU32(state, 32u - shift)));
|
||||
return MakePair(state, low, high);
|
||||
}
|
||||
return MakePair(state, ConstantU32(state, 0),
|
||||
shift == 32u ? pair.low
|
||||
: Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
|
||||
pair.low, ConstantU32(state, shift - 32u)));
|
||||
}
|
||||
if (shift < 32u) {
|
||||
const auto low = Binary(state, spv::OpBitwiseOr, TypeU32(state),
|
||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low,
|
||||
ConstantU32(state, shift)),
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.high,
|
||||
ConstantU32(state, 32u - shift)));
|
||||
const auto high =
|
||||
Binary(state, opcode, TypeU32(state), pair.high, ConstantU32(state, shift));
|
||||
return MakePair(state, low, high);
|
||||
}
|
||||
const auto high = opcode == spv::OpShiftRightArithmetic
|
||||
? Binary(state, spv::OpShiftRightArithmetic, TypeU32(state), pair.high,
|
||||
ConstantU32(state, 31u))
|
||||
: ConstantU32(state, 0);
|
||||
const auto low = shift == 32u ? pair.high
|
||||
: Binary(state, opcode, TypeU32(state), pair.high,
|
||||
ConstantU32(state, shift - 32u));
|
||||
return MakePair(state, low, high);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMax3(EmitterState& state, uint32_t a, uint32_t b, uint32_t c, bool signed_value,
|
||||
bool max_value) {
|
||||
const auto ab = signed_value ? EmitMinMaxI32Value(state, a, b, max_value)
|
||||
@@ -223,6 +119,28 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto result = EmitGlsl<GLSLstd450Fma, IR::Type::F32>(state, a, b, c);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
// Explicit denormal checks typically drop 3DMiniGolf menu FPS from 28-29 to 21-24
|
||||
// on NVIDIA RTX 5080 Laptop GPU, so leave them disabled for this experiment.
|
||||
// a = EmitFlushF32DenormToSignedZero(state, a);
|
||||
// b = EmitFlushF32DenormToSignedZero(state, b);
|
||||
// c = EmitFlushF32DenormToSignedZero(state, c);
|
||||
const auto product = EmitFPMul32(state, a, b);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction);
|
||||
// const auto sum = EmitFPAdd32(state, EmitFlushF32DenormToSignedZero(state, product), c);
|
||||
const auto sum = EmitFPAdd32(state, product, c);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction);
|
||||
// return EmitFlushF32DenormToSignedZero(state, sum);
|
||||
return sum;
|
||||
}
|
||||
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
|
||||
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
|
||||
@@ -237,57 +155,15 @@ uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c)
|
||||
}
|
||||
|
||||
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
|
||||
const auto pair = ExtractPair(state, value);
|
||||
const auto high_i = state.builder.AllocateId();
|
||||
const auto low_i = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeI32(state), high_i, GlslStd450(state),
|
||||
GLSLstd450FindUMsb, pair.high);
|
||||
state.builder.AddFunction(spv::OpExtInst, TypeI32(state), low_i, GlslStd450(state),
|
||||
GLSLstd450FindUMsb, pair.low);
|
||||
const auto high = Unary(state, spv::OpBitcast, TypeU32(state), high_i);
|
||||
const auto low = Unary(state, spv::OpBitcast, TypeU32(state), low_i);
|
||||
const auto high_nonzero =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, 0));
|
||||
return Select(state, TypeU32(state), high_nonzero,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), high, ConstantU32(state, 32)), low);
|
||||
}
|
||||
|
||||
uint32_t EmitIMul64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
|
||||
const auto lhs = ExtractPair(state, lhs_value);
|
||||
const auto rhs = ExtractPair(state, rhs_value);
|
||||
const auto low = Binary(state, spv::OpIMul, TypeU32(state), lhs.low, rhs.low);
|
||||
const auto high0 = EmitMulHigh(state, lhs.low, rhs.low, false);
|
||||
const auto high1 = Binary(state, spv::OpIMul, TypeU32(state), lhs.low, rhs.high);
|
||||
const auto high2 = Binary(state, spv::OpIMul, TypeU32(state), lhs.high, rhs.low);
|
||||
return MakePair(state, low,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state),
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), high0, high1), high2));
|
||||
}
|
||||
|
||||
uint32_t EmitISub64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
|
||||
const auto lhs = ExtractPair(state, lhs_value);
|
||||
const auto rhs = ExtractPair(state, rhs_value);
|
||||
const auto low = Binary(state, spv::OpISub, TypeU32(state), lhs.low, rhs.low);
|
||||
const auto borrow = Binary(state, spv::OpULessThan, TypeBool(state), lhs.low, rhs.low);
|
||||
const auto borrow_u32 =
|
||||
Select(state, TypeU32(state), borrow, ConstantU32(state, 1), ConstantU32(state, 0));
|
||||
const auto high0 = Binary(state, spv::OpISub, TypeU32(state), lhs.high, rhs.high);
|
||||
const auto high = Binary(state, spv::OpISub, TypeU32(state), high0, borrow_u32);
|
||||
return MakePair(state, low, high);
|
||||
}
|
||||
|
||||
uint32_t EmitIAdd64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
|
||||
const auto lhs = ExtractPair(state, lhs_value);
|
||||
const auto rhs = ExtractPair(state, rhs_value);
|
||||
const auto low_pair = state.builder.AllocateId();
|
||||
const auto low = state.builder.AllocateId();
|
||||
const auto carry = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), low_pair, lhs.low, rhs.low);
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, low_pair, 0);
|
||||
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), carry, low_pair, 1);
|
||||
const auto high0 = Binary(state, spv::OpIAdd, TypeU32(state), lhs.high, rhs.high);
|
||||
const auto high = Binary(state, spv::OpIAdd, TypeU32(state), high0, carry);
|
||||
return MakePair(state, low, high);
|
||||
// GLSL FindUMsb is restricted to 32-bit components.
|
||||
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
|
||||
const auto pair =
|
||||
ExtractPair(state, EmitExt(state, TypeU32Vector(state, 2), GLSLstd450FindUMsb, {unpacked}));
|
||||
const auto high_found =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, UINT32_MAX));
|
||||
return Select(state, TypeU32(state), high_found,
|
||||
Binary(state, spv::OpIAdd, TypeU32(state), pair.high, ConstantU32(state, 32)),
|
||||
pair.low);
|
||||
}
|
||||
|
||||
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0) {
|
||||
@@ -316,7 +192,7 @@ uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0) {
|
||||
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
|
||||
const auto converted = Unary(state, spv::OpFConvert, TypeF32(state), arg0);
|
||||
const auto source = EmitNative<spv::OpCompositeExtract, IR::Type::U32>(
|
||||
state, Unary(state, spv::OpBitcast, TypeU64(state), arg0), 1u);
|
||||
state, Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0), 1u);
|
||||
const auto exponent = EmitAndConstant(state, source, 0x7ff00000u);
|
||||
const auto overflow =
|
||||
Binary(state, spv::OpLogicalAnd, TypeBool(state),
|
||||
@@ -334,7 +210,21 @@ uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0) {
|
||||
EmitFlushF32DenormToSignedZero(state, arg0));
|
||||
}
|
||||
|
||||
uint32_t EmitCompositeConstructU64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
|
||||
arg1 = arg1.Resolve();
|
||||
return arg1.IsImmediate() && arg1.U32() == 0u
|
||||
? Unary(ctx.state, spv::OpUConvert, TypeU64(ctx.state), arg0)
|
||||
: PackU64(ctx.state, arg0, ctx.Def(arg1));
|
||||
}
|
||||
|
||||
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1) {
|
||||
const auto value = arg1.U32() == 0u ? arg0
|
||||
: Binary(state, spv::OpShiftRightLogical, TypeU64(state),
|
||||
arg0, ConstantU32(state, 32));
|
||||
return Unary(state, spv::OpUConvert, TypeU32(state), value);
|
||||
}
|
||||
|
||||
uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value arg1) {
|
||||
return EmitNative<spv::OpCompositeExtract, IR::Type::U32>(state, arg0, arg1.U32());
|
||||
}
|
||||
|
||||
@@ -366,33 +256,11 @@ uint32_t EmitIAbs32(EmitterState& state, uint32_t arg0) {
|
||||
return Select(state, TypeU32(state), negative, neg, value);
|
||||
}
|
||||
|
||||
uint32_t EmitShiftLeftLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
|
||||
auto& state = ctx.state;
|
||||
return arg1.Resolve().IsImmediate()
|
||||
? EmitConstantShift64(state, spv::OpShiftLeftLogical, arg0, arg1.Resolve().U32())
|
||||
: EmitShift64(state, spv::OpShiftLeftLogical, arg0, ctx.Def(arg1));
|
||||
}
|
||||
|
||||
uint32_t EmitShiftRightLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
|
||||
auto& state = ctx.state;
|
||||
return arg1.Resolve().IsImmediate()
|
||||
? EmitConstantShift64(state, spv::OpShiftRightLogical, arg0, arg1.Resolve().U32())
|
||||
: EmitShift64(state, spv::OpShiftRightLogical, arg0, ctx.Def(arg1));
|
||||
}
|
||||
|
||||
uint32_t EmitShiftRightArithmetic64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
|
||||
auto& state = ctx.state;
|
||||
return arg1.Resolve().IsImmediate()
|
||||
? EmitConstantShift64(state, spv::OpShiftRightArithmetic, arg0, arg1.Resolve().U32())
|
||||
: EmitShift64(state, spv::OpShiftRightArithmetic, arg0, ctx.Def(arg1));
|
||||
}
|
||||
|
||||
uint32_t EmitBitwiseAnd64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return Binary(state, spv::OpBitwiseAnd, TypeU64(state), arg0, arg1);
|
||||
}
|
||||
|
||||
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0) {
|
||||
const auto pair = ExtractPair(state, Unary(state, spv::OpBitCount, TypeU64(state), arg0));
|
||||
// Vulkan bit-count operands remain 32-bit, including when counting a U64.
|
||||
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0);
|
||||
const auto pair =
|
||||
ExtractPair(state, Unary(state, spv::OpBitCount, TypeU32Vector(state, 2), unpacked));
|
||||
return Binary(state, spv::OpIAdd, TypeU32(state), pair.low, pair.high);
|
||||
}
|
||||
|
||||
@@ -446,38 +314,6 @@ uint32_t EmitUMedTri32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32
|
||||
return EmitMed3(state, arg0, arg1, arg2, false);
|
||||
}
|
||||
|
||||
uint32_t EmitIEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareEqual64(state, arg0, arg1, false);
|
||||
}
|
||||
|
||||
uint32_t EmitINotEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareEqual64(state, arg0, arg1, true);
|
||||
}
|
||||
|
||||
uint32_t EmitULessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpULessThan, spv::OpULessThan);
|
||||
}
|
||||
|
||||
uint32_t EmitSLessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpSLessThan, spv::OpULessThan);
|
||||
}
|
||||
|
||||
uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThan);
|
||||
}
|
||||
|
||||
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpSLessThan, spv::OpULessThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpULessThan, spv::OpULessThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
|
||||
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThanEqual);
|
||||
}
|
||||
|
||||
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0) {
|
||||
return EmitNative<spv::OpFUnordNotEqual, IR::Type::U1>(state, arg0, arg0);
|
||||
}
|
||||
|
||||
@@ -125,20 +125,23 @@ uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
return EmitLogicalAndBool(state, selected, class_match);
|
||||
}
|
||||
|
||||
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
|
||||
const auto bits = EmitBitcastF32ToU32(state, value);
|
||||
const auto sign_bits = EmitAndConstant(state, bits, 0x80000000u);
|
||||
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
const auto exponent_bits = EmitAndConstant(state, abs_bits, 0x7f800000u);
|
||||
const auto mantissa_bits = EmitAndConstant(state, abs_bits, 0x007fffffu);
|
||||
const auto quiet_bits = EmitAndConstant(state, mantissa_bits, 0x00400000u);
|
||||
static uint32_t EmitClassMaskBits(EmitterState& state, uint32_t bits, uint32_t mask, bool half) {
|
||||
const uint32_t sign_mask = half ? 0x8000u : 0x80000000u;
|
||||
const uint32_t exponent_mask = half ? 0x7c00u : 0x7f800000u;
|
||||
const uint32_t mantissa_mask = half ? 0x03ffu : 0x007fffffu;
|
||||
const uint32_t quiet_mask = half ? 0x0200u : 0x00400000u;
|
||||
const auto sign_bits = EmitAndConstant(state, bits, sign_mask);
|
||||
const auto abs_bits = EmitAndConstant(state, bits, sign_mask - 1u);
|
||||
const auto exponent_bits = EmitAndConstant(state, abs_bits, exponent_mask);
|
||||
const auto mantissa_bits = EmitAndConstant(state, abs_bits, mantissa_mask);
|
||||
const auto quiet_bits = EmitAndConstant(state, mantissa_bits, quiet_mask);
|
||||
|
||||
const auto sign = EmitCompareU32Constant(state, spv::OpINotEqual, sign_bits, 0);
|
||||
const auto positive = EmitLogicalNotBool(state, sign);
|
||||
const auto exponent_zero = EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, 0);
|
||||
const auto exponent_nonzero = EmitLogicalNotBool(state, exponent_zero);
|
||||
const auto exponent_inf =
|
||||
EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, 0x7f800000u);
|
||||
EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, exponent_mask);
|
||||
const auto finite_exponent = EmitLogicalNotBool(state, exponent_inf);
|
||||
const auto mantissa_zero = EmitCompareU32Constant(state, spv::OpIEqual, mantissa_bits, 0);
|
||||
const auto mantissa_nonzero = EmitLogicalNotBool(state, mantissa_zero);
|
||||
@@ -178,6 +181,14 @@ uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
|
||||
EmitClassMaskBitMatch(state, mask, 9, EmitLogicalAndBool(state, inf, positive)));
|
||||
}
|
||||
|
||||
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
|
||||
return EmitClassMaskBits(state, EmitBitcastF32ToU32(state, value), mask, false);
|
||||
}
|
||||
|
||||
uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
|
||||
return EmitClassMaskBits(state, bits, mask, true);
|
||||
}
|
||||
|
||||
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
|
||||
const auto lhs_class = EmitClassifyF32(state, lhs);
|
||||
const auto rhs_class = EmitClassifyF32(state, rhs);
|
||||
|
||||
@@ -136,8 +136,8 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image) {
|
||||
EXIT("invalid image resource class");
|
||||
}
|
||||
const auto& info = ImageDimensionInfoFor(image.dimension);
|
||||
const auto scalar_type = image.atomic64 ? TypeScalarU64(state)
|
||||
: ImageScalarType(state, image.numeric_class);
|
||||
const auto scalar_type =
|
||||
image.atomic64 ? TypeU64(state) : ImageScalarType(state, image.numeric_class);
|
||||
return state.builder.Type(spv::OpTypeImage, scalar_type,
|
||||
info.spirv_dimension, image.depth_compare ? 1u : 0u, info.arrayed,
|
||||
info.multisampled, sampled, format);
|
||||
|
||||
@@ -19,8 +19,8 @@ uint32_t Vector(EmitterState& s, uint32_t type, std::span<const uint32_t> compon
|
||||
|
||||
uint32_t NodeWord(EmitterState& s, uint32_t block, uint32_t word) {
|
||||
const auto offset = Binary(s, spv::OpShiftLeftLogical, TypeU32(s), word, ConstantU32(s, 2));
|
||||
const auto address = Binary(s, spv::OpIAdd, TypeScalarU64(s), block,
|
||||
Unary(s, spv::OpUConvert, TypeScalarU64(s), offset));
|
||||
const auto address =
|
||||
Binary(s, spv::OpIAdd, TypeU64(s), block, Unary(s, spv::OpUConvert, TypeU64(s), offset));
|
||||
const auto pointer = Unary(s, spv::OpConvertUToPtr, TypePhysicalU32Pointer(s), address);
|
||||
const auto result = s.builder.AllocateId();
|
||||
s.builder.AddFunction(spv::OpLoad, TypeU32(s), result, pointer, spv::MemoryAccessAlignedMask, 4u);
|
||||
@@ -224,7 +224,7 @@ uint32_t Boxes(EmitterState& s, uint32_t first, uint32_t second, uint32_t half,
|
||||
|
||||
void DefineBvhIntersect(EmitterState& s) {
|
||||
if (!s.requirements.bvh) return;
|
||||
const auto u = TypeU32(s), f = TypeF32(s), b = TypeBool(s), wide = TypeScalarU64(s);
|
||||
const auto u = TypeU32(s), f = TypeF32(s), b = TypeBool(s), wide = TypeU64(s);
|
||||
const auto vec4 = TypeU32Vector(s, 4), vec3 = TypeF32Vector(s, 3);
|
||||
const auto signature = s.builder.Type(spv::OpTypeFunction, vec4, vec4, wide, f, vec3, vec3, vec3);
|
||||
s.bvh_intersect_function = s.builder.AllocateId();
|
||||
@@ -260,22 +260,24 @@ void DefineBvhIntersect(EmitterState& s) {
|
||||
const auto bary = Binary(s, spv::OpINotEqual, b, and_bits(words[3], 0x01000000), ConstantU32(s, 0));
|
||||
const auto sort = Binary(s, spv::OpINotEqual, b, and_bits(words[1], 0x80000000), ConstantU32(s, 0));
|
||||
const auto grow = and_bits(shr(words[1], 23), 0xff);
|
||||
const auto base = Binary(s, spv::OpShiftLeftLogical, wide,
|
||||
DeviceAddressFromWords(s, words[0], and_bits(words[1], 0xff)), ConstantDeviceAddress(s, 8));
|
||||
const auto index = Binary(s, spv::OpShiftRightLogical, wide, node, ConstantDeviceAddress(s, 3));
|
||||
const auto base = Binary(s, spv::OpShiftLeftLogical, wide,
|
||||
PackU64(s, words[0], and_bits(words[1], 0xff)), ConstantU32(s, 8));
|
||||
const auto index = Binary(s, spv::OpShiftRightLogical, wide, node, ConstantU32(s, 3));
|
||||
const auto last_index = Binary(s, spv::OpIAdd, wide, index,
|
||||
Select(s, wide, full, ConstantDeviceAddress(s, 1), ConstantDeviceAddress(s, 0)));
|
||||
Select(s, wide, full, ConstantU64(s, 1), ConstantU64(s, 0)));
|
||||
const auto address = Binary(s, spv::OpIAdd, wide, base,
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, index, ConstantDeviceAddress(s, 6)));
|
||||
const auto last_address = Binary(s, spv::OpIAdd, wide, base,
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, last_index, ConstantDeviceAddress(s, 6)));
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, index, ConstantU32(s, 6)));
|
||||
const auto last_address =
|
||||
Binary(s, spv::OpIAdd, wide, base,
|
||||
Binary(s, spv::OpShiftLeftLogical, wide, last_index, ConstantU32(s, 6)));
|
||||
auto valid = Binary(s, spv::OpIEqual, b, shr(words[3], 28), ConstantU32(s, 8));
|
||||
const auto require = [&](uint32_t condition) {
|
||||
valid = Binary(s, spv::OpLogicalAnd, b, valid, condition);
|
||||
};
|
||||
require(Binary(s, spv::OpULessThanEqual, b, kind, ConstantU32(s, 5)));
|
||||
require(Binary(s, spv::OpULessThanEqual, b, last_index, DeviceAddressFromWords(s, words[2], and_bits(words[3], 0x3ff))));
|
||||
require(Binary(s, spv::OpULessThan, b, last_address, ConstantDeviceAddress(s, uint64_t{1} << 48)));
|
||||
require(Binary(s, spv::OpULessThanEqual, b, last_index,
|
||||
PackU64(s, words[2], and_bits(words[3], 0x3ff))));
|
||||
require(Binary(s, spv::OpULessThan, b, last_address, ConstantU64(s, uint64_t {1} << 48)));
|
||||
require(Unary(s, spv::OpLogicalNot, b, Unary(s, spv::OpIsNan, b, extent)));
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
for (const auto value: {origin[i], direction[i], inverse[i]})
|
||||
@@ -292,13 +294,13 @@ void DefineBvhIntersect(EmitterState& s) {
|
||||
const auto invalid = Vector(s, vec4, invalid_words);
|
||||
const auto result = EmitValueOrDefaultIfCondition(s, valid, vec4, invalid, [&] {
|
||||
const auto first = GetBdaPointer(s, address);
|
||||
const auto second = EmitValueOrDefaultIfCondition(s, full, wide, ConstantDeviceAddress(s, 0), [&] {
|
||||
return GetBdaPointer(s, Binary(s, spv::OpIAdd, wide, address, ConstantDeviceAddress(s, 64)));
|
||||
const auto second = EmitValueOrDefaultIfCondition(s, full, wide, ConstantU64(s, 0), [&] {
|
||||
return GetBdaPointer(s, Binary(s, spv::OpIAdd, wide, address, ConstantU64(s, 64)));
|
||||
});
|
||||
const auto present = Binary(s, spv::OpLogicalAnd, b,
|
||||
Binary(s, spv::OpINotEqual, b, first, ConstantDeviceAddress(s, 0)),
|
||||
const auto present = Binary(
|
||||
s, spv::OpLogicalAnd, b, Binary(s, spv::OpINotEqual, b, first, ConstantU64(s, 0)),
|
||||
Binary(s, spv::OpLogicalOr, b, Unary(s, spv::OpLogicalNot, b, full),
|
||||
Binary(s, spv::OpINotEqual, b, second, ConstantDeviceAddress(s, 0))));
|
||||
Binary(s, spv::OpINotEqual, b, second, ConstantU64(s, 0))));
|
||||
return EmitValueOrDefaultIfCondition(s, present, vec4, invalid, [&] {
|
||||
const auto tri_label = s.builder.AllocateId(), box_label = s.builder.AllocateId();
|
||||
const auto merge = s.builder.AllocateId();
|
||||
@@ -336,9 +338,8 @@ uint32_t EmitBvhIntersect(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return Vector(s, TypeF32Vector(s, 3), values);
|
||||
};
|
||||
const auto descriptor = ctx.Arg(inst, 0);
|
||||
const auto node = node_words == 2u
|
||||
? DeviceAddressFromWords(s, ctx.Arg(*ray, 0), ctx.Arg(*ray, 1))
|
||||
: Unary(s, spv::OpUConvert, TypeScalarU64(s), ctx.Arg(*ray, 0));
|
||||
const auto node = node_words == 2u ? PackU64(s, ctx.Arg(*ray, 0), ctx.Arg(*ray, 1))
|
||||
: Unary(s, spv::OpUConvert, TypeU64(s), ctx.Arg(*ray, 0));
|
||||
const auto extent = Unary(s, spv::OpBitcast, TypeF32(s), ctx.Arg(*ray, node_words));
|
||||
const auto origin = vector(node_words + 1u), direction = vector(node_words + 4u),
|
||||
inverse = vector(node_words + 7u);
|
||||
|
||||
@@ -430,16 +430,16 @@ uint32_t EmitWqmU64(EmitterState& state, uint32_t value) {
|
||||
const auto quad_bits = state.builder.AllocateId();
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU64(state), shifted_one, value,
|
||||
ConstantU64(state, 0x0000000100000001ull));
|
||||
ConstantU32(state, 1));
|
||||
state.builder.AddFunction(spv::OpBitwiseOr, TypeU64(state), merged_one, value, shifted_one);
|
||||
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU64(state), shifted_two, merged_one,
|
||||
ConstantU64(state, 0x0000000200000002ull));
|
||||
ConstantU32(state, 2));
|
||||
state.builder.AddFunction(spv::OpBitwiseOr, TypeU64(state), merged_two, merged_one,
|
||||
shifted_two);
|
||||
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU64(state), quad_bits, merged_two,
|
||||
ConstantU64(state, 0x1111111111111111ull));
|
||||
state.builder.AddFunction(spv::OpIMul, TypeU64(state), result, quad_bits,
|
||||
ConstantU64(state, 0x0000000f0000000full));
|
||||
ConstantU64(state, 15));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -930,9 +932,9 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
ctx.Define(inst, EmitValueOrDefaultIfCondition(
|
||||
state, ctx.Arg(inst, inst.NumArgs() - 1), result_type, zero, [&]() {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
const auto pointer_type = state.builder.Type(
|
||||
spv::OpTypePointer, spv::StorageClassImage,
|
||||
image.atomic64 ? TypeScalarU64(state) : TypeU32(state));
|
||||
const auto pointer_type =
|
||||
state.builder.Type(spv::OpTypePointer, spv::StorageClassImage,
|
||||
image.atomic64 ? TypeU64(state) : TypeU32(state));
|
||||
state.builder.AddFunction(spv::OpImageTexelPointer, pointer_type, pointer,
|
||||
ImageDescriptorPointer(state, mem.resource),
|
||||
CoordU32(ctx, mem, *address, dimension),
|
||||
|
||||
@@ -34,16 +34,16 @@ uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0);
|
||||
uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0);
|
||||
EMIT_NATIVE(ConvertF32U32, OpConvertUToF, F32, uint32_t)
|
||||
EMIT_NATIVE(ConvertF64U32, OpConvertUToF, F64, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructU64, OpCompositeConstruct, U64, uint32_t, uint32_t)
|
||||
uint32_t EmitCompositeConstructU64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
|
||||
EMIT_NATIVE(CompositeConstructU32x2, OpCompositeConstruct, U32x2, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructU32x3, OpCompositeConstruct, U32x3, uint32_t, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructF32x2, OpCompositeConstruct, F32x2, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(CompositeConstructU32x4, OpCompositeConstruct, U32x4, uint32_t, uint32_t, uint32_t,
|
||||
uint32_t)
|
||||
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1);
|
||||
inline constexpr auto EmitCompositeExtractU32x2 = EmitCompositeExtractU64;
|
||||
inline constexpr auto EmitCompositeExtractU32x3 = EmitCompositeExtractU64;
|
||||
inline constexpr auto EmitCompositeExtractU32x4 = EmitCompositeExtractU64;
|
||||
uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value arg1);
|
||||
inline constexpr auto EmitCompositeExtractU32x3 = EmitCompositeExtractU32x2;
|
||||
inline constexpr auto EmitCompositeExtractU32x4 = EmitCompositeExtractU32x2;
|
||||
inline constexpr auto EmitPackHalf2x16 = EmitGlsl<GLSLstd450PackHalf2x16, IR::Type::U32, uint32_t>;
|
||||
inline constexpr auto EmitPackSnorm2x16 =
|
||||
EmitGlsl<GLSLstd450PackSnorm2x16, IR::Type::U32, uint32_t>;
|
||||
@@ -63,9 +63,9 @@ EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(UDiv32, OpUDiv, U32, uint32_t, uint32_t)
|
||||
uint32_t EmitIAdd64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitISub64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitIMul64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
EMIT_NATIVE(IAdd64, OpIAdd, U64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ISub64, OpISub, U64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(IMul64, OpIMul, U64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(IAddCarry32, OpIAddCarry, U32x2, uint32_t, uint32_t)
|
||||
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitUMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
@@ -73,14 +73,26 @@ uint32_t EmitIAbs32(EmitterState& state, uint32_t arg0);
|
||||
EMIT_NATIVE(ShiftLeftLogical32, OpShiftLeftLogical, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ShiftRightLogical32, OpShiftRightLogical, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ShiftRightArithmetic32, OpShiftRightArithmetic, U32, uint32_t, uint32_t)
|
||||
uint32_t EmitShiftLeftLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
|
||||
uint32_t EmitShiftRightLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
|
||||
uint32_t EmitShiftRightArithmetic64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
|
||||
template <spv::Op opcode>
|
||||
uint32_t EmitShift64(ValueEmitContext& ctx, uint32_t value, IR::Value shift) {
|
||||
shift = shift.Resolve();
|
||||
if (shift.IsImmediate()) {
|
||||
const auto amount = shift.U32() & 63u;
|
||||
return amount == 0u ? value
|
||||
: Binary(ctx.state, opcode, TypeU64(ctx.state), value,
|
||||
ConstantU32(ctx.state, amount));
|
||||
}
|
||||
return Binary(ctx.state, opcode, TypeU64(ctx.state), value,
|
||||
EmitAndConstant(ctx.state, ctx.Def(shift), 63u));
|
||||
}
|
||||
inline constexpr auto EmitShiftLeftLogical64 = EmitShift64<spv::OpShiftLeftLogical>;
|
||||
inline constexpr auto EmitShiftRightLogical64 = EmitShift64<spv::OpShiftRightLogical>;
|
||||
inline constexpr auto EmitShiftRightArithmetic64 = EmitShift64<spv::OpShiftRightArithmetic>;
|
||||
EMIT_NATIVE(BitwiseAnd32, OpBitwiseAnd, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(BitwiseOr32, OpBitwiseOr, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(BitwiseXor32, OpBitwiseXor, U32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(BitwiseNot32, OpNot, U32, uint32_t)
|
||||
uint32_t EmitBitwiseAnd64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
EMIT_NATIVE(BitwiseAnd64, OpBitwiseAnd, U64, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(BitReverse32, OpBitReverse, U32, uint32_t)
|
||||
EMIT_NATIVE(BitCount32, OpBitCount, U32, uint32_t)
|
||||
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0);
|
||||
@@ -107,14 +119,14 @@ EMIT_NATIVE(UGreaterThan32, OpUGreaterThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(INotEqual32, OpINotEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SGreaterThanEqual32, OpSGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(UGreaterThanEqual32, OpUGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
uint32_t EmitIEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitINotEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitULessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitSLessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
EMIT_NATIVE(IEqual64, OpIEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(INotEqual64, OpINotEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ULessThan64, OpULessThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SLessThan64, OpSLessThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(SLessThanEqual64, OpSLessThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(ULessThanEqual64, OpULessThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(UGreaterThanEqual64, OpUGreaterThanEqual, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(UGreaterThan64, OpUGreaterThan, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(LogicalOr, OpLogicalOr, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(LogicalAnd, OpLogicalAnd, U1, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(LogicalXor, OpLogicalNotEqual, U1, uint32_t, uint32_t)
|
||||
@@ -152,6 +164,7 @@ inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFO
|
||||
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
|
||||
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0);
|
||||
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
|
||||
inline constexpr auto EmitFPCmpClass16 = EmitClassMaskF16;
|
||||
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
|
||||
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
|
||||
@@ -160,8 +173,8 @@ EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
|
||||
inline constexpr auto EmitFPFma64 =
|
||||
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
|
||||
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0);
|
||||
inline constexpr auto EmitFPFma32 =
|
||||
EmitGlsl<GLSLstd450Fma, IR::Type::F32, uint32_t, uint32_t, uint32_t>;
|
||||
uint32_t EmitFPFma32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2);
|
||||
uint32_t EmitFPMad32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2);
|
||||
uint32_t EmitFPMin32(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitFPMax32(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
uint32_t EmitFPMin64(EmitterState& state, uint32_t arg0, uint32_t arg1);
|
||||
@@ -321,15 +334,21 @@ inline constexpr auto EmitImageSampleRaw = EmitImage;
|
||||
inline constexpr auto EmitImageGatherRaw = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicCompareSwap32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSwap32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSwap64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicIAdd32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicIAdd64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMin64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicSMax32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicUMax64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicAnd64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicOr64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicXor64 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicFMin32 = EmitImage;
|
||||
inline constexpr auto EmitImageAtomicFMax32 = EmitImage;
|
||||
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst);
|
||||
|
||||
@@ -98,7 +98,7 @@ struct EmitterState {
|
||||
uint32_t void_type = 0;
|
||||
uint32_t bool_type = 0;
|
||||
uint32_t u32_type = 0;
|
||||
uint32_t native_u64_type = 0;
|
||||
uint32_t u64_type = 0;
|
||||
uint32_t i32_type = 0;
|
||||
uint32_t f32_type = 0;
|
||||
uint32_t f64_type = 0;
|
||||
@@ -168,7 +168,6 @@ uint32_t TypeBool(EmitterState& state);
|
||||
uint32_t TypeBoolVector(EmitterState& state, uint32_t components);
|
||||
uint32_t TypeU32(EmitterState& state);
|
||||
uint32_t TypeU64(EmitterState& state);
|
||||
uint32_t TypeScalarU64(EmitterState& state);
|
||||
uint32_t TypeU32Pair(EmitterState& state);
|
||||
uint32_t TypeI32(EmitterState& state);
|
||||
uint32_t TypeI32Pair(EmitterState& state);
|
||||
@@ -221,6 +220,12 @@ inline uint32_t Unary(EmitterState& state, spv::Op opcode, uint32_t type, uint32
|
||||
return result;
|
||||
}
|
||||
|
||||
inline uint32_t PackU64(EmitterState& state, uint32_t low, uint32_t high) {
|
||||
const auto pair = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), pair, low, high);
|
||||
return Unary(state, spv::OpBitcast, TypeU64(state), pair);
|
||||
}
|
||||
|
||||
inline uint32_t Binary(EmitterState& state, spv::Op opcode, uint32_t type, uint32_t lhs,
|
||||
uint32_t rhs) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
@@ -463,6 +468,9 @@ void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind);
|
||||
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
bool max_value);
|
||||
|
||||
uint32_t EmitDsFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
bool max_value);
|
||||
|
||||
uint32_t EmitDsSwizzleTargetLane(EmitterState& state, uint32_t subid, uint32_t control);
|
||||
|
||||
inline constexpr auto EmitSelectValueU32 =
|
||||
@@ -507,6 +515,7 @@ uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
|
||||
uint32_t class_match);
|
||||
|
||||
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask);
|
||||
uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask);
|
||||
|
||||
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value);
|
||||
|
||||
@@ -527,8 +536,6 @@ void EmitProgram(EmitterState& state);
|
||||
void DefineGetBdaPointer(EmitterState& state);
|
||||
void DefineBvhIntersect(EmitterState& state);
|
||||
uint32_t GetBdaPointer(EmitterState& state, uint32_t address);
|
||||
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value);
|
||||
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high);
|
||||
|
||||
// These templates accept local lambdas from several emitter translation units.
|
||||
template <typename Fn>
|
||||
|
||||
@@ -133,22 +133,22 @@ uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Mem
|
||||
}
|
||||
uint32_t address = 0;
|
||||
if (mem.address_is_full) {
|
||||
address = DeviceAddressFromWords(state, low, ctx.Arg(inst, 2));
|
||||
address = PackU64(state, low, ctx.Arg(inst, 2));
|
||||
} else {
|
||||
const auto* handle = inst.Arg(0).Resolve().TryInstruction();
|
||||
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetAddressResource ||
|
||||
handle->NumArgs() != 2) {
|
||||
ctx.Fail(inst, "has no address base pair");
|
||||
return ConstantDeviceAddress(state, 0);
|
||||
return ConstantU64(state, 0);
|
||||
}
|
||||
auto base_low = ctx.Arg(*handle, 0);
|
||||
if (mem.kind == IR::ResourceKind::ScalarAddress) {
|
||||
base_low = Binary(state, spv::OpBitwiseAnd, TypeU32(state), base_low,
|
||||
ConstantU32(state, ~3u));
|
||||
}
|
||||
const auto base = DeviceAddressFromWords(state, base_low, ctx.Arg(*handle, 1));
|
||||
address = Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), low));
|
||||
const auto base = PackU64(state, base_low, ctx.Arg(*handle, 1));
|
||||
address = Binary(state, spv::OpIAdd, TypeU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeU64(state), low));
|
||||
}
|
||||
auto immediate = static_cast<int32_t>(mem.offset);
|
||||
if (mem.kind == IR::ResourceKind::ScalarAddress) {
|
||||
@@ -156,9 +156,8 @@ uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Mem
|
||||
}
|
||||
return immediate == 0
|
||||
? address
|
||||
: Binary(state, spv::OpIAdd, TypeScalarU64(state), address,
|
||||
ConstantDeviceAddress(
|
||||
state, static_cast<uint64_t>(static_cast<int64_t>(immediate))));
|
||||
: Binary(state, spv::OpIAdd, TypeU64(state), address,
|
||||
ConstantU64(state, static_cast<uint64_t>(static_cast<int64_t>(immediate))));
|
||||
}
|
||||
|
||||
uint32_t FaultElementPointer(EmitterState& state, uint32_t index) {
|
||||
@@ -181,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, ConstantDeviceAddress(state, 0));
|
||||
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, TypeScalarU64(state), address,
|
||||
ConstantDeviceAddress(state, ~uint64_t {3}));
|
||||
const auto first = LoadBdaDword(ctx, aligned);
|
||||
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
|
||||
ConstantU64(state, ~uint64_t {3}));
|
||||
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,8 +228,8 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
|
||||
: Binary(state, bits == 16u ? spv::OpUGreaterThan : spv::OpINotEqual,
|
||||
TypeBool(state), byte, ConstantU32(state, bits == 16u ? 2u : 0u));
|
||||
const auto second = EmitValueOrZeroIfCondition(state, crosses, [&]() {
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeScalarU64(state), aligned,
|
||||
ConstantDeviceAddress(state, sizeof(uint32_t))));
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), aligned,
|
||||
ConstantU64(state, sizeof(uint32_t))), coherent);
|
||||
});
|
||||
const auto shift =
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte, ConstantU32(state, 3));
|
||||
@@ -710,24 +726,37 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
|
||||
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
|
||||
const Format::BufferFormatInfo& info,
|
||||
uint32_t component, uint32_t data) {
|
||||
auto& state = ctx.state;
|
||||
const auto bits = info.component_bits[component];
|
||||
if (info.type == Format::ComponentType::Unorm) {
|
||||
const auto value = EmitBitCastF32U32(ctx.state, data);
|
||||
const auto clamped = EmitFPMin32(
|
||||
ctx.state, EmitFPMax32(ctx.state, value, ConstantF32Value(ctx.state, 0.0f)),
|
||||
ConstantF32Value(ctx.state, 1.0f));
|
||||
const auto scaled = EmitFPMul32(
|
||||
ctx.state, clamped, ConstantF32Value(ctx.state, float((1u << bits) - 1u)));
|
||||
return Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state),
|
||||
EmitFPRoundEven32(ctx.state, scaled));
|
||||
const bool normalized = info.type == Format::ComponentType::Unorm ||
|
||||
info.type == Format::ComponentType::Snorm;
|
||||
const bool scaled = info.type == Format::ComponentType::Uscaled ||
|
||||
info.type == Format::ComponentType::Sscaled;
|
||||
if (normalized || scaled) {
|
||||
// Two-bit packed channels are unsigned even in signed formats.
|
||||
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Sscaled);
|
||||
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
|
||||
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
|
||||
: (is_signed ? -max_value - 1.0f : 0.0f);
|
||||
const auto upper = normalized ? 1.0f : max_value;
|
||||
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
|
||||
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
|
||||
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
|
||||
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
|
||||
if (normalized) {
|
||||
value = EmitFPRoundEven32(
|
||||
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
|
||||
}
|
||||
// Float-to-integer conversion truncates scaled values toward zero.
|
||||
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
|
||||
is_signed ? TypeI32(state) : TypeU32(state), value);
|
||||
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
|
||||
}
|
||||
if (bits == 16u && (info.type == Format::ComponentType::Snorm ||
|
||||
info.type == Format::ComponentType::Float)) {
|
||||
const auto value = EmitBitCastF32U32(ctx.state, data);
|
||||
const auto pair = EmitCompositeConstructF32x2(ctx.state, value,
|
||||
ConstantF32Value(ctx.state, 0.0f));
|
||||
return info.type == Format::ComponentType::Float ? EmitPackHalf2x16(ctx.state, pair)
|
||||
: EmitPackSnorm2x16(ctx.state, pair);
|
||||
if (bits == 16u && info.type == Format::ComponentType::Float) {
|
||||
const auto pair = EmitCompositeConstructF32x2(
|
||||
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
|
||||
return EmitPackHalf2x16(state, pair);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -792,26 +821,25 @@ uint32_t LoadIndirectScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto word1 = ctx.Arg(handle, 1);
|
||||
const auto stride = EmitBitFieldUExtract(state, word1, ConstantU32(state, 16),
|
||||
ConstantU32(state, 14));
|
||||
const auto size = Binary(
|
||||
state, spv::OpIMul, TypeScalarU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state),
|
||||
EmitUMax32(state, stride, ConstantU32(state, 1))),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), ctx.Arg(handle, 2)));
|
||||
const auto offset = Binary(
|
||||
state, spv::OpIAdd, TypeScalarU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1),
|
||||
ConstantU32(state, ~3u))),
|
||||
ConstantDeviceAddress(state, ctx.Memory(inst).offset & ~3u));
|
||||
const auto end = Binary(state, spv::OpIAdd, TypeScalarU64(state), offset,
|
||||
ConstantDeviceAddress(state, sizeof(uint32_t)));
|
||||
const auto size = Binary(state, spv::OpIMul, TypeU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeU64(state),
|
||||
EmitUMax32(state, stride, ConstantU32(state, 1))),
|
||||
Unary(state, spv::OpUConvert, TypeU64(state), ctx.Arg(handle, 2)));
|
||||
const auto offset = Binary(state, spv::OpIAdd, TypeU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeU64(state),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ctx.Arg(inst, 1), ConstantU32(state, ~3u))),
|
||||
ConstantU64(state, ctx.Memory(inst).offset & ~3u));
|
||||
const auto end =
|
||||
Binary(state, spv::OpIAdd, TypeU64(state), offset, ConstantU64(state, sizeof(uint32_t)));
|
||||
return EmitValueOrZeroIfCondition(
|
||||
state, Binary(state, spv::OpULessThanEqual, TypeBool(state), end, size), [&]() {
|
||||
const auto base = DeviceAddressFromWords(
|
||||
state, Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(handle, 0),
|
||||
ConstantU32(state, ~3u)),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1, ConstantU32(state, 0xffffu)));
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeScalarU64(state), base, offset));
|
||||
const auto base = PackU64(state,
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
ctx.Arg(handle, 0), ConstantU32(state, ~3u)),
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1,
|
||||
ConstantU32(state, 0xffffu)));
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), base, offset));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -852,21 +880,21 @@ IndirectBufferAccess PrepareIndirectBuffer(ValueEmitContext& ctx, const IR::Inst
|
||||
const auto address = CalculateBufferAddress(state, index, ctx.Arg(inst, 2), soffset,
|
||||
ctx.Memory(inst).offset, stride, swizzle,
|
||||
field(word3, 21, 2));
|
||||
const auto base = DeviceAddressFromWords(state, ctx.Arg(handle, 0), field(word1, 0, 16));
|
||||
const auto base = PackU64(state, ctx.Arg(handle, 0), field(word1, 0, 16));
|
||||
const auto raw_records = Binary(state, spv::OpISub, TypeU32(state), records, soffset);
|
||||
return {
|
||||
.address = Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), address.byte)),
|
||||
.offset = address.offset,
|
||||
.stride = stride,
|
||||
.swizzle = swizzle,
|
||||
.format = field(word3, 12, 7),
|
||||
.selector = ctx.Memory(inst).formatted ? field(word3, 0, 3) : 0u,
|
||||
.mode = field(word3, 28, 2),
|
||||
.records = records,
|
||||
.index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records),
|
||||
.address = Binary(state, spv::OpIAdd, TypeU64(state), base,
|
||||
Unary(state, spv::OpUConvert, TypeU64(state), address.byte)),
|
||||
.offset = address.offset,
|
||||
.stride = stride,
|
||||
.swizzle = swizzle,
|
||||
.format = field(word3, 12, 7),
|
||||
.selector = ctx.Memory(inst).formatted ? field(word3, 0, 3) : 0u,
|
||||
.mode = field(word3, 28, 2),
|
||||
.records = records,
|
||||
.index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records),
|
||||
.scalar_in_bounds = Binary(state, spv::OpULessThanEqual, TypeBool(state), soffset, records),
|
||||
.raw_records = raw_records,
|
||||
.raw_records = raw_records,
|
||||
.raw_index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, raw_records),
|
||||
};
|
||||
}
|
||||
@@ -912,13 +940,13 @@ uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_
|
||||
const auto buffer = PrepareIndirectBuffer(ctx, inst);
|
||||
const auto valid_format = Binary(state, spv::OpINotEqual, TypeBool(state), buffer.format,
|
||||
ConstantU32(state, 0));
|
||||
const auto base = Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), buffer.address,
|
||||
ConstantDeviceAddress(state, ~uint64_t {3}));
|
||||
const auto base = Binary(state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
ConstantU64(state, ~uint64_t {3}));
|
||||
std::array<uint32_t, 4> values {};
|
||||
for (uint32_t component = 0; component < components; ++component) {
|
||||
const auto address = component == 0u ? base : Binary(
|
||||
state, spv::OpIAdd, TypeScalarU64(state), base,
|
||||
ConstantDeviceAddress(state, component * 4u));
|
||||
const auto address = component == 0u ? base
|
||||
: Binary(state, spv::OpIAdd, TypeU64(state), base,
|
||||
ConstantU64(state, component * 4u));
|
||||
values[component] = EmitValueOrZeroIfCondition(
|
||||
state, AndCondition(state, valid_format,
|
||||
IndirectBufferInBounds(state, buffer, component * 4u, 4u, false)),
|
||||
@@ -946,13 +974,14 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
TypeU32(state), constant, [&]() {
|
||||
const auto in_bounds = IndirectBufferInBounds(state, buffer, 0u, info.byte_size, true);
|
||||
const auto base = Binary(
|
||||
state, spv::OpBitwiseAnd, TypeScalarU64(state), buffer.address,
|
||||
ConstantDeviceAddress(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
|
||||
state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
|
||||
ConstantU64(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
|
||||
const auto load = [&](uint32_t component, uint32_t bits) {
|
||||
const auto offset = Format::GetFormatComponentByteOffset(info, component);
|
||||
const auto address = offset == 0u ? base : Binary(
|
||||
state, spv::OpIAdd, TypeScalarU64(state), base,
|
||||
ConstantDeviceAddress(state, offset));
|
||||
const auto address = offset == 0u
|
||||
? base
|
||||
: Binary(state, spv::OpIAdd, TypeU64(state),
|
||||
base, ConstantU64(state, offset));
|
||||
return LoadBda(ctx, address, in_bounds, bits);
|
||||
};
|
||||
const auto emit_component = [&](uint32_t component) {
|
||||
@@ -1072,23 +1101,9 @@ void StoreWideShared(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compo
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value) {
|
||||
return state.builder.Constant(spv::OpConstant, TypeScalarU64(state),
|
||||
static_cast<uint32_t>(value),
|
||||
static_cast<uint32_t>(value >> 32u));
|
||||
}
|
||||
|
||||
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high) {
|
||||
const auto low64 = Unary(state, spv::OpUConvert, TypeScalarU64(state), low);
|
||||
const auto high64 = Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state),
|
||||
Unary(state, spv::OpUConvert, TypeScalarU64(state), high),
|
||||
ConstantDeviceAddress(state, 32));
|
||||
return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64);
|
||||
}
|
||||
|
||||
uint32_t GetBdaPointer(EmitterState& state, uint32_t address) {
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), result,
|
||||
state.builder.AddFunction(spv::OpFunctionCall, TypeU64(state), result,
|
||||
state.bda_pointer_function, address);
|
||||
return result;
|
||||
}
|
||||
@@ -1097,7 +1112,7 @@ void DefineGetBdaPointer(EmitterState& state) {
|
||||
if (!state.program.info.uses_dma) {
|
||||
return;
|
||||
}
|
||||
const auto type = TypeScalarU64(state);
|
||||
const auto type = TypeU64(state);
|
||||
const auto function_type = state.builder.Type(spv::OpTypeFunction, type, type);
|
||||
state.bda_pointer_function = state.builder.AllocateId();
|
||||
const auto address = state.builder.AllocateId();
|
||||
@@ -1109,14 +1124,14 @@ void DefineGetBdaPointer(EmitterState& state) {
|
||||
EmitLabel(state, entry_label);
|
||||
|
||||
const auto extended = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), address,
|
||||
ConstantDeviceAddress(state, LibKernel::Memory::kExtendedMemoryBase));
|
||||
const auto packed = Select(state, type, extended,
|
||||
Binary(state, spv::OpISub, type, address,
|
||||
ConstantDeviceAddress(state,
|
||||
LibKernel::Memory::kExtendedMemoryBase - LOWER_ADDRESS_SIZE)),
|
||||
address);
|
||||
ConstantU64(state, LibKernel::Memory::kExtendedMemoryBase));
|
||||
const auto packed = Select(
|
||||
state, type, extended,
|
||||
Binary(state, spv::OpISub, type, address,
|
||||
ConstantU64(state, LibKernel::Memory::kExtendedMemoryBase - LOWER_ADDRESS_SIZE)),
|
||||
address);
|
||||
const auto page64 = Binary(state, spv::OpShiftRightLogical, type, packed,
|
||||
ConstantDeviceAddress(state, BufferCache::CACHING_PAGEBITS));
|
||||
ConstantU32(state, BufferCache::CACHING_PAGEBITS));
|
||||
const auto page = Unary(state, spv::OpUConvert, TypeU32(state), page64);
|
||||
const auto entry_pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state, 64),
|
||||
@@ -1124,8 +1139,7 @@ void DefineGetBdaPointer(EmitterState& state) {
|
||||
page);
|
||||
const auto base = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpLoad, type, base, entry_pointer);
|
||||
const auto missing =
|
||||
Binary(state, spv::OpIEqual, TypeBool(state), base, ConstantDeviceAddress(state, 0));
|
||||
const auto missing = Binary(state, spv::OpIEqual, TypeBool(state), base, ConstantU64(state, 0));
|
||||
const auto fault_label = state.builder.AllocateId();
|
||||
const auto available_label = state.builder.AllocateId();
|
||||
const auto merge_label = state.builder.AllocateId();
|
||||
@@ -1138,14 +1152,14 @@ void DefineGetBdaPointer(EmitterState& state) {
|
||||
|
||||
EmitLabel(state, available_label);
|
||||
const auto offset = Binary(state, spv::OpBitwiseAnd, type, address,
|
||||
ConstantDeviceAddress(state, BufferCache::CACHING_PAGESIZE - 1));
|
||||
ConstantU64(state, BufferCache::CACHING_PAGESIZE - 1));
|
||||
const auto available = Binary(state, spv::OpIAdd, type, base, offset);
|
||||
state.builder.AddFunction(spv::OpBranch, merge_label);
|
||||
|
||||
EmitLabel(state, merge_label);
|
||||
const auto result = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpPhi, type, result, ConstantDeviceAddress(state, 0),
|
||||
fault_label, available, available_label);
|
||||
state.builder.AddFunction(spv::OpPhi, type, result, ConstantU64(state, 0), fault_label,
|
||||
available, available_label);
|
||||
state.builder.AddFunction(spv::OpReturnValue, result);
|
||||
state.builder.AddFunction(spv::OpFunctionEnd);
|
||||
}
|
||||
@@ -1203,12 +1217,12 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
semantics = spv::MemorySemanticsUniformMemoryMask;
|
||||
}
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain,
|
||||
TypePointer(state, state.lds_storage_class, TypeScalarU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto value = Unary(state, spv::OpBitcast, TypeScalarU64(state), ctx.Arg(inst, 1));
|
||||
state.builder.AddFunction(
|
||||
SpirvAtomicOpcode(inst.GetOpcode()), TypeScalarU64(state), state.builder.AllocateId(),
|
||||
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
|
||||
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
|
||||
const auto value = ctx.Arg(inst, 1);
|
||||
state.builder.AddFunction(
|
||||
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
|
||||
pointer, ConstantU32(state, spv::ScopeWorkgroup),
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
|
||||
});
|
||||
@@ -1227,34 +1241,10 @@ uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
void EmitSharedFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
const auto& mem = ctx.Memory(inst);
|
||||
const bool max_value = inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMax32;
|
||||
EmitIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
|
||||
const auto index = DwordIndex(ctx, inst, mem);
|
||||
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
|
||||
EmitIfCondition(
|
||||
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
|
||||
ctx.state.builder.AddFunction(spv::OpStore, ctx.scratch_u32_variable,
|
||||
ctx.Arg(inst, 1));
|
||||
const auto data = ctx.state.builder.AllocateId();
|
||||
ctx.state.builder.AddFunction(spv::OpLoad, TypeU32(ctx.state), data,
|
||||
ctx.scratch_u32_variable);
|
||||
AtomicUpdate(
|
||||
ctx.state, EmitMemoryElementPointer(ctx.state, resource, index),
|
||||
mem.kind, [&](uint32_t old) {
|
||||
const auto old_f =
|
||||
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), old);
|
||||
const auto compare_f =
|
||||
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), ctx.Arg(inst, 2));
|
||||
const auto data_f =
|
||||
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), data);
|
||||
const auto compare = Binary(
|
||||
ctx.state, max_value ? spv::OpFOrdGreaterThan : spv::OpFOrdLessThan,
|
||||
TypeBool(ctx.state), max_value ? old_f : compare_f,
|
||||
max_value ? compare_f : old_f);
|
||||
return Unary(ctx.state, spv::OpBitcast, TypeU32(ctx.state),
|
||||
Select(ctx.state, TypeF32(ctx.state), compare, data_f, old_f));
|
||||
});
|
||||
});
|
||||
});
|
||||
EmitAtomicUpdate(ctx, inst, mem,
|
||||
[max_value](EmitterState& state, uint32_t old, uint32_t value) {
|
||||
return EmitDsFloatAtomicReplacement(state, old, value, max_value);
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
@@ -1386,7 +1376,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),
|
||||
@@ -138,7 +148,7 @@ void EnsureLdsStorage(EmitterState& state) {
|
||||
};
|
||||
if (state.requirements.shared_int64_atomics) {
|
||||
state.lds_variable = define(TypeU32(state), 4u);
|
||||
state.lds_u64_variable = define(TypeScalarU64(state), 8u);
|
||||
state.lds_u64_variable = define(TypeU64(state), 8u);
|
||||
state.builder.AddName(state.lds_u64_variable, "lds_qwords");
|
||||
} else {
|
||||
state.lds_variable = state.builder.DefineGlobalVariable(
|
||||
@@ -361,10 +371,12 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
|
||||
case IR::ValueOpcode::ImageAtomicSwap32:
|
||||
case IR::ValueOpcode::ImageAtomicSwap64:
|
||||
case IR::ValueOpcode::BufferAtomicSwap32:
|
||||
case IR::ValueOpcode::BufferAtomicSwap64:
|
||||
case IR::ValueOpcode::SharedAtomicSwap32: return spv::OpAtomicExchange;
|
||||
case IR::ValueOpcode::ImageAtomicIAdd32:
|
||||
case IR::ValueOpcode::ImageAtomicIAdd64:
|
||||
case IR::ValueOpcode::BufferAtomicIAdd32:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd64:
|
||||
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
|
||||
@@ -374,6 +386,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::BufferAtomicSMin32:
|
||||
case IR::ValueOpcode::SharedAtomicSMin32: return spv::OpAtomicSMin;
|
||||
case IR::ValueOpcode::ImageAtomicUMin32:
|
||||
case IR::ValueOpcode::ImageAtomicUMin64:
|
||||
case IR::ValueOpcode::BufferAtomicUMin32:
|
||||
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
|
||||
case IR::ValueOpcode::ImageAtomicSMax32:
|
||||
@@ -384,15 +397,18 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
|
||||
case IR::ValueOpcode::BufferAtomicUMax32:
|
||||
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
|
||||
case IR::ValueOpcode::ImageAtomicAnd32:
|
||||
case IR::ValueOpcode::ImageAtomicAnd64:
|
||||
case IR::ValueOpcode::BufferAtomicAnd32:
|
||||
case IR::ValueOpcode::BufferAtomicAnd64:
|
||||
case IR::ValueOpcode::SharedAtomicAnd32: return spv::OpAtomicAnd;
|
||||
case IR::ValueOpcode::ImageAtomicOr32:
|
||||
case IR::ValueOpcode::ImageAtomicOr64:
|
||||
case IR::ValueOpcode::BufferAtomicOr32:
|
||||
case IR::ValueOpcode::BufferAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr64:
|
||||
case IR::ValueOpcode::SharedAtomicOr32: return spv::OpAtomicOr;
|
||||
case IR::ValueOpcode::ImageAtomicXor32:
|
||||
case IR::ValueOpcode::ImageAtomicXor64:
|
||||
case IR::ValueOpcode::BufferAtomicXor32:
|
||||
case IR::ValueOpcode::SharedAtomicXor32: return spv::OpAtomicXor;
|
||||
default: return spv::OpNop;
|
||||
@@ -404,7 +420,7 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
|
||||
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
|
||||
const auto old = ctx.state.builder.AllocateId();
|
||||
const bool wide = inst.GetType() == IR::Type::U64;
|
||||
const auto type = wide ? TypeScalarU64(ctx.state) : TypeU32(ctx.state);
|
||||
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
|
||||
if (opcode == spv::OpAtomicCompareExchange) {
|
||||
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
|
||||
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
@@ -413,13 +429,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
|
||||
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), desired, comparator);
|
||||
} else {
|
||||
auto value = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
if (wide) value = Unary(ctx.state, spv::OpBitcast, type, value);
|
||||
const auto value = ctx.Arg(inst, inst.NumArgs() - 2);
|
||||
ctx.state.builder.AddFunction(opcode, type, old, pointer,
|
||||
ConstantU32(ctx.state, scope),
|
||||
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), value);
|
||||
}
|
||||
return wide ? Unary(ctx.state, spv::OpBitcast, TypeU64(ctx.state), old) : old;
|
||||
return old;
|
||||
}
|
||||
|
||||
void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
|
||||
@@ -438,6 +453,20 @@ void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
|
||||
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t FloatOrderKey(EmitterState& state, uint32_t bits) {
|
||||
const auto negative = EmitCompareU32Constant(state, spv::OpINotEqual,
|
||||
EmitAndConstant(state, bits, 0x80000000u), 0u);
|
||||
const auto negative_key = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpNot, TypeU32(state), negative_key, bits);
|
||||
const auto positive_key =
|
||||
EmitBinaryU32(state, spv::OpBitwiseXor, bits, ConstantU32(state, 0x80000000u));
|
||||
return EmitSelectValueU32(state, negative, negative_key, positive_key);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
bool max_value) {
|
||||
struct OrderedBits {
|
||||
@@ -447,15 +476,7 @@ uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t
|
||||
};
|
||||
const auto classify = [&](uint32_t bits) {
|
||||
const auto cls = EmitClassifyF32Bits(state, bits);
|
||||
const auto negative = EmitCompareU32Constant(state, spv::OpINotEqual,
|
||||
EmitAndConstant(state, bits, 0x80000000u), 0u);
|
||||
const auto negative_key = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpNot, TypeU32(state), negative_key, bits);
|
||||
const auto positive_key =
|
||||
EmitBinaryU32(state, spv::OpBitwiseXor, bits, ConstantU32(state, 0x80000000u));
|
||||
return OrderedBits {
|
||||
cls.nan, cls.zero,
|
||||
EmitSelectValueU32(state, negative, negative_key, positive_key)};
|
||||
return OrderedBits {cls.nan, cls.zero, FloatOrderKey(state, bits)};
|
||||
};
|
||||
const auto source_class = classify(source);
|
||||
const auto old_class = classify(old);
|
||||
@@ -470,6 +491,45 @@ uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t
|
||||
old);
|
||||
}
|
||||
|
||||
uint32_t EmitDsFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
|
||||
bool max_value) {
|
||||
struct OrderedBits {
|
||||
uint32_t nan;
|
||||
uint32_t signaling_nan;
|
||||
uint32_t key;
|
||||
};
|
||||
const auto classify = [&](uint32_t bits) {
|
||||
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
|
||||
const auto nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs, 0x7f800000u);
|
||||
const auto signaling_nan = EmitLogicalAndBool(
|
||||
state, nan, EmitCompareU32Constant(state, spv::OpIEqual,
|
||||
EmitAndConstant(state, bits, 0x00400000u), 0u));
|
||||
const auto small = EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x00800000u);
|
||||
const auto sign = EmitAndConstant(state, bits, 0x80000000u);
|
||||
const auto positive_denorm = EmitLogicalAndBool(
|
||||
state, EmitCompareU32Constant(state, spv::OpIEqual, sign, 0u),
|
||||
EmitCompareU32Constant(state, spv::OpINotEqual, abs, 0u));
|
||||
// Negative denorms tie with -0; positive denorms tie only with each other.
|
||||
const auto small_bits = EmitSelectValueU32(
|
||||
state, positive_denorm, ConstantU32(state, 1u), sign);
|
||||
const auto key_bits = EmitSelectValueU32(state, small, small_bits, bits);
|
||||
return OrderedBits {nan, signaling_nan, FloatOrderKey(state, key_bits)};
|
||||
};
|
||||
const auto lhs = classify(old);
|
||||
const auto rhs = classify(source);
|
||||
const auto choose_source = state.builder.AllocateId();
|
||||
state.builder.AddFunction(max_value ? spv::OpUGreaterThan : spv::OpULessThanEqual,
|
||||
TypeBool(state), choose_source, rhs.key, lhs.key);
|
||||
// Keep original bits: ordering denorms does not flush the selected operand.
|
||||
auto result = EmitSelectValueU32(state, choose_source, source, old);
|
||||
result = EmitSelectValueU32(state, lhs.nan, source, result);
|
||||
result = EmitSelectValueU32(state, rhs.nan, old, result);
|
||||
result = EmitSelectValueU32(state, rhs.signaling_nan,
|
||||
EmitOrU32(state, source, ConstantU32(state, 0x00400000u)), result);
|
||||
return EmitSelectValueU32(state, lhs.signaling_nan,
|
||||
EmitOrU32(state, old, ConstantU32(state, 0x00400000u)), result);
|
||||
}
|
||||
|
||||
uint32_t EmitDsSwizzleTargetLane(EmitterState& state, uint32_t subid, uint32_t control) {
|
||||
if ((control & 0xc000u) == 0xc000u) {
|
||||
const uint32_t mask = control & 0x1fu;
|
||||
|
||||
@@ -35,14 +35,11 @@ uint32_t TypeU32(EmitterState& state) {
|
||||
}
|
||||
|
||||
uint32_t TypeU64(EmitterState& state) {
|
||||
return TypeU32Vector(state, 2);
|
||||
}
|
||||
|
||||
uint32_t TypeScalarU64(EmitterState& state) {
|
||||
if (state.native_u64_type == 0) {
|
||||
state.native_u64_type = state.builder.Type(spv::OpTypeInt, 64, 0);
|
||||
if (state.u64_type == 0) {
|
||||
state.builder.RequireCapability(spv::CapabilityInt64);
|
||||
state.u64_type = state.builder.Type(spv::OpTypeInt, 64, 0);
|
||||
}
|
||||
return state.native_u64_type;
|
||||
return state.u64_type;
|
||||
}
|
||||
|
||||
uint32_t TypeU32Pair(EmitterState& state) {
|
||||
@@ -124,8 +121,9 @@ uint32_t TypeFunction(EmitterState& state) {
|
||||
}
|
||||
|
||||
uint32_t TypeStorageBufferElement(EmitterState& state, uint32_t bits) {
|
||||
return bits == 32u ? TypeU32(state) : bits == 64u ? TypeScalarU64(state)
|
||||
: state.builder.Type(spv::OpTypeInt, bits, 0);
|
||||
return bits == 32u ? TypeU32(state)
|
||||
: bits == 64u ? TypeU64(state)
|
||||
: state.builder.Type(spv::OpTypeInt, bits, 0);
|
||||
}
|
||||
|
||||
uint32_t StorageRuntimeArrayType(EmitterState& state, uint32_t bits) {
|
||||
@@ -331,9 +329,8 @@ uint32_t ConstantBool(EmitterState& state, bool value) {
|
||||
}
|
||||
|
||||
uint32_t ConstantU64(EmitterState& state, uint64_t value) {
|
||||
return state.builder.Constant(spv::OpConstantComposite, TypeU64(state),
|
||||
ConstantU32(state, static_cast<uint32_t>(value)),
|
||||
ConstantU32(state, static_cast<uint32_t>(value >> 32u)));
|
||||
return state.builder.Constant(spv::OpConstant, TypeU64(state), static_cast<uint32_t>(value),
|
||||
static_cast<uint32_t>(value >> 32u));
|
||||
}
|
||||
|
||||
uint32_t ConstantU32CompositeZero(EmitterState& state, uint32_t components) {
|
||||
|
||||
@@ -689,9 +689,7 @@ void EmitProgram(EmitterState& state) {
|
||||
DefineBvhIntersect(state);
|
||||
for (const auto* block: program.blocks) {
|
||||
if (std::ranges::any_of(*block, [](const IR::Inst& inst) {
|
||||
return inst.GetOpcode() == IR::ValueOpcode::SwizzleU32 ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMin32 ||
|
||||
inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMax32;
|
||||
return inst.GetOpcode() == IR::ValueOpcode::SwizzleU32;
|
||||
})) {
|
||||
ctx.scratch_u32_variable = state.builder.AllocateId();
|
||||
if (state.lane_count == 2) {
|
||||
|
||||
@@ -20,6 +20,12 @@ struct MimgGatherInfo {
|
||||
uint32_t flags = 0;
|
||||
};
|
||||
|
||||
struct MimgAtomicInfo {
|
||||
uint32_t encoding = 0;
|
||||
Opcode decoded = Opcode::UNSUPPORTED;
|
||||
bool supports_64 = false;
|
||||
};
|
||||
|
||||
constexpr ImageDimension DecodeImageDimension(uint32_t dim) {
|
||||
switch (dim) {
|
||||
case 0u: return ImageDimension::Dim1D;
|
||||
@@ -190,17 +196,17 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
|
||||
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
|
||||
};
|
||||
|
||||
constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
{0x0fu, Opcode::IMAGE_ATOMIC_SWAP},
|
||||
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
|
||||
{0x0fu, Opcode::IMAGE_ATOMIC_SWAP, true},
|
||||
{0x10u, Opcode::IMAGE_ATOMIC_CMPSWAP},
|
||||
{0x11u, Opcode::IMAGE_ATOMIC_ADD},
|
||||
{0x11u, Opcode::IMAGE_ATOMIC_ADD, true},
|
||||
{0x14u, Opcode::IMAGE_ATOMIC_SMIN},
|
||||
{0x15u, Opcode::IMAGE_ATOMIC_UMIN},
|
||||
{0x15u, Opcode::IMAGE_ATOMIC_UMIN, true},
|
||||
{0x16u, Opcode::IMAGE_ATOMIC_SMAX},
|
||||
{0x17u, Opcode::IMAGE_ATOMIC_UMAX},
|
||||
{0x18u, Opcode::IMAGE_ATOMIC_AND},
|
||||
{0x19u, Opcode::IMAGE_ATOMIC_OR},
|
||||
{0x1au, Opcode::IMAGE_ATOMIC_XOR},
|
||||
{0x17u, Opcode::IMAGE_ATOMIC_UMAX, true},
|
||||
{0x18u, Opcode::IMAGE_ATOMIC_AND, true},
|
||||
{0x19u, Opcode::IMAGE_ATOMIC_OR, true},
|
||||
{0x1au, Opcode::IMAGE_ATOMIC_XOR, true},
|
||||
{0x1eu, Opcode::IMAGE_ATOMIC_FMIN},
|
||||
{0x1fu, Opcode::IMAGE_ATOMIC_FMAX},
|
||||
};
|
||||
@@ -210,7 +216,7 @@ constexpr auto MIMG_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCO
|
||||
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_OPCODE_LIST);
|
||||
|
||||
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgGatherInfo* gather,
|
||||
const Detail::OpcodeMap* atomic) {
|
||||
const MimgAtomicInfo* atomic) {
|
||||
if (sample != nullptr) {
|
||||
return Opcode::IMAGE_SAMPLE;
|
||||
}
|
||||
@@ -246,7 +252,7 @@ uint32_t DecodeMimgSampleFlags(const MimgSampleInfo* sample, const MimgGatherInf
|
||||
|
||||
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
|
||||
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
|
||||
const Detail::OpcodeMap* atomic) {
|
||||
const MimgAtomicInfo* atomic) {
|
||||
if (sample != nullptr) {
|
||||
return ImageSampleAddressComponents(sample->flags, dimension);
|
||||
}
|
||||
@@ -370,7 +376,7 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
SetUnsupported(inst, Family::MIMG, opcode, "MIMG image atomic has invalid DMASK");
|
||||
} else if (inst.dmask == mask64) {
|
||||
inst.data_bits = 64u;
|
||||
if (inst.opcode != Opcode::IMAGE_ATOMIC_UMAX) {
|
||||
if (!atomic->supports_64) {
|
||||
SetUnsupported(inst, Family::MIMG, opcode,
|
||||
"MIMG 64-bit image atomic opcode is not implemented");
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
|
||||
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
|
||||
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
|
||||
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
|
||||
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
|
||||
};
|
||||
|
||||
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
@@ -108,6 +109,7 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
|
||||
{0x39u, Opcode::DS_READ_I8, 1, 8, true}, {0x3au, Opcode::DS_READ_U8, 1, 8},
|
||||
{0x3bu, Opcode::DS_READ_I16, 1, 16, true}, {0x3cu, Opcode::DS_READ_U16, 1, 16},
|
||||
{0x3du, Opcode::DS_CONSUME, 1, 32}, {0x3eu, Opcode::DS_APPEND, 1, 32},
|
||||
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
|
||||
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
|
||||
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
|
||||
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
|
||||
@@ -205,14 +207,15 @@ uint32_t DsSourceCount(Opcode opcode) {
|
||||
case Opcode::DS_WRITE2ST64_B32:
|
||||
case Opcode::DS_WRITE2_B64:
|
||||
case Opcode::DS_WRITE2ST64_B64:
|
||||
case Opcode::DS_MSKOR_B32:
|
||||
case Opcode::DS_MSKOR_B32: return 3u;
|
||||
case Opcode::DS_MIN_F32:
|
||||
case Opcode::DS_MAX_F32: return 3u;
|
||||
case Opcode::DS_MAX_F32:
|
||||
case Opcode::DS_PERMUTE_B32:
|
||||
case Opcode::DS_BPERMUTE_B32: return 2u;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -342,7 +345,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;
|
||||
@@ -354,6 +356,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;
|
||||
@@ -363,7 +366,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;
|
||||
}
|
||||
@@ -373,6 +376,10 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
}
|
||||
|
||||
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
|
||||
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
|
||||
// D16 loads reuse partial destinations to preserve the untouched high half.
|
||||
inst.dst.sdwa_sel = 4u;
|
||||
}
|
||||
DecodeVectorGpr(addr, inst.src0);
|
||||
inst.src_count = 1;
|
||||
if (seg == 0u || saddr == 0x7du || saddr == 0x7fu) {
|
||||
@@ -407,6 +414,14 @@ void DecodeDs(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
|
||||
if (inst.opcode == Opcode::UNSUPPORTED) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS opcode is not implemented");
|
||||
}
|
||||
if (inst.opcode == Opcode::DS_ORDERED_COUNT) {
|
||||
// Keep OFFSET1's wave type as well as its add/swap selector for GDS addressing.
|
||||
inst.offset = offset0 & 0xfcu;
|
||||
inst.secondary_offset = offset1;
|
||||
if (!inst.gds || ((offset1 >> 4u) & 3u) > 1u || ((offset1 >> 6u) & 3u) != 0u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS ordered count variant is not implemented");
|
||||
}
|
||||
}
|
||||
if (inst.opcode == Opcode::DS_SWIZZLE_B32 && inst.offset >= 0xe000u) {
|
||||
SetUnsupported(inst, Family::DS, opcode, "DS swizzle FFT mode is not implemented");
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -663,6 +664,7 @@ std::string InstructionToString(const Instruction& inst) {
|
||||
case Opcode::FLAT_LOAD_SBYTE:
|
||||
case Opcode::FLAT_LOAD_USHORT:
|
||||
case Opcode::FLAT_LOAD_SSHORT:
|
||||
case Opcode::FLAT_LOAD_SHORT_D16:
|
||||
case Opcode::FLAT_LOAD_DWORD:
|
||||
case Opcode::FLAT_LOAD_DWORDX2:
|
||||
case Opcode::FLAT_LOAD_DWORDX3:
|
||||
|
||||
@@ -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,
|
||||
@@ -224,6 +225,9 @@ enum class Opcode {
|
||||
V_MAC_F32,
|
||||
V_MADMK_F32,
|
||||
V_MADAK_F32,
|
||||
V_FMAC_F32,
|
||||
V_FMAMK_F32,
|
||||
V_FMAAK_F32,
|
||||
V_MIN_F32,
|
||||
V_MAX_F32,
|
||||
V_ADD_F16,
|
||||
@@ -378,6 +382,7 @@ enum class Opcode {
|
||||
V_CMPX_LG_F32,
|
||||
V_CMPX_GE_F32,
|
||||
V_CMPX_O_F32,
|
||||
V_CMPX_U_F32,
|
||||
V_CMPX_NGE_F32,
|
||||
V_CMPX_NLG_F32,
|
||||
V_CMPX_NGT_F32,
|
||||
@@ -394,6 +399,7 @@ enum class Opcode {
|
||||
V_CMP_T_I32,
|
||||
V_CMP_CLASS_F32,
|
||||
V_CMPX_CLASS_F32,
|
||||
V_CMPX_CLASS_F16,
|
||||
V_CMP_LT_I16,
|
||||
V_CMP_EQ_I16,
|
||||
V_CMP_LE_I16,
|
||||
@@ -438,6 +444,7 @@ enum class Opcode {
|
||||
V_CMPX_LT_U16,
|
||||
V_CMPX_EQ_U16,
|
||||
V_CMPX_GT_U16,
|
||||
V_CMPX_NE_U16,
|
||||
V_CMP_NE_U16,
|
||||
V_CMP_GE_U16,
|
||||
V_CMP_F_U32,
|
||||
@@ -529,6 +536,7 @@ enum class Opcode {
|
||||
FLAT_LOAD_SBYTE,
|
||||
FLAT_LOAD_USHORT,
|
||||
FLAT_LOAD_SSHORT,
|
||||
FLAT_LOAD_SHORT_D16,
|
||||
FLAT_LOAD_DWORD,
|
||||
FLAT_LOAD_DWORDX2,
|
||||
FLAT_LOAD_DWORDX3,
|
||||
@@ -572,6 +580,7 @@ enum class Opcode {
|
||||
DS_BPERMUTE_B32,
|
||||
DS_CONSUME,
|
||||
DS_APPEND,
|
||||
DS_ORDERED_COUNT,
|
||||
DS_READ_I8,
|
||||
DS_READ_U8,
|
||||
DS_READ_I16,
|
||||
|
||||
@@ -35,7 +35,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
|
||||
{0x04u, Opcode::V_SUB_F32, Vop2SdwaProfile::Float32},
|
||||
{0x05u, Opcode::V_SUBREV_F32},
|
||||
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
|
||||
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerFullDestination},
|
||||
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
|
||||
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
|
||||
{0x0fu, Opcode::V_MIN_F32},
|
||||
{0x10u, Opcode::V_MAX_F32},
|
||||
@@ -65,9 +65,9 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
|
||||
{0x2au, Opcode::V_SUBREV_CO_CI_U32},
|
||||
{0x26u, Opcode::V_SUB_NC_U32, Vop2SdwaProfile::IntegerPartialDestination},
|
||||
{0x27u, Opcode::V_SUBREV_NC_U32, Vop2SdwaProfile::IntegerFullDestination},
|
||||
{0x2bu, Opcode::V_MAC_F32},
|
||||
{0x2cu, Opcode::V_MADMK_F32},
|
||||
{0x2du, Opcode::V_MADAK_F32},
|
||||
{0x2bu, Opcode::V_FMAC_F32},
|
||||
{0x2cu, Opcode::V_FMAMK_F32},
|
||||
{0x2du, Opcode::V_FMAAK_F32},
|
||||
{0x2fu, Opcode::V_CVT_PKRTZ_F16_F32, Vop2SdwaProfile::PackedFloat16},
|
||||
{0x32u, Opcode::V_ADD_F16, Vop2SdwaProfile::Float16},
|
||||
{0x33u, Opcode::V_SUB_F16, Vop2SdwaProfile::Float16},
|
||||
@@ -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},
|
||||
@@ -252,11 +252,13 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
|
||||
{0x99u, Opcode::V_CMPX_LT_I16, false}, {0x9au, Opcode::V_CMPX_EQ_I16, false},
|
||||
{0x9bu, Opcode::V_CMPX_LE_I16, false}, {0x9cu, Opcode::V_CMPX_GT_I16, false},
|
||||
{0x9du, Opcode::V_CMPX_NE_I16, false}, {0x9eu, Opcode::V_CMPX_GE_I16, false},
|
||||
{0x9fu, Opcode::V_CMPX_CLASS_F16, false},
|
||||
{0xa9u, Opcode::V_CMP_LT_U16}, {0xaau, Opcode::V_CMP_EQ_U16},
|
||||
{0xabu, Opcode::V_CMP_LE_U16}, {0xacu, Opcode::V_CMP_GT_U16},
|
||||
{0xadu, Opcode::V_CMP_NE_U16}, {0xaeu, Opcode::V_CMP_GE_U16},
|
||||
{0xb9u, Opcode::V_CMPX_LT_U16, false}, {0xbau, Opcode::V_CMPX_EQ_U16, false},
|
||||
{0xbcu, Opcode::V_CMPX_GT_U16}, {0xc0u, Opcode::V_CMP_F_U32},
|
||||
{0xbcu, Opcode::V_CMPX_GT_U16}, {0xbdu, Opcode::V_CMPX_NE_U16, false},
|
||||
{0xc0u, Opcode::V_CMP_F_U32},
|
||||
{0xc1u, Opcode::V_CMP_LT_U32}, {0xc2u, Opcode::V_CMP_EQ_U32},
|
||||
{0xc3u, Opcode::V_CMP_LE_U32}, {0xc4u, Opcode::V_CMP_GT_U32},
|
||||
{0xc5u, Opcode::V_CMP_NE_U32}, {0xc6u, Opcode::V_CMP_GE_U32},
|
||||
@@ -800,6 +802,9 @@ bool IsVop2FloatOpcode(Opcode opcode) {
|
||||
case Opcode::V_MAC_F32:
|
||||
case Opcode::V_MADMK_F32:
|
||||
case Opcode::V_MADAK_F32:
|
||||
case Opcode::V_FMAC_F32:
|
||||
case Opcode::V_FMAMK_F32:
|
||||
case Opcode::V_FMAAK_F32:
|
||||
case Opcode::V_CVT_PKRTZ_F16_F32:
|
||||
case Opcode::V_ADD_F16:
|
||||
case Opcode::V_SUB_F16:
|
||||
@@ -887,6 +892,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
@@ -913,6 +919,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
|
||||
case Opcode::V_CMPX_NEQ_F16:
|
||||
case Opcode::V_CMPX_NLT_F16:
|
||||
case Opcode::V_CMP_CLASS_F32:
|
||||
case Opcode::V_CMPX_CLASS_F16:
|
||||
case Opcode::V_CMPX_CLASS_F32: return true;
|
||||
default: return false;
|
||||
}
|
||||
@@ -1057,6 +1064,7 @@ void FinalizeVop2Instruction(std::span<const uint32_t> code, uint32_t word_index
|
||||
Instruction& inst) {
|
||||
switch (inst.opcode) {
|
||||
case Opcode::V_MADMK_F32:
|
||||
case Opcode::V_FMAMK_F32:
|
||||
case Opcode::V_FMAMK_F16:
|
||||
inst.src2 = inst.src1;
|
||||
inst.src1 = {};
|
||||
@@ -1064,6 +1072,7 @@ void FinalizeVop2Instruction(std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.src_count = 3;
|
||||
break;
|
||||
case Opcode::V_MADAK_F32:
|
||||
case Opcode::V_FMAAK_F32:
|
||||
case Opcode::V_FMAAK_F16:
|
||||
inst.src2 = {};
|
||||
inst.src2.kind = OperandKind::LiteralConstant;
|
||||
@@ -1401,6 +1410,7 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
|
||||
case Opcode::V_MIN_F32:
|
||||
case Opcode::V_MAX_F32:
|
||||
case Opcode::V_MAC_F32:
|
||||
case Opcode::V_FMAC_F32:
|
||||
case Opcode::V_MAD_F32:
|
||||
case Opcode::V_ADD_F64:
|
||||
case Opcode::V_MUL_F64:
|
||||
@@ -1436,9 +1446,13 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
|
||||
case Opcode::V_MIN_F32:
|
||||
case Opcode::V_MAX_F32:
|
||||
case Opcode::V_MAC_F32:
|
||||
case Opcode::V_FMAC_F32:
|
||||
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:
|
||||
@@ -1469,7 +1483,8 @@ bool HasUnsupportedNativeVop3Modifiers(Opcode opcode, bool permlane, bool mad_mi
|
||||
return clamp != 0u || omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3F16TernaryOpcode(opcode)) {
|
||||
return opcode != Opcode::V_FMA_F16 && (clamp != 0u || omod != 0u);
|
||||
// Clamp is applied by WriteF16 for every f16 ternary; omod only for V_FMA_F16.
|
||||
return opcode != Opcode::V_FMA_F16 && omod != 0u;
|
||||
}
|
||||
if (IsNativeVop3I16TernaryOpcode(opcode)) {
|
||||
return abs != 0u || (clamp != 0u && !clamp_modifier) || omod != 0u || neg != 0u;
|
||||
@@ -1519,6 +1534,7 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LG_F32:
|
||||
case Opcode::V_CMPX_GE_F32:
|
||||
case Opcode::V_CMPX_O_F32:
|
||||
case Opcode::V_CMPX_U_F32:
|
||||
case Opcode::V_CMPX_NGE_F32:
|
||||
case Opcode::V_CMPX_NLG_F32:
|
||||
case Opcode::V_CMPX_NGT_F32:
|
||||
@@ -1532,6 +1548,7 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_NE_I32:
|
||||
case Opcode::V_CMPX_GE_I32:
|
||||
case Opcode::V_CMPX_CLASS_F32:
|
||||
case Opcode::V_CMPX_CLASS_F16:
|
||||
case Opcode::V_CMPX_LT_U32:
|
||||
case Opcode::V_CMPX_EQ_U32:
|
||||
case Opcode::V_CMPX_LE_U32:
|
||||
@@ -1550,6 +1567,7 @@ bool IsVopcCompareExec(Opcode opcode) {
|
||||
case Opcode::V_CMPX_LT_U16:
|
||||
case Opcode::V_CMPX_EQ_U16:
|
||||
case Opcode::V_CMPX_GT_U16:
|
||||
case Opcode::V_CMPX_NE_U16:
|
||||
case Opcode::V_CMPX_LT_F16:
|
||||
case Opcode::V_CMPX_EQ_F16:
|
||||
case Opcode::V_CMPX_LE_F16:
|
||||
|
||||
@@ -72,11 +72,13 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
|
||||
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
|
||||
}
|
||||
|
||||
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx) {
|
||||
const auto value = ReadOperand(inst.src0, IR::Type::F32);
|
||||
const auto mask = ReadOperand(inst.src1, IR::Type::U32);
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(IR::ValueOpcode::FPCmpClass32, {value, mask})), false,
|
||||
cmpx);
|
||||
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
|
||||
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
|
||||
: ReadOperand(inst.src0, IR::Type::F32);
|
||||
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
|
||||
: ReadOperand(inst.src1, IR::Type::U32);
|
||||
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
|
||||
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
|
||||
}
|
||||
|
||||
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
|
||||
@@ -248,6 +248,7 @@ void Translator::S_CSELECT_B32(const Decoder::Instruction& inst) {
|
||||
|
||||
void Translator::ScalarSelect64(const Decoder::Instruction& inst,
|
||||
const Decoder::Operand& false_source) {
|
||||
// Preserve per-word expressions for descriptor tracking and mask provenance.
|
||||
const auto condition = ir.GetScc();
|
||||
const auto lhs = ReadU32Pair(inst.src0);
|
||||
const auto rhs = ReadU32Pair(false_source);
|
||||
|
||||
@@ -8,13 +8,13 @@ namespace Libs::Graphics::ShaderRecompiler::Frontend {
|
||||
void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
|
||||
bool accumulator, bool quiet_snan) {
|
||||
const auto translate_lane = [&](bool high) {
|
||||
const auto lhs = ReadF16LaneAsF32(inst.src0, high, true);
|
||||
const auto rhs = ReadF16LaneAsF32(inst.src1, high, true);
|
||||
const auto lhs = ReadF16LaneAsF32(inst.src0, high);
|
||||
const auto rhs = ReadF16LaneAsF32(inst.src1, high);
|
||||
IR::F32 result;
|
||||
if (accumulator) {
|
||||
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.dst, high, true)}));
|
||||
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.dst, high)}));
|
||||
} else if (inst.src_count == 3u) {
|
||||
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.src2, high, true)}));
|
||||
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.src2, high)}));
|
||||
} else {
|
||||
result = IR::F32(ir.Emit(opcode, {lhs, rhs}));
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
|
||||
return;
|
||||
}
|
||||
|
||||
// Descriptor tracking reasons about each scalar word independently.
|
||||
const auto lhs = ReadU32Pair(inst.src0);
|
||||
auto mask_valid = ReadMaskValid(inst.src0);
|
||||
if (inst.src_count > 1u) {
|
||||
@@ -269,22 +270,18 @@ void Translator::Integer24(const Decoder::Instruction& inst, bool sign, bool add
|
||||
}
|
||||
|
||||
void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
|
||||
const auto lhs = ReadU32(inst.src0);
|
||||
const auto rhs = ReadU32(inst.src1);
|
||||
const auto add = ExtractU64(ReadU64(inst.src2));
|
||||
const auto mul_low = ir.IMul(lhs, rhs);
|
||||
const auto mul_high = IR::U32(ir.Emit(IR::ValueOpcode::UMulHi, {lhs, rhs}));
|
||||
const auto low = ir.IAdd(mul_low, add[0]);
|
||||
const auto carry_low = ir.ULessThan(low, mul_low);
|
||||
const auto high0 = ir.IAdd(mul_high, add[1]);
|
||||
const auto carry0 = ir.ULessThan(high0, mul_high);
|
||||
const auto high =
|
||||
ir.IAdd(high0, ir.Select(carry_low, IR::U32(IR::Value(1u)), IR::U32(IR::Value(0u))));
|
||||
const auto carry1 = ir.ULessThan(high, high0);
|
||||
WriteOperand(DestinationOperand(inst), ir.ConstructU64(low, high));
|
||||
const auto lhs = ReadU32(inst.src0);
|
||||
const auto rhs = ReadU32(inst.src1);
|
||||
// Keep the 32x32 product in words: native widening IMul64 fails NVIDIA compilation
|
||||
// in large vertex shaders (Astro's Playroom). The 64-bit addition stays native.
|
||||
const auto product = ir.ConstructU64(
|
||||
ir.IMul(lhs, rhs), IR::U32(ir.Emit(IR::ValueOpcode::UMulHi, {lhs, rhs})));
|
||||
const auto result = ir.Emit(IR::ValueOpcode::IAdd64, {product, ReadU64(inst.src2)});
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
if (inst.dst2.kind != Decoder::OperandKind::Null &&
|
||||
inst.dst2.kind != Decoder::OperandKind::Unknown) {
|
||||
WriteMask(inst.dst2, ir.LogicalOr(carry0, carry1));
|
||||
// The 32x32 product fits in 64 bits, so only the addition can carry.
|
||||
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,9 +404,12 @@ IR::U64 Translator::RightMask64(IR::U32 count) {
|
||||
}
|
||||
|
||||
void Translator::S_BFM_B64(const Decoder::Instruction& inst) {
|
||||
const auto count = ir.BitwiseAnd(ReadU32(inst.src0), IR::U32(IR::Value(63u)));
|
||||
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(63u)));
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {RightMask64(count), offset});
|
||||
const auto count = ReadU32(inst.src0);
|
||||
const auto offset = ReadU32(inst.src1);
|
||||
const auto one = IR::Value(uint64_t {1});
|
||||
const auto limit = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {one, count});
|
||||
const auto mask = ir.Emit(IR::ValueOpcode::ISub64, {limit, one});
|
||||
const auto result = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {mask, offset});
|
||||
WriteOperand(DestinationOperand(inst), result);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -546,7 +547,10 @@ void Translator::BUFFER_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode
|
||||
}
|
||||
}
|
||||
|
||||
void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
|
||||
void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode32,
|
||||
IR::ValueOpcode opcode64) {
|
||||
const auto opcode = inst.data_bits == 64u ? opcode64 : opcode32;
|
||||
EXIT_IF(opcode == IR::ValueOpcode::Count);
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto resource = GetImageResource(memory);
|
||||
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 1));
|
||||
@@ -879,6 +883,50 @@ void Translator::DS_SWIZZLE_B32(const Decoder::Instruction& inst) {
|
||||
ReadU32(MemorySourceAt(inst, 1)), ir.GetExec()}));
|
||||
}
|
||||
|
||||
void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
|
||||
// Approximate ordered counting with one GDS atomic per wave. Wave-launch ordering and
|
||||
// release/done synchronization are not emulated.
|
||||
const auto memory = MemoryInfoFromDecoded(inst);
|
||||
const auto m0 = ir.GetM0();
|
||||
auto address = ir.BitwiseAnd(ir.ShiftRightLogical(m0, IR::U32(IR::Value(16u))),
|
||||
IR::U32(IR::Value(0xfffcu)));
|
||||
if (((inst.secondary_offset >> 2u) & 3u) == 1u) {
|
||||
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
|
||||
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
|
||||
}
|
||||
const auto exec = ir.GetExec();
|
||||
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
|
||||
const auto low = ir.CompositeExtract(ballot, 0u);
|
||||
const auto high = ir.CompositeExtract(ballot, 1u);
|
||||
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
|
||||
const auto first = ir.Select(
|
||||
ir.INotEqual(low, IR::U32(IR::Value(0u))),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {low})),
|
||||
ir.IAdd(IR::U32(IR::Value(32u)), IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {high}))));
|
||||
// An empty EXEC still returns the counter. Elect lane zero and perform an add of zero.
|
||||
const auto selected = ir.Select(active, first, IR::U32(IR::Value(0u)));
|
||||
const auto lane = IR::U32(ir.Emit(IR::ValueOpcode::LaneId));
|
||||
const auto is_first = ir.IEqual(lane, selected);
|
||||
const auto source = IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane,
|
||||
{ReadU32(inst.src0), is_first}));
|
||||
const auto value = ir.Select(active, source, IR::U32(IR::Value(0u)));
|
||||
const auto flags = AddMemoryInfo(memory, inst.pc);
|
||||
IR::U32 result;
|
||||
if (((inst.secondary_offset >> 4u) & 3u) == 1u) {
|
||||
const auto swapped = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicSwap32,
|
||||
{address, value, ir.LogicalAnd(is_first, active)}, flags));
|
||||
const auto unchanged = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
|
||||
{address, IR::Value(0u), ir.LogicalAnd(is_first, ir.LogicalNot(active))}, flags));
|
||||
result = ir.Select(active, swapped, unchanged);
|
||||
} else {
|
||||
result = IR::U32(ir.Emit(IR::ValueOpcode::SharedAtomicIAdd32,
|
||||
{address, value, is_first}, flags));
|
||||
}
|
||||
// DS_ORDERED_COUNT writes every destination lane, regardless of EXEC.
|
||||
ir.SetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg),
|
||||
IR::U32(ir.Emit(IR::ValueOpcode::ReadFirstLane, {result, is_first})));
|
||||
}
|
||||
|
||||
void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
|
||||
const auto address = ir.IAdd(ReadU32(inst.src0), IR::U32(IR::Value(inst.offset)));
|
||||
WriteOperand(inst.dst, ir.Emit(backward ? IR::ValueOpcode::BpermuteU32
|
||||
@@ -1041,24 +1089,30 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_CMPSWAP:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicCompareSwap32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SWAP:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSwap32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSwap32,
|
||||
IR::ValueOpcode::ImageAtomicSwap64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_ADD:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicIAdd32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicIAdd32,
|
||||
IR::ValueOpcode::ImageAtomicIAdd64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMin32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_UMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMin32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMin32,
|
||||
IR::ValueOpcode::ImageAtomicUMin64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_SMAX:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMax32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_UMAX:
|
||||
return IMAGE_ATOMIC(inst, inst.data_bits == 64u ? IR::ValueOpcode::ImageAtomicUMax64
|
||||
: IR::ValueOpcode::ImageAtomicUMax32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMax32,
|
||||
IR::ValueOpcode::ImageAtomicUMax64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_AND:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicAnd32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicAnd32,
|
||||
IR::ValueOpcode::ImageAtomicAnd64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_OR:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32,
|
||||
IR::ValueOpcode::ImageAtomicOr64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_XOR:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32);
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32,
|
||||
IR::ValueOpcode::ImageAtomicXor64);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_FMIN:
|
||||
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32);
|
||||
case Decoder::Opcode::IMAGE_ATOMIC_FMAX:
|
||||
@@ -1068,6 +1122,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::FLAT_LOAD_SBYTE:
|
||||
case Decoder::Opcode::FLAT_LOAD_USHORT:
|
||||
case Decoder::Opcode::FLAT_LOAD_SSHORT:
|
||||
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORD:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
|
||||
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
|
||||
@@ -1103,6 +1158,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
|
||||
case Decoder::Opcode::DS_SWIZZLE_B32: return DS_SWIZZLE_B32(inst);
|
||||
case Decoder::Opcode::DS_PERMUTE_B32: return DS_PERMUTE(inst, false);
|
||||
case Decoder::Opcode::DS_BPERMUTE_B32: return DS_PERMUTE(inst, true);
|
||||
case Decoder::Opcode::DS_ORDERED_COUNT: return DS_ORDERED_COUNT(inst);
|
||||
case Decoder::Opcode::DS_CONSUME:
|
||||
return DS_APPEND_CONSUME(inst, IR::ValueOpcode::DataConsume);
|
||||
case Decoder::Opcode::DS_APPEND:
|
||||
|
||||
@@ -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);
|
||||
@@ -165,9 +169,16 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
|
||||
case O::S_BCNT1_I32_B32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
|
||||
true);
|
||||
case O::S_BCNT1_I32_B64:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::BitCount64, IR::Type::U64, false, false,
|
||||
true);
|
||||
case O::S_BCNT1_I32_B64: {
|
||||
// Vulkan bit counts operate on 32-bit words; avoid packing only to split again.
|
||||
const auto value = ReadU32Pair(inst.src0);
|
||||
const auto low = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[0]}));
|
||||
const auto high = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[1]}));
|
||||
const auto result = ir.IAdd(low, high);
|
||||
WriteOperand(inst.dst, result);
|
||||
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
|
||||
return;
|
||||
}
|
||||
case O::S_FF1_I32_B32:
|
||||
return SimpleInteger(inst, IR::ValueOpcode::FindILsb32, IR::Type::U32, false, false,
|
||||
false);
|
||||
|
||||
@@ -503,41 +503,11 @@ IR::U64 Translator::ReadU64(const Decoder::Operand& operand) {
|
||||
return IR::U64(ReadOperand(operand, IR::Type::U64));
|
||||
}
|
||||
|
||||
IR::F32 Translator::ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane, bool packed) {
|
||||
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
const bool use_zero = packed && (high_lane ? operand.op_sel_hi : operand.op_sel);
|
||||
auto value = use_zero ? IR::F32(IR::Value::F32(0.0f))
|
||||
: ir.BitCastF32(IR::U32(IR::Value(operand.value)));
|
||||
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {value}));
|
||||
value = IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
|
||||
if (operand.absolute) {
|
||||
value = IR::F32(ir.Emit(IR::ValueOpcode::FPAbs32, {value}));
|
||||
}
|
||||
if (high_lane ? operand.negate_hi : operand.negate) {
|
||||
value = IR::F32(ir.Emit(IR::ValueOpcode::FPNeg32, {value}));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
auto raw_operand = operand;
|
||||
raw_operand.sdwa_sel = 6;
|
||||
raw_operand.sdwa_sext = false;
|
||||
const auto bits = ApplyBitSourceModifiers(raw_operand, ReadRawU32(operand));
|
||||
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
|
||||
if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
|
||||
offset = operand.sdwa_sel == 5u ? 16u : 0u;
|
||||
}
|
||||
const auto half_u32 = IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldUExtract, {bits, IR::Value(offset), IR::Value(16u)}));
|
||||
const auto half_u16 = IR::U16(ir.Emit(IR::ValueOpcode::ConvertU16U32, {half_u32}));
|
||||
IR::F32 Translator::ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane) {
|
||||
const auto bits = Read16LaneBits(operand, high_lane);
|
||||
const auto half_u16 = IR::U16(ir.Emit(IR::ValueOpcode::ConvertU16U32, {bits}));
|
||||
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::BitCastF16U16, {half_u16}));
|
||||
auto value = IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
|
||||
if (operand.absolute) {
|
||||
value = IR::F32(ir.Emit(IR::ValueOpcode::FPAbs32, {value}));
|
||||
}
|
||||
if (high_lane ? operand.negate_hi : operand.negate) {
|
||||
value = IR::F32(ir.Emit(IR::ValueOpcode::FPNeg32, {value}));
|
||||
}
|
||||
return value;
|
||||
return IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
|
||||
}
|
||||
|
||||
IR::F32 Translator::ReadF16AsF32(const Decoder::Operand& operand) {
|
||||
@@ -555,29 +525,6 @@ IR::F32 Translator::ReadMixF32(const Decoder::Operand& operand) {
|
||||
return IR::F32(ReadOperand(value_operand, IR::Type::F32));
|
||||
}
|
||||
|
||||
IR::U32 Translator::ReadU16LaneRaw(const Decoder::Operand& operand, bool high_lane) {
|
||||
auto raw_operand = operand;
|
||||
raw_operand.sdwa_sel = 6;
|
||||
raw_operand.sdwa_sext = false;
|
||||
const auto bits = ApplyBitSourceModifiers(raw_operand, ReadRawU32(operand));
|
||||
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
|
||||
uint32_t width = 16u;
|
||||
if (operand.sdwa_sel <= 3u) {
|
||||
offset = operand.sdwa_sel * 8u;
|
||||
width = 8u;
|
||||
} else if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
|
||||
offset = operand.sdwa_sel == 5u ? 16u : 0u;
|
||||
}
|
||||
if (width == 8u && operand.sdwa_sext) {
|
||||
// Extend the selected byte into the 16-bit operand before halfword modifiers.
|
||||
const auto value = IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldSExtract, {bits, IR::Value(offset), IR::Value(width)}));
|
||||
return ir.BitwiseAnd(value, IR::U32(IR::Value(0xffffu)));
|
||||
}
|
||||
return IR::U32(
|
||||
ir.Emit(IR::ValueOpcode::BitFieldUExtract, {bits, IR::Value(offset), IR::Value(width)}));
|
||||
}
|
||||
|
||||
IR::U32 Translator::ReadU16LaneAsU32(const Decoder::Operand& operand, bool high_lane,
|
||||
bool sign_extend) {
|
||||
auto value = Read16LaneBits(operand, high_lane);
|
||||
@@ -593,12 +540,36 @@ IR::U32 Translator::ReadU16AsU32(const Decoder::Operand& operand, bool sign_exte
|
||||
}
|
||||
|
||||
IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_lane) {
|
||||
auto value = ReadU16LaneRaw(operand, high_lane);
|
||||
auto raw_operand = operand;
|
||||
raw_operand.sdwa_sel = 6;
|
||||
raw_operand.sdwa_sext = false;
|
||||
auto bits = ReadRawU32(operand);
|
||||
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
|
||||
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
|
||||
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
|
||||
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
|
||||
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
|
||||
}
|
||||
bits = ApplyBitSourceModifiers(raw_operand, bits);
|
||||
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
|
||||
uint32_t width = 16u;
|
||||
if (operand.sdwa_sel <= 3u) {
|
||||
offset = operand.sdwa_sel * 8u;
|
||||
width = 8u;
|
||||
} else if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
|
||||
offset = operand.sdwa_sel == 5u ? 16u : 0u;
|
||||
}
|
||||
const auto extract = width == 8u && operand.sdwa_sext
|
||||
? IR::ValueOpcode::BitFieldSExtract
|
||||
: IR::ValueOpcode::BitFieldUExtract;
|
||||
auto value = IR::U32(ir.Emit(extract, {bits, IR::Value(offset), IR::Value(width)}));
|
||||
if (width == 8u && operand.sdwa_sext) {
|
||||
value = ir.BitwiseAnd(value, IR::U32(IR::Value(0xffffu)));
|
||||
}
|
||||
if (operand.absolute) {
|
||||
value = ir.BitwiseAnd(value, IR::U32(IR::Value(0x7fffu)));
|
||||
}
|
||||
if (high_lane ? operand.negate_hi : operand.negate) {
|
||||
// RDNA2 source NEG flips the sign bit, including packed integer operands.
|
||||
value = ir.BitwiseXor(value, IR::U32(IR::Value(0x8000u)));
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -40,10 +40,9 @@ private:
|
||||
IR::U32 ReadU32(const Decoder::Operand& operand);
|
||||
std::array<IR::U32, 2> ReadU32Pair(const Decoder::Operand& operand);
|
||||
IR::U64 ReadU64(const Decoder::Operand& operand);
|
||||
IR::F32 ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane, bool packed = false);
|
||||
IR::F32 ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane);
|
||||
IR::F32 ReadF16AsF32(const Decoder::Operand& operand);
|
||||
IR::F32 ReadMixF32(const Decoder::Operand& operand);
|
||||
IR::U32 ReadU16LaneRaw(const Decoder::Operand& operand, bool high_lane);
|
||||
IR::U32 ReadU16LaneAsU32(const Decoder::Operand& operand, bool high_lane, bool sign_extend);
|
||||
IR::U32 ReadU16AsU32(const Decoder::Operand& operand, bool sign_extend);
|
||||
IR::U32 Read16LaneBits(const Decoder::Operand& operand, bool high_lane);
|
||||
@@ -86,7 +85,8 @@ private:
|
||||
void BUFFER_LOAD(const Decoder::Instruction& inst);
|
||||
void BUFFER_STORE(const Decoder::Instruction& inst);
|
||||
void BUFFER_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
|
||||
void IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
|
||||
void IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode32,
|
||||
IR::ValueOpcode opcode64 = IR::ValueOpcode::Count);
|
||||
void DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool returns_value);
|
||||
void FLAT_LOAD(const Decoder::Instruction& inst);
|
||||
void FLAT_STORE(const Decoder::Instruction& inst);
|
||||
@@ -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,
|
||||
@@ -126,7 +127,7 @@ private:
|
||||
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
|
||||
bool cmpx);
|
||||
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx);
|
||||
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx);
|
||||
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
|
||||
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
|
||||
void V_CVT_F32_U32(const Decoder::Instruction& inst);
|
||||
void V_CVT_F32_I32(const Decoder::Instruction& inst);
|
||||
|
||||
@@ -143,6 +143,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_CMP_NE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
|
||||
return;
|
||||
case O::V_CMPX_NE_U16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
|
||||
return;
|
||||
case O::V_CMP_NE_I16:
|
||||
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
|
||||
return;
|
||||
@@ -341,8 +344,10 @@ 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;
|
||||
|
||||
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
|
||||
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
|
||||
@@ -479,10 +484,13 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
|
||||
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
|
||||
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
|
||||
case O::V_LDEXP_F32: return FloatBinary(inst, IR::ValueOpcode::FPLdexp, false);
|
||||
case O::V_MAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, true, true);
|
||||
case O::V_MAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPMad32, true, true);
|
||||
case O::V_MADMK_F32:
|
||||
case O::V_MADAK_F32:
|
||||
case O::V_MAD_F32:
|
||||
case O::V_MAD_F32: return FloatTernary(inst, IR::ValueOpcode::FPMad32, false, true);
|
||||
case O::V_FMAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, true, true);
|
||||
case O::V_FMAMK_F32:
|
||||
case O::V_FMAAK_F32:
|
||||
case O::V_FMA_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, false, true);
|
||||
case O::V_FMA_F64: return FloatTernary(inst, IR::ValueOpcode::FPFma64, false, false);
|
||||
case O::V_MIN3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMinTri32, false, false);
|
||||
|
||||
@@ -485,14 +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;
|
||||
Value key_count;
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
|
||||
@@ -224,15 +224,21 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
|
||||
return {ImageAccess::Write, ImageResourceClass::Storage, false};
|
||||
case ValueOpcode::ImageAtomicCompareSwap32:
|
||||
case ValueOpcode::ImageAtomicSwap32:
|
||||
case ValueOpcode::ImageAtomicSwap64:
|
||||
case ValueOpcode::ImageAtomicIAdd32:
|
||||
case ValueOpcode::ImageAtomicIAdd64:
|
||||
case ValueOpcode::ImageAtomicSMin32:
|
||||
case ValueOpcode::ImageAtomicUMin32:
|
||||
case ValueOpcode::ImageAtomicUMin64:
|
||||
case ValueOpcode::ImageAtomicSMax32:
|
||||
case ValueOpcode::ImageAtomicUMax32:
|
||||
case ValueOpcode::ImageAtomicUMax64:
|
||||
case ValueOpcode::ImageAtomicAnd32:
|
||||
case ValueOpcode::ImageAtomicAnd64:
|
||||
case ValueOpcode::ImageAtomicOr32:
|
||||
case ValueOpcode::ImageAtomicOr64:
|
||||
case ValueOpcode::ImageAtomicXor32:
|
||||
case ValueOpcode::ImageAtomicXor64:
|
||||
case ValueOpcode::ImageAtomicFMin32:
|
||||
case ValueOpcode::ImageAtomicFMax32:
|
||||
return {ImageAccess::Atomic, ImageResourceClass::Storage, false};
|
||||
|
||||
@@ -175,10 +175,12 @@ VALUE_OPCODE(FPOrdGreaterThanEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPUnordGreaterThanEqual32, U1, F32, F32)
|
||||
VALUE_OPCODE(FPIsNan32, U1, F32)
|
||||
VALUE_OPCODE(FPCmpClass32, U1, F32, U32)
|
||||
VALUE_OPCODE(FPCmpClass16, U1, U32, U32)
|
||||
|
||||
VALUE_OPCODE(FPAdd32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPSub32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMad32, F32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPMul32, F32, F32, F32)
|
||||
VALUE_OPCODE(FPAdd64, F64, F64, F64)
|
||||
VALUE_OPCODE(FPMul64, F64, F64, F64)
|
||||
@@ -279,8 +281,8 @@ VALUE_OPCODE(WriteSharedU32, Void, U32, U32, U1)
|
||||
VALUE_OPCODE(WriteSharedU32x2, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(WriteSharedU32x3, Void, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(WriteSharedU32x4, Void, U32, U32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicFMin32, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicFMax32, Void, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicFMin32, Void, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicFMax32, Void, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicSwap32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicIAdd32, U32, U32, U32, U1)
|
||||
VALUE_OPCODE(SharedAtomicIAdd64, Void, U32, U64, U1)
|
||||
@@ -309,15 +311,21 @@ VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress
|
||||
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
|
||||
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicIAdd32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicIAdd64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicSMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMin64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicSMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicUMax64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicAnd64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicOr64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicXor64, U64, ImageResource, ImageAddress, U64, U1)
|
||||
VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1)
|
||||
VALUE_OPCODE(GetAttribute, U32, U32, U32)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -347,6 +347,17 @@ void FoldInstruction(Block& block, Block::iterator instruction,
|
||||
if (IsImmediate(low, Type::U32) && IsImmediate(high, Type::U32)) {
|
||||
Replace(inst, Value(static_cast<uint64_t>(low.U32()) |
|
||||
(static_cast<uint64_t>(high.U32()) << 32u)));
|
||||
} else {
|
||||
const auto* low_extract = low.TryInstruction();
|
||||
const auto* high_extract = high.TryInstruction();
|
||||
if (low_extract != nullptr && high_extract != nullptr &&
|
||||
low_extract->GetOpcode() == ValueOpcode::CompositeExtractU64 &&
|
||||
high_extract->GetOpcode() == ValueOpcode::CompositeExtractU64 &&
|
||||
Arg(*low_extract, 1) == Value(0u) && Arg(*high_extract, 1) == Value(1u) &&
|
||||
Arg(*low_extract, 0) == Arg(*high_extract, 0)) {
|
||||
// Guest register pairs need not unpack and repack between wide operations.
|
||||
Replace(inst, Arg(*low_extract, 0));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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,19 +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 && (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;
|
||||
@@ -334,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),
|
||||
@@ -343,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) {
|
||||
if (indirect.selector_shift != 0u) {
|
||||
for (auto& key: keys) key >>= indirect.selector_shift;
|
||||
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);
|
||||
@@ -382,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;
|
||||
@@ -395,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;
|
||||
@@ -565,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");
|
||||
}
|
||||
@@ -663,7 +694,6 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
|
||||
}
|
||||
}
|
||||
std::vector<ResourceBlock> blocks(program.blocks.size());
|
||||
std::vector<std::vector<uint32_t>> predecessors(blocks.size());
|
||||
for (uint32_t i = 0; i < blocks.size(); i++) {
|
||||
auto& block = blocks[i];
|
||||
const auto& info = program.block_info[i];
|
||||
@@ -687,7 +717,6 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
|
||||
return {};
|
||||
}
|
||||
block.successors.push_back(found->second);
|
||||
predecessors[found->second].push_back(i);
|
||||
}
|
||||
for (const auto& inst: *program.blocks[i]) {
|
||||
const auto op = inst.GetOpcode();
|
||||
@@ -717,30 +746,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
|
||||
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
|
||||
block.sources.end());
|
||||
}
|
||||
// Host branches only matter when they guard a later resource access. Leave
|
||||
// data-only branches (for example a mutable flag checked before a trap) on the GPU.
|
||||
std::vector<uint8_t> resource_reachable(blocks.size());
|
||||
std::vector<uint32_t> pending;
|
||||
for (uint32_t i = 0; i < blocks.size(); ++i) {
|
||||
if (!blocks[i].sources.empty() || !blocks[i].srt_reads.empty()) {
|
||||
resource_reachable[i] = 1;
|
||||
pending.push_back(i);
|
||||
}
|
||||
}
|
||||
while (!pending.empty()) {
|
||||
const auto index = pending.back();
|
||||
pending.pop_back();
|
||||
for (const auto predecessor: predecessors[index]) {
|
||||
if (resource_reachable[predecessor]) continue;
|
||||
resource_reachable[predecessor] = 1;
|
||||
pending.push_back(predecessor);
|
||||
}
|
||||
}
|
||||
for (auto& block: blocks) {
|
||||
block.condition = std::ranges::any_of(block.successors, [&](uint32_t successor) {
|
||||
return resource_reachable[successor] != 0;
|
||||
}) ? predicate(block.condition) : Value {};
|
||||
}
|
||||
for (auto& block: blocks) block.condition = predicate(block.condition);
|
||||
if (std::ranges::none_of(
|
||||
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
|
||||
return {};
|
||||
@@ -1034,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()) {
|
||||
@@ -1180,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 {
|
||||
@@ -742,6 +802,15 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto* block: m_program.blocks) {
|
||||
for (auto& inst: *block) {
|
||||
uint32_t index = 0;
|
||||
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
|
||||
ScalarReadMemory(inst, index) != nullptr && inst.Arg(1).Resolve().IsImmediate() &&
|
||||
ValidateRuntimeValue(m_program, Value(&inst)))
|
||||
CollectScalarRead(Value(&inst), inst.Flags<MemoryFlags>().pc);
|
||||
}
|
||||
}
|
||||
for (auto* read: m_scalar_reads) {
|
||||
const auto flags = read->Flags<MemoryFlags>();
|
||||
auto& memory = m_program.memory_info[flags.index];
|
||||
@@ -825,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;
|
||||
@@ -833,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.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() ||
|
||||
@@ -851,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;
|
||||
}
|
||||
}
|
||||
@@ -940,41 +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();
|
||||
}
|
||||
|
||||
const Inst* MaterialKeyShift(Value key, uint32_t& amount) const {
|
||||
const auto* shift = key.Resolve().TryInstruction();
|
||||
return shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
|
||||
ImmediateU32(shift->Arg(1), amount) && amount != 0u && amount < 32u
|
||||
? shift : nullptr;
|
||||
}
|
||||
|
||||
enum class LaneQuantifier { Any, All };
|
||||
struct EdgePredicate {
|
||||
Value condition;
|
||||
@@ -1421,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;
|
||||
@@ -1584,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) {
|
||||
@@ -1593,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;
|
||||
}
|
||||
@@ -1608,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;
|
||||
@@ -1642,36 +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;
|
||||
uint32_t amount = 0;
|
||||
const auto* shift = MaterialKeyShift(selected->Arg(1), amount);
|
||||
if (shift == nullptr) return false;
|
||||
const auto* loaded = shift->Arg(0).Resolve().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;
|
||||
indirect.selector_shift = amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
|
||||
@@ -1679,20 +1722,21 @@ 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);
|
||||
} else if (table_stride != 32u) {
|
||||
return false;
|
||||
plan.retain_reads = true;
|
||||
} else if (table_source.dword_count == 2u) {
|
||||
if (table_stride != 32u) return false;
|
||||
const auto* selector = key.Resolve().TryInstruction();
|
||||
const bool bitscan = selector != nullptr && selector->GetOpcode() == ValueOpcode::FindILsb32 &&
|
||||
selector->NumArgs() == 1u && !m_shader_writes &&
|
||||
@@ -1709,32 +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)) {
|
||||
const auto* key_shift = MaterialKeyShift(key, indirect.selector_shift);
|
||||
auto* material_read =
|
||||
(key_shift != nullptr ? key_shift->Arg(0) : key).Resolve().TryInstruction();
|
||||
uint32_t material_memory_index = 0;
|
||||
const auto* memory = material_read != nullptr
|
||||
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
|
||||
if (table_offset != 0u || 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;
|
||||
}
|
||||
|
||||
if (plan.retain_reads) plan.reads.fill(nullptr);
|
||||
indirect.table_source = InternSource(table_source);
|
||||
DescriptorSource image_source;
|
||||
image_source.dword_count = 8u;
|
||||
@@ -1768,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;
|
||||
@@ -1802,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));
|
||||
@@ -2032,6 +2066,8 @@ private:
|
||||
DescriptorSource descriptor;
|
||||
MakeSource(*handle, inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
|
||||
false, false, memory.resource * 4u, descriptor, flags.pc);
|
||||
uint32_t bad_dword = 0;
|
||||
if (ValidateSource(descriptor, bad_dword)) continue;
|
||||
IndirectDescriptorPlan plan;
|
||||
if (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteImage(*handle, plan) ||
|
||||
TryMakeIndirectBuffer(*handle, descriptor, plan)) {
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace {
|
||||
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
|
||||
|
||||
bool AddSignedAddress(uint64_t base, int64_t offset, uint64_t& result) {
|
||||
if (base > AddressMask) {
|
||||
return false;
|
||||
}
|
||||
if (offset < 0) {
|
||||
const auto magnitude = uint64_t {0} - static_cast<uint64_t>(offset);
|
||||
if (magnitude > base) {
|
||||
@@ -32,7 +35,7 @@ bool AddSignedAddress(uint64_t base, int64_t offset, uint64_t& result) {
|
||||
return true;
|
||||
}
|
||||
const auto magnitude = static_cast<uint64_t>(offset);
|
||||
if (magnitude > UINT64_MAX - base) {
|
||||
if (magnitude > AddressMask - base) {
|
||||
return false;
|
||||
}
|
||||
result = base + magnitude;
|
||||
@@ -477,7 +480,7 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
|
||||
if (!Arg(*handle, 0, low) || !Arg(*handle, 1, high) || !Arg(inst, 1, offset)) {
|
||||
return false;
|
||||
}
|
||||
const auto base = (high << 32u) | static_cast<uint32_t>(low);
|
||||
const auto base = ((high << 32u) | static_cast<uint32_t>(low)) & AddressMask;
|
||||
const auto immediate = static_cast<int64_t>(static_cast<int32_t>(mem.offset));
|
||||
uint64_t address = 0;
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer || vector) {
|
||||
@@ -507,7 +510,7 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
|
||||
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
|
||||
return false;
|
||||
}
|
||||
address = (base & AddressMask & ~uint64_t {3}) + byte_offset;
|
||||
address = (base & ~uint64_t {3}) + byte_offset;
|
||||
} else {
|
||||
const auto relative = (immediate & ~int64_t {3}) +
|
||||
static_cast<int64_t>(static_cast<uint32_t>(offset) & ~3u);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -96,6 +96,7 @@ install(TARGETS launcher
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
|
||||
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
|
||||
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
|
||||
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
|
||||
if(NOT KYTY_MACDEPLOYQT)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>760</width>
|
||||
<width>900</width>
|
||||
<height>620</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -182,7 +182,22 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="notifications_group">
|
||||
<property name="title"><string>Notifications</string></property>
|
||||
<layout class="QVBoxLayout" name="notificationsLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_trophy_notifications">
|
||||
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
|
||||
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
|
||||
<property name="text"><string>Enable trophy notifications</string></property>
|
||||
<property name="checked"><bool>true</bool></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="debug_group">
|
||||
<property name="title">
|
||||
<string>Debugging && logs</string>
|
||||
@@ -385,6 +400,13 @@
|
||||
<property name="toolTip"><string>Restore default settings</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="settings_button">
|
||||
<property name="minimumSize"><size><width>80</width><height>25</height></size></property>
|
||||
<property name="text"><string>Game Configs</string></property>
|
||||
<property name="toolTip"><string>Import or export game configs</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="buttonSpacer">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Edit configuration</p></body></html></string>
|
||||
<string>Edit game config</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
@@ -150,7 +150,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Delete configuration</p></body></html></string>
|
||||
<string>Clear game config</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true"/>
|
||||
@@ -169,6 +169,28 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="trophy_overview_button">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>36</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>View saved trophies</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#000" d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M26 16a10 10 0 1 1-2.93-7.07L27 13"/>
|
||||
<path d="M27 5v8h-8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 252 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M9 5h14v8a7 7 0 0 1-14 0Z"/>
|
||||
<path d="M9 8H5v3a4 4 0 0 0 4 4M23 8h4v3a4 4 0 0 1-4 4"/>
|
||||
<path d="M16 20v4M12 28v-4h8v4M11 28h10"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -122,10 +120,11 @@ public:
|
||||
bool hide_cursor_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
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";
|
||||
@@ -154,6 +153,7 @@ public:
|
||||
hide_cursor_enabled = other.hide_cursor_enabled;
|
||||
readback_linear_images = other.readback_linear_images;
|
||||
tessellation_enabled = other.tessellation_enabled;
|
||||
trophy_enabled = other.trophy_enabled;
|
||||
vblank_frequency = other.vblank_frequency;
|
||||
console_language = other.console_language;
|
||||
vulkan_validation_enabled = other.vulkan_validation_enabled;
|
||||
@@ -186,88 +186,16 @@ public:
|
||||
game_comment = other.game_comment;
|
||||
}
|
||||
|
||||
void WriteSettings(QSettings* s) const {
|
||||
KYTY_CFG_SET(name);
|
||||
KYTY_CFG_SET(basedir);
|
||||
KYTY_CFG_SET(game_path);
|
||||
KYTY_CFG_SET(custom_settings);
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(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);
|
||||
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;
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
~ConfigurationListWidget() override;
|
||||
|
||||
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
|
||||
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
|
||||
|
||||
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
|
||||
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
|
||||
@@ -42,6 +43,7 @@ public:
|
||||
bool EnsureGameDirectory();
|
||||
void ScanGameDirectory();
|
||||
void ViewTrophies();
|
||||
void ViewTrophyOverview();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -72,12 +74,15 @@ 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;
|
||||
bool m_run_enabled = true;
|
||||
Ui::ConfigurationListWidget* m_ui = nullptr;
|
||||
QString m_settings_file;
|
||||
QString m_runtime_directory;
|
||||
QStringList m_game_dirs;
|
||||
Configuration m_global_info;
|
||||
QMap<QString, Configuration*> m_custom_infos;
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace GameContent {
|
||||
|
||||
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
|
||||
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
|
||||
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
|
||||
|
||||
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
|
||||
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,10 +14,13 @@ public:
|
||||
explicit TrophyViewerDialog(QWidget* parent = nullptr);
|
||||
|
||||
static bool HasTrophyData(const Configuration* info);
|
||||
static void ShowForGame(const Configuration* info, QWidget* parent);
|
||||
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, QString& error);
|
||||
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
|
||||
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
|
||||
<file alias="refresh.svg">icons/refresh.svg</file>
|
||||
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
|
||||
<file alias="trophy.svg">icons/trophy.svg</file>
|
||||
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="/styles">
|
||||
<file alias="launcher.qss">styles/launcher.qss</file>
|
||||
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "configuration.h"
|
||||
|
||||
QVariantMap Configuration::GameSettings() const {
|
||||
QVariantMap values;
|
||||
#define KYTY_CFG_SET(n) values.insert(QStringLiteral(#n), QVariant::fromValue(n).toString())
|
||||
KYTY_CFG_SET(screen_resolution);
|
||||
KYTY_CFG_SET(user_name);
|
||||
KYTY_CFG_SET(user_id);
|
||||
KYTY_CFG_SET(audio_input_device);
|
||||
KYTY_CFG_SET(present_mode);
|
||||
KYTY_CFG_SET(gpu_index);
|
||||
KYTY_CFG_SET(fullscreen_enabled);
|
||||
KYTY_CFG_SET(hide_cursor_enabled);
|
||||
KYTY_CFG_SET(readback_linear_images);
|
||||
KYTY_CFG_SET(tessellation_enabled);
|
||||
KYTY_CFG_SET(trophy_enabled);
|
||||
KYTY_CFG_SET(vblank_frequency);
|
||||
KYTY_CFG_SET(console_language);
|
||||
KYTY_CFG_SET(vulkan_validation_enabled);
|
||||
KYTY_CFG_SET(shader_validation_enabled);
|
||||
KYTY_CFG_SET(shader_optimization_type);
|
||||
KYTY_CFG_SET(shader_log_direction);
|
||||
KYTY_CFG_SET(shader_log_folder);
|
||||
KYTY_CFG_SET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_SET(command_buffer_dump_folder);
|
||||
KYTY_CFG_SET(printf_direction);
|
||||
KYTY_CFG_SET(printf_output_file);
|
||||
KYTY_CFG_SET(profiler_enabled);
|
||||
KYTY_CFG_SET(renderdoc_enabled);
|
||||
KYTY_CFG_SET(amd_cpu_enabled);
|
||||
#if defined(_WIN32)
|
||||
KYTY_CFG_SET(red_zone_protection_enabled);
|
||||
#endif
|
||||
KYTY_CFG_SET(elf);
|
||||
#undef KYTY_CFG_SET
|
||||
return values;
|
||||
}
|
||||
|
||||
template <class Settings>
|
||||
void Configuration::ReadGameSettingsValues(const Settings& s) {
|
||||
#define KYTY_CFG_GET(n) n = s.value(QStringLiteral(#n)).template value<decltype(n)>()
|
||||
KYTY_CFG_GET(screen_resolution);
|
||||
user_name = s.value("user_name", user_name).toString();
|
||||
bool user_id_ok = false;
|
||||
auto saved_user_id = s.value("user_id", user_id).toInt(&user_id_ok);
|
||||
user_id = user_id_ok && Config::IsConfiguredUserIdValid(saved_user_id)
|
||||
? saved_user_id
|
||||
: Config::DEFAULT_USER_ID;
|
||||
audio_input_device = s.value("audio_input_device", audio_input_device).toString();
|
||||
KYTY_CFG_GET(present_mode);
|
||||
gpu_index = s.value("gpu_index", -1).toInt();
|
||||
if (EnumToText(present_mode).isEmpty()) {
|
||||
present_mode = PresentMode::Mailbox;
|
||||
}
|
||||
KYTY_CFG_GET(fullscreen_enabled);
|
||||
KYTY_CFG_GET(hide_cursor_enabled);
|
||||
KYTY_CFG_GET(readback_linear_images);
|
||||
KYTY_CFG_GET(tessellation_enabled);
|
||||
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
|
||||
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
|
||||
console_language = s.value("console_language", console_language).toInt();
|
||||
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
|
||||
console_language = DEFAULT_CONSOLE_LANGUAGE;
|
||||
}
|
||||
KYTY_CFG_GET(vulkan_validation_enabled);
|
||||
KYTY_CFG_GET(shader_validation_enabled);
|
||||
KYTY_CFG_GET(shader_optimization_type);
|
||||
KYTY_CFG_GET(shader_log_direction);
|
||||
KYTY_CFG_GET(shader_log_folder);
|
||||
KYTY_CFG_GET(command_buffer_dump_enabled);
|
||||
KYTY_CFG_GET(command_buffer_dump_folder);
|
||||
KYTY_CFG_GET(printf_direction);
|
||||
KYTY_CFG_GET(printf_output_file);
|
||||
KYTY_CFG_GET(profiler_enabled);
|
||||
KYTY_CFG_GET(renderdoc_enabled);
|
||||
amd_cpu_enabled = s.value("amd_cpu_enabled", false).toBool();
|
||||
#if defined(_WIN32)
|
||||
red_zone_protection_enabled =
|
||||
s.value("red_zone_protection_enabled", red_zone_protection_enabled).toBool();
|
||||
#endif
|
||||
elf = s.value("elf", elf).toString();
|
||||
#undef KYTY_CFG_GET
|
||||
}
|
||||
|
||||
void Configuration::WriteSettings(QSettings* s) const {
|
||||
const auto values = GameSettings();
|
||||
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
|
||||
s->setValue(it.key(), it.value());
|
||||
}
|
||||
s->setValue("name", name);
|
||||
s->setValue("basedir", basedir);
|
||||
s->setValue("game_path", game_path);
|
||||
s->setValue("custom_settings", QVariant::fromValue(custom_settings).toString());
|
||||
s->setValue("host_input_mapping", host_input_mapping);
|
||||
}
|
||||
|
||||
void Configuration::ReadSettings(QSettings* s) {
|
||||
ReadGameSettingsValues(*s);
|
||||
name = s->value("name").toString();
|
||||
basedir = s->value("basedir").toString();
|
||||
game_path = s->value("game_path").toString();
|
||||
custom_settings = s->value("custom_settings").toBool();
|
||||
host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList();
|
||||
}
|
||||
|
||||
bool Configuration::SetGameSettings(const QJsonObject& settings, QString& error) {
|
||||
const auto fail = [&error](const QString& message) {
|
||||
error = message;
|
||||
return false;
|
||||
};
|
||||
auto values = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (!values.contains(it.key())) {
|
||||
return fail(tr("Unknown game setting: %1").arg(it.key()));
|
||||
}
|
||||
if (!it.value().isString()) {
|
||||
return fail(tr("Game setting %1 must be a string.").arg(it.key()));
|
||||
}
|
||||
values.insert(it.key(), it.value().toString());
|
||||
}
|
||||
|
||||
ReadGameSettingsValues(values);
|
||||
const auto canonical = GameSettings();
|
||||
for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) {
|
||||
if (canonical.value(it.key()) != values.value(it.key())) {
|
||||
return fail(tr("Invalid value for game setting: %1").arg(it.key()));
|
||||
}
|
||||
}
|
||||
if (user_name.trimmed().isEmpty() || user_name.toUtf8().size() > Config::MAX_USER_NAME_LENGTH) {
|
||||
return fail(tr("User name must contain 1-16 UTF-8 bytes."));
|
||||
}
|
||||
if (gpu_index < -1) {
|
||||
return fail(tr("GPU index must be -1 or greater."));
|
||||
}
|
||||
if (vblank_frequency < 30 || vblank_frequency > 360) {
|
||||
return fail(tr("Vblank frequency must be between 30 and 360."));
|
||||
}
|
||||
if ((shader_log_direction == LogDirection::File && shader_log_folder.isEmpty()) ||
|
||||
(printf_direction == LogDirection::File && printf_output_file.isEmpty()) ||
|
||||
(command_buffer_dump_enabled && command_buffer_dump_folder.isEmpty())) {
|
||||
return fail(tr("Enabled logging and command buffer dumps require an output path."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
@@ -123,8 +124,10 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
|
||||
m_info(info) {
|
||||
m_ui->setupUi(this);
|
||||
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);
|
||||
@@ -164,9 +167,6 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
|
||||
m_ui->lineEdit_printf_file->setEnabled(log == Configuration::LogDirection::File);
|
||||
});
|
||||
|
||||
// Keep the controls at a usable minimum while allowing the settings window
|
||||
// and its expanding fields to use any additional space the user gives them.
|
||||
layout()->setSizeConstraint(QLayout::SetMinimumSize);
|
||||
setSizeGripEnabled(true);
|
||||
|
||||
restoreGeometry(g_last_geometry);
|
||||
@@ -276,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_trophy_notifications->setChecked(info.trophy_enabled);
|
||||
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
|
||||
m_ui->comboBox_console_language->clear();
|
||||
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
|
||||
@@ -355,6 +356,11 @@ void ConfigurationEditDialog::InitGameDirectories() {
|
||||
|
||||
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
|
||||
m_global_settings = true;
|
||||
auto* menu = new QMenu(m_ui->settings_button);
|
||||
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
|
||||
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
|
||||
m_ui->settings_button->setMenu(menu);
|
||||
m_ui->settings_button->setVisible(true);
|
||||
m_game_dirs_list->clear();
|
||||
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
|
||||
|
||||
@@ -429,6 +435,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.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
|
||||
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
|
||||
info.console_language = ui.comboBox_console_language->currentIndex();
|
||||
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
|
||||
|
||||
@@ -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() {
|
||||
@@ -835,7 +973,22 @@ void ConfigurationListWidget::ViewTrophies() {
|
||||
return;
|
||||
}
|
||||
|
||||
TrophyViewerDialog::ShowForGame(&item->GetInfo(), this);
|
||||
const auto config = CreateConfiguration(*item);
|
||||
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() {
|
||||
@@ -1014,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();
|
||||
|
||||
@@ -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>
|
||||
@@ -170,6 +170,7 @@ void MainDialogPrivate::FindInterpreter() {
|
||||
}
|
||||
|
||||
bool found = QFile::exists(m_interpreter);
|
||||
m_ui->widget->SetRuntimeDirectory(QFileInfo(m_interpreter).absolutePath());
|
||||
|
||||
if (found) {
|
||||
m_ui->label_Interpreter->setText(tr("Emulator: ") + m_interpreter);
|
||||
@@ -244,6 +245,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
args << "--hide-cursor";
|
||||
}
|
||||
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
|
||||
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
|
||||
if (info.tessellation_enabled) {
|
||||
args << "--tessellation";
|
||||
}
|
||||
@@ -283,7 +285,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,498 +1,69 @@
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/trophies.h"
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QHBoxLayout>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QMap>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTabBar>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QVariant>
|
||||
#include <QWidget>
|
||||
#include <QtEndian>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr quint32 UCP_MAGIC = 0xb228c60a;
|
||||
constexpr quint32 UCP_VERSION = 1;
|
||||
constexpr int UCP_HEADER_LEN = 0x40;
|
||||
constexpr int UCP_TOC_SKIP = 0x20;
|
||||
constexpr int UCP_ENTRY_LEN = 0x40;
|
||||
constexpr int UCP_NAME_LEN = 0x20;
|
||||
constexpr quint32 UCP_MAX_FILES = 4096;
|
||||
constexpr quint64 UCP_MAX_EXTRACTED_SIZE = quint64 {64} << 20u;
|
||||
|
||||
struct UcpEntry {
|
||||
QString name;
|
||||
quint64 offset = 0;
|
||||
quint64 size = 0;
|
||||
};
|
||||
|
||||
struct TrophyDefinition {
|
||||
QString id;
|
||||
QString grade;
|
||||
bool hidden = false;
|
||||
bool has_reward = false;
|
||||
};
|
||||
|
||||
struct TrophyText {
|
||||
QString name;
|
||||
QString detail;
|
||||
QString reward;
|
||||
};
|
||||
|
||||
struct TrophyRow {
|
||||
QString id;
|
||||
QString name;
|
||||
QString detail;
|
||||
QString grade;
|
||||
QString reward;
|
||||
bool hidden = false;
|
||||
bool has_reward = false;
|
||||
QPixmap icon;
|
||||
};
|
||||
|
||||
struct TrophySet {
|
||||
QString tab_title;
|
||||
QList<TrophyRow> trophies;
|
||||
};
|
||||
|
||||
static bool CanRead(const QByteArray& data, qsizetype offset, qsizetype size) {
|
||||
return offset >= 0 && size >= 0 && offset <= data.size() && size <= data.size() - offset;
|
||||
}
|
||||
|
||||
static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizetype max_size) {
|
||||
if (!CanRead(data, offset, max_size)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
qsizetype len = 0;
|
||||
while (len < max_size && data.at(offset + len) != '\0') {
|
||||
len++;
|
||||
}
|
||||
|
||||
return QString::fromLatin1(data.constData() + offset, len);
|
||||
}
|
||||
|
||||
static bool IsUsedUcpEntry(const QString& name) {
|
||||
const auto lower = name.toCaseFolded();
|
||||
return lower == QStringLiteral("tropconf.json") || lower == QStringLiteral("tropmeta.json") ||
|
||||
(lower.startsWith(QStringLiteral("tropmeta_")) &&
|
||||
lower.endsWith(QStringLiteral(".json"))) ||
|
||||
(lower.startsWith(QStringLiteral("trop")) && lower.endsWith(QStringLiteral(".png")));
|
||||
}
|
||||
|
||||
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
|
||||
const QByteArray data =
|
||||
GameContent::ReadPath(GameContent::ToPath(file_name), GameContent::MaxTrophyPackageSize);
|
||||
if (data.isEmpty()) {
|
||||
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.size() < UCP_HEADER_LEN) {
|
||||
error = QObject::tr("%1 is too small to be a trophy package.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto magic = qFromBigEndian<quint32>(data.constData() + 0x00);
|
||||
if (magic != UCP_MAGIC) {
|
||||
error = QObject::tr("%1 has an invalid trophy package magic.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto version = qFromBigEndian<quint32>(data.constData() + 0x04);
|
||||
if (version != UCP_VERSION) {
|
||||
error = QObject::tr("%1 uses unsupported trophy package version %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), QString::number(version));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto declared_size = qFromBigEndian<quint64>(data.constData() + 0x08);
|
||||
if (declared_size < UCP_HEADER_LEN || declared_size > static_cast<quint64>(data.size())) {
|
||||
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_count = qFromBigEndian<quint32>(data.constData() + 0x10);
|
||||
if (file_count > UCP_MAX_FILES) {
|
||||
error = QObject::tr("%1 contains too many files.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
const auto toc_offset = static_cast<quint64>(qFromBigEndian<quint32>(data.constData() + 0x14));
|
||||
const auto data_size = declared_size;
|
||||
|
||||
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
|
||||
if (toc_offset > data_size || table_size > data_size - toc_offset ||
|
||||
!std::in_range<qsizetype>(toc_offset + table_size)) {
|
||||
error = QObject::tr("%1 has an invalid table of contents.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 extracted_size = 0;
|
||||
for (quint32 i = 0; i < file_count; i++) {
|
||||
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
|
||||
static_cast<quint64>(i) * UCP_ENTRY_LEN);
|
||||
UcpEntry entry;
|
||||
entry.name = ReadFixedString(data, entry_offset, UCP_NAME_LEN).trimmed();
|
||||
entry.offset = qFromBigEndian<quint64>(data.constData() + entry_offset + 0x20);
|
||||
entry.size = qFromBigEndian<quint64>(data.constData() + entry_offset + 0x28);
|
||||
|
||||
if (entry.name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.offset > data_size || entry.size > data_size - entry.offset ||
|
||||
!std::in_range<qsizetype>(entry.offset) || !std::in_range<qsizetype>(entry.size)) {
|
||||
error = QObject::tr("%1 has an invalid entry for %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
if (!IsUsedUcpEntry(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto entry_limit = entry.name.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)
|
||||
? GameContent::MaxImageSize
|
||||
: GameContent::MaxMetadataSize;
|
||||
if (entry.size > entry_limit || entry.size > UCP_MAX_EXTRACTED_SIZE - extracted_size) {
|
||||
error = QObject::tr("%1 contains an oversized entry for %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
|
||||
static_cast<qsizetype>(entry.size)));
|
||||
extracted_size += entry.size;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static const QByteArray* FindFile(const QMap<QString, QByteArray>& files, const QString& name) {
|
||||
const auto it = files.constFind(name.toCaseFolded());
|
||||
return it == files.constEnd() ? nullptr : &it.value();
|
||||
}
|
||||
|
||||
static const QByteArray* FindFirstTrophyMetadataFile(const QMap<QString, QByteArray>& files) {
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
if (it.key().startsWith(QStringLiteral("tropmeta_")) &&
|
||||
it.key().endsWith(QStringLiteral(".json"))) {
|
||||
return &it.value();
|
||||
}
|
||||
}
|
||||
|
||||
return FindFile(files, QStringLiteral("tropmeta.json"));
|
||||
}
|
||||
|
||||
static bool ReadJsonObject(const QByteArray& data, const QString& file_name, QJsonObject& object,
|
||||
QString& error) {
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
error = QObject::tr("Could not read %1: %2").arg(file_name, parse_error.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
object = doc.object();
|
||||
return true;
|
||||
}
|
||||
|
||||
static QString JsonString(const QJsonValue& value) {
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
if (number == static_cast<int>(number)) {
|
||||
return QString::number(static_cast<int>(number));
|
||||
}
|
||||
return QString::number(number);
|
||||
}
|
||||
if (value.isBool()) {
|
||||
return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QList<TrophyDefinition> ReadDefinitions(const QJsonObject& tropconf) {
|
||||
QList<TrophyDefinition> ret;
|
||||
|
||||
const auto trophies = tropconf.value(QStringLiteral("trophies")).toArray();
|
||||
for (const auto& value: trophies) {
|
||||
const auto obj = value.toObject();
|
||||
TrophyDefinition def;
|
||||
def.id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
|
||||
def.grade = JsonString(obj.value(QStringLiteral("grade"))).trimmed();
|
||||
def.hidden = obj.value(QStringLiteral("hidden")).toBool(false);
|
||||
def.has_reward = obj.value(QStringLiteral("hasReward")).toBool(false);
|
||||
|
||||
if (!def.id.isEmpty()) {
|
||||
ret.append(def);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static QMap<QString, TrophyText> ReadMetadata(const QJsonObject& tropmeta) {
|
||||
QMap<QString, TrophyText> ret;
|
||||
|
||||
const auto metadata = tropmeta.value(QStringLiteral("metadata")).toObject();
|
||||
const auto trophies = metadata.value(QStringLiteral("trophyMetadata")).toArray();
|
||||
for (const auto& value: trophies) {
|
||||
const auto obj = value.toObject();
|
||||
const auto id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
|
||||
if (id.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TrophyText text;
|
||||
text.name = JsonString(obj.value(QStringLiteral("name"))).trimmed();
|
||||
text.detail = JsonString(obj.value(QStringLiteral("detail"))).trimmed();
|
||||
text.reward = JsonString(obj.value(QStringLiteral("reward"))).trimmed();
|
||||
ret.insert(id, text);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static QString GradeToText(const QString& grade) {
|
||||
if (grade == QStringLiteral("P")) {
|
||||
return QObject::tr("Platinum");
|
||||
}
|
||||
if (grade == QStringLiteral("G")) {
|
||||
return QObject::tr("Gold");
|
||||
}
|
||||
if (grade == QStringLiteral("S")) {
|
||||
return QObject::tr("Silver");
|
||||
}
|
||||
if (grade == QStringLiteral("B")) {
|
||||
return QObject::tr("Bronze");
|
||||
}
|
||||
|
||||
return grade;
|
||||
}
|
||||
|
||||
static QPixmap LoadTrophyIcon(const QMap<QString, QByteArray>& files, const QString& id) {
|
||||
QStringList names;
|
||||
names.append(QStringLiteral("trop%1.png").arg(id));
|
||||
|
||||
bool id_is_number = false;
|
||||
const int id_num = id.toInt(&id_is_number);
|
||||
if (id_is_number) {
|
||||
names.append(QStringLiteral("trop%1.png").arg(id_num, 4, 10, QLatin1Char('0')));
|
||||
}
|
||||
|
||||
for (const auto& name: names) {
|
||||
const auto* data = FindFile(files, name);
|
||||
if (data == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QPixmap pixmap;
|
||||
if (pixmap.loadFromData(*data)) {
|
||||
return pixmap;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString TrophyTabTitle(const QString& file_name) {
|
||||
static const QRegularExpression trophy_file_re(QStringLiteral("^trophy(\\d+)\\.ucp$"),
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
const auto name = QFileInfo(file_name).fileName();
|
||||
const auto match = trophy_file_re.match(name);
|
||||
if (match.hasMatch()) {
|
||||
return QObject::tr("Trophy %1").arg(match.captured(1));
|
||||
}
|
||||
|
||||
return QFileInfo(file_name).completeBaseName();
|
||||
}
|
||||
|
||||
static bool BuildTrophySet(const QString& ucp_file, TrophySet& set, QString& error) {
|
||||
QMap<QString, QByteArray> files;
|
||||
if (!ReadUcp(ucp_file, files, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* conf_data = FindFile(files, QStringLiteral("tropconf.json"));
|
||||
if (conf_data == nullptr) {
|
||||
error =
|
||||
QObject::tr("%1 does not contain tropconf.json.").arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject tropconf;
|
||||
if (!ReadJsonObject(*conf_data, QStringLiteral("tropconf.json"), tropconf, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto default_language =
|
||||
JsonString(tropconf.value(QStringLiteral("defaultLanguage"))).trimmed();
|
||||
const QByteArray* meta_data = nullptr;
|
||||
if (!default_language.isEmpty()) {
|
||||
meta_data = FindFile(files, QStringLiteral("tropmeta_%1.json").arg(default_language));
|
||||
}
|
||||
if (meta_data == nullptr) {
|
||||
meta_data = FindFirstTrophyMetadataFile(files);
|
||||
}
|
||||
if (meta_data == nullptr) {
|
||||
error = QObject::tr("%1 does not contain readable trophy metadata.")
|
||||
.arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject tropmeta;
|
||||
if (!ReadJsonObject(*meta_data, QStringLiteral("tropmeta.json"), tropmeta, error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto definitions = ReadDefinitions(tropconf);
|
||||
if (definitions.isEmpty()) {
|
||||
error = QObject::tr("%1 does not define any trophies.").arg(QFileInfo(ucp_file).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto texts = ReadMetadata(tropmeta);
|
||||
|
||||
set.tab_title = TrophyTabTitle(ucp_file);
|
||||
for (const auto& def: definitions) {
|
||||
const auto text = texts.value(def.id);
|
||||
|
||||
TrophyRow row;
|
||||
row.id = def.id;
|
||||
row.grade = def.grade;
|
||||
row.hidden = def.hidden;
|
||||
row.has_reward = def.has_reward;
|
||||
row.reward = text.reward;
|
||||
row.name = text.name;
|
||||
row.detail = text.detail;
|
||||
row.icon = LoadTrophyIcon(files, def.id);
|
||||
|
||||
if (row.name.isEmpty()) {
|
||||
row.name =
|
||||
row.hidden ? QObject::tr("Hidden Trophy") : QObject::tr("Trophy %1").arg(def.id);
|
||||
}
|
||||
if (row.detail.isEmpty() && row.hidden) {
|
||||
row.detail = QObject::tr("This trophy is hidden.");
|
||||
}
|
||||
|
||||
set.trophies.append(row);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
static const QRegularExpression TrophyFilePattern(QStringLiteral("^trophy(\\d+)\\.ucp$"));
|
||||
|
||||
static QStringList FindTrophyFiles(const Configuration* info) {
|
||||
if (info == nullptr || info->basedir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList files;
|
||||
for (const auto& file:
|
||||
GameContent::ListFiles(info->basedir, QStringLiteral("sce_sys/trophy2"))) {
|
||||
const auto name = QFileInfo(file).fileName();
|
||||
if (name.startsWith(QStringLiteral("trophy"), Qt::CaseInsensitive) &&
|
||||
name.endsWith(QStringLiteral(".ucp"), Qt::CaseInsensitive)) {
|
||||
for (const auto& file: GameContent::ListFiles(
|
||||
info->basedir, QString::fromLatin1(Common::Trophies::PackageDirectory))) {
|
||||
if (TrophyFilePattern.match(QFileInfo(file).fileName()).hasMatch()) {
|
||||
files.append(file);
|
||||
}
|
||||
}
|
||||
files.sort(Qt::CaseInsensitive);
|
||||
|
||||
QStringList trophy_files;
|
||||
QSet<QString> seen;
|
||||
for (const auto& file: files) {
|
||||
auto key = QFileInfo(file).canonicalFilePath();
|
||||
if (key.isEmpty()) {
|
||||
key = file;
|
||||
}
|
||||
key = QDir::cleanPath(key).toCaseFolded();
|
||||
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
trophy_files.append(file);
|
||||
}
|
||||
}
|
||||
return trophy_files;
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
static void PrepareTable(QTableWidget* table) {
|
||||
table->setColumnCount(4);
|
||||
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->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 GradeToText(int grade) {
|
||||
switch (grade) {
|
||||
case 1: return QObject::tr("Platinum");
|
||||
case 2: return QObject::tr("Gold");
|
||||
case 3: return QObject::tr("Silver");
|
||||
case 4: return QObject::tr("Bronze");
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
static QTableWidgetItem* CreateItem(const QString& text) {
|
||||
auto* item = new QTableWidgetItem(text);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
return item;
|
||||
}
|
||||
|
||||
static QString TrophyTooltip(const TrophyRow& row) {
|
||||
QStringList lines;
|
||||
lines.append(row.name);
|
||||
if (!row.detail.isEmpty()) {
|
||||
lines.append(row.detail);
|
||||
}
|
||||
|
||||
const auto grade = GradeToText(row.grade);
|
||||
if (!grade.isEmpty()) {
|
||||
lines.append(QObject::tr("Grade: %1").arg(grade));
|
||||
}
|
||||
if (row.hidden) {
|
||||
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
|
||||
QStringList lines {QString::fromStdString(trophy.name),
|
||||
QString::fromStdString(trophy.description),
|
||||
QObject::tr("Grade: %1").arg(GradeToText(trophy.grade))};
|
||||
if (trophy.hidden) {
|
||||
lines.append(QObject::tr("Hidden trophy"));
|
||||
}
|
||||
if (row.has_reward && !row.reward.isEmpty()) {
|
||||
lines.append(QObject::tr("Reward: %1").arg(row.reward));
|
||||
if (trophy.has_reward && !trophy.reward.empty()) {
|
||||
lines.append(QObject::tr("Reward: %1").arg(QString::fromStdString(trophy.reward)));
|
||||
}
|
||||
|
||||
return lines.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
@@ -516,7 +87,227 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
|
||||
return !FindTrophyFiles(info).isEmpty();
|
||||
}
|
||||
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -527,7 +318,7 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
|
||||
}
|
||||
|
||||
QString error;
|
||||
if (!dlg.LoadGame(*info, error)) {
|
||||
if (!dlg.LoadGame(*info, runtime_directory, error)) {
|
||||
QMessageBox::warning(parent, tr("Trophy Viewer"), error);
|
||||
return;
|
||||
}
|
||||
@@ -535,66 +326,151 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
|
||||
bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runtime_directory,
|
||||
QString& error) {
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
|
||||
const auto trophy_files = FindTrophyFiles(&info);
|
||||
if (trophy_files.isEmpty()) {
|
||||
error = tr("No trophy package found in sce_sys/trophy2.");
|
||||
error = tr("No trophy package found in %1.")
|
||||
.arg(QString::fromLatin1(Common::Trophies::PackageDirectory));
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList errors;
|
||||
for (const auto& file: trophy_files) {
|
||||
TrophySet set;
|
||||
QString set_error;
|
||||
if (!BuildTrophySet(file, set, set_error)) {
|
||||
errors.append(set_error);
|
||||
const auto package =
|
||||
Common::Trophies::LoadPackage(GameContent::ToPath(file), info.console_language);
|
||||
if (package.trophies.empty()) {
|
||||
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
|
||||
continue;
|
||||
}
|
||||
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* table = new QTableWidget(set.trophies.size(), 4, m_tabs);
|
||||
PrepareTable(table);
|
||||
|
||||
for (int row_index = 0; row_index < set.trophies.size(); row_index++) {
|
||||
const auto& row = set.trophies.at(row_index);
|
||||
table->setRowHeight(row_index, 112);
|
||||
|
||||
auto* status = CreateItem(tr("locked"));
|
||||
status->setTextAlignment(Qt::AlignCenter);
|
||||
if (row.hidden) {
|
||||
status->setForeground(QBrush(Qt::gray));
|
||||
}
|
||||
table->setItem(row_index, 0, status);
|
||||
|
||||
auto* icon_label = new QLabel(table);
|
||||
icon_label->setAlignment(Qt::AlignCenter);
|
||||
icon_label->setMinimumSize(QSize(108, 108));
|
||||
if (!row.icon.isNull()) {
|
||||
icon_label->setPixmap(
|
||||
row.icon.scaled(QSize(96, 96), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
table->setCellWidget(row_index, 1, icon_label);
|
||||
|
||||
auto* name = CreateItem(row.name);
|
||||
auto font = name->font();
|
||||
font.setBold(true);
|
||||
name->setFont(font);
|
||||
name->setToolTip(TrophyTooltip(row));
|
||||
table->setItem(row_index, 2, name);
|
||||
|
||||
auto* detail = CreateItem(row.detail);
|
||||
detail->setToolTip(TrophyTooltip(row));
|
||||
table->setItem(row_index, 3, detail);
|
||||
auto* header = new QHBoxLayout;
|
||||
auto* art = new QLabel(page);
|
||||
if (const auto pixmap = LoadGameArt(&info, 56); !pixmap.isNull()) {
|
||||
art->setPixmap(pixmap);
|
||||
header->addWidget(art);
|
||||
}
|
||||
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);
|
||||
|
||||
m_tabs->addTab(table, set.tab_title);
|
||||
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.isEmpty() ? tr("No readable trophy data found.")
|
||||
: errors.join(QLatin1Char('\n'));
|
||||
error = errors.join(QLatin1Char('\n'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+17
-5
@@ -2529,7 +2529,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 +2786,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 +2915,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;
|
||||
}
|
||||
|
||||
+56
-29
@@ -2831,6 +2831,8 @@ constexpr int32_t KERNEL_AIO_STATE_ABORTED = 4;
|
||||
constexpr int32_t KERNEL_AIO_STATE_NOTIFIED = 0x10000;
|
||||
constexpr int32_t KERNEL_AIO_MAX_QUEUE = 512;
|
||||
constexpr int32_t KERNEL_AIO_MAX_REQUESTS = 128;
|
||||
constexpr uint32_t KERNEL_AIO_WAIT_AND = 1;
|
||||
constexpr uint32_t KERNEL_AIO_WAIT_OR = 2;
|
||||
|
||||
struct KernelAioResult {
|
||||
int64_t return_value;
|
||||
@@ -2933,26 +2935,28 @@ int KYTY_SYSV_ABI KernelAioSubmitWriteCommands(KernelAioRwRequest* req, int32_t
|
||||
return OK;
|
||||
}
|
||||
|
||||
static int32_t kernel_aio_get_state(int32_t id, bool notify) {
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
return LibKernel::KERNEL_ERROR_ESRCH;
|
||||
}
|
||||
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (notify && (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED)) {
|
||||
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
|
||||
std::memory_order_acq_rel)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioPollRequest(int32_t id, int32_t* state) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (state == nullptr) {
|
||||
return LibKernel::KERNEL_ERROR_EFAULT;
|
||||
}
|
||||
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
*state = LibKernel::KERNEL_ERROR_ESRCH;
|
||||
return OK;
|
||||
}
|
||||
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED) {
|
||||
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
|
||||
std::memory_order_acq_rel)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*state = current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
|
||||
*state = kernel_aio_get_state(id, true);
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -2969,31 +2973,53 @@ int KYTY_SYSV_ABI KernelAioPollRequests(int32_t* ids, int32_t num, int32_t* stat
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequests(int32_t* ids, int32_t num, int32_t* states, uint32_t mode,
|
||||
uint32_t* usec) {
|
||||
PRINT_NAME();
|
||||
|
||||
if (state == nullptr) {
|
||||
if (ids == nullptr || states == nullptr) {
|
||||
return LibKernel::KERNEL_ERROR_EFAULT;
|
||||
}
|
||||
|
||||
if (!kernel_aio_is_valid_id(id)) {
|
||||
return KernelAioPollRequest(id, state);
|
||||
if (num <= 0 || num > KERNEL_AIO_MAX_REQUESTS ||
|
||||
(num != 1 && mode != KERNEL_AIO_WAIT_AND && mode != KERNEL_AIO_WAIT_OR)) {
|
||||
return LibKernel::KERNEL_ERROR_EINVAL;
|
||||
}
|
||||
|
||||
uint32_t waited = 0;
|
||||
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
while (current == KERNEL_AIO_STATE_PROCESSING) {
|
||||
if (usec != nullptr && *usec != 0 && waited >= *usec) {
|
||||
*state = current;
|
||||
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
const auto collect_states = [&](bool notify) {
|
||||
int32_t finished = 0;
|
||||
bool newly_complete = false;
|
||||
for (int32_t i = 0; i < num; i++) {
|
||||
const auto current = kernel_aio_get_state(ids[i], notify);
|
||||
states[i] = current;
|
||||
finished += current != KERNEL_AIO_STATE_PROCESSING;
|
||||
newly_complete |= current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED;
|
||||
}
|
||||
return finished == num || (mode == KERNEL_AIO_WAIT_OR && newly_complete);
|
||||
};
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
const bool complete = collect_states(false);
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
const bool timed_out = usec != nullptr && elapsed >= *usec;
|
||||
if (complete || timed_out) {
|
||||
// A competing waiter can claim the newly completed request after the first scan.
|
||||
if (collect_states(true)) {
|
||||
if (usec != nullptr) {
|
||||
*usec = elapsed < *usec ? *usec - static_cast<uint32_t>(elapsed) : 0;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
if (timed_out) {
|
||||
return LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Thread::SleepMicro(10);
|
||||
waited += 10;
|
||||
current = g_kernel_aio_state[id].load(std::memory_order_acquire);
|
||||
}
|
||||
}
|
||||
|
||||
return KernelAioPollRequest(id, state);
|
||||
int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
|
||||
return KernelAioWaitRequests(&id, 1, state, KERNEL_AIO_WAIT_AND, usec);
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI KernelAioDeleteRequest(int32_t id, int32_t* ret) {
|
||||
@@ -3376,6 +3402,7 @@ LIB_DEFINE(InitLibKernel_1) {
|
||||
LIB_FUNC("2pOuoWoCxdk", KernelAioPollRequest);
|
||||
LIB_FUNC("o7O4z3jwKzo", KernelAioPollRequests);
|
||||
LIB_FUNC("KOF-oJbQVvc", KernelAioWaitRequest);
|
||||
LIB_FUNC("lgK+oIWkJyA", KernelAioWaitRequests);
|
||||
LIB_FUNC("XQ8C8y+de+E", KernelAioSubmitWriteCommands);
|
||||
LIB_FUNC("nu4a0-arQis", KernelAioInitializeParam);
|
||||
LIB_FUNC("il03nluKfMk", LibKernel::KernelRaiseException);
|
||||
|
||||
+88
-1143
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+84
-12
@@ -992,6 +992,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case WSAENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case WSAENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case WSAENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case WSAENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case WSAEOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case WSAEPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
case WSAESHUTDOWN: posix_error = Posix::POSIX_ESHUTDOWN; break;
|
||||
@@ -1028,6 +1029,7 @@ static int ConvertHostSocketError(int error) {
|
||||
case ENETUNREACH: posix_error = Posix::POSIX_ENETUNREACH; break;
|
||||
case ENOTCONN: posix_error = Posix::POSIX_ENOTCONN; break;
|
||||
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
|
||||
case ENOPROTOOPT: posix_error = Posix::POSIX_ENOPROTOOPT; break;
|
||||
case EOPNOTSUPP: posix_error = Posix::POSIX_EOPNOTSUPP; break;
|
||||
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
|
||||
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
|
||||
@@ -1066,6 +1068,26 @@ static int ConvertSocketOptionLevel(int level) {
|
||||
return (level == 0xffff ? SOL_SOCKET : level);
|
||||
}
|
||||
|
||||
static int ConvertSocketOptionName(int level, int option) {
|
||||
if (level == 0xffff) {
|
||||
switch (option) {
|
||||
case 0x0020: return SO_BROADCAST;
|
||||
case 0x1001: return SO_SNDBUF;
|
||||
case 0x1002: return SO_RCVBUF;
|
||||
case 0x1007: return SO_ERROR;
|
||||
case 0x1105: return SO_SNDTIMEO;
|
||||
default: break;
|
||||
}
|
||||
} else if (level == 6 && option == 1) {
|
||||
return TCP_NODELAY;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return option;
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int* P2pSocketOption(P2pEndpoint& endpoint, int option) {
|
||||
switch (option) {
|
||||
case 0x0020: return &endpoint.broadcast;
|
||||
@@ -2149,21 +2171,41 @@ int KYTY_SYSV_ABI Getsockopt(int s, int level, int optname, void* optval, uint32
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Guest socket options: SOL_SOCKET=0xffff, SO_ERROR=0x1007.
|
||||
const bool socket_error = (level == 0xffff && optname == 0x1007);
|
||||
#if !defined(_WIN32)
|
||||
if (!socket_error) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (socket_error) {
|
||||
optname = SO_ERROR;
|
||||
}
|
||||
SocketLength len = static_cast<SocketLength>(*optlen);
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(optval),
|
||||
void* value = optval;
|
||||
if (send_timeout) {
|
||||
if (*optlen < sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
value = &timeout;
|
||||
len = sizeof(timeout);
|
||||
}
|
||||
if (::getsockopt(socket, ConvertSocketOptionLevel(level), optname, static_cast<char*>(value),
|
||||
&len) != 0) {
|
||||
return SetHostSocketError();
|
||||
}
|
||||
if (send_timeout) {
|
||||
#if defined(_WIN32)
|
||||
const int64_t usec = static_cast<int64_t>(timeout) * 1000;
|
||||
#else
|
||||
const int64_t usec = static_cast<int64_t>(timeout.tv_sec) * 1'000'000 + timeout.tv_usec;
|
||||
#endif
|
||||
const int guest_timeout =
|
||||
static_cast<int>(std::min<int64_t>(usec, std::numeric_limits<int>::max()));
|
||||
std::memcpy(optval, &guest_timeout, sizeof(guest_timeout));
|
||||
len = sizeof(guest_timeout);
|
||||
}
|
||||
if (socket_error && len >= static_cast<SocketLength>(sizeof(int))) {
|
||||
auto* error = static_cast<int*>(optval);
|
||||
*error = ConvertHostSocketError(*error);
|
||||
@@ -2215,6 +2257,18 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
if (level == 0xffff && optname == 0x10000) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int enabled = 0;
|
||||
std::memcpy(&enabled, optval, sizeof(enabled));
|
||||
// Native sends preserve 255.255.255.255 without changing broadcast permission.
|
||||
return enabled != 0 ? 0 : SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int ORBIS_SO_NBIO = 0x1200;
|
||||
if (ConvertSocketOptionLevel(level) == SOL_SOCKET && optname == ORBIS_SO_NBIO &&
|
||||
optlen >= sizeof(int)) {
|
||||
@@ -2230,14 +2284,32 @@ int KYTY_SYSV_ABI Setsockopt(int s, int level, int optname, const void* optval,
|
||||
#endif
|
||||
return failed ? SetHostSocketError() : 0;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
// Guest TCP options: IPPROTO_TCP=6, TCP_NODELAY=1.
|
||||
if (level != 6 || optname != 1) {
|
||||
const bool send_timeout = (level == 0xffff && optname == 0x1105);
|
||||
optname = ConvertSocketOptionName(level, optname);
|
||||
if (optname < 0) {
|
||||
return SetGuestSocketError(Posix::POSIX_ENOPROTOOPT);
|
||||
}
|
||||
level = IPPROTO_TCP;
|
||||
optname = TCP_NODELAY;
|
||||
#if defined(_WIN32)
|
||||
DWORD timeout {};
|
||||
#else
|
||||
timeval timeout {};
|
||||
#endif
|
||||
if (send_timeout) {
|
||||
if (optlen != sizeof(int)) {
|
||||
return SetGuestSocketError(Posix::POSIX_EINVAL);
|
||||
}
|
||||
int usec = 0;
|
||||
std::memcpy(&usec, optval, sizeof(usec));
|
||||
usec = std::max(usec, 0);
|
||||
#if defined(_WIN32)
|
||||
timeout = static_cast<DWORD>((static_cast<int64_t>(usec) + 999) / 1000);
|
||||
#else
|
||||
timeout.tv_sec = usec / 1'000'000;
|
||||
timeout.tv_usec = usec % 1'000'000;
|
||||
#endif
|
||||
optval = &timeout;
|
||||
optlen = sizeof(timeout);
|
||||
}
|
||||
|
||||
if (::setsockopt(socket, ConvertSocketOptionLevel(level), optname,
|
||||
static_cast<const char*>(optval), static_cast<SocketLength>(optlen)) != 0) {
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#if !defined(__APPLE__)
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <xbyak/xbyak_util.h>
|
||||
#endif
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
@@ -33,9 +31,7 @@
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
using namespace Xbyak::util;
|
||||
#endif
|
||||
|
||||
namespace Loader {
|
||||
|
||||
@@ -52,8 +48,6 @@ using s64 = int64_t;
|
||||
|
||||
constexpr size_t NearJumpSize = 5;
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
struct PatchModule {
|
||||
std::mutex mutex {};
|
||||
u8* start = nullptr;
|
||||
@@ -1573,19 +1567,6 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
|
||||
return {};
|
||||
}
|
||||
|
||||
GuestInstructionPatchResult PatchGuestInstructions(u64, u64, std::span<const uintptr_t>, bool, bool,
|
||||
GuestInstructionHostFeatures) {
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f;
|
||||
@@ -1763,7 +1744,6 @@ bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_fram
|
||||
|
||||
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
void* trampoline_area_ptr, uint64_t trampoline_area_size) {
|
||||
#if !defined(__APPLE__)
|
||||
EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr ||
|
||||
trampoline_area_size == 0);
|
||||
const auto module_addr = reinterpret_cast<u64>(module_ptr);
|
||||
@@ -1772,20 +1752,10 @@ void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
||||
std::forward_as_tuple(static_cast<u8*>(module_ptr), module_size,
|
||||
static_cast<u8*>(trampoline_area_ptr),
|
||||
trampoline_area_size));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
(void)module_size;
|
||||
(void)trampoline_area_ptr;
|
||||
(void)trampoline_area_size;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
|
||||
#if !defined(__APPLE__)
|
||||
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
|
||||
#else
|
||||
(void)module_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
#undef ASSERT
|
||||
|
||||
@@ -1670,11 +1670,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
|
||||
uint64_t tls_handler_size = is_shared ? 0 : Jit::SafeCall::GetSize();
|
||||
EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned);
|
||||
program->mapped_size = program->base_size_aligned + tls_handler_size;
|
||||
#if !defined(__APPLE__)
|
||||
const bool emulate_amd = Config::AmdCpuEnabled();
|
||||
#else
|
||||
const bool emulate_amd = false;
|
||||
#endif
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
const bool protect_memory_faults = Config::RedZoneProtectionEnabled();
|
||||
#else
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
#include <windows.h> // IWYU pragma: keep
|
||||
#elif defined(__APPLE__)
|
||||
#include <sched.h>
|
||||
#include <sys/ucontext.h>
|
||||
#else
|
||||
#include <sched.h>
|
||||
@@ -468,6 +469,16 @@ struct Context {
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
// The kernel hands AVX threads the longer context that carries YMM[255:128]; a plain SSE
|
||||
// context has no such state to clear.
|
||||
void ClearUpperYmm(uint8_t index) const {
|
||||
if (index >= 16 || native->uc_mcsize < sizeof(_STRUCT_MCONTEXT_AVX64)) {
|
||||
return;
|
||||
}
|
||||
auto* avx = reinterpret_cast<_STRUCT_MCONTEXT_AVX64*>(native->uc_mcontext);
|
||||
auto* ymmh = &avx->__fs.__fpu_ymmh0;
|
||||
ymmh[index] = {};
|
||||
}
|
||||
#else
|
||||
ucontext_t* native;
|
||||
|
||||
@@ -703,8 +714,6 @@ static bool TryEmulateCpuExtensions(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
|
||||
static bool TryEmulateMonitorxMwaitx(Context& context) {
|
||||
const auto* rip = reinterpret_cast<const uint8_t*>(context.Rip());
|
||||
if (rip[0] != 0x0f || rip[1] != 0x01 || (rip[2] != 0xfa && rip[2] != 0xfb)) {
|
||||
@@ -790,8 +799,6 @@ static bool TryEmulateReciprocalSquareRoot(Context& context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool TryEmulate(void* native_context) {
|
||||
if (native_context == nullptr) {
|
||||
return false;
|
||||
@@ -807,12 +814,12 @@ bool TryEmulate(void* native_context) {
|
||||
#else
|
||||
Context context {static_cast<ucontext_t*>(native_context)};
|
||||
#endif
|
||||
#if !defined(__APPLE__)
|
||||
return TryEmulateReciprocalSquareRoot(context) || TryEmulateMonitorxMwaitx(context) ||
|
||||
TryEmulateSse4a(context) || TryEmulateShaNi(context) || TryEmulateCpuExtensions(context);
|
||||
#else
|
||||
return TryEmulateSse4a(context) || TryEmulateCpuExtensions(context);
|
||||
TryEmulateSse4a(context) ||
|
||||
#if !defined(__APPLE__)
|
||||
TryEmulateShaNi(context) ||
|
||||
#endif
|
||||
TryEmulateCpuExtensions(context);
|
||||
}
|
||||
|
||||
} // namespace Loader::X64InstructionEmulator
|
||||
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user