mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-22 03:46:53 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb7ff7ec03 | |||
| 7d2422abbe | |||
| b929bbe3e0 | |||
| ae1e92621d | |||
| 5a491abbda | |||
| afbb322b32 | |||
| 5601861d67 | |||
| cd06601022 | |||
| c23de62184 | |||
| 9caec9f2c8 | |||
| cdb64bfd4f | |||
| e5dcfdb87b | |||
| 984010227f | |||
| 57c97ebd86 |
@@ -413,6 +413,12 @@ 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(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")
|
||||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
@@ -606,6 +612,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>)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
+215
-26
@@ -5,9 +5,9 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <span>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <span>
|
||||
|
||||
namespace Common::Trophies {
|
||||
namespace {
|
||||
@@ -40,6 +40,25 @@ int ReadId(const Json& value) {
|
||||
return error == std::errc {} && end == text.data() + text.size() && id >= 0 ? id : -1;
|
||||
}
|
||||
|
||||
std::optional<uint64_t> ReadUnsigned(const Json& value) {
|
||||
if (value.is_number_unsigned()) {
|
||||
return value.get<uint64_t>();
|
||||
}
|
||||
if (value.is_number_integer()) {
|
||||
const auto number = value.get<int64_t>();
|
||||
return number >= 0 ? std::optional<uint64_t>(static_cast<uint64_t>(number)) : std::nullopt;
|
||||
}
|
||||
if (!value.is_string()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& text = value.get_ref<const std::string&>();
|
||||
uint64_t number = 0;
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number);
|
||||
return error == std::errc {} && end == text.data() + text.size()
|
||||
? std::optional<uint64_t>(number)
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
Json ReadJson(const Files& files, std::string_view name) {
|
||||
const auto entry = files.find(name);
|
||||
if (entry == files.end()) {
|
||||
@@ -48,19 +67,18 @@ Json ReadJson(const Files& files, std::string_view name) {
|
||||
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
|
||||
}
|
||||
|
||||
Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
bool ReadFiles(std::span<const std::byte> data, Files& files, uint64_t& size) {
|
||||
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
|
||||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
const auto size = ReadBigEndian(data.data() + 8, 8);
|
||||
size = ReadBigEndian(data.data() + 8, 8);
|
||||
const auto count = ReadBigEndian(data.data() + 0x10, 4);
|
||||
const auto toc = ReadBigEndian(data.data() + 0x14, 4);
|
||||
if (size < 0x40 || size > data.size() || count > 4096 || toc > size ||
|
||||
0x20 + count * 0x40 > size - toc) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
Files files;
|
||||
for (uint64_t i = 0; i < count; ++i) {
|
||||
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
|
||||
const auto* name = reinterpret_cast<const char*>(entry);
|
||||
@@ -68,10 +86,19 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
const auto offset = ReadBigEndian(entry + 0x20, 8);
|
||||
const auto length = ReadBigEndian(entry + 0x28, 8);
|
||||
if (offset > size || length > size - offset) {
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
files.emplace(filename, data.subspan(offset, length));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
Files files;
|
||||
uint64_t size = 0;
|
||||
if (!ReadFiles(data, files, size)) {
|
||||
return {};
|
||||
}
|
||||
const auto conf = ReadJson(files, "tropconf.json");
|
||||
if (!conf.is_object() || !conf.contains("trophies") || !conf["trophies"].is_array() ||
|
||||
conf["trophies"].size() > 1000 || !conf.contains("defaultLanguage") ||
|
||||
@@ -129,19 +156,29 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
|
||||
if (definition.contains("unlockCondition")) {
|
||||
const auto& condition = definition["unlockCondition"];
|
||||
if (condition.is_object() && condition.contains("progressive") &&
|
||||
condition["progressive"] == true) {
|
||||
if (!condition.contains("targetValue") || !condition["targetValue"].is_string()) {
|
||||
return {};
|
||||
if (condition.is_object()) {
|
||||
trophy.progressive =
|
||||
condition.contains("progressive") && condition["progressive"] == true;
|
||||
if (condition.contains("udsStatId")) {
|
||||
trophy.uds_stat_id = ReadUnsigned(condition["udsStatId"]);
|
||||
if (!trophy.uds_stat_id) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const auto& value = condition["targetValue"].get_ref<const std::string&>();
|
||||
uint64_t target = 0;
|
||||
const auto [end, error] =
|
||||
std::from_chars(value.data(), value.data() + value.size(), target);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return {};
|
||||
if (condition.contains("targetValue")) {
|
||||
trophy.target = ReadUnsigned(condition["targetValue"]);
|
||||
}
|
||||
const auto comparison = condition.find("comparator");
|
||||
if (comparison != condition.end()) {
|
||||
if (*comparison == "ge")
|
||||
trophy.comparison = Comparison::GreaterEqual;
|
||||
else if (*comparison == "gt")
|
||||
trophy.comparison = Comparison::Greater;
|
||||
else if (*comparison == "le")
|
||||
trophy.comparison = Comparison::LessEqual;
|
||||
else if (*comparison == "lt")
|
||||
trophy.comparison = Comparison::Less;
|
||||
}
|
||||
trophy.target = target;
|
||||
}
|
||||
}
|
||||
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
|
||||
@@ -210,11 +247,11 @@ std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string
|
||||
})) {
|
||||
return {};
|
||||
}
|
||||
return root / "_SaveData" / title_id /
|
||||
return root / "_Trophies" / title_id /
|
||||
fmt::format("trophies_{}_{}.json", user_id, service_label);
|
||||
}
|
||||
|
||||
std::set<int> LoadUnlocks(const std::filesystem::path& path) {
|
||||
UnlockData LoadUnlockData(const std::filesystem::path& path) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
|
||||
return {};
|
||||
@@ -225,22 +262,40 @@ std::set<int> LoadUnlocks(const std::filesystem::path& path) {
|
||||
!json["unlockedTrophies"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
std::set<int> unlocked;
|
||||
UnlockData unlocks;
|
||||
for (const auto& value: json["unlockedTrophies"]) {
|
||||
const auto id = ReadId(value);
|
||||
if (id >= 0) {
|
||||
unlocked.insert(id);
|
||||
unlocks.unlocked.insert(id);
|
||||
}
|
||||
}
|
||||
return unlocked;
|
||||
if (json.contains("unlockedAt") && json["unlockedAt"].is_object()) {
|
||||
for (auto it = json["unlockedAt"].begin(); it != json["unlockedAt"].end(); ++it) {
|
||||
const auto id = ReadId(Json(it.key()));
|
||||
if (id >= 0 && unlocks.unlocked.contains(id)) {
|
||||
const auto tick = ReadUnsigned(it.value());
|
||||
if (tick && *tick >= UnixEpochTick && *tick <= 315537897599999999ULL) {
|
||||
unlocks.timestamps.emplace(id, *tick);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return unlocks;
|
||||
}
|
||||
|
||||
bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked) {
|
||||
bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
|
||||
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
|
||||
return false;
|
||||
}
|
||||
const auto text = Json {{"unlockedTrophies", unlocked}}.dump();
|
||||
auto temporary = path;
|
||||
Json timestamps = Json::object();
|
||||
for (const auto& [id, tick]: unlocks.timestamps) {
|
||||
if (unlocks.unlocked.contains(id)) {
|
||||
timestamps[std::to_string(id)] = tick;
|
||||
}
|
||||
}
|
||||
const auto text =
|
||||
Json {{"unlockedTrophies", unlocks.unlocked}, {"unlockedAt", timestamps}}.dump();
|
||||
auto temporary = path;
|
||||
temporary += ".tmp";
|
||||
File file;
|
||||
if (text.size() > MaxUnlocksSize || !file.Create(temporary)) {
|
||||
@@ -261,4 +316,138 @@ bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocke
|
||||
return false;
|
||||
}
|
||||
|
||||
UdsRules LoadUdsRules(const std::filesystem::path& path) {
|
||||
File file(path, File::Mode::Read);
|
||||
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
|
||||
return {};
|
||||
}
|
||||
const auto bytes = file.ReadWholeBuffer();
|
||||
Files files;
|
||||
uint64_t size = 0;
|
||||
if (!ReadFiles(bytes, files, size)) {
|
||||
return {};
|
||||
}
|
||||
const auto definitions = ReadJson(files, "stats_definition.json");
|
||||
const auto extraction = ReadJson(files, "stats_extraction.json");
|
||||
if (!definitions.is_object() || !definitions.contains("statDefinitionArray") ||
|
||||
!definitions["statDefinitionArray"].is_array() || !extraction.is_object() ||
|
||||
!extraction.contains("statsExtractionRuleArray") ||
|
||||
!extraction["statsExtractionRuleArray"].is_array()) {
|
||||
return {};
|
||||
}
|
||||
// Only direct unsigned latest-value stats are evaluated here. Aggregated stats
|
||||
// and filtered/nested extraction require UDS state and event types we do not implement.
|
||||
std::map<uint64_t, bool> stats;
|
||||
for (const auto& definition: definitions["statDefinitionArray"]) {
|
||||
if (!definition.is_object() || !definition.contains("statId") ||
|
||||
!definition.contains("aggregation") || definition["aggregation"] != "latest" ||
|
||||
!definition.contains("dataType")) {
|
||||
continue;
|
||||
}
|
||||
const auto id = ReadUnsigned(definition["statId"]);
|
||||
const auto& type = definition["dataType"];
|
||||
if (!id || (type != "uint64" && type != "uint32")) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t maximum = type == "uint32" ? UINT32_MAX : UINT64_MAX;
|
||||
if ((definition.contains("minValue") && ReadUnsigned(definition["minValue"]) != 0) ||
|
||||
(definition.contains("maxValue") && ReadUnsigned(definition["maxValue"]) != maximum)) {
|
||||
continue;
|
||||
}
|
||||
stats.emplace(*id, type == "uint32");
|
||||
}
|
||||
UdsRules events;
|
||||
for (const auto& rule: extraction["statsExtractionRuleArray"]) {
|
||||
if (!rule.is_object() || !rule.contains("condition") || !rule.contains("action")) {
|
||||
continue;
|
||||
}
|
||||
const auto& condition = rule["condition"];
|
||||
const auto& action = rule["action"];
|
||||
if (!condition.is_object() || condition.size() != 1 || !condition.contains("eventName") ||
|
||||
!condition["eventName"].is_string() || !action.is_object() ||
|
||||
!action.contains("input") || !action["input"].is_string() ||
|
||||
!action.contains("output") || !action["output"].is_object() ||
|
||||
!action["output"].contains("statId")) {
|
||||
continue;
|
||||
}
|
||||
const auto id = ReadUnsigned(action["output"]["statId"]);
|
||||
const auto& input = action["input"].get_ref<const std::string&>();
|
||||
if (!id || !stats.contains(*id) || !input.starts_with("$.") || input.size() == 2 ||
|
||||
input.find_first_not_of(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", 2) !=
|
||||
std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
events[condition["eventName"].get<std::string>()].push_back(
|
||||
{*id, input.substr(2), stats.at(*id)});
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
std::filesystem::path UdsPackagePath(uint32_t service_label) {
|
||||
return std::filesystem::path("sce_sys/uds") / fmt::format("uds{:02}.ucp", service_label);
|
||||
}
|
||||
|
||||
std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
|
||||
const std::map<std::string, UdsInteger>& properties) {
|
||||
const auto rules = package.event_rules.find(std::string(event_name));
|
||||
if (rules == package.event_rules.end()) {
|
||||
return {};
|
||||
}
|
||||
std::vector<int> ids;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
if (trophy.grade == 1 || !trophy.uds_stat_id || !trophy.target) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& rule: rules->second) {
|
||||
const auto value = properties.find(rule.input);
|
||||
if (rule.stat_id != *trophy.uds_stat_id || value == properties.end()) {
|
||||
continue;
|
||||
}
|
||||
std::optional<uint64_t> number;
|
||||
if (rule.is_uint32) {
|
||||
if (const auto* input = std::get_if<uint32_t>(&value->second)) number = *input;
|
||||
} else if (const auto* input = std::get_if<uint64_t>(&value->second)) {
|
||||
number = *input;
|
||||
}
|
||||
if (!number) continue;
|
||||
bool matched = false;
|
||||
switch (trophy.comparison) {
|
||||
case Comparison::GreaterEqual: matched = *number >= *trophy.target; break;
|
||||
case Comparison::Greater: matched = *number > *trophy.target; break;
|
||||
case Comparison::LessEqual: matched = *number <= *trophy.target; break;
|
||||
case Comparison::Less: matched = *number < *trophy.target; break;
|
||||
case Comparison::None: break;
|
||||
}
|
||||
if (matched) {
|
||||
ids.push_back(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
uint32_t Progress::Percentage() const {
|
||||
const auto points = total_grade[2] * 6 + total_grade[3] * 2 + total_grade[4];
|
||||
const auto earned_points = earned_grade[2] * 6 + earned_grade[3] * 2 + earned_grade[4];
|
||||
return points == 0 ? 0 : earned_points * 100 / points;
|
||||
}
|
||||
|
||||
Progress GetProgress(const Package& package, const UnlockData& unlocks, std::optional<int> group) {
|
||||
Progress progress;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
if (group && trophy.group_id != *group) {
|
||||
continue;
|
||||
}
|
||||
++progress.total;
|
||||
++progress.total_grade[trophy.grade];
|
||||
if (unlocks.unlocked.contains(id)) {
|
||||
++progress.earned;
|
||||
++progress.earned_grade[trophy.grade];
|
||||
}
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
+42
-3
@@ -1,6 +1,7 @@
|
||||
#ifndef KYTY_COMMON_TROPHIES_H_
|
||||
#define KYTY_COMMON_TROPHIES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -9,11 +10,15 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace Common::Trophies {
|
||||
|
||||
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
|
||||
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
|
||||
inline constexpr uint64_t UnixEpochTick = 62135596800000000ULL;
|
||||
|
||||
enum class Comparison { None, GreaterEqual, Greater, LessEqual, Less };
|
||||
|
||||
struct Trophy {
|
||||
int id = 0;
|
||||
@@ -21,6 +26,9 @@ struct Trophy {
|
||||
int platinum_id = -1;
|
||||
int grade = 0;
|
||||
std::optional<uint64_t> target;
|
||||
std::optional<uint64_t> uds_stat_id;
|
||||
Comparison comparison = Comparison::None;
|
||||
bool progressive = false;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string reward;
|
||||
@@ -29,19 +37,50 @@ struct Trophy {
|
||||
bool has_reward = false;
|
||||
};
|
||||
|
||||
struct UdsRule {
|
||||
uint64_t stat_id;
|
||||
std::string input;
|
||||
bool is_uint32;
|
||||
};
|
||||
|
||||
using UdsRules = std::map<std::string, std::vector<UdsRule>>;
|
||||
using UdsInteger = std::variant<int32_t, uint32_t, uint64_t>;
|
||||
|
||||
struct Package {
|
||||
std::string title;
|
||||
std::map<int, std::string> groups;
|
||||
std::map<int, Trophy> trophies;
|
||||
UdsRules event_rules;
|
||||
};
|
||||
|
||||
struct UnlockData {
|
||||
std::set<int> unlocked;
|
||||
std::map<int, uint64_t> timestamps;
|
||||
};
|
||||
|
||||
struct Progress {
|
||||
std::array<uint32_t, 5> total_grade {};
|
||||
std::array<uint32_t, 5> earned_grade {};
|
||||
uint32_t total = 0;
|
||||
uint32_t earned = 0;
|
||||
|
||||
[[nodiscard]] uint32_t Percentage() const;
|
||||
};
|
||||
|
||||
[[nodiscard]] Progress GetProgress(const Package& package, const UnlockData& unlocks,
|
||||
std::optional<int> group = std::nullopt);
|
||||
|
||||
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
|
||||
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
|
||||
[[nodiscard]] std::filesystem::path UdsPackagePath(uint32_t service_label);
|
||||
[[nodiscard]] UdsRules LoadUdsRules(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
|
||||
const std::map<std::string, UdsInteger>& properties);
|
||||
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
|
||||
std::string_view title_id, int user_id,
|
||||
uint32_t service_label);
|
||||
[[nodiscard]] std::set<int> LoadUnlocks(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked);
|
||||
[[nodiscard]] UnlockData LoadUnlockData(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
|
||||
@@ -876,64 +876,15 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
|
||||
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
|
||||
|
||||
const auto* args_addr =
|
||||
reinterpret_cast<const void*>(m_draw_indirect_args_base_addr + data_offset);
|
||||
|
||||
if (!indexed) {
|
||||
DrawIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
m_num_instances = args.instance_count;
|
||||
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
|
||||
.instance_count = args.instance_count,
|
||||
.first_vertex = args.start_vertex_location,
|
||||
.first_instance = args.start_instance_location,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
return;
|
||||
}
|
||||
|
||||
DrawIndexedIndirectArgs args {};
|
||||
std::memcpy(&args, args_addr, sizeof(args));
|
||||
|
||||
uint64_t index_size = 0;
|
||||
switch (m_index_type_and_size) {
|
||||
case 0: index_size = 2; break;
|
||||
case 1: index_size = 4; break;
|
||||
case 2: index_size = 1; break;
|
||||
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
|
||||
}
|
||||
|
||||
auto* index_addr = reinterpret_cast<const void*>(
|
||||
m_index_base_addr + static_cast<uint64_t>(args.start_index_location) * index_size);
|
||||
|
||||
const uint32_t index_count =
|
||||
(m_index_buffer_size != 0 ? std::min(args.index_count_per_instance, m_index_buffer_size)
|
||||
: args.index_count_per_instance);
|
||||
if (GraphicsRunDebugDumpEnabled() && index_count != args.index_count_per_instance) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
|
||||
LOGF("\t DrawIndexIndirect: clamped index_count from %" PRIu32 " to %" PRIu32
|
||||
" using INDEX_BUFFER_SIZE\n",
|
||||
args.index_count_per_instance, index_count);
|
||||
}
|
||||
}
|
||||
|
||||
m_num_instances = args.instance_count;
|
||||
DrawIndex({.index_count = index_count,
|
||||
.index_addr = index_addr,
|
||||
.instance_count = args.instance_count,
|
||||
.base_vertex = static_cast<int32_t>(args.base_vertex_location),
|
||||
.first_instance = args.start_instance_location,
|
||||
.offset_source = DrawOffsetSource::IndirectArgs});
|
||||
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
|
||||
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
|
||||
}
|
||||
|
||||
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
|
||||
const volatile uint32_t* count_addr,
|
||||
uint32_t stride_in_bytes, uint32_t draw_initiator,
|
||||
bool indexed) {
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
|
||||
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
|
||||
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
|
||||
|
||||
uint32_t draw_count = max_count_or_count;
|
||||
|
||||
@@ -315,8 +315,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
|
||||
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
|
||||
|
||||
r.depth_bounds_test_enable = dc.depth_bounds_enable;
|
||||
r.depth_min_bounds = hw.GetDepthBoundsMin();
|
||||
r.depth_max_bounds = hw.GetDepthBoundsMax();
|
||||
|
||||
r.stencil_clear_enable =
|
||||
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
|
||||
|
||||
@@ -29,8 +29,6 @@ struct RenderDepthInfo {
|
||||
bool depth_write_enable = false;
|
||||
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
|
||||
bool depth_bounds_test_enable = false;
|
||||
float depth_min_bounds = 0.0f;
|
||||
float depth_max_bounds = 0.0f;
|
||||
bool stencil_clear_enable = false;
|
||||
uint8_t stencil_clear_value = 0;
|
||||
bool stencil_test_enable = false;
|
||||
|
||||
@@ -390,7 +390,28 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
||||
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
|
||||
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
|
||||
};
|
||||
const auto float16 = [](uint32_t value) {
|
||||
const auto sign = (value & 0x8000u) << 16u;
|
||||
const auto exponent = (value >> 10u) & 0x1fu;
|
||||
const auto mantissa = value & 0x3ffu;
|
||||
if (exponent == 0) {
|
||||
return std::copysign(static_cast<float>(mantissa) * 0x1p-24f,
|
||||
std::bit_cast<float>(sign));
|
||||
}
|
||||
return std::bit_cast<float>(sign | ((exponent == 31 ? 255u : exponent + 112u) << 23u) |
|
||||
(mantissa << 13u));
|
||||
};
|
||||
switch (format) {
|
||||
case vk::Format::eR16G16Sfloat:
|
||||
next.float32[1] = float16(packed >> 16u);
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
|
||||
case vk::Format::eR16G16Unorm:
|
||||
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
|
||||
[[fallthrough]];
|
||||
case vk::Format::eR16Unorm:
|
||||
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
|
||||
break;
|
||||
// A single-plane float target carries its clear as raw float bits, the same encoding the
|
||||
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
|
||||
// contents.
|
||||
@@ -443,6 +464,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
|
||||
// Keep this separate: the first register word alone cannot describe a 64-bit color.
|
||||
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
|
||||
vk::ClearColorValue& clear) {
|
||||
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
|
||||
(packed & 0xffffu) != (packed >> 16u)) {
|
||||
return false;
|
||||
}
|
||||
if (format == vk::Format::eR16G16B16A16Sfloat) {
|
||||
if (packed != 0) {
|
||||
return false;
|
||||
|
||||
@@ -807,30 +807,52 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
|
||||
}
|
||||
}
|
||||
|
||||
void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
|
||||
void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
|
||||
KYTY_PROFILER_FUNCTION();
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
auto& cache = m_context.GetBufferCache();
|
||||
|
||||
prepared.buffer_sources.clear();
|
||||
const auto& layout = program.bindings;
|
||||
if (layout.memory_offset_count == 0) {
|
||||
return;
|
||||
}
|
||||
const auto& resources = layout.descriptors.front().resources;
|
||||
prepared.buffer_sources.reserve(resources.size());
|
||||
for (const auto resource: resources) {
|
||||
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
const auto requested_size = descriptor.GetSize();
|
||||
if (address == 0 || requested_size == 0) {
|
||||
prepared.buffer_sources.push_back({});
|
||||
auto& cache = m_context.GetBufferCache();
|
||||
for (auto* stage: stages) {
|
||||
auto& prepared = *stage;
|
||||
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
|
||||
const auto& program = *prepared.runtime->program;
|
||||
const auto& snapshot = *prepared.runtime->resources;
|
||||
prepared.buffer_sources.clear();
|
||||
const auto& layout = program.bindings;
|
||||
if (layout.memory_offset_count == 0) {
|
||||
continue;
|
||||
}
|
||||
const auto size = Libs::LibKernel::Memory::ClampRangeSize(address, requested_size);
|
||||
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
|
||||
const auto& resources = layout.descriptors.front().resources;
|
||||
prepared.buffer_sources.reserve(resources.size());
|
||||
for (const auto resource: resources) {
|
||||
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
|
||||
const auto address = descriptor.Base48();
|
||||
auto size = descriptor.GetSize();
|
||||
if (address == 0 || size == 0) {
|
||||
prepared.buffer_sources.push_back({});
|
||||
continue;
|
||||
}
|
||||
if (descriptor.NumRecords() == UINT32_MAX) {
|
||||
if (program.info.buffers[resource].written) {
|
||||
// Sentinel write ranges end before tables captured by any bound stage.
|
||||
for (const auto* reader: stages) {
|
||||
for (const auto [read_address, read_size]: reader->runtime->resources->specialization_reads) {
|
||||
if (read_size == 0) continue;
|
||||
if (read_address > address) {
|
||||
size = std::min(size, read_address - address);
|
||||
} else if (address - read_address < read_size) {
|
||||
EXIT("scalar resource reads overlap a shader buffer write\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto& graphics = m_context.GetGraphics();
|
||||
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
|
||||
(graphics.StorageMinAlignment() - 1);
|
||||
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
|
||||
}
|
||||
}
|
||||
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
|
||||
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,8 +941,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
|
||||
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
|
||||
std::span<RenderColorInfo> colors) {
|
||||
bool uses_dma = false;
|
||||
FindBuffers(stages);
|
||||
for (auto* stage: stages) {
|
||||
FindBuffers(*stage);
|
||||
uses_dma |= stage->runtime->program->info.uses_dma;
|
||||
}
|
||||
if (uses_dma) {
|
||||
|
||||
@@ -831,9 +831,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
|
||||
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
|
||||
}
|
||||
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
|
||||
static_params.depth_min_bounds = depth.depth_min_bounds;
|
||||
static_params.depth_max_bounds = depth.depth_max_bounds;
|
||||
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
|
||||
static_params.cull_back = !rect_list && mc.cull_back;
|
||||
static_params.cull_front = !rect_list && mc.cull_front;
|
||||
|
||||
@@ -38,9 +38,6 @@ struct PipelineStaticParameters {
|
||||
bool primitive_restart_enable = false;
|
||||
uint32_t samples = 1;
|
||||
bool sample_shading_enable = false;
|
||||
bool depth_bounds_test_enable = false;
|
||||
float depth_min_bounds = 0.0f;
|
||||
float depth_max_bounds = 0.0f;
|
||||
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
|
||||
bool cull_front = false;
|
||||
bool cull_back = false;
|
||||
@@ -64,7 +61,7 @@ struct PipelineStaticParameters {
|
||||
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
|
||||
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
|
||||
static_assert(alignof(PipelineStaticParameters) == 1);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 125);
|
||||
static_assert(sizeof(PipelineStaticParameters) == 116);
|
||||
|
||||
struct PipelineRenderingState {
|
||||
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
|
||||
|
||||
@@ -471,14 +471,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
|
||||
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
|
||||
depth_stencil_info.depthBoundsTestEnable =
|
||||
#if defined(__APPLE__)
|
||||
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
|
||||
#else
|
||||
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
#endif
|
||||
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
|
||||
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
|
||||
|
||||
std::vector<vk::DynamicState> dynamic_states {
|
||||
vk::DynamicState::eViewportWithCount,
|
||||
@@ -497,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::DynamicState::eBlendConstants,
|
||||
};
|
||||
#if !defined(__APPLE__)
|
||||
dynamic_states.push_back(vk::DynamicState::eDepthBoundsTestEnable);
|
||||
dynamic_states.push_back(vk::DynamicState::eDepthBounds);
|
||||
if (rendering.color_count != 0) {
|
||||
dynamic_states.push_back(vk::DynamicState::eColorWriteEnableEXT);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ public:
|
||||
uint32_t mode);
|
||||
|
||||
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
|
||||
void FindBuffers(PreparedBindings& bindings);
|
||||
void FindBuffers(std::span<PreparedBindings* const> stages);
|
||||
void RebindBuffers(PreparedBindings& bindings);
|
||||
void RebindImages(PreparedBindings& bindings);
|
||||
void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point,
|
||||
|
||||
@@ -342,7 +342,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
std::copy(std::begin(input_info.dispatch_threads_num), std::end(input_info.dispatch_threads_num),
|
||||
bindings.shader_data.begin() + program.bindings.dispatch_thread_dword);
|
||||
}
|
||||
FindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
FindBuffers(std::span {&descriptor_stage, 1u});
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
}
|
||||
@@ -351,7 +352,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
RebindBuffers(bindings);
|
||||
|
||||
auto vk_buffer = buffer.Handle();
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
|
||||
std::span {&descriptor_stage, 1u});
|
||||
bool has_storage_writes = bindings.shared_memory.buffer != nullptr ||
|
||||
@@ -457,7 +457,8 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
|
||||
auto& bindings = m_compute_bindings;
|
||||
PrepareBindings(input_info.stage, bindings);
|
||||
FindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
FindBuffers(std::span {&descriptor_stage, 1u});
|
||||
const auto& program = *input_info.stage.program;
|
||||
if (program.info.uses_dma) {
|
||||
m_context.PrepareBda();
|
||||
@@ -469,7 +470,6 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
|
||||
args_addr, sizeof(vk::DispatchIndirectCommand), false);
|
||||
EXIT_IF(args_buffer == nullptr || (args_offset & 3u) != 0);
|
||||
RebindBuffers(bindings);
|
||||
PreparedBindings* descriptor_stage = &bindings;
|
||||
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
|
||||
std::span {&descriptor_stage, 1u});
|
||||
const auto vk_buffer = buffer.Handle();
|
||||
|
||||
@@ -379,6 +379,12 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
|
||||
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
|
||||
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
|
||||
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
|
||||
#if !defined(__APPLE__)
|
||||
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
|
||||
if (depth.depth_bounds_test_enable) {
|
||||
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto& mode = ctx.GetModeControl();
|
||||
const auto& poly_offset = ctx.GetPolyOffset();
|
||||
|
||||
@@ -1100,56 +1100,68 @@ struct SystemOverlay::Impl {
|
||||
static_cast<float>(frame_extent.height) / 1080.0f),
|
||||
0.65f, 1.25f);
|
||||
const float width = std::max(
|
||||
0.0f, std::min(440.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
|
||||
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
|
||||
const float height = 104.0f * screen_scale;
|
||||
const float rounding = 14.0f * screen_scale;
|
||||
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
|
||||
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
|
||||
const float alpha = std::min(fade_in, fade_out);
|
||||
const float slide = (1.0f - fade_in) * (width + 24.0f);
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 24.0f - width + slide,
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
|
||||
32.0f};
|
||||
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
draw->AddRectFilled({top_left.x + 5.0f, top_left.y + 6.0f},
|
||||
{bottom_right.x + 5.0f, bottom_right.y + 6.0f},
|
||||
IM_COL32(0, 0, 0, static_cast<int>(95.0f * alpha)), 10.0f);
|
||||
draw->AddRectFilled(top_left, bottom_right,
|
||||
IM_COL32(18, 21, 29, static_cast<int>(242.0f * alpha)), 10.0f);
|
||||
draw->AddRect(top_left, bottom_right,
|
||||
IM_COL32(190, 154, 78, static_cast<int>(200.0f * alpha)), 10.0f,
|
||||
ImDrawFlags_RoundCornersAll, 1.5f);
|
||||
const auto fade = [alpha](int r, int g, int b, int a) {
|
||||
return IM_COL32(r, g, b, static_cast<int>(static_cast<float>(a) * alpha));
|
||||
};
|
||||
draw->AddRectFilled({top_left.x + 3.0f, top_left.y + 5.0f},
|
||||
{bottom_right.x + 3.0f, bottom_right.y + 5.0f}, fade(0, 0, 0, 90),
|
||||
rounding);
|
||||
draw->AddRectFilled(top_left, bottom_right, fade(24, 26, 32, 235), rounding);
|
||||
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
|
||||
ImDrawFlags_RoundCornersAll, 1.0f);
|
||||
|
||||
const float pad = 10.0f * screen_scale;
|
||||
const float icon_size = height - 2.0f * pad;
|
||||
float text_left = top_left.x + 20.0f * screen_scale;
|
||||
if (trophy_notification_image != nullptr) {
|
||||
constexpr float ICON_SIZE = 88.0f;
|
||||
const ImVec2 icon_min {top_left.x + 8.0f * screen_scale,
|
||||
top_left.y + (height - ICON_SIZE * screen_scale) * 0.5f};
|
||||
const ImVec2 icon_max {icon_min.x + ICON_SIZE * screen_scale,
|
||||
icon_min.y + ICON_SIZE * screen_scale};
|
||||
draw->AddImage(trophy_notification_image->GetTexRef(), icon_min, icon_max, {0, 0},
|
||||
{1, 1}, IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)));
|
||||
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
|
||||
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
|
||||
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
|
||||
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
|
||||
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
|
||||
ImDrawFlags_RoundCornersAll, 1.0f);
|
||||
text_left = icon_max.x + 18.0f * screen_scale;
|
||||
}
|
||||
|
||||
const ImVec2 text_position {top_left.x +
|
||||
(trophy_notification_image ? 112.0f : 18.0f) * screen_scale,
|
||||
top_left.y + 18.0f * screen_scale};
|
||||
draw->PushClipRect(top_left, {bottom_right.x - 12.0f * screen_scale, bottom_right.y}, true);
|
||||
draw->AddText(nullptr, 13.0f * screen_scale, text_position,
|
||||
IM_COL32(232, 203, 139, static_cast<int>(255.0f * alpha)), "Trophy earned");
|
||||
draw->AddText(nullptr, 21.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 22.0f * screen_scale},
|
||||
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)),
|
||||
trophy_notification->name.c_str());
|
||||
constexpr std::array GRADE_LABELS {"Platinum", "Gold", "Silver", "Bronze"};
|
||||
constexpr std::array GRADE_COLORS {
|
||||
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
|
||||
IM_COL32(211, 142, 91, 255)};
|
||||
const int grade = trophy_notification->grade;
|
||||
if (grade > 0 && grade <= static_cast<int>(GRADE_LABELS.size())) {
|
||||
draw->AddText(nullptr, 13.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 50.0f * screen_scale},
|
||||
ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha),
|
||||
GRADE_LABELS[grade - 1]);
|
||||
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
|
||||
IM_COL32(211, 220, 232, 255),
|
||||
IM_COL32(211, 142, 91, 255)};
|
||||
const int grade = trophy_notification->grade;
|
||||
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
|
||||
float title_x = text_left;
|
||||
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
|
||||
// Small cup glyph tinted with the trophy grade.
|
||||
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
|
||||
const float u = screen_scale;
|
||||
const float cx = text_left + 9.0f * u;
|
||||
const float cy = title_y + 15.0f * u;
|
||||
draw->AddRectFilled({cx - 8.0f * u, cy - 11.0f * u}, {cx + 8.0f * u, cy + 2.0f * u},
|
||||
cup, 2.0f * u, ImDrawFlags_RoundCornersBottom);
|
||||
draw->AddRect({cx - 11.0f * u, cy - 9.0f * u}, {cx - 6.0f * u, cy - 3.0f * u}, cup,
|
||||
2.0f * u, ImDrawFlags_RoundCornersLeft, 1.8f * u);
|
||||
draw->AddRect({cx + 6.0f * u, cy - 9.0f * u}, {cx + 11.0f * u, cy - 3.0f * u}, cup,
|
||||
2.0f * u, ImDrawFlags_RoundCornersRight, 1.8f * u);
|
||||
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
|
||||
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
|
||||
1.5f * u);
|
||||
title_x += 30.0f * u;
|
||||
}
|
||||
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
|
||||
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
|
||||
fade(255, 255, 255, 255), trophy_notification->name.c_str());
|
||||
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
|
||||
fade(176, 180, 192, 255), "Trophy earned!");
|
||||
draw->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,8 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
if (memory_index >= program.memory_info.size()) {
|
||||
Fail(program, "address operation has invalid memory metadata");
|
||||
}
|
||||
const auto kind = program.memory_info[memory_index].kind;
|
||||
const auto& memory = program.memory_info[memory_index];
|
||||
const auto kind = memory.kind;
|
||||
if (kind == IR::ResourceKind::Scratch || kind == IR::ResourceKind::FlatLocal) {
|
||||
if (program.scratch_dwords == 0) {
|
||||
Fail(program, "scratch operation has no per-thread storage");
|
||||
@@ -217,6 +218,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
|
||||
}
|
||||
} else if (address_access == IR::AddressAccess::Write) {
|
||||
Fail(program, "writable FLAT/GLOBAL addresses require GPU ownership tracking");
|
||||
} else {
|
||||
// Descriptor stores can alias coherent physical-address loads.
|
||||
requirements.coherent_buffers |= memory.coherent;
|
||||
}
|
||||
}
|
||||
if (IR::BufferAccessOf(inst.GetOpcode()) != IR::BufferAccess::None) {
|
||||
|
||||
@@ -849,74 +849,76 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
|
||||
return;
|
||||
}
|
||||
const auto key = ctx.Def(handle->Arg(0));
|
||||
if (state.flattened_srt_variable == 0 || image.indirect_search_iterations == 0u ||
|
||||
if (state.flattened_srt_variable == 0 ||
|
||||
image.indirect_resources.size() < 2u) {
|
||||
ctx.Fail(inst, "has no indirect image runtime mapping");
|
||||
return;
|
||||
}
|
||||
const auto selected = EmitIndirectResourceIndex(
|
||||
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
|
||||
const auto kind = *IR::DescriptorBindingForImage(image);
|
||||
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
|
||||
uint32_t first_child = 0;
|
||||
bool homogeneous = true;
|
||||
struct SampleRun {
|
||||
uint32_t first;
|
||||
uint32_t count;
|
||||
uint32_t resource;
|
||||
uint32_t slot_bias;
|
||||
};
|
||||
std::vector<SampleRun> runs;
|
||||
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
|
||||
const auto resource = image.indirect_resources[ordinal];
|
||||
const auto& candidate = state.program.info.images[resource];
|
||||
if (candidate.dimension != image.dimension || candidate.cube != image.cube ||
|
||||
IR::DescriptorBindingForImage(candidate) != kind ||
|
||||
candidate.mip_count != 1u) {
|
||||
homogeneous = false;
|
||||
break;
|
||||
const auto kind = *IR::DescriptorBindingForImage(candidate);
|
||||
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
|
||||
if (!runs.empty()) {
|
||||
auto& run = runs.back();
|
||||
const auto& first = state.program.info.images[run.resource];
|
||||
const auto next_slot = run.slot_bias + ordinal;
|
||||
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
|
||||
IR::DescriptorBindingForImage(first) == kind &&
|
||||
candidate.mip_count == 1u && first.mip_count == 1u &&
|
||||
(ordinal == 1u || (next_slot < binding.resources.size() &&
|
||||
binding.resources[next_slot] == resource))) {
|
||||
// Native roots precede appended children; only the root may have a slot gap.
|
||||
if (ordinal == 1u)
|
||||
run.slot_bias = ResourceForDescriptor(state, kind, resource) - ordinal;
|
||||
++run.count;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (ordinal == 1u) first_child = ResourceForDescriptor(state, kind, resource);
|
||||
const auto child_slot = first_child + ordinal - 1u;
|
||||
if (ordinal > 1u && (child_slot >= binding.resources.size() ||
|
||||
binding.resources[child_slot] != resource)) {
|
||||
homogeneous = false;
|
||||
break;
|
||||
runs.push_back({ordinal, 1u, resource,
|
||||
ResourceForDescriptor(state, kind, resource) - ordinal});
|
||||
}
|
||||
const auto EmitRun = [&](uint32_t index) {
|
||||
const auto& run = runs[index];
|
||||
if (run.count == 1u) return EmitSample(run.resource);
|
||||
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, run.slot_bias));
|
||||
if (run.first == 0u) {
|
||||
const auto kind = *IR::DescriptorBindingForImage(image);
|
||||
const auto root_slot = ResourceForDescriptor(state, kind, mem.resource);
|
||||
if (root_slot != run.slot_bias) {
|
||||
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
|
||||
ConstantU32(state, 0u));
|
||||
const auto root_or_child = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
|
||||
is_root, ConstantU32(state, root_slot), slot);
|
||||
slot = root_or_child;
|
||||
}
|
||||
}
|
||||
return EmitSample(run.resource, slot);
|
||||
};
|
||||
auto run_index = ConstantU32(state, 0u);
|
||||
for (uint32_t index = 1; index < runs.size(); ++index) {
|
||||
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
|
||||
selected, ConstantU32(state, runs[index].first));
|
||||
const auto next = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
|
||||
ConstantU32(state, index), run_index);
|
||||
run_index = next;
|
||||
}
|
||||
if (homogeneous) {
|
||||
const auto child_index = Binary(state, spv::OpIAdd, TypeU32(state), selected,
|
||||
ConstantU32(state, first_child - 1u));
|
||||
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
|
||||
ConstantU32(state, 0u));
|
||||
const auto array_index = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpSelect, TypeU32(state), array_index, is_root,
|
||||
ConstantU32(state, ResourceForDescriptor(state, kind, mem.resource)),
|
||||
child_index);
|
||||
const auto sample = EmitSample(mem.resource, array_index);
|
||||
const auto result = dref ? sample : UnpackImageTexel(ctx, mem, sample);
|
||||
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
|
||||
return;
|
||||
}
|
||||
const auto default_label = state.builder.AllocateId();
|
||||
const auto merge_label = state.builder.AllocateId();
|
||||
std::vector<uint32_t> labels(image.indirect_resources.size() - 1u);
|
||||
std::vector<uint32_t> switch_words {spv::OpSwitch, selected, default_label};
|
||||
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
|
||||
labels[candidate - 1u] = state.builder.AllocateId();
|
||||
switch_words.push_back(candidate);
|
||||
switch_words.push_back(labels[candidate - 1u]);
|
||||
}
|
||||
state.builder.AddFunction(spv::OpSelectionMerge, merge_label,
|
||||
spv::SelectionControlMaskNone);
|
||||
state.builder.AddFunction(switch_words);
|
||||
std::vector<uint32_t> phi_words {spv::OpPhi, result_type, state.builder.AllocateId()};
|
||||
EmitLabel(state, default_label);
|
||||
phi_words.push_back(EmitSample(image.indirect_resources[0]));
|
||||
phi_words.push_back(default_label);
|
||||
state.builder.AddFunction(spv::OpBranch, merge_label);
|
||||
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
|
||||
EmitLabel(state, labels[candidate - 1u]);
|
||||
phi_words.push_back(EmitSample(image.indirect_resources[candidate]));
|
||||
phi_words.push_back(labels[candidate - 1u]);
|
||||
state.builder.AddFunction(spv::OpBranch, merge_label);
|
||||
}
|
||||
EmitLabel(state, merge_label);
|
||||
state.builder.AddFunction(phi_words);
|
||||
auto result = phi_words[2];
|
||||
auto result = runs.size() == 1u
|
||||
? EmitRun(0u)
|
||||
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
|
||||
result_type, EmitRun);
|
||||
if (!dref) {
|
||||
result = UnpackImageTexel(ctx, mem, result);
|
||||
}
|
||||
|
||||
@@ -180,29 +180,46 @@ void RecordBdaFault(EmitterState& state, uint32_t page) {
|
||||
Binary(state, spv::OpBitwiseOr, TypeU32(state), value, bit));
|
||||
}
|
||||
|
||||
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
|
||||
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address, bool coherent = false) {
|
||||
auto& state = ctx.state;
|
||||
const auto bda = GetBdaPointer(state, address);
|
||||
const auto present =
|
||||
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantU64(state, 0));
|
||||
return EmitValueOrZeroIfCondition(state, present, [&]() {
|
||||
const auto pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
|
||||
bda);
|
||||
auto pointer = state.builder.AllocateId();
|
||||
if (coherent) {
|
||||
const auto block = state.builder.DecoratedType(
|
||||
spv::OpTypeStruct, {{spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}},
|
||||
{spv::OpMemberDecorate, {0, spv::DecorationCoherent}},
|
||||
{spv::OpDecorate, {spv::DecorationBlock}}}, TypeU32(state));
|
||||
state.builder.AddFunction(
|
||||
spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, block),
|
||||
pointer, bda);
|
||||
const auto base = pointer;
|
||||
pointer = state.builder.AllocateId();
|
||||
state.builder.AddFunction(spv::OpAccessChain, TypePhysicalU32Pointer(state), pointer,
|
||||
base, ConstantU32(state, 0));
|
||||
} else {
|
||||
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
|
||||
bda);
|
||||
}
|
||||
const auto value = state.builder.AllocateId();
|
||||
constexpr uint32_t alignment = sizeof(uint32_t);
|
||||
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer,
|
||||
spv::MemoryAccessAlignedMask, alignment);
|
||||
spv::MemoryAccessAlignedMask |
|
||||
(coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone),
|
||||
alignment);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
|
||||
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits,
|
||||
bool coherent = false) {
|
||||
auto& state = ctx.state;
|
||||
return EmitValueOrZeroIfCondition(state, active, [&]() {
|
||||
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
|
||||
ConstantU64(state, ~uint64_t {3}));
|
||||
const auto first = LoadBdaDword(ctx, aligned);
|
||||
const auto first = LoadBdaDword(ctx, aligned, coherent);
|
||||
const auto byte =
|
||||
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
|
||||
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
|
||||
@@ -212,7 +229,7 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
|
||||
TypeBool(state), byte, ConstantU32(state, bits == 16u ? 2u : 0u));
|
||||
const auto second = EmitValueOrZeroIfCondition(state, crosses, [&]() {
|
||||
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), aligned,
|
||||
ConstantU64(state, sizeof(uint32_t))));
|
||||
ConstantU64(state, sizeof(uint32_t))), coherent);
|
||||
});
|
||||
const auto shift =
|
||||
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte, ConstantU32(state, 3));
|
||||
@@ -1359,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),
|
||||
|
||||
@@ -343,7 +343,6 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
const uint32_t word0 = code[word_index];
|
||||
const uint32_t word1 = code[word_index + 1u];
|
||||
const uint32_t offset = word0 & 0xfffu;
|
||||
const uint32_t dlc = (word0 >> 12u) & 1u;
|
||||
const uint32_t lds = (word0 >> 13u) & 1u;
|
||||
const uint32_t seg = (word0 >> 14u) & 0x3u;
|
||||
const uint32_t opcode = (word0 >> 18u) & 0x7fu;
|
||||
@@ -355,6 +354,7 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
|
||||
inst.pc = pc;
|
||||
inst.word_count = 2;
|
||||
inst.offset = seg == 0u ? (offset & 0x7ffu) : SignExtendU32(offset, 12u);
|
||||
inst.dlc = ((word0 >> 12u) & 1u) != 0;
|
||||
inst.glc = ((word0 >> 16u) & 1u) != 0;
|
||||
inst.slc = ((word0 >> 17u) & 1u) != 0;
|
||||
inst.family = Family::FLAT;
|
||||
@@ -364,7 +364,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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -485,15 +485,21 @@ struct BlockInfo {
|
||||
|
||||
struct DescriptorSource {
|
||||
struct IndirectDescriptor {
|
||||
uint32_t material_source = UINT32_MAX;
|
||||
struct SelectorRead {
|
||||
uint32_t source = UINT32_MAX;
|
||||
uint32_t stride = 0;
|
||||
uint32_t offset = 0;
|
||||
|
||||
bool operator==(const SelectorRead& other) const = default;
|
||||
};
|
||||
std::optional<SelectorRead> selector;
|
||||
uint32_t table_source = 0;
|
||||
uint32_t selector_stride = 0;
|
||||
uint32_t selector_offset = 0;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_record_bytes = 0;
|
||||
bool table_scalar = false;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
uint32_t selector_shift = 0;
|
||||
uint32_t selector_bits = UINT32_MAX;
|
||||
Value key_count;
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
|
||||
@@ -101,7 +101,7 @@ SharedMemoryResources CollectMemoryResources(const Program& program, std::vector
|
||||
|
||||
bool UsesFlattenedSrt(const Program& program) {
|
||||
const auto uses_mapping = [](const auto& resource) {
|
||||
return resource.indirect_search_iterations != 0u;
|
||||
return resource.indirect_root != UINT32_MAX;
|
||||
};
|
||||
return std::ranges::any_of(program.blocks, [](const Block* block) {
|
||||
return std::ranges::any_of(*block, [](const Inst& inst) {
|
||||
|
||||
@@ -238,6 +238,13 @@ bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsBoundedDescriptorTable(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect) {
|
||||
const auto* table = Source(program, indirect.table_source);
|
||||
return indirect.sources.empty() && !indirect.selector &&
|
||||
indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u;
|
||||
}
|
||||
|
||||
template <typename Specialization, typename Normalize>
|
||||
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
|
||||
const DescriptorSource::IndirectDescriptor& indirect,
|
||||
@@ -249,7 +256,24 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
uint32_t maximum_resources, Normalize&& normalize) {
|
||||
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
|
||||
const auto& sources = indirect.sources;
|
||||
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
|
||||
auto& keys = program.material_keys;
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
|
||||
ordinal = 0u;
|
||||
if (candidate == descriptors[resource_index]) return true;
|
||||
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
if (found == descriptors.end()) {
|
||||
if (descriptors.size() >= maximum_resources) return false;
|
||||
descriptors.push_back(candidate);
|
||||
auto child = root_resource;
|
||||
child.indirect_root = resource_index;
|
||||
specializations.push_back(child);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
|
||||
uint64_t step, uint64_t count) {
|
||||
keys.resize(count);
|
||||
@@ -269,8 +293,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.clear();
|
||||
DescriptorValue material_value;
|
||||
DescriptorValue table_value;
|
||||
if ((indirect.material_source != UINT32_MAX &&
|
||||
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
|
||||
if ((selector != nullptr &&
|
||||
!clean.EvaluateDescriptor(selector->source, material_value)) ||
|
||||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
|
||||
return false;
|
||||
}
|
||||
@@ -283,7 +307,48 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (indirect.material_source == UINT32_MAX) {
|
||||
if (IsBoundedDescriptorTable(program, indirect)) {
|
||||
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
return true;
|
||||
}
|
||||
if (table_size > UINT32_MAX ||
|
||||
(indirect.table_record_bytes != 0u &&
|
||||
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
|
||||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
|
||||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
|
||||
return false;
|
||||
// Descriptor bytes are refreshed together. The GPU selects the DWORD address;
|
||||
// no material selector or unused table field is interpreted on the CPU.
|
||||
keys.resize(table_size / 4u);
|
||||
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false;
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
descriptors[resource_index] = {.dword_count = dword_count};
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
|
||||
for (uint32_t word = 0; word < keys.size(); ++word) {
|
||||
DescriptorValue candidate {.dword_count = dword_count};
|
||||
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
|
||||
candidate.dwords.begin());
|
||||
if (!normalize(candidate)) return false;
|
||||
uint32_t ordinal;
|
||||
if (!intern_candidate(candidate, ordinal)) return false;
|
||||
if (ordinal == 0u) continue;
|
||||
snapshot.flattened_srt.resize(mapping_offset + 2u + word, 0u);
|
||||
snapshot.flattened_srt[mapping_offset + 1u + word] = ordinal;
|
||||
}
|
||||
if (descriptors.size() == children_begin) {
|
||||
snapshot.flattened_srt.resize(mapping_offset);
|
||||
} else {
|
||||
snapshot.flattened_srt[mapping_offset] =
|
||||
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
|
||||
auto& root = specializations[resource_index];
|
||||
root.indirect_root = resource_index;
|
||||
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
|
||||
root.indirect_search_iterations = 0u;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (selector == nullptr) {
|
||||
uint32_t key_count = 0;
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
|
||||
@@ -295,7 +360,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
|
||||
}
|
||||
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} +
|
||||
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
|
||||
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
|
||||
UINT32_MAX + 1ull)) {
|
||||
return false;
|
||||
@@ -306,20 +371,18 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
ShaderBufferResource material;
|
||||
uint32_t first = 0, count = 0;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
|
||||
((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) &&
|
||||
(material.Base48() & 3u) != 0u) ||
|
||||
material.SwizzleEnabled() ||
|
||||
material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
|
||||
uint64_t {selector->offset} + 4u > material.Stride() ||
|
||||
!clean.Evaluate(indirect.selector_first, first) ||
|
||||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
|
||||
uint64_t {first} + count > material.NumRecords())
|
||||
return false;
|
||||
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
|
||||
indirect.selector_offset + 4u >
|
||||
selector->offset + 4u >
|
||||
uint64_t {UINT32_MAX} + 1u)
|
||||
return false;
|
||||
if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset,
|
||||
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
|
||||
material.Stride(), count)) return false;
|
||||
} else if (!indirect.selector_mask.IsEmpty()) {
|
||||
uint32_t mask = 0;
|
||||
@@ -335,8 +398,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.reserve(std::popcount(mask));
|
||||
while (mask != 0u) {
|
||||
const auto index = std::countr_zero(mask);
|
||||
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
|
||||
static_cast<uint64_t>(index) * indirect.selector_stride;
|
||||
const auto offset = static_cast<uint64_t>(selector->offset) +
|
||||
static_cast<uint64_t>(index) * selector->stride;
|
||||
if (offset > UINT32_MAX) return false;
|
||||
uint32_t key = 0;
|
||||
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
|
||||
@@ -344,37 +407,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
keys.push_back(key);
|
||||
mask &= mask - 1u;
|
||||
}
|
||||
} else {
|
||||
ShaderBufferResource material;
|
||||
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) {
|
||||
return false;
|
||||
}
|
||||
// The first aligned offset includes the immediate added after shader U32 arithmetic.
|
||||
const auto step = std::max<uint64_t>(4u,
|
||||
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
|
||||
const uint64_t first = indirect.selector_offset;
|
||||
const auto size = material.GetSize();
|
||||
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
|
||||
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
|
||||
if (probe_count > MaxIndirectDescriptorProbes) {
|
||||
return false;
|
||||
}
|
||||
if (!read_keys(material, first, step, probe_count)) return false;
|
||||
if (indirect.selector_shift == 0u) keys.push_back(0u);
|
||||
}
|
||||
if (indirect.material_source != UINT32_MAX) {
|
||||
for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) {
|
||||
keys.push_back(0u); // An out-of-range material load returns zero.
|
||||
}
|
||||
} else return false;
|
||||
if (selector != nullptr) {
|
||||
std::ranges::sort(keys);
|
||||
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
|
||||
}
|
||||
}
|
||||
|
||||
const auto children_begin = descriptors.size();
|
||||
const auto mapping_offset = snapshot.flattened_srt.size();
|
||||
const auto root_resource = specializations[resource_index];
|
||||
const auto key_count = sources.empty() ? keys.size() : sources.size();
|
||||
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
|
||||
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
|
||||
@@ -383,8 +423,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
DescriptorValue candidate;
|
||||
candidate.dword_count = dword_count;
|
||||
if (sources.empty()) {
|
||||
const auto table_offset =
|
||||
static_cast<uint32_t>(key * indirect.table_stride) + indirect.table_offset;
|
||||
const uint64_t table_offset =
|
||||
uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
|
||||
(indirect.table_immediate & ~3u);
|
||||
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
|
||||
std::span(candidate.dwords).first(dword_count))) {
|
||||
return false;
|
||||
@@ -396,19 +437,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
uint32_t ordinal = 0;
|
||||
if (entry == 0) {
|
||||
descriptors[resource_index] = candidate;
|
||||
} else if (descriptors[resource_index] != candidate) {
|
||||
const auto found =
|
||||
std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
|
||||
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
|
||||
if (found == descriptors.end()) {
|
||||
if (descriptors.size() >= maximum_resources) {
|
||||
return false;
|
||||
}
|
||||
descriptors.push_back(candidate);
|
||||
auto child = root_resource;
|
||||
child.indirect_root = resource_index;
|
||||
specializations.push_back(child);
|
||||
}
|
||||
} else if (!intern_candidate(candidate, ordinal)) {
|
||||
return false;
|
||||
}
|
||||
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
|
||||
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
|
||||
@@ -566,9 +596,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
|
||||
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
|
||||
? snapshot.flattened_srt[root.indirect_mapping_offset]
|
||||
: 0u;
|
||||
if (root.indirect_search_iterations == 0u || key_count < 2u ||
|
||||
if (key_count == 0u ||
|
||||
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
|
||||
static_cast<size_t>(key_count) * 2u >
|
||||
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
|
||||
snapshot.flattened_srt.size()) {
|
||||
return SpecializationFail("indirect image specialization has an invalid key mapping");
|
||||
}
|
||||
@@ -1010,9 +1040,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
|
||||
if (!source.indirect_descriptor.has_value()) continue;
|
||||
const auto& indirect = *source.indirect_descriptor;
|
||||
plan.requires_specialization_memory = true;
|
||||
capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty();
|
||||
MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots,
|
||||
indirect.selector_mask);
|
||||
capture_indirect_reads = true;
|
||||
if (indirect.selector)
|
||||
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
|
||||
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
|
||||
if (indirect.sources.empty()) {
|
||||
@@ -1156,10 +1187,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
|
||||
continue;
|
||||
}
|
||||
const auto& indirect = *source->indirect_descriptor;
|
||||
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
|
||||
if (!MaterializeIndirectDescriptor(
|
||||
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
|
||||
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
|
||||
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128))
|
||||
// A broad heap also contains resources for other typed image operations.
|
||||
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
|
||||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
|
||||
value.dwords.fill(0u);
|
||||
return true;
|
||||
})) {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <algorithm>
|
||||
#include <fmt/format.h>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
@@ -374,11 +373,68 @@ public:
|
||||
}
|
||||
}
|
||||
for (const auto& plan: m_indirect_descriptors) {
|
||||
auto key = plan.key;
|
||||
if (plan.table_read != nullptr) {
|
||||
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
|
||||
auto* block = anchor->Parent();
|
||||
const auto where = std::ranges::find_if(block->Instructions(),
|
||||
[&](const Inst& inst) { return &inst == anchor; });
|
||||
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
|
||||
return Value(&*block->PrependNewInst(where, op, args));
|
||||
};
|
||||
const auto& read = *plan.table_read;
|
||||
const auto& memory = m_program.memory_info[read.Flags<MemoryFlags>().index];
|
||||
Value offset;
|
||||
Value valid(true);
|
||||
if (plan.table_indexed) {
|
||||
const auto* table = read.Arg(0).ResolveInstruction();
|
||||
const auto stride = emit(ValueOpcode::BitFieldUExtract,
|
||||
{table->Arg(1), Value(16u), Value(14u)});
|
||||
const auto format = emit(ValueOpcode::BitFieldUExtract,
|
||||
{table->Arg(3), Value(12u), Value(7u)});
|
||||
offset = emit(ValueOpcode::IAdd32,
|
||||
{emit(ValueOpcode::IMul32, {read.Arg(1), stride}), Value(memory.offset)});
|
||||
valid = emit(ValueOpcode::LogicalAnd,
|
||||
{emit(ValueOpcode::ULessThan32, {read.Arg(1), table->Arg(2)}),
|
||||
emit(ValueOpcode::INotEqual32, {format, Value(0u)})});
|
||||
} else {
|
||||
const auto dynamic = emit(ValueOpcode::BitwiseAnd32, {read.Arg(1), Value(~3u)});
|
||||
offset = emit(ValueOpcode::IAdd32, {dynamic, Value(memory.offset & ~3u)});
|
||||
valid = emit(ValueOpcode::ULessThanEqual32, {dynamic, offset});
|
||||
}
|
||||
key = emit(ValueOpcode::SelectU32, {valid, offset, Value(UINT32_MAX)});
|
||||
if (!plan.table_indexed) {
|
||||
// NativeDescriptorSource proved this read reaches the image. Preserve its
|
||||
// SSA Phi edges while retaining descriptor DWORDs for other consumers.
|
||||
std::map<const Inst*, Value> projected;
|
||||
const auto project = [&](auto&& self, Value value) -> Value {
|
||||
value = value.Resolve();
|
||||
const auto* phi = value.TryInstruction();
|
||||
if (phi == plan.table_read) return key;
|
||||
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi) return Value(UINT32_MAX);
|
||||
if (const auto found = projected.find(phi); found != projected.end()) return found->second;
|
||||
auto* parent = phi->Parent();
|
||||
const auto position = std::ranges::find_if(parent->Instructions(),
|
||||
[&](const Inst& inst) { return &inst == phi; });
|
||||
auto& selected = *parent->PrependNewInst(position, ValueOpcode::Phi);
|
||||
selected.SetFlags(Type::U32);
|
||||
projected.emplace(phi, Value(&selected));
|
||||
for (uint32_t arm = 0; arm < phi->NumArgs(); ++arm)
|
||||
selected.AddPhiOperand(phi->PhiBlock(arm), self(self, phi->Arg(arm)));
|
||||
return Value(&selected);
|
||||
};
|
||||
key = project(project, plan.handle->Arg(0));
|
||||
}
|
||||
plan.handle->SetArg(0, key);
|
||||
for (uint32_t word = 1; word < plan.handle->NumArgs(); ++word)
|
||||
plan.handle->SetArg(word, Value(0u));
|
||||
continue;
|
||||
}
|
||||
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
|
||||
plan.handle->SetArg(0, plan.key);
|
||||
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
|
||||
} else {
|
||||
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, plan.key);
|
||||
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, key);
|
||||
for (uint32_t dword = 0; dword < 4u; dword++) {
|
||||
plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]);
|
||||
}
|
||||
@@ -437,6 +493,10 @@ private:
|
||||
std::array<Value, 8> roots {};
|
||||
std::array<uint32_t, 8> memory {};
|
||||
std::array<const Inst*, 8> reads {};
|
||||
bool split_offsets = false;
|
||||
bool retain_reads = false;
|
||||
bool table_indexed = false;
|
||||
const Inst* table_read = nullptr;
|
||||
};
|
||||
|
||||
[[noreturn]] void Fail(uint32_t pc, const std::string& reason) const {
|
||||
@@ -834,7 +894,7 @@ private:
|
||||
|
||||
uint32_t InternSource(const DescriptorSource& descriptor) {
|
||||
for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) {
|
||||
const auto& current = m_sources[candidate];
|
||||
auto& current = m_sources[candidate];
|
||||
if (current.dword_count != descriptor.dword_count ||
|
||||
current.indirect_descriptor.has_value() != descriptor.indirect_descriptor.has_value()) {
|
||||
continue;
|
||||
@@ -842,10 +902,9 @@ private:
|
||||
if (current.indirect_descriptor.has_value()) {
|
||||
const auto& a = *current.indirect_descriptor;
|
||||
const auto& b = *descriptor.indirect_descriptor;
|
||||
if (a.material_source != b.material_source || a.table_source != b.table_source ||
|
||||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
|
||||
a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits ||
|
||||
a.table_offset != b.table_offset || a.table_stride != b.table_stride ||
|
||||
if (a.selector != b.selector || a.table_source != b.table_source ||
|
||||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
|
||||
a.table_stride != b.table_stride ||
|
||||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
|
||||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
|
||||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
|
||||
@@ -860,6 +919,12 @@ private:
|
||||
same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]);
|
||||
}
|
||||
if (same) {
|
||||
if (current.indirect_descriptor) {
|
||||
current.indirect_descriptor->table_scalar |= descriptor.indirect_descriptor->table_scalar;
|
||||
current.indirect_descriptor->table_record_bytes = std::max(
|
||||
current.indirect_descriptor->table_record_bytes,
|
||||
descriptor.indirect_descriptor->table_record_bytes);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -949,54 +1014,6 @@ private:
|
||||
return ValidateSource(descriptor, bad_dword);
|
||||
}
|
||||
|
||||
bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const {
|
||||
stride = 1u;
|
||||
offset = 0u;
|
||||
value = value.Resolve();
|
||||
while (const auto* inst = value.TryInstruction()) {
|
||||
if (inst->NumArgs() != 2u) break;
|
||||
uint32_t constant = 0;
|
||||
Value remaining;
|
||||
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), constant) && constant < 32u) {
|
||||
stride *= 1u << constant;
|
||||
remaining = inst->Arg(0);
|
||||
} else if (inst->GetOpcode() == ValueOpcode::IAdd32 ||
|
||||
inst->GetOpcode() == ValueOpcode::IMul32) {
|
||||
if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1);
|
||||
else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0);
|
||||
else break;
|
||||
if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant;
|
||||
else stride *= constant;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
value = remaining.Resolve();
|
||||
}
|
||||
// Treat the remaining GPU-derived scalar as any U32 value.
|
||||
return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate();
|
||||
}
|
||||
|
||||
Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
indirect.selector_shift = 0u;
|
||||
indirect.selector_bits = UINT32_MAX;
|
||||
const auto* inst = key.Resolve().TryInstruction();
|
||||
uint32_t immediate;
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) {
|
||||
if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0);
|
||||
else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1);
|
||||
else return key;
|
||||
indirect.selector_bits = immediate;
|
||||
inst = key.Resolve().TryInstruction();
|
||||
}
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) {
|
||||
indirect.selector_shift = immediate;
|
||||
key = inst->Arg(0);
|
||||
}
|
||||
return key.Resolve();
|
||||
}
|
||||
|
||||
enum class LaneQuantifier { Any, All };
|
||||
struct EdgePredicate {
|
||||
Value condition;
|
||||
@@ -1443,9 +1460,9 @@ private:
|
||||
static_cast<uint64_t>(UINT32_MAX) + 1u) return false;
|
||||
const auto mask = BoundedSetBitMask(offset.index, active);
|
||||
if (mask.IsEmpty()) return false;
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector_stride = static_cast<uint32_t>(offset.stride);
|
||||
indirect.selector_offset = static_cast<uint32_t>(offset.offset + memory.offset);
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.stride = static_cast<uint32_t>(offset.stride),
|
||||
.offset = static_cast<uint32_t>(offset.offset + memory.offset)});
|
||||
indirect.key_count = Value(32u);
|
||||
indirect.selector_mask = mask;
|
||||
return true;
|
||||
@@ -1606,8 +1623,9 @@ private:
|
||||
bool MatchDescriptorTable(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan,
|
||||
DescriptorSource& table_source, Value& key, uint32_t& table_offset,
|
||||
uint32_t& table_stride) {
|
||||
uint32_t& table_stride, uint32_t& table_immediate) {
|
||||
Inst* table_handle = nullptr;
|
||||
Value table_guard;
|
||||
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
|
||||
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
|
||||
if (read == nullptr) {
|
||||
@@ -1615,7 +1633,31 @@ private:
|
||||
}
|
||||
uint32_t memory_index = 0;
|
||||
const auto* memory = ScalarReadMemory(*read, memory_index);
|
||||
if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
|
||||
uint32_t component = 0;
|
||||
const bool indexed = memory == nullptr;
|
||||
if (indexed) {
|
||||
if (handle.NumArgs() != 8u) return false;
|
||||
const auto* selected = UniformizedMaterialValue(descriptor.dwords[dword], handle);
|
||||
if (selected == nullptr) return false;
|
||||
const auto* extract = selected->Arg(1).Resolve().TryInstruction();
|
||||
if (extract == nullptr || extract->GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
|
||||
!ImmediateU32(extract->Arg(1), component) || component >= 4u) return false;
|
||||
read = extract->Arg(0).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x4) return false;
|
||||
memory_index = read->Flags<MemoryFlags>().index;
|
||||
if (memory_index >= m_program.memory_info.size()) return false;
|
||||
memory = &m_program.memory_info[memory_index];
|
||||
uint32_t offset = 1u, scalar = 1u;
|
||||
if (memory->kind != ResourceKind::Buffer || memory->typed || memory->formatted ||
|
||||
!memory->idxen || memory->offen || memory->data_bits != 32u ||
|
||||
memory->data_dwords != 4u ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, read->Arg(4), selected->Arg(0)) ||
|
||||
(dword != 0u && !EquivalentValue(m_program, table_guard, read->Arg(4)))) return false;
|
||||
table_guard = read->Arg(4);
|
||||
}
|
||||
if (memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
|
||||
!MemoryIndexBelongsTo(memory_index, *read)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1630,23 +1672,32 @@ private:
|
||||
(memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) ||
|
||||
(table_handle != nullptr &&
|
||||
!EquivalentValue(m_program, Value(table_handle), Value(current_handle))) ||
|
||||
!MatchTableOffset(read->Arg(1), current_key, offset, stride) ||
|
||||
memory->offset > UINT32_MAX - offset) {
|
||||
(!indexed && !MatchTableOffset(read->Arg(1), current_key, offset, stride))) {
|
||||
return false;
|
||||
}
|
||||
offset += memory->offset;
|
||||
if (indexed) {
|
||||
current_key = read->Arg(1);
|
||||
offset = component * 4u;
|
||||
}
|
||||
if (dword == 0u) {
|
||||
if (component != 0u) return false;
|
||||
plan.table_indexed = indexed;
|
||||
key = current_key;
|
||||
table_offset = offset;
|
||||
table_immediate = memory->offset;
|
||||
table_stride = stride;
|
||||
} else if (table_stride != stride || !EquivalentValue(m_program, key, current_key) ||
|
||||
static_cast<uint64_t>(table_offset) + dword * sizeof(uint32_t) != offset) {
|
||||
} else if (plan.table_indexed != indexed || table_stride != stride ||
|
||||
!EquivalentValue(m_program, key, current_key) ||
|
||||
uint64_t {table_offset} + table_immediate + dword * sizeof(uint32_t) !=
|
||||
uint64_t {offset} + memory->offset) {
|
||||
return false;
|
||||
}
|
||||
plan.split_offsets |= !indexed && offset != table_offset;
|
||||
table_handle = current_handle;
|
||||
if (handle.NumArgs() == 8u
|
||||
? WorkgroupAxis(key) == UINT32_MAX && !UsesOnlyImageDescriptors(*read) :
|
||||
std::ranges::any_of(read->Uses(), [](const Use& use) {
|
||||
if (handle.NumArgs() == 8u) {
|
||||
if (memory->kind == ResourceKind::ScalarAddress)
|
||||
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
|
||||
} else if (std::ranges::any_of(read->Uses(), [](const Use& use) {
|
||||
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
|
||||
std::ranges::any_of(use.user->Uses(), [](const Use& consumer) {
|
||||
return consumer.user->GetOpcode() != ValueOpcode::StoreBufferU32;
|
||||
@@ -1664,34 +1715,6 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatchUniformizedBufferKey(Value key, const Inst& image,
|
||||
DescriptorSource::IndirectDescriptor& indirect,
|
||||
DescriptorSource& material_source) {
|
||||
const auto* selected = UniformizedMaterialValue(key, image);
|
||||
if (selected == nullptr) return false;
|
||||
const auto value = MaterialKey(selected->Arg(1), indirect);
|
||||
if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false;
|
||||
const auto* loaded = value.TryInstruction();
|
||||
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
|
||||
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
|
||||
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false;
|
||||
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
|
||||
uint32_t offset = 1u, scalar = 1u;
|
||||
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
|
||||
!memory.idxen || memory.offen ||
|
||||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
|
||||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) ||
|
||||
!MakeRuntimeTableSource(*read, material_source)) return false;
|
||||
// GPU index arithmetic may wrap. Every record in the bounded V# is a candidate.
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector_first = Value(0u);
|
||||
indirect.key_count = material_source.dwords[2];
|
||||
indirect.selector_offset = memory.offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
|
||||
IndirectDescriptorPlan& plan) {
|
||||
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
|
||||
@@ -1699,17 +1722,19 @@ private:
|
||||
Value key;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
|
||||
table_stride)) return false;
|
||||
table_stride, table_immediate)) return false;
|
||||
DescriptorSource material_source;
|
||||
DescriptorSource::IndirectDescriptor indirect;
|
||||
indirect.table_offset = table_offset;
|
||||
indirect.table_immediate = table_immediate;
|
||||
indirect.table_stride = table_stride;
|
||||
indirect.workgroup_axis = WorkgroupAxis(key);
|
||||
if (indirect.workgroup_axis != UINT32_MAX) {
|
||||
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
|
||||
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
|
||||
// only the image handle is projected onto the bounded workgroup key.
|
||||
plan.reads.fill(nullptr);
|
||||
plan.retain_reads = true;
|
||||
} else if (table_source.dword_count == 2u) {
|
||||
if (table_stride != 32u) return false;
|
||||
const auto* selector = key.Resolve().TryInstruction();
|
||||
@@ -1728,36 +1753,21 @@ private:
|
||||
}
|
||||
if ((table_offset & 3u) != 0u ||
|
||||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
|
||||
} else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) {
|
||||
auto* material_read = MaterialKey(key, indirect).TryInstruction();
|
||||
uint32_t material_memory_index = 0;
|
||||
const auto* memory = material_read != nullptr
|
||||
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
|
||||
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
|
||||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
|
||||
return false;
|
||||
}
|
||||
if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride,
|
||||
indirect.selector_offset)) {
|
||||
return false;
|
||||
}
|
||||
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
|
||||
indirect.selector_offset =
|
||||
(static_cast<uint32_t>(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u);
|
||||
const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction();
|
||||
if (material_handle == nullptr ||
|
||||
material_handle->GetOpcode() != ValueOpcode::GetBufferResource ||
|
||||
!MakeRuntimeTableSource(*material_read, material_source)) {
|
||||
return false;
|
||||
}
|
||||
indirect.material_source = InternSource(material_source);
|
||||
} else {
|
||||
// A bounded V# supplies the complete image table. Leave every GPU selector
|
||||
// and descriptor read in the shader; the host only translates table bytes.
|
||||
if (plan.split_offsets) return false;
|
||||
indirect.table_stride = 0u;
|
||||
indirect.table_offset = 0u;
|
||||
indirect.table_immediate = 0u;
|
||||
indirect.workgroup_axis = UINT32_MAX;
|
||||
plan.table_read = plan.reads[0];
|
||||
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
|
||||
else indirect.table_scalar = true;
|
||||
plan.retain_reads = true;
|
||||
}
|
||||
// This plan combines shader byte-offset additions with the scalar immediate.
|
||||
// A nonzero combined offset is exact only when neither can cross U32 wrap.
|
||||
const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u &&
|
||||
table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX)
|
||||
return false;
|
||||
|
||||
if (plan.retain_reads) plan.reads.fill(nullptr);
|
||||
indirect.table_source = InternSource(table_source);
|
||||
DescriptorSource image_source;
|
||||
image_source.dword_count = 8u;
|
||||
@@ -1791,9 +1801,10 @@ private:
|
||||
Value key;
|
||||
uint32_t table_offset = 0;
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t table_immediate = 0;
|
||||
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
|
||||
table_stride) || table_stride != 16u ||
|
||||
table_source.dword_count != 4u || table_offset != 0u) return false;
|
||||
table_stride, table_immediate) || table_stride != 16u ||
|
||||
table_source.dword_count != 4u || table_offset != 0u || table_immediate != 0u) return false;
|
||||
const auto* lane_read = key.Resolve().TryInstruction();
|
||||
if (lane_read == nullptr || lane_read->GetOpcode() != ValueOpcode::ReadFirstLane)
|
||||
return false;
|
||||
@@ -1825,9 +1836,9 @@ private:
|
||||
indirect.table_stride = table_stride;
|
||||
if (!MakeRuntimeTableSource(*read, material_source) ||
|
||||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
|
||||
indirect.material_source = InternSource(material_source);
|
||||
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
|
||||
.offset = memory.offset + lane * 4u});
|
||||
indirect.table_source = InternSource(table_source);
|
||||
indirect.selector_offset = memory.offset + lane * 4u;
|
||||
DescriptorSource source;
|
||||
source.dword_count = 4u;
|
||||
source.dwords.fill(Value(0u));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
bool EnsureGameDirectory();
|
||||
void ScanGameDirectory();
|
||||
void ViewTrophies();
|
||||
void ViewTrophyOverview();
|
||||
|
||||
signals:
|
||||
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
#ifndef PATCHES_DIALOG_H
|
||||
#define PATCHES_DIALOG_H
|
||||
|
||||
#include "cheatRepository.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDialog>
|
||||
#include <QJsonDocument>
|
||||
#include <QString>
|
||||
|
||||
class Configuration;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
class QTabWidget;
|
||||
class QTreeWidget;
|
||||
|
||||
class PatchesDialog final: public QDialog {
|
||||
public:
|
||||
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
||||
~PatchesDialog() override = default;
|
||||
|
||||
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
|
||||
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
|
||||
|
||||
private:
|
||||
void Load();
|
||||
void Save();
|
||||
void LoadRemote();
|
||||
void PreviewRemote();
|
||||
void ImportRemote();
|
||||
|
||||
QString m_title_id;
|
||||
QListWidget* m_patches = nullptr;
|
||||
QLabel* m_status = nullptr;
|
||||
QPushButton* m_apply = nullptr;
|
||||
QString m_title_id;
|
||||
QString m_version;
|
||||
QString m_process;
|
||||
QByteArray m_local_data;
|
||||
QJsonDocument m_local_document;
|
||||
QJsonDocument m_remote_document;
|
||||
bool m_local_readable = false;
|
||||
Cheats::CheatRepository* m_repository = nullptr;
|
||||
QList<Cheats::RemoteFile> m_remote_files;
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
QTreeWidget* m_files = nullptr;
|
||||
QListWidget* m_preview = nullptr;
|
||||
QLabel* m_remote_status = nullptr;
|
||||
QLabel* m_preview_status = nullptr;
|
||||
QPushButton* m_refresh = nullptr;
|
||||
QPushButton* m_import = nullptr;
|
||||
QListWidget* m_patches = nullptr;
|
||||
QLabel* m_status = nullptr;
|
||||
QPushButton* m_apply = nullptr;
|
||||
};
|
||||
|
||||
#endif // PATCHES_DIALOG_H
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class QTabWidget;
|
||||
class QWidget;
|
||||
|
||||
@@ -14,6 +16,8 @@ public:
|
||||
static bool HasTrophyData(const Configuration* info);
|
||||
static void ShowForGame(const Configuration* info, const QString& runtime_directory,
|
||||
QWidget* parent);
|
||||
static void ShowOverview(const std::vector<const Configuration*>& games,
|
||||
const QString& runtime_directory, QWidget* parent);
|
||||
|
||||
private:
|
||||
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "cheatFile.h"
|
||||
#include "common/archive.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
@@ -55,6 +56,7 @@
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ui_configuration_list_widget.h"
|
||||
|
||||
@@ -240,6 +242,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 +296,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() {
|
||||
@@ -839,6 +844,20 @@ void ConfigurationListWidget::ViewTrophies() {
|
||||
TrophyViewerDialog::ShowForGame(config.get(), m_runtime_directory, this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::ViewTrophyOverview() {
|
||||
std::vector<std::unique_ptr<Configuration>> configurations;
|
||||
std::vector<const Configuration*> games;
|
||||
configurations.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
|
||||
games.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
|
||||
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
|
||||
const auto* item =
|
||||
static_cast<const ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
|
||||
configurations.push_back(CreateConfiguration(*item));
|
||||
games.push_back(configurations.back().get());
|
||||
}
|
||||
TrophyViewerDialog::ShowOverview(games, m_runtime_directory, this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::open_game_folder() {
|
||||
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
|
||||
if (item == nullptr) {
|
||||
@@ -1015,7 +1034,7 @@ 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()));
|
||||
|
||||
@@ -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>
|
||||
@@ -285,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;
|
||||
}
|
||||
|
||||
+270
-101
@@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
auto root = document.object();
|
||||
auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||
if (mods.size() != m_patches->count()) {
|
||||
m_status->setText(changed_message);
|
||||
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;
|
||||
}
|
||||
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()));
|
||||
return;
|
||||
}
|
||||
m_status->setText(tr("Cheat selection saved."));
|
||||
m_local_document = document;
|
||||
m_local_data = document.toJson();
|
||||
m_status->setText(tr("Cheat selection saved. Changes take effect on the next game launch."));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (!m_local_readable) {
|
||||
m_preview_status->setText(
|
||||
tr("Could not read the local cheat file. Open Local and reload it before importing."));
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -6,18 +6,23 @@
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QHBoxLayout>
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QRegularExpression>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QTabBar>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
@@ -49,26 +54,6 @@ static QString GradeToText(int grade) {
|
||||
}
|
||||
}
|
||||
|
||||
static void PrepareTable(QTableWidget* table) {
|
||||
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
|
||||
QObject::tr("Name"), QObject::tr("Description")});
|
||||
table->setAlternatingRowColors(true);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setIconSize(QSize(96, 96));
|
||||
table->setShowGrid(false);
|
||||
table->setWordWrap(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->verticalHeader()->setDefaultSectionSize(112);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
table->horizontalHeader()->setHighlightSections(false);
|
||||
table->setColumnWidth(0, 100);
|
||||
table->setColumnWidth(1, 132);
|
||||
table->setColumnWidth(2, 260);
|
||||
table->setColumnWidth(3, 480);
|
||||
}
|
||||
|
||||
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
|
||||
QStringList lines {QString::fromStdString(trophy.name),
|
||||
QString::fromStdString(trophy.description),
|
||||
@@ -102,6 +87,225 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
|
||||
return !FindTrophyFiles(info).isEmpty();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Common::Trophies::Progress;
|
||||
|
||||
Common::Trophies::UnlockData LoadUnlocks(const Configuration& info,
|
||||
const QString& runtime_directory, const QString& file) {
|
||||
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
|
||||
return Common::Trophies::LoadUnlockData(Common::Trophies::UnlocksPath(
|
||||
GameContent::ToPath(runtime_directory), info.title_id.toStdString(), info.user_id, label));
|
||||
}
|
||||
|
||||
Progress GetGameProgress(const Configuration* info, const QString& runtime_directory) {
|
||||
Progress result;
|
||||
if (info == nullptr) {
|
||||
return result;
|
||||
}
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info->basedir));
|
||||
for (const auto& file: FindTrophyFiles(info)) {
|
||||
const auto package =
|
||||
Common::Trophies::LoadPackage(GameContent::ToPath(file), info->console_language);
|
||||
const auto progress =
|
||||
Common::Trophies::GetProgress(package, LoadUnlocks(*info, runtime_directory, file));
|
||||
result.total += progress.total;
|
||||
result.earned += progress.earned;
|
||||
for (size_t grade = 1; grade < result.total_grade.size(); ++grade) {
|
||||
result.total_grade[grade] += progress.total_grade[grade];
|
||||
result.earned_grade[grade] += progress.earned_grade[grade];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap MakeCupIcon(const QColor& color, int size) {
|
||||
QPixmap pixmap(size, size);
|
||||
pixmap.fill(Qt::transparent);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
const double u = size / 20.0;
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
QPainterPath cup;
|
||||
cup.moveTo(5 * u, 2 * u);
|
||||
cup.lineTo(15 * u, 2 * u);
|
||||
cup.lineTo(14 * u, 9 * u);
|
||||
cup.quadTo(10 * u, 13 * u, 6 * u, 9 * u);
|
||||
cup.closeSubpath();
|
||||
painter.drawPath(cup);
|
||||
painter.setPen(QPen(color, 1.6 * u));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawArc(QRectF(1.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, 180 * 16);
|
||||
painter.drawArc(QRectF(13.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, -180 * 16);
|
||||
painter.drawLine(QPointF(10 * u, 12 * u), QPointF(10 * u, 16 * u));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
painter.drawRoundedRect(QRectF(6 * u, 16 * u, 8 * u, 2.5 * u), u, u);
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QColor GradeColor(int grade) {
|
||||
switch (grade) {
|
||||
case 1: return QColor(0x7fc8ff);
|
||||
case 2: return QColor(0xf5c542);
|
||||
case 3: return QColor(0xc4cad2);
|
||||
default: return QColor(0xcd7f4f);
|
||||
}
|
||||
}
|
||||
|
||||
QWidget* MakeGradeCount(QWidget* parent, int grade, int value, int icon_size, int font_size) {
|
||||
auto* box = new QWidget(parent);
|
||||
auto* row = new QHBoxLayout(box);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
auto* icon = new QLabel(box);
|
||||
icon->setPixmap(MakeCupIcon(GradeColor(grade), icon_size));
|
||||
auto* text = new QLabel(QString::number(value), box);
|
||||
text->setStyleSheet(QStringLiteral("font-size: %1px;").arg(font_size));
|
||||
row->addWidget(icon);
|
||||
row->addWidget(text);
|
||||
return box;
|
||||
}
|
||||
|
||||
QPixmap LoadGameArt(const Configuration* game, int height) {
|
||||
QPixmap art;
|
||||
const auto data = GameContent::ReadFile(game->basedir, QStringLiteral("sce_sys/icon0.png"),
|
||||
GameContent::MaxImageSize);
|
||||
if (!data.isEmpty() && art.loadFromData(data)) {
|
||||
return art.scaledToHeight(height, Qt::SmoothTransformation);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void TrophyViewerDialog::ShowOverview(const std::vector<const Configuration*>& games,
|
||||
const QString& runtime_directory, QWidget* parent) {
|
||||
QDialog dialog(parent);
|
||||
dialog.setWindowTitle(QObject::tr("Trophies"));
|
||||
dialog.resize(820, 640);
|
||||
dialog.setStyleSheet(QStringLiteral(
|
||||
"QDialog { background: palette(window); }"
|
||||
"QLabel { color: palette(text); }"
|
||||
"QListWidget { background: transparent; border: none; outline: none; }"
|
||||
"QListWidget::item { border-bottom: 1px solid palette(mid); padding: 4px; }"
|
||||
"QListWidget::item:selected, QListWidget::item:hover { background: palette(midlight); "
|
||||
"border: 1px solid palette(highlight); }"
|
||||
"QProgressBar { background: palette(mid); border: none; height: 4px; }"
|
||||
"QProgressBar::chunk { background: palette(highlight); }"));
|
||||
auto* layout = new QVBoxLayout(&dialog);
|
||||
|
||||
struct GameProgress {
|
||||
const Configuration* game;
|
||||
Progress progress;
|
||||
};
|
||||
std::vector<GameProgress> trophy_games;
|
||||
Progress totals;
|
||||
for (const auto* game: games) {
|
||||
const auto progress = GetGameProgress(game, runtime_directory);
|
||||
if (progress.total == 0) {
|
||||
continue;
|
||||
}
|
||||
trophy_games.push_back({game, progress});
|
||||
totals.earned += progress.earned;
|
||||
for (size_t grade = 1; grade < totals.earned_grade.size(); ++grade) {
|
||||
totals.earned_grade[grade] += progress.earned_grade[grade];
|
||||
}
|
||||
}
|
||||
|
||||
auto* header = new QHBoxLayout;
|
||||
auto* title = new QLabel(QObject::tr("Trophies"), &dialog);
|
||||
title->setStyleSheet(QStringLiteral("font-size: 30px;"));
|
||||
header->addWidget(title);
|
||||
header->addStretch(1);
|
||||
auto* total = new QLabel(QObject::tr("Total %1").arg(totals.earned), &dialog);
|
||||
total->setAlignment(Qt::AlignCenter);
|
||||
total->setStyleSheet(QStringLiteral("font-size: 16px;"));
|
||||
header->addWidget(total);
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
header->addWidget(MakeGradeCount(&dialog, grade, totals.earned_grade[grade], 32, 18));
|
||||
}
|
||||
layout->addLayout(header);
|
||||
|
||||
auto* list = new QListWidget(&dialog);
|
||||
list->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
layout->addWidget(list, 1);
|
||||
|
||||
for (size_t index = 0; index < trophy_games.size(); ++index) {
|
||||
const auto& [game, progress] = trophy_games[index];
|
||||
auto* item = new QListWidgetItem(list);
|
||||
item->setSizeHint(QSize(760, 112));
|
||||
item->setData(Qt::UserRole, static_cast<int>(index));
|
||||
|
||||
auto* row_widget = new QWidget(list);
|
||||
row_widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
auto* row = new QHBoxLayout(row_widget);
|
||||
auto* art = new QLabel(row_widget);
|
||||
art->setFixedSize(80, 80);
|
||||
art->setAlignment(Qt::AlignCenter);
|
||||
const auto pixmap = LoadGameArt(game, 80);
|
||||
if (!pixmap.isNull()) {
|
||||
art->setPixmap(pixmap);
|
||||
} else {
|
||||
art->setStyleSheet(QStringLiteral("background: palette(mid);"));
|
||||
}
|
||||
row->addWidget(art);
|
||||
const auto name = !game->name.isEmpty() ? game->name : game->title_id;
|
||||
auto* text = new QLabel(QStringLiteral("%1\n%2").arg(name, game->title_id), row_widget);
|
||||
text->setStyleSheet(QStringLiteral("font-size: 18px;"));
|
||||
row->addWidget(text, 1);
|
||||
|
||||
auto* percent_box = new QVBoxLayout;
|
||||
percent_box->setSpacing(2);
|
||||
percent_box->setContentsMargins(0, 0, 0, 0);
|
||||
auto* percent = new QLabel(QStringLiteral("%1%").arg(progress.Percentage()), row_widget);
|
||||
percent->setAlignment(Qt::AlignRight);
|
||||
percent->setStyleSheet(QStringLiteral("font-size: 24px;"));
|
||||
percent->setMinimumHeight(34);
|
||||
auto* earned = new QLabel(
|
||||
QObject::tr("Earned %1/%2").arg(progress.earned).arg(progress.total), row_widget);
|
||||
earned->setAlignment(Qt::AlignRight);
|
||||
earned->setMinimumHeight(18);
|
||||
auto* bar = new QProgressBar(row_widget);
|
||||
bar->setRange(0, 100);
|
||||
bar->setValue(progress.Percentage());
|
||||
bar->setTextVisible(false);
|
||||
bar->setFixedWidth(130);
|
||||
percent_box->addWidget(percent);
|
||||
percent_box->addWidget(earned);
|
||||
percent_box->addWidget(bar);
|
||||
row->addLayout(percent_box);
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
if (grade == 1 && progress.total_grade[1] == 0) {
|
||||
continue;
|
||||
}
|
||||
row->addWidget(MakeGradeCount(row_widget, grade, progress.earned_grade[grade], 24, 18));
|
||||
}
|
||||
list->setItemWidget(item, row_widget);
|
||||
}
|
||||
if (trophy_games.empty()) {
|
||||
auto* empty =
|
||||
new QLabel(QObject::tr("No games with trophies found in the game folders."), &dialog);
|
||||
empty->setAlignment(Qt::AlignCenter);
|
||||
layout->insertWidget(1, empty);
|
||||
list->hide();
|
||||
}
|
||||
|
||||
connect(list, &QListWidget::itemClicked, &dialog,
|
||||
[&dialog, &trophy_games, &runtime_directory](QListWidgetItem* item) {
|
||||
const auto index = item->data(Qt::UserRole).toInt();
|
||||
if (index >= 0 && static_cast<size_t>(index) < trophy_games.size()) {
|
||||
ShowForGame(trophy_games[static_cast<size_t>(index)].game, runtime_directory,
|
||||
&dialog);
|
||||
}
|
||||
});
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
layout->addWidget(buttons);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, const QString& runtime_directory,
|
||||
QWidget* parent) {
|
||||
if (info == nullptr) {
|
||||
@@ -139,51 +343,131 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
|
||||
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
|
||||
continue;
|
||||
}
|
||||
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
|
||||
const auto unlocked = Common::Trophies::LoadUnlocks(
|
||||
Common::Trophies::UnlocksPath(GameContent::ToPath(runtime_directory),
|
||||
info.title_id.toStdString(), info.user_id, label));
|
||||
auto* table = new QTableWidget(static_cast<int>(package.trophies.size()), 4, m_tabs);
|
||||
PrepareTable(table);
|
||||
int row_index = 0;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
const bool trophy_unlocked = unlocked.contains(id);
|
||||
auto* status = new QTableWidgetItem(trophy_unlocked ? tr("UNLOCKED") : tr("LOCKED"));
|
||||
status->setTextAlignment(Qt::AlignCenter);
|
||||
auto status_font = status->font();
|
||||
status_font.setBold(true);
|
||||
status->setFont(status_font);
|
||||
table->setItem(row_index, 0, status);
|
||||
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
|
||||
const auto counts = Common::Trophies::GetProgress(package, unlocks);
|
||||
auto* page = new QWidget(m_tabs);
|
||||
auto* page_layout = new QVBoxLayout(page);
|
||||
|
||||
auto* icon_item = new QTableWidgetItem;
|
||||
icon_item->setTextAlignment(Qt::AlignCenter);
|
||||
QPixmap icon;
|
||||
if (icon.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
|
||||
static_cast<uint>(trophy.icon_png.size()))) {
|
||||
if (!trophy_unlocked) {
|
||||
icon = QPixmap::fromImage(
|
||||
icon.toImage().convertToFormat(QImage::Format_Grayscale8));
|
||||
}
|
||||
icon_item->setIcon(QIcon(icon));
|
||||
}
|
||||
table->setItem(row_index, 1, icon_item);
|
||||
|
||||
const auto name_text = trophy.name.empty() ? (trophy.hidden ? tr("Hidden Trophy")
|
||||
: tr("Trophy %1").arg(id))
|
||||
: QString::fromStdString(trophy.name);
|
||||
auto* name = new QTableWidgetItem(name_text);
|
||||
auto font = name->font();
|
||||
font.setBold(true);
|
||||
name->setFont(font);
|
||||
name->setToolTip(TrophyTooltip(trophy));
|
||||
auto* detail = new QTableWidgetItem(QString::fromStdString(trophy.description));
|
||||
detail->setToolTip(name->toolTip());
|
||||
table->setItem(row_index, 2, name);
|
||||
table->setItem(row_index, 3, detail);
|
||||
++row_index;
|
||||
auto* header = new QHBoxLayout;
|
||||
auto* art = new QLabel(page);
|
||||
if (const auto pixmap = LoadGameArt(&info, 56); !pixmap.isNull()) {
|
||||
art->setPixmap(pixmap);
|
||||
header->addWidget(art);
|
||||
}
|
||||
m_tabs->addTab(table, tr("Trophy %1").arg(label));
|
||||
auto* title = new QLabel(QString::fromStdString(package.title), page);
|
||||
title->setStyleSheet(QStringLiteral("font-size: 28px;"));
|
||||
header->addWidget(title, 1);
|
||||
const auto make_stat = [page](const QString& label, const QString& value) {
|
||||
auto* box = new QVBoxLayout;
|
||||
auto* name = new QLabel(label, page);
|
||||
auto* number = new QLabel(value, page);
|
||||
name->setStyleSheet(QStringLiteral("font-size: 14px;"));
|
||||
number->setStyleSheet(QStringLiteral("font-size: 22px;"));
|
||||
box->addWidget(name);
|
||||
box->addWidget(number);
|
||||
return box;
|
||||
};
|
||||
header->addLayout(
|
||||
make_stat(tr("Progress"), QStringLiteral("%1%").arg(counts.Percentage())));
|
||||
header->addLayout(
|
||||
make_stat(tr("Earned"), QStringLiteral("%1/%2").arg(counts.earned).arg(counts.total)));
|
||||
for (int grade = 1; grade <= 4; ++grade) {
|
||||
if (counts.total_grade[grade] > 0) {
|
||||
header->addWidget(MakeGradeCount(page, grade, counts.earned_grade[grade], 30, 20));
|
||||
}
|
||||
}
|
||||
page_layout->addLayout(header);
|
||||
auto* progress = new QProgressBar(page);
|
||||
progress->setRange(0, 100);
|
||||
progress->setValue(counts.Percentage());
|
||||
progress->setTextVisible(false);
|
||||
page_layout->addWidget(progress);
|
||||
auto* count_label = new QLabel(tr("All trophies: %1").arg(counts.total), page);
|
||||
page_layout->addWidget(count_label);
|
||||
|
||||
auto* list = new QListWidget(page);
|
||||
list->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
list->setSpacing(4);
|
||||
page_layout->addWidget(list, 1);
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
const bool trophy_unlocked = unlocks.unlocked.contains(id);
|
||||
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
|
||||
auto* item = new QListWidgetItem(list);
|
||||
item->setSizeHint(QSize(900, 96));
|
||||
|
||||
auto* card = new QWidget(list);
|
||||
card->setStyleSheet(
|
||||
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
|
||||
card->setObjectName(QStringLiteral("card"));
|
||||
auto* row = new QHBoxLayout(card);
|
||||
auto* icon = new QLabel(card);
|
||||
icon->setFixedSize(76, 76);
|
||||
icon->setAlignment(Qt::AlignCenter);
|
||||
QPixmap pixmap;
|
||||
if (hidden_locked) {
|
||||
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
|
||||
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
|
||||
static_cast<uint>(trophy.icon_png.size())) &&
|
||||
!trophy_unlocked) {
|
||||
pixmap =
|
||||
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
|
||||
}
|
||||
if (!pixmap.isNull()) {
|
||||
icon->setPixmap(
|
||||
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
row->addWidget(icon);
|
||||
|
||||
auto* left = new QVBoxLayout;
|
||||
const auto name_text = hidden_locked ? tr("Hidden trophy")
|
||||
: trophy.name.empty() ? tr("Trophy %1").arg(id)
|
||||
: QString::fromStdString(trophy.name);
|
||||
auto* name = new QLabel(name_text, card);
|
||||
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
|
||||
left->addWidget(name);
|
||||
if (!hidden_locked) {
|
||||
auto* grade_row = new QHBoxLayout;
|
||||
auto* cup = new QLabel(card);
|
||||
cup->setPixmap(MakeCupIcon(GradeColor(trophy.grade), 18));
|
||||
auto* grade_text = new QLabel(GradeToText(trophy.grade), card);
|
||||
grade_text->setStyleSheet(QStringLiteral("font-size: 15px;"));
|
||||
grade_row->addWidget(cup);
|
||||
grade_row->addWidget(grade_text);
|
||||
grade_row->addStretch(1);
|
||||
left->addLayout(grade_row);
|
||||
}
|
||||
|
||||
row->addLayout(left, 2);
|
||||
|
||||
auto* right = new QVBoxLayout;
|
||||
QString date_text;
|
||||
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
|
||||
date_text = QDateTime::fromMSecsSinceEpoch(
|
||||
static_cast<qint64>(
|
||||
(found->second - Common::Trophies::UnixEpochTick) / 1000))
|
||||
.toLocalTime()
|
||||
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
||||
} else if (trophy_unlocked) {
|
||||
date_text = tr("Earned");
|
||||
}
|
||||
auto* date = new QLabel(date_text, card);
|
||||
date->setAlignment(Qt::AlignRight);
|
||||
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
|
||||
auto* detail = new QLabel(
|
||||
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
|
||||
detail->setWordWrap(true);
|
||||
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
|
||||
right->addWidget(date);
|
||||
right->addWidget(detail, 1);
|
||||
row->addLayout(right, 3);
|
||||
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
|
||||
list->setItemWidget(item, card);
|
||||
}
|
||||
m_tabs->addTab(page, QString::fromStdString(package.title));
|
||||
}
|
||||
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
|
||||
if (m_tabs->count() == 0) {
|
||||
error = errors.join(QLatin1Char('\n'));
|
||||
return false;
|
||||
|
||||
@@ -39,9 +39,11 @@ QLineEdit#search_line_edit:focus {
|
||||
border-color: palette(highlight);
|
||||
}
|
||||
|
||||
QToolButton#refresh_button,
|
||||
QToolButton#global_settings_button,
|
||||
QToolButton#input_mapping_button,
|
||||
QToolButton#edit_button,
|
||||
QToolButton#trophy_overview_button,
|
||||
QToolButton#delete_button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
@@ -49,17 +51,21 @@ QToolButton#delete_button {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
QToolButton#refresh_button:hover,
|
||||
QToolButton#global_settings_button:hover,
|
||||
QToolButton#input_mapping_button:hover,
|
||||
QToolButton#edit_button:hover,
|
||||
QToolButton#trophy_overview_button:hover,
|
||||
QToolButton#delete_button:hover {
|
||||
background: palette(button);
|
||||
border-color: palette(mid);
|
||||
}
|
||||
|
||||
QToolButton#refresh_button:pressed,
|
||||
QToolButton#global_settings_button:pressed,
|
||||
QToolButton#input_mapping_button:pressed,
|
||||
QToolButton#edit_button:pressed,
|
||||
QToolButton#trophy_overview_button:pressed,
|
||||
QToolButton#delete_button:pressed {
|
||||
background: palette(midlight);
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+86
-49
@@ -7,13 +7,12 @@
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -104,7 +103,7 @@ using TrophyKey = std::pair<int, uint32_t>;
|
||||
|
||||
struct TrophyState {
|
||||
const Trophies::Package* package = nullptr;
|
||||
std::set<int> unlocked;
|
||||
Trophies::UnlockData unlocks;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
@@ -130,20 +129,33 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
|
||||
if (const auto it = g_states.find(key); it != g_states.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
std::string title_id;
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
auto& package = g_packages[key.second];
|
||||
if (package.trophies.empty()) {
|
||||
const auto filename = "/app0/" + Trophies::PackagePath(key.second).generic_string();
|
||||
package = Trophies::LoadPackage(LibKernel::FileSystem::GetRealFilename(filename),
|
||||
Config::GetConsoleLanguage());
|
||||
if (package.trophies.empty()) {
|
||||
LOGF("[Trophy] could not load trophy package %s for %s\n", filename.c_str(),
|
||||
title_id.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
LOGF("[Trophy] loaded %zu trophy definitions for %s (service %u)\n",
|
||||
package.trophies.size(), title_id.c_str(), key.second);
|
||||
const auto uds_filename = "/app0/" + Trophies::UdsPackagePath(key.second).generic_string();
|
||||
package.event_rules =
|
||||
Trophies::LoadUdsRules(LibKernel::FileSystem::GetRealFilename(uds_filename));
|
||||
if (package.event_rules.empty()) {
|
||||
LOGF("[Trophy] no supported UDS extraction rules in %s\n", uds_filename.c_str());
|
||||
} else {
|
||||
LOGF("[Trophy] loaded UDS extraction rules for %zu events from %s\n",
|
||||
package.event_rules.size(), uds_filename.c_str());
|
||||
}
|
||||
}
|
||||
std::string title_id;
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocked = Trophies::LoadUnlocks(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocked), std::move(path)})
|
||||
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
|
||||
auto unlocks = Trophies::LoadUnlockData(path);
|
||||
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
|
||||
.first->second;
|
||||
}
|
||||
|
||||
@@ -165,7 +177,7 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
details->group_id = trophy.group_id;
|
||||
details->hidden = trophy.hidden;
|
||||
details->has_reward = trophy.has_reward;
|
||||
if (trophy.target) {
|
||||
if (trophy.progressive && trophy.target) {
|
||||
details->target.type = 1;
|
||||
details->target.value = *trophy.target;
|
||||
}
|
||||
@@ -177,8 +189,12 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
if (data != nullptr) {
|
||||
*data = {};
|
||||
data->trophy_id = trophy.id;
|
||||
data->unlocked = state.unlocked.contains(trophy.id);
|
||||
if (trophy.target) {
|
||||
data->unlocked = state.unlocks.unlocked.contains(trophy.id);
|
||||
if (const auto timestamp = state.unlocks.timestamps.find(trophy.id);
|
||||
timestamp != state.unlocks.timestamps.end()) {
|
||||
data->timestamp_tick = timestamp->second;
|
||||
}
|
||||
if (trophy.progressive && trophy.target) {
|
||||
data->progress.type = 1;
|
||||
data->progress.value = data->unlocked ? *trophy.target : 0;
|
||||
}
|
||||
@@ -188,19 +204,13 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
|
||||
// Game and group structures share the same count fields.
|
||||
template <typename Details, typename Data>
|
||||
static void FillTrophyCounts(const TrophyState& state, int group, Details* details, Data* data) {
|
||||
std::array<uint32_t, 5> total {}, unlocked {};
|
||||
for (const auto& [id, trophy]: state.package->trophies) {
|
||||
if (group != -2 && trophy.group_id != group) {
|
||||
continue;
|
||||
}
|
||||
++total[trophy.grade];
|
||||
if (state.unlocked.contains(id)) {
|
||||
++unlocked[trophy.grade];
|
||||
}
|
||||
}
|
||||
const auto progress = Trophies::GetProgress(
|
||||
*state.package, state.unlocks, group == -2 ? std::nullopt : std::optional<int>(group));
|
||||
const auto& total = progress.total_grade;
|
||||
const auto& unlocked = progress.earned_grade;
|
||||
if (details != nullptr) {
|
||||
*details = {};
|
||||
details->num_trophies = total[1] + total[2] + total[3] + total[4];
|
||||
details->num_trophies = progress.total;
|
||||
details->num_platinum = total[1];
|
||||
details->num_gold = total[2];
|
||||
details->num_silver = total[3];
|
||||
@@ -208,14 +218,12 @@ static void FillTrophyCounts(const TrophyState& state, int group, Details* detai
|
||||
}
|
||||
if (data != nullptr) {
|
||||
*data = {};
|
||||
data->unlocked_trophies = unlocked[1] + unlocked[2] + unlocked[3] + unlocked[4];
|
||||
data->unlocked_trophies = progress.earned;
|
||||
data->unlocked_platinum = unlocked[1];
|
||||
data->unlocked_gold = unlocked[2];
|
||||
data->unlocked_silver = unlocked[3];
|
||||
data->unlocked_bronze = unlocked[4];
|
||||
const auto points = total[2] * 6 + total[3] * 2 + total[4];
|
||||
const auto earned = unlocked[2] * 6 + unlocked[3] * 2 + unlocked[4];
|
||||
data->progress_percentage = points == 0 ? 0 : earned * 100 / points;
|
||||
data->progress_percentage = progress.Percentage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,31 +239,45 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
|
||||
}
|
||||
}
|
||||
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id) {
|
||||
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
|
||||
const auto found = state.package->trophies.find(trophy_id);
|
||||
if (found == state.package->trophies.end() || found->second.grade == 1 ||
|
||||
found->second.target || !state.unlocked.insert(trophy_id).second) {
|
||||
if (found == state.package->trophies.end()) {
|
||||
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
|
||||
return;
|
||||
}
|
||||
if (found->second.grade == 1 || (!from_stat && found->second.progressive) ||
|
||||
!state.unlocks.unlocked.insert(trophy_id).second) {
|
||||
return;
|
||||
}
|
||||
const auto timestamp =
|
||||
Trophies::UnixEpochTick + std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
state.unlocks.timestamps.emplace(trophy_id, timestamp);
|
||||
std::vector<int> earned {trophy_id};
|
||||
const auto platinum_id = found->second.platinum_id;
|
||||
if (platinum_id >= 0 && state.package->trophies.contains(platinum_id) &&
|
||||
std::all_of(state.package->trophies.begin(), state.package->trophies.end(),
|
||||
[&](const auto& entry) {
|
||||
return entry.second.platinum_id != platinum_id ||
|
||||
state.unlocked.contains(entry.first);
|
||||
state.unlocks.unlocked.contains(entry.first);
|
||||
}) &&
|
||||
state.unlocked.insert(platinum_id).second) {
|
||||
state.unlocks.unlocked.insert(platinum_id).second) {
|
||||
state.unlocks.timestamps.emplace(platinum_id, timestamp);
|
||||
earned.push_back(platinum_id);
|
||||
}
|
||||
if (!Trophies::SaveUnlocks(state.path, state.unlocked)) {
|
||||
if (!Trophies::SaveUnlockData(state.path, state.unlocks)) {
|
||||
LOGF("[Trophy] could not save unlocks to %s\n", state.path.string().c_str());
|
||||
} else {
|
||||
LOGF("[Trophy] recorded unlock %d: %s\n", trophy_id, found->second.name.c_str());
|
||||
}
|
||||
if (Config::TrophyEnabled()) {
|
||||
for (const auto id: earned) {
|
||||
const auto& trophy = state.package->trophies.at(id);
|
||||
Graphics::NotifyTrophyUnlocked(trophy.name, trophy.grade, trophy.icon_png);
|
||||
}
|
||||
} else {
|
||||
LOGF("[Trophy] notification disabled by configuration\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,11 +563,13 @@ struct NpUniversalDataSystemMemoryStat {
|
||||
};
|
||||
|
||||
struct NpUniversalDataSystemEventPropertyObject {
|
||||
std::optional<int32_t> trophy_id;
|
||||
std::map<std::string, Common::Trophies::UdsInteger> integers;
|
||||
|
||||
void SetTrophyId(const char* key, std::optional<int32_t> value) {
|
||||
if (std::strcmp(key, "_trophy_id") == 0) {
|
||||
trophy_id = value;
|
||||
void SetInteger(const char* key, std::optional<Common::Trophies::UdsInteger> value) {
|
||||
if (value) {
|
||||
integers.insert_or_assign(key, *value);
|
||||
} else {
|
||||
integers.erase(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -647,6 +671,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemCreateEvent(
|
||||
}
|
||||
*new_event = new NpUniversalDataSystemEvent {
|
||||
event_name, prop != nullptr ? *prop : NpUniversalDataSystemEventPropertyObject {}};
|
||||
LOGF("[UDS] created event: %s\n", event_name);
|
||||
if (prop_ptr != nullptr) {
|
||||
*prop_ptr = &(*new_event)->properties;
|
||||
}
|
||||
@@ -668,14 +693,26 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemPostEvent(int context, int handle,
|
||||
return static_cast<int>(0x80553120); // NOT_REGISTERED
|
||||
}
|
||||
const auto& uds_event = *static_cast<const NpUniversalDataSystemEvent*>(event);
|
||||
LOGF("[UDS] posted event: %s\n", uds_event.name.c_str());
|
||||
if (uds_event.name == "_UnlockTrophy") {
|
||||
const auto trophy_id = uds_event.properties.trophy_id;
|
||||
if (!trophy_id || *trophy_id < 0) {
|
||||
const auto property = uds_event.properties.integers.find("_trophy_id");
|
||||
const auto* trophy_id = property == uds_event.properties.integers.end()
|
||||
? nullptr
|
||||
: std::get_if<int32_t>(&property->second);
|
||||
if (trophy_id == nullptr || *trophy_id < 0) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
|
||||
LibNpTrophy2::RecordTrophyUnlock(*state, *trophy_id);
|
||||
}
|
||||
} else if (!uds_event.properties.integers.empty()) {
|
||||
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
|
||||
const auto ids = LibNpTrophy2::Trophies::FindUdsTrophies(
|
||||
*state->package, uds_event.name, uds_event.properties.integers);
|
||||
for (const auto trophy_id: ids) {
|
||||
LibNpTrophy2::RecordTrophyUnlock(*state, trophy_id, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -809,7 +846,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetString(
|
||||
if (object == nullptr || key == nullptr || value == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -826,7 +863,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, value);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -838,7 +875,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -855,7 +892,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -872,7 +909,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, value);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -889,7 +926,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat32(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -906,7 +943,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat64(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -923,7 +960,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBool(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -941,7 +978,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBinary(
|
||||
if (object == nullptr || key == nullptr || (value == nullptr && value_size != 0)) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
LOGF("\t object = 0x%016" PRIx64 "\n"
|
||||
"\t key = %s\n"
|
||||
@@ -962,7 +999,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetObject(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
if (value_ptr != nullptr) {
|
||||
*value_ptr =
|
||||
@@ -996,7 +1033,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetArray(
|
||||
if (object == nullptr || key == nullptr) {
|
||||
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
object->SetTrophyId(key, std::nullopt);
|
||||
object->SetInteger(key, std::nullopt);
|
||||
|
||||
if (value_ptr != nullptr) {
|
||||
*value_ptr = (value != nullptr ? const_cast<NpUniversalDataSystemEventPropertyArray*>(value)
|
||||
|
||||
@@ -28,6 +28,46 @@ void CheckConcurrentResult(int result, const char* text) {
|
||||
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
|
||||
}
|
||||
|
||||
void TestLevelUserEventDelivery() {
|
||||
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
||||
Check(EventQueue::KernelCreateEqueue(&queue, "level-user-events") == OK, "create user event queue");
|
||||
Check(EventQueue::KernelAddUserEvent(queue, 1) == OK &&
|
||||
EventQueue::KernelAddUserEvent(queue, 2) == OK &&
|
||||
EventQueue::KernelAddUserEventEdge(queue, 3) == OK, "add level and edge events");
|
||||
for (int id = 1; id <= 3; ++id)
|
||||
Check(EventQueue::KernelTriggerUserEvent(queue, id, reinterpret_cast<void*>(uintptr_t(id))) == OK,
|
||||
"trigger distinct user events");
|
||||
EventQueue::KernelEvent events[16] {};
|
||||
Libs::LibKernel::KernelUseconds timeout = 0;
|
||||
int out = 0;
|
||||
const auto find_event = [&](uintptr_t id) {
|
||||
return std::find_if(events, events + out, [=](const auto& event) { return event.ident == id; });
|
||||
};
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 3,
|
||||
"level events are returned once per wait without starving following events");
|
||||
for (uintptr_t id = 1; id <= 3; ++id) {
|
||||
const auto* event = find_event(id);
|
||||
Check(event != events + out && event->data == static_cast<intptr_t>(id) &&
|
||||
event->udata == reinterpret_cast<void*>(id), "distinct event payloads");
|
||||
}
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 2 &&
|
||||
find_event(1) != events + out && find_event(2) != events + out,
|
||||
"level persists while edge clears");
|
||||
Check(EventQueue::KernelDeleteUserEvent(queue, 1) == OK, "delete first level event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].ident == 2, "remaining level event is delivered only once");
|
||||
Check(EventQueue::KernelDeleteUserEvent(queue, 2) == OK, "delete remaining level event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
|
||||
"deleted levels and consumed edge do not reappear");
|
||||
Check(EventQueue::KernelTriggerUserEvent(queue, 3, nullptr) == OK, "retrigger edge event");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].ident == 3, "retriggered edge is delivered once");
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
|
||||
"retriggered edge clears after delivery");
|
||||
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete user event queue");
|
||||
}
|
||||
|
||||
void TestPeriodicTimerEvents() {
|
||||
using namespace std::chrono_literals;
|
||||
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
|
||||
@@ -188,6 +228,12 @@ void TestDuplicateAddPreservesEventState() {
|
||||
"duplicate add updates deadline metadata");
|
||||
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
|
||||
"deadline trigger retains updated duplicate metadata");
|
||||
for (uintptr_t value: {0x9abcu, 0xdef0u})
|
||||
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
|
||||
reinterpret_cast<void*>(value)) == OK, "queue bounded read payload");
|
||||
for (intptr_t value: {0x9abcu, 0xdef0u})
|
||||
Check(EventQueue::KernelWaitEqueue(queue, events, 1, &out, &timeout) == OK && out == 1 &&
|
||||
events[0].data == value, "capacity-one reads preserve pending event order");
|
||||
|
||||
auto retained_owner = weak_original.lock();
|
||||
Check(retained_owner != nullptr, "original owner alive before delete");
|
||||
@@ -495,6 +541,7 @@ void TestConcurrentDelete() {
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestLevelUserEventDelivery();
|
||||
TestPeriodicTimerEvents();
|
||||
TestDuplicateAddPreservesEventState();
|
||||
TestCallbackStateOutlivesPort();
|
||||
|
||||
+289
-352
@@ -206,10 +206,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
|
||||
}
|
||||
|
||||
std::unique_ptr<Fixture>
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
bool memory_backed_material = false, uint32_t member_offset = 0,
|
||||
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
|
||||
uint32_t table_stride = 32, uint32_t table_offset = 0) {
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
|
||||
uint32_t table_shader_offset = 0, bool nested = false) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> material_words;
|
||||
std::array<Value, 4> heap_words;
|
||||
@@ -217,54 +215,38 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
material_words[dword] = fixture->UserData(dword);
|
||||
heap_words[dword] = fixture->UserData(dword + 4u);
|
||||
}
|
||||
if (memory_backed_material) {
|
||||
const auto pointer_address =
|
||||
fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0);
|
||||
MemoryInfo pointer_word;
|
||||
pointer_word.kind = ResourceKind::ScalarAddress;
|
||||
const auto pointer =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{pointer_address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(pointer_word, 0x10b0));
|
||||
const auto address = fixture->Address(pointer, Value(0u), 0x10c0);
|
||||
MemoryInfo descriptor_word;
|
||||
descriptor_word.kind = ResourceKind::ScalarAddress;
|
||||
material_words[0] =
|
||||
fixture->Emit(ValueOpcode::LoadAddressU32,
|
||||
{address, Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(descriptor_word, 0x10c0));
|
||||
}
|
||||
const auto material = fixture->Buffer(material_words, 0x10d8);
|
||||
const auto heap = fixture->Buffer(heap_words, 0x10d8);
|
||||
if (memory_backed_material) {
|
||||
MemoryInfo shared_buffer;
|
||||
shared_buffer.kind = ResourceKind::Buffer;
|
||||
const auto load =
|
||||
fixture->Emit(ValueOpcode::LoadBufferU32,
|
||||
{material, Value(0u), Value(0u), Value(0u), Value(true)},
|
||||
fixture->AddMemory(shared_buffer, 0x10d8));
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {load});
|
||||
}
|
||||
const auto invocation = fixture->Emit(
|
||||
ValueOpcode::GetBuiltin,
|
||||
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
|
||||
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
|
||||
{invocation, Value(true)});
|
||||
const auto record =
|
||||
fixture->Emit(ValueOpcode::IMul32, {selector, Value(material_stride)});
|
||||
const auto member = member_offset == 0 ? record :
|
||||
fixture->Emit(ValueOpcode::IAdd32, {record, Value(member_offset)});
|
||||
Value member;
|
||||
if (nested) {
|
||||
std::array<Value, 4> root_words;
|
||||
for (uint32_t word = 0; word < 4u; ++word) root_words[word] = fixture->UserData(12u + word);
|
||||
const auto root = fixture->Buffer(root_words);
|
||||
MemoryInfo root_memory;
|
||||
root_memory.kind = ResourceKind::ScalarBuffer;
|
||||
const auto root_offset = fixture->Emit(ValueOpcode::IMul32, {selector, Value(32u)});
|
||||
const auto pointer = fixture->Emit(ValueOpcode::ReadConstBuffer, {root, root_offset},
|
||||
fixture->AddMemory(root_memory, 0x1000));
|
||||
const auto masked = fixture->Emit(ValueOpcode::BitwiseAnd32, {pointer, Value(0xfffffff1u)});
|
||||
member = fixture->Emit(ValueOpcode::IAdd32, {masked, Value(16u)});
|
||||
} else {
|
||||
member = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
|
||||
}
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {member});
|
||||
MemoryInfo material_scalar;
|
||||
material_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
material_scalar.offset = material_immediate;
|
||||
auto key =
|
||||
material_scalar.offset = 4u;
|
||||
const auto key =
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
|
||||
fixture->AddMemory(material_scalar, 0x10d8));
|
||||
if (selector_bits != UINT32_MAX)
|
||||
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
|
||||
const auto heap_offset =
|
||||
fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
if (table_shader_offset != 0u)
|
||||
heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)});
|
||||
std::array<Value, 8> image_words;
|
||||
MemoryInfo heap_scalar;
|
||||
heap_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
@@ -278,6 +260,10 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset},
|
||||
fixture->AddMemory(component, 0x10d8));
|
||||
}
|
||||
if (nested) {
|
||||
const auto field = fixture->Emit(ValueOpcode::BitwiseAnd32, {image_words[4], Value(7u)});
|
||||
fixture->Emit(ValueOpcode::ReferenceU32, {field});
|
||||
}
|
||||
const auto image = fixture->Image(image_words, 0x10f0);
|
||||
const auto sampler =
|
||||
fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}, 0x10f0);
|
||||
@@ -294,331 +280,280 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
}
|
||||
|
||||
void TestInvariantIndirectImageMaterialization() {
|
||||
auto fixture = MakeIndirectImageFixture(false);
|
||||
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto resource_plan = ExtractResourcePlan(fixture->program);
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
EliminateDeadCode(fixture->program.blocks);
|
||||
ValidateProgram(fixture->program, true);
|
||||
|
||||
Check(fixture->program.info.buffers.size() == 1 &&
|
||||
fixture->program.info.images.size() == 1 &&
|
||||
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
|
||||
}),
|
||||
"indirect image key was not retained as a scalar-buffer read");
|
||||
const auto source = fixture->program.info.images[0].source;
|
||||
Check(source < fixture->program.descriptor_sources.size() &&
|
||||
fixture->program.descriptor_sources[source]
|
||||
.indirect_descriptor.has_value(),
|
||||
"indirect image source was not retained for runtime proof");
|
||||
const auto image_handle =
|
||||
std::ranges::find_if(*fixture->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
Check(image_handle != fixture->block->end() &&
|
||||
image_handle->Arg(0).ResolveInstruction() != nullptr &&
|
||||
image_handle->Arg(0).ResolveInstruction()->GetOpcode() ==
|
||||
ValueOpcode::ReadConstBuffer,
|
||||
"indirect image handle discarded the live material key");
|
||||
|
||||
std::array<uint32_t, 9> user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u,
|
||||
16u << 16u, 4u, 0u, 7u};
|
||||
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
|
||||
Check(!source.selector && fixture->program.info.buffers.size() == 3u,
|
||||
"bounded image table retained CPU selector traversal or lost live loads");
|
||||
std::array<uint32_t, 8> descriptor{};
|
||||
descriptor[0] = 0x20u;
|
||||
descriptor[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
|
||||
descriptor[2] = 3u | (3u << 14u);
|
||||
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
|
||||
std::array<uint32_t, 16> data{0x3000u, 16u << 16u, 0x200000u, 0u,
|
||||
0x2000u, 48u << 16u, 3u, 0u};
|
||||
std::copy_n(data.begin(), 4u, data.begin() + 12u);
|
||||
LinearTestMemory memory;
|
||||
std::array<uint32_t, 8> image_descriptor{};
|
||||
image_descriptor[0] = 0x20u;
|
||||
image_descriptor[1] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
|
||||
<< 20u;
|
||||
image_descriptor[2] = 3u | (3u << 14u);
|
||||
image_descriptor[3] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
|
||||
<< 28u);
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
|
||||
SrtRuntime runtime{.user_data = user_data,
|
||||
.userdata = &memory,
|
||||
memory.fail_address = 0x3000u; // GPU-only material backing must never be read on the CPU.
|
||||
memory.watched_address = 0x2000u;
|
||||
const auto put = [&](uint32_t offset, uint32_t identity) {
|
||||
auto value = descriptor;
|
||||
value[0] += identity;
|
||||
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1000u + offset) / 4u);
|
||||
};
|
||||
put(16u, 0u);
|
||||
put(64u, 1u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"invariant indirect image table did not materialize");
|
||||
|
||||
user_data[5] = 0u;
|
||||
user_data[6] = 19u;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u && memory.watched_reads == 1u &&
|
||||
memory.watched_dwords == 36u && memory.reads == 1u,
|
||||
"image table was not read once independently of its GPU selector chain");
|
||||
const auto root = specialization.images[0];
|
||||
const auto mapping = root.indirect_mapping_offset;
|
||||
Check(root.indirect_search_iterations == 0u && snapshot.flattened_srt[mapping] == 17u &&
|
||||
snapshot.flattened_srt[mapping + 1u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 5u] == 1u &&
|
||||
snapshot.flattened_srt[mapping + 17u] == 2u &&
|
||||
snapshot.specialization_reads == std::vector<std::pair<uint64_t, uint64_t>>{{0x2000u, 144u}},
|
||||
"dense image mapping lost its null slot, trimmed length or coherent read dependency");
|
||||
const auto original_specialization = specialization;
|
||||
std::fill_n(memory.words.begin() + 0x1040u / 4u, 8u, 0u);
|
||||
put(112u, 1u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == original_specialization && memory.watched_reads == 2u &&
|
||||
snapshot.flattened_srt[mapping] == 29u,
|
||||
"moving a table entry changed shader specialization or reused stale mapping bytes");
|
||||
put(112u, 7u);
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images[2].dwords[0] == descriptor[0] + 7u,
|
||||
"changed table descriptor was not refreshed");
|
||||
// Scalar reads align each DWORD and return zero beyond the V# byte size.
|
||||
data[5] = 0u;
|
||||
data[6] = 19u;
|
||||
std::fill(memory.words.begin() + 0x1000u / 4u, memory.words.end(), 0u);
|
||||
put(0u, 0u);
|
||||
memory.fail_address = 0x2010u;
|
||||
memory.watched_address = 0x2000u;
|
||||
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u &&
|
||||
std::equal(image_descriptor.begin(), image_descriptor.end(),
|
||||
snapshot.images[0].dwords.begin()),
|
||||
"partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail");
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
memory.watched_dwords == 4u && snapshot.images.size() == 2u &&
|
||||
snapshot.images[1].dwords == descriptor,
|
||||
"partial descriptor prefix did not zero its out-of-bounds DWORDs");
|
||||
memory.fail_address = 0x2008u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization),
|
||||
"unreadable memory inside the descriptor prefix was accepted");
|
||||
user_data[5] = 16u << 16u;
|
||||
user_data[6] = 4u;
|
||||
memory.watched_address = UINT64_MAX;
|
||||
|
||||
memory.fail_address = 0x1004u;
|
||||
Check(!MaterializeResources(resource_plan, runtime, snapshot,
|
||||
specialization),
|
||||
"unreadable material-table selector was accepted");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unreadable in-range table bytes were accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] = 0u;
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] = 0u;
|
||||
for (uint32_t size : {0u, 3u, 4u}) {
|
||||
data[6] = size;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage &&
|
||||
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"empty image table did not specialize as a safe null image");
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1];
|
||||
memory.words[(0x2020u - memory.base) / 4u + 3u] =
|
||||
image_descriptor[3] ^ (1u << 28u);
|
||||
ResourceSnapshot null_snapshot;
|
||||
ResourceSpecialization null_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, null_snapshot,
|
||||
null_specialization) &&
|
||||
std::ranges::all_of(null_snapshot.images[0].dwords,
|
||||
[](uint32_t dword) { return dword == 0u; }),
|
||||
"stale typed null image descriptors were not canonicalized");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
// The GPU computes these offsets; host materialization must not enumerate the key.
|
||||
for (const auto [shader, immediate, key, expected] :
|
||||
{std::array{16u, 0u, 0x55555555u, 0u},
|
||||
std::array{0u, 16u, 0x55555555u, UINT32_MAX},
|
||||
std::array{0xfffffff0u, 32u, 0u, UINT32_MAX}}) {
|
||||
auto wrap = MakeIndirectImageFixture(false, 48u, immediate, shader);
|
||||
wrap->PlanAndTrack();
|
||||
for (auto &inst : *wrap->block) {
|
||||
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
|
||||
inst.ReplaceUsesWith(Value(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
SrtWalker walker(wrap->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
|
||||
}
|
||||
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
|
||||
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
|
||||
ResourceSnapshot dynamic_snapshot;
|
||||
ResourceSpecialization dynamic_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot,
|
||||
dynamic_specialization) &&
|
||||
dynamic_snapshot.images.size() == 2 &&
|
||||
dynamic_specialization.images.size() == 2,
|
||||
"dynamic indirect image table did not materialize");
|
||||
const auto second_image = (0x2020u - memory.base) / 4u;
|
||||
memory.words[second_image + 1u] |= 3u << 30u;
|
||||
memory.words[second_image + 2u] = 3u << 14u;
|
||||
memory.words[second_image + 3u] =
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) |
|
||||
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
|
||||
memory.words[second_image + 4u] = 11u;
|
||||
ResourceSnapshot mixed_snapshot;
|
||||
ResourceSpecialization mixed_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization) &&
|
||||
mixed_snapshot.images.size() == 2 &&
|
||||
mixed_specialization.images.size() == 2 &&
|
||||
mixed_specialization.images[0].dimension ==
|
||||
Decoder::ImageDimension::Dim2D &&
|
||||
!mixed_specialization.images[0].cube &&
|
||||
mixed_specialization.images[1].dimension ==
|
||||
Decoder::ImageDimension::Dim2DArray &&
|
||||
mixed_specialization.images[1].cube &&
|
||||
std::equal(mixed_snapshot.images[1].dwords.begin(),
|
||||
mixed_snapshot.images[1].dwords.end(),
|
||||
memory.words.begin() + second_image),
|
||||
"mixed 2D and cube candidates were rejected or discarded");
|
||||
memory.words[second_image + 1u] =
|
||||
static_cast<uint32_t>(
|
||||
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
|
||||
<< 20u |
|
||||
(3u << 30u);
|
||||
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
|
||||
mixed_specialization),
|
||||
"indirect images with different numeric classes were accepted");
|
||||
for (const auto dword : {1u, 2u, 3u, 4u}) {
|
||||
memory.words[second_image + dword] = image_descriptor[dword];
|
||||
}
|
||||
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
|
||||
Check(fixture->program.info.images.size() == 2 &&
|
||||
fixture->program.info.images[0].indirect_root == 0 &&
|
||||
fixture->program.info.images[0].indirect_search_iterations != 0 &&
|
||||
fixture->program.info.images[0].indirect_resources.size() == 2 &&
|
||||
dynamic_snapshot.images.size() == 2,
|
||||
"dynamic indirect image table was not specialized transactionally");
|
||||
const auto &mapping = dynamic_specialization.images[0];
|
||||
const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset];
|
||||
Check(mapping.indirect_search_iterations == std::bit_width(key_count) &&
|
||||
mapping.indirect_mapping_offset + 1u + key_count * 2u ==
|
||||
dynamic_snapshot.flattened_srt.size(),
|
||||
"indirect image mapping retained worst-case padding");
|
||||
|
||||
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2000u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
memory.words[(0x2020u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] += 0x101u;
|
||||
ResourceSnapshot rebound_snapshot;
|
||||
ResourceSpecialization rebound_specialization;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization == dynamic_specialization,
|
||||
"stable indirect key mapping did not accept changed image addresses");
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u];
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != dynamic_specialization,
|
||||
"collapsed indirect candidates did not select a new specialization");
|
||||
const auto collapsed_specialization = rebound_specialization;
|
||||
ResourceSnapshot capacity_snapshot;
|
||||
ResourceSpecialization capacity_specialization;
|
||||
for (const uint32_t records : {1u, 3u}) {
|
||||
user_data[2] = records;
|
||||
Check(MaterializeResources(resource_plan, runtime, capacity_snapshot,
|
||||
capacity_specialization),
|
||||
"runtime indirect key mapping rejected a valid material-table size");
|
||||
}
|
||||
user_data[2] = 2u;
|
||||
memory.words[(0x2020u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 1u;
|
||||
memory.words[(0x2040u - memory.base) / 4u] =
|
||||
memory.words[(0x2000u - memory.base) / 4u] + 2u;
|
||||
for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) {
|
||||
memory.words[(0x2040u - memory.base) / 4u + dword] =
|
||||
image_descriptor[dword];
|
||||
}
|
||||
memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u;
|
||||
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
|
||||
rebound_specialization) &&
|
||||
rebound_specialization != collapsed_specialization,
|
||||
"larger indirect candidate topology reused the old specialization");
|
||||
|
||||
auto memory_backed = MakeIndirectImageFixture(false, 4u, true);
|
||||
memory_backed->PlanAndTrack();
|
||||
auto memory_backed_plan = ExtractResourcePlan(memory_backed->program);
|
||||
EliminateDeadCode(memory_backed->program.blocks);
|
||||
std::array<uint32_t, 11> memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u,
|
||||
0x2000u, 16u << 16u, 4u, 0u,
|
||||
7u, 0x3100u, 0u};
|
||||
memory.words[(0x3100u - memory.base) / 4u] = 0x3000u;
|
||||
memory.words[(0x3000u - memory.base) / 4u] = 0x1000u;
|
||||
memory.fail_address = 0x3100u;
|
||||
SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory =
|
||||
ReadLinearTestMemory};
|
||||
Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization),
|
||||
"unreadable indirect table descriptor was accepted");
|
||||
memory.fail_address = UINT64_MAX;
|
||||
|
||||
memory.watched_address = 0x3100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u,
|
||||
"descriptor, flattened SRT and indirect-table roots repeated a clean pointer read");
|
||||
const auto* buffer_storage = snapshot.buffers.data();
|
||||
const auto* image_storage = snapshot.images.data();
|
||||
const auto* flat_storage = snapshot.flattened_srt.data();
|
||||
const auto* user_data_storage = snapshot.user_data.data();
|
||||
const auto* buffer_specialization_storage = specialization.buffers.data();
|
||||
const auto* image_specialization_storage = specialization.images.data();
|
||||
const auto old_address = snapshot.images[0].dwords[0];
|
||||
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
|
||||
memory.watched_reads = 0;
|
||||
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
|
||||
snapshot, specialization) && memory.watched_reads == 1u &&
|
||||
snapshot.images[0].dwords[0] == old_address + 0x100u,
|
||||
"cache refresh reused stale table contents or repeated its clean pointer read");
|
||||
Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage &&
|
||||
snapshot.flattened_srt.data() == flat_storage &&
|
||||
snapshot.user_data.data() == user_data_storage &&
|
||||
specialization.buffers.data() == buffer_specialization_storage &&
|
||||
specialization.images.data() == image_specialization_storage,
|
||||
"a same-shape refresh discarded the runtime output storage");
|
||||
|
||||
auto malformed = MakeIndirectImageFixture(true);
|
||||
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"malformed indirect image pattern was accepted");
|
||||
Check(!malformed->program.resource_tracking_complete &&
|
||||
malformed->program.info.images.empty() &&
|
||||
malformed->program.descriptor_sources.empty(),
|
||||
"malformed indirect image pattern was partially accepted");
|
||||
"noncontiguous descriptor DWORDs entered the table plan");
|
||||
Check(!malformed->program.resource_tracking_complete && malformed->program.descriptor_sources.empty(),
|
||||
"malformed image table tracking was not transactional");
|
||||
}
|
||||
|
||||
auto negative_immediate = MakeIndirectImageFixture(false, 0xfffffffcu);
|
||||
CheckFatal([&] { negative_immediate->PlanAndTrack(); },
|
||||
"not a valid runtime value",
|
||||
"negative scalar immediate entered the indirect image proof");
|
||||
|
||||
for (const auto [immediate, member, first, stride] :
|
||||
{std::array{0u, 4u, 4u, 224u}, std::array{4u, 4u, 8u, 224u},
|
||||
std::array{36u, 0u, 36u, 224u}, std::array{1u, 3u, 0u, 224u},
|
||||
std::array{1u, 3u, 0u, 1u}, std::array{1u, 3u, 0u, 2u}}) {
|
||||
auto split_offset = MakeIndirectImageFixture(false, immediate, false, member, stride);
|
||||
split_offset->PlanAndTrack();
|
||||
const auto split_plan = ExtractResourcePlan(split_offset->program);
|
||||
LinearTestMemory split_memory;
|
||||
std::copy(image_descriptor.begin(), image_descriptor.end(),
|
||||
split_memory.words.begin() + 0x1000u / 4u);
|
||||
split_memory.fail_address = first == 36u ? 0x1004u : UINT64_MAX;
|
||||
split_memory.watched_address = split_memory.base + first;
|
||||
user_data[1] = stride << 16u;
|
||||
user_data[2] = stride < 4u ? 8u / stride : 1u;
|
||||
SrtRuntime split_runtime{.user_data = user_data,
|
||||
.userdata = &split_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(split_plan, split_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1 &&
|
||||
snapshot.images[0].dwords == image_descriptor &&
|
||||
split_memory.watched_reads == 1u,
|
||||
"indirect scalar offsets did not align and bound their components independently");
|
||||
void TestBoundedImageViewEligibility() {
|
||||
using Type = Libs::Graphics::Prospero::ImageType;
|
||||
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
|
||||
fixture->PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
std::array<uint32_t, 8> data{0x3000u, 16u << 16u, 1u, 0u,
|
||||
0x2000u, 48u << 16u, 5u, 0u};
|
||||
LinearTestMemory memory;
|
||||
memory.fail_address = 0x3000u;
|
||||
const auto descriptor = [](uint32_t identity, Type type) {
|
||||
return std::array<uint32_t, 8>{identity, 75u << 20u, 3u | (3u << 14u),
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast<uint32_t>(type) << 28u),
|
||||
0u, 0u, 0u, 0u};
|
||||
};
|
||||
const std::array types{Type::kColor2D, Type::kCube, Type::kColor3D,
|
||||
Type::kColor2DArray, Type::kColor2D};
|
||||
for (uint32_t row = 0; row < types.size(); ++row) {
|
||||
const auto value = descriptor(0x20u + row, types[row]);
|
||||
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1010u + row * 48u) / 4u);
|
||||
}
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 4u && memory.reads == 1u,
|
||||
"unrelated cube/3D heap entries prevented a typed 2D table view");
|
||||
const auto mapping = specialization.images[0].indirect_mapping_offset;
|
||||
Check(snapshot.flattened_srt[mapping] == 53u &&
|
||||
snapshot.flattened_srt[mapping + 5u] == 1u &&
|
||||
snapshot.flattened_srt[mapping + 17u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 29u] == 0u &&
|
||||
snapshot.flattened_srt[mapping + 41u] == 2u &&
|
||||
snapshot.flattened_srt[mapping + 53u] == 3u &&
|
||||
snapshot.images[1].dwords == descriptor(0x20u, Type::kColor2D) &&
|
||||
snapshot.images[2].dwords == descriptor(0x23u, Type::kColor2DArray) &&
|
||||
snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D),
|
||||
"typed table eligibility changed byte offsets or discarded compatible views");
|
||||
|
||||
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
|
||||
// each 64-byte material record; the image occupies bytes 16..47 of its record.
|
||||
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
|
||||
packed->PlanAndTrack();
|
||||
const auto packed_plan = ExtractResourcePlan(packed->program);
|
||||
LinearTestMemory packed_memory;
|
||||
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
|
||||
0x2000u, 48u << 16u, 2u, 0u};
|
||||
for (uint32_t i = 0; i < 3u; ++i)
|
||||
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
auto descriptor = image_descriptor;
|
||||
descriptor[0] += i;
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
|
||||
// An explicitly selected descriptor is not an unrelated entry in a broad heap.
|
||||
const auto image_source = plan.info.images[0].source;
|
||||
std::vector<uint32_t> selected;
|
||||
for (auto type : {Type::kColor2D, Type::kColor3D}) {
|
||||
DescriptorSource source;
|
||||
source.dword_count = 8u;
|
||||
const auto value = descriptor(0x30u, type);
|
||||
for (uint32_t word = 0; word < value.size(); ++word) source.dwords[word] = Value(value[word]);
|
||||
selected.push_back(static_cast<uint32_t>(plan.descriptor_sources.size()));
|
||||
plan.descriptor_sources.push_back(source);
|
||||
}
|
||||
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
|
||||
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
|
||||
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
|
||||
"packed material flags changed the table key or 48-byte descriptor addressing");
|
||||
packed_memory.words[60u / 4u] = 0xffff8002u;
|
||||
packed_memory.fail_address = 0x2070u;
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u &&
|
||||
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"masked out-of-range descriptor did not return zero without reading past table bounds");
|
||||
packed_memory.fail_address = 0x205cu;
|
||||
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
|
||||
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
|
||||
auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
|
||||
UINT32_MAX, 48u, 16u);
|
||||
CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"unbounded table offset conflated shader U32 wrap with scalar immediate addition");
|
||||
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"explicitly selected incompatible descriptor was silently normalized to null");
|
||||
}
|
||||
|
||||
void TestWaterfallImageTable() {
|
||||
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
|
||||
const auto make = [](bool equality, bool scalar) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
auto *entry = fixture->block;
|
||||
auto *body = fixture->AddBlock();
|
||||
auto *exit = fixture->AddBlock();
|
||||
entry->AddBranch(body);
|
||||
entry->AddBranch(exit);
|
||||
body->AddBranch(exit);
|
||||
std::array<Value, 4> words;
|
||||
for (uint32_t i = 0; i < 4u; ++i) words[i] = fixture->UserData(i);
|
||||
const auto table = fixture->Buffer(words);
|
||||
const auto lane_value = fixture->Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)});
|
||||
const auto active = fixture->Emit(ValueOpcode::INotEqual32, {lane_value, Value(0u)});
|
||||
const auto index = fixture->UserData(4u);
|
||||
std::array<Value, 8> image_words;
|
||||
auto guard = active;
|
||||
for (uint32_t group = 0; group < 2u; ++group) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Buffer;
|
||||
memory.idxen = true;
|
||||
memory.data_dwords = 4u;
|
||||
memory.offset = 16u + group * 16u;
|
||||
const auto loaded = fixture->Emit(ValueOpcode::LoadBufferU32x4,
|
||||
{table, index, Value(0u), Value(0u), active}, fixture->AddMemory(memory, 0x850u));
|
||||
for (uint32_t component = 0; component < 4u; ++component) {
|
||||
const auto word = group * 4u + component;
|
||||
const auto local = fixture->Emit(ValueOpcode::SelectU32,
|
||||
{active, fixture->Emit(ValueOpcode::CompositeExtractU32x4,
|
||||
{loaded, Value(component)}), Value(0xdeadu)});
|
||||
image_words[word] = fixture->Emit(ValueOpcode::ReadLane, {local, Value(0u)});
|
||||
if (equality || word != 7u)
|
||||
guard = fixture->Emit(ValueOpcode::LogicalAnd,
|
||||
{guard, fixture->Emit(ValueOpcode::IEqual32, {image_words[word], local})});
|
||||
}
|
||||
}
|
||||
fixture->program.block_info[0].condition = guard;
|
||||
fixture->program.block_info[0].terminator = {
|
||||
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u};
|
||||
const auto sample = [&](Value image, Block *block) {
|
||||
const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::Image;
|
||||
memory.image_dimension = Decoder::ImageDimension::Dim2D;
|
||||
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
|
||||
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
|
||||
fixture->Emit(ValueOpcode::ReferenceU32,
|
||||
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
|
||||
};
|
||||
if (scalar) {
|
||||
const auto key = fixture->Emit(ValueOpcode::ReadFirstLane, {lane_value, active});
|
||||
const auto offset = fixture->Emit(ValueOpcode::IAdd32,
|
||||
{fixture->Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(16u)});
|
||||
std::array<Value, 8> scalar_words;
|
||||
for (uint32_t word = 0; word < 8u; ++word) {
|
||||
MemoryInfo memory;
|
||||
memory.kind = ResourceKind::ScalarBuffer;
|
||||
memory.offset = word * 4u;
|
||||
scalar_words[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
|
||||
fixture->AddMemory(memory, 0x7bcu));
|
||||
}
|
||||
sample(fixture->Image(scalar_words), entry);
|
||||
}
|
||||
const auto image = fixture->Emit(ValueOpcode::GetImageResource,
|
||||
{image_words[0], image_words[1], image_words[2], image_words[3],
|
||||
image_words[4], image_words[5], image_words[6], image_words[7]}, 0, body);
|
||||
sample(image, body);
|
||||
return fixture;
|
||||
};
|
||||
auto fixture = make(true, true);
|
||||
fixture->PlanAndTrack();
|
||||
auto plan = ExtractResourcePlan(fixture->program);
|
||||
Check(plan.info.images.size() == 1u &&
|
||||
plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor->table_record_bytes == 48u,
|
||||
"scalar and waterfall uses did not share their native image table");
|
||||
std::array<uint32_t, 5> data{0x2000u, 48u << 16u, 2u, 0x5204u, 1u};
|
||||
LinearTestMemory memory;
|
||||
const std::array<uint32_t, 8> descriptor{0x20u, 75u << 20u, 3u | (3u << 14u),
|
||||
Libs::Graphics::DstSel(4, 5, 6, 7) | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
|
||||
SrtRuntime runtime{.user_data = data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1u &&
|
||||
snapshot.images.size() == 2u,
|
||||
"shared scalar/waterfall table repeated its coherent bulk read");
|
||||
const auto handle = std::ranges::find_if(*fixture->program.blocks[1], [](const Inst &inst) {
|
||||
return inst.GetOpcode() == ValueOpcode::GetImageResource;
|
||||
});
|
||||
for (const auto [index, format, expected] :
|
||||
{std::array{1u, 0x5204u, 64u}, std::array{2u, 0x5204u, UINT32_MAX},
|
||||
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
|
||||
data[4] = index;
|
||||
data[3] = format;
|
||||
SrtWalker walker(fixture->program, runtime);
|
||||
uint32_t offset = 0;
|
||||
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
|
||||
"VMEM table projection lost record bounds, format disable or multiplication carry");
|
||||
}
|
||||
auto disabled = make(true, false);
|
||||
disabled->PlanAndTrack();
|
||||
const auto disabled_plan = ExtractResourcePlan(disabled->program);
|
||||
data[3] = 0u;
|
||||
memory.fail_address = 0x2000u;
|
||||
Check(MaterializeResources(disabled_plan, runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 1u &&
|
||||
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
|
||||
"disabled VMEM descriptor table read its unavailable backing");
|
||||
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"VMEM format disable incorrectly disabled a shared scalar descriptor read");
|
||||
auto malformed = make(false, false);
|
||||
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"waterfall without all descriptor equality witnesses was accepted");
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
@@ -709,8 +644,8 @@ void TestGuardedDirectImageTable() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->material_source == UINT32_MAX &&
|
||||
indirect->selector_stride == 0u && indirect->table_offset == 344u &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 344u &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
indirect->key_count.Resolve().U32() == 32u &&
|
||||
fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u,
|
||||
@@ -1007,7 +942,7 @@ void TestBoundedComputeImageLoop() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->material_source == UINT32_MAX &&
|
||||
Check(indirect && !indirect->selector.has_value() &&
|
||||
indirect->table_offset == 0x6b0u &&
|
||||
indirect->key_count.Resolve() == count.Resolve(),
|
||||
"bounded compute loop lost its runtime image count");
|
||||
@@ -1362,8 +1297,8 @@ void TestUniformizedMaterialImageKeys() {
|
||||
fixture.PlanAndTrack();
|
||||
const auto source = fixture.program.info.images[0].source;
|
||||
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
|
||||
Check(indirect && indirect->selector_stride == 0x90u &&
|
||||
indirect->selector_offset == 0xc00u &&
|
||||
Check(indirect && indirect->selector->stride == 0x90u &&
|
||||
indirect->selector->offset == 0xc00u &&
|
||||
indirect->table_offset == 0x20e0u &&
|
||||
!indirect->selector_mask.IsEmpty() &&
|
||||
indirect->key_count.Resolve().IsImmediate() &&
|
||||
@@ -3637,6 +3572,8 @@ int main() {
|
||||
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
|
||||
Run("dynamic storage mips", TestDynamicStorageMipTracking);
|
||||
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
|
||||
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
|
||||
Run("waterfall image table", TestWaterfallImageTable);
|
||||
Run("guarded direct image table", TestGuardedDirectImageTable);
|
||||
Run("bounded compute image loop", TestBoundedComputeImageLoop);
|
||||
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
|
||||
|
||||
@@ -10161,6 +10161,46 @@ public:
|
||||
|
||||
void CheckRenderExecutorColorMetadataClear() {
|
||||
constexpr const char *name = "RenderExecutorColorMetadataClear";
|
||||
struct PackedCase {
|
||||
vk::Format format;
|
||||
uint32_t word;
|
||||
std::array<float, 4> channels;
|
||||
};
|
||||
const std::array packed_cases{
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x7bff7bffu, {65504.0f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0xbc003c00u, {1.0f, -1.0f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0xffff0001u, {0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x00008001u, {-0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x000003ffu, {1023.0f * 0x1p-24f}},
|
||||
PackedCase{vk::Format::eR16Sfloat, 0x00000400u, {0x1p-14f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0x80000000u, {0.0f, -0.0f}},
|
||||
PackedCase{vk::Format::eR16G16Sfloat, 0xfc007c00u,
|
||||
{std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity()}},
|
||||
PackedCase{vk::Format::eR16Unorm, 0x0000ffffu, {1.0f}},
|
||||
PackedCase{vk::Format::eR16G16Unorm, 0x0000ffffu, {1.0f, 0.0f}},
|
||||
PackedCase{vk::Format::eR16G16Unorm, 0x80000001u,
|
||||
{1.0f / 65535.0f, 32768.0f / 65535.0f}},
|
||||
};
|
||||
for (const auto &test : packed_cases) {
|
||||
vk::ClearColorValue clear{};
|
||||
Require(name, "packed 16-bit clear",
|
||||
DecodePackedColorClear(test.format, test.word, clear) &&
|
||||
std::bit_cast<std::array<uint32_t, 4>>(clear.float32) ==
|
||||
std::bit_cast<std::array<uint32_t, 4>>(test.channels),
|
||||
"16-bit clear lost channel order, sign, range or unused-channel zeros");
|
||||
}
|
||||
vk::ClearColorValue clear{};
|
||||
Require(name, "half NaN clear",
|
||||
DecodePackedColorClear(vk::Format::eR16Sfloat, 0x7e00u, clear) &&
|
||||
std::isnan(clear.float32[0]),
|
||||
"half NaN did not remain NaN");
|
||||
for (const auto format : {vk::Format::eR16Sfloat, vk::Format::eR16Unorm}) {
|
||||
Require(name, "R16 DWORD fill",
|
||||
!DecodeColorDwordFill(format, 0xbc003c00u, clear) &&
|
||||
DecodeColorDwordFill(format, 0x3c003c00u, clear),
|
||||
"alternating R16 pixels were treated as a uniform clear");
|
||||
}
|
||||
constexpr uintptr_t base = 0x0000000204100000ull;
|
||||
constexpr uint64_t allocation_size = 0x200000;
|
||||
constexpr uint64_t allocation_alignment = 0x10000;
|
||||
@@ -10170,6 +10210,9 @@ public:
|
||||
bool reuse_unorm = false;
|
||||
bool cmask = false;
|
||||
bool reuse_mips = false;
|
||||
Prospero::ChannelLayout layout = Prospero::ChannelLayout::k16_16_16_16;
|
||||
Prospero::ChannelType type = Prospero::ChannelType::kFloat;
|
||||
uint32_t clear_word = 0;
|
||||
};
|
||||
constexpr std::array cases{
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}},
|
||||
@@ -10177,6 +10220,16 @@ public:
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}, true},
|
||||
FillCase{0, {0x804020ffu, 0}, false, true},
|
||||
FillCase{0x40404040u, {0, 0x3c000000u}, false, false, true},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x7bff7bffu},
|
||||
.layout = Prospero::ChannelLayout::k16, .clear_word = 0x7bff7bffu},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0xbc003c00u},
|
||||
.layout = Prospero::ChannelLayout::k16_16, .clear_word = 0xbc003c00u},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x80008000u},
|
||||
.layout = Prospero::ChannelLayout::k16,
|
||||
.type = Prospero::ChannelType::kUNorm, .clear_word = 0x80008000u},
|
||||
FillCase{.fill = 0x20202020u, .texel = {0x0000ffffu},
|
||||
.layout = Prospero::ChannelLayout::k16_16,
|
||||
.type = Prospero::ChannelType::kUNorm, .clear_word = 0x0000ffffu},
|
||||
};
|
||||
EnsureRuntimeContext();
|
||||
// Astro's generic metadata fill, through S_ENDPGM; trailing debug data is omitted.
|
||||
@@ -10214,10 +10267,6 @@ public:
|
||||
{.type = Prospero::ShaderBinaryType::kCs,
|
||||
.code_size_bytes = sizeof(native_fill)});
|
||||
}
|
||||
TileSizeAlign dcc_size{};
|
||||
(void)TileGetDccSize(512, 256, 1, 8, 1,
|
||||
Prospero::TileMode::kRenderTarget, dcc_size, 0);
|
||||
|
||||
int64_t direct_offset = -1;
|
||||
Require(name, "direct allocation",
|
||||
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
|
||||
@@ -10234,11 +10283,19 @@ public:
|
||||
std::memset(mapped, 0, allocation_size);
|
||||
|
||||
for (const auto &fill_case : cases) {
|
||||
const auto layout = fill_case.cmask ? Prospero::ChannelLayout::k8_8_8_8 : fill_case.layout;
|
||||
const auto type = fill_case.cmask ? Prospero::ChannelType::kUNorm : fill_case.type;
|
||||
const auto bytes_per_pixel = TextureGetRenderTargetFormat(
|
||||
layout, type, Prospero::ChannelOrder::kStandard).bytes_per_element;
|
||||
const uint32_t probe_width = bytes_per_pixel == 2 ? 2 : 1;
|
||||
TileSizeAlign dcc_size{};
|
||||
(void)TileGetDccSize(512, 256, 1, bytes_per_pixel, 1,
|
||||
Prospero::TileMode::kRenderTarget, dcc_size, 0);
|
||||
const uint64_t dcc_address = base + (fill_case.reuse_mips ? 0x160000 : 0x100000);
|
||||
const uint32_t selected_layer = fill_case.cmask ? 1 : 0;
|
||||
const uint64_t metadata_size = fill_case.cmask ? 0x2000 : dcc_size.size;
|
||||
const std::vector<u32> expected(
|
||||
fill_case.texel.begin(), fill_case.texel.begin() + (fill_case.cmask ? 1 : 2));
|
||||
fill_case.texel.begin(), fill_case.texel.begin() + std::max(bytes_per_pixel / 4u, 1u));
|
||||
RenderContext context(m_runtime_context);
|
||||
HW::Context registers{};
|
||||
HW::UserConfig user_config{};
|
||||
@@ -10251,10 +10308,8 @@ public:
|
||||
registers.SetColorInfo(
|
||||
0, {.cmask_fast_clear_enable = !fill_case.reuse_unorm,
|
||||
.dcc_compression_enable = !fill_case.cmask,
|
||||
.format = fill_case.cmask ? Prospero::ChannelLayout::k8_8_8_8
|
||||
: Prospero::ChannelLayout::k16_16_16_16,
|
||||
.channel_type = fill_case.cmask ? Prospero::ChannelType::kUNorm
|
||||
: Prospero::ChannelType::kFloat,
|
||||
.format = layout,
|
||||
.channel_type = type,
|
||||
.channel_order = Prospero::ChannelOrder::kStandard});
|
||||
registers.SetColorView(0, {.base_array_slice_index = selected_layer,
|
||||
.last_array_slice_index = selected_layer});
|
||||
@@ -10265,7 +10320,7 @@ public:
|
||||
.metadata_pipe_aligned = true});
|
||||
registers.SetColorDccAddr(0, {.addr = dcc_address});
|
||||
registers.SetColorCmask(0, {.addr = dcc_address});
|
||||
registers.SetColorClearWord0(0, {.word0 = fill_case.cmask ? expected[0] : 0});
|
||||
registers.SetColorClearWord0(0, {.word0 = fill_case.cmask ? expected[0] : fill_case.clear_word});
|
||||
registers.SetColorClearWord1(0, {.word1 = 0});
|
||||
registers.SetRenderTargetMask(0x0f);
|
||||
scheduler.Begin(registers, user_config, shaders);
|
||||
@@ -10354,12 +10409,13 @@ public:
|
||||
!texture_cache.IsMeta(dcc_address),
|
||||
"color metadata did not retain its type and full native allocation");
|
||||
const auto read_texel = [&] {
|
||||
return ReadCachedTexel(name, context, color.image_id, {}, {1, 1, 1}, selected_layer);
|
||||
return ReadCachedTexel(name, context, color.image_id, {}, {probe_width, 1, 1}, selected_layer);
|
||||
};
|
||||
Require(name, "GPU color clear value",
|
||||
read_texel() == expected &&
|
||||
ReadCachedTexel(name, context, color.image_id, {511, 255, 0},
|
||||
{1, 1, 1}, selected_layer) == expected,
|
||||
ReadCachedTexel(name, context, color.image_id,
|
||||
{static_cast<int32_t>(512 - probe_width), 255, 0},
|
||||
{probe_width, 1, 1}, selected_layer) == expected,
|
||||
"native metadata did not materialize the expected color");
|
||||
if (fill_case.reuse_mips) {
|
||||
const auto &image = texture_cache.GetImage(color.image_id);
|
||||
@@ -10405,6 +10461,9 @@ public:
|
||||
painted = {0x0000ffffu, 0xffffffffu};
|
||||
} else if (fill_case.cmask) {
|
||||
painted = {0xffff00ffu};
|
||||
} else if (bytes_per_pixel <= 4) {
|
||||
const uint32_t one = type == Prospero::ChannelType::kFloat ? 0x3c00u : 0xffffu;
|
||||
painted = {bytes_per_pixel == 2 ? one | (one << 16u) : one};
|
||||
}
|
||||
const auto retains_painted_texel = [&] {
|
||||
const auto texels = ReadCachedTexel(name, context, color.image_id, {},
|
||||
@@ -11711,7 +11770,8 @@ public:
|
||||
value.dword_count = 4;
|
||||
PreparedBindings buffer_bindings;
|
||||
executor.PrepareBindings(buffer_runtime, buffer_bindings);
|
||||
executor.FindBuffers(buffer_bindings);
|
||||
std::array<PreparedBindings *, 1> buffer_stages{&buffer_bindings};
|
||||
executor.FindBuffers(buffer_stages);
|
||||
const auto original_id = buffer_bindings.buffer_sources[0].id;
|
||||
|
||||
auto &buffer_cache = resources.GetBufferCache();
|
||||
@@ -11744,7 +11804,8 @@ public:
|
||||
ShaderStageRuntime runtime{&buffer_program, &snapshot};
|
||||
PreparedBindings bindings;
|
||||
executor.PrepareBindings(runtime, bindings);
|
||||
executor.FindBuffers(bindings);
|
||||
std::array<PreparedBindings *, 1> stages{&bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(bindings);
|
||||
const auto &owner = resources.GetBufferCache().GetBuffer(bindings.buffer_sources[0].id);
|
||||
Require(name, "halfword buffer base and exact tail",
|
||||
@@ -11755,6 +11816,74 @@ public:
|
||||
"native binding lost the byte adjustment or rounded the final halfword range");
|
||||
}
|
||||
|
||||
{
|
||||
constexpr uint64_t address = base + allocation_size - 0x4000 + 12;
|
||||
constexpr uint64_t mapped_tail = base + allocation_size - address;
|
||||
auto writer = make_buffer_program(ShaderType::Vertex, {.written = true});
|
||||
ShaderRecompiler::IR::CompiledShaderInfo reader{};
|
||||
reader.stage = ShaderType::Pixel;
|
||||
ShaderRecompiler::IR::ResourceSnapshot writer_snapshot, reader_snapshot;
|
||||
ShaderBufferResource descriptor{};
|
||||
descriptor.UpdateAddress48(address);
|
||||
descriptor.fields[2] = UINT32_MAX;
|
||||
auto &value = writer_snapshot.buffers.emplace_back();
|
||||
std::copy_n(descriptor.fields, 4, value.dwords.begin());
|
||||
value.dword_count = 4;
|
||||
ShaderStageRuntime writer_runtime{&writer, &writer_snapshot};
|
||||
ShaderStageRuntime reader_runtime{&reader, &reader_snapshot};
|
||||
PreparedBindings writer_bindings, reader_bindings;
|
||||
const auto check_range = [&](uint64_t expected, const char *description) {
|
||||
executor.PrepareBindings(writer_runtime, writer_bindings);
|
||||
executor.PrepareBindings(reader_runtime, reader_bindings);
|
||||
std::array<PreparedBindings *, 2> stages{&reader_bindings, &writer_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(writer_bindings);
|
||||
const auto &source = writer_bindings.buffer_sources[0];
|
||||
const auto &binding = writer_bindings.buffers[0];
|
||||
const auto &owner = resources.GetBufferCache().GetBuffer(source.id);
|
||||
Require(name, description, source.address == address && source.size == expected &&
|
||||
binding.buffer == owner.Handle() && binding.offset <= owner.Offset(address) &&
|
||||
binding.range == expected + owner.Offset(address) - binding.offset,
|
||||
"descriptor discovery or rebind changed the write boundary or base adjustment");
|
||||
};
|
||||
writer_snapshot.specialization_reads = {{address + 0x500, 4},
|
||||
{address - 0x40, 0x40}, {address + 0x80, 0}};
|
||||
reader_snapshot.specialization_reads = {{address + 0x300, 4}, {address + 0x100, 4}};
|
||||
check_range(0x100, "unbounded write stops before nearest cross-stage read");
|
||||
reader_snapshot.specialization_reads.clear();
|
||||
check_range(0x500, "unbounded write stops before same-stage read");
|
||||
writer_snapshot.specialization_reads = {{address - 0x40, 0x40},
|
||||
{address + mapped_tail + 0x100, 4}};
|
||||
check_range(mapped_tail, "earlier and unmapped reads preserve mapped tail");
|
||||
writer_snapshot.specialization_reads.clear();
|
||||
check_range(mapped_tail, "unbounded write without reads preserves mapped tail");
|
||||
reader_snapshot.specialization_reads = {{address + 0x100, 4}};
|
||||
value.dwords[2] = 0x300;
|
||||
check_range(0x300, "finite write preserves its declared range");
|
||||
value.dwords[2] = UINT32_MAX;
|
||||
writer.info.buffers[0].written = false;
|
||||
writer.info.buffers[0].read = true;
|
||||
check_range(mapped_tail, "read-only sentinel preserves mapped tail");
|
||||
auto &graphics = context.GetGraphics();
|
||||
auto &storage_limit = graphics.physical_device_properties.limits.maxStorageBufferRange;
|
||||
const auto original_limit = storage_limit;
|
||||
storage_limit = 0x2000;
|
||||
value.dwords[1] |= 16u << 16u;
|
||||
check_range(storage_limit - (graphics.StorageMinAlignment() - 1),
|
||||
"read-only structured sentinel reserves native alignment prefix");
|
||||
Require(name, "read-only native range fits device", writer_bindings.buffers[0].range <= storage_limit,
|
||||
"read-only cap omitted the native descriptor alignment prefix");
|
||||
value.dwords[2] = 0x300; // A finite 12 KiB descriptor remains finite despite the mock limit.
|
||||
std::array<PreparedBindings *, 1> stages{&writer_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
Require(name, "finite read exceeds mock cap unchanged", writer_bindings.buffer_sources[0].size == 0x3000,
|
||||
"read-only window changed a finite descriptor's declared range");
|
||||
storage_limit = original_limit;
|
||||
check_range(0x3000, "finite structured read preserves native range");
|
||||
value.dwords[2] = UINT32_MAX;
|
||||
check_range(mapped_tail, "read-only structured sentinel preserves smaller mapping");
|
||||
}
|
||||
|
||||
for (const auto format : {Prospero::BufferFormat::k8UNorm,
|
||||
Prospero::BufferFormat::k16UNorm}) {
|
||||
const uint64_t alias_address = base + 0xe00000 +
|
||||
@@ -14462,7 +14591,8 @@ public:
|
||||
plane_dependency.memoryBarrierCount = 1;
|
||||
plane_dependency.pMemoryBarriers = &plane_barrier;
|
||||
const auto write_plane = [&] {
|
||||
executor.FindBuffers(plane_bindings);
|
||||
std::array<PreparedBindings *, 1> stages{&plane_bindings};
|
||||
executor.FindBuffers(stages);
|
||||
executor.RebindBuffers(plane_bindings);
|
||||
const auto &plane_buffer = plane_bindings.buffers[0];
|
||||
const vk::BufferCopy plane_copy{0, plane_buffer.offset, plane_size};
|
||||
@@ -14879,7 +15009,8 @@ public:
|
||||
const Image *sampled_image = nullptr,
|
||||
const Image *storage_image = nullptr,
|
||||
const Image *storage_image_uint = nullptr,
|
||||
vk::Sampler sampler = nullptr) {
|
||||
vk::Sampler sampler = nullptr,
|
||||
std::span<const Image> sampled_resources = {}) {
|
||||
using Kind = ShaderRecompiler::IR::DescriptorBindingKind;
|
||||
const auto &layout = compiled.program.bindings;
|
||||
auto shader_data = compiled.packed_user_data;
|
||||
@@ -15155,7 +15286,7 @@ public:
|
||||
write.pBufferInfo = &gds_info;
|
||||
writes.push_back(write);
|
||||
}
|
||||
const ShaderRecompiler::IR::DescriptorBinding *sampled = nullptr;
|
||||
std::vector<const ShaderRecompiler::IR::DescriptorBinding *> sampled_bindings;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage = nullptr;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage_uint = nullptr;
|
||||
const ShaderRecompiler::IR::DescriptorBinding *storage_atomic = nullptr;
|
||||
@@ -15168,10 +15299,7 @@ public:
|
||||
const auto &image =
|
||||
compiled.program.info.images.at(binding.resources.front());
|
||||
if (resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled) {
|
||||
Require(test.name, "dispatch", sampled == nullptr,
|
||||
"Vulkan test harness needs separate sampled images for mixed "
|
||||
"descriptor classes");
|
||||
sampled = &binding;
|
||||
sampled_bindings.push_back(&binding);
|
||||
} else if (image.atomic) {
|
||||
storage_atomic = &binding;
|
||||
} else if (image.numeric_class == Prospero::TextureNumericClass::Float) {
|
||||
@@ -15180,32 +15308,41 @@ public:
|
||||
storage_uint = &binding;
|
||||
}
|
||||
}
|
||||
if (sampled != nullptr) {
|
||||
Require(test.name, "dispatch", sampled_image != nullptr,
|
||||
size_t sampled_count = 0;
|
||||
for (const auto *binding : sampled_bindings) sampled_count += binding->resources.size();
|
||||
sampled_infos.reserve(sampled_count);
|
||||
Require(test.name, "dispatch", sampled_resources.empty() ||
|
||||
sampled_resources.size() == compiled.program.info.images.size(),
|
||||
"sampled resources must match the logical images");
|
||||
for (const auto *sampled : sampled_bindings) {
|
||||
Require(test.name, "dispatch", sampled_image != nullptr || !sampled_resources.empty(),
|
||||
"sampled image descriptor requested but no sampled image was "
|
||||
"provided");
|
||||
sampled_infos.resize(sampled->resources.size());
|
||||
const auto first_info = sampled_infos.size();
|
||||
sampled_infos.resize(first_info + sampled->resources.size());
|
||||
std::vector<u32> mip_indices(compiled.program.info.images.size());
|
||||
for (u32 slot = 0; slot < sampled_infos.size(); slot++) {
|
||||
auto &info = sampled_infos[slot];
|
||||
info.imageView = sampled_image->view;
|
||||
info.imageLayout = sampled_image->layout;
|
||||
for (u32 slot = 0; slot < sampled->resources.size(); slot++) {
|
||||
auto &info = sampled_infos[first_info + slot];
|
||||
const auto resource = sampled->resources[slot];
|
||||
const auto *source_image = sampled_resources.empty() ? sampled_image
|
||||
: &sampled_resources[resource];
|
||||
info.imageView = source_image->view;
|
||||
info.imageLayout = source_image->layout;
|
||||
if (compiled.program.info.images[resource].mip_mode ==
|
||||
ShaderRecompiler::IR::ImageMipMode::Dynamic) {
|
||||
const auto mip = test.sampled_image_view_base_mip + mip_indices[resource]++;
|
||||
Require(test.name, "dispatch", mip < sampled_image->mip_levels,
|
||||
Require(test.name, "dispatch", mip < source_image->mip_levels,
|
||||
"sampled mip descriptor exceeds the supplied image");
|
||||
vk::ImageViewCreateInfo view{};
|
||||
view.image = sampled_image->image;
|
||||
view.image = source_image->image;
|
||||
view.viewType = test.sampled_image_view_type;
|
||||
view.format = sampled_image->format;
|
||||
view.format = source_image->format;
|
||||
view.subresourceRange = {
|
||||
vk::ImageAspectFlagBits::eColor, mip, 1,
|
||||
test.sampled_image_view_base_layer,
|
||||
test.sampled_image_view_layers != 0
|
||||
? test.sampled_image_view_layers
|
||||
: sampled_image->layers - test.sampled_image_view_base_layer};
|
||||
: source_image->layers - test.sampled_image_view_base_layer};
|
||||
RequireVk(test.name, "dispatch",
|
||||
m_device.createImageView(&view, nullptr, &info.imageView),
|
||||
"vkCreateImageView(sampled mip)");
|
||||
@@ -15216,9 +15353,9 @@ public:
|
||||
write.sType = vk::StructureType::eWriteDescriptorSet;
|
||||
write.dstSet = descriptor_set;
|
||||
write.dstBinding = Native(sampled->kind);
|
||||
write.descriptorCount = static_cast<u32>(sampled_infos.size());
|
||||
write.descriptorCount = static_cast<u32>(sampled->resources.size());
|
||||
write.descriptorType = vk::DescriptorType::eSampledImage;
|
||||
write.pImageInfo = sampled_infos.data();
|
||||
write.pImageInfo = sampled_infos.data() + first_info;
|
||||
writes.push_back(write);
|
||||
}
|
||||
const auto BindStorage =
|
||||
@@ -15619,6 +15756,12 @@ public:
|
||||
cmd.setDepthTestEnable(depth.depth_test_enable);
|
||||
cmd.setDepthWriteEnable(depth.depth_write_enable);
|
||||
cmd.setDepthCompareOp(depth.depth_compare_op);
|
||||
#if !defined(__APPLE__)
|
||||
cmd.setDepthBoundsTestEnable(depth.depth_bounds_test_enable);
|
||||
if (depth.depth_bounds_test_enable) {
|
||||
cmd.setDepthBounds(registers.GetDepthBoundsMin(), registers.GetDepthBoundsMax());
|
||||
}
|
||||
#endif
|
||||
cmd.setDepthBiasEnable(false);
|
||||
cmd.setStencilTestEnable(depth.stencil_test_enable);
|
||||
if (depth.stencil_test_enable) {
|
||||
@@ -15675,6 +15818,26 @@ public:
|
||||
"depth feedback lost a prior write or the read-only draw changed depth");
|
||||
}
|
||||
}
|
||||
depth.depth_test_enable = false;
|
||||
for (uint32_t pass = 0; pass < 3; pass++) {
|
||||
depth.depth_bounds_test_enable = pass != 2;
|
||||
registers.SetDepthBoundsMin(pass == 0 ? 0.5f : 0.625f);
|
||||
registers.SetDepthBoundsMax(pass == 0 ? 0.5625f : 0.6875f);
|
||||
Require(name, "dynamic depth bounds pipeline reuse",
|
||||
pipeline(true, 2, 2).pipeline == filled.pipeline,
|
||||
"changing depth bounds or their enable state created another pipeline");
|
||||
draw(filled);
|
||||
const auto bounded_pixels = read_color();
|
||||
for (size_t index = 0; index < stored_depth.size(); index++) {
|
||||
const auto z = std::bit_cast<float>(stored_depth[index]);
|
||||
const bool passes = !depth.depth_bounds_test_enable ||
|
||||
(z >= registers.GetDepthBoundsMin() &&
|
||||
z <= registers.GetDepthBoundsMax());
|
||||
Require(name, "dynamic stored-depth bounds",
|
||||
bounded_pixels[index * 4] == (passes ? stored_depth[index] : 0u),
|
||||
"depth bounds did not filter stored depth independently of the depth test");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto &wireframe = pipeline(true, 1, 1);
|
||||
Require(name, "pipeline cache", filled.pipeline != wireframe.pipeline &&
|
||||
@@ -17876,6 +18039,7 @@ private:
|
||||
"vertex layer output is not supported");
|
||||
m_rasterization_supported = available_features.fillModeNonSolid &&
|
||||
available_features.tessellationShader &&
|
||||
available_features.depthBounds &&
|
||||
available_depth_clip.depthClipEnable &&
|
||||
available_clip_control.depthClipControl &&
|
||||
available_color_write.colorWriteEnable &&
|
||||
@@ -17958,6 +18122,7 @@ private:
|
||||
device_features.shaderFloat64 = available_features.shaderFloat64;
|
||||
device_features.fillModeNonSolid = m_rasterization_supported;
|
||||
device_features.tessellationShader = m_rasterization_supported;
|
||||
device_features.depthBounds = m_rasterization_supported;
|
||||
device_info.pEnabledFeatures = &device_features;
|
||||
std::vector<const char *> device_extensions{
|
||||
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
|
||||
@@ -20170,6 +20335,57 @@ TestCase ScalarBrevB32PreservesScc() {
|
||||
O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase ScalarSextI16Captured() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = "ScalarSextI16Captured";
|
||||
constexpr u32 wave_size = 64, high = 0x80000001u;
|
||||
test.initial = {0xdead0000u, 0xabcd7fffu, 0x12348000u, 0xabcfffffu};
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
for (u32 i = 0; i < 4u; ++i) {
|
||||
const auto extended = static_cast<u32>(static_cast<int32_t>(static_cast<int16_t>(test.initial[i])));
|
||||
for (const bool scc : {false, true}) {
|
||||
AppendVMovU32(&code, 30, i * 4u);
|
||||
AppendBufferLoadDword(&code, 3, 30);
|
||||
code.push_back(EncodeVop1(0x02, 11, Vgpr(3)));
|
||||
AppendSMovLiteral(&code, 107, high);
|
||||
code.push_back(EncodeSopc(0x06, InlineU32(scc ? 1u : 0u), InlineU32(1)));
|
||||
code.push_back(EncodeSop1(0x04, 126, 128)); // Scalar operations execute with empty EXEC.
|
||||
code.push_back(0xbeea1a0bu); // Captured s_sext_i32_i16 vcc_lo, s11.
|
||||
code.push_back(EncodeSMovB32(20, 106));
|
||||
code.push_back(EncodeSMovB32(21, 107));
|
||||
code.push_back(EncodeSMovB32(22, 253));
|
||||
code.push_back(EncodeSop1(0x04, 126, 193));
|
||||
AppendVMovU32(&code, 1, 1);
|
||||
code.push_back(EncodeVop2(0x01, 2, InlineU32(0), 1));
|
||||
const u32 expected[] = {extended, high, scc ? 1u : 0u};
|
||||
for (u32 word = 0; word < 3u; ++word) {
|
||||
AppendStoreSgprAtLaneDwordOffset(&code, 20u + word, 0, test.expected.size());
|
||||
test.expected.insert(test.expected.end(), wave_size, expected[word]);
|
||||
}
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, 2, 0, test.expected.size());
|
||||
const uint64_t mask = (uint64_t{high} << 32u) | extended;
|
||||
for (u32 lane = 0; lane < wave_size; ++lane)
|
||||
test.expected.push_back(static_cast<u32>((mask >> lane) & 1u));
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.initial.resize(test.expected.size());
|
||||
test.opcodes = {O::BUFFER_LOAD_DWORD, O::V_READFIRSTLANE_B32, O::S_MOV_B32,
|
||||
O::S_MOV_B64, O::S_CMP_EQ_U32, O::S_SEXT_I32_I16,
|
||||
O::V_CNDMASK_B32, O::V_MOV_B32, O::V_ADD_NC_U32,
|
||||
O::V_LSHLREV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"S_SEXT_I32_I16", 8u}};
|
||||
test.required_spirv = {"OpBitFieldSExtract"};
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarBrevB64OperandsAndMasks(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
@@ -29438,6 +29654,41 @@ TestCase TBufferStoreVariants() {
|
||||
O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase FlatLoadDlcCaptured(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "FlatLoadDlcCapturedWave64" : "FlatLoadDlcCapturedWave32";
|
||||
auto &code = test.code;
|
||||
code.push_back(EncodeSop1(0x04, 126, InlineU32(1)));
|
||||
AppendVMovU32(&code, 4, 0);
|
||||
AppendVMovU32(&code, 5, 31);
|
||||
// PPSA04288 CS 05b58beb842bacbf:0x638, FLAT_LOAD_DWORDX2 v[0:1], v[4:5] DLC.
|
||||
code.insert(code.end(), {0xdc341000u, 0x007d0004u});
|
||||
AppendStoreVgpr(&code, 0, 0);
|
||||
AppendStoreVgpr(&code, 1, 1);
|
||||
// The descriptor store aliases the physical address read by the next load.
|
||||
AppendStoreVgpr(&code, 1, 8);
|
||||
code.push_back(0xbbfd0000u); // S_WAITCNT_VSCNT null, 0.
|
||||
code.insert(code.end(), {0xdc341000u, 0x007d0004u});
|
||||
AppendStoreVgpr(&code, 0, 2);
|
||||
AppendStoreVgpr(&code, 1, 3);
|
||||
AppendEnd(&code);
|
||||
test.initial = {0, 0, 0, 0, 0, 0, 0, 0, 0x11223344u, 0x55667788u};
|
||||
test.expected = {0x11223344u, 0x55667788u, 0x55667788u, 0x55667788u,
|
||||
0, 0, 0, 0, 0x55667788u, 0x55667788u};
|
||||
test.bda_mappings = {{0x1f00000000ull, 32}};
|
||||
test.opcodes = {O::S_MOV_B64, O::V_MOV_B32, O::FLAT_LOAD_DWORDX2,
|
||||
O::BUFFER_STORE_DWORD, O::S_WAITCNT_VSCNT, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpDecorate %buffers Coherent", " 0 Coherent", "Volatile|Aligned 4"};
|
||||
test.decoded_counts = {{"dlc=1", 2}};
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase FlatLoadVariants() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -33348,7 +33599,7 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
|
||||
std::printf("[compute] %-32s ok\n", test.name);
|
||||
}
|
||||
|
||||
void CheckIndirectImageKeySwitch() {
|
||||
void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
|
||||
constexpr const char *name = "IndirectImageKeySwitch";
|
||||
constexpr uint32_t mapping_capacity = 1793u;
|
||||
using namespace ShaderRecompiler::IR;
|
||||
@@ -33401,8 +33652,10 @@ void CheckIndirectImageKeySwitch() {
|
||||
program.descriptor_sources.resize(2);
|
||||
program.descriptor_sources[0].dword_count = 8;
|
||||
program.descriptor_sources[0].indirect_descriptor =
|
||||
DescriptorSource::IndirectDescriptor{.material_source = 0, .table_source = 0,
|
||||
.selector_stride = 224, .selector_offset = 12, .table_stride = 32};
|
||||
DescriptorSource::IndirectDescriptor{
|
||||
.selector = DescriptorSource::IndirectDescriptor::SelectorRead{
|
||||
.source = 0, .stride = 224, .offset = 12},
|
||||
.table_source = 0, .table_stride = 32};
|
||||
program.descriptor_sources[1].dword_count = 4;
|
||||
|
||||
ImageResource root{};
|
||||
@@ -33451,6 +33704,19 @@ void CheckIndirectImageKeySwitch() {
|
||||
sampled_image != 0u && std::ranges::find(nonuniform, sampled_image) != nonuniform.end(),
|
||||
"the sample operand lacks its nonuniform decoration");
|
||||
|
||||
program.info.images[0].indirect_search_iterations = 0;
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
|
||||
ValidateSpirv("DenseIndirectImageTable", spirv);
|
||||
Require(name, "dense image map disassembly", tools.Disassemble(spirv, &text),
|
||||
"failed to disassemble dense image table shader");
|
||||
Require(name, "bounded dense image selection",
|
||||
CountText(text, "OpULessThan") == 1 &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 1 &&
|
||||
text.find("SampledImageArrayNonUniformIndexing") != std::string::npos,
|
||||
"dense image selection lost its bounds check or native nonuniform array");
|
||||
|
||||
// Materialized children follow all native roots, including unrelated images.
|
||||
root.indirect_resources = {0u};
|
||||
program.info.images = {root, candidate};
|
||||
@@ -33471,6 +33737,17 @@ void CheckIndirectImageKeySwitch() {
|
||||
CountText(text, "OpImageSampleExplicitLod") == 1,
|
||||
"materialized images expanded into per-candidate samples");
|
||||
|
||||
program.info.images[root.indirect_resources[103]].dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.info.images[root.indirect_resources[103]].cube = true;
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
spirv = ShaderRecompiler::Spirv::EmitProgram(program, {.compute = &compute});
|
||||
ValidateSpirv(name, spirv);
|
||||
Require(name, "mixed contiguous image runs", tools.Disassemble(spirv, &text) &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 3,
|
||||
"one cube split the native arrays into per-image samples");
|
||||
|
||||
program.memory_info[0].image_dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.memory_info[0].image_address_components = 4;
|
||||
@@ -33527,6 +33804,82 @@ void CheckIndirectImageKeySwitch() {
|
||||
Require(name, "mixed sample count", samples == 3u,
|
||||
"mixed candidate switch did not retain every image");
|
||||
}
|
||||
// Keep an unrelated native root between the null image and appended children,
|
||||
// then select both sides of a cube boundary and the unmapped suffix on the GPU.
|
||||
root.indirect_resources = {0u, 2u, 3u, 4u, 5u, 6u};
|
||||
root.indirect_search_iterations = 0;
|
||||
program.info.images.assign(7, candidate);
|
||||
program.info.images[0] = root;
|
||||
program.info.images[1].indirect_root = ImageResource::NoIndirectImage;
|
||||
program.info.images[4].dimension =
|
||||
ShaderRecompiler::Decoder::ImageDimension::Dim2DArray;
|
||||
program.info.images[4].cube = true;
|
||||
for (u32 component = 0; component < 4u; ++component) {
|
||||
constexpr std::array values{1.5f, 1.5f, 0.0f, 0.0f};
|
||||
address.SetArg(component, Value(std::bit_cast<u32>(values[component])));
|
||||
}
|
||||
auto &byte_offset = *block->PrependNewInst(
|
||||
std::next(block->begin()), ValueOpcode::IMul32, {Value(&key), Value(4u)});
|
||||
image.SetArg(0, Value(&byte_offset));
|
||||
auto &output = block->AppendNewInst(ValueOpcode::GetBufferResource,
|
||||
{Value(0u), Value(0u), Value(0u), Value(0u)});
|
||||
output.SetFlags<u32>(0u);
|
||||
program.memory_info.push_back({.kind = ResourceKind::Buffer, .offen = true});
|
||||
auto &store = block->AppendNewInst(
|
||||
ValueOpcode::StoreBufferU32,
|
||||
{Value(&output), Value(0u), Value(&byte_offset), Value(0u), Value(&sample_x), Value(true)});
|
||||
store.SetFlags(MemoryFlags{1u, 0u});
|
||||
program.info.buffers.push_back({.packed_stride = 1, .written = true});
|
||||
program.binding_layout_complete = false;
|
||||
AllocateBindings(program);
|
||||
|
||||
TestCase test;
|
||||
test.name = "IndirectImageContiguousRuns";
|
||||
CompiledShader compiled;
|
||||
compiled.program = std::move(program);
|
||||
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
|
||||
constexpr std::array ordinals{0u, 1u, 2u, 3u, 4u, 5u, 2u, 0u};
|
||||
compiled.resources.flattened_srt.push_back(ordinals.size());
|
||||
compiled.resources.flattened_srt.insert(compiled.resources.flattened_srt.end(),
|
||||
ordinals.begin(), ordinals.end());
|
||||
std::vector<VulkanHarness::Image> textures;
|
||||
for (u32 resource = 0; resource < 7u; ++resource) {
|
||||
const auto layers = resource == 4u ? 6u : 1u;
|
||||
std::vector<u32> pixels(layers * 4u, std::bit_cast<u32>(float(resource)));
|
||||
textures.push_back(vulkan.CreateImageMips(
|
||||
test.name, 1, 1, vk::Format::eR32G32B32A32Sfloat,
|
||||
vk::ImageUsageFlagBits::eSampled, {pixels}, 4u,
|
||||
vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageType::e2D,
|
||||
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
|
||||
}
|
||||
const auto native_sampler = vulkan.CreateSampler(test.name);
|
||||
for (const u32 wave_size : {32u, 64u}) {
|
||||
compute.wave_size = wave_size;
|
||||
compute.host_subgroup_size = vulkan.SubgroupSize();
|
||||
compute.threads_num[0] = wave_size;
|
||||
compute.threads_num[1] = compute.threads_num[2] = 1u;
|
||||
compiled.program.wave_size = wave_size;
|
||||
compiled.spirv = ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &compute});
|
||||
ValidateSpirv(test.name, compiled.spirv);
|
||||
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
|
||||
Require(test.name, "native array run samples", tools.Disassemble(compiled.spirv, &text) &&
|
||||
CountText(text, "OpImageSampleExplicitLod") == 3u * halves,
|
||||
"mixed runs expanded into per-image samples");
|
||||
test.initial.assign(wave_size, 0xdeadbeefu);
|
||||
test.expected.assign(wave_size, 0u);
|
||||
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
|
||||
test.expected[lane] = std::bit_cast<u32>(float(root.indirect_resources[ordinals[lane]]));
|
||||
}
|
||||
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
|
||||
vulkan.Dispatch(test, compiled, buffer, nullptr, nullptr, nullptr, nullptr,
|
||||
native_sampler, textures);
|
||||
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
|
||||
vulkan.DestroyBuffer(&buffer);
|
||||
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
|
||||
}
|
||||
vulkan.Device().destroySampler(native_sampler);
|
||||
for (auto &texture : textures) vulkan.DestroyImage(&texture);
|
||||
std::printf("[compute] %-32s ok\n", test.name);
|
||||
}
|
||||
|
||||
TestCase ImageStoreMipSelectsPpsa01340Descriptor() {
|
||||
@@ -34873,6 +35226,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ScalarBitcmpB64DynamicOperands);
|
||||
AddCase(ScalarBitcmpB64IntegerConstants);
|
||||
AddCase(ScalarBrevB32PreservesScc);
|
||||
AddCase(ScalarSextI16Captured);
|
||||
cases.push_back(ScalarBrevB64OperandsAndMasks(32));
|
||||
cases.push_back(ScalarBrevB64OperandsAndMasks(64));
|
||||
AddCase(ScalarBfeI32CapturedRawSignExtends);
|
||||
@@ -35154,6 +35508,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(TBufferStoreFormatXy88IntegerComponents);
|
||||
AddCase(TBufferLoadFormatXy88IntegerComponents);
|
||||
AddCase(TBufferStoreVariants);
|
||||
cases.push_back(FlatLoadDlcCaptured(32));
|
||||
cases.push_back(FlatLoadDlcCaptured(64));
|
||||
AddCase(FlatLoadVariants);
|
||||
AddCase(FlatSubdwordLoadsApplyByteOffset);
|
||||
cases.push_back(GlobalLoadShortD16Captured(32));
|
||||
@@ -37856,15 +38212,6 @@ void CheckImageSamplerSpecialization() {
|
||||
options.user_data = user_data;
|
||||
options.input_info.compute = &compute;
|
||||
auto translated = ShaderRecompiler::TranslateProgram(code, options);
|
||||
if (scalar) {
|
||||
bool key_phi = false;
|
||||
for (const auto *block : translated.program.blocks)
|
||||
for (const auto &inst : *block)
|
||||
key_phi |= inst.GetOpcode() == ValueOpcode::GetImageResource &&
|
||||
inst.Arg(0).Resolve().IsPhi();
|
||||
Require(name, "descriptor key reconvergence", key_phi,
|
||||
"native crossed exits did not retain a descriptor key Phi");
|
||||
}
|
||||
auto plan = ExtractResourcePlan(translated.program);
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
@@ -37876,56 +38223,57 @@ void CheckImageSamplerSpecialization() {
|
||||
snapshot.images.size() == count + 1u,
|
||||
"GPU-selected material descriptors did not expand beyond native root capacity");
|
||||
const auto offset = specialization.images[0].indirect_mapping_offset;
|
||||
const auto ordinal = [&](u32 key) {
|
||||
for (u32 i = 0; i < snapshot.flattened_srt[offset]; ++i)
|
||||
if (snapshot.flattened_srt[offset + 1u + 2u * i] == key)
|
||||
return snapshot.flattened_srt[offset + 2u + 2u * i];
|
||||
return UINT32_MAX;
|
||||
const auto ordinal = [&](u32 byte_offset) {
|
||||
const auto word = byte_offset >> 2u;
|
||||
return word < snapshot.flattened_srt[offset]
|
||||
? snapshot.flattened_srt[offset + 1u + word] : 0u;
|
||||
};
|
||||
Require(name, "shift, wrap and whole-record read",
|
||||
snapshot.flattened_srt[offset] == count + 2u && ordinal(1u) != UINT32_MAX &&
|
||||
ordinal(0x08000001u) == ordinal(1u) && ordinal(count) < snapshot.images.size() &&
|
||||
snapshot.images[ordinal(count)].dwords[0] == 0u &&
|
||||
std::ranges::count(snapshot.specialization_reads,
|
||||
std::pair<uint64_t, uint64_t>{material_base, (count + 3u) * 4u}) == 1,
|
||||
"material keys lost shift/dedup, scalar wrap/OOB, or bulk strict-read semantics");
|
||||
Require(name, "dense table map and coherent snapshot",
|
||||
specialization.images[0].indirect_search_iterations == 0u &&
|
||||
snapshot.flattened_srt[offset] == (count - 1u) * 8u + 1u &&
|
||||
ordinal(32u) != 0u &&
|
||||
ordinal(count * 32u) == 0u && snapshot.images[0].dwords[0] == 0u &&
|
||||
snapshot.specialization_reads ==
|
||||
std::vector<std::pair<uint64_t, uint64_t>>{{table_base, count * 32u}},
|
||||
"image table lost its dense offsets, explicit null or single coherent read");
|
||||
const auto previous = specialization;
|
||||
memory[table_base / 4u + 8u] += 0x10000u;
|
||||
Require(name, "runtime descriptor refresh",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == previous && snapshot.images[1].dwords[0] == 0x11100u,
|
||||
specialization == previous && snapshot.images[ordinal(32u)].dwords[0] == 0x11100u,
|
||||
"descriptor addresses were frozen or entered the shader specialization");
|
||||
auto compiled = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization);
|
||||
ValidateSpirv(name, compiled.spirv);
|
||||
if (!scalar) {
|
||||
user_data[4] += 1u;
|
||||
Require(name, "misaligned material rejection",
|
||||
!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"unaligned raw buffer base was treated as separately aligned scalar memory");
|
||||
Require(name, "GPU material reads remain on GPU",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
specialization == previous && snapshot.specialization_reads ==
|
||||
std::vector<std::pair<uint64_t, uint64_t>>{{table_base, count * 32u}},
|
||||
"GPU material addressing reentered host descriptor selection");
|
||||
user_data[4] -= 1u;
|
||||
}
|
||||
auto dirty = runtime;
|
||||
dirty.read_specialization_memory = +[](void*, uint64_t, std::span<u32>) { return false; };
|
||||
Require(name, "dirty material rejection",
|
||||
Require(name, "unreadable image table rejection",
|
||||
!MaterializeResources(plan, dirty, snapshot, specialization),
|
||||
"GPU-selected texture materialization bypassed strict read provenance");
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
if (scalar) {
|
||||
ExpectFatal("ScalarImageDescriptorNumericConsumer", [&] {
|
||||
code.insert(code.begin() + table_read_end, EncodeVop1(0x01, 18, 12));
|
||||
code.pop_back();
|
||||
AppendBufferStoreDword(&code, 18, 0);
|
||||
code[2] = EncodeSopp(0x08, static_cast<int16_t>(code.size() - 3u));
|
||||
AppendEnd(&code);
|
||||
(void)ShaderRecompiler::TranslateProgram(code, options);
|
||||
});
|
||||
code.insert(code.begin() + table_read_end, EncodeVop1(0x01, 18, 12));
|
||||
code.pop_back();
|
||||
AppendBufferStoreDword(&code, 18, 0);
|
||||
code[2] = EncodeSopp(0x08, static_cast<int16_t>(code.size() - 3u));
|
||||
AppendEnd(&code);
|
||||
} else {
|
||||
ExpectFatal("MaterialImageWithoutEntryWitness", [&] {
|
||||
code[2] = EncodeSopp(0x00, 0); // S_NOP removes the original nonempty-EXEC witness.
|
||||
(void)ShaderRecompiler::TranslateProgram(code, options);
|
||||
});
|
||||
code[2] = EncodeSopp(0x00, 0); // GPU selectors do not need a host nonempty-EXEC witness.
|
||||
}
|
||||
#endif
|
||||
auto retained = ShaderRecompiler::TranslateProgram(code, options);
|
||||
auto retained_plan = ExtractResourcePlan(retained.program);
|
||||
Require(name, "retained descriptor consumers",
|
||||
MaterializeResources(retained_plan, runtime, snapshot, specialization),
|
||||
"retained GPU descriptor reads could not use the shared image table");
|
||||
compiled = ShaderRecompiler::CompileProgram(std::move(retained), options, specialization);
|
||||
ValidateSpirv(name, compiled.spirv);
|
||||
}
|
||||
|
||||
std::printf("[host] %-32s ok\n", "ImageSpecializationPipelineId");
|
||||
@@ -39789,6 +40137,64 @@ void CheckPm4DrawIndirectMultiPacket(RenderContext &renderer) {
|
||||
packet_mismatch &&
|
||||
!Gen5::AgcIsInternalDataPacket(0xc0017904u, invalid_payload),
|
||||
"indirect multi-draw packet differs from the expected stream");
|
||||
|
||||
constexpr uint32_t data_offset = 0x5c08;
|
||||
constexpr uint32_t stride = 32;
|
||||
std::array<uint32_t, data_offset / 4 + stride / 4 + 5> arguments{};
|
||||
const std::array<uint32_t, 5> first{0, 1, 3, 0xfffffffeu, 9};
|
||||
const std::array<uint32_t, 5> second{0, 2, 5, 7, 11};
|
||||
std::copy(first.begin(), first.end(), arguments.begin() + data_offset / 4);
|
||||
std::copy(second.begin(), second.end(), arguments.begin() + (data_offset + stride) / 4);
|
||||
std::array<uint32_t, 8> indices{};
|
||||
processor.BufferInit();
|
||||
processor.SetDrawIndirectArgsBaseAddress(reinterpret_cast<uint64_t>(arguments.data()));
|
||||
processor.SetIndexBaseAddress(reinterpret_cast<uint64_t>(indices.data()));
|
||||
processor.SetIndexType(0);
|
||||
std::array<uint32_t, 5> captured{0xc0032500u, data_offset, 0x94, 0x95, 0};
|
||||
Require("Pm4DrawIndirectMulti", "captured indexed single execution",
|
||||
execute(captured.data(), captured.size()),
|
||||
"indexed indirect draw rejected native source-select zero");
|
||||
// Zero primitive counts still traverse CP argument decoding and renderer entry,
|
||||
// while avoiding unrelated shader and attachment requirements.
|
||||
for (const bool indexed : {false, true}) {
|
||||
for (const bool multi : {false, true}) {
|
||||
for (const uint64_t draw_modifier : {0ull, 0x100ull, 1ull << 32u, (1ull << 32u) | 0x100ull}) {
|
||||
packet.fill(0);
|
||||
dcb.cursor_up = packet.data();
|
||||
auto *buffer = reinterpret_cast<Gen5::CommandBuffer *>(&dcb);
|
||||
alignas(4) uint32_t draw_count = 3;
|
||||
if (multi) {
|
||||
emitted = indexed ? Gen5::AgcDcbDrawIndexIndirectMulti(buffer, data_offset, 1, 2,
|
||||
&draw_count, stride, draw_modifier)
|
||||
: Gen5::AgcDcbDrawIndirectMulti(buffer, data_offset, 1, 2,
|
||||
&draw_count, stride, draw_modifier);
|
||||
} else {
|
||||
emitted = indexed ? Gen5::AgcDcbDrawIndexIndirect(buffer, data_offset, draw_modifier)
|
||||
: Gen5::AgcDcbDrawIndirect(buffer, data_offset, draw_modifier);
|
||||
}
|
||||
const uint32_t begin = multi && !indexed ? 3 : 0;
|
||||
const uint32_t words = multi ? (indexed ? 10 : 16) : 5;
|
||||
const uint32_t opcode = multi ? (indexed ? 0xc0083800u : 0xc0082c00u)
|
||||
: (indexed ? 0xc0032500u : 0xc0032400u);
|
||||
const uint32_t initiator = (indexed ? 0 : 2) |
|
||||
(draw_modifier == 0x100 ? 0x20 : 0);
|
||||
Require("Pm4DrawIndirectMulti", "indexed/auto initiator emission",
|
||||
emitted == packet.data() && dcb.cursor_up == packet.data() + words &&
|
||||
packet[begin] == opcode && packet[begin + 1] == data_offset &&
|
||||
packet[begin + (multi ? 9 : 4)] == initiator,
|
||||
"indirect opcode, source select, or modifier bits differ from the native packet");
|
||||
Require("Pm4DrawIndirectMulti", "single/multi argument execution",
|
||||
execute(packet.data(), words),
|
||||
"indirect draw failed shared argument decoding or padded/count-limited traversal");
|
||||
if (multi) {
|
||||
draw_count = 0;
|
||||
Require("Pm4DrawIndirectMulti", "zero indirect count execution",
|
||||
execute(packet.data(), words), "zero indirect draw count did not complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.GetCommandScheduler().Finish();
|
||||
std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti");
|
||||
}
|
||||
|
||||
@@ -40460,8 +40866,15 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--scalar-sext-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ScalarSextI16Captured());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--flat-d16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, FlatLoadDlcCaptured(32));
|
||||
RunCase(&vulkan, FlatLoadDlcCaptured(64));
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(32));
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(64));
|
||||
RunCase(&vulkan, FlatLoadShortD16AddressSegments());
|
||||
@@ -40933,8 +41346,8 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--indirect-image-only") == 0) {
|
||||
CheckImageSamplerSpecialization();
|
||||
CheckIndirectImageKeySwitch();
|
||||
VulkanHarness vulkan;
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
RunCase(&vulkan, ImageCubeGradientsPreserveDerivatives());
|
||||
return 0;
|
||||
}
|
||||
@@ -41096,7 +41509,7 @@ int main(int argc, char **argv) {
|
||||
CheckIndirectBufferStore(vulkan);
|
||||
CheckRuntimeBufferRecords(vulkan);
|
||||
CheckComputeThreadDimensions(vulkan);
|
||||
CheckIndirectImageKeySwitch();
|
||||
CheckIndirectImageKeySwitch(vulkan);
|
||||
CheckWave64WholeWaveResults();
|
||||
CheckPs5GameExampleImageClearRuntimeShape();
|
||||
vulkan.CheckSchedulerTimeline();
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "common/file.h"
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
namespace Trophies = Common::Trophies;
|
||||
|
||||
void Check(bool value, const char *text) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "TrophySystemTests: failed: %s\n", text);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
const auto unique =
|
||||
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_path = std::filesystem::temp_directory_path() /
|
||||
("kyty_trophy_test_" + std::to_string(unique));
|
||||
Check(std::filesystem::create_directories(m_path),
|
||||
"create temporary directory");
|
||||
}
|
||||
~TempDirectory() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(m_path, error);
|
||||
}
|
||||
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
|
||||
|
||||
private:
|
||||
std::filesystem::path m_path;
|
||||
};
|
||||
|
||||
void WriteBigEndian(std::vector<char> &bytes, size_t offset, uint64_t value,
|
||||
size_t length) {
|
||||
Check(offset <= bytes.size() && length <= bytes.size() - offset,
|
||||
"write within package");
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
bytes[offset + length - 1 - i] = static_cast<char>(value & 0xff);
|
||||
value >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
void WritePackage(const std::filesystem::path &path,
|
||||
const std::map<std::string, std::string> &files) {
|
||||
size_t offset = 0x40 + files.size() * 0x40;
|
||||
size_t size = offset;
|
||||
for (const auto &[name, contents] : files)
|
||||
size += contents.size();
|
||||
std::vector<char> bytes(size);
|
||||
WriteBigEndian(bytes, 0, 0xb228c60a, 4);
|
||||
WriteBigEndian(bytes, 4, 1, 4);
|
||||
WriteBigEndian(bytes, 8, bytes.size(), 8);
|
||||
WriteBigEndian(bytes, 0x10, files.size(), 4);
|
||||
WriteBigEndian(bytes, 0x14, 0x20, 4);
|
||||
size_t entry = 0x40;
|
||||
for (const auto &[name, contents] : files) {
|
||||
Check(name.size() < 0x20, "fixture file name fits");
|
||||
std::memcpy(bytes.data() + entry, name.data(), name.size());
|
||||
WriteBigEndian(bytes, entry + 0x20, offset, 8);
|
||||
WriteBigEndian(bytes, entry + 0x28, contents.size(), 8);
|
||||
std::memcpy(bytes.data() + offset, contents.data(), contents.size());
|
||||
offset += contents.size();
|
||||
entry += 0x40;
|
||||
}
|
||||
Common::File file;
|
||||
Check(file.Create(path), "create package fixture");
|
||||
uint32_t written = 0;
|
||||
file.Write(bytes.data(), static_cast<uint32_t>(bytes.size()), &written);
|
||||
Check(written == bytes.size(), "write package fixture");
|
||||
}
|
||||
|
||||
Trophies::Package MakePackage(const std::filesystem::path &directory) {
|
||||
const auto path = directory / "trophy00.ucp";
|
||||
WritePackage(
|
||||
path,
|
||||
{{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[
|
||||
{"id":"0","grade":"P"},
|
||||
{"id":"1","grade":"G","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
|
||||
{"id":"2","grade":"S","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100","progressive":false}},
|
||||
{"id":"3","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5","progressive":false}},
|
||||
{"id":"4","grade":"B","unlockCondition":{"udsStatId":"10","comparator":"lt","targetValue":"-1","progressive":false}}
|
||||
]})"},
|
||||
{"tropmeta_en-US.json",
|
||||
R"({"metadata":{"titleMetadata":{"name":"Test game"},"trophyMetadata":[
|
||||
{"id":"1","name":"Find All Coins","detail":"Find all coins in the level."}
|
||||
]}})"}});
|
||||
return Trophies::LoadPackage(path, 1);
|
||||
}
|
||||
|
||||
void TestEventExtraction(const std::filesystem::path &directory) {
|
||||
auto package = MakePackage(directory);
|
||||
Check(package.trophies.size() == 5,
|
||||
"unsupported signed target does not discard trophy metadata");
|
||||
Check(package.trophies.at(1).uds_stat_id == 10 &&
|
||||
package.trophies.at(1).target == 10 &&
|
||||
!package.trophies.at(1).progressive,
|
||||
"non-progressive trophy retains its stat target");
|
||||
Check(!package.trophies.at(4).target,
|
||||
"negative target is not converted to unsigned");
|
||||
Check(Trophies::FindUdsTrophies(package, "FIND_ALL_COINS",
|
||||
{{"counter", uint64_t{100}}})
|
||||
.empty(),
|
||||
"missing mapping never guesses from trophy text");
|
||||
const auto path = directory / "uds00.ucp";
|
||||
WritePackage(path, {{"stats_definition.json", R"({"statDefinitionArray":[
|
||||
{"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"},
|
||||
{"statId":20,"dataType":"uint64","aggregation":"latest"},
|
||||
{"statId":30,"dataType":"uint32","aggregation":"latest"},
|
||||
{"statId":40,"dataType":"uint64","aggregation":"sum"},
|
||||
{"statId":50,"dataType":"uint64","aggregation":"count"},
|
||||
{"statId":60,"dataType":"uint64","aggregation":"max"},
|
||||
{"statId":70,"dataType":"uint64","aggregation":"latest","maxValue":"3"}
|
||||
]})"},
|
||||
{"stats_extraction.json", R"({"statsExtractionRuleArray":[
|
||||
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counterBase","output":{"statId":20}}},
|
||||
{"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}},
|
||||
{"condition":{"eventName":"FILTERED","property":{"path":"$.level","comparator":"==","value":1}},"action":{"input":"$.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"NESTED"},"action":{"input":"$.player.counter","output":{"statId":10}}},
|
||||
{"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":40}}},
|
||||
{"condition":{"eventName":"COUNT"},"action":{"output":{"statId":50}}},
|
||||
{"condition":{"eventName":"MAX"},"action":{"input":"$.counter","output":{"statId":60}}},
|
||||
{"condition":{"eventName":"BOUNDED"},"action":{"input":"$.counter","output":{"statId":70}}},
|
||||
{"bad":1}
|
||||
]})"}});
|
||||
package.event_rules = Trophies::LoadUdsRules(path);
|
||||
Check(package.event_rules.size() == 2,
|
||||
"skip unsupported extraction and aggregation");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{9}}, {"counterBase", uint64_t{100}}})
|
||||
.empty(),
|
||||
"do not add independent counters or treat gt as ge");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{100}}}) ==
|
||||
std::vector<int>{1},
|
||||
"each stat reads its configured property");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{0}}, {"counterBase", uint64_t{101}}}) ==
|
||||
std::vector<int>{2},
|
||||
"strict comparison requires greater value");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "COLLECT",
|
||||
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{101}}}) ==
|
||||
std::vector<int>({1, 2}),
|
||||
"one event can unlock multiple mapped trophies");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "TIME", {{"seconds", uint32_t{5}}}) == std::vector<int>{3},
|
||||
"less-equal condition accepts its boundary");
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{6}}})
|
||||
.empty(),
|
||||
"less-equal condition rejects larger input");
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"counter", uint64_t{0}}})
|
||||
.empty(),
|
||||
"missing property is not zero");
|
||||
package.trophies.at(3).comparison = Trophies::Comparison::Less;
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{5}}})
|
||||
.empty(),
|
||||
"strict less rejects its boundary");
|
||||
Check(Trophies::FindUdsTrophies(
|
||||
package, "TIME", {{"seconds", uint32_t{4}}}) == std::vector<int>{3},
|
||||
"strict less accepts lower input");
|
||||
package.trophies.at(3).comparison = Trophies::Comparison::GreaterEqual;
|
||||
Check(Trophies::FindUdsTrophies(package, "TIME",
|
||||
{{"seconds", uint64_t{UINT32_MAX} + 1}})
|
||||
.empty(),
|
||||
"uint64 input cannot update a uint32 stat");
|
||||
Check(
|
||||
Trophies::FindUdsTrophies(package, "COLLECT", {{"counter", int32_t{100}}})
|
||||
.empty(),
|
||||
"signed inputs cannot update unsigned stats");
|
||||
Check(Trophies::FindUdsTrophies(package, "COLLECT",
|
||||
{{"counter", uint32_t{100}}})
|
||||
.empty(),
|
||||
"unsigned input width must match the stat");
|
||||
package.trophies.at(1).comparison = Trophies::Comparison::None;
|
||||
Check(Trophies::FindUdsTrophies(package, "COLLECT",
|
||||
{{"counter", uint64_t{100}}})
|
||||
.empty(),
|
||||
"unknown comparator cannot unlock");
|
||||
WritePackage(path, {{"stats_extraction.json", "{}"}});
|
||||
Check(Trophies::LoadUdsRules(path).empty(),
|
||||
"missing stat definitions cannot infer aggregation");
|
||||
Check(Trophies::LoadUdsRules(directory / "missing.ucp").empty(),
|
||||
"missing UDS package has no rules");
|
||||
}
|
||||
|
||||
void TestProgress(const std::filesystem::path &directory) {
|
||||
const auto package = MakePackage(directory);
|
||||
Trophies::UnlockData unlocks;
|
||||
unlocks.unlocked = {0, 1, 999};
|
||||
const auto progress = Trophies::GetProgress(package, unlocks);
|
||||
Check(progress.total == 5 && progress.earned == 2,
|
||||
"ignore unlock IDs absent from package");
|
||||
Check(progress.earned_grade[1] == 1 && progress.earned_grade[2] == 1 &&
|
||||
progress.Percentage() == 60,
|
||||
"percentage uses 6:2:1 grade points and excludes platinum");
|
||||
const auto group = Trophies::GetProgress(package, unlocks, 1);
|
||||
Check(group.total == 1 && group.earned == 0 && group.Percentage() == 0,
|
||||
"group counts filter trophies");
|
||||
Check(Trophies::GetProgress({}, {}).Percentage() == 0,
|
||||
"empty package progress is zero");
|
||||
}
|
||||
|
||||
void TestUnlockPersistence(const std::filesystem::path &directory) {
|
||||
const auto path = Trophies::UnlocksPath(directory, "PPSA00001", 1000, 0);
|
||||
Check(path == directory / "_Trophies/PPSA00001/trophies_1000_0.json",
|
||||
"store trophies outside save data");
|
||||
Check(Trophies::UnlocksPath(directory, "../bad", 1000, 0).empty(),
|
||||
"reject unsafe title IDs");
|
||||
Check(Trophies::LoadUnlockData(path).unlocked.empty(),
|
||||
"missing unlock file loads as empty");
|
||||
Trophies::UnlockData unlocks;
|
||||
unlocks.unlocked = {4, 42};
|
||||
const auto tick = Trophies::UnixEpochTick + 1791288000123456ULL;
|
||||
unlocks.timestamps.emplace(42, tick);
|
||||
Check(Trophies::SaveUnlockData(path, unlocks), "save timestamped unlocks");
|
||||
auto loaded = Trophies::LoadUnlockData(path);
|
||||
Check(loaded.unlocked == unlocks.unlocked &&
|
||||
loaded.timestamps == unlocks.timestamps,
|
||||
"persist full UTC microsecond timestamp");
|
||||
loaded.unlocked.insert(43);
|
||||
loaded.timestamps.emplace(43, tick + 1000000);
|
||||
Check(Trophies::SaveUnlockData(path, loaded), "replace existing unlock file");
|
||||
Check(Trophies::LoadUnlockData(path).timestamps.at(42) == tick,
|
||||
"retain original unlock time");
|
||||
|
||||
const auto blocked = directory / "blocked.json";
|
||||
std::filesystem::create_directory(blocked);
|
||||
Common::File sentinel;
|
||||
Check(sentinel.Create(blocked / "keep"),
|
||||
"create replacement failure fixture");
|
||||
sentinel.Close();
|
||||
Check(!Trophies::SaveUnlockData(blocked, loaded) &&
|
||||
std::filesystem::exists(blocked / "keep"),
|
||||
"failed replacement leaves destination intact");
|
||||
Check(!std::filesystem::exists(directory / "blocked.json.tmp"),
|
||||
"failed replacement removes temporary file");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TempDirectory directory;
|
||||
TestEventExtraction(directory.Path());
|
||||
TestProgress(directory.Path());
|
||||
TestUnlockPersistence(directory.Path());
|
||||
std::printf("TrophySystemTests: all cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user