Compare commits

..

14 Commits

Author SHA1 Message Date
Felipe fb7ff7ec03 Map UDS events to trophies from dumped metadata; trophy viewer/overview improvements (#1088)
* Map UDS events to trophies via dumped metadata and redesign trophy UI

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

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

* Fix trophy rule evaluation and share progress accounting

* Match trophy and reload toolbar styling

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-06 17:45:26 +04:00
nmzik 7d2422abbe add remote cheats launcher 2026-10-06 15:44:26 +04:00
nmzik b929bbe3e0 shader: limit broad image tables to compatible views 2026-10-06 14:59:05 +04:00
nmzik ae1e92621d shader: sample mixed bindless images through contiguous native arrays 2026-10-06 14:53:17 +04:00
nmzik 5a491abbda shader: remove redundant descriptor sorting and stale null check 2026-10-06 14:53:17 +04:00
Claxten afbb322b32 kernel: deliver persistent user events once per wait 2026-10-06 14:53:17 +04:00
Claxten 5601861d67 shader: implement RDNA2 scalar 16-bit sign extension 2026-10-06 14:53:17 +04:00
Claxten cd06601022 shader: replace CPU selector chains with direct image-table lookup 2026-10-06 14:53:17 +04:00
nmzik c23de62184 shader: resolve nested descriptor selector tables with exact scalar offsets 2026-10-06 14:53:17 +04:00
Claxten 9caec9f2c8 shader: preserve RDNA2 DLC coherence for flat address loads 2026-10-06 14:53:17 +04:00
nmzik cdb64bfd4f graphics: share indirect draw handling and honor indexed sources 2026-10-06 00:35:02 +04:00
nmzik e5dcfdb87b graphics: bound unbounded buffer reads and writes (#1066)
Co-authored-by: João Lobo <joaodlobo@gmail.com>
2026-10-06 00:35:02 +04:00
nmzik 984010227f graphics: make depth bounds dynamic to reuse pipelines
Co-authored-by: TheCruZ <thecruzwtf@gmail.com>
2026-10-06 00:35:02 +04:00
TheCruZ 57c97ebd86 Decode 16-bit color clears (TheCruZ/KytyPS5-GTA@b8dcd826) 2026-10-06 00:35:02 +04:00
53 changed files with 3142 additions and 1093 deletions
+7
View File
@@ -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
View File
@@ -5,9 +5,9 @@
#include <algorithm>
#include <array>
#include <charconv>
#include <span>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <span>
namespace Common::Trophies {
namespace {
@@ -40,6 +40,25 @@ int ReadId(const Json& value) {
return error == std::errc {} && end == text.data() + text.size() && id >= 0 ? id : -1;
}
std::optional<uint64_t> ReadUnsigned(const Json& value) {
if (value.is_number_unsigned()) {
return value.get<uint64_t>();
}
if (value.is_number_integer()) {
const auto number = value.get<int64_t>();
return number >= 0 ? std::optional<uint64_t>(static_cast<uint64_t>(number)) : std::nullopt;
}
if (!value.is_string()) {
return std::nullopt;
}
const auto& text = value.get_ref<const std::string&>();
uint64_t number = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number);
return error == std::errc {} && end == text.data() + text.size()
? std::optional<uint64_t>(number)
: std::nullopt;
}
Json ReadJson(const Files& files, std::string_view name) {
const auto entry = files.find(name);
if (entry == files.end()) {
@@ -48,19 +67,18 @@ Json ReadJson(const Files& files, std::string_view name) {
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
}
Package ParsePackage(std::span<const std::byte> data, int console_language) {
bool ReadFiles(std::span<const std::byte> data, Files& files, uint64_t& size) {
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
return {};
return false;
}
const auto size = ReadBigEndian(data.data() + 8, 8);
size = ReadBigEndian(data.data() + 8, 8);
const auto count = ReadBigEndian(data.data() + 0x10, 4);
const auto toc = ReadBigEndian(data.data() + 0x14, 4);
if (size < 0x40 || size > data.size() || count > 4096 || toc > size ||
0x20 + count * 0x40 > size - toc) {
return {};
return false;
}
Files files;
for (uint64_t i = 0; i < count; ++i) {
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
const auto* name = reinterpret_cast<const char*>(entry);
@@ -68,10 +86,19 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
const auto offset = ReadBigEndian(entry + 0x20, 8);
const auto length = ReadBigEndian(entry + 0x28, 8);
if (offset > size || length > size - offset) {
return {};
return false;
}
files.emplace(filename, data.subspan(offset, length));
}
return true;
}
Package ParsePackage(std::span<const std::byte> data, int console_language) {
Files files;
uint64_t size = 0;
if (!ReadFiles(data, files, size)) {
return {};
}
const auto conf = ReadJson(files, "tropconf.json");
if (!conf.is_object() || !conf.contains("trophies") || !conf["trophies"].is_array() ||
conf["trophies"].size() > 1000 || !conf.contains("defaultLanguage") ||
@@ -129,19 +156,29 @@ Package ParsePackage(std::span<const std::byte> data, int console_language) {
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
if (definition.contains("unlockCondition")) {
const auto& condition = definition["unlockCondition"];
if (condition.is_object() && condition.contains("progressive") &&
condition["progressive"] == true) {
if (!condition.contains("targetValue") || !condition["targetValue"].is_string()) {
return {};
if (condition.is_object()) {
trophy.progressive =
condition.contains("progressive") && condition["progressive"] == true;
if (condition.contains("udsStatId")) {
trophy.uds_stat_id = ReadUnsigned(condition["udsStatId"]);
if (!trophy.uds_stat_id) {
return {};
}
}
const auto& value = condition["targetValue"].get_ref<const std::string&>();
uint64_t target = 0;
const auto [end, error] =
std::from_chars(value.data(), value.data() + value.size(), target);
if (error != std::errc {} || end != value.data() + value.size()) {
return {};
if (condition.contains("targetValue")) {
trophy.target = ReadUnsigned(condition["targetValue"]);
}
const auto comparison = condition.find("comparator");
if (comparison != condition.end()) {
if (*comparison == "ge")
trophy.comparison = Comparison::GreaterEqual;
else if (*comparison == "gt")
trophy.comparison = Comparison::Greater;
else if (*comparison == "le")
trophy.comparison = Comparison::LessEqual;
else if (*comparison == "lt")
trophy.comparison = Comparison::Less;
}
trophy.target = target;
}
}
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
@@ -210,11 +247,11 @@ std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string
})) {
return {};
}
return root / "_SaveData" / title_id /
return root / "_Trophies" / title_id /
fmt::format("trophies_{}_{}.json", user_id, service_label);
}
std::set<int> LoadUnlocks(const std::filesystem::path& path) {
UnlockData LoadUnlockData(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
return {};
@@ -225,22 +262,40 @@ std::set<int> LoadUnlocks(const std::filesystem::path& path) {
!json["unlockedTrophies"].is_array()) {
return {};
}
std::set<int> unlocked;
UnlockData unlocks;
for (const auto& value: json["unlockedTrophies"]) {
const auto id = ReadId(value);
if (id >= 0) {
unlocked.insert(id);
unlocks.unlocked.insert(id);
}
}
return unlocked;
if (json.contains("unlockedAt") && json["unlockedAt"].is_object()) {
for (auto it = json["unlockedAt"].begin(); it != json["unlockedAt"].end(); ++it) {
const auto id = ReadId(Json(it.key()));
if (id >= 0 && unlocks.unlocked.contains(id)) {
const auto tick = ReadUnsigned(it.value());
if (tick && *tick >= UnixEpochTick && *tick <= 315537897599999999ULL) {
unlocks.timestamps.emplace(id, *tick);
}
}
}
}
return unlocks;
}
bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked) {
bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
return false;
}
const auto text = Json {{"unlockedTrophies", unlocked}}.dump();
auto temporary = path;
Json timestamps = Json::object();
for (const auto& [id, tick]: unlocks.timestamps) {
if (unlocks.unlocked.contains(id)) {
timestamps[std::to_string(id)] = tick;
}
}
const auto text =
Json {{"unlockedTrophies", unlocks.unlocked}, {"unlockedAt", timestamps}}.dump();
auto temporary = path;
temporary += ".tmp";
File file;
if (text.size() > MaxUnlocksSize || !file.Create(temporary)) {
@@ -261,4 +316,138 @@ bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocke
return false;
}
UdsRules LoadUdsRules(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
return {};
}
const auto bytes = file.ReadWholeBuffer();
Files files;
uint64_t size = 0;
if (!ReadFiles(bytes, files, size)) {
return {};
}
const auto definitions = ReadJson(files, "stats_definition.json");
const auto extraction = ReadJson(files, "stats_extraction.json");
if (!definitions.is_object() || !definitions.contains("statDefinitionArray") ||
!definitions["statDefinitionArray"].is_array() || !extraction.is_object() ||
!extraction.contains("statsExtractionRuleArray") ||
!extraction["statsExtractionRuleArray"].is_array()) {
return {};
}
// Only direct unsigned latest-value stats are evaluated here. Aggregated stats
// and filtered/nested extraction require UDS state and event types we do not implement.
std::map<uint64_t, bool> stats;
for (const auto& definition: definitions["statDefinitionArray"]) {
if (!definition.is_object() || !definition.contains("statId") ||
!definition.contains("aggregation") || definition["aggregation"] != "latest" ||
!definition.contains("dataType")) {
continue;
}
const auto id = ReadUnsigned(definition["statId"]);
const auto& type = definition["dataType"];
if (!id || (type != "uint64" && type != "uint32")) {
continue;
}
const uint64_t maximum = type == "uint32" ? UINT32_MAX : UINT64_MAX;
if ((definition.contains("minValue") && ReadUnsigned(definition["minValue"]) != 0) ||
(definition.contains("maxValue") && ReadUnsigned(definition["maxValue"]) != maximum)) {
continue;
}
stats.emplace(*id, type == "uint32");
}
UdsRules events;
for (const auto& rule: extraction["statsExtractionRuleArray"]) {
if (!rule.is_object() || !rule.contains("condition") || !rule.contains("action")) {
continue;
}
const auto& condition = rule["condition"];
const auto& action = rule["action"];
if (!condition.is_object() || condition.size() != 1 || !condition.contains("eventName") ||
!condition["eventName"].is_string() || !action.is_object() ||
!action.contains("input") || !action["input"].is_string() ||
!action.contains("output") || !action["output"].is_object() ||
!action["output"].contains("statId")) {
continue;
}
const auto id = ReadUnsigned(action["output"]["statId"]);
const auto& input = action["input"].get_ref<const std::string&>();
if (!id || !stats.contains(*id) || !input.starts_with("$.") || input.size() == 2 ||
input.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", 2) !=
std::string::npos) {
continue;
}
events[condition["eventName"].get<std::string>()].push_back(
{*id, input.substr(2), stats.at(*id)});
}
return events;
}
std::filesystem::path UdsPackagePath(uint32_t service_label) {
return std::filesystem::path("sce_sys/uds") / fmt::format("uds{:02}.ucp", service_label);
}
std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties) {
const auto rules = package.event_rules.find(std::string(event_name));
if (rules == package.event_rules.end()) {
return {};
}
std::vector<int> ids;
for (const auto& [id, trophy]: package.trophies) {
if (trophy.grade == 1 || !trophy.uds_stat_id || !trophy.target) {
continue;
}
for (const auto& rule: rules->second) {
const auto value = properties.find(rule.input);
if (rule.stat_id != *trophy.uds_stat_id || value == properties.end()) {
continue;
}
std::optional<uint64_t> number;
if (rule.is_uint32) {
if (const auto* input = std::get_if<uint32_t>(&value->second)) number = *input;
} else if (const auto* input = std::get_if<uint64_t>(&value->second)) {
number = *input;
}
if (!number) continue;
bool matched = false;
switch (trophy.comparison) {
case Comparison::GreaterEqual: matched = *number >= *trophy.target; break;
case Comparison::Greater: matched = *number > *trophy.target; break;
case Comparison::LessEqual: matched = *number <= *trophy.target; break;
case Comparison::Less: matched = *number < *trophy.target; break;
case Comparison::None: break;
}
if (matched) {
ids.push_back(id);
break;
}
}
}
return ids;
}
uint32_t Progress::Percentage() const {
const auto points = total_grade[2] * 6 + total_grade[3] * 2 + total_grade[4];
const auto earned_points = earned_grade[2] * 6 + earned_grade[3] * 2 + earned_grade[4];
return points == 0 ? 0 : earned_points * 100 / points;
}
Progress GetProgress(const Package& package, const UnlockData& unlocks, std::optional<int> group) {
Progress progress;
for (const auto& [id, trophy]: package.trophies) {
if (group && trophy.group_id != *group) {
continue;
}
++progress.total;
++progress.total_grade[trophy.grade];
if (unlocks.unlocked.contains(id)) {
++progress.earned;
++progress.earned_grade[trophy.grade];
}
}
return progress;
}
} // namespace Common::Trophies
+42 -3
View File
@@ -1,6 +1,7 @@
#ifndef KYTY_COMMON_TROPHIES_H_
#define KYTY_COMMON_TROPHIES_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
@@ -9,11 +10,15 @@
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace Common::Trophies {
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
inline constexpr uint64_t UnixEpochTick = 62135596800000000ULL;
enum class Comparison { None, GreaterEqual, Greater, LessEqual, Less };
struct Trophy {
int id = 0;
@@ -21,6 +26,9 @@ struct Trophy {
int platinum_id = -1;
int grade = 0;
std::optional<uint64_t> target;
std::optional<uint64_t> uds_stat_id;
Comparison comparison = Comparison::None;
bool progressive = false;
std::string name;
std::string description;
std::string reward;
@@ -29,19 +37,50 @@ struct Trophy {
bool has_reward = false;
};
struct UdsRule {
uint64_t stat_id;
std::string input;
bool is_uint32;
};
using UdsRules = std::map<std::string, std::vector<UdsRule>>;
using UdsInteger = std::variant<int32_t, uint32_t, uint64_t>;
struct Package {
std::string title;
std::map<int, std::string> groups;
std::map<int, Trophy> trophies;
UdsRules event_rules;
};
struct UnlockData {
std::set<int> unlocked;
std::map<int, uint64_t> timestamps;
};
struct Progress {
std::array<uint32_t, 5> total_grade {};
std::array<uint32_t, 5> earned_grade {};
uint32_t total = 0;
uint32_t earned = 0;
[[nodiscard]] uint32_t Percentage() const;
};
[[nodiscard]] Progress GetProgress(const Package& package, const UnlockData& unlocks,
std::optional<int> group = std::nullopt);
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
[[nodiscard]] std::filesystem::path UdsPackagePath(uint32_t service_label);
[[nodiscard]] UdsRules LoadUdsRules(const std::filesystem::path& path);
[[nodiscard]] std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties);
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
std::string_view title_id, int user_id,
uint32_t service_label);
[[nodiscard]] std::set<int> LoadUnlocks(const std::filesystem::path& path);
[[nodiscard]] bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked);
[[nodiscard]] UnlockData LoadUnlockData(const std::filesystem::path& path);
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
} // namespace Common::Trophies
+3 -52
View File
@@ -876,64 +876,15 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, uint32_t draw_initiator, bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
const auto* args_addr =
reinterpret_cast<const void*>(m_draw_indirect_args_base_addr + data_offset);
if (!indexed) {
DrawIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
m_num_instances = args.instance_count;
DrawIndexAuto({.vertex_count = args.vertex_count_per_instance,
.instance_count = args.instance_count,
.first_vertex = args.start_vertex_location,
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
return;
}
DrawIndexedIndirectArgs args {};
std::memcpy(&args, args_addr, sizeof(args));
uint64_t index_size = 0;
switch (m_index_type_and_size) {
case 0: index_size = 2; break;
case 1: index_size = 4; break;
case 2: index_size = 1; break;
default: EXIT("unknown index_type_and_size: %u\n", m_index_type_and_size);
}
auto* index_addr = reinterpret_cast<const void*>(
m_index_base_addr + static_cast<uint64_t>(args.start_index_location) * index_size);
const uint32_t index_count =
(m_index_buffer_size != 0 ? std::min(args.index_count_per_instance, m_index_buffer_size)
: args.index_count_per_instance);
if (GraphicsRunDebugDumpEnabled() && index_count != args.index_count_per_instance) {
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1, std::memory_order_relaxed) < 64) {
LOGF("\t DrawIndexIndirect: clamped index_count from %" PRIu32 " to %" PRIu32
" using INDEX_BUFFER_SIZE\n",
args.index_count_per_instance, index_count);
}
}
m_num_instances = args.instance_count;
DrawIndex({.index_count = index_count,
.index_addr = index_addr,
.instance_count = args.instance_count,
.base_vertex = static_cast<int32_t>(args.base_vertex_location),
.first_instance = args.start_instance_location,
.offset_source = DrawOffsetSource::IndirectArgs});
const auto args_size = indexed ? sizeof(DrawIndexedIndirectArgs) : sizeof(DrawIndirectArgs);
DrawIndirectMulti(data_offset, 1, nullptr, args_size, draw_initiator, indexed);
}
void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_count_or_count,
const volatile uint32_t* count_addr,
uint32_t stride_in_bytes, uint32_t draw_initiator,
bool indexed) {
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != 2u);
EXIT_NOT_IMPLEMENTED((draw_initiator & ~0x20u) != (indexed ? 0u : 2u));
EXIT_NOT_IMPLEMENTED(m_draw_indirect_args_base_addr == 0);
uint32_t draw_count = max_count_or_count;
@@ -315,8 +315,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
r.depth_bounds_test_enable = dc.depth_bounds_enable;
r.depth_min_bounds = hw.GetDepthBoundsMin();
r.depth_max_bounds = hw.GetDepthBoundsMax();
r.stencil_clear_enable =
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
@@ -29,8 +29,6 @@ struct RenderDepthInfo {
bool depth_write_enable = false;
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
bool stencil_clear_enable = false;
uint8_t stencil_clear_value = 0;
bool stencil_test_enable = false;
@@ -390,7 +390,28 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
const auto mantissa = value & 0x3ffu;
if (exponent == 0) {
return std::copysign(static_cast<float>(mantissa) * 0x1p-24f,
std::bit_cast<float>(sign));
}
return std::bit_cast<float>(sign | ((exponent == 31 ? 255u : exponent + 112u) << 23u) |
(mantissa << 13u));
};
switch (format) {
case vk::Format::eR16G16Sfloat:
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
break;
// A single-plane float target carries its clear as raw float bits, the same encoding the
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
// contents.
@@ -443,6 +464,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -807,30 +807,52 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
}
}
void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
auto& cache = m_context.GetBufferCache();
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
return;
}
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
const auto requested_size = descriptor.GetSize();
if (address == 0 || requested_size == 0) {
prepared.buffer_sources.push_back({});
auto& cache = m_context.GetBufferCache();
for (auto* stage: stages) {
auto& prepared = *stage;
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
continue;
}
const auto size = Libs::LibKernel::Memory::ClampRangeSize(address, requested_size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
auto size = descriptor.GetSize();
if (address == 0 || size == 0) {
prepared.buffer_sources.push_back({});
continue;
}
if (descriptor.NumRecords() == UINT32_MAX) {
if (program.info.buffers[resource].written) {
// Sentinel write ranges end before tables captured by any bound stage.
for (const auto* reader: stages) {
for (const auto [read_address, read_size]: reader->runtime->resources->specialization_reads) {
if (read_size == 0) continue;
if (read_address > address) {
size = std::min(size, read_address - address);
} else if (address - read_address < read_size) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
}
} else {
const auto& graphics = m_context.GetGraphics();
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
(graphics.StorageMinAlignment() - 1);
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
}
}
}
@@ -919,8 +941,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
std::span<RenderColorInfo> colors) {
bool uses_dma = false;
FindBuffers(stages);
for (auto* stage: stages) {
FindBuffers(*stage);
uses_dma |= stage->runtime->program->info.uses_dma;
}
if (uses_dma) {
@@ -831,9 +831,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
}
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
static_params.depth_min_bounds = depth.depth_min_bounds;
static_params.depth_max_bounds = depth.depth_max_bounds;
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
@@ -38,9 +38,6 @@ struct PipelineStaticParameters {
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
@@ -64,7 +61,7 @@ struct PipelineStaticParameters {
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 125);
static_assert(sizeof(PipelineStaticParameters) == 116);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
@@ -471,14 +471,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
depth_stencil_info.depthBoundsTestEnable =
#if defined(__APPLE__)
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
#else
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
#endif
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
std::vector<vk::DynamicState> dynamic_states {
vk::DynamicState::eViewportWithCount,
@@ -497,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::DynamicState::eBlendConstants,
};
#if !defined(__APPLE__)
dynamic_states.push_back(vk::DynamicState::eDepthBoundsTestEnable);
dynamic_states.push_back(vk::DynamicState::eDepthBounds);
if (rendering.color_count != 0) {
dynamic_states.push_back(vk::DynamicState::eColorWriteEnableEXT);
}
+1 -1
View File
@@ -161,7 +161,7 @@ public:
uint32_t mode);
void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared);
void FindBuffers(PreparedBindings& bindings);
void FindBuffers(std::span<PreparedBindings* const> stages);
void RebindBuffers(PreparedBindings& bindings);
void RebindImages(PreparedBindings& bindings);
void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point,
@@ -342,7 +342,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
std::copy(std::begin(input_info.dispatch_threads_num), std::end(input_info.dispatch_threads_num),
bindings.shader_data.begin() + program.bindings.dispatch_thread_dword);
}
FindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
if (program.info.uses_dma) {
m_context.PrepareBda();
}
@@ -351,7 +352,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
RebindBuffers(bindings);
auto vk_buffer = buffer.Handle();
PreparedBindings* descriptor_stage = &bindings;
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
std::span {&descriptor_stage, 1u});
bool has_storage_writes = bindings.shared_memory.buffer != nullptr ||
@@ -457,7 +457,8 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& bindings = m_compute_bindings;
PrepareBindings(input_info.stage, bindings);
FindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
const auto& program = *input_info.stage.program;
if (program.info.uses_dma) {
m_context.PrepareBda();
@@ -469,7 +470,6 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
args_addr, sizeof(vk::DispatchIndirectCommand), false);
EXIT_IF(args_buffer == nullptr || (args_offset & 3u) != 0);
RebindBuffers(bindings);
PreparedBindings* descriptor_stage = &bindings;
CommitBindings(buffer, vk::PipelineBindPoint::eCompute, pipeline,
std::span {&descriptor_stage, 1u});
const auto vk_buffer = buffer.Handle();
@@ -379,6 +379,12 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
#if !defined(__APPLE__)
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
if (depth.depth_bounds_test_enable) {
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
}
#endif
const auto& mode = ctx.GetModeControl();
const auto& poly_offset = ctx.GetPolyOffset();
+49 -37
View File
@@ -1100,56 +1100,68 @@ struct SystemOverlay::Impl {
static_cast<float>(frame_extent.height) / 1080.0f),
0.65f, 1.25f);
const float width = std::max(
0.0f, std::min(440.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
const float height = 104.0f * screen_scale;
const float rounding = 14.0f * screen_scale;
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
const float alpha = std::min(fade_in, fade_out);
const float slide = (1.0f - fade_in) * (width + 24.0f);
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 24.0f - width + slide,
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
32.0f};
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
auto* draw = ImGui::GetForegroundDrawList();
draw->AddRectFilled({top_left.x + 5.0f, top_left.y + 6.0f},
{bottom_right.x + 5.0f, bottom_right.y + 6.0f},
IM_COL32(0, 0, 0, static_cast<int>(95.0f * alpha)), 10.0f);
draw->AddRectFilled(top_left, bottom_right,
IM_COL32(18, 21, 29, static_cast<int>(242.0f * alpha)), 10.0f);
draw->AddRect(top_left, bottom_right,
IM_COL32(190, 154, 78, static_cast<int>(200.0f * alpha)), 10.0f,
ImDrawFlags_RoundCornersAll, 1.5f);
const auto fade = [alpha](int r, int g, int b, int a) {
return IM_COL32(r, g, b, static_cast<int>(static_cast<float>(a) * alpha));
};
draw->AddRectFilled({top_left.x + 3.0f, top_left.y + 5.0f},
{bottom_right.x + 3.0f, bottom_right.y + 5.0f}, fade(0, 0, 0, 90),
rounding);
draw->AddRectFilled(top_left, bottom_right, fade(24, 26, 32, 235), rounding);
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
ImDrawFlags_RoundCornersAll, 1.0f);
const float pad = 10.0f * screen_scale;
const float icon_size = height - 2.0f * pad;
float text_left = top_left.x + 20.0f * screen_scale;
if (trophy_notification_image != nullptr) {
constexpr float ICON_SIZE = 88.0f;
const ImVec2 icon_min {top_left.x + 8.0f * screen_scale,
top_left.y + (height - ICON_SIZE * screen_scale) * 0.5f};
const ImVec2 icon_max {icon_min.x + ICON_SIZE * screen_scale,
icon_min.y + ICON_SIZE * screen_scale};
draw->AddImage(trophy_notification_image->GetTexRef(), icon_min, icon_max, {0, 0},
{1, 1}, IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)));
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
ImDrawFlags_RoundCornersAll, 1.0f);
text_left = icon_max.x + 18.0f * screen_scale;
}
const ImVec2 text_position {top_left.x +
(trophy_notification_image ? 112.0f : 18.0f) * screen_scale,
top_left.y + 18.0f * screen_scale};
draw->PushClipRect(top_left, {bottom_right.x - 12.0f * screen_scale, bottom_right.y}, true);
draw->AddText(nullptr, 13.0f * screen_scale, text_position,
IM_COL32(232, 203, 139, static_cast<int>(255.0f * alpha)), "Trophy earned");
draw->AddText(nullptr, 21.0f * screen_scale,
{text_position.x, text_position.y + 22.0f * screen_scale},
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)),
trophy_notification->name.c_str());
constexpr std::array GRADE_LABELS {"Platinum", "Gold", "Silver", "Bronze"};
constexpr std::array GRADE_COLORS {
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
if (grade > 0 && grade <= static_cast<int>(GRADE_LABELS.size())) {
draw->AddText(nullptr, 13.0f * screen_scale,
{text_position.x, text_position.y + 50.0f * screen_scale},
ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha),
GRADE_LABELS[grade - 1]);
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
float title_x = text_left;
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
// Small cup glyph tinted with the trophy grade.
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
const float u = screen_scale;
const float cx = text_left + 9.0f * u;
const float cy = title_y + 15.0f * u;
draw->AddRectFilled({cx - 8.0f * u, cy - 11.0f * u}, {cx + 8.0f * u, cy + 2.0f * u},
cup, 2.0f * u, ImDrawFlags_RoundCornersBottom);
draw->AddRect({cx - 11.0f * u, cy - 9.0f * u}, {cx - 6.0f * u, cy - 3.0f * u}, cup,
2.0f * u, ImDrawFlags_RoundCornersLeft, 1.8f * u);
draw->AddRect({cx + 6.0f * u, cy - 9.0f * u}, {cx + 11.0f * u, cy - 3.0f * u}, cup,
2.0f * u, ImDrawFlags_RoundCornersRight, 1.8f * u);
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
1.5f * u);
title_x += 30.0f * u;
}
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
fade(255, 255, 255, 255), trophy_notification->name.c_str());
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
fade(176, 180, 192, 255), "Trophy earned!");
draw->PopClipRect();
}
@@ -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);
+11 -5
View File
@@ -485,15 +485,21 @@ struct BlockInfo {
struct DescriptorSource {
struct IndirectDescriptor {
uint32_t material_source = UINT32_MAX;
struct SelectorRead {
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
bool operator==(const SelectorRead& other) const = default;
};
std::optional<SelectorRead> selector;
uint32_t table_source = 0;
uint32_t selector_stride = 0;
uint32_t selector_offset = 0;
uint32_t table_offset = 0;
uint32_t table_immediate = 0;
uint32_t table_stride = 0;
uint32_t table_record_bytes = 0;
bool table_scalar = false;
uint32_t workgroup_axis = UINT32_MAX;
uint32_t selector_shift = 0;
uint32_t selector_bits = UINT32_MAX;
Value key_count;
Value selector_first;
Value selector_mask;
@@ -101,7 +101,7 @@ SharedMemoryResources CollectMemoryResources(const Program& program, std::vector
bool UsesFlattenedSrt(const Program& program) {
const auto uses_mapping = [](const auto& resource) {
return resource.indirect_search_iterations != 0u;
return resource.indirect_root != UINT32_MAX;
};
return std::ranges::any_of(program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
@@ -238,6 +238,13 @@ bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
return true;
}
bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u;
}
template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect,
@@ -249,7 +256,24 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t maximum_resources, Normalize&& normalize) {
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
auto& keys = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) return false;
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
return true;
};
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
uint64_t step, uint64_t count) {
keys.resize(count);
@@ -269,8 +293,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.clear();
DescriptorValue material_value;
DescriptorValue table_value;
if ((indirect.material_source != UINT32_MAX &&
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
if ((selector != nullptr &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
}
@@ -283,7 +307,48 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
return false;
}
if (indirect.material_source == UINT32_MAX) {
if (IsBoundedDescriptorTable(program, indirect)) {
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
descriptors[resource_index] = {.dword_count = dword_count};
return true;
}
if (table_size > UINT32_MAX ||
(indirect.table_record_bytes != 0u &&
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
return false;
// Descriptor bytes are refreshed together. The GPU selects the DWORD address;
// no material selector or unused table field is interpreted on the CPU.
keys.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false;
const auto mapping_offset = snapshot.flattened_srt.size();
descriptors[resource_index] = {.dword_count = dword_count};
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
for (uint32_t word = 0; word < keys.size(); ++word) {
DescriptorValue candidate {.dword_count = dword_count};
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
candidate.dwords.begin());
if (!normalize(candidate)) return false;
uint32_t ordinal;
if (!intern_candidate(candidate, ordinal)) return false;
if (ordinal == 0u) continue;
snapshot.flattened_srt.resize(mapping_offset + 2u + word, 0u);
snapshot.flattened_srt[mapping_offset + 1u + word] = ordinal;
}
if (descriptors.size() == children_begin) {
snapshot.flattened_srt.resize(mapping_offset);
} else {
snapshot.flattened_srt[mapping_offset] =
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
auto& root = specializations[resource_index];
root.indirect_root = resource_index;
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
root.indirect_search_iterations = 0u;
}
return true;
}
if (selector == nullptr) {
uint32_t key_count = 0;
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
@@ -295,7 +360,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
}
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(key_count != 0u && uint64_t {indirect.table_offset} +
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
UINT32_MAX + 1ull)) {
return false;
@@ -306,20 +371,18 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
ShaderBufferResource material;
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) &&
(material.Base48() & 3u) != 0u) ||
material.SwizzleEnabled() ||
material.AddTid() || material.OutOfBounds() != 0u ||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
uint64_t {selector->offset} + 4u > material.Stride() ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
uint64_t {first} + count > material.NumRecords())
return false;
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
indirect.selector_offset + 4u >
selector->offset + 4u >
uint64_t {UINT32_MAX} + 1u)
return false;
if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset,
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
material.Stride(), count)) return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
@@ -335,8 +398,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
static_cast<uint64_t>(index) * indirect.selector_stride;
const auto offset = static_cast<uint64_t>(selector->offset) +
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
@@ -344,37 +407,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.push_back(key);
mask &= mask - 1u;
}
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) {
return false;
}
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectDescriptorProbes) {
return false;
}
if (!read_keys(material, first, step, probe_count)) return false;
if (indirect.selector_shift == 0u) keys.push_back(0u);
}
if (indirect.material_source != UINT32_MAX) {
for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits;
if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) {
keys.push_back(0u); // An out-of-range material load returns zero.
}
} else return false;
if (selector != nullptr) {
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
}
const auto children_begin = descriptors.size();
const auto mapping_offset = snapshot.flattened_srt.size();
const auto root_resource = specializations[resource_index];
const auto key_count = sources.empty() ? keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
@@ -383,8 +423,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
DescriptorValue candidate;
candidate.dword_count = dword_count;
if (sources.empty()) {
const auto table_offset =
static_cast<uint32_t>(key * indirect.table_stride) + indirect.table_offset;
const uint64_t table_offset =
uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(dword_count))) {
return false;
@@ -396,19 +437,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t ordinal = 0;
if (entry == 0) {
descriptors[resource_index] = candidate;
} else if (descriptors[resource_index] != candidate) {
const auto found =
std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) {
return false;
}
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
} else if (!intern_candidate(candidate, ordinal)) {
return false;
}
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
@@ -566,9 +596,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
? snapshot.flattened_srt[root.indirect_mapping_offset]
: 0u;
if (root.indirect_search_iterations == 0u || key_count < 2u ||
if (key_count == 0u ||
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
static_cast<size_t>(key_count) * 2u >
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
snapshot.flattened_srt.size()) {
return SpecializationFail("indirect image specialization has an invalid key mapping");
}
@@ -1010,9 +1040,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
plan.requires_specialization_memory = true;
capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty();
MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots,
indirect.selector_mask);
capture_indirect_reads = true;
if (indirect.selector)
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
if (indirect.sources.empty()) {
@@ -1156,10 +1187,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
continue;
}
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128))
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
value.dwords.fill(0u);
return true;
})) {
@@ -8,7 +8,6 @@
#include <algorithm>
#include <fmt/format.h>
#include <map>
#include <numeric>
#include <optional>
#include <span>
#include <tuple>
@@ -374,11 +373,68 @@ public:
}
}
for (const auto& plan: m_indirect_descriptors) {
auto key = plan.key;
if (plan.table_read != nullptr) {
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
auto* block = anchor->Parent();
const auto where = std::ranges::find_if(block->Instructions(),
[&](const Inst& inst) { return &inst == anchor; });
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
return Value(&*block->PrependNewInst(where, op, args));
};
const auto& read = *plan.table_read;
const auto& memory = m_program.memory_info[read.Flags<MemoryFlags>().index];
Value offset;
Value valid(true);
if (plan.table_indexed) {
const auto* table = read.Arg(0).ResolveInstruction();
const auto stride = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(1), Value(16u), Value(14u)});
const auto format = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(3), Value(12u), Value(7u)});
offset = emit(ValueOpcode::IAdd32,
{emit(ValueOpcode::IMul32, {read.Arg(1), stride}), Value(memory.offset)});
valid = emit(ValueOpcode::LogicalAnd,
{emit(ValueOpcode::ULessThan32, {read.Arg(1), table->Arg(2)}),
emit(ValueOpcode::INotEqual32, {format, Value(0u)})});
} else {
const auto dynamic = emit(ValueOpcode::BitwiseAnd32, {read.Arg(1), Value(~3u)});
offset = emit(ValueOpcode::IAdd32, {dynamic, Value(memory.offset & ~3u)});
valid = emit(ValueOpcode::ULessThanEqual32, {dynamic, offset});
}
key = emit(ValueOpcode::SelectU32, {valid, offset, Value(UINT32_MAX)});
if (!plan.table_indexed) {
// NativeDescriptorSource proved this read reaches the image. Preserve its
// SSA Phi edges while retaining descriptor DWORDs for other consumers.
std::map<const Inst*, Value> projected;
const auto project = [&](auto&& self, Value value) -> Value {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (phi == plan.table_read) return key;
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi) return Value(UINT32_MAX);
if (const auto found = projected.find(phi); found != projected.end()) return found->second;
auto* parent = phi->Parent();
const auto position = std::ranges::find_if(parent->Instructions(),
[&](const Inst& inst) { return &inst == phi; });
auto& selected = *parent->PrependNewInst(position, ValueOpcode::Phi);
selected.SetFlags(Type::U32);
projected.emplace(phi, Value(&selected));
for (uint32_t arm = 0; arm < phi->NumArgs(); ++arm)
selected.AddPhiOperand(phi->PhiBlock(arm), self(self, phi->Arg(arm)));
return Value(&selected);
};
key = project(project, plan.handle->Arg(0));
}
plan.handle->SetArg(0, key);
for (uint32_t word = 1; word < plan.handle->NumArgs(); ++word)
plan.handle->SetArg(word, Value(0u));
continue;
}
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
plan.handle->SetArg(0, plan.key);
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
} else {
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, plan.key);
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, key);
for (uint32_t dword = 0; dword < 4u; dword++) {
plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]);
}
@@ -437,6 +493,10 @@ private:
std::array<Value, 8> roots {};
std::array<uint32_t, 8> memory {};
std::array<const Inst*, 8> reads {};
bool split_offsets = false;
bool retain_reads = false;
bool table_indexed = false;
const Inst* table_read = nullptr;
};
[[noreturn]] void Fail(uint32_t pc, const std::string& reason) const {
@@ -834,7 +894,7 @@ private:
uint32_t InternSource(const DescriptorSource& descriptor) {
for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) {
const auto& current = m_sources[candidate];
auto& current = m_sources[candidate];
if (current.dword_count != descriptor.dword_count ||
current.indirect_descriptor.has_value() != descriptor.indirect_descriptor.has_value()) {
continue;
@@ -842,10 +902,9 @@ private:
if (current.indirect_descriptor.has_value()) {
const auto& a = *current.indirect_descriptor;
const auto& b = *descriptor.indirect_descriptor;
if (a.material_source != b.material_source || a.table_source != b.table_source ||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits ||
a.table_offset != b.table_offset || a.table_stride != b.table_stride ||
if (a.selector != b.selector || a.table_source != b.table_source ||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
a.table_stride != b.table_stride ||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
@@ -860,6 +919,12 @@ private:
same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]);
}
if (same) {
if (current.indirect_descriptor) {
current.indirect_descriptor->table_scalar |= descriptor.indirect_descriptor->table_scalar;
current.indirect_descriptor->table_record_bytes = std::max(
current.indirect_descriptor->table_record_bytes,
descriptor.indirect_descriptor->table_record_bytes);
}
return candidate;
}
}
@@ -949,54 +1014,6 @@ private:
return ValidateSource(descriptor, bad_dword);
}
bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const {
stride = 1u;
offset = 0u;
value = value.Resolve();
while (const auto* inst = value.TryInstruction()) {
if (inst->NumArgs() != 2u) break;
uint32_t constant = 0;
Value remaining;
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
ImmediateU32(inst->Arg(1), constant) && constant < 32u) {
stride *= 1u << constant;
remaining = inst->Arg(0);
} else if (inst->GetOpcode() == ValueOpcode::IAdd32 ||
inst->GetOpcode() == ValueOpcode::IMul32) {
if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1);
else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0);
else break;
if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant;
else stride *= constant;
} else {
break;
}
value = remaining.Resolve();
}
// Treat the remaining GPU-derived scalar as any U32 value.
return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate();
}
Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
indirect.selector_shift = 0u;
indirect.selector_bits = UINT32_MAX;
const auto* inst = key.Resolve().TryInstruction();
uint32_t immediate;
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) {
if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0);
else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1);
else return key;
indirect.selector_bits = immediate;
inst = key.Resolve().TryInstruction();
}
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) {
indirect.selector_shift = immediate;
key = inst->Arg(0);
}
return key.Resolve();
}
enum class LaneQuantifier { Any, All };
struct EdgePredicate {
Value condition;
@@ -1443,9 +1460,9 @@ private:
static_cast<uint64_t>(UINT32_MAX) + 1u) return false;
const auto mask = BoundedSetBitMask(offset.index, active);
if (mask.IsEmpty()) return false;
indirect.material_source = InternSource(material_source);
indirect.selector_stride = static_cast<uint32_t>(offset.stride);
indirect.selector_offset = static_cast<uint32_t>(offset.offset + memory.offset);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.stride = static_cast<uint32_t>(offset.stride),
.offset = static_cast<uint32_t>(offset.offset + memory.offset)});
indirect.key_count = Value(32u);
indirect.selector_mask = mask;
return true;
@@ -1606,8 +1623,9 @@ private:
bool MatchDescriptorTable(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan,
DescriptorSource& table_source, Value& key, uint32_t& table_offset,
uint32_t& table_stride) {
uint32_t& table_stride, uint32_t& table_immediate) {
Inst* table_handle = nullptr;
Value table_guard;
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
if (read == nullptr) {
@@ -1615,7 +1633,31 @@ private:
}
uint32_t memory_index = 0;
const auto* memory = ScalarReadMemory(*read, memory_index);
if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
uint32_t component = 0;
const bool indexed = memory == nullptr;
if (indexed) {
if (handle.NumArgs() != 8u) return false;
const auto* selected = UniformizedMaterialValue(descriptor.dwords[dword], handle);
if (selected == nullptr) return false;
const auto* extract = selected->Arg(1).Resolve().TryInstruction();
if (extract == nullptr || extract->GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!ImmediateU32(extract->Arg(1), component) || component >= 4u) return false;
read = extract->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x4) return false;
memory_index = read->Flags<MemoryFlags>().index;
if (memory_index >= m_program.memory_info.size()) return false;
memory = &m_program.memory_info[memory_index];
uint32_t offset = 1u, scalar = 1u;
if (memory->kind != ResourceKind::Buffer || memory->typed || memory->formatted ||
!memory->idxen || memory->offen || memory->data_bits != 32u ||
memory->data_dwords != 4u ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, read->Arg(4), selected->Arg(0)) ||
(dword != 0u && !EquivalentValue(m_program, table_guard, read->Arg(4)))) return false;
table_guard = read->Arg(4);
}
if (memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
!MemoryIndexBelongsTo(memory_index, *read)) {
return false;
}
@@ -1630,23 +1672,32 @@ private:
(memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) ||
(table_handle != nullptr &&
!EquivalentValue(m_program, Value(table_handle), Value(current_handle))) ||
!MatchTableOffset(read->Arg(1), current_key, offset, stride) ||
memory->offset > UINT32_MAX - offset) {
(!indexed && !MatchTableOffset(read->Arg(1), current_key, offset, stride))) {
return false;
}
offset += memory->offset;
if (indexed) {
current_key = read->Arg(1);
offset = component * 4u;
}
if (dword == 0u) {
if (component != 0u) return false;
plan.table_indexed = indexed;
key = current_key;
table_offset = offset;
table_immediate = memory->offset;
table_stride = stride;
} else if (table_stride != stride || !EquivalentValue(m_program, key, current_key) ||
static_cast<uint64_t>(table_offset) + dword * sizeof(uint32_t) != offset) {
} else if (plan.table_indexed != indexed || table_stride != stride ||
!EquivalentValue(m_program, key, current_key) ||
uint64_t {table_offset} + table_immediate + dword * sizeof(uint32_t) !=
uint64_t {offset} + memory->offset) {
return false;
}
plan.split_offsets |= !indexed && offset != table_offset;
table_handle = current_handle;
if (handle.NumArgs() == 8u
? WorkgroupAxis(key) == UINT32_MAX && !UsesOnlyImageDescriptors(*read) :
std::ranges::any_of(read->Uses(), [](const Use& use) {
if (handle.NumArgs() == 8u) {
if (memory->kind == ResourceKind::ScalarAddress)
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
} else if (std::ranges::any_of(read->Uses(), [](const Use& use) {
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
std::ranges::any_of(use.user->Uses(), [](const Use& consumer) {
return consumer.user->GetOpcode() != ValueOpcode::StoreBufferU32;
@@ -1664,34 +1715,6 @@ private:
return true;
}
bool MatchUniformizedBufferKey(Value key, const Inst& image,
DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
const auto* selected = UniformizedMaterialValue(key, image);
if (selected == nullptr) return false;
const auto value = MaterialKey(selected->Arg(1), indirect);
if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false;
const auto* loaded = value.TryInstruction();
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false;
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
uint32_t offset = 1u, scalar = 1u;
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) ||
!MakeRuntimeTableSource(*read, material_source)) return false;
// GPU index arithmetic may wrap. Every record in the bounded V# is a candidate.
indirect.material_source = InternSource(material_source);
indirect.selector_first = Value(0u);
indirect.key_count = material_source.dwords[2];
indirect.selector_offset = memory.offset;
return true;
}
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
@@ -1699,17 +1722,19 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride)) return false;
table_stride, table_immediate)) return false;
DescriptorSource material_source;
DescriptorSource::IndirectDescriptor indirect;
indirect.table_offset = table_offset;
indirect.table_immediate = table_immediate;
indirect.table_stride = table_stride;
indirect.workgroup_axis = WorkgroupAxis(key);
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
// only the image handle is projected onto the bounded workgroup key.
plan.reads.fill(nullptr);
plan.retain_reads = true;
} else if (table_source.dword_count == 2u) {
if (table_stride != 32u) return false;
const auto* selector = key.Resolve().TryInstruction();
@@ -1728,36 +1753,21 @@ private:
}
if ((table_offset & 3u) != 0u ||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
} else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) {
auto* material_read = MaterialKey(key, indirect).TryInstruction();
uint32_t material_memory_index = 0;
const auto* memory = material_read != nullptr
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
return false;
}
if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride,
indirect.selector_offset)) {
return false;
}
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
indirect.selector_offset =
(static_cast<uint32_t>(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u);
const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction();
if (material_handle == nullptr ||
material_handle->GetOpcode() != ValueOpcode::GetBufferResource ||
!MakeRuntimeTableSource(*material_read, material_source)) {
return false;
}
indirect.material_source = InternSource(material_source);
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
if (plan.split_offsets) return false;
indirect.table_stride = 0u;
indirect.table_offset = 0u;
indirect.table_immediate = 0u;
indirect.workgroup_axis = UINT32_MAX;
plan.table_read = plan.reads[0];
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
else indirect.table_scalar = true;
plan.retain_reads = true;
}
// This plan combines shader byte-offset additions with the scalar immediate.
// A nonzero combined offset is exact only when neither can cross U32 wrap.
const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits;
if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u &&
table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX)
return false;
if (plan.retain_reads) plan.reads.fill(nullptr);
indirect.table_source = InternSource(table_source);
DescriptorSource image_source;
image_source.dword_count = 8u;
@@ -1791,9 +1801,10 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u) return false;
table_stride, table_immediate) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u || table_immediate != 0u) return false;
const auto* lane_read = key.Resolve().TryInstruction();
if (lane_read == nullptr || lane_read->GetOpcode() != ValueOpcode::ReadFirstLane)
return false;
@@ -1825,9 +1836,9 @@ private:
indirect.table_stride = table_stride;
if (!MakeRuntimeTableSource(*read, material_source) ||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
indirect.material_source = InternSource(material_source);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.offset = memory.offset + lane * 4u});
indirect.table_source = InternSource(table_source);
indirect.selector_offset = memory.offset + lane * 4u;
DescriptorSource source;
source.dword_count = 4u;
source.dwords.fill(Value(0u));
+3
View File
@@ -121,6 +121,9 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) {
event.event = event.pending_events.front();
event.pending_events.pop_front();
event.triggered = true;
} else {
// A persistent level is delivered again by the next wait, not this batch.
break;
}
if (ret >= num) {
@@ -169,6 +169,28 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="trophy_overview_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>View saved trophies</string>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
+6 -1
View File
@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#000" d="M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M26 16a10 10 0 1 1-2.93-7.07L27 13"/>
<path d="M27 5v8h-8"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 252 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M9 5h14v8a7 7 0 0 1-14 0Z"/>
<path d="M9 8H5v3a4 4 0 0 0 4 4M23 8h4v3a4 4 0 0 1-4 4"/>
<path d="M16 20v4M12 28v-4h8v4M11 28h10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 325 B

+26
View File
@@ -0,0 +1,26 @@
#ifndef CHEAT_FILE_H
#define CHEAT_FILE_H
#include <QByteArray>
#include <QJsonDocument>
#include <QList>
#include <QString>
namespace Cheats {
[[nodiscard]] bool IsSupportedTitleId(const QString& title_id);
[[nodiscard]] QString PlanPath(const QString& title_id);
// Local files retain their existing defaults; remote imports receive additional validation.
[[nodiscard]] QString ParseDocument(const QByteArray& data, QJsonDocument* document);
[[nodiscard]] QString ValidateImport(const QJsonDocument& document, const QString& title_id,
const QString& version, const QString& process);
[[nodiscard]] QJsonDocument WithSelection(const QJsonDocument& document,
const QList<bool>& enabled);
// Refuse to replace a file changed since it was loaded; empty data also permits creation.
[[nodiscard]] QString SaveDocument(const QString& path, const QJsonDocument& document,
const QByteArray& expected_data);
} // namespace Cheats
#endif // CHEAT_FILE_H
+66
View File
@@ -0,0 +1,66 @@
#ifndef CHEAT_REPOSITORY_H
#define CHEAT_REPOSITORY_H
#include <QByteArray>
#include <QList>
#include <QNetworkAccessManager>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <functional>
class QNetworkReply;
namespace Cheats {
struct RemoteFile {
QString name;
QString title;
QString version;
QString format;
[[nodiscard]] bool IsSupported() const;
[[nodiscard]] QUrl Url() const;
};
struct CatalogResult {
QList<RemoteFile> files;
QString error;
};
[[nodiscard]] CatalogResult ParseIndex(const QByteArray& data, const QString& format,
const QString& title_id);
class CheatRepository final: public QObject {
Q_OBJECT
public:
explicit CheatRepository(QObject* parent = nullptr);
void Load(const QString& title_id);
void Fetch(const RemoteFile& file);
void CancelFetch();
signals:
void CatalogLoaded(QList<Cheats::RemoteFile> files, QString error);
void FileLoaded(QByteArray data, QString error);
private:
QNetworkReply* Request(const QUrl& url, qint64 limit,
std::function<void(QByteArray, QString)> complete);
QNetworkAccessManager m_network;
QList<QNetworkReply*> m_catalog_replies;
QPointer<QNetworkReply> m_file_reply;
QList<RemoteFile> m_files;
QStringList m_errors;
int m_catalog_generation = 0;
int m_fetch_generation = 0;
int m_pending = 0;
};
} // namespace Cheats
#endif // CHEAT_REPOSITORY_H
@@ -43,6 +43,7 @@ public:
bool EnsureGameDirectory();
void ScanGameDirectory();
void ViewTrophies();
void ViewTrophyOverview();
signals:
+28 -7
View File
@@ -1,30 +1,51 @@
#ifndef PATCHES_DIALOG_H
#define PATCHES_DIALOG_H
#include "cheatRepository.h"
#include <QByteArray>
#include <QDialog>
#include <QJsonDocument>
#include <QString>
class Configuration;
class QLabel;
class QListWidget;
class QPushButton;
class QTabWidget;
class QTreeWidget;
class PatchesDialog final: public QDialog {
public:
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
~PatchesDialog() override = default;
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
private:
void Load();
void Save();
void LoadRemote();
void PreviewRemote();
void ImportRemote();
QString m_title_id;
QListWidget* m_patches = nullptr;
QLabel* m_status = nullptr;
QPushButton* m_apply = nullptr;
QString m_title_id;
QString m_version;
QString m_process;
QByteArray m_local_data;
QJsonDocument m_local_document;
QJsonDocument m_remote_document;
bool m_local_readable = false;
Cheats::CheatRepository* m_repository = nullptr;
QList<Cheats::RemoteFile> m_remote_files;
QTabWidget* m_tabs = nullptr;
QTreeWidget* m_files = nullptr;
QListWidget* m_preview = nullptr;
QLabel* m_remote_status = nullptr;
QLabel* m_preview_status = nullptr;
QPushButton* m_refresh = nullptr;
QPushButton* m_import = nullptr;
QListWidget* m_patches = nullptr;
QLabel* m_status = nullptr;
QPushButton* m_apply = nullptr;
};
#endif // PATCHES_DIALOG_H
@@ -3,6 +3,8 @@
#include <QDialog>
#include <vector>
class QTabWidget;
class QWidget;
@@ -14,6 +16,8 @@ public:
static bool HasTrophyData(const Configuration* info);
static void ShowForGame(const Configuration* info, const QString& runtime_directory,
QWidget* parent);
static void ShowOverview(const std::vector<const Configuration*>& games,
const QString& runtime_directory, QWidget* parent);
private:
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
+2
View File
@@ -5,6 +5,8 @@
<file alias="input-mapping.svg">icons/input-mapping.svg</file>
<file alias="refresh.svg">icons/refresh.svg</file>
<file alias="remove-configuration.svg">icons/remove-configuration.svg</file>
<file alias="trophy.svg">icons/trophy.svg</file>
<file alias="hidden-trophy.png">../../assets/images/hidden-trophy.png</file>
</qresource>
<qresource prefix="/styles">
<file alias="launcher.qss">styles/launcher.qss</file>
+180
View File
@@ -0,0 +1,180 @@
#include "cheatFile.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSaveFile>
namespace Cheats {
namespace {
QString Tr(const char* text) {
return QCoreApplication::translate("Cheats", text);
}
QString ValidateDocument(const QJsonDocument& document) {
if (!document.isObject()) {
return Tr("Invalid cheat JSON.");
}
const auto mods = document.object().value(QStringLiteral("mods"));
if (!mods.isArray()) {
return Tr("Cheat JSON has no \"mods\" array.");
}
for (const auto& value: mods.toArray()) {
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!value.isObject() || !name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
return Tr("Invalid cheat entry.");
}
}
return {};
}
} // namespace
bool IsSupportedTitleId(const QString& title_id) {
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
return pattern.match(title_id.trimmed().toUpper()).hasMatch();
}
QString PlanPath(const QString& title_id) {
if (!IsSupportedTitleId(title_id)) {
return {};
}
return QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
}
QString ParseDocument(const QByteArray& data, QJsonDocument* document) {
const auto parsed = QJsonDocument::fromJson(data);
const auto error = ValidateDocument(parsed);
*document = error.isEmpty() ? parsed : QJsonDocument();
return error;
}
QString ValidateImport(const QJsonDocument& document, const QString& title_id,
const QString& version, const QString& process) {
const auto parse_error = ValidateDocument(document);
if (!parse_error.isEmpty()) {
return parse_error;
}
const auto root = document.object();
if (!IsSupportedTitleId(title_id) ||
root.value(QStringLiteral("id"))
.toString()
.compare(title_id.trimmed(), Qt::CaseInsensitive) != 0) {
return Tr("The cheat file does not match this game's title ID.");
}
if (version.isEmpty() || root.value(QStringLiteral("version")).toString() != version) {
return Tr("The cheat file does not match this game's version (%1).").arg(version);
}
if (process.isEmpty() ||
root.value(QStringLiteral("process")).toString().compare(process, Qt::CaseInsensitive) !=
0) {
return Tr("The cheat file does not match this game's executable (%1).").arg(process);
}
static const QRegularExpression offset_pattern(
QStringLiteral("\\A(?:0[xX])?[0-9a-fA-F]{1,16}\\z"));
static const QRegularExpression bytes_pattern(QStringLiteral("\\A(?:[0-9a-fA-F]{2})+\\z"));
const auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.isEmpty()) {
return Tr("The cheat file contains no cheats.");
}
if (root.contains(QStringLiteral("master"))) {
return Tr("This JSON uses master codes, which are not supported yet.");
}
for (const auto& value: mods) {
const auto mod = value.toObject();
const auto memory = mod.value(QStringLiteral("memory"));
if (mod.contains(QStringLiteral("module_name"))) {
return Tr("This JSON uses module-specific cheats, which are not supported yet.");
}
if (!memory.isArray() || memory.toArray().isEmpty()) {
return Tr("Unsupported memory entries in cheat \"%1\".")
.arg(mod.value(QStringLiteral("name")).toString());
}
for (const auto& entry: memory.toArray()) {
const auto write = entry.toObject();
const auto offset = write.value(QStringLiteral("offset")).toString();
const auto off = write.value(QStringLiteral("off")).toString();
const auto on = write.value(QStringLiteral("on")).toString();
const auto absolute = write.value(QStringLiteral("absolute"));
const auto expected = write.value(QStringLiteral("expected"));
if ((!absolute.isUndefined() && (!absolute.isBool() || absolute.toBool())) ||
(!expected.isUndefined() &&
(!expected.isString() ||
expected.toString().compare(off, Qt::CaseInsensitive) != 0)) ||
write.contains(QStringLiteral("section")) ||
write.contains(QStringLiteral("sectionName")) ||
write.contains(QStringLiteral("sectionProtection")) ||
write.contains(QStringLiteral("logicalOffset")) ||
write.contains(QStringLiteral("sectionOffset"))) {
return Tr("This JSON uses memory addressing features that are not supported yet.");
}
if (!entry.isObject() || !offset_pattern.match(offset).hasMatch() ||
!bytes_pattern.match(off).hasMatch() || !bytes_pattern.match(on).hasMatch() ||
off.size() != on.size()) {
return Tr("Unsupported memory entries in cheat \"%1\".")
.arg(mod.value(QStringLiteral("name")).toString());
}
}
}
return {};
}
QJsonDocument WithSelection(const QJsonDocument& document, const QList<bool>& enabled) {
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != enabled.size()) {
return {};
}
for (qsizetype index = 0; index < mods.size(); ++index) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), enabled[index]);
mods[index] = mod;
}
root.insert(QStringLiteral("mods"), mods);
return QJsonDocument(root);
}
QString SaveDocument(const QString& path, const QJsonDocument& document,
const QByteArray& expected_data) {
if (path.isEmpty() || !document.isObject()) {
return Tr("Invalid cheat file.");
}
QFile input(path);
if (input.exists()) {
if (!input.open(QIODevice::ReadOnly)) {
return Tr("Could not read cheat file: %1").arg(input.errorString());
}
const auto current = input.readAll();
if (input.error() != QFile::NoError) {
return Tr("Could not read cheat file: %1").arg(input.errorString());
}
if (current != expected_data) {
return Tr("Cheat file changed; reload the dialog.");
}
} else if (!expected_data.isEmpty()) {
return Tr("Cheat file changed; reload the dialog.");
}
input.close();
if (!QDir().mkpath(QFileInfo(path).absolutePath())) {
return Tr("Could not create the local cheats directory.");
}
QSaveFile output(path);
const auto data = document.toJson();
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() ||
!output.commit()) {
return Tr("Could not save cheat file: %1").arg(output.errorString());
}
return {};
}
} // namespace Cheats
+200
View File
@@ -0,0 +1,200 @@
#include "cheatRepository.h"
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QSet>
#include <QTimer>
#include <algorithm>
#include <utility>
namespace Cheats {
namespace {
constexpr qint64 kIndexLimit = 2 * 1024 * 1024;
constexpr qint64 kFileLimit = 8 * 1024 * 1024;
const QString kBaseUrl = QStringLiteral(
"https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/");
bool IsFormat(const QString& format) {
return format == QStringLiteral("json") || format == QStringLiteral("mc4") ||
format == QStringLiteral("shn");
}
QRegularExpressionMatch MatchName(const QString& name) {
static const QRegularExpression pattern(QStringLiteral(
"\\A([A-Z]{4}[0-9]{5})_([0-9]+(?:\\.[0-9]+)+)_[A-Za-z0-9_.-]+\\.(json|mc4|shn)\\z"));
return pattern.match(name);
}
} // namespace
bool RemoteFile::IsSupported() const {
return format == QStringLiteral("json") && !Url().isEmpty();
}
QUrl RemoteFile::Url() const {
const auto match = MatchName(name);
if (!IsFormat(format) || !match.hasMatch() || match.captured(3) != format) {
return {};
}
return QUrl(kBaseUrl + format + QLatin1Char('/') + name);
}
CatalogResult ParseIndex(const QByteArray& data, const QString& format, const QString& title_id) {
CatalogResult result;
if (!IsFormat(format) || data.isEmpty() || data.size() > kIndexLimit) {
result.error = CheatRepository::tr("Invalid cheat index.");
return result;
}
const auto id = title_id.trimmed().toUpper();
QSet<QString> seen;
int invalid = 0;
auto text = QString::fromUtf8(data);
if (text.startsWith(QChar(0xfeff))) {
text.remove(0, 1);
}
for (const auto& line: text.split(QLatin1Char('\n'))) {
const auto trimmed = line.trimmed();
if (trimmed.isEmpty()) {
continue;
}
const auto separator = trimmed.indexOf(QLatin1Char('='));
const auto name = trimmed.left(separator);
const auto match = MatchName(name);
if (separator < 0 || !match.hasMatch() || match.captured(3) != format) {
invalid++;
continue;
}
if (match.captured(1) != id || seen.contains(name)) {
continue;
}
seen.insert(name);
result.files.append(
{name, trimmed.mid(separator + 1).trimmed(), match.captured(2), format});
}
if (invalid != 0) {
result.error = CheatRepository::tr("Skipped %1 invalid cheat index entries.").arg(invalid);
}
return result;
}
CheatRepository::CheatRepository(QObject* parent): QObject(parent), m_network(this) {}
QNetworkReply* CheatRepository::Request(const QUrl& url, qint64 limit,
std::function<void(QByteArray, QString)> complete) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::SameOriginRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
reply->setReadBufferSize(limit + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply, limit]() {
if (reply->bytesAvailable() > limit) {
reply->setProperty("too_large", true);
reply->abort();
}
});
QTimer::singleShot(30000, reply, [reply]() {
if (!reply->isFinished()) {
reply->setProperty("timed_out", true);
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this,
[this, reply, limit, complete = std::move(complete)]() {
QString error;
if (reply->property("too_large").toBool() || reply->bytesAvailable() > limit) {
error = tr("Cheat download exceeds the size limit.");
} else if (reply->property("timed_out").toBool()) {
error = tr("Cheat download timed out.");
} else if (reply->error() != QNetworkReply::NoError) {
error = reply->errorString();
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) {
error = tr("Unexpected response from the cheat repository.");
}
const auto data = error.isEmpty() ? reply->readAll() : QByteArray();
m_catalog_replies.removeAll(reply);
if (m_file_reply == reply) {
m_file_reply.clear();
}
reply->deleteLater();
complete(data, error);
});
return reply;
}
void CheatRepository::Load(const QString& title_id) {
const auto generation = ++m_catalog_generation;
for (auto* reply: std::exchange(m_catalog_replies, {})) {
reply->abort();
}
CancelFetch();
m_files.clear();
m_errors.clear();
m_pending = 3;
for (const auto& format:
{QStringLiteral("json"), QStringLiteral("mc4"), QStringLiteral("shn")}) {
auto* reply =
Request(QUrl(kBaseUrl + format + QStringLiteral(".txt")), kIndexLimit,
[this, generation, format, title_id](const QByteArray& data, QString error) {
if (generation != m_catalog_generation) {
return;
}
if (error.isEmpty()) {
auto result = ParseIndex(data, format, title_id);
m_files.append(result.files);
error = result.error;
}
if (!error.isEmpty()) {
m_errors.append(format.toUpper() + QStringLiteral(": ") + error);
}
if (--m_pending == 0) {
std::sort(m_files.begin(), m_files.end(),
[](const RemoteFile& left, const RemoteFile& right) {
if (left.version != right.version) {
return left.version > right.version;
}
if (left.format != right.format) {
return left.format < right.format;
}
return left.name < right.name;
});
emit CatalogLoaded(m_files, m_errors.join(QLatin1Char('\n')));
}
});
m_catalog_replies.append(reply);
}
}
void CheatRepository::Fetch(const RemoteFile& file) {
CancelFetch();
if (!file.IsSupported()) {
emit FileLoaded({}, tr("Only JSON cheat files are supported."));
return;
}
const auto generation = m_fetch_generation;
m_file_reply = Request(file.Url(), kFileLimit,
[this, generation](const QByteArray& data, const QString& error) {
if (generation == m_fetch_generation) {
emit FileLoaded(data, error);
}
});
}
void CheatRepository::CancelFetch() {
m_fetch_generation++;
if (m_file_reply != nullptr) {
auto* reply = m_file_reply.data();
m_file_reply.clear();
reply->abort();
}
}
} // namespace Cheats
+20 -1
View File
@@ -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()));
+2 -2
View File
@@ -1,11 +1,11 @@
#include "mainDialog.h"
#include "cheatFile.h"
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include "controllerLightbar.h"
#include "gameContent.h"
#include "patchesDialog.h"
#include "updateChecker.h"
#include <QApplication>
@@ -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
View File
@@ -1,140 +1,309 @@
#include "patchesDialog.h"
#include "cheatFile.h"
#include "configuration.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QSplitter>
#include <QStringList>
#include <QTabWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <algorithm>
#include <utility>
namespace {
void ShowMods(QListWidget* list, const QJsonDocument& document, bool checkable) {
list->clear();
for (const auto& value: document.object().value(QStringLiteral("mods")).toArray()) {
const auto mod = value.toObject();
auto* item = new QListWidgetItem(mod.value(QStringLiteral("name")).toString(), list);
if (checkable) {
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(mod.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
: Qt::Unchecked);
}
}
}
QLabel* StatusLabel(QWidget* parent) {
auto* label = new QLabel(parent);
label->setWordWrap(true);
label->setTextFormat(Qt::PlainText);
return label;
}
} // namespace
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_version(game.gameVersion),
m_process(QFileInfo(game.elf.isEmpty() ? QStringLiteral("eboot.bin") : game.elf).fileName()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
resize(640, 480);
resize(800, 640);
auto* layout = new QVBoxLayout(this);
m_patches = new QListWidget(this);
m_status = new QLabel(this);
m_apply = new QPushButton(tr("Apply selection"), this);
auto* close = new QPushButton(tr("Close"), this);
auto* layout = new QVBoxLayout(this);
auto* game_label = StatusLabel(this);
game_label->setText(tr("%1 · Installed version: %2")
.arg(m_title_id, m_version.isEmpty() ? tr("Unknown") : m_version));
layout->addWidget(game_label);
m_tabs = new QTabWidget(this);
layout->addWidget(m_tabs);
m_status->setWordWrap(true);
layout->addWidget(m_patches);
layout->addWidget(m_status);
auto* remote = new QWidget(m_tabs);
auto* remote_layout = new QVBoxLayout(remote);
m_tabs->addTab(remote, tr("Remote"));
auto* credits = new QLabel(
tr("Cheats by <a href=\"https://github.com/TeeKay87/HEN-Cheats-Collection\">TeeKay87 / HEN "
"Cheats Collection</a> and its contributors."),
remote);
credits->setOpenExternalLinks(true);
credits->setWordWrap(true);
remote_layout->addWidget(credits);
auto* support = StatusLabel(remote);
support->setText(tr("Only JSON cheats are supported. MC4 and SHN files are shown disabled. "
"Select a JSON file to preview its cheats."));
remote_layout->addWidget(support);
auto* split = new QSplitter(Qt::Vertical, remote);
m_files = new QTreeWidget(split);
m_files->setHeaderLabels({tr("Version"), tr("Format"), tr("File")});
m_files->setRootIsDecorated(false);
m_files->setSelectionMode(QAbstractItemView::SingleSelection);
m_files->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(2, QHeaderView::Stretch);
m_preview = new QListWidget(split);
remote_layout->addWidget(split, 1);
m_remote_status = StatusLabel(remote);
m_preview_status = StatusLabel(remote);
remote_layout->addWidget(m_remote_status);
remote_layout->addWidget(m_preview_status);
auto* remote_buttons = new QDialogButtonBox(remote);
m_refresh = remote_buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
m_import = remote_buttons->addButton(tr("Import to Local"), QDialogButtonBox::ActionRole);
m_import->setEnabled(false);
remote_layout->addWidget(remote_buttons);
auto* buttons = new QDialogButtonBox(this);
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
buttons->addButton(close, QDialogButtonBox::RejectRole);
auto* local = new QWidget(m_tabs);
auto* local_layout = new QVBoxLayout(local);
m_tabs->addTab(local, tr("Local"));
m_patches = new QListWidget(local);
m_status = StatusLabel(local);
local_layout->addWidget(m_patches, 1);
local_layout->addWidget(m_status);
auto* local_buttons = new QDialogButtonBox(local);
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
local_layout->addWidget(local_buttons);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
connect(reload, &QPushButton::clicked, this, &PatchesDialog::Load);
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
connect(close, &QPushButton::clicked, this, &QDialog::close);
connect(m_refresh, &QPushButton::clicked, this, &PatchesDialog::LoadRemote);
connect(m_import, &QPushButton::clicked, this, &PatchesDialog::ImportRemote);
connect(m_files, &QTreeWidget::currentItemChanged, this, &PatchesDialog::PreviewRemote);
m_repository = new Cheats::CheatRepository(this);
connect(
m_repository, &Cheats::CheatRepository::CatalogLoaded, this,
[this](QList<Cheats::RemoteFile> files, const QString& error) {
std::stable_sort(files.begin(), files.end(), [this](const auto& a, const auto& b) {
return (a.version == m_version) > (b.version == m_version);
});
m_remote_files = std::move(files);
for (qsizetype index = 0; index < m_remote_files.size(); ++index) {
const auto& file = m_remote_files[index];
auto* item = new QTreeWidgetItem(
m_files, {file.version == m_version ? tr("%1 (installed)").arg(file.version)
: file.version,
file.format.toUpper(), file.name});
item->setData(0, Qt::UserRole, index);
for (int column = 0; column < 3; ++column) {
item->setToolTip(column, file.IsSupported()
? file.title
: tr("%1 — %2 is not supported.")
.arg(file.title, file.format.toUpper()));
}
if (!file.IsSupported()) {
item->setFlags(item->flags() & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
}
}
auto status =
m_remote_files.isEmpty() && error.isEmpty()
? tr("No remote cheats found for %1.").arg(m_title_id)
: tr("%1 remote file(s) for %2.").arg(m_remote_files.size()).arg(m_title_id);
if (!error.isEmpty()) {
status = tr("%1\nCould not load the complete collection: %2\nUse Refresh to retry.")
.arg(status, error);
}
m_remote_status->setText(status);
m_refresh->setEnabled(true);
});
connect(
m_repository, &Cheats::CheatRepository::FileLoaded, this,
[this](const QByteArray& data, QString error) {
if (error.isEmpty()) {
error = Cheats::ParseDocument(data, &m_remote_document);
}
if (!error.isEmpty()) {
m_preview_status->setText(
tr("Could not preview this file: %1\nSelect another file or refresh to retry.")
.arg(error));
return;
}
ShowMods(m_preview, m_remote_document, false);
error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
const auto root = m_remote_document.object();
QStringList creators;
for (const auto& value: root.value(QStringLiteral("credits")).toArray()) {
if (value.isString()) {
creators.append(value.toString());
}
}
const auto metadata =
tr("%1\nCredits: %2 · Version: %3")
.arg(root.value(QStringLiteral("name")).toString(),
creators.isEmpty() ? root.value(QStringLiteral("author")).toString()
: creators.join(QStringLiteral(", ")),
root.value(QStringLiteral("version")).toString());
m_preview_status->setText(
metadata + QStringLiteral("\n\n") +
(error.isEmpty() ? tr("Import to Local, then select the cheats to enable. Changes "
"take effect on the next game launch.")
: error));
m_import->setEnabled(error.isEmpty());
});
Load();
}
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
}
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
return QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
LoadRemote();
}
void PatchesDialog::Load() {
QFile file(PatchPlanPath(m_title_id));
m_patches->clear();
m_local_data.clear();
m_local_document = {};
m_local_readable = false;
m_apply->setEnabled(false);
const auto path = Cheats::PlanPath(m_title_id);
if (path.isEmpty()) {
m_status->setText(tr("Unsupported title ID."));
return;
}
QFile file(path);
if (!file.exists()) {
m_local_readable = true;
m_status->setText(tr("No local cheat file: %1").arg(path));
return;
}
if (!file.open(QIODevice::ReadOnly)) {
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
m_apply->setEnabled(false);
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_data = file.readAll();
if (file.error() != QFile::NoError) {
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto root = document.object();
if (!root.value(QStringLiteral("mods")).isArray()) {
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_readable = true;
const auto error = Cheats::ParseDocument(m_local_data, &m_local_document);
if (!error.isEmpty()) {
m_status->setText(tr("%1\n%2").arg(error, path));
return;
}
const auto mods = root.value(QStringLiteral("mods")).toArray();
for (const auto& value: mods) {
if (!value.isObject()) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
auto* item = new QListWidgetItem(name.toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
}
m_apply->setEnabled(!mods.isEmpty());
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
ShowMods(m_patches, m_local_document, true);
m_apply->setEnabled(m_patches->count() > 0);
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(m_patches->count()).arg(path));
}
void PatchesDialog::Save() {
const auto path = PatchPlanPath(m_title_id);
QFile input(path);
if (!input.open(QIODevice::ReadOnly)) {
QList<bool> enabled;
for (int index = 0; index < m_patches->count(); ++index) {
enabled.append(m_patches->item(index)->checkState() == Qt::Checked);
}
const auto document = Cheats::WithSelection(m_local_document, enabled);
const auto error = Cheats::SaveDocument(Cheats::PlanPath(m_title_id), document, m_local_data);
if (!error.isEmpty()) {
m_status->setText(error);
return;
}
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);
}
+350 -66
View File
@@ -6,18 +6,23 @@
#include "gameContent.h"
#include <QAbstractItemView>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QFont>
#include <QHeaderView>
#include <QIcon>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QRegularExpression>
#include <QSize>
#include <QStringList>
#include <QTabBar>
#include <QTabWidget>
#include <QTableWidget>
#include <QVBoxLayout>
namespace {
@@ -49,26 +54,6 @@ static QString GradeToText(int grade) {
}
}
static void PrepareTable(QTableWidget* table) {
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
QObject::tr("Name"), QObject::tr("Description")});
table->setAlternatingRowColors(true);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setIconSize(QSize(96, 96));
table->setShowGrid(false);
table->setWordWrap(true);
table->verticalHeader()->setVisible(false);
table->verticalHeader()->setDefaultSectionSize(112);
table->horizontalHeader()->setStretchLastSection(true);
table->horizontalHeader()->setHighlightSections(false);
table->setColumnWidth(0, 100);
table->setColumnWidth(1, 132);
table->setColumnWidth(2, 260);
table->setColumnWidth(3, 480);
}
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
QStringList lines {QString::fromStdString(trophy.name),
QString::fromStdString(trophy.description),
@@ -102,6 +87,225 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
return !FindTrophyFiles(info).isEmpty();
}
namespace {
using Common::Trophies::Progress;
Common::Trophies::UnlockData LoadUnlocks(const Configuration& info,
const QString& runtime_directory, const QString& file) {
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
return Common::Trophies::LoadUnlockData(Common::Trophies::UnlocksPath(
GameContent::ToPath(runtime_directory), info.title_id.toStdString(), info.user_id, label));
}
Progress GetGameProgress(const Configuration* info, const QString& runtime_directory) {
Progress result;
if (info == nullptr) {
return result;
}
const auto reader = Common::OpenArchive(GameContent::ToPath(info->basedir));
for (const auto& file: FindTrophyFiles(info)) {
const auto package =
Common::Trophies::LoadPackage(GameContent::ToPath(file), info->console_language);
const auto progress =
Common::Trophies::GetProgress(package, LoadUnlocks(*info, runtime_directory, file));
result.total += progress.total;
result.earned += progress.earned;
for (size_t grade = 1; grade < result.total_grade.size(); ++grade) {
result.total_grade[grade] += progress.total_grade[grade];
result.earned_grade[grade] += progress.earned_grade[grade];
}
}
return result;
}
QPixmap MakeCupIcon(const QColor& color, int size) {
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
const double u = size / 20.0;
painter.setPen(Qt::NoPen);
painter.setBrush(color);
QPainterPath cup;
cup.moveTo(5 * u, 2 * u);
cup.lineTo(15 * u, 2 * u);
cup.lineTo(14 * u, 9 * u);
cup.quadTo(10 * u, 13 * u, 6 * u, 9 * u);
cup.closeSubpath();
painter.drawPath(cup);
painter.setPen(QPen(color, 1.6 * u));
painter.setBrush(Qt::NoBrush);
painter.drawArc(QRectF(1.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, 180 * 16);
painter.drawArc(QRectF(13.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, -180 * 16);
painter.drawLine(QPointF(10 * u, 12 * u), QPointF(10 * u, 16 * u));
painter.setPen(Qt::NoPen);
painter.setBrush(color);
painter.drawRoundedRect(QRectF(6 * u, 16 * u, 8 * u, 2.5 * u), u, u);
return pixmap;
}
QColor GradeColor(int grade) {
switch (grade) {
case 1: return QColor(0x7fc8ff);
case 2: return QColor(0xf5c542);
case 3: return QColor(0xc4cad2);
default: return QColor(0xcd7f4f);
}
}
QWidget* MakeGradeCount(QWidget* parent, int grade, int value, int icon_size, int font_size) {
auto* box = new QWidget(parent);
auto* row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
auto* icon = new QLabel(box);
icon->setPixmap(MakeCupIcon(GradeColor(grade), icon_size));
auto* text = new QLabel(QString::number(value), box);
text->setStyleSheet(QStringLiteral("font-size: %1px;").arg(font_size));
row->addWidget(icon);
row->addWidget(text);
return box;
}
QPixmap LoadGameArt(const Configuration* game, int height) {
QPixmap art;
const auto data = GameContent::ReadFile(game->basedir, QStringLiteral("sce_sys/icon0.png"),
GameContent::MaxImageSize);
if (!data.isEmpty() && art.loadFromData(data)) {
return art.scaledToHeight(height, Qt::SmoothTransformation);
}
return {};
}
} // namespace
void TrophyViewerDialog::ShowOverview(const std::vector<const Configuration*>& games,
const QString& runtime_directory, QWidget* parent) {
QDialog dialog(parent);
dialog.setWindowTitle(QObject::tr("Trophies"));
dialog.resize(820, 640);
dialog.setStyleSheet(QStringLiteral(
"QDialog { background: palette(window); }"
"QLabel { color: palette(text); }"
"QListWidget { background: transparent; border: none; outline: none; }"
"QListWidget::item { border-bottom: 1px solid palette(mid); padding: 4px; }"
"QListWidget::item:selected, QListWidget::item:hover { background: palette(midlight); "
"border: 1px solid palette(highlight); }"
"QProgressBar { background: palette(mid); border: none; height: 4px; }"
"QProgressBar::chunk { background: palette(highlight); }"));
auto* layout = new QVBoxLayout(&dialog);
struct GameProgress {
const Configuration* game;
Progress progress;
};
std::vector<GameProgress> trophy_games;
Progress totals;
for (const auto* game: games) {
const auto progress = GetGameProgress(game, runtime_directory);
if (progress.total == 0) {
continue;
}
trophy_games.push_back({game, progress});
totals.earned += progress.earned;
for (size_t grade = 1; grade < totals.earned_grade.size(); ++grade) {
totals.earned_grade[grade] += progress.earned_grade[grade];
}
}
auto* header = new QHBoxLayout;
auto* title = new QLabel(QObject::tr("Trophies"), &dialog);
title->setStyleSheet(QStringLiteral("font-size: 30px;"));
header->addWidget(title);
header->addStretch(1);
auto* total = new QLabel(QObject::tr("Total %1").arg(totals.earned), &dialog);
total->setAlignment(Qt::AlignCenter);
total->setStyleSheet(QStringLiteral("font-size: 16px;"));
header->addWidget(total);
for (int grade = 1; grade <= 4; ++grade) {
header->addWidget(MakeGradeCount(&dialog, grade, totals.earned_grade[grade], 32, 18));
}
layout->addLayout(header);
auto* list = new QListWidget(&dialog);
list->setSelectionMode(QAbstractItemView::SingleSelection);
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
layout->addWidget(list, 1);
for (size_t index = 0; index < trophy_games.size(); ++index) {
const auto& [game, progress] = trophy_games[index];
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(760, 112));
item->setData(Qt::UserRole, static_cast<int>(index));
auto* row_widget = new QWidget(list);
row_widget->setAttribute(Qt::WA_TransparentForMouseEvents);
auto* row = new QHBoxLayout(row_widget);
auto* art = new QLabel(row_widget);
art->setFixedSize(80, 80);
art->setAlignment(Qt::AlignCenter);
const auto pixmap = LoadGameArt(game, 80);
if (!pixmap.isNull()) {
art->setPixmap(pixmap);
} else {
art->setStyleSheet(QStringLiteral("background: palette(mid);"));
}
row->addWidget(art);
const auto name = !game->name.isEmpty() ? game->name : game->title_id;
auto* text = new QLabel(QStringLiteral("%1\n%2").arg(name, game->title_id), row_widget);
text->setStyleSheet(QStringLiteral("font-size: 18px;"));
row->addWidget(text, 1);
auto* percent_box = new QVBoxLayout;
percent_box->setSpacing(2);
percent_box->setContentsMargins(0, 0, 0, 0);
auto* percent = new QLabel(QStringLiteral("%1%").arg(progress.Percentage()), row_widget);
percent->setAlignment(Qt::AlignRight);
percent->setStyleSheet(QStringLiteral("font-size: 24px;"));
percent->setMinimumHeight(34);
auto* earned = new QLabel(
QObject::tr("Earned %1/%2").arg(progress.earned).arg(progress.total), row_widget);
earned->setAlignment(Qt::AlignRight);
earned->setMinimumHeight(18);
auto* bar = new QProgressBar(row_widget);
bar->setRange(0, 100);
bar->setValue(progress.Percentage());
bar->setTextVisible(false);
bar->setFixedWidth(130);
percent_box->addWidget(percent);
percent_box->addWidget(earned);
percent_box->addWidget(bar);
row->addLayout(percent_box);
for (int grade = 1; grade <= 4; ++grade) {
if (grade == 1 && progress.total_grade[1] == 0) {
continue;
}
row->addWidget(MakeGradeCount(row_widget, grade, progress.earned_grade[grade], 24, 18));
}
list->setItemWidget(item, row_widget);
}
if (trophy_games.empty()) {
auto* empty =
new QLabel(QObject::tr("No games with trophies found in the game folders."), &dialog);
empty->setAlignment(Qt::AlignCenter);
layout->insertWidget(1, empty);
list->hide();
}
connect(list, &QListWidget::itemClicked, &dialog,
[&dialog, &trophy_games, &runtime_directory](QListWidgetItem* item) {
const auto index = item->data(Qt::UserRole).toInt();
if (index >= 0 && static_cast<size_t>(index) < trophy_games.size()) {
ShowForGame(trophy_games[static_cast<size_t>(index)].game, runtime_directory,
&dialog);
}
});
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttons);
dialog.exec();
}
void TrophyViewerDialog::ShowForGame(const Configuration* info, const QString& runtime_directory,
QWidget* parent) {
if (info == nullptr) {
@@ -139,51 +343,131 @@ bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runt
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
continue;
}
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
const auto unlocked = Common::Trophies::LoadUnlocks(
Common::Trophies::UnlocksPath(GameContent::ToPath(runtime_directory),
info.title_id.toStdString(), info.user_id, label));
auto* table = new QTableWidget(static_cast<int>(package.trophies.size()), 4, m_tabs);
PrepareTable(table);
int row_index = 0;
for (const auto& [id, trophy]: package.trophies) {
const bool trophy_unlocked = unlocked.contains(id);
auto* status = new QTableWidgetItem(trophy_unlocked ? tr("UNLOCKED") : tr("LOCKED"));
status->setTextAlignment(Qt::AlignCenter);
auto status_font = status->font();
status_font.setBold(true);
status->setFont(status_font);
table->setItem(row_index, 0, status);
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
const auto counts = Common::Trophies::GetProgress(package, unlocks);
auto* page = new QWidget(m_tabs);
auto* page_layout = new QVBoxLayout(page);
auto* icon_item = new QTableWidgetItem;
icon_item->setTextAlignment(Qt::AlignCenter);
QPixmap icon;
if (icon.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size()))) {
if (!trophy_unlocked) {
icon = QPixmap::fromImage(
icon.toImage().convertToFormat(QImage::Format_Grayscale8));
}
icon_item->setIcon(QIcon(icon));
}
table->setItem(row_index, 1, icon_item);
const auto name_text = trophy.name.empty() ? (trophy.hidden ? tr("Hidden Trophy")
: tr("Trophy %1").arg(id))
: QString::fromStdString(trophy.name);
auto* name = new QTableWidgetItem(name_text);
auto font = name->font();
font.setBold(true);
name->setFont(font);
name->setToolTip(TrophyTooltip(trophy));
auto* detail = new QTableWidgetItem(QString::fromStdString(trophy.description));
detail->setToolTip(name->toolTip());
table->setItem(row_index, 2, name);
table->setItem(row_index, 3, detail);
++row_index;
auto* header = new QHBoxLayout;
auto* art = new QLabel(page);
if (const auto pixmap = LoadGameArt(&info, 56); !pixmap.isNull()) {
art->setPixmap(pixmap);
header->addWidget(art);
}
m_tabs->addTab(table, tr("Trophy %1").arg(label));
auto* title = new QLabel(QString::fromStdString(package.title), page);
title->setStyleSheet(QStringLiteral("font-size: 28px;"));
header->addWidget(title, 1);
const auto make_stat = [page](const QString& label, const QString& value) {
auto* box = new QVBoxLayout;
auto* name = new QLabel(label, page);
auto* number = new QLabel(value, page);
name->setStyleSheet(QStringLiteral("font-size: 14px;"));
number->setStyleSheet(QStringLiteral("font-size: 22px;"));
box->addWidget(name);
box->addWidget(number);
return box;
};
header->addLayout(
make_stat(tr("Progress"), QStringLiteral("%1%").arg(counts.Percentage())));
header->addLayout(
make_stat(tr("Earned"), QStringLiteral("%1/%2").arg(counts.earned).arg(counts.total)));
for (int grade = 1; grade <= 4; ++grade) {
if (counts.total_grade[grade] > 0) {
header->addWidget(MakeGradeCount(page, grade, counts.earned_grade[grade], 30, 20));
}
}
page_layout->addLayout(header);
auto* progress = new QProgressBar(page);
progress->setRange(0, 100);
progress->setValue(counts.Percentage());
progress->setTextVisible(false);
page_layout->addWidget(progress);
auto* count_label = new QLabel(tr("All trophies: %1").arg(counts.total), page);
page_layout->addWidget(count_label);
auto* list = new QListWidget(page);
list->setSelectionMode(QAbstractItemView::NoSelection);
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
list->setSpacing(4);
page_layout->addWidget(list, 1);
for (const auto& [id, trophy]: package.trophies) {
const bool trophy_unlocked = unlocks.unlocked.contains(id);
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(900, 96));
auto* card = new QWidget(list);
card->setStyleSheet(
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
card->setObjectName(QStringLiteral("card"));
auto* row = new QHBoxLayout(card);
auto* icon = new QLabel(card);
icon->setFixedSize(76, 76);
icon->setAlignment(Qt::AlignCenter);
QPixmap pixmap;
if (hidden_locked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!trophy_unlocked) {
pixmap =
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
}
if (!pixmap.isNull()) {
icon->setPixmap(
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
row->addWidget(icon);
auto* left = new QVBoxLayout;
const auto name_text = hidden_locked ? tr("Hidden trophy")
: trophy.name.empty() ? tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
auto* name = new QLabel(name_text, card);
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
left->addWidget(name);
if (!hidden_locked) {
auto* grade_row = new QHBoxLayout;
auto* cup = new QLabel(card);
cup->setPixmap(MakeCupIcon(GradeColor(trophy.grade), 18));
auto* grade_text = new QLabel(GradeToText(trophy.grade), card);
grade_text->setStyleSheet(QStringLiteral("font-size: 15px;"));
grade_row->addWidget(cup);
grade_row->addWidget(grade_text);
grade_row->addStretch(1);
left->addLayout(grade_row);
}
row->addLayout(left, 2);
auto* right = new QVBoxLayout;
QString date_text;
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
date_text = QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>(
(found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
} else if (trophy_unlocked) {
date_text = tr("Earned");
}
auto* date = new QLabel(date_text, card);
date->setAlignment(Qt::AlignRight);
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
auto* detail = new QLabel(
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
detail->setWordWrap(true);
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
right->addWidget(date);
right->addWidget(detail, 1);
row->addLayout(right, 3);
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
list->setItemWidget(item, card);
}
m_tabs->addTab(page, QString::fromStdString(package.title));
}
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
if (m_tabs->count() == 0) {
error = errors.join(QLatin1Char('\n'));
return false;
+6
View File
@@ -39,9 +39,11 @@ QLineEdit#search_line_edit:focus {
border-color: palette(highlight);
}
QToolButton#refresh_button,
QToolButton#global_settings_button,
QToolButton#input_mapping_button,
QToolButton#edit_button,
QToolButton#trophy_overview_button,
QToolButton#delete_button {
background: transparent;
border: 1px solid transparent;
@@ -49,17 +51,21 @@ QToolButton#delete_button {
padding: 3px;
}
QToolButton#refresh_button:hover,
QToolButton#global_settings_button:hover,
QToolButton#input_mapping_button:hover,
QToolButton#edit_button:hover,
QToolButton#trophy_overview_button:hover,
QToolButton#delete_button:hover {
background: palette(button);
border-color: palette(mid);
}
QToolButton#refresh_button:pressed,
QToolButton#global_settings_button:pressed,
QToolButton#input_mapping_button:pressed,
QToolButton#edit_button:pressed,
QToolButton#trophy_overview_button:pressed,
QToolButton#delete_button:pressed {
background: palette(midlight);
}
+17 -5
View File
@@ -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
View File
@@ -7,13 +7,12 @@
#include "loader/systemContent.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <vector>
@@ -104,7 +103,7 @@ using TrophyKey = std::pair<int, uint32_t>;
struct TrophyState {
const Trophies::Package* package = nullptr;
std::set<int> unlocked;
Trophies::UnlockData unlocks;
std::filesystem::path path;
};
@@ -130,20 +129,33 @@ static TrophyState* LoadTrophyState(TrophyKey key) {
if (const auto it = g_states.find(key); it != g_states.end()) {
return &it->second;
}
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
auto& package = g_packages[key.second];
if (package.trophies.empty()) {
const auto filename = "/app0/" + Trophies::PackagePath(key.second).generic_string();
package = Trophies::LoadPackage(LibKernel::FileSystem::GetRealFilename(filename),
Config::GetConsoleLanguage());
if (package.trophies.empty()) {
LOGF("[Trophy] could not load trophy package %s for %s\n", filename.c_str(),
title_id.c_str());
return nullptr;
}
LOGF("[Trophy] loaded %zu trophy definitions for %s (service %u)\n",
package.trophies.size(), title_id.c_str(), key.second);
const auto uds_filename = "/app0/" + Trophies::UdsPackagePath(key.second).generic_string();
package.event_rules =
Trophies::LoadUdsRules(LibKernel::FileSystem::GetRealFilename(uds_filename));
if (package.event_rules.empty()) {
LOGF("[Trophy] no supported UDS extraction rules in %s\n", uds_filename.c_str());
} else {
LOGF("[Trophy] loaded UDS extraction rules for %zu events from %s\n",
package.event_rules.size(), uds_filename.c_str());
}
}
std::string title_id;
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
auto unlocked = Trophies::LoadUnlocks(path);
return &g_states.emplace(key, TrophyState {&package, std::move(unlocked), std::move(path)})
auto path = Trophies::UnlocksPath({}, title_id, key.first, key.second);
auto unlocks = Trophies::LoadUnlockData(path);
return &g_states.emplace(key, TrophyState {&package, std::move(unlocks), std::move(path)})
.first->second;
}
@@ -165,7 +177,7 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
details->group_id = trophy.group_id;
details->hidden = trophy.hidden;
details->has_reward = trophy.has_reward;
if (trophy.target) {
if (trophy.progressive && trophy.target) {
details->target.type = 1;
details->target.value = *trophy.target;
}
@@ -177,8 +189,12 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
if (data != nullptr) {
*data = {};
data->trophy_id = trophy.id;
data->unlocked = state.unlocked.contains(trophy.id);
if (trophy.target) {
data->unlocked = state.unlocks.unlocked.contains(trophy.id);
if (const auto timestamp = state.unlocks.timestamps.find(trophy.id);
timestamp != state.unlocks.timestamps.end()) {
data->timestamp_tick = timestamp->second;
}
if (trophy.progressive && trophy.target) {
data->progress.type = 1;
data->progress.value = data->unlocked ? *trophy.target : 0;
}
@@ -188,19 +204,13 @@ static void FillTrophyInfo(const TrophyState& state, const Trophies::Trophy& tro
// Game and group structures share the same count fields.
template <typename Details, typename Data>
static void FillTrophyCounts(const TrophyState& state, int group, Details* details, Data* data) {
std::array<uint32_t, 5> total {}, unlocked {};
for (const auto& [id, trophy]: state.package->trophies) {
if (group != -2 && trophy.group_id != group) {
continue;
}
++total[trophy.grade];
if (state.unlocked.contains(id)) {
++unlocked[trophy.grade];
}
}
const auto progress = Trophies::GetProgress(
*state.package, state.unlocks, group == -2 ? std::nullopt : std::optional<int>(group));
const auto& total = progress.total_grade;
const auto& unlocked = progress.earned_grade;
if (details != nullptr) {
*details = {};
details->num_trophies = total[1] + total[2] + total[3] + total[4];
details->num_trophies = progress.total;
details->num_platinum = total[1];
details->num_gold = total[2];
details->num_silver = total[3];
@@ -208,14 +218,12 @@ static void FillTrophyCounts(const TrophyState& state, int group, Details* detai
}
if (data != nullptr) {
*data = {};
data->unlocked_trophies = unlocked[1] + unlocked[2] + unlocked[3] + unlocked[4];
data->unlocked_trophies = progress.earned;
data->unlocked_platinum = unlocked[1];
data->unlocked_gold = unlocked[2];
data->unlocked_silver = unlocked[3];
data->unlocked_bronze = unlocked[4];
const auto points = total[2] * 6 + total[3] * 2 + total[4];
const auto earned = unlocked[2] * 6 + unlocked[3] * 2 + unlocked[4];
data->progress_percentage = points == 0 ? 0 : earned * 100 / points;
data->progress_percentage = progress.Percentage();
}
}
@@ -231,31 +239,45 @@ static void FillGroupInfo(const TrophyState& state, int group_id, const std::str
}
}
static void RecordTrophyUnlock(TrophyState& state, int trophy_id) {
static void RecordTrophyUnlock(TrophyState& state, int trophy_id, bool from_stat = false) {
const auto found = state.package->trophies.find(trophy_id);
if (found == state.package->trophies.end() || found->second.grade == 1 ||
found->second.target || !state.unlocked.insert(trophy_id).second) {
if (found == state.package->trophies.end()) {
LOGF("[Trophy] ignored unknown trophy ID %d\n", trophy_id);
return;
}
if (found->second.grade == 1 || (!from_stat && found->second.progressive) ||
!state.unlocks.unlocked.insert(trophy_id).second) {
return;
}
const auto timestamp =
Trophies::UnixEpochTick + std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
state.unlocks.timestamps.emplace(trophy_id, timestamp);
std::vector<int> earned {trophy_id};
const auto platinum_id = found->second.platinum_id;
if (platinum_id >= 0 && state.package->trophies.contains(platinum_id) &&
std::all_of(state.package->trophies.begin(), state.package->trophies.end(),
[&](const auto& entry) {
return entry.second.platinum_id != platinum_id ||
state.unlocked.contains(entry.first);
state.unlocks.unlocked.contains(entry.first);
}) &&
state.unlocked.insert(platinum_id).second) {
state.unlocks.unlocked.insert(platinum_id).second) {
state.unlocks.timestamps.emplace(platinum_id, timestamp);
earned.push_back(platinum_id);
}
if (!Trophies::SaveUnlocks(state.path, state.unlocked)) {
if (!Trophies::SaveUnlockData(state.path, state.unlocks)) {
LOGF("[Trophy] could not save unlocks to %s\n", state.path.string().c_str());
} else {
LOGF("[Trophy] recorded unlock %d: %s\n", trophy_id, found->second.name.c_str());
}
if (Config::TrophyEnabled()) {
for (const auto id: earned) {
const auto& trophy = state.package->trophies.at(id);
Graphics::NotifyTrophyUnlocked(trophy.name, trophy.grade, trophy.icon_png);
}
} else {
LOGF("[Trophy] notification disabled by configuration\n");
}
}
@@ -541,11 +563,13 @@ struct NpUniversalDataSystemMemoryStat {
};
struct NpUniversalDataSystemEventPropertyObject {
std::optional<int32_t> trophy_id;
std::map<std::string, Common::Trophies::UdsInteger> integers;
void SetTrophyId(const char* key, std::optional<int32_t> value) {
if (std::strcmp(key, "_trophy_id") == 0) {
trophy_id = value;
void SetInteger(const char* key, std::optional<Common::Trophies::UdsInteger> value) {
if (value) {
integers.insert_or_assign(key, *value);
} else {
integers.erase(key);
}
}
};
@@ -647,6 +671,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemCreateEvent(
}
*new_event = new NpUniversalDataSystemEvent {
event_name, prop != nullptr ? *prop : NpUniversalDataSystemEventPropertyObject {}};
LOGF("[UDS] created event: %s\n", event_name);
if (prop_ptr != nullptr) {
*prop_ptr = &(*new_event)->properties;
}
@@ -668,14 +693,26 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemPostEvent(int context, int handle,
return static_cast<int>(0x80553120); // NOT_REGISTERED
}
const auto& uds_event = *static_cast<const NpUniversalDataSystemEvent*>(event);
LOGF("[UDS] posted event: %s\n", uds_event.name.c_str());
if (uds_event.name == "_UnlockTrophy") {
const auto trophy_id = uds_event.properties.trophy_id;
if (!trophy_id || *trophy_id < 0) {
const auto property = uds_event.properties.integers.find("_trophy_id");
const auto* trophy_id = property == uds_event.properties.integers.end()
? nullptr
: std::get_if<int32_t>(&property->second);
if (trophy_id == nullptr || *trophy_id < 0) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
LibNpTrophy2::RecordTrophyUnlock(*state, *trophy_id);
}
} else if (!uds_event.properties.integers.empty()) {
if (auto* state = LibNpTrophy2::LoadTrophyState(it->second.key); state != nullptr) {
const auto ids = LibNpTrophy2::Trophies::FindUdsTrophies(
*state->package, uds_event.name, uds_event.properties.integers);
for (const auto trophy_id: ids) {
LibNpTrophy2::RecordTrophyUnlock(*state, trophy_id, true);
}
}
}
return 0;
}
@@ -809,7 +846,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetString(
if (object == nullptr || key == nullptr || value == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
return 0;
}
@@ -826,7 +863,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt32(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, value);
object->SetInteger(key, value);
return 0;
}
@@ -838,7 +875,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt32(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, value);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -855,7 +892,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetInt64(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -872,7 +909,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetUInt64(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, value);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -889,7 +926,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat32(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -906,7 +943,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetFloat64(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -923,7 +960,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBool(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -941,7 +978,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetBinary(
if (object == nullptr || key == nullptr || (value == nullptr && value_size != 0)) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
LOGF("\t object = 0x%016" PRIx64 "\n"
"\t key = %s\n"
@@ -962,7 +999,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetObject(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
if (value_ptr != nullptr) {
*value_ptr =
@@ -996,7 +1033,7 @@ static int KYTY_SYSV_ABI NpUniversalDataSystemEventPropertyObjectSetArray(
if (object == nullptr || key == nullptr) {
return NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT;
}
object->SetTrophyId(key, std::nullopt);
object->SetInteger(key, std::nullopt);
if (value_ptr != nullptr) {
*value_ptr = (value != nullptr ? const_cast<NpUniversalDataSystemEventPropertyArray*>(value)
+47
View File
@@ -28,6 +28,46 @@ void CheckConcurrentResult(int result, const char* text) {
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
}
void TestLevelUserEventDelivery() {
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
Check(EventQueue::KernelCreateEqueue(&queue, "level-user-events") == OK, "create user event queue");
Check(EventQueue::KernelAddUserEvent(queue, 1) == OK &&
EventQueue::KernelAddUserEvent(queue, 2) == OK &&
EventQueue::KernelAddUserEventEdge(queue, 3) == OK, "add level and edge events");
for (int id = 1; id <= 3; ++id)
Check(EventQueue::KernelTriggerUserEvent(queue, id, reinterpret_cast<void*>(uintptr_t(id))) == OK,
"trigger distinct user events");
EventQueue::KernelEvent events[16] {};
Libs::LibKernel::KernelUseconds timeout = 0;
int out = 0;
const auto find_event = [&](uintptr_t id) {
return std::find_if(events, events + out, [=](const auto& event) { return event.ident == id; });
};
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 3,
"level events are returned once per wait without starving following events");
for (uintptr_t id = 1; id <= 3; ++id) {
const auto* event = find_event(id);
Check(event != events + out && event->data == static_cast<intptr_t>(id) &&
event->udata == reinterpret_cast<void*>(id), "distinct event payloads");
}
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 2 &&
find_event(1) != events + out && find_event(2) != events + out,
"level persists while edge clears");
Check(EventQueue::KernelDeleteUserEvent(queue, 1) == OK, "delete first level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 2, "remaining level event is delivered only once");
Check(EventQueue::KernelDeleteUserEvent(queue, 2) == OK, "delete remaining level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"deleted levels and consumed edge do not reappear");
Check(EventQueue::KernelTriggerUserEvent(queue, 3, nullptr) == OK, "retrigger edge event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 3, "retriggered edge is delivered once");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"retriggered edge clears after delivery");
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete user event queue");
}
void TestPeriodicTimerEvents() {
using namespace std::chrono_literals;
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
@@ -188,6 +228,12 @@ void TestDuplicateAddPreservesEventState() {
"duplicate add updates deadline metadata");
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
"deadline trigger retains updated duplicate metadata");
for (uintptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
reinterpret_cast<void*>(value)) == OK, "queue bounded read payload");
for (intptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelWaitEqueue(queue, events, 1, &out, &timeout) == OK && out == 1 &&
events[0].data == value, "capacity-one reads preserve pending event order");
auto retained_owner = weak_original.lock();
Check(retained_owner != nullptr, "original owner alive before delete");
@@ -495,6 +541,7 @@ void TestConcurrentDelete() {
} // namespace
int main() {
TestLevelUserEventDelivery();
TestPeriodicTimerEvents();
TestDuplicateAddPreservesEventState();
TestCallbackStateOutlivesPort();
+289 -352
View File
@@ -206,10 +206,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
}
std::unique_ptr<Fixture>
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
bool memory_backed_material = false, uint32_t member_offset = 0,
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
uint32_t table_stride = 32, uint32_t table_offset = 0) {
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
uint32_t table_shader_offset = 0, bool nested = false) {
auto fixture = std::make_unique<Fixture>();
std::array<Value, 4> material_words;
std::array<Value, 4> heap_words;
@@ -217,54 +215,38 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
material_words[dword] = fixture->UserData(dword);
heap_words[dword] = fixture->UserData(dword + 4u);
}
if (memory_backed_material) {
const auto pointer_address =
fixture->Address(fixture->UserData(9), fixture->UserData(10), 0x10b0);
MemoryInfo pointer_word;
pointer_word.kind = ResourceKind::ScalarAddress;
const auto pointer =
fixture->Emit(ValueOpcode::LoadAddressU32,
{pointer_address, Value(0u), Value(0u), Value(true)},
fixture->AddMemory(pointer_word, 0x10b0));
const auto address = fixture->Address(pointer, Value(0u), 0x10c0);
MemoryInfo descriptor_word;
descriptor_word.kind = ResourceKind::ScalarAddress;
material_words[0] =
fixture->Emit(ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)},
fixture->AddMemory(descriptor_word, 0x10c0));
}
const auto material = fixture->Buffer(material_words, 0x10d8);
const auto heap = fixture->Buffer(heap_words, 0x10d8);
if (memory_backed_material) {
MemoryInfo shared_buffer;
shared_buffer.kind = ResourceKind::Buffer;
const auto load =
fixture->Emit(ValueOpcode::LoadBufferU32,
{material, Value(0u), Value(0u), Value(0u), Value(true)},
fixture->AddMemory(shared_buffer, 0x10d8));
fixture->Emit(ValueOpcode::ReferenceU32, {load});
}
const auto invocation = fixture->Emit(
ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::GlobalInvocationId)), Value(0u)});
const auto selector = fixture->Emit(ValueOpcode::ReadFirstLane,
{invocation, Value(true)});
const auto record =
fixture->Emit(ValueOpcode::IMul32, {selector, Value(material_stride)});
const auto member = member_offset == 0 ? record :
fixture->Emit(ValueOpcode::IAdd32, {record, Value(member_offset)});
Value member;
if (nested) {
std::array<Value, 4> root_words;
for (uint32_t word = 0; word < 4u; ++word) root_words[word] = fixture->UserData(12u + word);
const auto root = fixture->Buffer(root_words);
MemoryInfo root_memory;
root_memory.kind = ResourceKind::ScalarBuffer;
const auto root_offset = fixture->Emit(ValueOpcode::IMul32, {selector, Value(32u)});
const auto pointer = fixture->Emit(ValueOpcode::ReadConstBuffer, {root, root_offset},
fixture->AddMemory(root_memory, 0x1000));
const auto masked = fixture->Emit(ValueOpcode::BitwiseAnd32, {pointer, Value(0xfffffff1u)});
member = fixture->Emit(ValueOpcode::IAdd32, {masked, Value(16u)});
} else {
member = fixture->Emit(ValueOpcode::IMul32, {selector, Value(224u)});
}
fixture->Emit(ValueOpcode::ReferenceU32, {member});
MemoryInfo material_scalar;
material_scalar.kind = ResourceKind::ScalarBuffer;
material_scalar.offset = material_immediate;
auto key =
material_scalar.offset = 4u;
const auto key =
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
fixture->AddMemory(material_scalar, 0x10d8));
if (selector_bits != UINT32_MAX)
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
const auto heap_offset =
fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
auto heap_offset = fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
if (table_shader_offset != 0u)
heap_offset = fixture->Emit(ValueOpcode::IAdd32, {heap_offset, Value(table_shader_offset)});
std::array<Value, 8> image_words;
MemoryInfo heap_scalar;
heap_scalar.kind = ResourceKind::ScalarBuffer;
@@ -278,6 +260,10 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
fixture->Emit(ValueOpcode::ReadConstBuffer, {heap, heap_offset},
fixture->AddMemory(component, 0x10d8));
}
if (nested) {
const auto field = fixture->Emit(ValueOpcode::BitwiseAnd32, {image_words[4], Value(7u)});
fixture->Emit(ValueOpcode::ReferenceU32, {field});
}
const auto image = fixture->Image(image_words, 0x10f0);
const auto sampler =
fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)}, 0x10f0);
@@ -294,331 +280,280 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
}
void TestInvariantIndirectImageMaterialization() {
auto fixture = MakeIndirectImageFixture(false);
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u, true);
fixture->PlanAndTrack();
auto resource_plan = ExtractResourcePlan(fixture->program);
auto plan = ExtractResourcePlan(fixture->program);
EliminateDeadCode(fixture->program.blocks);
ValidateProgram(fixture->program, true);
Check(fixture->program.info.buffers.size() == 1 &&
fixture->program.info.images.size() == 1 &&
std::ranges::any_of(*fixture->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::ReadConstBuffer;
}),
"indirect image key was not retained as a scalar-buffer read");
const auto source = fixture->program.info.images[0].source;
Check(source < fixture->program.descriptor_sources.size() &&
fixture->program.descriptor_sources[source]
.indirect_descriptor.has_value(),
"indirect image source was not retained for runtime proof");
const auto image_handle =
std::ranges::find_if(*fixture->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
Check(image_handle != fixture->block->end() &&
image_handle->Arg(0).ResolveInstruction() != nullptr &&
image_handle->Arg(0).ResolveInstruction()->GetOpcode() ==
ValueOpcode::ReadConstBuffer,
"indirect image handle discarded the live material key");
std::array<uint32_t, 9> user_data{0x1000u, 224u << 16u, 2u, 0u, 0x2000u,
16u << 16u, 4u, 0u, 7u};
const auto &source = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(!source.selector && fixture->program.info.buffers.size() == 3u,
"bounded image table retained CPU selector traversal or lost live loads");
std::array<uint32_t, 8> descriptor{};
descriptor[0] = 0x20u;
descriptor[1] = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u;
descriptor[2] = 3u | (3u << 14u);
descriptor[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
std::array<uint32_t, 16> data{0x3000u, 16u << 16u, 0x200000u, 0u,
0x2000u, 48u << 16u, 3u, 0u};
std::copy_n(data.begin(), 4u, data.begin() + 12u);
LinearTestMemory memory;
std::array<uint32_t, 8> image_descriptor{};
image_descriptor[0] = 0x20u;
image_descriptor[1] =
static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float)
<< 20u;
image_descriptor[2] = 3u | (3u << 14u);
image_descriptor[3] =
Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D)
<< 28u);
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
SrtRuntime runtime{.user_data = user_data,
.userdata = &memory,
memory.fail_address = 0x3000u; // GPU-only material backing must never be read on the CPU.
memory.watched_address = 0x2000u;
const auto put = [&](uint32_t offset, uint32_t identity) {
auto value = descriptor;
value[0] += identity;
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1000u + offset) / 4u);
};
put(16u, 0u);
put(64u, 1u);
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1 &&
std::equal(image_descriptor.begin(), image_descriptor.end(),
snapshot.images[0].dwords.begin()),
"invariant indirect image table did not materialize");
user_data[5] = 0u;
user_data[6] = 19u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 3u && memory.watched_reads == 1u &&
memory.watched_dwords == 36u && memory.reads == 1u,
"image table was not read once independently of its GPU selector chain");
const auto root = specialization.images[0];
const auto mapping = root.indirect_mapping_offset;
Check(root.indirect_search_iterations == 0u && snapshot.flattened_srt[mapping] == 17u &&
snapshot.flattened_srt[mapping + 1u] == 0u &&
snapshot.flattened_srt[mapping + 5u] == 1u &&
snapshot.flattened_srt[mapping + 17u] == 2u &&
snapshot.specialization_reads == std::vector<std::pair<uint64_t, uint64_t>>{{0x2000u, 144u}},
"dense image mapping lost its null slot, trimmed length or coherent read dependency");
const auto original_specialization = specialization;
std::fill_n(memory.words.begin() + 0x1040u / 4u, 8u, 0u);
put(112u, 1u);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
specialization == original_specialization && memory.watched_reads == 2u &&
snapshot.flattened_srt[mapping] == 29u,
"moving a table entry changed shader specialization or reused stale mapping bytes");
put(112u, 7u);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images[2].dwords[0] == descriptor[0] + 7u,
"changed table descriptor was not refreshed");
// Scalar reads align each DWORD and return zero beyond the V# byte size.
data[5] = 0u;
data[6] = 19u;
std::fill(memory.words.begin() + 0x1000u / 4u, memory.words.end(), 0u);
put(0u, 0u);
memory.fail_address = 0x2010u;
memory.watched_address = 0x2000u;
Check(MaterializeResources(resource_plan, runtime, snapshot, specialization) &&
memory.watched_dwords == 4u &&
std::equal(image_descriptor.begin(), image_descriptor.end(),
snapshot.images[0].dwords.begin()),
"partial scalar-buffer descriptor read crossed bounds instead of zeroing its tail");
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.watched_dwords == 4u && snapshot.images.size() == 2u &&
snapshot.images[1].dwords == descriptor,
"partial descriptor prefix did not zero its out-of-bounds DWORDs");
memory.fail_address = 0x2008u;
Check(!MaterializeResources(resource_plan, runtime, snapshot, specialization),
"unreadable memory inside the descriptor prefix was accepted");
user_data[5] = 16u << 16u;
user_data[6] = 4u;
memory.watched_address = UINT64_MAX;
memory.fail_address = 0x1004u;
Check(!MaterializeResources(resource_plan, runtime, snapshot,
specialization),
"unreadable material-table selector was accepted");
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable in-range table bytes were accepted");
memory.fail_address = UINT64_MAX;
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] = 0u;
memory.words[(0x2020u - memory.base) / 4u + dword] = 0u;
for (uint32_t size : {0u, 3u, 4u}) {
data[6] = size;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1u &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage &&
std::ranges::all_of(snapshot.images[0].dwords, [](uint32_t word) { return word == 0u; }),
"empty image table did not specialize as a safe null image");
}
memory.words[(0x2000u - memory.base) / 4u + 1u] = image_descriptor[1];
memory.words[(0x2000u - memory.base) / 4u + 3u] = image_descriptor[3];
memory.words[(0x2020u - memory.base) / 4u + 1u] = image_descriptor[1];
memory.words[(0x2020u - memory.base) / 4u + 3u] =
image_descriptor[3] ^ (1u << 28u);
ResourceSnapshot null_snapshot;
ResourceSpecialization null_specialization;
Check(MaterializeResources(resource_plan, runtime, null_snapshot,
null_specialization) &&
std::ranges::all_of(null_snapshot.images[0].dwords,
[](uint32_t dword) { return dword == 0u; }),
"stale typed null image descriptors were not canonicalized");
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
// The GPU computes these offsets; host materialization must not enumerate the key.
for (const auto [shader, immediate, key, expected] :
{std::array{16u, 0u, 0x55555555u, 0u},
std::array{0u, 16u, 0x55555555u, UINT32_MAX},
std::array{0xfffffff0u, 32u, 0u, UINT32_MAX}}) {
auto wrap = MakeIndirectImageFixture(false, 48u, immediate, shader);
wrap->PlanAndTrack();
for (auto &inst : *wrap->block) {
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer) {
inst.ReplaceUsesWith(Value(key));
break;
}
}
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
SrtWalker walker(wrap->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
}
memory.words[(0x2020u - memory.base) / 4u] ^= 1u;
memory.words[(0x1000u - memory.base + 36u) / 4u] = 1u;
ResourceSnapshot dynamic_snapshot;
ResourceSpecialization dynamic_specialization;
Check(MaterializeResources(resource_plan, runtime, dynamic_snapshot,
dynamic_specialization) &&
dynamic_snapshot.images.size() == 2 &&
dynamic_specialization.images.size() == 2,
"dynamic indirect image table did not materialize");
const auto second_image = (0x2020u - memory.base) / 4u;
memory.words[second_image + 1u] |= 3u << 30u;
memory.words[second_image + 2u] = 3u << 14u;
memory.words[second_image + 3u] =
Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kCube) << 28u);
memory.words[second_image + 4u] = 11u;
ResourceSnapshot mixed_snapshot;
ResourceSpecialization mixed_specialization;
Check(MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization) &&
mixed_snapshot.images.size() == 2 &&
mixed_specialization.images.size() == 2 &&
mixed_specialization.images[0].dimension ==
Decoder::ImageDimension::Dim2D &&
!mixed_specialization.images[0].cube &&
mixed_specialization.images[1].dimension ==
Decoder::ImageDimension::Dim2DArray &&
mixed_specialization.images[1].cube &&
std::equal(mixed_snapshot.images[1].dwords.begin(),
mixed_snapshot.images[1].dwords.end(),
memory.words.begin() + second_image),
"mixed 2D and cube candidates were rejected or discarded");
memory.words[second_image + 1u] =
static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32UInt)
<< 20u |
(3u << 30u);
Check(!MaterializeResources(resource_plan, runtime, mixed_snapshot,
mixed_specialization),
"indirect images with different numeric classes were accepted");
for (const auto dword : {1u, 2u, 3u, 4u}) {
memory.words[second_image + dword] = image_descriptor[dword];
}
ApplyResourceSpecialization(fixture->program, dynamic_specialization);
Check(fixture->program.info.images.size() == 2 &&
fixture->program.info.images[0].indirect_root == 0 &&
fixture->program.info.images[0].indirect_search_iterations != 0 &&
fixture->program.info.images[0].indirect_resources.size() == 2 &&
dynamic_snapshot.images.size() == 2,
"dynamic indirect image table was not specialized transactionally");
const auto &mapping = dynamic_specialization.images[0];
const auto key_count = dynamic_snapshot.flattened_srt[mapping.indirect_mapping_offset];
Check(mapping.indirect_search_iterations == std::bit_width(key_count) &&
mapping.indirect_mapping_offset + 1u + key_count * 2u ==
dynamic_snapshot.flattened_srt.size(),
"indirect image mapping retained worst-case padding");
for (uint32_t dword = 0; dword < image_descriptor.size(); dword++) {
memory.words[(0x2000u - memory.base) / 4u + dword] =
image_descriptor[dword];
memory.words[(0x2020u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
memory.words[(0x2020u - memory.base) / 4u] += 0x101u;
ResourceSnapshot rebound_snapshot;
ResourceSpecialization rebound_specialization;
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization == dynamic_specialization,
"stable indirect key mapping did not accept changed image addresses");
memory.words[(0x2020u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u];
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization != dynamic_specialization,
"collapsed indirect candidates did not select a new specialization");
const auto collapsed_specialization = rebound_specialization;
ResourceSnapshot capacity_snapshot;
ResourceSpecialization capacity_specialization;
for (const uint32_t records : {1u, 3u}) {
user_data[2] = records;
Check(MaterializeResources(resource_plan, runtime, capacity_snapshot,
capacity_specialization),
"runtime indirect key mapping rejected a valid material-table size");
}
user_data[2] = 2u;
memory.words[(0x2020u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u] + 1u;
memory.words[(0x2040u - memory.base) / 4u] =
memory.words[(0x2000u - memory.base) / 4u] + 2u;
for (uint32_t dword = 1; dword < image_descriptor.size(); dword++) {
memory.words[(0x2040u - memory.base) / 4u + dword] =
image_descriptor[dword];
}
memory.words[(0x1000u - memory.base + 68u) / 4u] = 2u;
Check(MaterializeResources(resource_plan, runtime, rebound_snapshot,
rebound_specialization) &&
rebound_specialization != collapsed_specialization,
"larger indirect candidate topology reused the old specialization");
auto memory_backed = MakeIndirectImageFixture(false, 4u, true);
memory_backed->PlanAndTrack();
auto memory_backed_plan = ExtractResourcePlan(memory_backed->program);
EliminateDeadCode(memory_backed->program.blocks);
std::array<uint32_t, 11> memory_backed_user_data{0x1000u, 224u << 16u, 2u, 0u,
0x2000u, 16u << 16u, 4u, 0u,
7u, 0x3100u, 0u};
memory.words[(0x3100u - memory.base) / 4u] = 0x3000u;
memory.words[(0x3000u - memory.base) / 4u] = 0x1000u;
memory.fail_address = 0x3100u;
SrtRuntime memory_backed_runtime{.user_data = memory_backed_user_data,
.userdata = &memory,
.read_specialization_memory =
ReadLinearTestMemory};
Check(!MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization),
"unreadable indirect table descriptor was accepted");
memory.fail_address = UINT64_MAX;
memory.watched_address = 0x3100u;
memory.watched_reads = 0;
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization) && memory.watched_reads == 1u,
"descriptor, flattened SRT and indirect-table roots repeated a clean pointer read");
const auto* buffer_storage = snapshot.buffers.data();
const auto* image_storage = snapshot.images.data();
const auto* flat_storage = snapshot.flattened_srt.data();
const auto* user_data_storage = snapshot.user_data.data();
const auto* buffer_specialization_storage = specialization.buffers.data();
const auto* image_specialization_storage = specialization.images.data();
const auto old_address = snapshot.images[0].dwords[0];
memory.words[(0x2000u - memory.base) / 4u] += 0x100u;
memory.watched_reads = 0;
Check(MaterializeResources(memory_backed_plan, memory_backed_runtime,
snapshot, specialization) && memory.watched_reads == 1u &&
snapshot.images[0].dwords[0] == old_address + 0x100u,
"cache refresh reused stale table contents or repeated its clean pointer read");
Check(snapshot.buffers.data() == buffer_storage && snapshot.images.data() == image_storage &&
snapshot.flattened_srt.data() == flat_storage &&
snapshot.user_data.data() == user_data_storage &&
specialization.buffers.data() == buffer_specialization_storage &&
specialization.images.data() == image_specialization_storage,
"a same-shape refresh discarded the runtime output storage");
auto malformed = MakeIndirectImageFixture(true);
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
"malformed indirect image pattern was accepted");
Check(!malformed->program.resource_tracking_complete &&
malformed->program.info.images.empty() &&
malformed->program.descriptor_sources.empty(),
"malformed indirect image pattern was partially accepted");
"noncontiguous descriptor DWORDs entered the table plan");
Check(!malformed->program.resource_tracking_complete && malformed->program.descriptor_sources.empty(),
"malformed image table tracking was not transactional");
}
auto negative_immediate = MakeIndirectImageFixture(false, 0xfffffffcu);
CheckFatal([&] { negative_immediate->PlanAndTrack(); },
"not a valid runtime value",
"negative scalar immediate entered the indirect image proof");
for (const auto [immediate, member, first, stride] :
{std::array{0u, 4u, 4u, 224u}, std::array{4u, 4u, 8u, 224u},
std::array{36u, 0u, 36u, 224u}, std::array{1u, 3u, 0u, 224u},
std::array{1u, 3u, 0u, 1u}, std::array{1u, 3u, 0u, 2u}}) {
auto split_offset = MakeIndirectImageFixture(false, immediate, false, member, stride);
split_offset->PlanAndTrack();
const auto split_plan = ExtractResourcePlan(split_offset->program);
LinearTestMemory split_memory;
std::copy(image_descriptor.begin(), image_descriptor.end(),
split_memory.words.begin() + 0x1000u / 4u);
split_memory.fail_address = first == 36u ? 0x1004u : UINT64_MAX;
split_memory.watched_address = split_memory.base + first;
user_data[1] = stride << 16u;
user_data[2] = stride < 4u ? 8u / stride : 1u;
SrtRuntime split_runtime{.user_data = user_data,
.userdata = &split_memory,
.read_specialization_memory = ReadLinearTestMemory};
Check(MaterializeResources(split_plan, split_runtime, snapshot, specialization) &&
snapshot.images.size() == 1 &&
snapshot.images[0].dwords == image_descriptor &&
split_memory.watched_reads == 1u,
"indirect scalar offsets did not align and bound their components independently");
void TestBoundedImageViewEligibility() {
using Type = Libs::Graphics::Prospero::ImageType;
auto fixture = MakeIndirectImageFixture(false, 48u, 0u, 16u);
fixture->PlanAndTrack();
auto plan = ExtractResourcePlan(fixture->program);
std::array<uint32_t, 8> data{0x3000u, 16u << 16u, 1u, 0u,
0x2000u, 48u << 16u, 5u, 0u};
LinearTestMemory memory;
memory.fail_address = 0x3000u;
const auto descriptor = [](uint32_t identity, Type type) {
return std::array<uint32_t, 8>{identity, 75u << 20u, 3u | (3u << 14u),
Libs::Graphics::DstSel(4, 5, 6, 7) | (static_cast<uint32_t>(type) << 28u),
0u, 0u, 0u, 0u};
};
const std::array types{Type::kColor2D, Type::kCube, Type::kColor3D,
Type::kColor2DArray, Type::kColor2D};
for (uint32_t row = 0; row < types.size(); ++row) {
const auto value = descriptor(0x20u + row, types[row]);
std::copy(value.begin(), value.end(), memory.words.begin() + (0x1010u + row * 48u) / 4u);
}
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 4u && memory.reads == 1u,
"unrelated cube/3D heap entries prevented a typed 2D table view");
const auto mapping = specialization.images[0].indirect_mapping_offset;
Check(snapshot.flattened_srt[mapping] == 53u &&
snapshot.flattened_srt[mapping + 5u] == 1u &&
snapshot.flattened_srt[mapping + 17u] == 0u &&
snapshot.flattened_srt[mapping + 29u] == 0u &&
snapshot.flattened_srt[mapping + 41u] == 2u &&
snapshot.flattened_srt[mapping + 53u] == 3u &&
snapshot.images[1].dwords == descriptor(0x20u, Type::kColor2D) &&
snapshot.images[2].dwords == descriptor(0x23u, Type::kColor2DArray) &&
snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D),
"typed table eligibility changed byte offsets or discarded compatible views");
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
// each 64-byte material record; the image occupies bytes 16..47 of its record.
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
packed->PlanAndTrack();
const auto packed_plan = ExtractResourcePlan(packed->program);
LinearTestMemory packed_memory;
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
0x2000u, 48u << 16u, 2u, 0u};
for (uint32_t i = 0; i < 3u; ++i)
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
for (uint32_t i = 0; i < 2u; ++i) {
auto descriptor = image_descriptor;
descriptor[0] += i;
std::copy(descriptor.begin(), descriptor.end(),
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
// An explicitly selected descriptor is not an unrelated entry in a broad heap.
const auto image_source = plan.info.images[0].source;
std::vector<uint32_t> selected;
for (auto type : {Type::kColor2D, Type::kColor3D}) {
DescriptorSource source;
source.dword_count = 8u;
const auto value = descriptor(0x30u, type);
for (uint32_t word = 0; word < value.size(); ++word) source.dwords[word] = Value(value[word]);
selected.push_back(static_cast<uint32_t>(plan.descriptor_sources.size()));
plan.descriptor_sources.push_back(source);
}
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
.read_specialization_memory = ReadLinearTestMemory};
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
"packed material flags changed the table key or 48-byte descriptor addressing");
packed_memory.words[60u / 4u] = 0xffff8002u;
packed_memory.fail_address = 0x2070u;
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
snapshot.images.size() == 3u &&
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
"masked out-of-range descriptor did not return zero without reading past table bounds");
packed_memory.fail_address = 0x205cu;
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
UINT32_MAX, 48u, 16u);
CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value",
"unbounded table offset conflated shader U32 wrap with scalar immediate addition");
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicitly selected incompatible descriptor was silently normalized to null");
}
void TestWaterfallImageTable() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
const auto make = [](bool equality, bool scalar) {
auto fixture = std::make_unique<Fixture>();
auto *entry = fixture->block;
auto *body = fixture->AddBlock();
auto *exit = fixture->AddBlock();
entry->AddBranch(body);
entry->AddBranch(exit);
body->AddBranch(exit);
std::array<Value, 4> words;
for (uint32_t i = 0; i < 4u; ++i) words[i] = fixture->UserData(i);
const auto table = fixture->Buffer(words);
const auto lane_value = fixture->Emit(ValueOpcode::GetAttribute, {Value(0u), Value(0u)});
const auto active = fixture->Emit(ValueOpcode::INotEqual32, {lane_value, Value(0u)});
const auto index = fixture->UserData(4u);
std::array<Value, 8> image_words;
auto guard = active;
for (uint32_t group = 0; group < 2u; ++group) {
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
memory.idxen = true;
memory.data_dwords = 4u;
memory.offset = 16u + group * 16u;
const auto loaded = fixture->Emit(ValueOpcode::LoadBufferU32x4,
{table, index, Value(0u), Value(0u), active}, fixture->AddMemory(memory, 0x850u));
for (uint32_t component = 0; component < 4u; ++component) {
const auto word = group * 4u + component;
const auto local = fixture->Emit(ValueOpcode::SelectU32,
{active, fixture->Emit(ValueOpcode::CompositeExtractU32x4,
{loaded, Value(component)}), Value(0xdeadu)});
image_words[word] = fixture->Emit(ValueOpcode::ReadLane, {local, Value(0u)});
if (equality || word != 7u)
guard = fixture->Emit(ValueOpcode::LogicalAnd,
{guard, fixture->Emit(ValueOpcode::IEqual32, {image_words[word], local})});
}
}
fixture->program.block_info[0].condition = guard;
fixture->program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1u, .false_block = 2u};
const auto sample = [&](Value image, Block *block) {
const auto sampler = fixture->Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
fixture->Emit(ValueOpcode::ReferenceU32,
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
};
if (scalar) {
const auto key = fixture->Emit(ValueOpcode::ReadFirstLane, {lane_value, active});
const auto offset = fixture->Emit(ValueOpcode::IAdd32,
{fixture->Emit(ValueOpcode::IMul32, {key, Value(48u)}), Value(16u)});
std::array<Value, 8> scalar_words;
for (uint32_t word = 0; word < 8u; ++word) {
MemoryInfo memory;
memory.kind = ResourceKind::ScalarBuffer;
memory.offset = word * 4u;
scalar_words[word] = fixture->Emit(ValueOpcode::ReadConstBuffer, {table, offset},
fixture->AddMemory(memory, 0x7bcu));
}
sample(fixture->Image(scalar_words), entry);
}
const auto image = fixture->Emit(ValueOpcode::GetImageResource,
{image_words[0], image_words[1], image_words[2], image_words[3],
image_words[4], image_words[5], image_words[6], image_words[7]}, 0, body);
sample(image, body);
return fixture;
};
auto fixture = make(true, true);
fixture->PlanAndTrack();
auto plan = ExtractResourcePlan(fixture->program);
Check(plan.info.images.size() == 1u &&
plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor->table_record_bytes == 48u,
"scalar and waterfall uses did not share their native image table");
std::array<uint32_t, 5> data{0x2000u, 48u << 16u, 2u, 0x5204u, 1u};
LinearTestMemory memory;
const std::array<uint32_t, 8> descriptor{0x20u, 75u << 20u, 3u | (3u << 14u),
Libs::Graphics::DstSel(4, 5, 6, 7) | (9u << 28u), 0u, 0u, 0u, 0u};
std::copy(descriptor.begin(), descriptor.end(), memory.words.begin() + 0x1010u / 4u);
SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1u &&
snapshot.images.size() == 2u,
"shared scalar/waterfall table repeated its coherent bulk read");
const auto handle = std::ranges::find_if(*fixture->program.blocks[1], [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
for (const auto [index, format, expected] :
{std::array{1u, 0x5204u, 64u}, std::array{2u, 0x5204u, UINT32_MAX},
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
data[4] = index;
data[3] = format;
SrtWalker walker(fixture->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
"VMEM table projection lost record bounds, format disable or multiplication carry");
}
auto disabled = make(true, false);
disabled->PlanAndTrack();
const auto disabled_plan = ExtractResourcePlan(disabled->program);
data[3] = 0u;
memory.fail_address = 0x2000u;
Check(MaterializeResources(disabled_plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 1u &&
specialization.images[0].indirect_root == ImageResource::NoIndirectImage,
"disabled VMEM descriptor table read its unavailable backing");
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"VMEM format disable incorrectly disabled a shared scalar descriptor read");
auto malformed = make(false, false);
CheckFatal([&] { malformed->PlanAndTrack(); }, "not a valid runtime value",
"waterfall without all descriptor equality witnesses was accepted");
}
void TestGuardedDirectImageTable() {
@@ -709,8 +644,8 @@ void TestGuardedDirectImageTable() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->material_source == UINT32_MAX &&
indirect->selector_stride == 0u && indirect->table_offset == 344u &&
Check(indirect && !indirect->selector.has_value() &&
indirect->table_offset == 344u &&
indirect->key_count.Resolve().IsImmediate() &&
indirect->key_count.Resolve().U32() == 32u &&
fixture.program.descriptor_sources[indirect->table_source].dword_count == 2u,
@@ -1007,7 +942,7 @@ void TestBoundedComputeImageLoop() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->material_source == UINT32_MAX &&
Check(indirect && !indirect->selector.has_value() &&
indirect->table_offset == 0x6b0u &&
indirect->key_count.Resolve() == count.Resolve(),
"bounded compute loop lost its runtime image count");
@@ -1362,8 +1297,8 @@ void TestUniformizedMaterialImageKeys() {
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &indirect = fixture.program.descriptor_sources[source].indirect_descriptor;
Check(indirect && indirect->selector_stride == 0x90u &&
indirect->selector_offset == 0xc00u &&
Check(indirect && indirect->selector->stride == 0x90u &&
indirect->selector->offset == 0xc00u &&
indirect->table_offset == 0x20e0u &&
!indirect->selector_mask.IsEmpty() &&
indirect->key_count.Resolve().IsImmediate() &&
@@ -3637,6 +3572,8 @@ int main() {
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
Run("dynamic storage mips", TestDynamicStorageMipTracking);
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
Run("bounded image view eligibility", TestBoundedImageViewEligibility);
Run("waterfall image table", TestWaterfallImageTable);
Run("guarded direct image table", TestGuardedDirectImageTable);
Run("bounded compute image loop", TestBoundedComputeImageLoop);
Run("uniformized material image keys", TestUniformizedMaterialImageKeys);
+493 -80
View File
@@ -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();
+261
View File
@@ -0,0 +1,261 @@
#include "common/file.h"
#include "common/trophies.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace {
namespace Trophies = Common::Trophies;
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "TrophySystemTests: failed: %s\n", text);
std::abort();
}
}
class TempDirectory {
public:
TempDirectory() {
const auto unique =
std::chrono::steady_clock::now().time_since_epoch().count();
m_path = std::filesystem::temp_directory_path() /
("kyty_trophy_test_" + std::to_string(unique));
Check(std::filesystem::create_directories(m_path),
"create temporary directory");
}
~TempDirectory() {
std::error_code error;
std::filesystem::remove_all(m_path, error);
}
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
private:
std::filesystem::path m_path;
};
void WriteBigEndian(std::vector<char> &bytes, size_t offset, uint64_t value,
size_t length) {
Check(offset <= bytes.size() && length <= bytes.size() - offset,
"write within package");
for (size_t i = 0; i < length; ++i) {
bytes[offset + length - 1 - i] = static_cast<char>(value & 0xff);
value >>= 8;
}
}
void WritePackage(const std::filesystem::path &path,
const std::map<std::string, std::string> &files) {
size_t offset = 0x40 + files.size() * 0x40;
size_t size = offset;
for (const auto &[name, contents] : files)
size += contents.size();
std::vector<char> bytes(size);
WriteBigEndian(bytes, 0, 0xb228c60a, 4);
WriteBigEndian(bytes, 4, 1, 4);
WriteBigEndian(bytes, 8, bytes.size(), 8);
WriteBigEndian(bytes, 0x10, files.size(), 4);
WriteBigEndian(bytes, 0x14, 0x20, 4);
size_t entry = 0x40;
for (const auto &[name, contents] : files) {
Check(name.size() < 0x20, "fixture file name fits");
std::memcpy(bytes.data() + entry, name.data(), name.size());
WriteBigEndian(bytes, entry + 0x20, offset, 8);
WriteBigEndian(bytes, entry + 0x28, contents.size(), 8);
std::memcpy(bytes.data() + offset, contents.data(), contents.size());
offset += contents.size();
entry += 0x40;
}
Common::File file;
Check(file.Create(path), "create package fixture");
uint32_t written = 0;
file.Write(bytes.data(), static_cast<uint32_t>(bytes.size()), &written);
Check(written == bytes.size(), "write package fixture");
}
Trophies::Package MakePackage(const std::filesystem::path &directory) {
const auto path = directory / "trophy00.ucp";
WritePackage(
path,
{{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[
{"id":"0","grade":"P"},
{"id":"1","grade":"G","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
{"id":"2","grade":"S","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100","progressive":false}},
{"id":"3","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5","progressive":false}},
{"id":"4","grade":"B","unlockCondition":{"udsStatId":"10","comparator":"lt","targetValue":"-1","progressive":false}}
]})"},
{"tropmeta_en-US.json",
R"({"metadata":{"titleMetadata":{"name":"Test game"},"trophyMetadata":[
{"id":"1","name":"Find All Coins","detail":"Find all coins in the level."}
]}})"}});
return Trophies::LoadPackage(path, 1);
}
void TestEventExtraction(const std::filesystem::path &directory) {
auto package = MakePackage(directory);
Check(package.trophies.size() == 5,
"unsupported signed target does not discard trophy metadata");
Check(package.trophies.at(1).uds_stat_id == 10 &&
package.trophies.at(1).target == 10 &&
!package.trophies.at(1).progressive,
"non-progressive trophy retains its stat target");
Check(!package.trophies.at(4).target,
"negative target is not converted to unsigned");
Check(Trophies::FindUdsTrophies(package, "FIND_ALL_COINS",
{{"counter", uint64_t{100}}})
.empty(),
"missing mapping never guesses from trophy text");
const auto path = directory / "uds00.ucp";
WritePackage(path, {{"stats_definition.json", R"({"statDefinitionArray":[
{"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"},
{"statId":20,"dataType":"uint64","aggregation":"latest"},
{"statId":30,"dataType":"uint32","aggregation":"latest"},
{"statId":40,"dataType":"uint64","aggregation":"sum"},
{"statId":50,"dataType":"uint64","aggregation":"count"},
{"statId":60,"dataType":"uint64","aggregation":"max"},
{"statId":70,"dataType":"uint64","aggregation":"latest","maxValue":"3"}
]})"},
{"stats_extraction.json", R"({"statsExtractionRuleArray":[
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}},
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counterBase","output":{"statId":20}}},
{"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}},
{"condition":{"eventName":"FILTERED","property":{"path":"$.level","comparator":"==","value":1}},"action":{"input":"$.counter","output":{"statId":10}}},
{"condition":{"eventName":"NESTED"},"action":{"input":"$.player.counter","output":{"statId":10}}},
{"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":40}}},
{"condition":{"eventName":"COUNT"},"action":{"output":{"statId":50}}},
{"condition":{"eventName":"MAX"},"action":{"input":"$.counter","output":{"statId":60}}},
{"condition":{"eventName":"BOUNDED"},"action":{"input":"$.counter","output":{"statId":70}}},
{"bad":1}
]})"}});
package.event_rules = Trophies::LoadUdsRules(path);
Check(package.event_rules.size() == 2,
"skip unsupported extraction and aggregation");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{9}}, {"counterBase", uint64_t{100}}})
.empty(),
"do not add independent counters or treat gt as ge");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{100}}}) ==
std::vector<int>{1},
"each stat reads its configured property");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{0}}, {"counterBase", uint64_t{101}}}) ==
std::vector<int>{2},
"strict comparison requires greater value");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{101}}}) ==
std::vector<int>({1, 2}),
"one event can unlock multiple mapped trophies");
Check(Trophies::FindUdsTrophies(
package, "TIME", {{"seconds", uint32_t{5}}}) == std::vector<int>{3},
"less-equal condition accepts its boundary");
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{6}}})
.empty(),
"less-equal condition rejects larger input");
Check(Trophies::FindUdsTrophies(package, "TIME", {{"counter", uint64_t{0}}})
.empty(),
"missing property is not zero");
package.trophies.at(3).comparison = Trophies::Comparison::Less;
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{5}}})
.empty(),
"strict less rejects its boundary");
Check(Trophies::FindUdsTrophies(
package, "TIME", {{"seconds", uint32_t{4}}}) == std::vector<int>{3},
"strict less accepts lower input");
package.trophies.at(3).comparison = Trophies::Comparison::GreaterEqual;
Check(Trophies::FindUdsTrophies(package, "TIME",
{{"seconds", uint64_t{UINT32_MAX} + 1}})
.empty(),
"uint64 input cannot update a uint32 stat");
Check(
Trophies::FindUdsTrophies(package, "COLLECT", {{"counter", int32_t{100}}})
.empty(),
"signed inputs cannot update unsigned stats");
Check(Trophies::FindUdsTrophies(package, "COLLECT",
{{"counter", uint32_t{100}}})
.empty(),
"unsigned input width must match the stat");
package.trophies.at(1).comparison = Trophies::Comparison::None;
Check(Trophies::FindUdsTrophies(package, "COLLECT",
{{"counter", uint64_t{100}}})
.empty(),
"unknown comparator cannot unlock");
WritePackage(path, {{"stats_extraction.json", "{}"}});
Check(Trophies::LoadUdsRules(path).empty(),
"missing stat definitions cannot infer aggregation");
Check(Trophies::LoadUdsRules(directory / "missing.ucp").empty(),
"missing UDS package has no rules");
}
void TestProgress(const std::filesystem::path &directory) {
const auto package = MakePackage(directory);
Trophies::UnlockData unlocks;
unlocks.unlocked = {0, 1, 999};
const auto progress = Trophies::GetProgress(package, unlocks);
Check(progress.total == 5 && progress.earned == 2,
"ignore unlock IDs absent from package");
Check(progress.earned_grade[1] == 1 && progress.earned_grade[2] == 1 &&
progress.Percentage() == 60,
"percentage uses 6:2:1 grade points and excludes platinum");
const auto group = Trophies::GetProgress(package, unlocks, 1);
Check(group.total == 1 && group.earned == 0 && group.Percentage() == 0,
"group counts filter trophies");
Check(Trophies::GetProgress({}, {}).Percentage() == 0,
"empty package progress is zero");
}
void TestUnlockPersistence(const std::filesystem::path &directory) {
const auto path = Trophies::UnlocksPath(directory, "PPSA00001", 1000, 0);
Check(path == directory / "_Trophies/PPSA00001/trophies_1000_0.json",
"store trophies outside save data");
Check(Trophies::UnlocksPath(directory, "../bad", 1000, 0).empty(),
"reject unsafe title IDs");
Check(Trophies::LoadUnlockData(path).unlocked.empty(),
"missing unlock file loads as empty");
Trophies::UnlockData unlocks;
unlocks.unlocked = {4, 42};
const auto tick = Trophies::UnixEpochTick + 1791288000123456ULL;
unlocks.timestamps.emplace(42, tick);
Check(Trophies::SaveUnlockData(path, unlocks), "save timestamped unlocks");
auto loaded = Trophies::LoadUnlockData(path);
Check(loaded.unlocked == unlocks.unlocked &&
loaded.timestamps == unlocks.timestamps,
"persist full UTC microsecond timestamp");
loaded.unlocked.insert(43);
loaded.timestamps.emplace(43, tick + 1000000);
Check(Trophies::SaveUnlockData(path, loaded), "replace existing unlock file");
Check(Trophies::LoadUnlockData(path).timestamps.at(42) == tick,
"retain original unlock time");
const auto blocked = directory / "blocked.json";
std::filesystem::create_directory(blocked);
Common::File sentinel;
Check(sentinel.Create(blocked / "keep"),
"create replacement failure fixture");
sentinel.Close();
Check(!Trophies::SaveUnlockData(blocked, loaded) &&
std::filesystem::exists(blocked / "keep"),
"failed replacement leaves destination intact");
Check(!std::filesystem::exists(directory / "blocked.json.tmp"),
"failed replacement removes temporary file");
}
} // namespace
int main() {
TempDirectory directory;
TestEventExtraction(directory.Path());
TestProgress(directory.Path());
TestUnlockPersistence(directory.Path());
std::printf("TrophySystemTests: all cases passed\n");
return 0;
}