Compare commits

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

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

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

* Fix trophy rule evaluation and share progress accounting

* Match trophy and reload toolbar styling

---------

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

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

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

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

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

* libNet: simplify URI build code and tests

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-05 21:31:45 +04:00
nmzik e6fce45d3a gpu: skip drains for CPU-only memory unmaps 2026-10-05 21:00:12 +04:00
nmzik 5b826c6e4a shader: emit native 64-bit operations where compatible 2026-10-05 20:41:03 +04:00
nmzik 77d7722173 shader: disable explicit MAD denormal checks for performance testing
Comment out the five explicit flush checks and record the substantial 3DMiniGolf menu FPS drop on NVIDIA RTX 5080 Laptop GPU. Preserve separate multiply/add rounding and both NoContraction decorations.

The matched runs used silent logging, no debugger, no Vulkan validation, and the cd9fc6ff revert on both revisions. Native denormal behavior remains GPU-dependent.
2026-10-05 18:39:31 +04:00
nmzik 266c12cec6 Revert "shader: keep scalar data native and preserve raw addresses"
This reverts commit cd9fc6ff2d.
2026-10-05 18:39:31 +04:00
nmzik 7a0e1dafd7 shader: classify native half values with shared operand decoding 2026-10-05 18:39:31 +04:00
nmzik 1d41ddb378 shader: allow RDNA2 SDWA partial destinations for i24 multiply 2026-10-05 18:39:31 +04:00
nmzik 5c83060481 shader: resolve masked image keys in strided descriptor records 2026-10-05 14:43:21 +04:00
nmzikandFelipe 72e4989b1e Add trophy notification support
Co-authored-by: Felipe <54341095+Pipe530@users.noreply.github.com>
2026-10-05 10:24:12 +04:00
fxpw 1d55272467 shader: support V_CMPX_NE_U16 2026-10-05 09:25:19 +04:00
nmzik 2b1b7ed0e4 shader: FLAT opcode=0x24 2026-10-05 08:48:20 +04:00
nmzikandshadowbeat070 af3011cd69 shader: add missing 64-bit integer image atomics
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-05 04:12:06 +04:00
nmzikandshadowbeat070 de9c15fa79 shader: encode SNORM and scaled formatted buffer stores
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-05 04:11:48 +04:00
nmzikandshadowbeat070 e85279ea7b shader: correct DS float min/max operands and special values
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-05 04:11:48 +04:00
nmzikandshadowbeat070 d8be245c22 shader: separate legacy FP32 MAD rounding from fused FMA
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-05 04:11:48 +04:00
95 changed files with 6798 additions and 3463 deletions

No files matched your search

+17 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.18...4.3)
cmake_minimum_required(VERSION 3.19...4.3)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "Minimum policy version for bundled dependencies")
@@ -413,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>)
@@ -725,6 +732,15 @@ endif()
add_executable(kyty_emulator "${KYTY_SOURCE_DIR}/main.cpp" ${kyty_emulator_src})
configure_macos_guest_address_space(kyty_emulator)
set(KYTY_TROPHY_UNLOCK_SOUND "${CMAKE_CURRENT_SOURCE_DIR}/assets/sounds/trophy-unlock.wav")
add_custom_target(kyty_trophy_sound
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_TROPHY_UNLOCK_SOUND}"
"$<TARGET_FILE_DIR:kyty_emulator>/assets/sounds/trophy-unlock.wav"
VERBATIM)
add_dependencies(kyty_emulator kyty_trophy_sound)
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION assets/sounds)
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
if (WIN32)
target_link_libraries(kyty_emulator opengl32)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.
+1
View File
@@ -27,6 +27,7 @@ target_link_libraries(common
kyty_git_version
spdlog::spdlog
ZArchive::zarchive
nlohmann_json::nlohmann_json
)
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
+4
View File
@@ -156,6 +156,10 @@ bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool TrophyEnabled() {
return g_config->trophy_enabled;
}
bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
+2
View File
@@ -74,6 +74,7 @@ struct ConfigOptions {
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool playgo_hack_enabled = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
@@ -123,6 +124,7 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
bool PlayGoHackEnabled();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
+453
View File
@@ -0,0 +1,453 @@
#include "common/trophies.h"
#include "common/file.h"
#include <algorithm>
#include <array>
#include <charconv>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <span>
namespace Common::Trophies {
namespace {
using Json = nlohmann::json;
using Files = std::map<std::string_view, std::span<const std::byte>>;
constexpr uint64_t MaxUnlocksSize = uint64_t {1} << 20u;
constexpr uint64_t MaxPackageSize = uint64_t {1} << 30u;
uint64_t ReadBigEndian(const std::byte* data, size_t size) {
uint64_t value = 0;
for (size_t i = 0; i < size; ++i) {
value = (value << 8u) | std::to_integer<uint8_t>(data[i]);
}
return value;
}
int ReadId(const Json& value) {
if (value.is_number_integer()) {
const auto number = value.get<int64_t>();
return number >= 0 && number <= INT32_MAX ? static_cast<int>(number) : -1;
}
if (!value.is_string()) {
return -1;
}
const auto& text = value.get_ref<const std::string&>();
int id = -1;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id);
return error == std::errc {} && end == text.data() + text.size() && id >= 0 ? id : -1;
}
std::optional<uint64_t> ReadUnsigned(const Json& value) {
if (value.is_number_unsigned()) {
return value.get<uint64_t>();
}
if (value.is_number_integer()) {
const auto number = value.get<int64_t>();
return number >= 0 ? std::optional<uint64_t>(static_cast<uint64_t>(number)) : std::nullopt;
}
if (!value.is_string()) {
return std::nullopt;
}
const auto& text = value.get_ref<const std::string&>();
uint64_t number = 0;
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), number);
return error == std::errc {} && end == text.data() + text.size()
? std::optional<uint64_t>(number)
: std::nullopt;
}
Json ReadJson(const Files& files, std::string_view name) {
const auto entry = files.find(name);
if (entry == files.end()) {
return {};
}
return Json::parse(entry->second.begin(), entry->second.end(), nullptr, false);
}
bool ReadFiles(std::span<const std::byte> data, Files& files, uint64_t& size) {
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
return false;
}
size = ReadBigEndian(data.data() + 8, 8);
const auto count = ReadBigEndian(data.data() + 0x10, 4);
const auto toc = ReadBigEndian(data.data() + 0x14, 4);
if (size < 0x40 || size > data.size() || count > 4096 || toc > size ||
0x20 + count * 0x40 > size - toc) {
return false;
}
for (uint64_t i = 0; i < count; ++i) {
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
const auto* name = reinterpret_cast<const char*>(entry);
const std::string_view filename(name, std::find(name, name + 0x20, '\0') - name);
const auto offset = ReadBigEndian(entry + 0x20, 8);
const auto length = ReadBigEndian(entry + 0x28, 8);
if (offset > size || length > size - offset) {
return false;
}
files.emplace(filename, data.subspan(offset, length));
}
return true;
}
Package ParsePackage(std::span<const std::byte> data, int console_language) {
Files files;
uint64_t size = 0;
if (!ReadFiles(data, files, size)) {
return {};
}
const auto conf = ReadJson(files, "tropconf.json");
if (!conf.is_object() || !conf.contains("trophies") || !conf["trophies"].is_array() ||
conf["trophies"].size() > 1000 || !conf.contains("defaultLanguage") ||
!conf["defaultLanguage"].is_string()) {
return {};
}
static constexpr std::array<std::string_view, 30> locales = {
"ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT",
"ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO",
"pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ",
"hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID"};
const auto locale =
locales[console_language >= 0 && console_language < locales.size() ? console_language : 1];
auto meta = ReadJson(files, fmt::format("tropmeta_{}.json", locale));
if (meta.is_null()) {
meta = ReadJson(files, fmt::format("tropmeta_{}.json",
conf["defaultLanguage"].get_ref<const std::string&>()));
}
if (!meta.is_object() || !meta.contains("metadata") || !meta["metadata"].is_object()) {
return {};
}
const auto& texts = meta["metadata"];
if (!texts.contains("titleMetadata") || !texts["titleMetadata"].is_object() ||
!texts["titleMetadata"].contains("name") || !texts["titleMetadata"]["name"].is_string() ||
!texts.contains("trophyMetadata") || !texts["trophyMetadata"].is_array()) {
return {};
}
Package package;
package.title = texts["titleMetadata"]["name"].get<std::string>();
package.groups.emplace(-1, package.title);
uint64_t icon_bytes = 0;
for (const auto& definition: conf["trophies"]) {
if (!definition.is_object() || !definition.contains("id") ||
!definition.contains("grade") || !definition["grade"].is_string()) {
return {};
}
Trophy trophy;
trophy.id = ReadId(definition["id"]);
const auto grade = definition["grade"].get<std::string>();
const auto grade_index = std::string_view("PGSB").find(grade);
if (trophy.id < 0 || grade.size() != 1 || grade_index == std::string_view::npos) {
return {};
}
trophy.grade = static_cast<int>(grade_index) + 1;
if (definition.contains("groupId")) {
trophy.group_id = ReadId(definition["groupId"]);
if (trophy.group_id < 0) {
return {};
}
}
if (definition.contains("platinumTrophyId")) {
trophy.platinum_id = ReadId(definition["platinumTrophyId"]);
}
trophy.hidden = definition.contains("hidden") && definition["hidden"] == true;
trophy.has_reward = definition.contains("hasReward") && definition["hasReward"] == true;
if (definition.contains("unlockCondition")) {
const auto& condition = definition["unlockCondition"];
if (condition.is_object()) {
trophy.progressive =
condition.contains("progressive") && condition["progressive"] == true;
if (condition.contains("udsStatId")) {
trophy.uds_stat_id = ReadUnsigned(condition["udsStatId"]);
if (!trophy.uds_stat_id) {
return {};
}
}
if (condition.contains("targetValue")) {
trophy.target = ReadUnsigned(condition["targetValue"]);
}
const auto comparison = condition.find("comparator");
if (comparison != condition.end()) {
if (*comparison == "ge")
trophy.comparison = Comparison::GreaterEqual;
else if (*comparison == "gt")
trophy.comparison = Comparison::Greater;
else if (*comparison == "le")
trophy.comparison = Comparison::LessEqual;
else if (*comparison == "lt")
trophy.comparison = Comparison::Less;
}
}
}
const auto icon = files.find(fmt::format("trop{:04}.png", trophy.id));
if (icon != files.end()) {
if (icon->second.size() > size - icon_bytes) {
return {};
}
trophy.icon_png.assign(icon->second.begin(), icon->second.end());
icon_bytes += icon->second.size();
}
package.groups.try_emplace(trophy.group_id);
if (package.groups.size() > 50 ||
!package.trophies.emplace(trophy.id, std::move(trophy)).second) {
return {};
}
}
if (texts.contains("groupMetadata") && texts["groupMetadata"].is_array()) {
for (const auto& group: texts["groupMetadata"]) {
if (group.is_object() && group.contains("id") && group.contains("name") &&
group["name"].is_string()) {
const auto found = package.groups.find(ReadId(group["id"]));
if (found != package.groups.end()) {
found->second = group["name"].get<std::string>();
}
}
}
}
for (const auto& text: texts["trophyMetadata"]) {
if (!text.is_object() || !text.contains("id")) {
continue;
}
const auto found = package.trophies.find(ReadId(text["id"]));
if (found == package.trophies.end()) {
continue;
}
for (const auto& [key, field]: {std::pair {"name", &found->second.name},
{"detail", &found->second.description},
{"reward", &found->second.reward}}) {
if (text.contains(key) && text[key].is_string()) {
*field = text[key].get<std::string>();
}
}
}
return package;
}
} // namespace
Package LoadPackage(const std::filesystem::path& path, int console_language) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
return {};
}
return ParsePackage(file.ReadWholeBuffer(), console_language);
}
std::filesystem::path PackagePath(uint32_t service_label) {
return std::filesystem::path(PackageDirectory) / fmt::format("trophy{:02}.ucp", service_label);
}
std::filesystem::path UnlocksPath(const std::filesystem::path& root, std::string_view title_id,
int user_id, uint32_t service_label) {
if (title_id.empty() || !std::all_of(title_id.begin(), title_id.end(), [](char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
c == '_' || c == '-';
})) {
return {};
}
return root / "_Trophies" / title_id /
fmt::format("trophies_{}_{}.json", user_id, service_label);
}
UnlockData LoadUnlockData(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxUnlocksSize) {
return {};
}
const auto bytes = file.ReadWholeBuffer();
const auto json = Json::parse(bytes.begin(), bytes.end(), nullptr, false);
if (!json.is_object() || !json.contains("unlockedTrophies") ||
!json["unlockedTrophies"].is_array()) {
return {};
}
UnlockData unlocks;
for (const auto& value: json["unlockedTrophies"]) {
const auto id = ReadId(value);
if (id >= 0) {
unlocks.unlocked.insert(id);
}
}
if (json.contains("unlockedAt") && json["unlockedAt"].is_object()) {
for (auto it = json["unlockedAt"].begin(); it != json["unlockedAt"].end(); ++it) {
const auto id = ReadId(Json(it.key()));
if (id >= 0 && unlocks.unlocked.contains(id)) {
const auto tick = ReadUnsigned(it.value());
if (tick && *tick >= UnixEpochTick && *tick <= 315537897599999999ULL) {
unlocks.timestamps.emplace(id, *tick);
}
}
}
}
return unlocks;
}
bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks) {
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
return false;
}
Json timestamps = Json::object();
for (const auto& [id, tick]: unlocks.timestamps) {
if (unlocks.unlocked.contains(id)) {
timestamps[std::to_string(id)] = tick;
}
}
const auto text =
Json {{"unlockedTrophies", unlocks.unlocked}, {"unlockedAt", timestamps}}.dump();
auto temporary = path;
temporary += ".tmp";
File file;
if (text.size() > MaxUnlocksSize || !file.Create(temporary)) {
return false;
}
uint32_t written = 0;
file.Write(text.data(), static_cast<uint32_t>(text.size()), &written);
const bool complete = written == text.size() && file.Flush();
file.Close();
std::error_code error;
if (complete) {
std::filesystem::rename(temporary, path, error);
if (!error) {
return true;
}
}
std::filesystem::remove(temporary, error);
return false;
}
UdsRules LoadUdsRules(const std::filesystem::path& path) {
File file(path, File::Mode::Read);
if (file.IsInvalid() || file.Size() > MaxPackageSize) {
return {};
}
const auto bytes = file.ReadWholeBuffer();
Files files;
uint64_t size = 0;
if (!ReadFiles(bytes, files, size)) {
return {};
}
const auto definitions = ReadJson(files, "stats_definition.json");
const auto extraction = ReadJson(files, "stats_extraction.json");
if (!definitions.is_object() || !definitions.contains("statDefinitionArray") ||
!definitions["statDefinitionArray"].is_array() || !extraction.is_object() ||
!extraction.contains("statsExtractionRuleArray") ||
!extraction["statsExtractionRuleArray"].is_array()) {
return {};
}
// Only direct unsigned latest-value stats are evaluated here. Aggregated stats
// and filtered/nested extraction require UDS state and event types we do not implement.
std::map<uint64_t, bool> stats;
for (const auto& definition: definitions["statDefinitionArray"]) {
if (!definition.is_object() || !definition.contains("statId") ||
!definition.contains("aggregation") || definition["aggregation"] != "latest" ||
!definition.contains("dataType")) {
continue;
}
const auto id = ReadUnsigned(definition["statId"]);
const auto& type = definition["dataType"];
if (!id || (type != "uint64" && type != "uint32")) {
continue;
}
const uint64_t maximum = type == "uint32" ? UINT32_MAX : UINT64_MAX;
if ((definition.contains("minValue") && ReadUnsigned(definition["minValue"]) != 0) ||
(definition.contains("maxValue") && ReadUnsigned(definition["maxValue"]) != maximum)) {
continue;
}
stats.emplace(*id, type == "uint32");
}
UdsRules events;
for (const auto& rule: extraction["statsExtractionRuleArray"]) {
if (!rule.is_object() || !rule.contains("condition") || !rule.contains("action")) {
continue;
}
const auto& condition = rule["condition"];
const auto& action = rule["action"];
if (!condition.is_object() || condition.size() != 1 || !condition.contains("eventName") ||
!condition["eventName"].is_string() || !action.is_object() ||
!action.contains("input") || !action["input"].is_string() ||
!action.contains("output") || !action["output"].is_object() ||
!action["output"].contains("statId")) {
continue;
}
const auto id = ReadUnsigned(action["output"]["statId"]);
const auto& input = action["input"].get_ref<const std::string&>();
if (!id || !stats.contains(*id) || !input.starts_with("$.") || input.size() == 2 ||
input.find_first_not_of(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_", 2) !=
std::string::npos) {
continue;
}
events[condition["eventName"].get<std::string>()].push_back(
{*id, input.substr(2), stats.at(*id)});
}
return events;
}
std::filesystem::path UdsPackagePath(uint32_t service_label) {
return std::filesystem::path("sce_sys/uds") / fmt::format("uds{:02}.ucp", service_label);
}
std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties) {
const auto rules = package.event_rules.find(std::string(event_name));
if (rules == package.event_rules.end()) {
return {};
}
std::vector<int> ids;
for (const auto& [id, trophy]: package.trophies) {
if (trophy.grade == 1 || !trophy.uds_stat_id || !trophy.target) {
continue;
}
for (const auto& rule: rules->second) {
const auto value = properties.find(rule.input);
if (rule.stat_id != *trophy.uds_stat_id || value == properties.end()) {
continue;
}
std::optional<uint64_t> number;
if (rule.is_uint32) {
if (const auto* input = std::get_if<uint32_t>(&value->second)) number = *input;
} else if (const auto* input = std::get_if<uint64_t>(&value->second)) {
number = *input;
}
if (!number) continue;
bool matched = false;
switch (trophy.comparison) {
case Comparison::GreaterEqual: matched = *number >= *trophy.target; break;
case Comparison::Greater: matched = *number > *trophy.target; break;
case Comparison::LessEqual: matched = *number <= *trophy.target; break;
case Comparison::Less: matched = *number < *trophy.target; break;
case Comparison::None: break;
}
if (matched) {
ids.push_back(id);
break;
}
}
}
return ids;
}
uint32_t Progress::Percentage() const {
const auto points = total_grade[2] * 6 + total_grade[3] * 2 + total_grade[4];
const auto earned_points = earned_grade[2] * 6 + earned_grade[3] * 2 + earned_grade[4];
return points == 0 ? 0 : earned_points * 100 / points;
}
Progress GetProgress(const Package& package, const UnlockData& unlocks, std::optional<int> group) {
Progress progress;
for (const auto& [id, trophy]: package.trophies) {
if (group && trophy.group_id != *group) {
continue;
}
++progress.total;
++progress.total_grade[trophy.grade];
if (unlocks.unlocked.contains(id)) {
++progress.earned;
++progress.earned_grade[trophy.grade];
}
}
return progress;
}
} // namespace Common::Trophies
+87
View File
@@ -0,0 +1,87 @@
#ifndef KYTY_COMMON_TROPHIES_H_
#define KYTY_COMMON_TROPHIES_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
namespace Common::Trophies {
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
inline constexpr uint64_t UnixEpochTick = 62135596800000000ULL;
enum class Comparison { None, GreaterEqual, Greater, LessEqual, Less };
struct Trophy {
int id = 0;
int group_id = -1;
int platinum_id = -1;
int grade = 0;
std::optional<uint64_t> target;
std::optional<uint64_t> uds_stat_id;
Comparison comparison = Comparison::None;
bool progressive = false;
std::string name;
std::string description;
std::string reward;
std::vector<std::byte> icon_png;
bool hidden = false;
bool has_reward = false;
};
struct UdsRule {
uint64_t stat_id;
std::string input;
bool is_uint32;
};
using UdsRules = std::map<std::string, std::vector<UdsRule>>;
using UdsInteger = std::variant<int32_t, uint32_t, uint64_t>;
struct Package {
std::string title;
std::map<int, std::string> groups;
std::map<int, Trophy> trophies;
UdsRules event_rules;
};
struct UnlockData {
std::set<int> unlocked;
std::map<int, uint64_t> timestamps;
};
struct Progress {
std::array<uint32_t, 5> total_grade {};
std::array<uint32_t, 5> earned_grade {};
uint32_t total = 0;
uint32_t earned = 0;
[[nodiscard]] uint32_t Percentage() const;
};
[[nodiscard]] Progress GetProgress(const Package& package, const UnlockData& unlocks,
std::optional<int> group = std::nullopt);
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
[[nodiscard]] std::filesystem::path UdsPackagePath(uint32_t service_label);
[[nodiscard]] UdsRules LoadUdsRules(const std::filesystem::path& path);
[[nodiscard]] std::vector<int> FindUdsTrophies(const Package& package, std::string_view event_name,
const std::map<std::string, UdsInteger>& properties);
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
std::string_view title_id, int user_id,
uint32_t service_label);
[[nodiscard]] UnlockData LoadUnlockData(const std::filesystem::path& path);
[[nodiscard]] bool SaveUnlockData(const std::filesystem::path& path, const UnlockData& unlocks);
} // namespace Common::Trophies
#endif // KYTY_COMMON_TROPHIES_H_
+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;
+5
View File
@@ -1962,6 +1962,11 @@ bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
return true;
}
bool TextureCache::IsRegionRegistered(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_lock};
return !FindImagesInRegion(address, size, false).empty();
}
void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
if (!GuestRange {address, size}.Valid()) {
EXIT("TextureCache: invalid unmap range\n");
+1
View File
@@ -61,6 +61,7 @@ public:
uint32_t packed_clear);
void InvalidateMemory(uint64_t address, uint64_t size);
void InvalidateMemoryFromGPU(uint64_t address, uint64_t size);
[[nodiscard]] bool IsRegionRegistered(uint64_t address, uint64_t size);
[[nodiscard]] bool IsMeta(uint64_t address);
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice);
@@ -226,43 +226,37 @@ void CommandScheduler::PopPendingOperations() {
}
void CommandScheduler::DeferOperation(Common::UniqueFunction<void>&& operation) {
QueueOperation(std::move(operation), false);
}
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
QueueOperation(std::move(operation), true);
}
void CommandScheduler::QueueOperation(Common::UniqueFunction<void>&& operation, bool priority) {
CheckActive();
EXIT_IF(!operation);
std::unique_lock lock(m_operation_mutex);
if (m_operation_state == OperationState::Open) {
m_pending_operations.push({std::move(operation), CurrentTick()});
return;
}
if (g_deferred_callback_scheduler == this) {
auto& queue = priority ? m_priority_operations : m_pending_operations;
queue.push({std::move(operation), CurrentTick()});
lock.unlock();
operation();
if (priority) {
m_operation_available.notify_one();
}
return;
}
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
if (g_deferred_callback_scheduler != this) {
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
}
lock.unlock();
operation();
}
void CommandScheduler::DeferPriorityOperation(Common::UniqueFunction<void>&& operation) {
CheckActive();
EXIT_IF(!operation);
std::unique_lock lock(m_operation_mutex);
if (m_operation_state == OperationState::Open) {
m_priority_operations.push({std::move(operation), CurrentTick()});
lock.unlock();
m_operation_available.notify_one();
return;
}
if (g_deferred_callback_scheduler == this) {
lock.unlock();
operation();
return;
}
m_operation_available.wait(lock,
[this] { return m_operation_state == OperationState::Closed; });
lock.unlock();
operation();
bool CommandScheduler::HasPendingPriorityOperations() {
std::lock_guard lock(m_operation_mutex);
return !m_priority_operations.empty() || m_priority_active;
}
void CommandScheduler::PriorityOperationsThread(std::stop_token stop) {
@@ -38,8 +38,10 @@ public:
void PopPendingOperations();
void DrainPriorityOperations();
void WaitPriorityOperations(uint64_t tick);
// Guest-memory completions use the priority queue; normal callbacks maintain GPU resources.
void DeferOperation(Common::UniqueFunction<void>&& operation);
void DeferPriorityOperation(Common::UniqueFunction<void>&& operation);
[[nodiscard]] bool HasPendingPriorityOperations();
[[nodiscard]] static bool InDeferredOperation() noexcept;
[[nodiscard]] bool Active() const noexcept { return m_command.m_registers != nullptr; }
@@ -82,6 +84,7 @@ private:
void BeginNext();
void PriorityOperationsThread(std::stop_token stop);
void QueueOperation(Common::UniqueFunction<void>&& operation, bool priority);
void RunOperation(Common::UniqueFunction<void>&& operation);
MasterSemaphore m_master;
@@ -315,8 +315,6 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
r.depth_compare_op = static_cast<vk::CompareOp>(dc.zfunc);
r.depth_bounds_test_enable = dc.depth_bounds_enable;
r.depth_min_bounds = hw.GetDepthBoundsMin();
r.depth_max_bounds = hw.GetDepthBoundsMax();
r.stencil_clear_enable =
has_stencil && rc.stencil_clear_enable && !z.depth_view.stencil_write_disable;
@@ -29,8 +29,6 @@ struct RenderDepthInfo {
bool depth_write_enable = false;
vk::CompareOp depth_compare_op = vk::CompareOp::eNever;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
bool stencil_clear_enable = false;
uint8_t stencil_clear_value = 0;
bool stencil_test_enable = false;
@@ -390,7 +390,28 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
const auto mantissa = value & 0x3ffu;
if (exponent == 0) {
return std::copysign(static_cast<float>(mantissa) * 0x1p-24f,
std::bit_cast<float>(sign));
}
return std::bit_cast<float>(sign | ((exponent == 31 ? 255u : exponent + 112u) << 23u) |
(mantissa << 13u));
};
switch (format) {
case vk::Format::eR16G16Sfloat:
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
break;
// A single-plane float target carries its clear as raw float bits, the same encoding the
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
// contents.
@@ -443,6 +464,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -807,30 +807,52 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
}
}
void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
auto& cache = m_context.GetBufferCache();
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
return;
}
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
const auto requested_size = descriptor.GetSize();
if (address == 0 || requested_size == 0) {
prepared.buffer_sources.push_back({});
auto& cache = m_context.GetBufferCache();
for (auto* stage: stages) {
auto& prepared = *stage;
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
prepared.buffer_sources.clear();
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
continue;
}
const auto size = Libs::LibKernel::Memory::ClampRangeSize(address, requested_size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
auto size = descriptor.GetSize();
if (address == 0 || size == 0) {
prepared.buffer_sources.push_back({});
continue;
}
if (descriptor.NumRecords() == UINT32_MAX) {
if (program.info.buffers[resource].written) {
// Sentinel write ranges end before tables captured by any bound stage.
for (const auto* reader: stages) {
for (const auto [read_address, read_size]: reader->runtime->resources->specialization_reads) {
if (read_size == 0) continue;
if (read_address > address) {
size = std::min(size, read_address - address);
} else if (address - read_address < read_size) {
EXIT("scalar resource reads overlap a shader buffer write\n");
}
}
}
} else {
const auto& graphics = m_context.GetGraphics();
const auto limit = uint64_t {graphics.GetPhysicalDeviceProperties().limits.maxStorageBufferRange} -
(graphics.StorageMinAlignment() - 1);
size = std::min({size, uint64_t {256} * 1024 * 1024, limit});
}
}
size = Libs::LibKernel::Memory::ClampRangeSize(address, size);
prepared.buffer_sources.push_back({address, size, cache.FindBuffer(address, size)});
}
}
}
@@ -919,8 +941,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const> stages,
std::span<RenderColorInfo> colors) {
bool uses_dma = false;
FindBuffers(stages);
for (auto* stage: stages) {
FindBuffers(*stage);
uses_dma |= stage->runtime->program->info.uses_dma;
}
if (uses_dma) {
@@ -831,9 +831,6 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
}
static_params.depth_bounds_test_enable = depth.depth_bounds_test_enable;
static_params.depth_min_bounds = depth.depth_min_bounds;
static_params.depth_max_bounds = depth.depth_max_bounds;
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
@@ -38,9 +38,6 @@ struct PipelineStaticParameters {
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool depth_bounds_test_enable = false;
float depth_min_bounds = 0.0f;
float depth_max_bounds = 0.0f;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
@@ -64,7 +61,7 @@ struct PipelineStaticParameters {
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 125);
static_assert(sizeof(PipelineStaticParameters) == 116);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
@@ -471,14 +471,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
depth_stencil_info.depthBoundsTestEnable =
#if defined(__APPLE__)
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
#else
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
#endif
depth_stencil_info.minDepthBounds = static_params.depth_min_bounds;
depth_stencil_info.maxDepthBounds = static_params.depth_max_bounds;
std::vector<vk::DynamicState> dynamic_states {
vk::DynamicState::eViewportWithCount,
@@ -497,6 +489,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::DynamicState::eBlendConstants,
};
#if !defined(__APPLE__)
dynamic_states.push_back(vk::DynamicState::eDepthBoundsTestEnable);
dynamic_states.push_back(vk::DynamicState::eDepthBounds);
if (rendering.color_count != 0) {
dynamic_states.push_back(vk::DynamicState::eColorWriteEnableEXT);
}
+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();
@@ -99,7 +99,12 @@ void RenderContext::UnmapMemory(uint64_t vaddr, uint64_t size) {
vaddr, size);
}
const auto unmap = [this, vaddr, size] {
if (m_command_scheduler.Active()) {
// Check cache ownership on the GPU thread. Guest-memory callbacks can still
// access a range with no cached data, so they must finish before it is unmapped.
if (m_command_scheduler.Active() &&
(m_buffer_cache.IsRegionRegistered(vaddr, size) ||
m_texture_cache.IsRegionRegistered(vaddr, size) ||
m_command_scheduler.HasPendingPriorityOperations())) {
const auto tick = m_command_scheduler.CurrentTick();
m_command_scheduler.Finish();
m_command_scheduler.WaitPriorityOperations(tick);
@@ -379,6 +379,12 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
#if !defined(__APPLE__)
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
if (depth.depth_bounds_test_enable) {
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
}
#endif
const auto& mode = ctx.GetModeControl();
const auto& poly_offset = ctx.GetPolyOffset();
+239 -12
View File
@@ -3,14 +3,17 @@
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_vulkan.h"
#include "libs/controller.h"
#include "libs/dialog.h"
#include "libs/ime.h"
#include "libs/imeDialog.h"
#include "stb_image.h"
#include <algorithm>
#include <array>
@@ -18,12 +21,17 @@
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <deque>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace Libs::Graphics {
@@ -178,6 +186,14 @@ bool g_input_lifecycle_active = false;
bool g_controller_captured = false;
OverlaySession g_session;
SDL_Window* g_input_window = nullptr;
struct TrophyNotification {
std::string name;
int32_t grade;
std::vector<std::byte> icon_png;
};
std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
void ClearInputEvents() {
std::scoped_lock lock(g_input_mutex);
@@ -467,8 +483,9 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
const auto core = CoreIme::GetVisualState();
const auto dialog = DialogIme::GetVisualState();
const auto system = SystemDialog::GetVisualState();
return {core.active || dialog.active || system.active,
core.revision + dialog.revision + system.revision};
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@@ -599,12 +616,34 @@ bool ProcessSystemOverlayInput(const SDL_Event& event) {
}
}
void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
std::span<const std::byte> icon_png) {
std::scoped_lock lock(g_trophy_notification_mutex);
g_trophy_notifications.push_back(
{std::string(name), grade, std::vector<std::byte>(icon_png.begin(), icon_png.end())});
g_trophy_notification_active.store(true, std::memory_order_release);
}
struct SystemOverlay::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(GraphicContext& context): graphics(context) {}
~Impl() {
SDL_DestroyAudioStream(trophy_sound_stream);
SDL_free(trophy_sound_data);
if (trophy_sound_initialized) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
ReleaseVulkan();
if (imgui_context != nullptr) {
ImGui::SetCurrentContext(imgui_context);
if (trophy_notification_image) {
ImGui::UnregisterUserTexture(trophy_notification_image.get());
}
for (const auto& texture: retired_trophy_images) {
ImGui::UnregisterUserTexture(texture.get());
}
ImGui::DestroyContext(imgui_context);
}
}
@@ -631,6 +670,75 @@ struct SystemOverlay::Impl {
style.ItemSpacing = {8.0f, 8.0f};
}
void PlayTrophySound() {
if (!trophy_sound_attempted) {
trophy_sound_attempted = true;
trophy_sound_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
if (!trophy_sound_initialized) {
LOGF("[Trophy] could not initialize audio: %s\n", SDL_GetError());
return;
}
const char* base_path = SDL_GetBasePath();
if (base_path == nullptr) {
LOGF("[Trophy] could not locate sound: %s\n", SDL_GetError());
return;
}
const std::string path = std::string(base_path) + "assets/sounds/trophy-unlock.wav";
SDL_AudioSpec spec {};
if (!SDL_LoadWAV(path.c_str(), &spec, &trophy_sound_data, &trophy_sound_size)) {
LOGF("[Trophy] could not load %s: %s\n", path.c_str(), SDL_GetError());
return;
}
if (trophy_sound_size > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
LOGF("[Trophy] sound is too large: %s\n", path.c_str());
return;
}
trophy_sound_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec, nullptr, nullptr);
if (trophy_sound_stream == nullptr) {
LOGF("[Trophy] could not open audio stream: %s\n", SDL_GetError());
return;
}
}
if (trophy_sound_stream != nullptr &&
(!SDL_PutAudioStreamData(trophy_sound_stream, trophy_sound_data,
static_cast<int>(trophy_sound_size)) ||
!SDL_ResumeAudioStreamDevice(trophy_sound_stream))) {
LOGF("[Trophy] could not play sound: %s\n", SDL_GetError());
}
}
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
if (encoded.empty() ||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
const auto* data = reinterpret_cast<const stbi_uc*>(encoded.data());
const int size = static_cast<int>(encoded.size());
int width = 0;
int height = 0;
if (!stbi_info_from_memory(data, size, &width, &height, nullptr) || width <= 0 ||
height <= 0 || width > 512 || height > 512) {
LOGF("[Trophy] invalid icon or dimensions exceed 512x512\n");
return nullptr;
}
auto* pixels = stbi_load_from_memory(data, size, &width, &height, nullptr, 4);
if (pixels == nullptr) {
LOGF("[Trophy] could not decode trophy icon: %s\n", stbi_failure_reason());
return nullptr;
}
auto texture = std::make_unique<ImTextureData>();
texture->Create(ImTextureFormat_RGBA32, width, height);
std::memcpy(texture->Pixels, pixels,
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
texture->UseColors = true;
stbi_image_free(pixels);
ImGui::RegisterUserTexture(texture.get());
return texture;
}
void EnsureVulkan(vk::Format format, uint32_t image_count) {
EnsureContext();
if (vulkan_initialized) {
@@ -952,9 +1060,116 @@ struct SystemOverlay::Impl {
}
}
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
std::erase_if(retired_trophy_images, [](const auto& texture) {
if (texture->Status != ImTextureStatus_Destroyed) {
return false;
}
ImGui::UnregisterUserTexture(texture.get());
return true;
});
constexpr float TOAST_LIFETIME = 5.0f;
if (trophy_notification &&
std::chrono::duration<float>(now - trophy_notification_born).count() >=
TOAST_LIFETIME) {
trophy_notification.reset();
if (trophy_notification_image) {
// Keep the texture registered until the backend retires in-flight frames.
trophy_notification_image->WantDestroyNextFrame = true;
retired_trophy_images.push_back(std::move(trophy_notification_image));
}
}
if (!trophy_notification) {
{
std::scoped_lock lock(g_trophy_notification_mutex);
if (g_trophy_notifications.empty()) {
g_trophy_notification_active.store(!retired_trophy_images.empty(),
std::memory_order_release);
return;
}
trophy_notification = std::move(g_trophy_notifications.front());
g_trophy_notifications.pop_front();
}
trophy_notification_born = now;
trophy_notification_image = LoadTrophyIcon(trophy_notification->icon_png);
PlayTrophySound();
}
const float age = std::chrono::duration<float>(now - trophy_notification_born).count();
const float screen_scale =
std::clamp(std::min(static_cast<float>(frame_extent.width) / 1920.0f,
static_cast<float>(frame_extent.height) / 1080.0f),
0.65f, 1.25f);
const float width = std::max(
0.0f, std::min(520.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
const float height = 104.0f * screen_scale;
const float rounding = 14.0f * screen_scale;
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
const float alpha = std::min(fade_in, fade_out);
const float slide = (1.0f - fade_in) * (width + 24.0f);
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 32.0f - width + slide,
32.0f};
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
auto* draw = ImGui::GetForegroundDrawList();
const auto fade = [alpha](int r, int g, int b, int a) {
return IM_COL32(r, g, b, static_cast<int>(static_cast<float>(a) * alpha));
};
draw->AddRectFilled({top_left.x + 3.0f, top_left.y + 5.0f},
{bottom_right.x + 3.0f, bottom_right.y + 5.0f}, fade(0, 0, 0, 90),
rounding);
draw->AddRectFilled(top_left, bottom_right, fade(24, 26, 32, 235), rounding);
draw->AddRect(top_left, bottom_right, fade(255, 255, 255, 28), rounding,
ImDrawFlags_RoundCornersAll, 1.0f);
const float pad = 10.0f * screen_scale;
const float icon_size = height - 2.0f * pad;
float text_left = top_left.x + 20.0f * screen_scale;
if (trophy_notification_image != nullptr) {
const ImVec2 icon_min {top_left.x + pad, top_left.y + pad};
const ImVec2 icon_max {icon_min.x + icon_size, icon_min.y + icon_size};
draw->AddImageRounded(trophy_notification_image->GetTexRef(), icon_min, icon_max,
{0, 0}, {1, 1}, fade(255, 255, 255, 255), 8.0f * screen_scale);
draw->AddRect(icon_min, icon_max, fade(255, 255, 255, 40), 8.0f * screen_scale,
ImDrawFlags_RoundCornersAll, 1.0f);
text_left = icon_max.x + 18.0f * screen_scale;
}
constexpr std::array GRADE_COLORS {IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255),
IM_COL32(211, 220, 232, 255),
IM_COL32(211, 142, 91, 255)};
const int grade = trophy_notification->grade;
const float title_y = top_left.y + height * 0.5f - 25.0f * screen_scale;
float title_x = text_left;
if (grade > 0 && grade <= static_cast<int>(GRADE_COLORS.size())) {
// Small cup glyph tinted with the trophy grade.
const ImU32 cup = ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha);
const float u = screen_scale;
const float cx = text_left + 9.0f * u;
const float cy = title_y + 15.0f * u;
draw->AddRectFilled({cx - 8.0f * u, cy - 11.0f * u}, {cx + 8.0f * u, cy + 2.0f * u},
cup, 2.0f * u, ImDrawFlags_RoundCornersBottom);
draw->AddRect({cx - 11.0f * u, cy - 9.0f * u}, {cx - 6.0f * u, cy - 3.0f * u}, cup,
2.0f * u, ImDrawFlags_RoundCornersLeft, 1.8f * u);
draw->AddRect({cx + 6.0f * u, cy - 9.0f * u}, {cx + 11.0f * u, cy - 3.0f * u}, cup,
2.0f * u, ImDrawFlags_RoundCornersRight, 1.8f * u);
draw->AddRectFilled({cx - 1.5f * u, cy + 2.0f * u}, {cx + 1.5f * u, cy + 7.0f * u}, cup);
draw->AddRectFilled({cx - 5.0f * u, cy + 7.0f * u}, {cx + 5.0f * u, cy + 10.0f * u}, cup,
1.5f * u);
title_x += 30.0f * u;
}
draw->PushClipRect(top_left, {bottom_right.x - 16.0f * screen_scale, bottom_right.y}, true);
draw->AddText(nullptr, 26.0f * screen_scale, {title_x, title_y},
fade(255, 255, 255, 255), trophy_notification->name.c_str());
draw->AddText(nullptr, 20.0f * screen_scale, {text_left, title_y + 38.0f * screen_scale},
fade(176, 180, 192, 255), "Trophy earned!");
draw->PopClipRect();
}
bool PrepareFrame(vk::Extent2D frame_extent, vk::Format format, uint32_t image_count) {
OverlaySnapshot snapshot;
if (!GetOverlaySnapshot(&snapshot)) {
const bool has_overlay = GetOverlaySnapshot(&snapshot);
const auto now = Clock::now();
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) {
return false;
}
const auto prepared_session = snapshot.session;
@@ -977,7 +1192,6 @@ struct SystemOverlay::Impl {
auto& io = ImGui::GetIO();
io.DisplaySize = {static_cast<float>(frame_extent.width),
static_cast<float>(frame_extent.height)};
const auto now = std::chrono::steady_clock::now();
io.DeltaTime = last_frame == std::chrono::steady_clock::time_point {}
? 1.0f / 60.0f
: std::clamp(std::chrono::duration<float>(now - last_frame).count(),
@@ -985,15 +1199,19 @@ struct SystemOverlay::Impl {
last_frame = now;
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
if (!GetOverlaySnapshot(&snapshot) || snapshot.session != prepared_session) {
GetOverlaySnapshot(&snapshot);
if (snapshot.session != prepared_session) {
ImGui::EndFrame();
return false;
}
if (snapshot.session.kind == OverlayKind::Dialog) {
DrawDialog(snapshot.dialog, frame_extent);
} else {
DrawIme(snapshot.ime, frame_extent);
if (has_overlay) {
if (snapshot.session.kind == OverlayKind::Dialog) {
DrawDialog(snapshot.dialog, frame_extent);
} else {
DrawIme(snapshot.ime, frame_extent);
}
}
DrawTrophyNotification(frame_extent, now);
ImGui::Render();
extent = frame_extent;
return true;
@@ -1040,9 +1258,18 @@ struct SystemOverlay::Impl {
float button_height = 42.0f;
ImVec2 panel_offset {};
ImVec2 right_stick {};
OverlaySession session;
vk::Extent2D extent {};
std::chrono::steady_clock::time_point last_frame;
OverlaySession session;
std::unique_ptr<ImTextureData> trophy_notification_image;
std::optional<TrophyNotification> trophy_notification;
Clock::time_point trophy_notification_born;
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
SDL_AudioStream* trophy_sound_stream = nullptr;
uint8_t* trophy_sound_data = nullptr;
uint32_t trophy_sound_size = 0;
bool trophy_sound_attempted = false;
bool trophy_sound_initialized = false;
vk::Extent2D extent {};
std::chrono::steady_clock::time_point last_frame;
};
SystemOverlay::SystemOverlay(GraphicContext& graphics): m_impl(std::make_unique<Impl>(graphics)) {}
@@ -4,7 +4,10 @@
#include "common/common.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstddef>
#include <memory>
#include <span>
#include <string_view>
union SDL_Event;
struct SDL_Window;
@@ -22,6 +25,8 @@ void InitializeSystemOverlayInput(SDL_Window* window);
void ShutdownSystemOverlayInput();
bool ProcessSystemOverlayInput(const SDL_Event& event);
SystemOverlayVisualState GetSystemOverlayVisualState() noexcept;
void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
std::span<const std::byte> icon_png);
class SystemOverlay final {
public:
@@ -205,7 +205,8 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
if (memory_index >= program.memory_info.size()) {
Fail(program, "address operation has invalid memory metadata");
}
const auto kind = program.memory_info[memory_index].kind;
const auto& memory = program.memory_info[memory_index];
const auto kind = memory.kind;
if (kind == IR::ResourceKind::Scratch || kind == IR::ResourceKind::FlatLocal) {
if (program.scratch_dwords == 0) {
Fail(program, "scratch operation has no per-thread storage");
@@ -217,6 +218,9 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
}
} else if (address_access == IR::AddressAccess::Write) {
Fail(program, "writable FLAT/GLOBAL addresses require GPU ownership tracking");
} else {
// Descriptor stores can alias coherent physical-address loads.
requirements.coherent_buffers |= memory.coherent;
}
}
if (IR::BufferAccessOf(inst.GetOpcode()) != IR::BufferAccess::None) {
@@ -17,21 +17,8 @@ Pair ExtractPair(EmitterState& state, uint32_t value) {
return result;
}
uint32_t MakePair(EmitterState& state, uint32_t low, uint32_t high) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU64(state), result, low, high);
return result;
}
uint32_t CompareEqual64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value,
bool not_equal) {
const auto compare = Binary(state, not_equal ? spv::OpINotEqual : spv::OpIEqual,
TypeBoolVector(state, 2), lhs_value, rhs_value);
return Unary(state, not_equal ? spv::OpAny : spv::OpAll, TypeBool(state), compare);
}
uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto bits_type = TypeU64(state);
const auto bits_type = TypeU32Vector(state, 2);
const auto lhs_bits = Unary(state, spv::OpBitcast, bits_type, lhs);
const auto rhs_bits = Unary(state, spv::OpBitcast, bits_type, rhs);
const auto ordered = Binary(state, max_value ? spv::OpFOrdGreaterThan : spv::OpFOrdLessThan,
@@ -52,17 +39,6 @@ uint32_t EmitMinMaxF64(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max
rhs, result);
}
uint32_t CompareOrdered64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value,
spv::Op high_compare, spv::Op low_compare) {
const auto lhs = ExtractPair(state, lhs_value);
const auto rhs = ExtractPair(state, rhs_value);
const auto high_equal = Binary(state, spv::OpIEqual, TypeBool(state), lhs.high, rhs.high);
const auto high_result = Binary(state, high_compare, TypeBool(state), lhs.high, rhs.high);
const auto low_result = Binary(state, low_compare, TypeBool(state), lhs.low, rhs.low);
const auto low_path = Binary(state, spv::OpLogicalAnd, TypeBool(state), high_equal, low_result);
return Binary(state, spv::OpLogicalOr, TypeBool(state), high_result, low_path);
}
uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signed_value) {
const auto operand_type = signed_value ? TypeI32(state) : TypeU32(state);
const auto pair_type = signed_value ? TypeI32Pair(state) : TypeU32Pair(state);
@@ -80,86 +56,6 @@ uint32_t EmitMulHigh(EmitterState& state, uint32_t lhs, uint32_t rhs, bool signe
return signed_value ? Unary(state, spv::OpBitcast, TypeU32(state), high) : high;
}
uint32_t EmitShift64(EmitterState& state, spv::Op opcode, uint32_t value, uint32_t shift) {
const auto pair = ExtractPair(state, value);
const auto amount = EmitAndConstant(state, shift, 63u);
const auto word_shift = EmitAndConstant(state, amount, 31u);
const auto at_least_32 =
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), amount, ConstantU32(state, 32));
const auto nonzero =
Binary(state, spv::OpINotEqual, TypeBool(state), amount, ConstantU32(state, 0));
const auto carry_count = EmitAndConstant(
state, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32), word_shift), 31u);
if (opcode == spv::OpShiftLeftLogical) {
const auto low = Binary(state, opcode, TypeU32(state), pair.low, word_shift);
const auto carry =
Select(state, TypeU32(state), nonzero,
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low, carry_count),
ConstantU32(state, 0));
const auto high =
Binary(state, spv::OpBitwiseOr, TypeU32(state),
Binary(state, opcode, TypeU32(state), pair.high, word_shift), carry);
return MakePair(state,
Select(state, TypeU32(state), at_least_32, ConstantU32(state, 0), low),
Select(state, TypeU32(state), at_least_32, low, high));
}
const auto high = Binary(state, opcode, TypeU32(state), pair.high, word_shift);
const auto carry =
Select(state, TypeU32(state), nonzero,
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.high, carry_count),
ConstantU32(state, 0));
const auto low = Binary(
state, spv::OpBitwiseOr, TypeU32(state),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low, word_shift), carry);
const auto fill = opcode == spv::OpShiftRightArithmetic
? Binary(state, opcode, TypeU32(state), pair.high, ConstantU32(state, 31))
: ConstantU32(state, 0);
return MakePair(state, Select(state, TypeU32(state), at_least_32, high, low),
Select(state, TypeU32(state), at_least_32, fill, high));
}
uint32_t EmitConstantShift64(EmitterState& state, spv::Op opcode, uint32_t value, uint32_t shift) {
shift &= 63u;
if (shift == 0u) {
return value;
}
const auto pair = ExtractPair(state, value);
if (opcode == spv::OpShiftLeftLogical) {
if (shift < 32u) {
const auto low = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.low,
ConstantU32(state, shift));
const auto high = Binary(state, spv::OpBitwiseOr, TypeU32(state),
Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
pair.high, ConstantU32(state, shift)),
Binary(state, spv::OpShiftRightLogical, TypeU32(state),
pair.low, ConstantU32(state, 32u - shift)));
return MakePair(state, low, high);
}
return MakePair(state, ConstantU32(state, 0),
shift == 32u ? pair.low
: Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
pair.low, ConstantU32(state, shift - 32u)));
}
if (shift < 32u) {
const auto low = Binary(state, spv::OpBitwiseOr, TypeU32(state),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), pair.low,
ConstantU32(state, shift)),
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pair.high,
ConstantU32(state, 32u - shift)));
const auto high =
Binary(state, opcode, TypeU32(state), pair.high, ConstantU32(state, shift));
return MakePair(state, low, high);
}
const auto high = opcode == spv::OpShiftRightArithmetic
? Binary(state, spv::OpShiftRightArithmetic, TypeU32(state), pair.high,
ConstantU32(state, 31u))
: ConstantU32(state, 0);
const auto low = shift == 32u ? pair.high
: Binary(state, opcode, TypeU32(state), pair.high,
ConstantU32(state, shift - 32u));
return MakePair(state, low, high);
}
uint32_t EmitMinMax3(EmitterState& state, uint32_t a, uint32_t b, uint32_t c, bool signed_value,
bool max_value) {
const auto ab = signed_value ? EmitMinMaxI32Value(state, a, b, max_value)
@@ -223,6 +119,28 @@ uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
}
} // namespace
uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto result = EmitGlsl<GLSLstd450Fma, IR::Type::F32>(state, a, b, c);
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
return result;
}
uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
// Explicit denormal checks typically drop 3DMiniGolf menu FPS from 28-29 to 21-24
// on NVIDIA RTX 5080 Laptop GPU, so leave them disabled for this experiment.
// a = EmitFlushF32DenormToSignedZero(state, a);
// b = EmitFlushF32DenormToSignedZero(state, b);
// c = EmitFlushF32DenormToSignedZero(state, c);
const auto product = EmitFPMul32(state, a, b);
state.builder.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction);
// const auto sum = EmitFPAdd32(state, EmitFlushF32DenormToSignedZero(state, product), c);
const auto sum = EmitFPAdd32(state, product, c);
state.builder.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction);
// return EmitFlushF32DenormToSignedZero(state, sum);
return sum;
}
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto min_ab = EmitMinMaxF32Value(state, a, b, false);
const auto min3 = EmitMinMaxF32Value(state, min_ab, c, false);
@@ -237,57 +155,15 @@ uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c)
}
uint32_t EmitFindUMsb64(EmitterState& state, uint32_t value) {
const auto pair = ExtractPair(state, value);
const auto high_i = state.builder.AllocateId();
const auto low_i = state.builder.AllocateId();
state.builder.AddFunction(spv::OpExtInst, TypeI32(state), high_i, GlslStd450(state),
GLSLstd450FindUMsb, pair.high);
state.builder.AddFunction(spv::OpExtInst, TypeI32(state), low_i, GlslStd450(state),
GLSLstd450FindUMsb, pair.low);
const auto high = Unary(state, spv::OpBitcast, TypeU32(state), high_i);
const auto low = Unary(state, spv::OpBitcast, TypeU32(state), low_i);
const auto high_nonzero =
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, 0));
return Select(state, TypeU32(state), high_nonzero,
Binary(state, spv::OpIAdd, TypeU32(state), high, ConstantU32(state, 32)), low);
}
uint32_t EmitIMul64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
const auto lhs = ExtractPair(state, lhs_value);
const auto rhs = ExtractPair(state, rhs_value);
const auto low = Binary(state, spv::OpIMul, TypeU32(state), lhs.low, rhs.low);
const auto high0 = EmitMulHigh(state, lhs.low, rhs.low, false);
const auto high1 = Binary(state, spv::OpIMul, TypeU32(state), lhs.low, rhs.high);
const auto high2 = Binary(state, spv::OpIMul, TypeU32(state), lhs.high, rhs.low);
return MakePair(state, low,
Binary(state, spv::OpIAdd, TypeU32(state),
Binary(state, spv::OpIAdd, TypeU32(state), high0, high1), high2));
}
uint32_t EmitISub64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
const auto lhs = ExtractPair(state, lhs_value);
const auto rhs = ExtractPair(state, rhs_value);
const auto low = Binary(state, spv::OpISub, TypeU32(state), lhs.low, rhs.low);
const auto borrow = Binary(state, spv::OpULessThan, TypeBool(state), lhs.low, rhs.low);
const auto borrow_u32 =
Select(state, TypeU32(state), borrow, ConstantU32(state, 1), ConstantU32(state, 0));
const auto high0 = Binary(state, spv::OpISub, TypeU32(state), lhs.high, rhs.high);
const auto high = Binary(state, spv::OpISub, TypeU32(state), high0, borrow_u32);
return MakePair(state, low, high);
}
uint32_t EmitIAdd64(EmitterState& state, uint32_t lhs_value, uint32_t rhs_value) {
const auto lhs = ExtractPair(state, lhs_value);
const auto rhs = ExtractPair(state, rhs_value);
const auto low_pair = state.builder.AllocateId();
const auto low = state.builder.AllocateId();
const auto carry = state.builder.AllocateId();
state.builder.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), low_pair, lhs.low, rhs.low);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, low_pair, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), carry, low_pair, 1);
const auto high0 = Binary(state, spv::OpIAdd, TypeU32(state), lhs.high, rhs.high);
const auto high = Binary(state, spv::OpIAdd, TypeU32(state), high0, carry);
return MakePair(state, low, high);
// GLSL FindUMsb is restricted to 32-bit components.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), value);
const auto pair =
ExtractPair(state, EmitExt(state, TypeU32Vector(state, 2), GLSLstd450FindUMsb, {unpacked}));
const auto high_found =
Binary(state, spv::OpINotEqual, TypeBool(state), pair.high, ConstantU32(state, UINT32_MAX));
return Select(state, TypeU32(state), high_found,
Binary(state, spv::OpIAdd, TypeU32(state), pair.high, ConstantU32(state, 32)),
pair.low);
}
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0) {
@@ -316,7 +192,7 @@ uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0) {
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
const auto converted = Unary(state, spv::OpFConvert, TypeF32(state), arg0);
const auto source = EmitNative<spv::OpCompositeExtract, IR::Type::U32>(
state, Unary(state, spv::OpBitcast, TypeU64(state), arg0), 1u);
state, Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0), 1u);
const auto exponent = EmitAndConstant(state, source, 0x7ff00000u);
const auto overflow =
Binary(state, spv::OpLogicalAnd, TypeBool(state),
@@ -334,7 +210,21 @@ uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0) {
EmitFlushF32DenormToSignedZero(state, arg0));
}
uint32_t EmitCompositeConstructU64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
arg1 = arg1.Resolve();
return arg1.IsImmediate() && arg1.U32() == 0u
? Unary(ctx.state, spv::OpUConvert, TypeU64(ctx.state), arg0)
: PackU64(ctx.state, arg0, ctx.Def(arg1));
}
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1) {
const auto value = arg1.U32() == 0u ? arg0
: Binary(state, spv::OpShiftRightLogical, TypeU64(state),
arg0, ConstantU32(state, 32));
return Unary(state, spv::OpUConvert, TypeU32(state), value);
}
uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value arg1) {
return EmitNative<spv::OpCompositeExtract, IR::Type::U32>(state, arg0, arg1.U32());
}
@@ -366,33 +256,11 @@ uint32_t EmitIAbs32(EmitterState& state, uint32_t arg0) {
return Select(state, TypeU32(state), negative, neg, value);
}
uint32_t EmitShiftLeftLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
auto& state = ctx.state;
return arg1.Resolve().IsImmediate()
? EmitConstantShift64(state, spv::OpShiftLeftLogical, arg0, arg1.Resolve().U32())
: EmitShift64(state, spv::OpShiftLeftLogical, arg0, ctx.Def(arg1));
}
uint32_t EmitShiftRightLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
auto& state = ctx.state;
return arg1.Resolve().IsImmediate()
? EmitConstantShift64(state, spv::OpShiftRightLogical, arg0, arg1.Resolve().U32())
: EmitShift64(state, spv::OpShiftRightLogical, arg0, ctx.Def(arg1));
}
uint32_t EmitShiftRightArithmetic64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1) {
auto& state = ctx.state;
return arg1.Resolve().IsImmediate()
? EmitConstantShift64(state, spv::OpShiftRightArithmetic, arg0, arg1.Resolve().U32())
: EmitShift64(state, spv::OpShiftRightArithmetic, arg0, ctx.Def(arg1));
}
uint32_t EmitBitwiseAnd64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return Binary(state, spv::OpBitwiseAnd, TypeU64(state), arg0, arg1);
}
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0) {
const auto pair = ExtractPair(state, Unary(state, spv::OpBitCount, TypeU64(state), arg0));
// Vulkan bit-count operands remain 32-bit, including when counting a U64.
const auto unpacked = Unary(state, spv::OpBitcast, TypeU32Vector(state, 2), arg0);
const auto pair =
ExtractPair(state, Unary(state, spv::OpBitCount, TypeU32Vector(state, 2), unpacked));
return Binary(state, spv::OpIAdd, TypeU32(state), pair.low, pair.high);
}
@@ -446,38 +314,6 @@ uint32_t EmitUMedTri32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32
return EmitMed3(state, arg0, arg1, arg2, false);
}
uint32_t EmitIEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareEqual64(state, arg0, arg1, false);
}
uint32_t EmitINotEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareEqual64(state, arg0, arg1, true);
}
uint32_t EmitULessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpULessThan, spv::OpULessThan);
}
uint32_t EmitSLessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpSLessThan, spv::OpULessThan);
}
uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThan);
}
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpSLessThan, spv::OpULessThanEqual);
}
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpULessThan, spv::OpULessThanEqual);
}
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return CompareOrdered64(state, arg0, arg1, spv::OpUGreaterThan, spv::OpUGreaterThanEqual);
}
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0) {
return EmitNative<spv::OpFUnordNotEqual, IR::Type::U1>(state, arg0, arg0);
}
@@ -125,20 +125,23 @@ uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
return EmitLogicalAndBool(state, selected, class_match);
}
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
const auto bits = EmitBitcastF32ToU32(state, value);
const auto sign_bits = EmitAndConstant(state, bits, 0x80000000u);
const auto abs_bits = EmitAndConstant(state, bits, 0x7fffffffu);
const auto exponent_bits = EmitAndConstant(state, abs_bits, 0x7f800000u);
const auto mantissa_bits = EmitAndConstant(state, abs_bits, 0x007fffffu);
const auto quiet_bits = EmitAndConstant(state, mantissa_bits, 0x00400000u);
static uint32_t EmitClassMaskBits(EmitterState& state, uint32_t bits, uint32_t mask, bool half) {
const uint32_t sign_mask = half ? 0x8000u : 0x80000000u;
const uint32_t exponent_mask = half ? 0x7c00u : 0x7f800000u;
const uint32_t mantissa_mask = half ? 0x03ffu : 0x007fffffu;
const uint32_t quiet_mask = half ? 0x0200u : 0x00400000u;
const auto sign_bits = EmitAndConstant(state, bits, sign_mask);
const auto abs_bits = EmitAndConstant(state, bits, sign_mask - 1u);
const auto exponent_bits = EmitAndConstant(state, abs_bits, exponent_mask);
const auto mantissa_bits = EmitAndConstant(state, abs_bits, mantissa_mask);
const auto quiet_bits = EmitAndConstant(state, mantissa_bits, quiet_mask);
const auto sign = EmitCompareU32Constant(state, spv::OpINotEqual, sign_bits, 0);
const auto positive = EmitLogicalNotBool(state, sign);
const auto exponent_zero = EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, 0);
const auto exponent_nonzero = EmitLogicalNotBool(state, exponent_zero);
const auto exponent_inf =
EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, 0x7f800000u);
EmitCompareU32Constant(state, spv::OpIEqual, exponent_bits, exponent_mask);
const auto finite_exponent = EmitLogicalNotBool(state, exponent_inf);
const auto mantissa_zero = EmitCompareU32Constant(state, spv::OpIEqual, mantissa_bits, 0);
const auto mantissa_nonzero = EmitLogicalNotBool(state, mantissa_zero);
@@ -178,6 +181,14 @@ uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
EmitClassMaskBitMatch(state, mask, 9, EmitLogicalAndBool(state, inf, positive)));
}
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask) {
return EmitClassMaskBits(state, EmitBitcastF32ToU32(state, value), mask, false);
}
uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask) {
return EmitClassMaskBits(state, bits, mask, true);
}
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) {
const auto lhs_class = EmitClassifyF32(state, lhs);
const auto rhs_class = EmitClassifyF32(state, rhs);
@@ -136,8 +136,8 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image) {
EXIT("invalid image resource class");
}
const auto& info = ImageDimensionInfoFor(image.dimension);
const auto scalar_type = image.atomic64 ? TypeScalarU64(state)
: ImageScalarType(state, image.numeric_class);
const auto scalar_type =
image.atomic64 ? TypeU64(state) : ImageScalarType(state, image.numeric_class);
return state.builder.Type(spv::OpTypeImage, scalar_type,
info.spirv_dimension, image.depth_compare ? 1u : 0u, info.arrayed,
info.multisampled, sampled, format);
@@ -19,8 +19,8 @@ uint32_t Vector(EmitterState& s, uint32_t type, std::span<const uint32_t> compon
uint32_t NodeWord(EmitterState& s, uint32_t block, uint32_t word) {
const auto offset = Binary(s, spv::OpShiftLeftLogical, TypeU32(s), word, ConstantU32(s, 2));
const auto address = Binary(s, spv::OpIAdd, TypeScalarU64(s), block,
Unary(s, spv::OpUConvert, TypeScalarU64(s), offset));
const auto address =
Binary(s, spv::OpIAdd, TypeU64(s), block, Unary(s, spv::OpUConvert, TypeU64(s), offset));
const auto pointer = Unary(s, spv::OpConvertUToPtr, TypePhysicalU32Pointer(s), address);
const auto result = s.builder.AllocateId();
s.builder.AddFunction(spv::OpLoad, TypeU32(s), result, pointer, spv::MemoryAccessAlignedMask, 4u);
@@ -224,7 +224,7 @@ uint32_t Boxes(EmitterState& s, uint32_t first, uint32_t second, uint32_t half,
void DefineBvhIntersect(EmitterState& s) {
if (!s.requirements.bvh) return;
const auto u = TypeU32(s), f = TypeF32(s), b = TypeBool(s), wide = TypeScalarU64(s);
const auto u = TypeU32(s), f = TypeF32(s), b = TypeBool(s), wide = TypeU64(s);
const auto vec4 = TypeU32Vector(s, 4), vec3 = TypeF32Vector(s, 3);
const auto signature = s.builder.Type(spv::OpTypeFunction, vec4, vec4, wide, f, vec3, vec3, vec3);
s.bvh_intersect_function = s.builder.AllocateId();
@@ -260,22 +260,24 @@ void DefineBvhIntersect(EmitterState& s) {
const auto bary = Binary(s, spv::OpINotEqual, b, and_bits(words[3], 0x01000000), ConstantU32(s, 0));
const auto sort = Binary(s, spv::OpINotEqual, b, and_bits(words[1], 0x80000000), ConstantU32(s, 0));
const auto grow = and_bits(shr(words[1], 23), 0xff);
const auto base = Binary(s, spv::OpShiftLeftLogical, wide,
DeviceAddressFromWords(s, words[0], and_bits(words[1], 0xff)), ConstantDeviceAddress(s, 8));
const auto index = Binary(s, spv::OpShiftRightLogical, wide, node, ConstantDeviceAddress(s, 3));
const auto base = Binary(s, spv::OpShiftLeftLogical, wide,
PackU64(s, words[0], and_bits(words[1], 0xff)), ConstantU32(s, 8));
const auto index = Binary(s, spv::OpShiftRightLogical, wide, node, ConstantU32(s, 3));
const auto last_index = Binary(s, spv::OpIAdd, wide, index,
Select(s, wide, full, ConstantDeviceAddress(s, 1), ConstantDeviceAddress(s, 0)));
Select(s, wide, full, ConstantU64(s, 1), ConstantU64(s, 0)));
const auto address = Binary(s, spv::OpIAdd, wide, base,
Binary(s, spv::OpShiftLeftLogical, wide, index, ConstantDeviceAddress(s, 6)));
const auto last_address = Binary(s, spv::OpIAdd, wide, base,
Binary(s, spv::OpShiftLeftLogical, wide, last_index, ConstantDeviceAddress(s, 6)));
Binary(s, spv::OpShiftLeftLogical, wide, index, ConstantU32(s, 6)));
const auto last_address =
Binary(s, spv::OpIAdd, wide, base,
Binary(s, spv::OpShiftLeftLogical, wide, last_index, ConstantU32(s, 6)));
auto valid = Binary(s, spv::OpIEqual, b, shr(words[3], 28), ConstantU32(s, 8));
const auto require = [&](uint32_t condition) {
valid = Binary(s, spv::OpLogicalAnd, b, valid, condition);
};
require(Binary(s, spv::OpULessThanEqual, b, kind, ConstantU32(s, 5)));
require(Binary(s, spv::OpULessThanEqual, b, last_index, DeviceAddressFromWords(s, words[2], and_bits(words[3], 0x3ff))));
require(Binary(s, spv::OpULessThan, b, last_address, ConstantDeviceAddress(s, uint64_t{1} << 48)));
require(Binary(s, spv::OpULessThanEqual, b, last_index,
PackU64(s, words[2], and_bits(words[3], 0x3ff))));
require(Binary(s, spv::OpULessThan, b, last_address, ConstantU64(s, uint64_t {1} << 48)));
require(Unary(s, spv::OpLogicalNot, b, Unary(s, spv::OpIsNan, b, extent)));
for (uint32_t i = 0; i < 3; ++i) {
for (const auto value: {origin[i], direction[i], inverse[i]})
@@ -292,13 +294,13 @@ void DefineBvhIntersect(EmitterState& s) {
const auto invalid = Vector(s, vec4, invalid_words);
const auto result = EmitValueOrDefaultIfCondition(s, valid, vec4, invalid, [&] {
const auto first = GetBdaPointer(s, address);
const auto second = EmitValueOrDefaultIfCondition(s, full, wide, ConstantDeviceAddress(s, 0), [&] {
return GetBdaPointer(s, Binary(s, spv::OpIAdd, wide, address, ConstantDeviceAddress(s, 64)));
const auto second = EmitValueOrDefaultIfCondition(s, full, wide, ConstantU64(s, 0), [&] {
return GetBdaPointer(s, Binary(s, spv::OpIAdd, wide, address, ConstantU64(s, 64)));
});
const auto present = Binary(s, spv::OpLogicalAnd, b,
Binary(s, spv::OpINotEqual, b, first, ConstantDeviceAddress(s, 0)),
const auto present = Binary(
s, spv::OpLogicalAnd, b, Binary(s, spv::OpINotEqual, b, first, ConstantU64(s, 0)),
Binary(s, spv::OpLogicalOr, b, Unary(s, spv::OpLogicalNot, b, full),
Binary(s, spv::OpINotEqual, b, second, ConstantDeviceAddress(s, 0))));
Binary(s, spv::OpINotEqual, b, second, ConstantU64(s, 0))));
return EmitValueOrDefaultIfCondition(s, present, vec4, invalid, [&] {
const auto tri_label = s.builder.AllocateId(), box_label = s.builder.AllocateId();
const auto merge = s.builder.AllocateId();
@@ -336,9 +338,8 @@ uint32_t EmitBvhIntersect(ValueEmitContext& ctx, const IR::Inst& inst) {
return Vector(s, TypeF32Vector(s, 3), values);
};
const auto descriptor = ctx.Arg(inst, 0);
const auto node = node_words == 2u
? DeviceAddressFromWords(s, ctx.Arg(*ray, 0), ctx.Arg(*ray, 1))
: Unary(s, spv::OpUConvert, TypeScalarU64(s), ctx.Arg(*ray, 0));
const auto node = node_words == 2u ? PackU64(s, ctx.Arg(*ray, 0), ctx.Arg(*ray, 1))
: Unary(s, spv::OpUConvert, TypeU64(s), ctx.Arg(*ray, 0));
const auto extent = Unary(s, spv::OpBitcast, TypeF32(s), ctx.Arg(*ray, node_words));
const auto origin = vector(node_words + 1u), direction = vector(node_words + 4u),
inverse = vector(node_words + 7u);
@@ -430,16 +430,16 @@ uint32_t EmitWqmU64(EmitterState& state, uint32_t value) {
const auto quad_bits = state.builder.AllocateId();
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU64(state), shifted_one, value,
ConstantU64(state, 0x0000000100000001ull));
ConstantU32(state, 1));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU64(state), merged_one, value, shifted_one);
state.builder.AddFunction(spv::OpShiftRightLogical, TypeU64(state), shifted_two, merged_one,
ConstantU64(state, 0x0000000200000002ull));
ConstantU32(state, 2));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU64(state), merged_two, merged_one,
shifted_two);
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU64(state), quad_bits, merged_two,
ConstantU64(state, 0x1111111111111111ull));
state.builder.AddFunction(spv::OpIMul, TypeU64(state), result, quad_bits,
ConstantU64(state, 0x0000000f0000000full));
ConstantU64(state, 15));
return result;
}
@@ -849,74 +849,76 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
const auto key = ctx.Def(handle->Arg(0));
if (state.flattened_srt_variable == 0 || image.indirect_search_iterations == 0u ||
if (state.flattened_srt_variable == 0 ||
image.indirect_resources.size() < 2u) {
ctx.Fail(inst, "has no indirect image runtime mapping");
return;
}
const auto selected = EmitIndirectResourceIndex(
state, key, image.indirect_mapping_offset, image.indirect_search_iterations, 0u);
const auto kind = *IR::DescriptorBindingForImage(image);
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
uint32_t first_child = 0;
bool homogeneous = true;
struct SampleRun {
uint32_t first;
uint32_t count;
uint32_t resource;
uint32_t slot_bias;
};
std::vector<SampleRun> runs;
for (uint32_t ordinal = 0; ordinal < image.indirect_resources.size(); ++ordinal) {
const auto resource = image.indirect_resources[ordinal];
const auto& candidate = state.program.info.images[resource];
if (candidate.dimension != image.dimension || candidate.cube != image.cube ||
IR::DescriptorBindingForImage(candidate) != kind ||
candidate.mip_count != 1u) {
homogeneous = false;
break;
const auto kind = *IR::DescriptorBindingForImage(candidate);
const auto& binding = *IR::FindBinding(state.program.bindings, kind);
if (!runs.empty()) {
auto& run = runs.back();
const auto& first = state.program.info.images[run.resource];
const auto next_slot = run.slot_bias + ordinal;
if (candidate.dimension == first.dimension && candidate.cube == first.cube &&
IR::DescriptorBindingForImage(first) == kind &&
candidate.mip_count == 1u && first.mip_count == 1u &&
(ordinal == 1u || (next_slot < binding.resources.size() &&
binding.resources[next_slot] == resource))) {
// Native roots precede appended children; only the root may have a slot gap.
if (ordinal == 1u)
run.slot_bias = ResourceForDescriptor(state, kind, resource) - ordinal;
++run.count;
continue;
}
}
if (ordinal == 1u) first_child = ResourceForDescriptor(state, kind, resource);
const auto child_slot = first_child + ordinal - 1u;
if (ordinal > 1u && (child_slot >= binding.resources.size() ||
binding.resources[child_slot] != resource)) {
homogeneous = false;
break;
runs.push_back({ordinal, 1u, resource,
ResourceForDescriptor(state, kind, resource) - ordinal});
}
const auto EmitRun = [&](uint32_t index) {
const auto& run = runs[index];
if (run.count == 1u) return EmitSample(run.resource);
auto slot = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, run.slot_bias));
if (run.first == 0u) {
const auto kind = *IR::DescriptorBindingForImage(image);
const auto root_slot = ResourceForDescriptor(state, kind, mem.resource);
if (root_slot != run.slot_bias) {
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto root_or_child = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), root_or_child,
is_root, ConstantU32(state, root_slot), slot);
slot = root_or_child;
}
}
return EmitSample(run.resource, slot);
};
auto run_index = ConstantU32(state, 0u);
for (uint32_t index = 1; index < runs.size(); ++index) {
const auto at_or_after = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state),
selected, ConstantU32(state, runs[index].first));
const auto next = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), next, at_or_after,
ConstantU32(state, index), run_index);
run_index = next;
}
if (homogeneous) {
const auto child_index = Binary(state, spv::OpIAdd, TypeU32(state), selected,
ConstantU32(state, first_child - 1u));
const auto is_root = Binary(state, spv::OpIEqual, TypeBool(state), selected,
ConstantU32(state, 0u));
const auto array_index = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), array_index, is_root,
ConstantU32(state, ResourceForDescriptor(state, kind, mem.resource)),
child_index);
const auto sample = EmitSample(mem.resource, array_index);
const auto result = dref ? sample : UnpackImageTexel(ctx, mem, sample);
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
return;
}
const auto default_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
std::vector<uint32_t> labels(image.indirect_resources.size() - 1u);
std::vector<uint32_t> switch_words {spv::OpSwitch, selected, default_label};
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
labels[candidate - 1u] = state.builder.AllocateId();
switch_words.push_back(candidate);
switch_words.push_back(labels[candidate - 1u]);
}
state.builder.AddFunction(spv::OpSelectionMerge, merge_label,
spv::SelectionControlMaskNone);
state.builder.AddFunction(switch_words);
std::vector<uint32_t> phi_words {spv::OpPhi, result_type, state.builder.AllocateId()};
EmitLabel(state, default_label);
phi_words.push_back(EmitSample(image.indirect_resources[0]));
phi_words.push_back(default_label);
state.builder.AddFunction(spv::OpBranch, merge_label);
for (uint32_t candidate = 1; candidate < image.indirect_resources.size(); candidate++) {
EmitLabel(state, labels[candidate - 1u]);
phi_words.push_back(EmitSample(image.indirect_resources[candidate]));
phi_words.push_back(labels[candidate - 1u]);
state.builder.AddFunction(spv::OpBranch, merge_label);
}
EmitLabel(state, merge_label);
state.builder.AddFunction(phi_words);
auto result = phi_words[2];
auto result = runs.size() == 1u
? EmitRun(0u)
: EmitIndexSwitch(state, run_index, static_cast<uint32_t>(runs.size()),
result_type, EmitRun);
if (!dref) {
result = UnpackImageTexel(ctx, mem, result);
}
@@ -930,9 +932,9 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Define(inst, EmitValueOrDefaultIfCondition(
state, ctx.Arg(inst, inst.NumArgs() - 1), result_type, zero, [&]() {
const auto pointer = state.builder.AllocateId();
const auto pointer_type = state.builder.Type(
spv::OpTypePointer, spv::StorageClassImage,
image.atomic64 ? TypeScalarU64(state) : TypeU32(state));
const auto pointer_type =
state.builder.Type(spv::OpTypePointer, spv::StorageClassImage,
image.atomic64 ? TypeU64(state) : TypeU32(state));
state.builder.AddFunction(spv::OpImageTexelPointer, pointer_type, pointer,
ImageDescriptorPointer(state, mem.resource),
CoordU32(ctx, mem, *address, dimension),
@@ -34,16 +34,16 @@ uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0);
EMIT_NATIVE(ConvertF32U32, OpConvertUToF, F32, uint32_t)
EMIT_NATIVE(ConvertF64U32, OpConvertUToF, F64, uint32_t)
EMIT_NATIVE(CompositeConstructU64, OpCompositeConstruct, U64, uint32_t, uint32_t)
uint32_t EmitCompositeConstructU64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
EMIT_NATIVE(CompositeConstructU32x2, OpCompositeConstruct, U32x2, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructU32x3, OpCompositeConstruct, U32x3, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructF32x2, OpCompositeConstruct, F32x2, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructU32x4, OpCompositeConstruct, U32x4, uint32_t, uint32_t, uint32_t,
uint32_t)
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1);
inline constexpr auto EmitCompositeExtractU32x2 = EmitCompositeExtractU64;
inline constexpr auto EmitCompositeExtractU32x3 = EmitCompositeExtractU64;
inline constexpr auto EmitCompositeExtractU32x4 = EmitCompositeExtractU64;
uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value arg1);
inline constexpr auto EmitCompositeExtractU32x3 = EmitCompositeExtractU32x2;
inline constexpr auto EmitCompositeExtractU32x4 = EmitCompositeExtractU32x2;
inline constexpr auto EmitPackHalf2x16 = EmitGlsl<GLSLstd450PackHalf2x16, IR::Type::U32, uint32_t>;
inline constexpr auto EmitPackSnorm2x16 =
EmitGlsl<GLSLstd450PackSnorm2x16, IR::Type::U32, uint32_t>;
@@ -63,9 +63,9 @@ EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t)
EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t)
EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t)
EMIT_NATIVE(UDiv32, OpUDiv, U32, uint32_t, uint32_t)
uint32_t EmitIAdd64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitISub64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitIMul64(EmitterState& state, uint32_t arg0, uint32_t arg1);
EMIT_NATIVE(IAdd64, OpIAdd, U64, uint32_t, uint32_t)
EMIT_NATIVE(ISub64, OpISub, U64, uint32_t, uint32_t)
EMIT_NATIVE(IMul64, OpIMul, U64, uint32_t, uint32_t)
EMIT_NATIVE(IAddCarry32, OpIAddCarry, U32x2, uint32_t, uint32_t)
uint32_t EmitSMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitUMulHi(EmitterState& state, uint32_t arg0, uint32_t arg1);
@@ -73,14 +73,26 @@ uint32_t EmitIAbs32(EmitterState& state, uint32_t arg0);
EMIT_NATIVE(ShiftLeftLogical32, OpShiftLeftLogical, U32, uint32_t, uint32_t)
EMIT_NATIVE(ShiftRightLogical32, OpShiftRightLogical, U32, uint32_t, uint32_t)
EMIT_NATIVE(ShiftRightArithmetic32, OpShiftRightArithmetic, U32, uint32_t, uint32_t)
uint32_t EmitShiftLeftLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
uint32_t EmitShiftRightLogical64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
uint32_t EmitShiftRightArithmetic64(ValueEmitContext& ctx, uint32_t arg0, IR::Value arg1);
template <spv::Op opcode>
uint32_t EmitShift64(ValueEmitContext& ctx, uint32_t value, IR::Value shift) {
shift = shift.Resolve();
if (shift.IsImmediate()) {
const auto amount = shift.U32() & 63u;
return amount == 0u ? value
: Binary(ctx.state, opcode, TypeU64(ctx.state), value,
ConstantU32(ctx.state, amount));
}
return Binary(ctx.state, opcode, TypeU64(ctx.state), value,
EmitAndConstant(ctx.state, ctx.Def(shift), 63u));
}
inline constexpr auto EmitShiftLeftLogical64 = EmitShift64<spv::OpShiftLeftLogical>;
inline constexpr auto EmitShiftRightLogical64 = EmitShift64<spv::OpShiftRightLogical>;
inline constexpr auto EmitShiftRightArithmetic64 = EmitShift64<spv::OpShiftRightArithmetic>;
EMIT_NATIVE(BitwiseAnd32, OpBitwiseAnd, U32, uint32_t, uint32_t)
EMIT_NATIVE(BitwiseOr32, OpBitwiseOr, U32, uint32_t, uint32_t)
EMIT_NATIVE(BitwiseXor32, OpBitwiseXor, U32, uint32_t, uint32_t)
EMIT_NATIVE(BitwiseNot32, OpNot, U32, uint32_t)
uint32_t EmitBitwiseAnd64(EmitterState& state, uint32_t arg0, uint32_t arg1);
EMIT_NATIVE(BitwiseAnd64, OpBitwiseAnd, U64, uint32_t, uint32_t)
EMIT_NATIVE(BitReverse32, OpBitReverse, U32, uint32_t)
EMIT_NATIVE(BitCount32, OpBitCount, U32, uint32_t)
uint32_t EmitBitCount64(EmitterState& state, uint32_t arg0);
@@ -107,14 +119,14 @@ EMIT_NATIVE(UGreaterThan32, OpUGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(INotEqual32, OpINotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(SGreaterThanEqual32, OpSGreaterThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(UGreaterThanEqual32, OpUGreaterThanEqual, U1, uint32_t, uint32_t)
uint32_t EmitIEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitINotEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitULessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitSLessThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitSLessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitULessThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitUGreaterThanEqual64(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitUGreaterThan64(EmitterState& state, uint32_t arg0, uint32_t arg1);
EMIT_NATIVE(IEqual64, OpIEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(INotEqual64, OpINotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(ULessThan64, OpULessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(SLessThan64, OpSLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(SLessThanEqual64, OpSLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(ULessThanEqual64, OpULessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(UGreaterThanEqual64, OpUGreaterThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(UGreaterThan64, OpUGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalOr, OpLogicalOr, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalAnd, OpLogicalAnd, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalXor, OpLogicalNotEqual, U1, uint32_t, uint32_t)
@@ -152,6 +164,7 @@ inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFO
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
inline constexpr auto EmitFPCmpClass16 = EmitClassMaskF16;
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
@@ -160,8 +173,8 @@ EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
inline constexpr auto EmitFPFma64 =
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFPFma32 =
EmitGlsl<GLSLstd450Fma, IR::Type::F32, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPFma32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2);
uint32_t EmitFPMad32(EmitterState& state, uint32_t arg0, uint32_t arg1, uint32_t arg2);
uint32_t EmitFPMin32(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitFPMax32(EmitterState& state, uint32_t arg0, uint32_t arg1);
uint32_t EmitFPMin64(EmitterState& state, uint32_t arg0, uint32_t arg1);
@@ -321,15 +334,21 @@ inline constexpr auto EmitImageSampleRaw = EmitImage;
inline constexpr auto EmitImageGatherRaw = EmitImage;
inline constexpr auto EmitImageAtomicCompareSwap32 = EmitImage;
inline constexpr auto EmitImageAtomicSwap32 = EmitImage;
inline constexpr auto EmitImageAtomicSwap64 = EmitImage;
inline constexpr auto EmitImageAtomicIAdd32 = EmitImage;
inline constexpr auto EmitImageAtomicIAdd64 = EmitImage;
inline constexpr auto EmitImageAtomicSMin32 = EmitImage;
inline constexpr auto EmitImageAtomicUMin32 = EmitImage;
inline constexpr auto EmitImageAtomicUMin64 = EmitImage;
inline constexpr auto EmitImageAtomicSMax32 = EmitImage;
inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
inline constexpr auto EmitImageAtomicUMax64 = EmitImage;
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
inline constexpr auto EmitImageAtomicAnd64 = EmitImage;
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
inline constexpr auto EmitImageAtomicOr64 = EmitImage;
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
inline constexpr auto EmitImageAtomicXor64 = EmitImage;
inline constexpr auto EmitImageAtomicFMin32 = EmitImage;
inline constexpr auto EmitImageAtomicFMax32 = EmitImage;
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -98,7 +98,7 @@ struct EmitterState {
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t u32_type = 0;
uint32_t native_u64_type = 0;
uint32_t u64_type = 0;
uint32_t i32_type = 0;
uint32_t f32_type = 0;
uint32_t f64_type = 0;
@@ -168,7 +168,6 @@ uint32_t TypeBool(EmitterState& state);
uint32_t TypeBoolVector(EmitterState& state, uint32_t components);
uint32_t TypeU32(EmitterState& state);
uint32_t TypeU64(EmitterState& state);
uint32_t TypeScalarU64(EmitterState& state);
uint32_t TypeU32Pair(EmitterState& state);
uint32_t TypeI32(EmitterState& state);
uint32_t TypeI32Pair(EmitterState& state);
@@ -221,6 +220,12 @@ inline uint32_t Unary(EmitterState& state, spv::Op opcode, uint32_t type, uint32
return result;
}
inline uint32_t PackU64(EmitterState& state, uint32_t low, uint32_t high) {
const auto pair = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), pair, low, high);
return Unary(state, spv::OpBitcast, TypeU64(state), pair);
}
inline uint32_t Binary(EmitterState& state, spv::Op opcode, uint32_t type, uint32_t lhs,
uint32_t rhs) {
const auto result = state.builder.AllocateId();
@@ -463,6 +468,9 @@ void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind);
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
bool max_value);
uint32_t EmitDsFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
bool max_value);
uint32_t EmitDsSwizzleTargetLane(EmitterState& state, uint32_t subid, uint32_t control);
inline constexpr auto EmitSelectValueU32 =
@@ -507,6 +515,7 @@ uint32_t EmitClassMaskBitMatch(EmitterState& state, uint32_t mask, uint32_t bit,
uint32_t class_match);
uint32_t EmitClassMaskF32(EmitterState& state, uint32_t value, uint32_t mask);
uint32_t EmitClassMaskF16(EmitterState& state, uint32_t bits, uint32_t mask);
uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value);
@@ -527,8 +536,6 @@ void EmitProgram(EmitterState& state);
void DefineGetBdaPointer(EmitterState& state);
void DefineBvhIntersect(EmitterState& state);
uint32_t GetBdaPointer(EmitterState& state, uint32_t address);
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value);
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high);
// These templates accept local lambdas from several emitter translation units.
template <typename Fn>
@@ -133,22 +133,22 @@ uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Mem
}
uint32_t address = 0;
if (mem.address_is_full) {
address = DeviceAddressFromWords(state, low, ctx.Arg(inst, 2));
address = PackU64(state, low, ctx.Arg(inst, 2));
} else {
const auto* handle = inst.Arg(0).Resolve().TryInstruction();
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetAddressResource ||
handle->NumArgs() != 2) {
ctx.Fail(inst, "has no address base pair");
return ConstantDeviceAddress(state, 0);
return ConstantU64(state, 0);
}
auto base_low = ctx.Arg(*handle, 0);
if (mem.kind == IR::ResourceKind::ScalarAddress) {
base_low = Binary(state, spv::OpBitwiseAnd, TypeU32(state), base_low,
ConstantU32(state, ~3u));
}
const auto base = DeviceAddressFromWords(state, base_low, ctx.Arg(*handle, 1));
address = Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
Unary(state, spv::OpUConvert, TypeScalarU64(state), low));
const auto base = PackU64(state, base_low, ctx.Arg(*handle, 1));
address = Binary(state, spv::OpIAdd, TypeU64(state), base,
Unary(state, spv::OpUConvert, TypeU64(state), low));
}
auto immediate = static_cast<int32_t>(mem.offset);
if (mem.kind == IR::ResourceKind::ScalarAddress) {
@@ -156,9 +156,8 @@ uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Mem
}
return immediate == 0
? address
: Binary(state, spv::OpIAdd, TypeScalarU64(state), address,
ConstantDeviceAddress(
state, static_cast<uint64_t>(static_cast<int64_t>(immediate))));
: Binary(state, spv::OpIAdd, TypeU64(state), address,
ConstantU64(state, static_cast<uint64_t>(static_cast<int64_t>(immediate))));
}
uint32_t FaultElementPointer(EmitterState& state, uint32_t index) {
@@ -181,29 +180,46 @@ void RecordBdaFault(EmitterState& state, uint32_t page) {
Binary(state, spv::OpBitwiseOr, TypeU32(state), value, bit));
}
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address) {
uint32_t LoadBdaDword(ValueEmitContext& ctx, uint32_t address, bool coherent = false) {
auto& state = ctx.state;
const auto bda = GetBdaPointer(state, address);
const auto present =
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantDeviceAddress(state, 0));
Binary(state, spv::OpINotEqual, TypeBool(state), bda, ConstantU64(state, 0));
return EmitValueOrZeroIfCondition(state, present, [&]() {
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
bda);
auto pointer = state.builder.AllocateId();
if (coherent) {
const auto block = state.builder.DecoratedType(
spv::OpTypeStruct, {{spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}},
{spv::OpMemberDecorate, {0, spv::DecorationCoherent}},
{spv::OpDecorate, {spv::DecorationBlock}}}, TypeU32(state));
state.builder.AddFunction(
spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, block),
pointer, bda);
const auto base = pointer;
pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypePhysicalU32Pointer(state), pointer,
base, ConstantU32(state, 0));
} else {
state.builder.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer,
bda);
}
const auto value = state.builder.AllocateId();
constexpr uint32_t alignment = sizeof(uint32_t);
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer,
spv::MemoryAccessAlignedMask, alignment);
spv::MemoryAccessAlignedMask |
(coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone),
alignment);
return value;
});
}
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits) {
uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint32_t bits,
bool coherent = false) {
auto& state = ctx.state;
return EmitValueOrZeroIfCondition(state, active, [&]() {
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address,
ConstantDeviceAddress(state, ~uint64_t {3}));
const auto first = LoadBdaDword(ctx, aligned);
const auto aligned = Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
ConstantU64(state, ~uint64_t {3}));
const auto first = LoadBdaDword(ctx, aligned, coherent);
const auto byte =
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
@@ -212,8 +228,8 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
: Binary(state, bits == 16u ? spv::OpUGreaterThan : spv::OpINotEqual,
TypeBool(state), byte, ConstantU32(state, bits == 16u ? 2u : 0u));
const auto second = EmitValueOrZeroIfCondition(state, crosses, [&]() {
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeScalarU64(state), aligned,
ConstantDeviceAddress(state, sizeof(uint32_t))));
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), aligned,
ConstantU64(state, sizeof(uint32_t))), coherent);
});
const auto shift =
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte, ConstantU32(state, 3));
@@ -710,24 +726,37 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data) {
auto& state = ctx.state;
const auto bits = info.component_bits[component];
if (info.type == Format::ComponentType::Unorm) {
const auto value = EmitBitCastF32U32(ctx.state, data);
const auto clamped = EmitFPMin32(
ctx.state, EmitFPMax32(ctx.state, value, ConstantF32Value(ctx.state, 0.0f)),
ConstantF32Value(ctx.state, 1.0f));
const auto scaled = EmitFPMul32(
ctx.state, clamped, ConstantF32Value(ctx.state, float((1u << bits) - 1u)));
return Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state),
EmitFPRoundEven32(ctx.state, scaled));
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
info.type == Format::ComponentType::Sscaled;
if (normalized || scaled) {
// Two-bit packed channels are unsigned even in signed formats.
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
info.type == Format::ComponentType::Sscaled);
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
: (is_signed ? -max_value - 1.0f : 0.0f);
const auto upper = normalized ? 1.0f : max_value;
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
if (normalized) {
value = EmitFPRoundEven32(
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
}
// Float-to-integer conversion truncates scaled values toward zero.
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
is_signed ? TypeI32(state) : TypeU32(state), value);
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
}
if (bits == 16u && (info.type == Format::ComponentType::Snorm ||
info.type == Format::ComponentType::Float)) {
const auto value = EmitBitCastF32U32(ctx.state, data);
const auto pair = EmitCompositeConstructF32x2(ctx.state, value,
ConstantF32Value(ctx.state, 0.0f));
return info.type == Format::ComponentType::Float ? EmitPackHalf2x16(ctx.state, pair)
: EmitPackSnorm2x16(ctx.state, pair);
if (bits == 16u && info.type == Format::ComponentType::Float) {
const auto pair = EmitCompositeConstructF32x2(
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
return EmitPackHalf2x16(state, pair);
}
return data;
}
@@ -792,26 +821,25 @@ uint32_t LoadIndirectScalarBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto word1 = ctx.Arg(handle, 1);
const auto stride = EmitBitFieldUExtract(state, word1, ConstantU32(state, 16),
ConstantU32(state, 14));
const auto size = Binary(
state, spv::OpIMul, TypeScalarU64(state),
Unary(state, spv::OpUConvert, TypeScalarU64(state),
EmitUMax32(state, stride, ConstantU32(state, 1))),
Unary(state, spv::OpUConvert, TypeScalarU64(state), ctx.Arg(handle, 2)));
const auto offset = Binary(
state, spv::OpIAdd, TypeScalarU64(state),
Unary(state, spv::OpUConvert, TypeScalarU64(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, ~3u))),
ConstantDeviceAddress(state, ctx.Memory(inst).offset & ~3u));
const auto end = Binary(state, spv::OpIAdd, TypeScalarU64(state), offset,
ConstantDeviceAddress(state, sizeof(uint32_t)));
const auto size = Binary(state, spv::OpIMul, TypeU64(state),
Unary(state, spv::OpUConvert, TypeU64(state),
EmitUMax32(state, stride, ConstantU32(state, 1))),
Unary(state, spv::OpUConvert, TypeU64(state), ctx.Arg(handle, 2)));
const auto offset = Binary(state, spv::OpIAdd, TypeU64(state),
Unary(state, spv::OpUConvert, TypeU64(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ctx.Arg(inst, 1), ConstantU32(state, ~3u))),
ConstantU64(state, ctx.Memory(inst).offset & ~3u));
const auto end =
Binary(state, spv::OpIAdd, TypeU64(state), offset, ConstantU64(state, sizeof(uint32_t)));
return EmitValueOrZeroIfCondition(
state, Binary(state, spv::OpULessThanEqual, TypeBool(state), end, size), [&]() {
const auto base = DeviceAddressFromWords(
state, Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(handle, 0),
ConstantU32(state, ~3u)),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1, ConstantU32(state, 0xffffu)));
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeScalarU64(state), base, offset));
const auto base = PackU64(state,
Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ctx.Arg(handle, 0), ConstantU32(state, ~3u)),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), word1,
ConstantU32(state, 0xffffu)));
return LoadBdaDword(ctx, Binary(state, spv::OpIAdd, TypeU64(state), base, offset));
});
}
@@ -852,21 +880,21 @@ IndirectBufferAccess PrepareIndirectBuffer(ValueEmitContext& ctx, const IR::Inst
const auto address = CalculateBufferAddress(state, index, ctx.Arg(inst, 2), soffset,
ctx.Memory(inst).offset, stride, swizzle,
field(word3, 21, 2));
const auto base = DeviceAddressFromWords(state, ctx.Arg(handle, 0), field(word1, 0, 16));
const auto base = PackU64(state, ctx.Arg(handle, 0), field(word1, 0, 16));
const auto raw_records = Binary(state, spv::OpISub, TypeU32(state), records, soffset);
return {
.address = Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
Unary(state, spv::OpUConvert, TypeScalarU64(state), address.byte)),
.offset = address.offset,
.stride = stride,
.swizzle = swizzle,
.format = field(word3, 12, 7),
.selector = ctx.Memory(inst).formatted ? field(word3, 0, 3) : 0u,
.mode = field(word3, 28, 2),
.records = records,
.index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records),
.address = Binary(state, spv::OpIAdd, TypeU64(state), base,
Unary(state, spv::OpUConvert, TypeU64(state), address.byte)),
.offset = address.offset,
.stride = stride,
.swizzle = swizzle,
.format = field(word3, 12, 7),
.selector = ctx.Memory(inst).formatted ? field(word3, 0, 3) : 0u,
.mode = field(word3, 28, 2),
.records = records,
.index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, records),
.scalar_in_bounds = Binary(state, spv::OpULessThanEqual, TypeBool(state), soffset, records),
.raw_records = raw_records,
.raw_records = raw_records,
.raw_index_in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index, raw_records),
};
}
@@ -912,13 +940,13 @@ uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_
const auto buffer = PrepareIndirectBuffer(ctx, inst);
const auto valid_format = Binary(state, spv::OpINotEqual, TypeBool(state), buffer.format,
ConstantU32(state, 0));
const auto base = Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), buffer.address,
ConstantDeviceAddress(state, ~uint64_t {3}));
const auto base = Binary(state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
ConstantU64(state, ~uint64_t {3}));
std::array<uint32_t, 4> values {};
for (uint32_t component = 0; component < components; ++component) {
const auto address = component == 0u ? base : Binary(
state, spv::OpIAdd, TypeScalarU64(state), base,
ConstantDeviceAddress(state, component * 4u));
const auto address = component == 0u ? base
: Binary(state, spv::OpIAdd, TypeU64(state), base,
ConstantU64(state, component * 4u));
values[component] = EmitValueOrZeroIfCondition(
state, AndCondition(state, valid_format,
IndirectBufferInBounds(state, buffer, component * 4u, 4u, false)),
@@ -946,13 +974,14 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
TypeU32(state), constant, [&]() {
const auto in_bounds = IndirectBufferInBounds(state, buffer, 0u, info.byte_size, true);
const auto base = Binary(
state, spv::OpBitwiseAnd, TypeScalarU64(state), buffer.address,
ConstantDeviceAddress(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
ConstantU64(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
const auto load = [&](uint32_t component, uint32_t bits) {
const auto offset = Format::GetFormatComponentByteOffset(info, component);
const auto address = offset == 0u ? base : Binary(
state, spv::OpIAdd, TypeScalarU64(state), base,
ConstantDeviceAddress(state, offset));
const auto address = offset == 0u
? base
: Binary(state, spv::OpIAdd, TypeU64(state),
base, ConstantU64(state, offset));
return LoadBda(ctx, address, in_bounds, bits);
};
const auto emit_component = [&](uint32_t component) {
@@ -1072,23 +1101,9 @@ void StoreWideShared(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compo
} // namespace
uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value) {
return state.builder.Constant(spv::OpConstant, TypeScalarU64(state),
static_cast<uint32_t>(value),
static_cast<uint32_t>(value >> 32u));
}
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high) {
const auto low64 = Unary(state, spv::OpUConvert, TypeScalarU64(state), low);
const auto high64 = Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state),
Unary(state, spv::OpUConvert, TypeScalarU64(state), high),
ConstantDeviceAddress(state, 32));
return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64);
}
uint32_t GetBdaPointer(EmitterState& state, uint32_t address) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), result,
state.builder.AddFunction(spv::OpFunctionCall, TypeU64(state), result,
state.bda_pointer_function, address);
return result;
}
@@ -1097,7 +1112,7 @@ void DefineGetBdaPointer(EmitterState& state) {
if (!state.program.info.uses_dma) {
return;
}
const auto type = TypeScalarU64(state);
const auto type = TypeU64(state);
const auto function_type = state.builder.Type(spv::OpTypeFunction, type, type);
state.bda_pointer_function = state.builder.AllocateId();
const auto address = state.builder.AllocateId();
@@ -1109,14 +1124,14 @@ void DefineGetBdaPointer(EmitterState& state) {
EmitLabel(state, entry_label);
const auto extended = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), address,
ConstantDeviceAddress(state, LibKernel::Memory::kExtendedMemoryBase));
const auto packed = Select(state, type, extended,
Binary(state, spv::OpISub, type, address,
ConstantDeviceAddress(state,
LibKernel::Memory::kExtendedMemoryBase - LOWER_ADDRESS_SIZE)),
address);
ConstantU64(state, LibKernel::Memory::kExtendedMemoryBase));
const auto packed = Select(
state, type, extended,
Binary(state, spv::OpISub, type, address,
ConstantU64(state, LibKernel::Memory::kExtendedMemoryBase - LOWER_ADDRESS_SIZE)),
address);
const auto page64 = Binary(state, spv::OpShiftRightLogical, type, packed,
ConstantDeviceAddress(state, BufferCache::CACHING_PAGEBITS));
ConstantU32(state, BufferCache::CACHING_PAGEBITS));
const auto page = Unary(state, spv::OpUConvert, TypeU32(state), page64);
const auto entry_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state, 64),
@@ -1124,8 +1139,7 @@ void DefineGetBdaPointer(EmitterState& state) {
page);
const auto base = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, type, base, entry_pointer);
const auto missing =
Binary(state, spv::OpIEqual, TypeBool(state), base, ConstantDeviceAddress(state, 0));
const auto missing = Binary(state, spv::OpIEqual, TypeBool(state), base, ConstantU64(state, 0));
const auto fault_label = state.builder.AllocateId();
const auto available_label = state.builder.AllocateId();
const auto merge_label = state.builder.AllocateId();
@@ -1138,14 +1152,14 @@ void DefineGetBdaPointer(EmitterState& state) {
EmitLabel(state, available_label);
const auto offset = Binary(state, spv::OpBitwiseAnd, type, address,
ConstantDeviceAddress(state, BufferCache::CACHING_PAGESIZE - 1));
ConstantU64(state, BufferCache::CACHING_PAGESIZE - 1));
const auto available = Binary(state, spv::OpIAdd, type, base, offset);
state.builder.AddFunction(spv::OpBranch, merge_label);
EmitLabel(state, merge_label);
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpPhi, type, result, ConstantDeviceAddress(state, 0),
fault_label, available, available_label);
state.builder.AddFunction(spv::OpPhi, type, result, ConstantU64(state, 0), fault_label,
available, available_label);
state.builder.AddFunction(spv::OpReturnValue, result);
state.builder.AddFunction(spv::OpFunctionEnd);
}
@@ -1203,12 +1217,12 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
semantics = spv::MemorySemanticsUniformMemoryMask;
}
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain,
TypePointer(state, state.lds_storage_class, TypeScalarU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto value = Unary(state, spv::OpBitcast, TypeScalarU64(state), ctx.Arg(inst, 1));
state.builder.AddFunction(
SpirvAtomicOpcode(inst.GetOpcode()), TypeScalarU64(state), state.builder.AllocateId(),
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto value = ctx.Arg(inst, 1);
state.builder.AddFunction(
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
pointer, ConstantU32(state, spv::ScopeWorkgroup),
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
});
@@ -1227,34 +1241,10 @@ uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
void EmitSharedFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto& mem = ctx.Memory(inst);
const bool max_value = inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMax32;
EmitIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
const auto index = DwordIndex(ctx, inst, mem);
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
EmitIfCondition(
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
ctx.state.builder.AddFunction(spv::OpStore, ctx.scratch_u32_variable,
ctx.Arg(inst, 1));
const auto data = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpLoad, TypeU32(ctx.state), data,
ctx.scratch_u32_variable);
AtomicUpdate(
ctx.state, EmitMemoryElementPointer(ctx.state, resource, index),
mem.kind, [&](uint32_t old) {
const auto old_f =
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), old);
const auto compare_f =
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), ctx.Arg(inst, 2));
const auto data_f =
Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), data);
const auto compare = Binary(
ctx.state, max_value ? spv::OpFOrdGreaterThan : spv::OpFOrdLessThan,
TypeBool(ctx.state), max_value ? old_f : compare_f,
max_value ? compare_f : old_f);
return Unary(ctx.state, spv::OpBitcast, TypeU32(ctx.state),
Select(ctx.state, TypeF32(ctx.state), compare, data_f, old_f));
});
});
});
EmitAtomicUpdate(ctx, inst, mem,
[max_value](EmitterState& state, uint32_t old, uint32_t value) {
return EmitDsFloatAtomicReplacement(state, old, value, max_value);
});
}
uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -1386,7 +1376,7 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
else if (address_info.access == IR::AddressAccess::Read &&
mem.kind != IR::ResourceKind::Scratch)
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
address_info.data_bits);
address_info.data_bits, mem.coherent);
else if (op == IR::ValueOpcode::LoadBufferU32 && mem.formatted)
value = mem.kind == IR::ResourceKind::IndirectBuffer ? LoadIndirectFormattedX(ctx, inst)
: FormattedLoad(ctx, inst, mem);
@@ -44,6 +44,16 @@ uint32_t EmitIndirectResourceIndex(EmitterState& state, uint32_t key, uint32_t m
};
const auto mapping = ConstantU32(state, mapping_offset);
const auto count = LoadMapping(mapping);
if (search_iterations == 0u) {
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), key,
ConstantU32(state, 2u));
const auto valid = Binary(state, spv::OpULessThan, TypeBool(state), index, count);
const auto safe = Select(state, TypeU32(state), valid, index, ConstantU32(state, 0u));
const auto entry = Binary(state, spv::OpIAdd, TypeU32(state), safe,
ConstantU32(state, mapping_offset + 1u));
return Select(state, TypeU32(state), valid, LoadMapping(entry),
ConstantU32(state, default_resource));
}
const auto entry_at = [&](uint32_t index) {
return Binary(state, spv::OpIAdd, TypeU32(state), mapping,
Binary(state, spv::OpIAdd, TypeU32(state),
@@ -138,7 +148,7 @@ void EnsureLdsStorage(EmitterState& state) {
};
if (state.requirements.shared_int64_atomics) {
state.lds_variable = define(TypeU32(state), 4u);
state.lds_u64_variable = define(TypeScalarU64(state), 8u);
state.lds_u64_variable = define(TypeU64(state), 8u);
state.builder.AddName(state.lds_u64_variable, "lds_qwords");
} else {
state.lds_variable = state.builder.DefineGlobalVariable(
@@ -361,10 +371,12 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
case IR::ValueOpcode::ImageAtomicSwap32:
case IR::ValueOpcode::ImageAtomicSwap64:
case IR::ValueOpcode::BufferAtomicSwap32:
case IR::ValueOpcode::BufferAtomicSwap64:
case IR::ValueOpcode::SharedAtomicSwap32: return spv::OpAtomicExchange;
case IR::ValueOpcode::ImageAtomicIAdd32:
case IR::ValueOpcode::ImageAtomicIAdd64:
case IR::ValueOpcode::BufferAtomicIAdd32:
case IR::ValueOpcode::SharedAtomicIAdd64:
case IR::ValueOpcode::SharedAtomicIAdd32: return spv::OpAtomicIAdd;
@@ -374,6 +386,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::BufferAtomicSMin32:
case IR::ValueOpcode::SharedAtomicSMin32: return spv::OpAtomicSMin;
case IR::ValueOpcode::ImageAtomicUMin32:
case IR::ValueOpcode::ImageAtomicUMin64:
case IR::ValueOpcode::BufferAtomicUMin32:
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
case IR::ValueOpcode::ImageAtomicSMax32:
@@ -384,15 +397,18 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::BufferAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
case IR::ValueOpcode::ImageAtomicAnd32:
case IR::ValueOpcode::ImageAtomicAnd64:
case IR::ValueOpcode::BufferAtomicAnd32:
case IR::ValueOpcode::BufferAtomicAnd64:
case IR::ValueOpcode::SharedAtomicAnd32: return spv::OpAtomicAnd;
case IR::ValueOpcode::ImageAtomicOr32:
case IR::ValueOpcode::ImageAtomicOr64:
case IR::ValueOpcode::BufferAtomicOr32:
case IR::ValueOpcode::BufferAtomicOr64:
case IR::ValueOpcode::SharedAtomicOr64:
case IR::ValueOpcode::SharedAtomicOr32: return spv::OpAtomicOr;
case IR::ValueOpcode::ImageAtomicXor32:
case IR::ValueOpcode::ImageAtomicXor64:
case IR::ValueOpcode::BufferAtomicXor32:
case IR::ValueOpcode::SharedAtomicXor32: return spv::OpAtomicXor;
default: return spv::OpNop;
@@ -404,7 +420,7 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
const auto old = ctx.state.builder.AllocateId();
const bool wide = inst.GetType() == IR::Type::U64;
const auto type = wide ? TypeScalarU64(ctx.state) : TypeU32(ctx.state);
const auto type = wide ? TypeU64(ctx.state) : TypeU32(ctx.state);
if (opcode == spv::OpAtomicCompareExchange) {
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
@@ -413,13 +429,12 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), desired, comparator);
} else {
auto value = ctx.Arg(inst, inst.NumArgs() - 2);
if (wide) value = Unary(ctx.state, spv::OpBitcast, type, value);
const auto value = ctx.Arg(inst, inst.NumArgs() - 2);
ctx.state.builder.AddFunction(opcode, type, old, pointer,
ConstantU32(ctx.state, scope),
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), value);
}
return wide ? Unary(ctx.state, spv::OpBitcast, TypeU64(ctx.state), old) : old;
return old;
}
void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
@@ -438,6 +453,20 @@ void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory));
}
namespace {
uint32_t FloatOrderKey(EmitterState& state, uint32_t bits) {
const auto negative = EmitCompareU32Constant(state, spv::OpINotEqual,
EmitAndConstant(state, bits, 0x80000000u), 0u);
const auto negative_key = state.builder.AllocateId();
state.builder.AddFunction(spv::OpNot, TypeU32(state), negative_key, bits);
const auto positive_key =
EmitBinaryU32(state, spv::OpBitwiseXor, bits, ConstantU32(state, 0x80000000u));
return EmitSelectValueU32(state, negative, negative_key, positive_key);
}
} // namespace
uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
bool max_value) {
struct OrderedBits {
@@ -447,15 +476,7 @@ uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t
};
const auto classify = [&](uint32_t bits) {
const auto cls = EmitClassifyF32Bits(state, bits);
const auto negative = EmitCompareU32Constant(state, spv::OpINotEqual,
EmitAndConstant(state, bits, 0x80000000u), 0u);
const auto negative_key = state.builder.AllocateId();
state.builder.AddFunction(spv::OpNot, TypeU32(state), negative_key, bits);
const auto positive_key =
EmitBinaryU32(state, spv::OpBitwiseXor, bits, ConstantU32(state, 0x80000000u));
return OrderedBits {
cls.nan, cls.zero,
EmitSelectValueU32(state, negative, negative_key, positive_key)};
return OrderedBits {cls.nan, cls.zero, FloatOrderKey(state, bits)};
};
const auto source_class = classify(source);
const auto old_class = classify(old);
@@ -470,6 +491,45 @@ uint32_t EmitFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t
old);
}
uint32_t EmitDsFloatAtomicReplacement(EmitterState& state, uint32_t old, uint32_t source,
bool max_value) {
struct OrderedBits {
uint32_t nan;
uint32_t signaling_nan;
uint32_t key;
};
const auto classify = [&](uint32_t bits) {
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
const auto nan = EmitCompareU32Constant(state, spv::OpUGreaterThan, abs, 0x7f800000u);
const auto signaling_nan = EmitLogicalAndBool(
state, nan, EmitCompareU32Constant(state, spv::OpIEqual,
EmitAndConstant(state, bits, 0x00400000u), 0u));
const auto small = EmitCompareU32Constant(state, spv::OpULessThan, abs, 0x00800000u);
const auto sign = EmitAndConstant(state, bits, 0x80000000u);
const auto positive_denorm = EmitLogicalAndBool(
state, EmitCompareU32Constant(state, spv::OpIEqual, sign, 0u),
EmitCompareU32Constant(state, spv::OpINotEqual, abs, 0u));
// Negative denorms tie with -0; positive denorms tie only with each other.
const auto small_bits = EmitSelectValueU32(
state, positive_denorm, ConstantU32(state, 1u), sign);
const auto key_bits = EmitSelectValueU32(state, small, small_bits, bits);
return OrderedBits {nan, signaling_nan, FloatOrderKey(state, key_bits)};
};
const auto lhs = classify(old);
const auto rhs = classify(source);
const auto choose_source = state.builder.AllocateId();
state.builder.AddFunction(max_value ? spv::OpUGreaterThan : spv::OpULessThanEqual,
TypeBool(state), choose_source, rhs.key, lhs.key);
// Keep original bits: ordering denorms does not flush the selected operand.
auto result = EmitSelectValueU32(state, choose_source, source, old);
result = EmitSelectValueU32(state, lhs.nan, source, result);
result = EmitSelectValueU32(state, rhs.nan, old, result);
result = EmitSelectValueU32(state, rhs.signaling_nan,
EmitOrU32(state, source, ConstantU32(state, 0x00400000u)), result);
return EmitSelectValueU32(state, lhs.signaling_nan,
EmitOrU32(state, old, ConstantU32(state, 0x00400000u)), result);
}
uint32_t EmitDsSwizzleTargetLane(EmitterState& state, uint32_t subid, uint32_t control) {
if ((control & 0xc000u) == 0xc000u) {
const uint32_t mask = control & 0x1fu;
@@ -35,14 +35,11 @@ uint32_t TypeU32(EmitterState& state) {
}
uint32_t TypeU64(EmitterState& state) {
return TypeU32Vector(state, 2);
}
uint32_t TypeScalarU64(EmitterState& state) {
if (state.native_u64_type == 0) {
state.native_u64_type = state.builder.Type(spv::OpTypeInt, 64, 0);
if (state.u64_type == 0) {
state.builder.RequireCapability(spv::CapabilityInt64);
state.u64_type = state.builder.Type(spv::OpTypeInt, 64, 0);
}
return state.native_u64_type;
return state.u64_type;
}
uint32_t TypeU32Pair(EmitterState& state) {
@@ -124,8 +121,9 @@ uint32_t TypeFunction(EmitterState& state) {
}
uint32_t TypeStorageBufferElement(EmitterState& state, uint32_t bits) {
return bits == 32u ? TypeU32(state) : bits == 64u ? TypeScalarU64(state)
: state.builder.Type(spv::OpTypeInt, bits, 0);
return bits == 32u ? TypeU32(state)
: bits == 64u ? TypeU64(state)
: state.builder.Type(spv::OpTypeInt, bits, 0);
}
uint32_t StorageRuntimeArrayType(EmitterState& state, uint32_t bits) {
@@ -331,9 +329,8 @@ uint32_t ConstantBool(EmitterState& state, bool value) {
}
uint32_t ConstantU64(EmitterState& state, uint64_t value) {
return state.builder.Constant(spv::OpConstantComposite, TypeU64(state),
ConstantU32(state, static_cast<uint32_t>(value)),
ConstantU32(state, static_cast<uint32_t>(value >> 32u)));
return state.builder.Constant(spv::OpConstant, TypeU64(state), static_cast<uint32_t>(value),
static_cast<uint32_t>(value >> 32u));
}
uint32_t ConstantU32CompositeZero(EmitterState& state, uint32_t components) {
@@ -689,9 +689,7 @@ void EmitProgram(EmitterState& state) {
DefineBvhIntersect(state);
for (const auto* block: program.blocks) {
if (std::ranges::any_of(*block, [](const IR::Inst& inst) {
return inst.GetOpcode() == IR::ValueOpcode::SwizzleU32 ||
inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMin32 ||
inst.GetOpcode() == IR::ValueOpcode::SharedAtomicFMax32;
return inst.GetOpcode() == IR::ValueOpcode::SwizzleU32;
})) {
ctx.scratch_u32_variable = state.builder.AllocateId();
if (state.lane_count == 2) {
@@ -20,6 +20,12 @@ struct MimgGatherInfo {
uint32_t flags = 0;
};
struct MimgAtomicInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::UNSUPPORTED;
bool supports_64 = false;
};
constexpr ImageDimension DecodeImageDimension(uint32_t dim) {
switch (dim) {
case 0u: return ImageDimension::Dim1D;
@@ -190,17 +196,17 @@ constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
};
constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
{0x0fu, Opcode::IMAGE_ATOMIC_SWAP},
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
{0x0fu, Opcode::IMAGE_ATOMIC_SWAP, true},
{0x10u, Opcode::IMAGE_ATOMIC_CMPSWAP},
{0x11u, Opcode::IMAGE_ATOMIC_ADD},
{0x11u, Opcode::IMAGE_ATOMIC_ADD, true},
{0x14u, Opcode::IMAGE_ATOMIC_SMIN},
{0x15u, Opcode::IMAGE_ATOMIC_UMIN},
{0x15u, Opcode::IMAGE_ATOMIC_UMIN, true},
{0x16u, Opcode::IMAGE_ATOMIC_SMAX},
{0x17u, Opcode::IMAGE_ATOMIC_UMAX},
{0x18u, Opcode::IMAGE_ATOMIC_AND},
{0x19u, Opcode::IMAGE_ATOMIC_OR},
{0x1au, Opcode::IMAGE_ATOMIC_XOR},
{0x17u, Opcode::IMAGE_ATOMIC_UMAX, true},
{0x18u, Opcode::IMAGE_ATOMIC_AND, true},
{0x19u, Opcode::IMAGE_ATOMIC_OR, true},
{0x1au, Opcode::IMAGE_ATOMIC_XOR, true},
{0x1eu, Opcode::IMAGE_ATOMIC_FMIN},
{0x1fu, Opcode::IMAGE_ATOMIC_FMAX},
};
@@ -210,7 +216,7 @@ constexpr auto MIMG_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCO
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_OPCODE_LIST);
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgGatherInfo* gather,
const Detail::OpcodeMap* atomic) {
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return Opcode::IMAGE_SAMPLE;
}
@@ -246,7 +252,7 @@ uint32_t DecodeMimgSampleFlags(const MimgSampleInfo* sample, const MimgGatherInf
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
const Detail::OpcodeMap* atomic) {
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return ImageSampleAddressComponents(sample->flags, dimension);
}
@@ -370,7 +376,7 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
SetUnsupported(inst, Family::MIMG, opcode, "MIMG image atomic has invalid DMASK");
} else if (inst.dmask == mask64) {
inst.data_bits = 64u;
if (inst.opcode != Opcode::IMAGE_ATOMIC_UMAX) {
if (!atomic->supports_64) {
SetUnsupported(inst, Family::MIMG, opcode,
"MIMG 64-bit image atomic opcode is not implemented");
}
@@ -84,6 +84,7 @@ constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
};
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
@@ -205,9 +206,9 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_MSKOR_B32:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32: return 3u;
case Opcode::DS_MAX_F32:
case Opcode::DS_PERMUTE_B32:
case Opcode::DS_BPERMUTE_B32: return 2u;
case Opcode::DS_READ_ADDTID_B32:
@@ -342,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;
@@ -354,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;
@@ -363,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;
}
@@ -373,6 +374,10 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
// D16 loads reuse partial destinations to preserve the untouched high half.
inst.dst.sdwa_sel = 4u;
}
DecodeVectorGpr(addr, inst.src0);
inst.src_count = 1;
if (seg == 0u || saddr == 0x7du || saddr == 0x7fu) {
@@ -53,6 +53,7 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
{0x16u, Opcode::S_FLBIT_I32_B64},
{0x1au, Opcode::S_SEXT_I32_I16},
{0x1bu, Opcode::S_BITSET0_B32},
{0x1cu, Opcode::S_BITSET0_B64},
{0x1du, Opcode::S_BITSET1_B32},
@@ -503,6 +503,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.dst).c_str(),
OperandToString(inst.src0).c_str()));
case Opcode::S_ABS_I32:
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
case Opcode::S_BCNT1_I32_B32:
@@ -663,6 +664,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::FLAT_LOAD_SBYTE:
case Opcode::FLAT_LOAD_USHORT:
case Opcode::FLAT_LOAD_SSHORT:
case Opcode::FLAT_LOAD_SHORT_D16:
case Opcode::FLAT_LOAD_DWORD:
case Opcode::FLAT_LOAD_DWORDX2:
case Opcode::FLAT_LOAD_DWORDX3:
@@ -45,6 +45,7 @@ enum class Opcode {
S_CMOV_B64,
S_MOVK_I32,
S_ABS_I32,
S_SEXT_I32_I16,
S_ABSDIFF_I32,
S_BREV_B32,
S_BREV_B64,
@@ -224,6 +225,9 @@ enum class Opcode {
V_MAC_F32,
V_MADMK_F32,
V_MADAK_F32,
V_FMAC_F32,
V_FMAMK_F32,
V_FMAAK_F32,
V_MIN_F32,
V_MAX_F32,
V_ADD_F16,
@@ -394,6 +398,7 @@ enum class Opcode {
V_CMP_T_I32,
V_CMP_CLASS_F32,
V_CMPX_CLASS_F32,
V_CMPX_CLASS_F16,
V_CMP_LT_I16,
V_CMP_EQ_I16,
V_CMP_LE_I16,
@@ -438,6 +443,7 @@ enum class Opcode {
V_CMPX_LT_U16,
V_CMPX_EQ_U16,
V_CMPX_GT_U16,
V_CMPX_NE_U16,
V_CMP_NE_U16,
V_CMP_GE_U16,
V_CMP_F_U32,
@@ -529,6 +535,7 @@ enum class Opcode {
FLAT_LOAD_SBYTE,
FLAT_LOAD_USHORT,
FLAT_LOAD_SSHORT,
FLAT_LOAD_SHORT_D16,
FLAT_LOAD_DWORD,
FLAT_LOAD_DWORDX2,
FLAT_LOAD_DWORDX3,
@@ -35,7 +35,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x04u, Opcode::V_SUB_F32, Vop2SdwaProfile::Float32},
{0x05u, Opcode::V_SUBREV_F32},
{0x08u, Opcode::V_MUL_F32, Vop2SdwaProfile::Float32},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerFullDestination},
{0x09u, Opcode::V_MUL_I32_I24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerFullDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
@@ -65,9 +65,9 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x2au, Opcode::V_SUBREV_CO_CI_U32},
{0x26u, Opcode::V_SUB_NC_U32, Vop2SdwaProfile::IntegerPartialDestination},
{0x27u, Opcode::V_SUBREV_NC_U32, Vop2SdwaProfile::IntegerFullDestination},
{0x2bu, Opcode::V_MAC_F32},
{0x2cu, Opcode::V_MADMK_F32},
{0x2du, Opcode::V_MADAK_F32},
{0x2bu, Opcode::V_FMAC_F32},
{0x2cu, Opcode::V_FMAMK_F32},
{0x2du, Opcode::V_FMAAK_F32},
{0x2fu, Opcode::V_CVT_PKRTZ_F16_F32, Vop2SdwaProfile::PackedFloat16},
{0x32u, Opcode::V_ADD_F16, Vop2SdwaProfile::Float16},
{0x33u, Opcode::V_SUB_F16, Vop2SdwaProfile::Float16},
@@ -252,11 +252,13 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x99u, Opcode::V_CMPX_LT_I16, false}, {0x9au, Opcode::V_CMPX_EQ_I16, false},
{0x9bu, Opcode::V_CMPX_LE_I16, false}, {0x9cu, Opcode::V_CMPX_GT_I16, false},
{0x9du, Opcode::V_CMPX_NE_I16, false}, {0x9eu, Opcode::V_CMPX_GE_I16, false},
{0x9fu, Opcode::V_CMPX_CLASS_F16, false},
{0xa9u, Opcode::V_CMP_LT_U16}, {0xaau, Opcode::V_CMP_EQ_U16},
{0xabu, Opcode::V_CMP_LE_U16}, {0xacu, Opcode::V_CMP_GT_U16},
{0xadu, Opcode::V_CMP_NE_U16}, {0xaeu, Opcode::V_CMP_GE_U16},
{0xb9u, Opcode::V_CMPX_LT_U16, false}, {0xbau, Opcode::V_CMPX_EQ_U16, false},
{0xbcu, Opcode::V_CMPX_GT_U16}, {0xc0u, Opcode::V_CMP_F_U32},
{0xbcu, Opcode::V_CMPX_GT_U16}, {0xbdu, Opcode::V_CMPX_NE_U16, false},
{0xc0u, Opcode::V_CMP_F_U32},
{0xc1u, Opcode::V_CMP_LT_U32}, {0xc2u, Opcode::V_CMP_EQ_U32},
{0xc3u, Opcode::V_CMP_LE_U32}, {0xc4u, Opcode::V_CMP_GT_U32},
{0xc5u, Opcode::V_CMP_NE_U32}, {0xc6u, Opcode::V_CMP_GE_U32},
@@ -800,6 +802,9 @@ bool IsVop2FloatOpcode(Opcode opcode) {
case Opcode::V_MAC_F32:
case Opcode::V_MADMK_F32:
case Opcode::V_MADAK_F32:
case Opcode::V_FMAC_F32:
case Opcode::V_FMAMK_F32:
case Opcode::V_FMAAK_F32:
case Opcode::V_CVT_PKRTZ_F16_F32:
case Opcode::V_ADD_F16:
case Opcode::V_SUB_F16:
@@ -913,6 +918,7 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMPX_NEQ_F16:
case Opcode::V_CMPX_NLT_F16:
case Opcode::V_CMP_CLASS_F32:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMPX_CLASS_F32: return true;
default: return false;
}
@@ -1057,6 +1063,7 @@ void FinalizeVop2Instruction(std::span<const uint32_t> code, uint32_t word_index
Instruction& inst) {
switch (inst.opcode) {
case Opcode::V_MADMK_F32:
case Opcode::V_FMAMK_F32:
case Opcode::V_FMAMK_F16:
inst.src2 = inst.src1;
inst.src1 = {};
@@ -1064,6 +1071,7 @@ void FinalizeVop2Instruction(std::span<const uint32_t> code, uint32_t word_index
inst.src_count = 3;
break;
case Opcode::V_MADAK_F32:
case Opcode::V_FMAAK_F32:
case Opcode::V_FMAAK_F16:
inst.src2 = {};
inst.src2.kind = OperandKind::LiteralConstant;
@@ -1401,6 +1409,7 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
case Opcode::V_MIN_F32:
case Opcode::V_MAX_F32:
case Opcode::V_MAC_F32:
case Opcode::V_FMAC_F32:
case Opcode::V_MAD_F32:
case Opcode::V_ADD_F64:
case Opcode::V_MUL_F64:
@@ -1436,6 +1445,7 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
case Opcode::V_MIN_F32:
case Opcode::V_MAX_F32:
case Opcode::V_MAC_F32:
case Opcode::V_FMAC_F32:
case Opcode::V_MAD_F32:
case Opcode::V_FMA_F32:
case Opcode::V_FMA_F16:
@@ -1532,6 +1542,7 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_NE_I32:
case Opcode::V_CMPX_GE_I32:
case Opcode::V_CMPX_CLASS_F32:
case Opcode::V_CMPX_CLASS_F16:
case Opcode::V_CMPX_LT_U32:
case Opcode::V_CMPX_EQ_U32:
case Opcode::V_CMPX_LE_U32:
@@ -1550,6 +1561,7 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_LT_U16:
case Opcode::V_CMPX_EQ_U16:
case Opcode::V_CMPX_GT_U16:
case Opcode::V_CMPX_NE_U16:
case Opcode::V_CMPX_LT_F16:
case Opcode::V_CMPX_EQ_F16:
case Opcode::V_CMPX_LE_F16:
@@ -72,11 +72,13 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
}
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx) {
const auto value = ReadOperand(inst.src0, IR::Type::F32);
const auto mask = ReadOperand(inst.src1, IR::Type::U32);
EmitCompareResult(inst, IR::U1(ir.Emit(IR::ValueOpcode::FPCmpClass32, {value, mask})), false,
cmpx);
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
: ReadOperand(inst.src0, IR::Type::F32);
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
: ReadOperand(inst.src1, IR::Type::U32);
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -248,6 +248,7 @@ void Translator::S_CSELECT_B32(const Decoder::Instruction& inst) {
void Translator::ScalarSelect64(const Decoder::Instruction& inst,
const Decoder::Operand& false_source) {
// Preserve per-word expressions for descriptor tracking and mask provenance.
const auto condition = ir.GetScc();
const auto lhs = ReadU32Pair(inst.src0);
const auto rhs = ReadU32Pair(false_source);
@@ -8,13 +8,13 @@ namespace Libs::Graphics::ShaderRecompiler::Frontend {
void Translator::PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool accumulator, bool quiet_snan) {
const auto translate_lane = [&](bool high) {
const auto lhs = ReadF16LaneAsF32(inst.src0, high, true);
const auto rhs = ReadF16LaneAsF32(inst.src1, high, true);
const auto lhs = ReadF16LaneAsF32(inst.src0, high);
const auto rhs = ReadF16LaneAsF32(inst.src1, high);
IR::F32 result;
if (accumulator) {
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.dst, high, true)}));
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.dst, high)}));
} else if (inst.src_count == 3u) {
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.src2, high, true)}));
result = IR::F32(ir.Emit(opcode, {lhs, rhs, ReadF16LaneAsF32(inst.src2, high)}));
} else {
result = IR::F32(ir.Emit(opcode, {lhs, rhs}));
}
@@ -114,6 +114,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
return;
}
// Descriptor tracking reasons about each scalar word independently.
const auto lhs = ReadU32Pair(inst.src0);
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
@@ -269,22 +270,18 @@ void Translator::Integer24(const Decoder::Instruction& inst, bool sign, bool add
}
void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
const auto lhs = ReadU32(inst.src0);
const auto rhs = ReadU32(inst.src1);
const auto add = ExtractU64(ReadU64(inst.src2));
const auto mul_low = ir.IMul(lhs, rhs);
const auto mul_high = IR::U32(ir.Emit(IR::ValueOpcode::UMulHi, {lhs, rhs}));
const auto low = ir.IAdd(mul_low, add[0]);
const auto carry_low = ir.ULessThan(low, mul_low);
const auto high0 = ir.IAdd(mul_high, add[1]);
const auto carry0 = ir.ULessThan(high0, mul_high);
const auto high =
ir.IAdd(high0, ir.Select(carry_low, IR::U32(IR::Value(1u)), IR::U32(IR::Value(0u))));
const auto carry1 = ir.ULessThan(high, high0);
WriteOperand(DestinationOperand(inst), ir.ConstructU64(low, high));
const auto lhs = ReadU32(inst.src0);
const auto rhs = ReadU32(inst.src1);
// Keep the 32x32 product in words: native widening IMul64 fails NVIDIA compilation
// in large vertex shaders (Astro's Playroom). The 64-bit addition stays native.
const auto product = ir.ConstructU64(
ir.IMul(lhs, rhs), IR::U32(ir.Emit(IR::ValueOpcode::UMulHi, {lhs, rhs})));
const auto result = ir.Emit(IR::ValueOpcode::IAdd64, {product, ReadU64(inst.src2)});
WriteOperand(DestinationOperand(inst), result);
if (inst.dst2.kind != Decoder::OperandKind::Null &&
inst.dst2.kind != Decoder::OperandKind::Unknown) {
WriteMask(inst.dst2, ir.LogicalOr(carry0, carry1));
// The 32x32 product fits in 64 bits, so only the addition can carry.
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
}
}
@@ -407,9 +404,12 @@ IR::U64 Translator::RightMask64(IR::U32 count) {
}
void Translator::S_BFM_B64(const Decoder::Instruction& inst) {
const auto count = ir.BitwiseAnd(ReadU32(inst.src0), IR::U32(IR::Value(63u)));
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(63u)));
const auto result = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {RightMask64(count), offset});
const auto count = ReadU32(inst.src0);
const auto offset = ReadU32(inst.src1);
const auto one = IR::Value(uint64_t {1});
const auto limit = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {one, count});
const auto mask = ir.Emit(IR::ValueOpcode::ISub64, {limit, one});
const auto result = ir.Emit(IR::ValueOpcode::ShiftLeftLogical64, {mask, offset});
WriteOperand(DestinationOperand(inst), result);
}
@@ -129,8 +129,9 @@ IR::MemoryInfo MemoryInfoFromDecoded(const Decoder::Instruction& decoded) {
// Vector loads use GLC/DLC to bypass L0/GL1; atomics use GLC only to return data.
const bool buffer_atomic = decoded.opcode >= Decoder::Opcode::BUFFER_ATOMIC_SWAP &&
decoded.opcode <= Decoder::Opcode::BUFFER_ATOMIC_FMAX;
memory.coherent = memory.kind == ResourceKind::Buffer && !buffer_atomic &&
(decoded.glc || decoded.dlc);
memory.coherent = (memory.kind == ResourceKind::Buffer || memory.kind == ResourceKind::Flat ||
memory.kind == ResourceKind::Global) &&
!buffer_atomic && (decoded.glc || decoded.dlc);
memory.resource = ResourceIndexFromOperand(decoded.src1);
memory.sampler = ResourceIndexFromOperand(decoded.src2);
if (memory.kind == ResourceKind::ScalarBuffer) {
@@ -546,7 +547,10 @@ void Translator::BUFFER_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode
}
}
void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode32,
IR::ValueOpcode opcode64) {
const auto opcode = inst.data_bits == 64u ? opcode64 : opcode32;
EXIT_IF(opcode == IR::ValueOpcode::Count);
const auto memory = MemoryInfoFromDecoded(inst);
const auto resource = GetImageResource(memory);
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 1));
@@ -1041,24 +1045,30 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::IMAGE_ATOMIC_CMPSWAP:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicCompareSwap32);
case Decoder::Opcode::IMAGE_ATOMIC_SWAP:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSwap32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSwap32,
IR::ValueOpcode::ImageAtomicSwap64);
case Decoder::Opcode::IMAGE_ATOMIC_ADD:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicIAdd32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicIAdd32,
IR::ValueOpcode::ImageAtomicIAdd64);
case Decoder::Opcode::IMAGE_ATOMIC_SMIN:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMin32);
case Decoder::Opcode::IMAGE_ATOMIC_UMIN:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMin32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMin32,
IR::ValueOpcode::ImageAtomicUMin64);
case Decoder::Opcode::IMAGE_ATOMIC_SMAX:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMax32);
case Decoder::Opcode::IMAGE_ATOMIC_UMAX:
return IMAGE_ATOMIC(inst, inst.data_bits == 64u ? IR::ValueOpcode::ImageAtomicUMax64
: IR::ValueOpcode::ImageAtomicUMax32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMax32,
IR::ValueOpcode::ImageAtomicUMax64);
case Decoder::Opcode::IMAGE_ATOMIC_AND:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicAnd32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicAnd32,
IR::ValueOpcode::ImageAtomicAnd64);
case Decoder::Opcode::IMAGE_ATOMIC_OR:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32,
IR::ValueOpcode::ImageAtomicOr64);
case Decoder::Opcode::IMAGE_ATOMIC_XOR:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32);
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32,
IR::ValueOpcode::ImageAtomicXor64);
case Decoder::Opcode::IMAGE_ATOMIC_FMIN:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32);
case Decoder::Opcode::IMAGE_ATOMIC_FMAX:
@@ -1068,6 +1078,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::FLAT_LOAD_SBYTE:
case Decoder::Opcode::FLAT_LOAD_USHORT:
case Decoder::Opcode::FLAT_LOAD_SSHORT:
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
case Decoder::Opcode::FLAT_LOAD_DWORD:
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
@@ -134,6 +134,10 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_ABS_I32:
return SimpleInteger(inst, IR::ValueOpcode::IAbs32, IR::Type::U32, false, false, true);
case O::S_SEXT_I32_I16:
WriteOperand(inst.dst, IR::U32(ir.Emit(IR::ValueOpcode::BitFieldSExtract,
{ReadU32(inst.src0), IR::Value(0u), IR::Value(16u)})));
return;
case O::S_MUL_I32:
case O::S_MULK_I32:
return SimpleInteger(inst, IR::ValueOpcode::IMul32, IR::Type::U32, false, false, false);
@@ -165,9 +169,16 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_BCNT1_I32_B32:
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
true);
case O::S_BCNT1_I32_B64:
return SimpleInteger(inst, IR::ValueOpcode::BitCount64, IR::Type::U64, false, false,
true);
case O::S_BCNT1_I32_B64: {
// Vulkan bit counts operate on 32-bit words; avoid packing only to split again.
const auto value = ReadU32Pair(inst.src0);
const auto low = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[0]}));
const auto high = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[1]}));
const auto result = ir.IAdd(low, high);
WriteOperand(inst.dst, result);
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
return;
}
case O::S_FF1_I32_B32:
return SimpleInteger(inst, IR::ValueOpcode::FindILsb32, IR::Type::U32, false, false,
false);
@@ -503,41 +503,11 @@ IR::U64 Translator::ReadU64(const Decoder::Operand& operand) {
return IR::U64(ReadOperand(operand, IR::Type::U64));
}
IR::F32 Translator::ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane, bool packed) {
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const bool use_zero = packed && (high_lane ? operand.op_sel_hi : operand.op_sel);
auto value = use_zero ? IR::F32(IR::Value::F32(0.0f))
: ir.BitCastF32(IR::U32(IR::Value(operand.value)));
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {value}));
value = IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
if (operand.absolute) {
value = IR::F32(ir.Emit(IR::ValueOpcode::FPAbs32, {value}));
}
if (high_lane ? operand.negate_hi : operand.negate) {
value = IR::F32(ir.Emit(IR::ValueOpcode::FPNeg32, {value}));
}
return value;
}
auto raw_operand = operand;
raw_operand.sdwa_sel = 6;
raw_operand.sdwa_sext = false;
const auto bits = ApplyBitSourceModifiers(raw_operand, ReadRawU32(operand));
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
offset = operand.sdwa_sel == 5u ? 16u : 0u;
}
const auto half_u32 = IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldUExtract, {bits, IR::Value(offset), IR::Value(16u)}));
const auto half_u16 = IR::U16(ir.Emit(IR::ValueOpcode::ConvertU16U32, {half_u32}));
IR::F32 Translator::ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane) {
const auto bits = Read16LaneBits(operand, high_lane);
const auto half_u16 = IR::U16(ir.Emit(IR::ValueOpcode::ConvertU16U32, {bits}));
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::BitCastF16U16, {half_u16}));
auto value = IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
if (operand.absolute) {
value = IR::F32(ir.Emit(IR::ValueOpcode::FPAbs32, {value}));
}
if (high_lane ? operand.negate_hi : operand.negate) {
value = IR::F32(ir.Emit(IR::ValueOpcode::FPNeg32, {value}));
}
return value;
return IR::F32(ir.Emit(IR::ValueOpcode::ConvertF32F16, {half}));
}
IR::F32 Translator::ReadF16AsF32(const Decoder::Operand& operand) {
@@ -555,29 +525,6 @@ IR::F32 Translator::ReadMixF32(const Decoder::Operand& operand) {
return IR::F32(ReadOperand(value_operand, IR::Type::F32));
}
IR::U32 Translator::ReadU16LaneRaw(const Decoder::Operand& operand, bool high_lane) {
auto raw_operand = operand;
raw_operand.sdwa_sel = 6;
raw_operand.sdwa_sext = false;
const auto bits = ApplyBitSourceModifiers(raw_operand, ReadRawU32(operand));
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
uint32_t width = 16u;
if (operand.sdwa_sel <= 3u) {
offset = operand.sdwa_sel * 8u;
width = 8u;
} else if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
offset = operand.sdwa_sel == 5u ? 16u : 0u;
}
if (width == 8u && operand.sdwa_sext) {
// Extend the selected byte into the 16-bit operand before halfword modifiers.
const auto value = IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldSExtract, {bits, IR::Value(offset), IR::Value(width)}));
return ir.BitwiseAnd(value, IR::U32(IR::Value(0xffffu)));
}
return IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldUExtract, {bits, IR::Value(offset), IR::Value(width)}));
}
IR::U32 Translator::ReadU16LaneAsU32(const Decoder::Operand& operand, bool high_lane,
bool sign_extend) {
auto value = Read16LaneBits(operand, high_lane);
@@ -593,12 +540,36 @@ IR::U32 Translator::ReadU16AsU32(const Decoder::Operand& operand, bool sign_exte
}
IR::U32 Translator::Read16LaneBits(const Decoder::Operand& operand, bool high_lane) {
auto value = ReadU16LaneRaw(operand, high_lane);
auto raw_operand = operand;
raw_operand.sdwa_sel = 6;
raw_operand.sdwa_sext = false;
auto bits = ReadRawU32(operand);
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
// Inline floats encode the operand's width before SDWA or OP_SEL selects bits.
const auto half = IR::F16(ir.Emit(IR::ValueOpcode::ConvertF16F32, {ir.BitCastF32(bits)}));
const auto half_bits = IR::U16(ir.Emit(IR::ValueOpcode::BitCastU16F16, {half}));
bits = IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {half_bits}));
}
bits = ApplyBitSourceModifiers(raw_operand, bits);
uint32_t offset = (high_lane ? operand.op_sel_hi : operand.op_sel) ? 16u : 0u;
uint32_t width = 16u;
if (operand.sdwa_sel <= 3u) {
offset = operand.sdwa_sel * 8u;
width = 8u;
} else if (operand.sdwa_sel == 4u || operand.sdwa_sel == 5u) {
offset = operand.sdwa_sel == 5u ? 16u : 0u;
}
const auto extract = width == 8u && operand.sdwa_sext
? IR::ValueOpcode::BitFieldSExtract
: IR::ValueOpcode::BitFieldUExtract;
auto value = IR::U32(ir.Emit(extract, {bits, IR::Value(offset), IR::Value(width)}));
if (width == 8u && operand.sdwa_sext) {
value = ir.BitwiseAnd(value, IR::U32(IR::Value(0xffffu)));
}
if (operand.absolute) {
value = ir.BitwiseAnd(value, IR::U32(IR::Value(0x7fffu)));
}
if (high_lane ? operand.negate_hi : operand.negate) {
// RDNA2 source NEG flips the sign bit, including packed integer operands.
value = ir.BitwiseXor(value, IR::U32(IR::Value(0x8000u)));
}
return value;
@@ -40,10 +40,9 @@ private:
IR::U32 ReadU32(const Decoder::Operand& operand);
std::array<IR::U32, 2> ReadU32Pair(const Decoder::Operand& operand);
IR::U64 ReadU64(const Decoder::Operand& operand);
IR::F32 ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane, bool packed = false);
IR::F32 ReadF16LaneAsF32(const Decoder::Operand& operand, bool high_lane);
IR::F32 ReadF16AsF32(const Decoder::Operand& operand);
IR::F32 ReadMixF32(const Decoder::Operand& operand);
IR::U32 ReadU16LaneRaw(const Decoder::Operand& operand, bool high_lane);
IR::U32 ReadU16LaneAsU32(const Decoder::Operand& operand, bool high_lane, bool sign_extend);
IR::U32 ReadU16AsU32(const Decoder::Operand& operand, bool sign_extend);
IR::U32 Read16LaneBits(const Decoder::Operand& operand, bool high_lane);
@@ -86,7 +85,8 @@ private:
void BUFFER_LOAD(const Decoder::Instruction& inst);
void BUFFER_STORE(const Decoder::Instruction& inst);
void BUFFER_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode32,
IR::ValueOpcode opcode64 = IR::ValueOpcode::Count);
void DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool returns_value);
void FLAT_LOAD(const Decoder::Instruction& inst);
void FLAT_STORE(const Decoder::Instruction& inst);
@@ -126,7 +126,7 @@ private:
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
void V_CVT_F32_I32(const Decoder::Instruction& inst);
@@ -143,6 +143,9 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMP_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
return;
case O::V_CMPX_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
return;
case O::V_CMP_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
return;
@@ -343,6 +346,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
@@ -479,10 +483,13 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
case O::V_LDEXP_F32: return FloatBinary(inst, IR::ValueOpcode::FPLdexp, false);
case O::V_MAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, true, true);
case O::V_MAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPMad32, true, true);
case O::V_MADMK_F32:
case O::V_MADAK_F32:
case O::V_MAD_F32:
case O::V_MAD_F32: return FloatTernary(inst, IR::ValueOpcode::FPMad32, false, true);
case O::V_FMAC_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, true, true);
case O::V_FMAMK_F32:
case O::V_FMAAK_F32:
case O::V_FMA_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, false, true);
case O::V_FMA_F64: return FloatTernary(inst, IR::ValueOpcode::FPFma64, false, false);
case O::V_MIN3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMinTri32, false, false);
+11 -4
View File
@@ -485,14 +485,21 @@ struct BlockInfo {
struct DescriptorSource {
struct IndirectDescriptor {
uint32_t material_source = UINT32_MAX;
struct SelectorRead {
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
bool operator==(const SelectorRead& other) const = default;
};
std::optional<SelectorRead> selector;
uint32_t table_source = 0;
uint32_t selector_stride = 0;
uint32_t selector_offset = 0;
uint32_t table_offset = 0;
uint32_t table_immediate = 0;
uint32_t table_stride = 0;
uint32_t table_record_bytes = 0;
bool table_scalar = false;
uint32_t workgroup_axis = UINT32_MAX;
uint32_t selector_shift = 0;
Value key_count;
Value selector_first;
Value selector_mask;
@@ -224,15 +224,21 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
return {ImageAccess::Write, ImageResourceClass::Storage, false};
case ValueOpcode::ImageAtomicCompareSwap32:
case ValueOpcode::ImageAtomicSwap32:
case ValueOpcode::ImageAtomicSwap64:
case ValueOpcode::ImageAtomicIAdd32:
case ValueOpcode::ImageAtomicIAdd64:
case ValueOpcode::ImageAtomicSMin32:
case ValueOpcode::ImageAtomicUMin32:
case ValueOpcode::ImageAtomicUMin64:
case ValueOpcode::ImageAtomicSMax32:
case ValueOpcode::ImageAtomicUMax32:
case ValueOpcode::ImageAtomicUMax64:
case ValueOpcode::ImageAtomicAnd32:
case ValueOpcode::ImageAtomicAnd64:
case ValueOpcode::ImageAtomicOr32:
case ValueOpcode::ImageAtomicOr64:
case ValueOpcode::ImageAtomicXor32:
case ValueOpcode::ImageAtomicXor64:
case ValueOpcode::ImageAtomicFMin32:
case ValueOpcode::ImageAtomicFMax32:
return {ImageAccess::Atomic, ImageResourceClass::Storage, false};
@@ -175,10 +175,12 @@ VALUE_OPCODE(FPOrdGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPUnordGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPIsNan32, U1, F32)
VALUE_OPCODE(FPCmpClass32, U1, F32, U32)
VALUE_OPCODE(FPCmpClass16, U1, U32, U32)
VALUE_OPCODE(FPAdd32, F32, F32, F32)
VALUE_OPCODE(FPSub32, F32, F32, F32)
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
VALUE_OPCODE(FPMad32, F32, F32, F32, F32)
VALUE_OPCODE(FPMul32, F32, F32, F32)
VALUE_OPCODE(FPAdd64, F64, F64, F64)
VALUE_OPCODE(FPMul64, F64, F64, F64)
@@ -279,8 +281,8 @@ VALUE_OPCODE(WriteSharedU32, Void, U32, U32, U1)
VALUE_OPCODE(WriteSharedU32x2, Void, U32, U32, U32, U1)
VALUE_OPCODE(WriteSharedU32x3, Void, U32, U32, U32, U32, U1)
VALUE_OPCODE(WriteSharedU32x4, Void, U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicFMin32, Void, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicFMax32, Void, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicFMin32, Void, U32, U32, U1)
VALUE_OPCODE(SharedAtomicFMax32, Void, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSwap32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicIAdd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicIAdd64, Void, U32, U64, U1)
@@ -309,15 +311,21 @@ VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicIAdd32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicIAdd64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicSMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMin64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicSMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMax64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicAnd64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicOr64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicXor64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(GetAttribute, U32, U32, U32)
@@ -101,7 +101,7 @@ SharedMemoryResources CollectMemoryResources(const Program& program, std::vector
bool UsesFlattenedSrt(const Program& program) {
const auto uses_mapping = [](const auto& resource) {
return resource.indirect_search_iterations != 0u;
return resource.indirect_root != UINT32_MAX;
};
return std::ranges::any_of(program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
@@ -347,6 +347,17 @@ void FoldInstruction(Block& block, Block::iterator instruction,
if (IsImmediate(low, Type::U32) && IsImmediate(high, Type::U32)) {
Replace(inst, Value(static_cast<uint64_t>(low.U32()) |
(static_cast<uint64_t>(high.U32()) << 32u)));
} else {
const auto* low_extract = low.TryInstruction();
const auto* high_extract = high.TryInstruction();
if (low_extract != nullptr && high_extract != nullptr &&
low_extract->GetOpcode() == ValueOpcode::CompositeExtractU64 &&
high_extract->GetOpcode() == ValueOpcode::CompositeExtractU64 &&
Arg(*low_extract, 1) == Value(0u) && Arg(*high_extract, 1) == Value(1u) &&
Arg(*low_extract, 0) == Arg(*high_extract, 0)) {
// Guest register pairs need not unpack and repack between wide operations.
Replace(inst, Arg(*low_extract, 0));
}
}
return;
}
@@ -238,6 +238,13 @@ bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
return true;
}
bool IsBoundedDescriptorTable(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect) {
const auto* table = Source(program, indirect.table_source);
return indirect.sources.empty() && !indirect.selector &&
indirect.workgroup_axis == UINT32_MAX && table != nullptr && table->dword_count == 4u;
}
template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& indirect,
@@ -249,7 +256,24 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t maximum_resources, Normalize&& normalize) {
const auto descriptor_bytes = dword_count * sizeof(uint32_t);
const auto& sources = indirect.sources;
const auto* selector = indirect.selector ? &*indirect.selector : nullptr;
auto& keys = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
const auto found = std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) return false;
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
return true;
};
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
uint64_t step, uint64_t count) {
keys.resize(count);
@@ -269,8 +293,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.clear();
DescriptorValue material_value;
DescriptorValue table_value;
if ((indirect.material_source != UINT32_MAX &&
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
if ((selector != nullptr &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
}
@@ -283,7 +307,48 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
return false;
}
if (indirect.material_source == UINT32_MAX) {
if (IsBoundedDescriptorTable(program, indirect)) {
if (!indirect.table_scalar && table.Format() == Prospero::BufferFormat::kInvalid) {
descriptors[resource_index] = {.dword_count = dword_count};
return true;
}
if (table_size > UINT32_MAX ||
(indirect.table_record_bytes != 0u &&
(table.Type() != 0u || table.SwizzleEnabled() || table.AddTid() ||
table.OutOfBounds() != 0u || (table_base & 3u) != 0u ||
(table.Stride() & 3u) != 0u || table.Stride() < indirect.table_record_bytes)))
return false;
// Descriptor bytes are refreshed together. The GPU selects the DWORD address;
// no material selector or unused table field is interpreted on the CPU.
keys.resize(table_size / 4u);
if (!ReadScalarTable(table_base, table_size, 0u, runtime, keys)) return false;
const auto mapping_offset = snapshot.flattened_srt.size();
descriptors[resource_index] = {.dword_count = dword_count};
snapshot.flattened_srt.resize(mapping_offset + 2u, 0u);
for (uint32_t word = 0; word < keys.size(); ++word) {
DescriptorValue candidate {.dword_count = dword_count};
std::copy_n(keys.begin() + word, std::min<size_t>(dword_count, keys.size() - word),
candidate.dwords.begin());
if (!normalize(candidate)) return false;
uint32_t ordinal;
if (!intern_candidate(candidate, ordinal)) return false;
if (ordinal == 0u) continue;
snapshot.flattened_srt.resize(mapping_offset + 2u + word, 0u);
snapshot.flattened_srt[mapping_offset + 1u + word] = ordinal;
}
if (descriptors.size() == children_begin) {
snapshot.flattened_srt.resize(mapping_offset);
} else {
snapshot.flattened_srt[mapping_offset] =
static_cast<uint32_t>(snapshot.flattened_srt.size() - mapping_offset - 1u);
auto& root = specializations[resource_index];
root.indirect_root = resource_index;
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
root.indirect_search_iterations = 0u;
}
return true;
}
if (selector == nullptr) {
uint32_t key_count = 0;
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
@@ -295,7 +360,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
}
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(key_count != 0u && uint64_t {indirect.table_offset} +
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
UINT32_MAX + 1ull)) {
return false;
@@ -306,19 +371,18 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
ShaderBufferResource material;
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) || material.Type() != 0u ||
(indirect.selector_shift != 0u && (material.Base48() & 3u) != 0u) ||
material.SwizzleEnabled() ||
material.AddTid() || material.OutOfBounds() != 0u ||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
uint64_t {selector->offset} + 4u > material.Stride() ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes ||
uint64_t {first} + count > material.NumRecords())
return false;
if (count != 0u && (uint64_t {first} + count - 1u) * material.Stride() +
indirect.selector_offset + 4u >
selector->offset + 4u >
uint64_t {UINT32_MAX} + 1u)
return false;
if (!read_keys(material, uint64_t {first} * material.Stride() + indirect.selector_offset,
if (!read_keys(material, uint64_t {first} * material.Stride() + selector->offset,
material.Stride(), count)) return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
@@ -334,8 +398,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
static_cast<uint64_t>(index) * indirect.selector_stride;
const auto offset = static_cast<uint64_t>(selector->offset) +
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
@@ -343,37 +407,14 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
keys.push_back(key);
mask &= mask - 1u;
}
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u) {
return false;
}
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectDescriptorProbes) {
return false;
}
if (!read_keys(material, first, step, probe_count)) return false;
if (indirect.selector_shift == 0u) keys.push_back(0u);
}
if (indirect.material_source != UINT32_MAX) {
if (indirect.selector_shift != 0u) {
for (auto& key: keys) key >>= indirect.selector_shift;
keys.push_back(0u); // An out-of-range material load returns zero.
}
} else return false;
if (selector != nullptr) {
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
}
const auto children_begin = descriptors.size();
const auto mapping_offset = snapshot.flattened_srt.size();
const auto root_resource = specializations[resource_index];
const auto key_count = sources.empty() ? keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
@@ -382,8 +423,9 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
DescriptorValue candidate;
candidate.dword_count = dword_count;
if (sources.empty()) {
const auto table_offset =
static_cast<uint32_t>(key * indirect.table_stride) + indirect.table_offset;
const uint64_t table_offset =
uint64_t {(key * indirect.table_stride + indirect.table_offset) & ~3u} +
(indirect.table_immediate & ~3u);
if (!ReadScalarTable(table_base, table_size, table_offset, runtime,
std::span(candidate.dwords).first(dword_count))) {
return false;
@@ -395,19 +437,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint32_t ordinal = 0;
if (entry == 0) {
descriptors[resource_index] = candidate;
} else if (descriptors[resource_index] != candidate) {
const auto found =
std::find(descriptors.begin() + children_begin, descriptors.end(), candidate);
ordinal = static_cast<uint32_t>(found - descriptors.begin() - children_begin + 1u);
if (found == descriptors.end()) {
if (descriptors.size() >= maximum_resources) {
return false;
}
descriptors.push_back(candidate);
auto child = root_resource;
child.indirect_root = resource_index;
specializations.push_back(child);
}
} else if (!intern_candidate(candidate, ordinal)) {
return false;
}
snapshot.flattened_srt[mapping_offset + 1u + entry * 2u] = key;
snapshot.flattened_srt[mapping_offset + 2u + entry * 2u] = ordinal;
@@ -565,9 +596,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
const auto key_count = root.indirect_mapping_offset < snapshot.flattened_srt.size()
? snapshot.flattened_srt[root.indirect_mapping_offset]
: 0u;
if (root.indirect_search_iterations == 0u || key_count < 2u ||
if (key_count == 0u ||
static_cast<size_t>(root.indirect_mapping_offset) + 1u +
static_cast<size_t>(key_count) * 2u >
static_cast<size_t>(key_count) * (root.indirect_search_iterations == 0u ? 1u : 2u) >
snapshot.flattened_srt.size()) {
return SpecializationFail("indirect image specialization has an invalid key mapping");
}
@@ -663,7 +694,6 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
}
}
std::vector<ResourceBlock> blocks(program.blocks.size());
std::vector<std::vector<uint32_t>> predecessors(blocks.size());
for (uint32_t i = 0; i < blocks.size(); i++) {
auto& block = blocks[i];
const auto& info = program.block_info[i];
@@ -687,7 +717,6 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
return {};
}
block.successors.push_back(found->second);
predecessors[found->second].push_back(i);
}
for (const auto& inst: *program.blocks[i]) {
const auto op = inst.GetOpcode();
@@ -717,30 +746,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end());
}
// Host branches only matter when they guard a later resource access. Leave
// data-only branches (for example a mutable flag checked before a trap) on the GPU.
std::vector<uint8_t> resource_reachable(blocks.size());
std::vector<uint32_t> pending;
for (uint32_t i = 0; i < blocks.size(); ++i) {
if (!blocks[i].sources.empty() || !blocks[i].srt_reads.empty()) {
resource_reachable[i] = 1;
pending.push_back(i);
}
}
while (!pending.empty()) {
const auto index = pending.back();
pending.pop_back();
for (const auto predecessor: predecessors[index]) {
if (resource_reachable[predecessor]) continue;
resource_reachable[predecessor] = 1;
pending.push_back(predecessor);
}
}
for (auto& block: blocks) {
block.condition = std::ranges::any_of(block.successors, [&](uint32_t successor) {
return resource_reachable[successor] != 0;
}) ? predicate(block.condition) : Value {};
}
for (auto& block: blocks) block.condition = predicate(block.condition);
if (std::ranges::none_of(
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
return {};
@@ -1034,9 +1040,10 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
plan.requires_specialization_memory = true;
capture_indirect_reads |= indirect.material_source != UINT32_MAX || !indirect.sources.empty();
MarkCleanFlatSlots(plan, Source(plan, indirect.material_source), plan.clean_flat_slots,
indirect.selector_mask);
capture_indirect_reads = true;
if (indirect.selector)
MarkCleanFlatSlots(plan, Source(plan, indirect.selector->source), plan.clean_flat_slots);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_mask);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.selector_first);
MarkCleanFlatSlots(plan, nullptr, plan.clean_flat_slots, indirect.key_count);
if (indirect.sources.empty()) {
@@ -1180,10 +1187,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
continue;
}
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, indirect, i, 8u, observed, clean, snapshot, snapshot.images,
specialization.images, UINT32_MAX, [&](DescriptorValue& value) {
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128))
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
(bounded_table && DescriptorDimension(value, image.dimension) != image.dimension))
value.dwords.fill(0u);
return true;
})) {
@@ -8,7 +8,6 @@
#include <algorithm>
#include <fmt/format.h>
#include <map>
#include <numeric>
#include <optional>
#include <span>
#include <tuple>
@@ -374,11 +373,68 @@ public:
}
}
for (const auto& plan: m_indirect_descriptors) {
auto key = plan.key;
if (plan.table_read != nullptr) {
const auto* anchor = plan.table_indexed ? plan.handle : plan.table_read;
auto* block = anchor->Parent();
const auto where = std::ranges::find_if(block->Instructions(),
[&](const Inst& inst) { return &inst == anchor; });
const auto emit = [&](ValueOpcode op, std::initializer_list<Value> args) {
return Value(&*block->PrependNewInst(where, op, args));
};
const auto& read = *plan.table_read;
const auto& memory = m_program.memory_info[read.Flags<MemoryFlags>().index];
Value offset;
Value valid(true);
if (plan.table_indexed) {
const auto* table = read.Arg(0).ResolveInstruction();
const auto stride = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(1), Value(16u), Value(14u)});
const auto format = emit(ValueOpcode::BitFieldUExtract,
{table->Arg(3), Value(12u), Value(7u)});
offset = emit(ValueOpcode::IAdd32,
{emit(ValueOpcode::IMul32, {read.Arg(1), stride}), Value(memory.offset)});
valid = emit(ValueOpcode::LogicalAnd,
{emit(ValueOpcode::ULessThan32, {read.Arg(1), table->Arg(2)}),
emit(ValueOpcode::INotEqual32, {format, Value(0u)})});
} else {
const auto dynamic = emit(ValueOpcode::BitwiseAnd32, {read.Arg(1), Value(~3u)});
offset = emit(ValueOpcode::IAdd32, {dynamic, Value(memory.offset & ~3u)});
valid = emit(ValueOpcode::ULessThanEqual32, {dynamic, offset});
}
key = emit(ValueOpcode::SelectU32, {valid, offset, Value(UINT32_MAX)});
if (!plan.table_indexed) {
// NativeDescriptorSource proved this read reaches the image. Preserve its
// SSA Phi edges while retaining descriptor DWORDs for other consumers.
std::map<const Inst*, Value> projected;
const auto project = [&](auto&& self, Value value) -> Value {
value = value.Resolve();
const auto* phi = value.TryInstruction();
if (phi == plan.table_read) return key;
if (phi == nullptr || phi->GetOpcode() != ValueOpcode::Phi) return Value(UINT32_MAX);
if (const auto found = projected.find(phi); found != projected.end()) return found->second;
auto* parent = phi->Parent();
const auto position = std::ranges::find_if(parent->Instructions(),
[&](const Inst& inst) { return &inst == phi; });
auto& selected = *parent->PrependNewInst(position, ValueOpcode::Phi);
selected.SetFlags(Type::U32);
projected.emplace(phi, Value(&selected));
for (uint32_t arm = 0; arm < phi->NumArgs(); ++arm)
selected.AddPhiOperand(phi->PhiBlock(arm), self(self, phi->Arg(arm)));
return Value(&selected);
};
key = project(project, plan.handle->Arg(0));
}
plan.handle->SetArg(0, key);
for (uint32_t word = 1; word < plan.handle->NumArgs(); ++word)
plan.handle->SetArg(word, Value(0u));
continue;
}
if (plan.handle->GetOpcode() == ValueOpcode::GetBufferResource) {
plan.handle->SetArg(0, plan.key);
for (uint32_t word = 1; word < 4u; ++word) plan.handle->SetArg(word, Value(0u));
} else {
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, plan.key);
if (plan.reads[0] == nullptr) plan.handle->SetArg(0, key);
for (uint32_t dword = 0; dword < 4u; dword++) {
plan.handle->SetArg(dword + 1u, plan.roots[dword + 4u]);
}
@@ -437,6 +493,10 @@ private:
std::array<Value, 8> roots {};
std::array<uint32_t, 8> memory {};
std::array<const Inst*, 8> reads {};
bool split_offsets = false;
bool retain_reads = false;
bool table_indexed = false;
const Inst* table_read = nullptr;
};
[[noreturn]] void Fail(uint32_t pc, const std::string& reason) const {
@@ -742,6 +802,15 @@ private:
}
}
}
for (auto* block: m_program.blocks) {
for (auto& inst: *block) {
uint32_t index = 0;
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
ScalarReadMemory(inst, index) != nullptr && inst.Arg(1).Resolve().IsImmediate() &&
ValidateRuntimeValue(m_program, Value(&inst)))
CollectScalarRead(Value(&inst), inst.Flags<MemoryFlags>().pc);
}
}
for (auto* read: m_scalar_reads) {
const auto flags = read->Flags<MemoryFlags>();
auto& memory = m_program.memory_info[flags.index];
@@ -825,7 +894,7 @@ private:
uint32_t InternSource(const DescriptorSource& descriptor) {
for (uint32_t candidate = 0; candidate < m_sources.size(); candidate++) {
const auto& current = m_sources[candidate];
auto& current = m_sources[candidate];
if (current.dword_count != descriptor.dword_count ||
current.indirect_descriptor.has_value() != descriptor.indirect_descriptor.has_value()) {
continue;
@@ -833,10 +902,9 @@ private:
if (current.indirect_descriptor.has_value()) {
const auto& a = *current.indirect_descriptor;
const auto& b = *descriptor.indirect_descriptor;
if (a.material_source != b.material_source || a.table_source != b.table_source ||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
a.selector_shift != b.selector_shift ||
a.table_offset != b.table_offset || a.table_stride != b.table_stride ||
if (a.selector != b.selector || a.table_source != b.table_source ||
a.table_offset != b.table_offset || a.table_immediate != b.table_immediate ||
a.table_stride != b.table_stride ||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
@@ -851,6 +919,12 @@ private:
same = same && EquivalentValue(m_program, current.dwords[i], descriptor.dwords[i]);
}
if (same) {
if (current.indirect_descriptor) {
current.indirect_descriptor->table_scalar |= descriptor.indirect_descriptor->table_scalar;
current.indirect_descriptor->table_record_bytes = std::max(
current.indirect_descriptor->table_record_bytes,
descriptor.indirect_descriptor->table_record_bytes);
}
return candidate;
}
}
@@ -940,41 +1014,6 @@ private:
return ValidateSource(descriptor, bad_dword);
}
bool MatchMaterialOffset(Value value, uint32_t& stride, uint32_t& offset) const {
stride = 1u;
offset = 0u;
value = value.Resolve();
while (const auto* inst = value.TryInstruction()) {
if (inst->NumArgs() != 2u) break;
uint32_t constant = 0;
Value remaining;
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
ImmediateU32(inst->Arg(1), constant) && constant < 32u) {
stride *= 1u << constant;
remaining = inst->Arg(0);
} else if (inst->GetOpcode() == ValueOpcode::IAdd32 ||
inst->GetOpcode() == ValueOpcode::IMul32) {
if (ImmediateU32(inst->Arg(0), constant)) remaining = inst->Arg(1);
else if (ImmediateU32(inst->Arg(1), constant)) remaining = inst->Arg(0);
else break;
if (inst->GetOpcode() == ValueOpcode::IAdd32) offset += stride * constant;
else stride *= constant;
} else {
break;
}
value = remaining.Resolve();
}
// Treat the remaining GPU-derived scalar as any U32 value.
return stride != 0u && value.GetType() == Type::U32 && !value.IsImmediate();
}
const Inst* MaterialKeyShift(Value key, uint32_t& amount) const {
const auto* shift = key.Resolve().TryInstruction();
return shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
ImmediateU32(shift->Arg(1), amount) && amount != 0u && amount < 32u
? shift : nullptr;
}
enum class LaneQuantifier { Any, All };
struct EdgePredicate {
Value condition;
@@ -1421,9 +1460,9 @@ private:
static_cast<uint64_t>(UINT32_MAX) + 1u) return false;
const auto mask = BoundedSetBitMask(offset.index, active);
if (mask.IsEmpty()) return false;
indirect.material_source = InternSource(material_source);
indirect.selector_stride = static_cast<uint32_t>(offset.stride);
indirect.selector_offset = static_cast<uint32_t>(offset.offset + memory.offset);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.stride = static_cast<uint32_t>(offset.stride),
.offset = static_cast<uint32_t>(offset.offset + memory.offset)});
indirect.key_count = Value(32u);
indirect.selector_mask = mask;
return true;
@@ -1584,8 +1623,9 @@ private:
bool MatchDescriptorTable(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan,
DescriptorSource& table_source, Value& key, uint32_t& table_offset,
uint32_t& table_stride) {
uint32_t& table_stride, uint32_t& table_immediate) {
Inst* table_handle = nullptr;
Value table_guard;
for (uint32_t dword = 0; dword < handle.NumArgs(); ++dword) {
auto* read = descriptor.dwords[dword].Resolve().TryInstruction();
if (read == nullptr) {
@@ -1593,7 +1633,31 @@ private:
}
uint32_t memory_index = 0;
const auto* memory = ScalarReadMemory(*read, memory_index);
if (memory == nullptr || memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
uint32_t component = 0;
const bool indexed = memory == nullptr;
if (indexed) {
if (handle.NumArgs() != 8u) return false;
const auto* selected = UniformizedMaterialValue(descriptor.dwords[dword], handle);
if (selected == nullptr) return false;
const auto* extract = selected->Arg(1).Resolve().TryInstruction();
if (extract == nullptr || extract->GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!ImmediateU32(extract->Arg(1), component) || component >= 4u) return false;
read = extract->Arg(0).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32x4) return false;
memory_index = read->Flags<MemoryFlags>().index;
if (memory_index >= m_program.memory_info.size()) return false;
memory = &m_program.memory_info[memory_index];
uint32_t offset = 1u, scalar = 1u;
if (memory->kind != ResourceKind::Buffer || memory->typed || memory->formatted ||
!memory->idxen || memory->offen || memory->data_bits != 32u ||
memory->data_dwords != 4u ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, read->Arg(4), selected->Arg(0)) ||
(dword != 0u && !EquivalentValue(m_program, table_guard, read->Arg(4)))) return false;
table_guard = read->Arg(4);
}
if (memory->offset > INT32_MAX || (memory->offset & 3u) != 0u ||
!MemoryIndexBelongsTo(memory_index, *read)) {
return false;
}
@@ -1608,23 +1672,32 @@ private:
(memory->kind == ResourceKind::ScalarAddress && read->Parent() != handle.Parent()) ||
(table_handle != nullptr &&
!EquivalentValue(m_program, Value(table_handle), Value(current_handle))) ||
!MatchTableOffset(read->Arg(1), current_key, offset, stride) ||
memory->offset > UINT32_MAX - offset) {
(!indexed && !MatchTableOffset(read->Arg(1), current_key, offset, stride))) {
return false;
}
offset += memory->offset;
if (indexed) {
current_key = read->Arg(1);
offset = component * 4u;
}
if (dword == 0u) {
if (component != 0u) return false;
plan.table_indexed = indexed;
key = current_key;
table_offset = offset;
table_immediate = memory->offset;
table_stride = stride;
} else if (table_stride != stride || !EquivalentValue(m_program, key, current_key) ||
static_cast<uint64_t>(table_offset) + dword * sizeof(uint32_t) != offset) {
} else if (plan.table_indexed != indexed || table_stride != stride ||
!EquivalentValue(m_program, key, current_key) ||
uint64_t {table_offset} + table_immediate + dword * sizeof(uint32_t) !=
uint64_t {offset} + memory->offset) {
return false;
}
plan.split_offsets |= !indexed && offset != table_offset;
table_handle = current_handle;
if (handle.NumArgs() == 8u
? WorkgroupAxis(key) == UINT32_MAX && !UsesOnlyImageDescriptors(*read) :
std::ranges::any_of(read->Uses(), [](const Use& use) {
if (handle.NumArgs() == 8u) {
if (memory->kind == ResourceKind::ScalarAddress)
plan.retain_reads |= !UsesOnlyImageDescriptors(*read);
} else if (std::ranges::any_of(read->Uses(), [](const Use& use) {
return use.user->GetOpcode() != ValueOpcode::GetBufferResource ||
std::ranges::any_of(use.user->Uses(), [](const Use& consumer) {
return consumer.user->GetOpcode() != ValueOpcode::StoreBufferU32;
@@ -1642,36 +1715,6 @@ private:
return true;
}
bool MatchUniformizedBufferKey(Value key, const Inst& image,
DescriptorSource::IndirectDescriptor& indirect,
DescriptorSource& material_source) {
const auto* selected = UniformizedMaterialValue(key, image);
if (selected == nullptr) return false;
uint32_t amount = 0;
const auto* shift = MaterialKeyShift(selected->Arg(1), amount);
if (shift == nullptr) return false;
const auto* loaded = shift->Arg(0).Resolve().TryInstruction();
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadBufferU32) return false;
const auto& memory = m_program.memory_info[read->Flags<MemoryFlags>().index];
uint32_t offset = 1u, scalar = 1u;
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen ||
!ImmediateU32(read->Arg(2), offset) || offset != 0u ||
!ImmediateU32(read->Arg(3), scalar) || scalar != 0u ||
!EquivalentValue(m_program, selected->Arg(0), read->Arg(4)) ||
!MakeRuntimeTableSource(*read, material_source)) return false;
// GPU index arithmetic may wrap. Every record in the bounded V# is a candidate.
indirect.material_source = InternSource(material_source);
indirect.selector_first = Value(0u);
indirect.key_count = material_source.dwords[2];
indirect.selector_offset = memory.offset;
indirect.selector_shift = amount;
return true;
}
bool TryMakeIndirectImage(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u) return false;
@@ -1679,20 +1722,21 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride)) return false;
table_stride, table_immediate)) return false;
DescriptorSource material_source;
DescriptorSource::IndirectDescriptor indirect;
indirect.table_offset = table_offset;
indirect.table_immediate = table_immediate;
indirect.table_stride = table_stride;
indirect.workgroup_axis = WorkgroupAxis(key);
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis != UINT32_MAX && table_source.dword_count == 2u) {
// The descriptor also supplies dimensions to shader arithmetic. Keep its reads;
// only the image handle is projected onto the bounded workgroup key.
plan.reads.fill(nullptr);
} else if (table_stride != 32u) {
return false;
plan.retain_reads = true;
} else if (table_source.dword_count == 2u) {
if (table_stride != 32u) return false;
const auto* selector = key.Resolve().TryInstruction();
const bool bitscan = selector != nullptr && selector->GetOpcode() == ValueOpcode::FindILsb32 &&
selector->NumArgs() == 1u && !m_shader_writes &&
@@ -1709,32 +1753,21 @@ private:
}
if ((table_offset & 3u) != 0u ||
(bitscan && table_offset > UINT32_MAX - (32u * 32u - 1u))) return false;
} else if (!MatchUniformizedBufferKey(key, handle, indirect, material_source)) {
const auto* key_shift = MaterialKeyShift(key, indirect.selector_shift);
auto* material_read =
(key_shift != nullptr ? key_shift->Arg(0) : key).Resolve().TryInstruction();
uint32_t material_memory_index = 0;
const auto* memory = material_read != nullptr
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
if (table_offset != 0u || memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
return false;
}
if (!MatchMaterialOffset(material_read->Arg(1), indirect.selector_stride,
indirect.selector_offset)) {
return false;
}
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
indirect.selector_offset =
(static_cast<uint32_t>(indirect.selector_offset % step) & ~3u) + (memory->offset & ~3u);
const auto* material_handle = material_read->Arg(0).Resolve().TryInstruction();
if (material_handle == nullptr ||
material_handle->GetOpcode() != ValueOpcode::GetBufferResource ||
!MakeRuntimeTableSource(*material_read, material_source)) {
return false;
}
indirect.material_source = InternSource(material_source);
} else {
// A bounded V# supplies the complete image table. Leave every GPU selector
// and descriptor read in the shader; the host only translates table bytes.
if (plan.split_offsets) return false;
indirect.table_stride = 0u;
indirect.table_offset = 0u;
indirect.table_immediate = 0u;
indirect.workgroup_axis = UINT32_MAX;
plan.table_read = plan.reads[0];
if (plan.table_indexed) indirect.table_record_bytes = table_immediate + 32u;
else indirect.table_scalar = true;
plan.retain_reads = true;
}
if (plan.retain_reads) plan.reads.fill(nullptr);
indirect.table_source = InternSource(table_source);
DescriptorSource image_source;
image_source.dword_count = 8u;
@@ -1768,9 +1801,10 @@ private:
Value key;
uint32_t table_offset = 0;
uint32_t table_stride = 0;
uint32_t table_immediate = 0;
if (!MatchDescriptorTable(handle, descriptor, plan, table_source, key, table_offset,
table_stride) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u) return false;
table_stride, table_immediate) || table_stride != 16u ||
table_source.dword_count != 4u || table_offset != 0u || table_immediate != 0u) return false;
const auto* lane_read = key.Resolve().TryInstruction();
if (lane_read == nullptr || lane_read->GetOpcode() != ValueOpcode::ReadFirstLane)
return false;
@@ -1802,9 +1836,9 @@ private:
indirect.table_stride = table_stride;
if (!MakeRuntimeTableSource(*read, material_source) ||
!BoundedSelectedIndex(read->Arg(1), read->Arg(4), indirect)) return false;
indirect.material_source = InternSource(material_source);
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead{.source = InternSource(material_source),
.offset = memory.offset + lane * 4u});
indirect.table_source = InternSource(table_source);
indirect.selector_offset = memory.offset + lane * 4u;
DescriptorSource source;
source.dword_count = 4u;
source.dwords.fill(Value(0u));
@@ -2032,6 +2066,8 @@ private:
DescriptorSource descriptor;
MakeSource(*handle, inst.GetOpcode() == ValueOpcode::StoreBufferU32 ? 4u : 8u,
false, false, memory.resource * 4u, descriptor, flags.pc);
uint32_t bad_dword = 0;
if (ValidateSource(descriptor, bad_dword)) continue;
IndirectDescriptorPlan plan;
if (TryMakeIndirectImage(*handle, descriptor, plan) || TryMakeFiniteImage(*handle, plan) ||
TryMakeIndirectBuffer(*handle, descriptor, plan)) {
@@ -23,6 +23,9 @@ namespace {
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
bool AddSignedAddress(uint64_t base, int64_t offset, uint64_t& result) {
if (base > AddressMask) {
return false;
}
if (offset < 0) {
const auto magnitude = uint64_t {0} - static_cast<uint64_t>(offset);
if (magnitude > base) {
@@ -32,7 +35,7 @@ bool AddSignedAddress(uint64_t base, int64_t offset, uint64_t& result) {
return true;
}
const auto magnitude = static_cast<uint64_t>(offset);
if (magnitude > UINT64_MAX - base) {
if (magnitude > AddressMask - base) {
return false;
}
result = base + magnitude;
@@ -477,7 +480,7 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
if (!Arg(*handle, 0, low) || !Arg(*handle, 1, high) || !Arg(inst, 1, offset)) {
return false;
}
const auto base = (high << 32u) | static_cast<uint32_t>(low);
const auto base = ((high << 32u) | static_cast<uint32_t>(low)) & AddressMask;
const auto immediate = static_cast<int64_t>(static_cast<int32_t>(mem.offset));
uint64_t address = 0;
if (inst.GetOpcode() == ValueOpcode::ReadConstBuffer || vector) {
@@ -507,7 +510,7 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
return false;
}
address = (base & AddressMask & ~uint64_t {3}) + byte_offset;
address = (base & ~uint64_t {3}) + byte_offset;
} else {
const auto relative = (immediate & ~int64_t {3}) +
static_cast<int64_t>(static_cast<uint32_t>(offset) & ~3u);
+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) {
+1
View File
@@ -96,6 +96,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
if(NOT KYTY_MACDEPLOYQT)
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>760</width>
<width>840</width>
<height>620</height>
</rect>
</property>
@@ -182,7 +182,22 @@
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
@@ -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
+4
View File
@@ -122,6 +122,7 @@ public:
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -154,6 +155,7 @@ public:
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -201,6 +203,7 @@ public:
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -244,6 +247,7 @@ public:
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -28,6 +28,7 @@ public:
~ConfigurationListWidget() override;
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
@@ -42,6 +43,7 @@ public:
bool EnsureGameDirectory();
void ScanGameDirectory();
void ViewTrophies();
void ViewTrophyOverview();
signals:
@@ -78,6 +80,7 @@ private:
bool m_run_enabled = true;
Ui::ConfigurationListWidget* m_ui = nullptr;
QString m_settings_file;
QString m_runtime_directory;
QStringList m_game_dirs;
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
-1
View File
@@ -12,7 +12,6 @@ namespace GameContent {
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
+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
+7 -2
View File
@@ -3,6 +3,8 @@
#include <QDialog>
#include <vector>
class QTabWidget;
class QWidget;
@@ -12,10 +14,13 @@ public:
explicit TrophyViewerDialog(QWidget* parent = nullptr);
static bool HasTrophyData(const Configuration* info);
static void ShowForGame(const Configuration* info, QWidget* parent);
static void ShowForGame(const Configuration* info, const QString& runtime_directory,
QWidget* parent);
static void ShowOverview(const std::vector<const Configuration*>& games,
const QString& runtime_directory, QWidget* parent);
private:
bool LoadGame(const Configuration& info, QString& error);
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
QTabWidget* m_tabs = nullptr;
};
+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
+3 -3
View File
@@ -123,6 +123,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
m_info(info) {
m_ui->setupUi(this);
setMinimumWidth(width());
InitGameDirectories();
m_ui->controller_group->setVisible(false);
@@ -164,9 +165,6 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
m_ui->lineEdit_printf_file->setEnabled(log == Configuration::LogDirection::File);
});
// Keep the controls at a usable minimum while allowing the settings window
// and its expanding fields to use any additional space the user gives them.
layout()->setSizeConstraint(QLayout::SetMinimumSize);
setSizeGripEnabled(true);
restoreGeometry(g_last_geometry);
@@ -276,6 +274,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -429,6 +428,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+22 -2
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() {
@@ -835,7 +840,22 @@ void ConfigurationListWidget::ViewTrophies() {
return;
}
TrophyViewerDialog::ShowForGame(&item->GetInfo(), this);
const auto config = CreateConfiguration(*item);
TrophyViewerDialog::ShowForGame(config.get(), m_runtime_directory, this);
}
void ConfigurationListWidget::ViewTrophyOverview() {
std::vector<std::unique_ptr<Configuration>> configurations;
std::vector<const Configuration*> games;
configurations.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
games.reserve(static_cast<size_t>(m_ui->cfgs_list->topLevelItemCount()));
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); ++index) {
const auto* item =
static_cast<const ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
configurations.push_back(CreateConfiguration(*item));
games.push_back(configurations.back().get());
}
TrophyViewerDialog::ShowOverview(games, m_runtime_directory, this);
}
void ConfigurationListWidget::open_game_folder() {
@@ -1014,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()));
+4 -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>
@@ -170,6 +170,7 @@ void MainDialogPrivate::FindInterpreter() {
}
bool found = QFile::exists(m_interpreter);
m_ui->widget->SetRuntimeDirectory(QFileInfo(m_interpreter).absolutePath());
if (found) {
m_ui->label_Interpreter->setText(tr("Emulator: ") + m_interpreter);
@@ -244,6 +245,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
if (info.tessellation_enabled) {
args << "--tessellation";
}
@@ -283,7 +285,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
args << "--game" << game;
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
const auto patch_plan = Cheats::PlanPath(info.title_id);
if (QFileInfo::exists(patch_plan)) {
args << "--game-patch" << patch_plan;
}
+262 -93
View File
@@ -1,140 +1,309 @@
#include "patchesDialog.h"
#include "cheatFile.h"
#include "configuration.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QSplitter>
#include <QStringList>
#include <QTabWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
#include <algorithm>
#include <utility>
namespace {
void ShowMods(QListWidget* list, const QJsonDocument& document, bool checkable) {
list->clear();
for (const auto& value: document.object().value(QStringLiteral("mods")).toArray()) {
const auto mod = value.toObject();
auto* item = new QListWidgetItem(mod.value(QStringLiteral("name")).toString(), list);
if (checkable) {
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(mod.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
: Qt::Unchecked);
}
}
}
QLabel* StatusLabel(QWidget* parent) {
auto* label = new QLabel(parent);
label->setWordWrap(true);
label->setTextFormat(Qt::PlainText);
return label;
}
} // namespace
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_version(game.gameVersion),
m_process(QFileInfo(game.elf.isEmpty() ? QStringLiteral("eboot.bin") : game.elf).fileName()) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
resize(640, 480);
resize(800, 640);
auto* layout = new QVBoxLayout(this);
m_patches = new QListWidget(this);
m_status = new QLabel(this);
m_apply = new QPushButton(tr("Apply selection"), this);
auto* close = new QPushButton(tr("Close"), this);
auto* layout = new QVBoxLayout(this);
auto* game_label = StatusLabel(this);
game_label->setText(tr("%1 · Installed version: %2")
.arg(m_title_id, m_version.isEmpty() ? tr("Unknown") : m_version));
layout->addWidget(game_label);
m_tabs = new QTabWidget(this);
layout->addWidget(m_tabs);
m_status->setWordWrap(true);
layout->addWidget(m_patches);
layout->addWidget(m_status);
auto* remote = new QWidget(m_tabs);
auto* remote_layout = new QVBoxLayout(remote);
m_tabs->addTab(remote, tr("Remote"));
auto* credits = new QLabel(
tr("Cheats by <a href=\"https://github.com/TeeKay87/HEN-Cheats-Collection\">TeeKay87 / HEN "
"Cheats Collection</a> and its contributors."),
remote);
credits->setOpenExternalLinks(true);
credits->setWordWrap(true);
remote_layout->addWidget(credits);
auto* support = StatusLabel(remote);
support->setText(tr("Only JSON cheats are supported. MC4 and SHN files are shown disabled. "
"Select a JSON file to preview its cheats."));
remote_layout->addWidget(support);
auto* split = new QSplitter(Qt::Vertical, remote);
m_files = new QTreeWidget(split);
m_files->setHeaderLabels({tr("Version"), tr("Format"), tr("File")});
m_files->setRootIsDecorated(false);
m_files->setSelectionMode(QAbstractItemView::SingleSelection);
m_files->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_files->header()->setSectionResizeMode(2, QHeaderView::Stretch);
m_preview = new QListWidget(split);
remote_layout->addWidget(split, 1);
m_remote_status = StatusLabel(remote);
m_preview_status = StatusLabel(remote);
remote_layout->addWidget(m_remote_status);
remote_layout->addWidget(m_preview_status);
auto* remote_buttons = new QDialogButtonBox(remote);
m_refresh = remote_buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
m_import = remote_buttons->addButton(tr("Import to Local"), QDialogButtonBox::ActionRole);
m_import->setEnabled(false);
remote_layout->addWidget(remote_buttons);
auto* buttons = new QDialogButtonBox(this);
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
buttons->addButton(close, QDialogButtonBox::RejectRole);
auto* local = new QWidget(m_tabs);
auto* local_layout = new QVBoxLayout(local);
m_tabs->addTab(local, tr("Local"));
m_patches = new QListWidget(local);
m_status = StatusLabel(local);
local_layout->addWidget(m_patches, 1);
local_layout->addWidget(m_status);
auto* local_buttons = new QDialogButtonBox(local);
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
local_layout->addWidget(local_buttons);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
connect(reload, &QPushButton::clicked, this, &PatchesDialog::Load);
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
connect(close, &QPushButton::clicked, this, &QDialog::close);
connect(m_refresh, &QPushButton::clicked, this, &PatchesDialog::LoadRemote);
connect(m_import, &QPushButton::clicked, this, &PatchesDialog::ImportRemote);
connect(m_files, &QTreeWidget::currentItemChanged, this, &PatchesDialog::PreviewRemote);
m_repository = new Cheats::CheatRepository(this);
connect(
m_repository, &Cheats::CheatRepository::CatalogLoaded, this,
[this](QList<Cheats::RemoteFile> files, const QString& error) {
std::stable_sort(files.begin(), files.end(), [this](const auto& a, const auto& b) {
return (a.version == m_version) > (b.version == m_version);
});
m_remote_files = std::move(files);
for (qsizetype index = 0; index < m_remote_files.size(); ++index) {
const auto& file = m_remote_files[index];
auto* item = new QTreeWidgetItem(
m_files, {file.version == m_version ? tr("%1 (installed)").arg(file.version)
: file.version,
file.format.toUpper(), file.name});
item->setData(0, Qt::UserRole, index);
for (int column = 0; column < 3; ++column) {
item->setToolTip(column, file.IsSupported()
? file.title
: tr("%1 — %2 is not supported.")
.arg(file.title, file.format.toUpper()));
}
if (!file.IsSupported()) {
item->setFlags(item->flags() & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
}
}
auto status =
m_remote_files.isEmpty() && error.isEmpty()
? tr("No remote cheats found for %1.").arg(m_title_id)
: tr("%1 remote file(s) for %2.").arg(m_remote_files.size()).arg(m_title_id);
if (!error.isEmpty()) {
status = tr("%1\nCould not load the complete collection: %2\nUse Refresh to retry.")
.arg(status, error);
}
m_remote_status->setText(status);
m_refresh->setEnabled(true);
});
connect(
m_repository, &Cheats::CheatRepository::FileLoaded, this,
[this](const QByteArray& data, QString error) {
if (error.isEmpty()) {
error = Cheats::ParseDocument(data, &m_remote_document);
}
if (!error.isEmpty()) {
m_preview_status->setText(
tr("Could not preview this file: %1\nSelect another file or refresh to retry.")
.arg(error));
return;
}
ShowMods(m_preview, m_remote_document, false);
error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
const auto root = m_remote_document.object();
QStringList creators;
for (const auto& value: root.value(QStringLiteral("credits")).toArray()) {
if (value.isString()) {
creators.append(value.toString());
}
}
const auto metadata =
tr("%1\nCredits: %2 · Version: %3")
.arg(root.value(QStringLiteral("name")).toString(),
creators.isEmpty() ? root.value(QStringLiteral("author")).toString()
: creators.join(QStringLiteral(", ")),
root.value(QStringLiteral("version")).toString());
m_preview_status->setText(
metadata + QStringLiteral("\n\n") +
(error.isEmpty() ? tr("Import to Local, then select the cheats to enable. Changes "
"take effect on the next game launch.")
: error));
m_import->setEnabled(error.isEmpty());
});
Load();
}
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
}
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
return QDir(QCoreApplication::applicationDirPath())
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
LoadRemote();
}
void PatchesDialog::Load() {
QFile file(PatchPlanPath(m_title_id));
m_patches->clear();
m_local_data.clear();
m_local_document = {};
m_local_readable = false;
m_apply->setEnabled(false);
const auto path = Cheats::PlanPath(m_title_id);
if (path.isEmpty()) {
m_status->setText(tr("Unsupported title ID."));
return;
}
QFile file(path);
if (!file.exists()) {
m_local_readable = true;
m_status->setText(tr("No local cheat file: %1").arg(path));
return;
}
if (!file.open(QIODevice::ReadOnly)) {
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
m_apply->setEnabled(false);
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_data = file.readAll();
if (file.error() != QFile::NoError) {
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
return;
}
const auto root = document.object();
if (!root.value(QStringLiteral("mods")).isArray()) {
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_local_readable = true;
const auto error = Cheats::ParseDocument(m_local_data, &m_local_document);
if (!error.isEmpty()) {
m_status->setText(tr("%1\n%2").arg(error, path));
return;
}
const auto mods = root.value(QStringLiteral("mods")).toArray();
for (const auto& value: mods) {
if (!value.isObject()) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
const auto mod = value.toObject();
const auto name = mod.value(QStringLiteral("name"));
const auto enabled = mod.value(QStringLiteral("enabled"));
if (!name.isString() || name.toString().isEmpty() ||
(!enabled.isUndefined() && !enabled.isBool())) {
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
m_patches->clear();
return;
}
auto* item = new QListWidgetItem(name.toString(), m_patches);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
}
m_apply->setEnabled(!mods.isEmpty());
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
ShowMods(m_patches, m_local_document, true);
m_apply->setEnabled(m_patches->count() > 0);
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(m_patches->count()).arg(path));
}
void PatchesDialog::Save() {
const auto path = PatchPlanPath(m_title_id);
QFile input(path);
if (!input.open(QIODevice::ReadOnly)) {
QList<bool> enabled;
for (int index = 0; index < m_patches->count(); ++index) {
enabled.append(m_patches->item(index)->checkState() == Qt::Checked);
}
const auto document = Cheats::WithSelection(m_local_document, enabled);
const auto error = Cheats::SaveDocument(Cheats::PlanPath(m_title_id), document, m_local_data);
if (!error.isEmpty()) {
m_status->setText(error);
return;
}
m_local_document = document;
m_local_data = document.toJson();
m_status->setText(tr("Cheat selection saved. Changes take effect on the next game launch."));
}
auto document = QJsonDocument::fromJson(input.readAll());
input.close();
const auto changed_message = tr("Cheat file changed; reload the dialog.");
if (!document.isObject()) {
m_status->setText(changed_message);
void PatchesDialog::LoadRemote() {
m_repository->CancelFetch();
m_files->clear();
m_remote_files.clear();
m_preview->clear();
m_remote_document = {};
m_import->setEnabled(false);
m_refresh->setEnabled(false);
m_preview_status->clear();
m_remote_status->setText(tr("Loading remote cheats…"));
m_repository->Load(m_title_id);
}
void PatchesDialog::PreviewRemote() {
m_repository->CancelFetch();
m_preview->clear();
m_remote_document = {};
m_import->setEnabled(false);
m_preview_status->clear();
const auto* item = m_files->currentItem();
if (item == nullptr) {
return;
}
const auto index = item->data(0, Qt::UserRole).toInt();
if (index < 0 || index >= m_remote_files.size() || !m_remote_files[index].IsSupported()) {
return;
}
m_preview_status->setText(tr("Loading cheat preview…"));
m_repository->Fetch(m_remote_files[index]);
}
void PatchesDialog::ImportRemote() {
const auto error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
if (!error.isEmpty()) {
m_preview_status->setText(error);
return;
}
auto root = document.object();
auto mods = root.value(QStringLiteral("mods")).toArray();
if (mods.size() != m_patches->count()) {
m_status->setText(changed_message);
if (!m_local_readable) {
m_preview_status->setText(
tr("Could not read the local cheat file. Open Local and reload it before importing."));
return;
}
for (int index = 0; index < mods.size(); index++) {
auto mod = mods[index].toObject();
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
mods[index] = mod;
const auto path = Cheats::PlanPath(m_title_id);
if (QFile::exists(path) &&
QMessageBox::question(this, tr("Replace local cheats?"),
tr("Replace the local cheat file and selections for %1? Imported "
"cheats will start unchecked.")
.arg(m_title_id),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes) {
return;
}
root.insert(QStringLiteral("mods"), mods);
document.setObject(root);
QSaveFile output(path);
if (!output.open(QIODevice::WriteOnly) || output.write(document.toJson()) < 0 ||
!output.commit()) {
qWarning() << "Could not save cheat file:" << output.errorString();
m_status->setText(tr("Could not save cheat file: %1").arg(output.errorString()));
const auto mods = m_remote_document.object().value(QStringLiteral("mods")).toArray();
const auto document = Cheats::WithSelection(m_remote_document, QList<bool>(mods.size(), false));
const auto save_error = Cheats::SaveDocument(path, document, m_local_data);
if (!save_error.isEmpty()) {
m_preview_status->setText(save_error);
return;
}
m_status->setText(tr("Cheat selection saved."));
Load();
m_status->setText(
tr("Imported %1 cheat(s). Select cheats and apply your selection for the next game launch.")
.arg(mods.size()));
m_tabs->setCurrentIndex(1);
}
+383 -507
View File
@@ -1,498 +1,69 @@
#include "trophyViewerDialog.h"
#include "common/archive.h"
#include "common/trophies.h"
#include "configuration.h"
#include "gameContent.h"
#include <QAbstractItemView>
#include <QBrush>
#include <QByteArray>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileInfo>
#include <QFont>
#include <QHeaderView>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QMap>
#include <QListWidget>
#include <QMessageBox>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QProgressBar>
#include <QRegularExpression>
#include <QSet>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QTabBar>
#include <QTabWidget>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include <QVariant>
#include <QWidget>
#include <QtEndian>
#include <utility>
namespace {
constexpr quint32 UCP_MAGIC = 0xb228c60a;
constexpr quint32 UCP_VERSION = 1;
constexpr int UCP_HEADER_LEN = 0x40;
constexpr int UCP_TOC_SKIP = 0x20;
constexpr int UCP_ENTRY_LEN = 0x40;
constexpr int UCP_NAME_LEN = 0x20;
constexpr quint32 UCP_MAX_FILES = 4096;
constexpr quint64 UCP_MAX_EXTRACTED_SIZE = quint64 {64} << 20u;
struct UcpEntry {
QString name;
quint64 offset = 0;
quint64 size = 0;
};
struct TrophyDefinition {
QString id;
QString grade;
bool hidden = false;
bool has_reward = false;
};
struct TrophyText {
QString name;
QString detail;
QString reward;
};
struct TrophyRow {
QString id;
QString name;
QString detail;
QString grade;
QString reward;
bool hidden = false;
bool has_reward = false;
QPixmap icon;
};
struct TrophySet {
QString tab_title;
QList<TrophyRow> trophies;
};
static bool CanRead(const QByteArray& data, qsizetype offset, qsizetype size) {
return offset >= 0 && size >= 0 && offset <= data.size() && size <= data.size() - offset;
}
static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizetype max_size) {
if (!CanRead(data, offset, max_size)) {
return {};
}
qsizetype len = 0;
while (len < max_size && data.at(offset + len) != '\0') {
len++;
}
return QString::fromLatin1(data.constData() + offset, len);
}
static bool IsUsedUcpEntry(const QString& name) {
const auto lower = name.toCaseFolded();
return lower == QStringLiteral("tropconf.json") || lower == QStringLiteral("tropmeta.json") ||
(lower.startsWith(QStringLiteral("tropmeta_")) &&
lower.endsWith(QStringLiteral(".json"))) ||
(lower.startsWith(QStringLiteral("trop")) && lower.endsWith(QStringLiteral(".png")));
}
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
const QByteArray data =
GameContent::ReadPath(GameContent::ToPath(file_name), GameContent::MaxTrophyPackageSize);
if (data.isEmpty()) {
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
return false;
}
if (data.size() < UCP_HEADER_LEN) {
error = QObject::tr("%1 is too small to be a trophy package.")
.arg(QFileInfo(file_name).fileName());
return false;
}
const auto magic = qFromBigEndian<quint32>(data.constData() + 0x00);
if (magic != UCP_MAGIC) {
error = QObject::tr("%1 has an invalid trophy package magic.")
.arg(QFileInfo(file_name).fileName());
return false;
}
const auto version = qFromBigEndian<quint32>(data.constData() + 0x04);
if (version != UCP_VERSION) {
error = QObject::tr("%1 uses unsupported trophy package version %2.")
.arg(QFileInfo(file_name).fileName(), QString::number(version));
return false;
}
const auto declared_size = qFromBigEndian<quint64>(data.constData() + 0x08);
if (declared_size < UCP_HEADER_LEN || declared_size > static_cast<quint64>(data.size())) {
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
return false;
}
const auto file_count = qFromBigEndian<quint32>(data.constData() + 0x10);
if (file_count > UCP_MAX_FILES) {
error = QObject::tr("%1 contains too many files.").arg(QFileInfo(file_name).fileName());
return false;
}
const auto toc_offset = static_cast<quint64>(qFromBigEndian<quint32>(data.constData() + 0x14));
const auto data_size = declared_size;
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
if (toc_offset > data_size || table_size > data_size - toc_offset ||
!std::in_range<qsizetype>(toc_offset + table_size)) {
error = QObject::tr("%1 has an invalid table of contents.")
.arg(QFileInfo(file_name).fileName());
return false;
}
quint64 extracted_size = 0;
for (quint32 i = 0; i < file_count; i++) {
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
static_cast<quint64>(i) * UCP_ENTRY_LEN);
UcpEntry entry;
entry.name = ReadFixedString(data, entry_offset, UCP_NAME_LEN).trimmed();
entry.offset = qFromBigEndian<quint64>(data.constData() + entry_offset + 0x20);
entry.size = qFromBigEndian<quint64>(data.constData() + entry_offset + 0x28);
if (entry.name.isEmpty()) {
continue;
}
if (entry.offset > data_size || entry.size > data_size - entry.offset ||
!std::in_range<qsizetype>(entry.offset) || !std::in_range<qsizetype>(entry.size)) {
error = QObject::tr("%1 has an invalid entry for %2.")
.arg(QFileInfo(file_name).fileName(), entry.name);
return false;
}
if (!IsUsedUcpEntry(entry.name)) {
continue;
}
const auto entry_limit = entry.name.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)
? GameContent::MaxImageSize
: GameContent::MaxMetadataSize;
if (entry.size > entry_limit || entry.size > UCP_MAX_EXTRACTED_SIZE - extracted_size) {
error = QObject::tr("%1 contains an oversized entry for %2.")
.arg(QFileInfo(file_name).fileName(), entry.name);
return false;
}
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
static_cast<qsizetype>(entry.size)));
extracted_size += entry.size;
}
return true;
}
static const QByteArray* FindFile(const QMap<QString, QByteArray>& files, const QString& name) {
const auto it = files.constFind(name.toCaseFolded());
return it == files.constEnd() ? nullptr : &it.value();
}
static const QByteArray* FindFirstTrophyMetadataFile(const QMap<QString, QByteArray>& files) {
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
if (it.key().startsWith(QStringLiteral("tropmeta_")) &&
it.key().endsWith(QStringLiteral(".json"))) {
return &it.value();
}
}
return FindFile(files, QStringLiteral("tropmeta.json"));
}
static bool ReadJsonObject(const QByteArray& data, const QString& file_name, QJsonObject& object,
QString& error) {
QJsonParseError parse_error;
const auto doc = QJsonDocument::fromJson(data, &parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
error = QObject::tr("Could not read %1: %2").arg(file_name, parse_error.errorString());
return false;
}
object = doc.object();
return true;
}
static QString JsonString(const QJsonValue& value) {
if (value.isString()) {
return value.toString();
}
if (value.isDouble()) {
const double number = value.toDouble();
if (number == static_cast<int>(number)) {
return QString::number(static_cast<int>(number));
}
return QString::number(number);
}
if (value.isBool()) {
return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
}
return {};
}
static QList<TrophyDefinition> ReadDefinitions(const QJsonObject& tropconf) {
QList<TrophyDefinition> ret;
const auto trophies = tropconf.value(QStringLiteral("trophies")).toArray();
for (const auto& value: trophies) {
const auto obj = value.toObject();
TrophyDefinition def;
def.id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
def.grade = JsonString(obj.value(QStringLiteral("grade"))).trimmed();
def.hidden = obj.value(QStringLiteral("hidden")).toBool(false);
def.has_reward = obj.value(QStringLiteral("hasReward")).toBool(false);
if (!def.id.isEmpty()) {
ret.append(def);
}
}
return ret;
}
static QMap<QString, TrophyText> ReadMetadata(const QJsonObject& tropmeta) {
QMap<QString, TrophyText> ret;
const auto metadata = tropmeta.value(QStringLiteral("metadata")).toObject();
const auto trophies = metadata.value(QStringLiteral("trophyMetadata")).toArray();
for (const auto& value: trophies) {
const auto obj = value.toObject();
const auto id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
if (id.isEmpty()) {
continue;
}
TrophyText text;
text.name = JsonString(obj.value(QStringLiteral("name"))).trimmed();
text.detail = JsonString(obj.value(QStringLiteral("detail"))).trimmed();
text.reward = JsonString(obj.value(QStringLiteral("reward"))).trimmed();
ret.insert(id, text);
}
return ret;
}
static QString GradeToText(const QString& grade) {
if (grade == QStringLiteral("P")) {
return QObject::tr("Platinum");
}
if (grade == QStringLiteral("G")) {
return QObject::tr("Gold");
}
if (grade == QStringLiteral("S")) {
return QObject::tr("Silver");
}
if (grade == QStringLiteral("B")) {
return QObject::tr("Bronze");
}
return grade;
}
static QPixmap LoadTrophyIcon(const QMap<QString, QByteArray>& files, const QString& id) {
QStringList names;
names.append(QStringLiteral("trop%1.png").arg(id));
bool id_is_number = false;
const int id_num = id.toInt(&id_is_number);
if (id_is_number) {
names.append(QStringLiteral("trop%1.png").arg(id_num, 4, 10, QLatin1Char('0')));
}
for (const auto& name: names) {
const auto* data = FindFile(files, name);
if (data == nullptr) {
continue;
}
QPixmap pixmap;
if (pixmap.loadFromData(*data)) {
return pixmap;
}
}
return {};
}
static QString TrophyTabTitle(const QString& file_name) {
static const QRegularExpression trophy_file_re(QStringLiteral("^trophy(\\d+)\\.ucp$"),
QRegularExpression::CaseInsensitiveOption);
const auto name = QFileInfo(file_name).fileName();
const auto match = trophy_file_re.match(name);
if (match.hasMatch()) {
return QObject::tr("Trophy %1").arg(match.captured(1));
}
return QFileInfo(file_name).completeBaseName();
}
static bool BuildTrophySet(const QString& ucp_file, TrophySet& set, QString& error) {
QMap<QString, QByteArray> files;
if (!ReadUcp(ucp_file, files, error)) {
return false;
}
const auto* conf_data = FindFile(files, QStringLiteral("tropconf.json"));
if (conf_data == nullptr) {
error =
QObject::tr("%1 does not contain tropconf.json.").arg(QFileInfo(ucp_file).fileName());
return false;
}
QJsonObject tropconf;
if (!ReadJsonObject(*conf_data, QStringLiteral("tropconf.json"), tropconf, error)) {
return false;
}
const auto default_language =
JsonString(tropconf.value(QStringLiteral("defaultLanguage"))).trimmed();
const QByteArray* meta_data = nullptr;
if (!default_language.isEmpty()) {
meta_data = FindFile(files, QStringLiteral("tropmeta_%1.json").arg(default_language));
}
if (meta_data == nullptr) {
meta_data = FindFirstTrophyMetadataFile(files);
}
if (meta_data == nullptr) {
error = QObject::tr("%1 does not contain readable trophy metadata.")
.arg(QFileInfo(ucp_file).fileName());
return false;
}
QJsonObject tropmeta;
if (!ReadJsonObject(*meta_data, QStringLiteral("tropmeta.json"), tropmeta, error)) {
return false;
}
const auto definitions = ReadDefinitions(tropconf);
if (definitions.isEmpty()) {
error = QObject::tr("%1 does not define any trophies.").arg(QFileInfo(ucp_file).fileName());
return false;
}
const auto texts = ReadMetadata(tropmeta);
set.tab_title = TrophyTabTitle(ucp_file);
for (const auto& def: definitions) {
const auto text = texts.value(def.id);
TrophyRow row;
row.id = def.id;
row.grade = def.grade;
row.hidden = def.hidden;
row.has_reward = def.has_reward;
row.reward = text.reward;
row.name = text.name;
row.detail = text.detail;
row.icon = LoadTrophyIcon(files, def.id);
if (row.name.isEmpty()) {
row.name =
row.hidden ? QObject::tr("Hidden Trophy") : QObject::tr("Trophy %1").arg(def.id);
}
if (row.detail.isEmpty() && row.hidden) {
row.detail = QObject::tr("This trophy is hidden.");
}
set.trophies.append(row);
}
return true;
}
static const QRegularExpression TrophyFilePattern(QStringLiteral("^trophy(\\d+)\\.ucp$"));
static QStringList FindTrophyFiles(const Configuration* info) {
if (info == nullptr || info->basedir.isEmpty()) {
return {};
}
QStringList files;
for (const auto& file:
GameContent::ListFiles(info->basedir, QStringLiteral("sce_sys/trophy2"))) {
const auto name = QFileInfo(file).fileName();
if (name.startsWith(QStringLiteral("trophy"), Qt::CaseInsensitive) &&
name.endsWith(QStringLiteral(".ucp"), Qt::CaseInsensitive)) {
for (const auto& file: GameContent::ListFiles(
info->basedir, QString::fromLatin1(Common::Trophies::PackageDirectory))) {
if (TrophyFilePattern.match(QFileInfo(file).fileName()).hasMatch()) {
files.append(file);
}
}
files.sort(Qt::CaseInsensitive);
QStringList trophy_files;
QSet<QString> seen;
for (const auto& file: files) {
auto key = QFileInfo(file).canonicalFilePath();
if (key.isEmpty()) {
key = file;
}
key = QDir::cleanPath(key).toCaseFolded();
if (!seen.contains(key)) {
seen.insert(key);
trophy_files.append(file);
}
}
return trophy_files;
files.sort();
return files;
}
static void PrepareTable(QTableWidget* table) {
table->setColumnCount(4);
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
QObject::tr("Name"), QObject::tr("Description")});
table->setAlternatingRowColors(true);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setShowGrid(false);
table->setWordWrap(true);
table->verticalHeader()->setVisible(false);
table->verticalHeader()->setDefaultSectionSize(112);
table->horizontalHeader()->setStretchLastSection(true);
table->horizontalHeader()->setHighlightSections(false);
table->setColumnWidth(0, 100);
table->setColumnWidth(1, 132);
table->setColumnWidth(2, 260);
table->setColumnWidth(3, 480);
static QString GradeToText(int grade) {
switch (grade) {
case 1: return QObject::tr("Platinum");
case 2: return QObject::tr("Gold");
case 3: return QObject::tr("Silver");
case 4: return QObject::tr("Bronze");
default: return {};
}
}
static QTableWidgetItem* CreateItem(const QString& text) {
auto* item = new QTableWidgetItem(text);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
return item;
}
static QString TrophyTooltip(const TrophyRow& row) {
QStringList lines;
lines.append(row.name);
if (!row.detail.isEmpty()) {
lines.append(row.detail);
}
const auto grade = GradeToText(row.grade);
if (!grade.isEmpty()) {
lines.append(QObject::tr("Grade: %1").arg(grade));
}
if (row.hidden) {
static QString TrophyTooltip(const Common::Trophies::Trophy& trophy) {
QStringList lines {QString::fromStdString(trophy.name),
QString::fromStdString(trophy.description),
QObject::tr("Grade: %1").arg(GradeToText(trophy.grade))};
if (trophy.hidden) {
lines.append(QObject::tr("Hidden trophy"));
}
if (row.has_reward && !row.reward.isEmpty()) {
lines.append(QObject::tr("Reward: %1").arg(row.reward));
if (trophy.has_reward && !trophy.reward.empty()) {
lines.append(QObject::tr("Reward: %1").arg(QString::fromStdString(trophy.reward)));
}
return lines.join(QLatin1Char('\n'));
}
@@ -516,7 +87,227 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
return !FindTrophyFiles(info).isEmpty();
}
void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent) {
namespace {
using Common::Trophies::Progress;
Common::Trophies::UnlockData LoadUnlocks(const Configuration& info,
const QString& runtime_directory, const QString& file) {
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
return Common::Trophies::LoadUnlockData(Common::Trophies::UnlocksPath(
GameContent::ToPath(runtime_directory), info.title_id.toStdString(), info.user_id, label));
}
Progress GetGameProgress(const Configuration* info, const QString& runtime_directory) {
Progress result;
if (info == nullptr) {
return result;
}
const auto reader = Common::OpenArchive(GameContent::ToPath(info->basedir));
for (const auto& file: FindTrophyFiles(info)) {
const auto package =
Common::Trophies::LoadPackage(GameContent::ToPath(file), info->console_language);
const auto progress =
Common::Trophies::GetProgress(package, LoadUnlocks(*info, runtime_directory, file));
result.total += progress.total;
result.earned += progress.earned;
for (size_t grade = 1; grade < result.total_grade.size(); ++grade) {
result.total_grade[grade] += progress.total_grade[grade];
result.earned_grade[grade] += progress.earned_grade[grade];
}
}
return result;
}
QPixmap MakeCupIcon(const QColor& color, int size) {
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
const double u = size / 20.0;
painter.setPen(Qt::NoPen);
painter.setBrush(color);
QPainterPath cup;
cup.moveTo(5 * u, 2 * u);
cup.lineTo(15 * u, 2 * u);
cup.lineTo(14 * u, 9 * u);
cup.quadTo(10 * u, 13 * u, 6 * u, 9 * u);
cup.closeSubpath();
painter.drawPath(cup);
painter.setPen(QPen(color, 1.6 * u));
painter.setBrush(Qt::NoBrush);
painter.drawArc(QRectF(1.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, 180 * 16);
painter.drawArc(QRectF(13.5 * u, 3 * u, 5 * u, 5 * u), 90 * 16, -180 * 16);
painter.drawLine(QPointF(10 * u, 12 * u), QPointF(10 * u, 16 * u));
painter.setPen(Qt::NoPen);
painter.setBrush(color);
painter.drawRoundedRect(QRectF(6 * u, 16 * u, 8 * u, 2.5 * u), u, u);
return pixmap;
}
QColor GradeColor(int grade) {
switch (grade) {
case 1: return QColor(0x7fc8ff);
case 2: return QColor(0xf5c542);
case 3: return QColor(0xc4cad2);
default: return QColor(0xcd7f4f);
}
}
QWidget* MakeGradeCount(QWidget* parent, int grade, int value, int icon_size, int font_size) {
auto* box = new QWidget(parent);
auto* row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
auto* icon = new QLabel(box);
icon->setPixmap(MakeCupIcon(GradeColor(grade), icon_size));
auto* text = new QLabel(QString::number(value), box);
text->setStyleSheet(QStringLiteral("font-size: %1px;").arg(font_size));
row->addWidget(icon);
row->addWidget(text);
return box;
}
QPixmap LoadGameArt(const Configuration* game, int height) {
QPixmap art;
const auto data = GameContent::ReadFile(game->basedir, QStringLiteral("sce_sys/icon0.png"),
GameContent::MaxImageSize);
if (!data.isEmpty() && art.loadFromData(data)) {
return art.scaledToHeight(height, Qt::SmoothTransformation);
}
return {};
}
} // namespace
void TrophyViewerDialog::ShowOverview(const std::vector<const Configuration*>& games,
const QString& runtime_directory, QWidget* parent) {
QDialog dialog(parent);
dialog.setWindowTitle(QObject::tr("Trophies"));
dialog.resize(820, 640);
dialog.setStyleSheet(QStringLiteral(
"QDialog { background: palette(window); }"
"QLabel { color: palette(text); }"
"QListWidget { background: transparent; border: none; outline: none; }"
"QListWidget::item { border-bottom: 1px solid palette(mid); padding: 4px; }"
"QListWidget::item:selected, QListWidget::item:hover { background: palette(midlight); "
"border: 1px solid palette(highlight); }"
"QProgressBar { background: palette(mid); border: none; height: 4px; }"
"QProgressBar::chunk { background: palette(highlight); }"));
auto* layout = new QVBoxLayout(&dialog);
struct GameProgress {
const Configuration* game;
Progress progress;
};
std::vector<GameProgress> trophy_games;
Progress totals;
for (const auto* game: games) {
const auto progress = GetGameProgress(game, runtime_directory);
if (progress.total == 0) {
continue;
}
trophy_games.push_back({game, progress});
totals.earned += progress.earned;
for (size_t grade = 1; grade < totals.earned_grade.size(); ++grade) {
totals.earned_grade[grade] += progress.earned_grade[grade];
}
}
auto* header = new QHBoxLayout;
auto* title = new QLabel(QObject::tr("Trophies"), &dialog);
title->setStyleSheet(QStringLiteral("font-size: 30px;"));
header->addWidget(title);
header->addStretch(1);
auto* total = new QLabel(QObject::tr("Total %1").arg(totals.earned), &dialog);
total->setAlignment(Qt::AlignCenter);
total->setStyleSheet(QStringLiteral("font-size: 16px;"));
header->addWidget(total);
for (int grade = 1; grade <= 4; ++grade) {
header->addWidget(MakeGradeCount(&dialog, grade, totals.earned_grade[grade], 32, 18));
}
layout->addLayout(header);
auto* list = new QListWidget(&dialog);
list->setSelectionMode(QAbstractItemView::SingleSelection);
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
layout->addWidget(list, 1);
for (size_t index = 0; index < trophy_games.size(); ++index) {
const auto& [game, progress] = trophy_games[index];
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(760, 112));
item->setData(Qt::UserRole, static_cast<int>(index));
auto* row_widget = new QWidget(list);
row_widget->setAttribute(Qt::WA_TransparentForMouseEvents);
auto* row = new QHBoxLayout(row_widget);
auto* art = new QLabel(row_widget);
art->setFixedSize(80, 80);
art->setAlignment(Qt::AlignCenter);
const auto pixmap = LoadGameArt(game, 80);
if (!pixmap.isNull()) {
art->setPixmap(pixmap);
} else {
art->setStyleSheet(QStringLiteral("background: palette(mid);"));
}
row->addWidget(art);
const auto name = !game->name.isEmpty() ? game->name : game->title_id;
auto* text = new QLabel(QStringLiteral("%1\n%2").arg(name, game->title_id), row_widget);
text->setStyleSheet(QStringLiteral("font-size: 18px;"));
row->addWidget(text, 1);
auto* percent_box = new QVBoxLayout;
percent_box->setSpacing(2);
percent_box->setContentsMargins(0, 0, 0, 0);
auto* percent = new QLabel(QStringLiteral("%1%").arg(progress.Percentage()), row_widget);
percent->setAlignment(Qt::AlignRight);
percent->setStyleSheet(QStringLiteral("font-size: 24px;"));
percent->setMinimumHeight(34);
auto* earned = new QLabel(
QObject::tr("Earned %1/%2").arg(progress.earned).arg(progress.total), row_widget);
earned->setAlignment(Qt::AlignRight);
earned->setMinimumHeight(18);
auto* bar = new QProgressBar(row_widget);
bar->setRange(0, 100);
bar->setValue(progress.Percentage());
bar->setTextVisible(false);
bar->setFixedWidth(130);
percent_box->addWidget(percent);
percent_box->addWidget(earned);
percent_box->addWidget(bar);
row->addLayout(percent_box);
for (int grade = 1; grade <= 4; ++grade) {
if (grade == 1 && progress.total_grade[1] == 0) {
continue;
}
row->addWidget(MakeGradeCount(row_widget, grade, progress.earned_grade[grade], 24, 18));
}
list->setItemWidget(item, row_widget);
}
if (trophy_games.empty()) {
auto* empty =
new QLabel(QObject::tr("No games with trophies found in the game folders."), &dialog);
empty->setAlignment(Qt::AlignCenter);
layout->insertWidget(1, empty);
list->hide();
}
connect(list, &QListWidget::itemClicked, &dialog,
[&dialog, &trophy_games, &runtime_directory](QListWidgetItem* item) {
const auto index = item->data(Qt::UserRole).toInt();
if (index >= 0 && static_cast<size_t>(index) < trophy_games.size()) {
ShowForGame(trophy_games[static_cast<size_t>(index)].game, runtime_directory,
&dialog);
}
});
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttons);
dialog.exec();
}
void TrophyViewerDialog::ShowForGame(const Configuration* info, const QString& runtime_directory,
QWidget* parent) {
if (info == nullptr) {
return;
}
@@ -527,7 +318,7 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
}
QString error;
if (!dlg.LoadGame(*info, error)) {
if (!dlg.LoadGame(*info, runtime_directory, error)) {
QMessageBox::warning(parent, tr("Trophy Viewer"), error);
return;
}
@@ -535,66 +326,151 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
dlg.exec();
}
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
bool TrophyViewerDialog::LoadGame(const Configuration& info, const QString& runtime_directory,
QString& error) {
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
const auto trophy_files = FindTrophyFiles(&info);
if (trophy_files.isEmpty()) {
error = tr("No trophy package found in sce_sys/trophy2.");
error = tr("No trophy package found in %1.")
.arg(QString::fromLatin1(Common::Trophies::PackageDirectory));
return false;
}
QStringList errors;
for (const auto& file: trophy_files) {
TrophySet set;
QString set_error;
if (!BuildTrophySet(file, set, set_error)) {
errors.append(set_error);
const auto package =
Common::Trophies::LoadPackage(GameContent::ToPath(file), info.console_language);
if (package.trophies.empty()) {
errors.append(tr("Could not read trophy package %1.").arg(QFileInfo(file).fileName()));
continue;
}
const auto unlocks = LoadUnlocks(info, runtime_directory, file);
const auto counts = Common::Trophies::GetProgress(package, unlocks);
auto* page = new QWidget(m_tabs);
auto* page_layout = new QVBoxLayout(page);
auto* table = new QTableWidget(set.trophies.size(), 4, m_tabs);
PrepareTable(table);
for (int row_index = 0; row_index < set.trophies.size(); row_index++) {
const auto& row = set.trophies.at(row_index);
table->setRowHeight(row_index, 112);
auto* status = CreateItem(tr("locked"));
status->setTextAlignment(Qt::AlignCenter);
if (row.hidden) {
status->setForeground(QBrush(Qt::gray));
}
table->setItem(row_index, 0, status);
auto* icon_label = new QLabel(table);
icon_label->setAlignment(Qt::AlignCenter);
icon_label->setMinimumSize(QSize(108, 108));
if (!row.icon.isNull()) {
icon_label->setPixmap(
row.icon.scaled(QSize(96, 96), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
table->setCellWidget(row_index, 1, icon_label);
auto* name = CreateItem(row.name);
auto font = name->font();
font.setBold(true);
name->setFont(font);
name->setToolTip(TrophyTooltip(row));
table->setItem(row_index, 2, name);
auto* detail = CreateItem(row.detail);
detail->setToolTip(TrophyTooltip(row));
table->setItem(row_index, 3, detail);
auto* header = new QHBoxLayout;
auto* art = new QLabel(page);
if (const auto pixmap = LoadGameArt(&info, 56); !pixmap.isNull()) {
art->setPixmap(pixmap);
header->addWidget(art);
}
auto* title = new QLabel(QString::fromStdString(package.title), page);
title->setStyleSheet(QStringLiteral("font-size: 28px;"));
header->addWidget(title, 1);
const auto make_stat = [page](const QString& label, const QString& value) {
auto* box = new QVBoxLayout;
auto* name = new QLabel(label, page);
auto* number = new QLabel(value, page);
name->setStyleSheet(QStringLiteral("font-size: 14px;"));
number->setStyleSheet(QStringLiteral("font-size: 22px;"));
box->addWidget(name);
box->addWidget(number);
return box;
};
header->addLayout(
make_stat(tr("Progress"), QStringLiteral("%1%").arg(counts.Percentage())));
header->addLayout(
make_stat(tr("Earned"), QStringLiteral("%1/%2").arg(counts.earned).arg(counts.total)));
for (int grade = 1; grade <= 4; ++grade) {
if (counts.total_grade[grade] > 0) {
header->addWidget(MakeGradeCount(page, grade, counts.earned_grade[grade], 30, 20));
}
}
page_layout->addLayout(header);
auto* progress = new QProgressBar(page);
progress->setRange(0, 100);
progress->setValue(counts.Percentage());
progress->setTextVisible(false);
page_layout->addWidget(progress);
auto* count_label = new QLabel(tr("All trophies: %1").arg(counts.total), page);
page_layout->addWidget(count_label);
m_tabs->addTab(table, set.tab_title);
auto* list = new QListWidget(page);
list->setSelectionMode(QAbstractItemView::NoSelection);
list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
list->setSpacing(4);
page_layout->addWidget(list, 1);
for (const auto& [id, trophy]: package.trophies) {
const bool trophy_unlocked = unlocks.unlocked.contains(id);
const auto hidden_locked = trophy.hidden && !trophy_unlocked;
auto* item = new QListWidgetItem(list);
item->setSizeHint(QSize(900, 96));
auto* card = new QWidget(list);
card->setStyleSheet(
QStringLiteral("QWidget#card { background: palette(base); border-radius: 12px; }"));
card->setObjectName(QStringLiteral("card"));
auto* row = new QHBoxLayout(card);
auto* icon = new QLabel(card);
icon->setFixedSize(76, 76);
icon->setAlignment(Qt::AlignCenter);
QPixmap pixmap;
if (hidden_locked) {
pixmap = QPixmap(QStringLiteral(":/icons/hidden-trophy.png"));
} else if (pixmap.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
static_cast<uint>(trophy.icon_png.size())) &&
!trophy_unlocked) {
pixmap =
QPixmap::fromImage(pixmap.toImage().convertToFormat(QImage::Format_Grayscale8));
}
if (!pixmap.isNull()) {
icon->setPixmap(
pixmap.scaled(76, 76, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
row->addWidget(icon);
auto* left = new QVBoxLayout;
const auto name_text = hidden_locked ? tr("Hidden trophy")
: trophy.name.empty() ? tr("Trophy %1").arg(id)
: QString::fromStdString(trophy.name);
auto* name = new QLabel(name_text, card);
name->setStyleSheet(QStringLiteral("font-size: 20px; font-weight: bold;"));
left->addWidget(name);
if (!hidden_locked) {
auto* grade_row = new QHBoxLayout;
auto* cup = new QLabel(card);
cup->setPixmap(MakeCupIcon(GradeColor(trophy.grade), 18));
auto* grade_text = new QLabel(GradeToText(trophy.grade), card);
grade_text->setStyleSheet(QStringLiteral("font-size: 15px;"));
grade_row->addWidget(cup);
grade_row->addWidget(grade_text);
grade_row->addStretch(1);
left->addLayout(grade_row);
}
row->addLayout(left, 2);
auto* right = new QVBoxLayout;
QString date_text;
if (const auto found = unlocks.timestamps.find(id); found != unlocks.timestamps.end()) {
date_text = QDateTime::fromMSecsSinceEpoch(
static_cast<qint64>(
(found->second - Common::Trophies::UnixEpochTick) / 1000))
.toLocalTime()
.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
} else if (trophy_unlocked) {
date_text = tr("Earned");
}
auto* date = new QLabel(date_text, card);
date->setAlignment(Qt::AlignRight);
date->setStyleSheet(QStringLiteral("font-size: 15px;"));
auto* detail = new QLabel(
hidden_locked ? QString {} : QString::fromStdString(trophy.description), card);
detail->setWordWrap(true);
detail->setAlignment(Qt::AlignLeft | Qt::AlignTop);
detail->setStyleSheet(QStringLiteral("font-size: 17px;"));
right->addWidget(date);
right->addWidget(detail, 1);
row->addLayout(right, 3);
card->setToolTip(hidden_locked ? name_text : TrophyTooltip(trophy));
list->setItemWidget(item, card);
}
m_tabs->addTab(page, QString::fromStdString(package.title));
}
m_tabs->tabBar()->setVisible(m_tabs->count() > 1);
if (m_tabs->count() == 0) {
error = errors.isEmpty() ? tr("No readable trophy data found.")
: errors.join(QLatin1Char('\n'));
error = errors.join(QLatin1Char('\n'));
return false;
}
return true;
}
+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;
}
+62 -1141
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+7
View File
@@ -84,6 +84,8 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
@@ -421,6 +423,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--trophy-notifications") {
if (!ParseBool(value, options.config.trophy_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+47
View File
@@ -28,6 +28,46 @@ void CheckConcurrentResult(int result, const char* text) {
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
}
void TestLevelUserEventDelivery() {
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
Check(EventQueue::KernelCreateEqueue(&queue, "level-user-events") == OK, "create user event queue");
Check(EventQueue::KernelAddUserEvent(queue, 1) == OK &&
EventQueue::KernelAddUserEvent(queue, 2) == OK &&
EventQueue::KernelAddUserEventEdge(queue, 3) == OK, "add level and edge events");
for (int id = 1; id <= 3; ++id)
Check(EventQueue::KernelTriggerUserEvent(queue, id, reinterpret_cast<void*>(uintptr_t(id))) == OK,
"trigger distinct user events");
EventQueue::KernelEvent events[16] {};
Libs::LibKernel::KernelUseconds timeout = 0;
int out = 0;
const auto find_event = [&](uintptr_t id) {
return std::find_if(events, events + out, [=](const auto& event) { return event.ident == id; });
};
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 3,
"level events are returned once per wait without starving following events");
for (uintptr_t id = 1; id <= 3; ++id) {
const auto* event = find_event(id);
Check(event != events + out && event->data == static_cast<intptr_t>(id) &&
event->udata == reinterpret_cast<void*>(id), "distinct event payloads");
}
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 2 &&
find_event(1) != events + out && find_event(2) != events + out,
"level persists while edge clears");
Check(EventQueue::KernelDeleteUserEvent(queue, 1) == OK, "delete first level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 2, "remaining level event is delivered only once");
Check(EventQueue::KernelDeleteUserEvent(queue, 2) == OK, "delete remaining level event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"deleted levels and consumed edge do not reappear");
Check(EventQueue::KernelTriggerUserEvent(queue, 3, nullptr) == OK, "retrigger edge event");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == OK && out == 1 &&
events[0].ident == 3, "retriggered edge is delivered once");
Check(EventQueue::KernelWaitEqueue(queue, events, 16, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"retriggered edge clears after delivery");
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete user event queue");
}
void TestPeriodicTimerEvents() {
using namespace std::chrono_literals;
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
@@ -188,6 +228,12 @@ void TestDuplicateAddPreservesEventState() {
"duplicate add updates deadline metadata");
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
"deadline trigger retains updated duplicate metadata");
for (uintptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
reinterpret_cast<void*>(value)) == OK, "queue bounded read payload");
for (intptr_t value: {0x9abcu, 0xdef0u})
Check(EventQueue::KernelWaitEqueue(queue, events, 1, &out, &timeout) == OK && out == 1 &&
events[0].data == value, "capacity-one reads preserve pending event order");
auto retained_owner = weak_original.lock();
Check(retained_owner != nullptr, "original owner alive before delete");
@@ -495,6 +541,7 @@ void TestConcurrentDelete() {
} // namespace
int main() {
TestLevelUserEventDelivery();
TestPeriodicTimerEvents();
TestDuplicateAddPreservesEventState();
TestCallbackStateOutlivesPort();
+58 -9
View File
@@ -6,6 +6,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
namespace Libs::LibHttp {
void InitNet_1_Http(Loader::SymbolDatabase *symbols);
@@ -40,14 +41,14 @@ int failures = 0;
using HttpUriParse = int(KYTY_SYSV_ABI *)(SceHttpUriElement *, const char *,
void *, size_t *, size_t);
HttpUriParse GetHttpUriParse() {
Loader::SymbolDatabase symbols;
Libs::LibHttp::InitNet_1_Http(&symbols);
const auto *record =
symbols.FindByNid("IWalAn-guFs", Loader::SymbolType::Func);
using HttpUriBuild = int(KYTY_SYSV_ABI *)(char *, size_t *, size_t,
const SceHttpUriElement *, uint32_t);
template <typename T>
T GetHttpFunction(const Loader::SymbolDatabase &symbols, const char *nid) {
const auto *record = symbols.FindByNid(nid, Loader::SymbolType::Func);
CHECK(record != nullptr);
return record != nullptr ? reinterpret_cast<HttpUriParse>(record->vaddr)
: nullptr;
return record != nullptr ? reinterpret_cast<T>(record->vaddr) : nullptr;
}
template <size_t N>
@@ -117,13 +118,61 @@ void TestPresentQuery(HttpUriParse parse) {
}
}
std::string BuildWith(HttpUriBuild build, const SceHttpUriElement &element,
uint32_t option) {
size_t required = 0;
CHECK(build(nullptr, &required, 0, &element, option) == 0);
std::array<char, 256> out{};
size_t filled = 0;
CHECK(required <= out.size());
CHECK(build(out.data(), &filled, required, &element, option) == 0);
CHECK(filled == required);
return std::string(out.data());
}
void TestBuildHonoursOption(HttpUriParse parse, HttpUriBuild build) {
constexpr char url[] =
"https://user:pw@gssdk1.gamesci.com.cn:8443/VersionServerImpl?x=1#frag";
size_t required = 0;
CHECK(parse(nullptr, url, nullptr, &required, 0) == 0);
std::array<char, 256> pool{};
SceHttpUriElement element{};
CHECK(parse(&element, url, pool.data(), &required, required) == 0);
constexpr uint32_t scheme = 0x01, hostname = 0x02, port = 0x04, path = 0x08,
username = 0x10, password = 0x20, query = 0x40,
fragment = 0x80;
CHECK(BuildWith(build, element, scheme) == "https://");
CHECK(BuildWith(build, element, hostname) == "gssdk1.gamesci.com.cn");
CHECK(BuildWith(build, element, port) == "8443");
CHECK(BuildWith(build, element, path) == "/VersionServerImpl");
CHECK(BuildWith(build, element, username) == "user");
CHECK(BuildWith(build, element, password) == "pw");
CHECK(BuildWith(build, element, query) == "?x=1");
CHECK(BuildWith(build, element, fragment) == "#frag");
CHECK(BuildWith(build, element, scheme | hostname) ==
"https://gssdk1.gamesci.com.cn");
CHECK(BuildWith(build, element, hostname | path) ==
"gssdk1.gamesci.com.cn/VersionServerImpl");
CHECK(BuildWith(build, element, hostname | port) ==
"gssdk1.gamesci.com.cn:8443");
CHECK(BuildWith(build, element, 0xff) == url);
CHECK(BuildWith(build, element, 0) == url);
}
} // namespace
int main() {
const auto parse = GetHttpUriParse();
if (parse == nullptr) {
Loader::SymbolDatabase symbols;
Libs::LibHttp::InitNet_1_Http(&symbols);
const auto parse = GetHttpFunction<HttpUriParse>(symbols, "IWalAn-guFs");
const auto build = GetHttpFunction<HttpUriBuild>(symbols, "5LZA+KPISVA");
if (parse == nullptr || build == nullptr) {
return 1;
}
TestBuildHonoursOption(parse, build);
TestAbsentQuery(parse);
TestEmptyUri(parse);
TestPresentQuery(parse);
+7 -41
View File
@@ -32,8 +32,7 @@ AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) {
return *result;
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address,
bool buffer = false) {
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@@ -42,22 +41,16 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address,
auto &value_block = AddValueBlock(program);
MemoryInfo memory;
memory.kind = buffer ? ResourceKind::ScalarBuffer : ResourceKind::ScalarAddress;
memory.kind = ResourceKind::ScalarAddress;
memory.planning_only = true;
program.memory_info.push_back(memory);
const auto low = Value(static_cast<uint32_t>(address));
const auto high = Value(static_cast<uint32_t>(address >> 32u));
auto &handle = buffer
? value_block.AppendNewInst(ValueOpcode::GetBufferResource,
{low, high, Value(4u), Value(0u)})
: value_block.AppendNewInst(ValueOpcode::GetAddressResource,
{low, high});
auto &raw = buffer
? value_block.AppendNewInst(ValueOpcode::ReadConstBuffer,
{Value(&handle), Value(0u)})
: value_block.AppendNewInst(
ValueOpcode::LoadAddressU32,
{Value(&handle), Value(0u), Value(0u), Value(true)});
auto &handle =
value_block.AppendNewInst(ValueOpcode::GetAddressResource, {low, high});
auto &raw = value_block.AppendNewInst(
ValueOpcode::LoadAddressU32,
{Value(&handle), Value(0u), Value(0u), Value(true)});
raw.SetFlags(MemoryFlags{.index = 0, .pc = 0x40});
program.srt_reads.push_back({Value(&raw), 0});
@@ -175,32 +168,6 @@ void TestMappedSrtUsesDirectReaderByDefault() {
"cache rematerialization did not use the direct reader by default");
}
void TestRawScalarAddressPreservesHighBits() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
constexpr uint64_t address = 0xabcd000156780003ull;
for (const bool buffer : {false, true}) {
auto plan = SrtPlan(address, buffer);
uint64_t observed = 0;
const SrtRuntime runtime{
.read_memory = [](void *userdata, uint64_t requested,
std::span<uint32_t> words) {
if (words.size() != 1) return false;
*static_cast<uint64_t *>(userdata) = requested;
words[0] = 0x12345678;
return true;
},
.userdata = &observed};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"scalar address materialization failed");
Check(observed == (buffer ? 0x0000000156780000ull : address & ~uint64_t{3}),
"raw pointer high bits and buffer descriptor flags were conflated");
Check(snapshot.flattened_srt == std::vector<uint32_t>{0x12345678},
"scalar address read did not reach its flattened slot");
}
}
void TestIntegerRuntimeValueFollowsSrtReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = SrtPlan(0x10000);
@@ -599,7 +566,6 @@ void DbgExit(int) { std::abort(); }
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestRawScalarAddressPreservesHighBits();
TestIntegerRuntimeValueFollowsSrtReads();
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
+294 -388
View File
@@ -206,9 +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) {
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;
@@ -216,58 +215,44 @@ 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;
material_scalar.offset = 4u;
const auto key =
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
fixture->AddMemory(material_scalar, 0x10d8));
const auto heap_offset =
fixture->Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)});
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;
for (uint32_t dword = 0; dword < image_words.size(); dword++) {
auto component = heap_scalar;
component.offset = dword * sizeof(uint32_t);
component.offset = table_offset + dword * sizeof(uint32_t);
if (malformed && dword == image_words.size() - 1u) {
component.offset += sizeof(uint32_t);
}
@@ -275,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);
@@ -291,294 +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");
// 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);
}
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() {
@@ -669,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,
@@ -967,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");
@@ -1322,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() &&
@@ -1529,6 +1504,9 @@ void TestUniformScalarBufferImage() {
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
Check(std::ranges::none_of(plan.descriptor_sources, [](const DescriptorSource& source) {
return source.indirect_descriptor.has_value();
}), "draw-uniform scalar image was expanded to GPU-selected candidates");
std::array<uint32_t, 9> user_data{0x1000u, 0u, 0x100u, 0u,
0x2000u, 0u, 0x100u, 0u, 0u};
LinearTestMemory memory;
@@ -3053,79 +3031,6 @@ ResourcePlan ConditionalBufferPlan(ConditionalBufferUse use) {
return ExtractResourcePlan(fixture.program);
}
void TestNativeScalarAtomicDataDoesNotEnterResourcePlan() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
Fixture fixture;
auto *entry = fixture.block;
for (uint32_t i = 1; i < 5; ++i) fixture.AddBlock();
fixture.program.block_info[0].terminator = {
.kind = CFG::TerminatorKind::ConditionalBranch, .true_block = 1, .false_block = 4};
for (uint32_t i = 1; i < 4; ++i) {
fixture.program.block_info[i].terminator = {
.kind = CFG::TerminatorKind::Branch, .true_block = i + 1};
}
fixture.program.block_info[4].terminator.kind = CFG::TerminatorKind::Return;
const auto table = fixture.Address(fixture.UserData(0), fixture.UserData(1));
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarAddress;
scalar.component_count = 2;
scalar.offset = 136;
const auto low = fixture.Emit(ValueOpcode::LoadAddressU32,
{table, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 0x560));
scalar.offset = 140;
scalar.component_index = 1;
const auto high = fixture.Emit(ValueOpcode::LoadAddressU32,
{table, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 0x560));
const auto address = fixture.Address(low, high);
scalar.offset = 24;
scalar.component_count = 1;
scalar.component_index = 0;
const auto data_flags = fixture.AddMemory(scalar, 0x584);
const auto data = fixture.Emit(ValueOpcode::LoadAddressU32,
{address, Value(0u), Value(0u), Value(true)}, data_flags);
const auto descriptor = fixture.Buffer({low,
fixture.Emit(ValueOpcode::BitwiseOr32, {high, Value(256u << 16u)}),
Value(1u), Value(90628u)}, 0x58c);
MemoryInfo vector;
vector.kind = ResourceKind::Buffer;
vector.offset = 24;
vector.data_dwords = 2;
fixture.Emit(ValueOpcode::BufferAtomicOr64,
{descriptor, Value(0u), Value(0u), Value(0u), Value(uint64_t{1}), Value(true)},
fixture.AddMemory(vector, 0x58c));
const auto flag = fixture.Emit(ValueOpcode::BitwiseAnd32, {data, Value(16u)});
fixture.program.block_info[0].condition =
fixture.Emit(ValueOpcode::INotEqual32, {flag, Value(0u)});
fixture.PlanAndTrack();
Check(data.Instruction()->Parent() == entry &&
!fixture.program.memory_info[data_flags.index].planning_only &&
fixture.program.info.buffers.size() == 1 && fixture.program.info.buffers[0].atomic,
"mutable scalar data was replaced by a host snapshot before its native atomic write");
auto plan = ExtractResourcePlan(fixture.program);
Check(plan.srt_reads.size() == 2 && plan.control_flow.empty() &&
!plan.capture_specialization_reads,
"resource-free successor chain retained the shader's scalar data predicate");
struct Reads { uint32_t descriptors = 0; uint32_t data = 0; } reads;
const auto Read = +[](void *userdata, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(userdata);
if (address == 0x2018u) { ++reads.data; return false; }
if (words.size() != 1 || (address != 0x1088u && address != 0x108cu)) return false;
++reads.descriptors;
words[0] = address == 0x1088u ? 0x2000u : 0u;
return true;
};
const std::array<uint32_t, 2> user_data{0x1000u, 0u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = Read, .userdata = &reads,
.read_specialization_memory = Read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.descriptors == 2 && reads.data == 0 && snapshot.specialization_reads.empty() &&
snapshot.buffers[0].dwords[0] == 0x2000u,
"resource materialization read mutable scalar data or lost its descriptor pointer");
}
void TestConditionalBufferMaterialization() {
auto plan = ConditionalBufferPlan(ConditionalBufferUse::Optional);
// GTA III leaves packet words in s[12:15] when its scalar control word is zero.
@@ -3667,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);
@@ -3685,7 +3592,6 @@ int main() {
Run("DMA address materialization", TestDmaAddressMaterialization);
Run("dynamic FLAT address", TestDynamicFlatAddressesUseDma);
Run("buffer swizzle specialization", TestBufferSwizzleSpecialization);
Run("native scalar atomic data", TestNativeScalarAtomicDataDoesNotEnterResourcePlan);
Run("conditional buffer materialization", TestConditionalBufferMaterialization);
Run("guarded scalar descriptor reads", TestGuardedScalarDescriptorReads);
Run("conservative buffer reachability", TestConservativeBufferReachability);
File diff suppressed because it is too large. Load diff
+261
View File
@@ -0,0 +1,261 @@
#include "common/file.h"
#include "common/trophies.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace {
namespace Trophies = Common::Trophies;
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "TrophySystemTests: failed: %s\n", text);
std::abort();
}
}
class TempDirectory {
public:
TempDirectory() {
const auto unique =
std::chrono::steady_clock::now().time_since_epoch().count();
m_path = std::filesystem::temp_directory_path() /
("kyty_trophy_test_" + std::to_string(unique));
Check(std::filesystem::create_directories(m_path),
"create temporary directory");
}
~TempDirectory() {
std::error_code error;
std::filesystem::remove_all(m_path, error);
}
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
private:
std::filesystem::path m_path;
};
void WriteBigEndian(std::vector<char> &bytes, size_t offset, uint64_t value,
size_t length) {
Check(offset <= bytes.size() && length <= bytes.size() - offset,
"write within package");
for (size_t i = 0; i < length; ++i) {
bytes[offset + length - 1 - i] = static_cast<char>(value & 0xff);
value >>= 8;
}
}
void WritePackage(const std::filesystem::path &path,
const std::map<std::string, std::string> &files) {
size_t offset = 0x40 + files.size() * 0x40;
size_t size = offset;
for (const auto &[name, contents] : files)
size += contents.size();
std::vector<char> bytes(size);
WriteBigEndian(bytes, 0, 0xb228c60a, 4);
WriteBigEndian(bytes, 4, 1, 4);
WriteBigEndian(bytes, 8, bytes.size(), 8);
WriteBigEndian(bytes, 0x10, files.size(), 4);
WriteBigEndian(bytes, 0x14, 0x20, 4);
size_t entry = 0x40;
for (const auto &[name, contents] : files) {
Check(name.size() < 0x20, "fixture file name fits");
std::memcpy(bytes.data() + entry, name.data(), name.size());
WriteBigEndian(bytes, entry + 0x20, offset, 8);
WriteBigEndian(bytes, entry + 0x28, contents.size(), 8);
std::memcpy(bytes.data() + offset, contents.data(), contents.size());
offset += contents.size();
entry += 0x40;
}
Common::File file;
Check(file.Create(path), "create package fixture");
uint32_t written = 0;
file.Write(bytes.data(), static_cast<uint32_t>(bytes.size()), &written);
Check(written == bytes.size(), "write package fixture");
}
Trophies::Package MakePackage(const std::filesystem::path &directory) {
const auto path = directory / "trophy00.ucp";
WritePackage(
path,
{{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[
{"id":"0","grade":"P"},
{"id":"1","grade":"G","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}},
{"id":"2","grade":"S","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100","progressive":false}},
{"id":"3","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5","progressive":false}},
{"id":"4","grade":"B","unlockCondition":{"udsStatId":"10","comparator":"lt","targetValue":"-1","progressive":false}}
]})"},
{"tropmeta_en-US.json",
R"({"metadata":{"titleMetadata":{"name":"Test game"},"trophyMetadata":[
{"id":"1","name":"Find All Coins","detail":"Find all coins in the level."}
]}})"}});
return Trophies::LoadPackage(path, 1);
}
void TestEventExtraction(const std::filesystem::path &directory) {
auto package = MakePackage(directory);
Check(package.trophies.size() == 5,
"unsupported signed target does not discard trophy metadata");
Check(package.trophies.at(1).uds_stat_id == 10 &&
package.trophies.at(1).target == 10 &&
!package.trophies.at(1).progressive,
"non-progressive trophy retains its stat target");
Check(!package.trophies.at(4).target,
"negative target is not converted to unsigned");
Check(Trophies::FindUdsTrophies(package, "FIND_ALL_COINS",
{{"counter", uint64_t{100}}})
.empty(),
"missing mapping never guesses from trophy text");
const auto path = directory / "uds00.ucp";
WritePackage(path, {{"stats_definition.json", R"({"statDefinitionArray":[
{"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"},
{"statId":20,"dataType":"uint64","aggregation":"latest"},
{"statId":30,"dataType":"uint32","aggregation":"latest"},
{"statId":40,"dataType":"uint64","aggregation":"sum"},
{"statId":50,"dataType":"uint64","aggregation":"count"},
{"statId":60,"dataType":"uint64","aggregation":"max"},
{"statId":70,"dataType":"uint64","aggregation":"latest","maxValue":"3"}
]})"},
{"stats_extraction.json", R"({"statsExtractionRuleArray":[
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}},
{"condition":{"eventName":"COLLECT"},"action":{"input":"$.counterBase","output":{"statId":20}}},
{"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}},
{"condition":{"eventName":"FILTERED","property":{"path":"$.level","comparator":"==","value":1}},"action":{"input":"$.counter","output":{"statId":10}}},
{"condition":{"eventName":"NESTED"},"action":{"input":"$.player.counter","output":{"statId":10}}},
{"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":40}}},
{"condition":{"eventName":"COUNT"},"action":{"output":{"statId":50}}},
{"condition":{"eventName":"MAX"},"action":{"input":"$.counter","output":{"statId":60}}},
{"condition":{"eventName":"BOUNDED"},"action":{"input":"$.counter","output":{"statId":70}}},
{"bad":1}
]})"}});
package.event_rules = Trophies::LoadUdsRules(path);
Check(package.event_rules.size() == 2,
"skip unsupported extraction and aggregation");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{9}}, {"counterBase", uint64_t{100}}})
.empty(),
"do not add independent counters or treat gt as ge");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{100}}}) ==
std::vector<int>{1},
"each stat reads its configured property");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{0}}, {"counterBase", uint64_t{101}}}) ==
std::vector<int>{2},
"strict comparison requires greater value");
Check(Trophies::FindUdsTrophies(
package, "COLLECT",
{{"counter", uint64_t{10}}, {"counterBase", uint64_t{101}}}) ==
std::vector<int>({1, 2}),
"one event can unlock multiple mapped trophies");
Check(Trophies::FindUdsTrophies(
package, "TIME", {{"seconds", uint32_t{5}}}) == std::vector<int>{3},
"less-equal condition accepts its boundary");
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{6}}})
.empty(),
"less-equal condition rejects larger input");
Check(Trophies::FindUdsTrophies(package, "TIME", {{"counter", uint64_t{0}}})
.empty(),
"missing property is not zero");
package.trophies.at(3).comparison = Trophies::Comparison::Less;
Check(Trophies::FindUdsTrophies(package, "TIME", {{"seconds", uint32_t{5}}})
.empty(),
"strict less rejects its boundary");
Check(Trophies::FindUdsTrophies(
package, "TIME", {{"seconds", uint32_t{4}}}) == std::vector<int>{3},
"strict less accepts lower input");
package.trophies.at(3).comparison = Trophies::Comparison::GreaterEqual;
Check(Trophies::FindUdsTrophies(package, "TIME",
{{"seconds", uint64_t{UINT32_MAX} + 1}})
.empty(),
"uint64 input cannot update a uint32 stat");
Check(
Trophies::FindUdsTrophies(package, "COLLECT", {{"counter", int32_t{100}}})
.empty(),
"signed inputs cannot update unsigned stats");
Check(Trophies::FindUdsTrophies(package, "COLLECT",
{{"counter", uint32_t{100}}})
.empty(),
"unsigned input width must match the stat");
package.trophies.at(1).comparison = Trophies::Comparison::None;
Check(Trophies::FindUdsTrophies(package, "COLLECT",
{{"counter", uint64_t{100}}})
.empty(),
"unknown comparator cannot unlock");
WritePackage(path, {{"stats_extraction.json", "{}"}});
Check(Trophies::LoadUdsRules(path).empty(),
"missing stat definitions cannot infer aggregation");
Check(Trophies::LoadUdsRules(directory / "missing.ucp").empty(),
"missing UDS package has no rules");
}
void TestProgress(const std::filesystem::path &directory) {
const auto package = MakePackage(directory);
Trophies::UnlockData unlocks;
unlocks.unlocked = {0, 1, 999};
const auto progress = Trophies::GetProgress(package, unlocks);
Check(progress.total == 5 && progress.earned == 2,
"ignore unlock IDs absent from package");
Check(progress.earned_grade[1] == 1 && progress.earned_grade[2] == 1 &&
progress.Percentage() == 60,
"percentage uses 6:2:1 grade points and excludes platinum");
const auto group = Trophies::GetProgress(package, unlocks, 1);
Check(group.total == 1 && group.earned == 0 && group.Percentage() == 0,
"group counts filter trophies");
Check(Trophies::GetProgress({}, {}).Percentage() == 0,
"empty package progress is zero");
}
void TestUnlockPersistence(const std::filesystem::path &directory) {
const auto path = Trophies::UnlocksPath(directory, "PPSA00001", 1000, 0);
Check(path == directory / "_Trophies/PPSA00001/trophies_1000_0.json",
"store trophies outside save data");
Check(Trophies::UnlocksPath(directory, "../bad", 1000, 0).empty(),
"reject unsafe title IDs");
Check(Trophies::LoadUnlockData(path).unlocked.empty(),
"missing unlock file loads as empty");
Trophies::UnlockData unlocks;
unlocks.unlocked = {4, 42};
const auto tick = Trophies::UnixEpochTick + 1791288000123456ULL;
unlocks.timestamps.emplace(42, tick);
Check(Trophies::SaveUnlockData(path, unlocks), "save timestamped unlocks");
auto loaded = Trophies::LoadUnlockData(path);
Check(loaded.unlocked == unlocks.unlocked &&
loaded.timestamps == unlocks.timestamps,
"persist full UTC microsecond timestamp");
loaded.unlocked.insert(43);
loaded.timestamps.emplace(43, tick + 1000000);
Check(Trophies::SaveUnlockData(path, loaded), "replace existing unlock file");
Check(Trophies::LoadUnlockData(path).timestamps.at(42) == tick,
"retain original unlock time");
const auto blocked = directory / "blocked.json";
std::filesystem::create_directory(blocked);
Common::File sentinel;
Check(sentinel.Create(blocked / "keep"),
"create replacement failure fixture");
sentinel.Close();
Check(!Trophies::SaveUnlockData(blocked, loaded) &&
std::filesystem::exists(blocked / "keep"),
"failed replacement leaves destination intact");
Check(!std::filesystem::exists(directory / "blocked.json.tmp"),
"failed replacement removes temporary file");
}
} // namespace
int main() {
TempDirectory directory;
TestEventExtraction(directory.Path());
TestProgress(directory.Path());
TestUnlockPersistence(directory.Path());
std::printf("TrophySystemTests: all cases passed\n");
return 0;
}
+198 -68
View File
@@ -3668,6 +3668,39 @@ void TestNewShaderRecompilerVop1SdwaBfrev() {
}
}
void TestVop2SdwaMulI24Destination() {
using namespace ShaderRecompiler::Decoder;
const uint32_t captured[][2] = {
{0x121818f9u, 0x04861488u}, {0x123434f9u, 0x0486146au}};
for (uint32_t index = 0; index < 2; ++index) {
Instruction decoded;
DecodeInstruction(captured[index], 0, decoded);
const uint32_t reg = index == 0 ? 12u : 26u;
Check(decoded.opcode == Opcode::V_MUL_I32_I24 && decoded.word_count == 2 &&
decoded.dst.kind == OperandKind::Vgpr && decoded.dst.reg == reg &&
decoded.dst.sdwa_sel == 4 && decoded.dst.sdwa_dst_unused == 2 &&
decoded.dst.explicit_sdwa_dst && !decoded.dst.clamp &&
decoded.dst.omod == 0 && decoded.src0.sdwa_sel == 6 &&
decoded.src1.kind == OperandKind::Vgpr && decoded.src1.reg == reg &&
decoded.src1.sdwa_sel == 4 && !decoded.src1.sdwa_sext,
"captured V_MUL_I32_I24 SDWA destination/source metadata is incorrect");
Check(index == 0 ? decoded.src0.kind == OperandKind::IntegerInlineConstant &&
decoded.src0.value == 8
: decoded.src0.kind == OperandKind::VccLo,
"captured V_MUL_I32_I24 SDWA scalar source is incorrect");
}
for (const uint32_t modifier : {
0x04861788u, 0x04861c88u, 0x04871488u, 0x07861488u,
0x04961488u, 0x04a61488u, 0x14861488u, 0x24861488u,
0x04863488u, 0x04865488u}) {
const uint32_t invalid[] = {captured[0][0], modifier};
Instruction decoded;
DecodeInstruction(invalid, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.word_count == 2,
"V_MUL_I32_I24 SDWA accepted reserved selectors or float modifiers");
}
}
void TestNewShaderRecompilerVop1SdwaNotDestination() {
auto options = MakeCompileOptions(ShaderType::Pixel);
@@ -4413,25 +4446,32 @@ void TestImageAtomicWidthDecoder() {
decoded.dst.reg == 2u && decoded.dmask == 3u && !decoded.glc,
"captured image atomic lost its 64-bit data width");
for (const auto opcode : {0x10u, 0x17u}) {
for (const auto opcode : {0x0fu, 0x10u, 0x11u, 0x14u, 0x15u, 0x16u,
0x17u, 0x18u, 0x19u, 0x1au, 0x1eu, 0x1fu}) {
const bool compare_swap = opcode == 0x10u;
const bool supports_64 = opcode != 0x10u && opcode != 0x14u &&
opcode != 0x16u && opcode != 0x1eu && opcode != 0x1fu;
for (uint32_t mask = 0; mask < 16u; ++mask) {
const uint32_t words[] = {EncodeMimg0(opcode, mask), captured[1]};
DecodeInstruction(words, 0, decoded);
const bool compare_swap = opcode == 0x10u;
const bool supported = mask == (compare_swap ? 3u : 1u) ||
(!compare_swap && mask == 3u);
Check((decoded.opcode != Opcode::UNSUPPORTED) == supported,
"image atomic accepted an invalid or unsupported width mask");
if (supported) {
Check(decoded.data_bits == (!compare_swap && mask == 3u ? 64u : 32u),
"image atomic DMASK selected the wrong data width");
for (const bool glc : {false, true}) {
const uint32_t words[] = {EncodeMimg0(opcode, mask, glc), captured[1]};
DecodeInstruction(words, 0, decoded);
const bool supported = mask == (compare_swap ? 3u : 1u) ||
(supports_64 && mask == 3u);
Check((decoded.opcode != Opcode::UNSUPPORTED) == supported,
"image atomic accepted an invalid or unsupported width mask");
if (supported) {
const auto bits = !compare_swap && mask == 3u ? 64u : 32u;
Check(decoded.data_bits == bits && decoded.glc == glc &&
decoded.data_dwords == (compare_swap ? 2u : bits / 32u),
"image atomic DMASK selected the wrong data width or return mode");
}
const uint32_t d16_words[] = {words[0], words[1] | (1u << 31u)};
DecodeInstruction(d16_words, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED,
"image atomic incorrectly accepted D16 data");
}
}
}
const uint32_t unsupported[] = {EncodeMimg0(0x11u, 3u), captured[1]};
DecodeInstruction(unsupported, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.data_bits == 64u,
"unsupported 64-bit image atomic silently decoded as 32-bit");
}
void TestNewShaderDecoderArchitecture() {
@@ -4854,6 +4894,46 @@ void TestNewShaderRecompilerCapturedVopcSdwaCmpxClass() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestVopcCmpxClassF16Decoder() {
using namespace ShaderRecompiler::Decoder;
for (const uint32_t modifier : {0x86360000u, 0x86350002u}) {
const uint32_t captured[] = {0x7d3f70f9u, modifier};
Instruction decoded;
DecodeInstruction(captured, 0, decoded);
Check(decoded.opcode == Opcode::V_CMPX_CLASS_F16 && decoded.word_count == 2 &&
decoded.dst.kind == OperandKind::ExecLo &&
decoded.src0.kind == OperandKind::Vgpr &&
decoded.src0.reg == (modifier == 0x86360000u ? 0u : 2u) &&
decoded.src0.sdwa_sel == (modifier == 0x86360000u ? 6u : 5u) &&
decoded.src0.absolute && decoded.src0.negate &&
decoded.src1.kind == OperandKind::IntegerInlineConstant &&
decoded.src1.value == 56 && decoded.src1.sdwa_sel == 6,
"captured V_CMPX_CLASS_F16 SDWA fields are incorrect");
const uint32_t shader[] = {captured[0], captured[1], EncodeSopp(0x01)};
auto result = RecompileForTest(shader, MakeCompileOptions(ShaderType::Pixel));
CheckSpirvBinaryValidates(result.spirv);
}
const uint32_t compact[] = {EncodeVopc(0x9f, 256, 1)};
const uint32_t vop3[] = {EncodeVop3Word0(0x9f, 126),
EncodeVop3Word1(256, 257, 0)};
for (auto words : {std::span<const uint32_t>(compact),
std::span<const uint32_t>(vop3)}) {
Instruction decoded;
DecodeInstruction(words, 0, decoded);
Check(decoded.opcode == Opcode::V_CMPX_CLASS_F16 &&
decoded.dst.kind == OperandKind::ExecLo && decoded.src0.reg == 0 &&
decoded.src1.kind == OperandKind::Vgpr && decoded.src1.reg == 1,
"compact/VOP3 V_CMPX_CLASS_F16 did not decode to an EXEC compare");
}
for (const uint32_t modifier : {0x86370000u, 0x87360000u}) {
const uint32_t invalid[] = {0x7d3f70f9u, modifier};
Instruction decoded;
DecodeInstruction(invalid, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.word_count == 2,
"V_CMPX_CLASS_F16 accepted reserved SDWA selector 7");
}
}
void TestNewShaderRecompilerCapturedVopcSdwaCmpxLtU16() {
using namespace ShaderRecompiler;
@@ -7741,40 +7821,18 @@ void TestNewShaderRecompilerDsAddtidTranslation() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerDsFloatMinMaxTranslation() {
const uint32_t shader[] = {
EncodeDs0(0x12, 4), EncodeDs1Ex(0, 9, 7, 1), // ds_min_f32 v7, v9, v1
EncodeDs0(0x13, 8), EncodeDs1Ex(0, 10, 8, 1), // ds_max_f32 v8, v10, v1
0xbf810000u,
};
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check((result.decoded_dump.find("ds_min_f32") != std::string::npos),
"new decoder did not decode DS float min");
Check((result.decoded_dump.find("ds_max_f32") != std::string::npos),
"new decoder did not decode DS float max");
Check((result.ir_dump.find("DsMinF32 null, v7, v1") != std::string::npos),
"DS float min did not lower to explicit IR");
Check((result.ir_dump.find("DsMaxF32 null, v8, v1") != std::string::npos),
"DS float max did not lower to explicit IR");
Check((result.ir_dump.find("v9") != std::string::npos),
"DS float min did not retain DATA1 compare operand");
Check((result.ir_dump.find("v10") != std::string::npos),
"DS float max did not retain DATA1 compare operand");
Check(SpirvContainsOpcode(result.spirv, 12),
"SPIR-V binary does not contain OpExtInst");
Check(SpirvContainsOpcode(result.spirv, 61),
"SPIR-V binary does not contain OpLoad");
Check(SpirvContainsOpcode(result.spirv, 62),
"SPIR-V binary does not contain OpStore");
Check(SpirvContainsOpcode(result.spirv, 65),
"SPIR-V binary does not contain OpAccessChain");
Check(SpirvContainsOpcode(result.spirv, 124),
"SPIR-V binary does not contain OpBitcast");
CheckSpirvBinaryValidates(result.spirv);
void TestDsFloatMinMaxDecoder() {
using namespace ShaderRecompiler::Decoder;
for (bool max_value : {false, true}) {
const uint32_t shader[] = {
EncodeDs0(max_value ? 0x13 : 0x12, 4), EncodeDs1Ex(0, 250, 7, 1)};
Instruction decoded;
DecodeInstruction(shader, 0u, decoded);
Check(decoded.opcode == (max_value ? Opcode::DS_MAX_F32 : Opcode::DS_MIN_F32) &&
decoded.src_count == 2u && decoded.src0.reg == 1u &&
decoded.src1.reg == 7u && decoded.offset == 4u,
"DS float min/max must read only address and DATA0");
}
}
void TestNewShaderRecompilerCfgStraightLine() {
@@ -7884,7 +7942,7 @@ void TestNewShaderRecompilerStructuredU64Phi() {
const auto after = MeasureSpirv(spirv);
Check(after.phis == before.phis + 1u &&
after.function_variables == before.function_variables,
"structured U64 Phi was not emitted as a native vector OpPhi");
"structured U64 Phi was not emitted as a native scalar OpPhi");
}
void TestNewShaderRecompilerCfgTerminalExitMergePS() {
@@ -9808,7 +9866,7 @@ void TestNewShaderRecompilerPlanningOnlyLoads() {
"planning-only loads or references emitted GPU operands/instructions");
}
void TestNewShaderRecompilerU64PairTranslation() {
void TestNewShaderRecompilerNativeU64Translation() {
using namespace ShaderRecompiler;
auto options = MakeCompileOptions(ShaderType::Compute);
@@ -9938,20 +9996,23 @@ void TestNewShaderRecompilerU64PairTranslation() {
auto spirv = ShaderRecompiler::Spirv::EmitProgram(program, options.input_info);
CheckSpirvBinaryValidates(spirv);
const auto source = DisassembleSpirvBinary(spirv);
Check((source.find("OpCapability Int64") == std::string::npos) &&
(source.find("OpTypeInt 64") == std::string::npos),
"portable pair-U64 translation introduced native shader Int64");
Check(SpirvInstructionOpcodeCount(spirv, 149u) == 1u,
"pair-U64 addition did not use exactly one carry instruction");
Check(SpirvInstructionOpcodeCount(spirv, 154u) == 1u &&
SpirvInstructionOpcodeCount(spirv, 155u) == 1u,
"pair-U64 equality did not reduce its vector comparison with Any/All");
Check(source.find("OpCapability Int64") != std::string::npos &&
source.find("OpTypeInt 64 0") != std::string::npos &&
SpirvSourceHasInstructionUsing(source, "OpIAdd", "%ulong") &&
SpirvSourceHasInstructionUsing(source, "OpIMul", "%ulong") &&
SpirvSourceHasInstructionUsing(source, "OpBitwiseAnd", "%ulong"),
"U64 arithmetic did not emit native 64-bit scalar instructions");
Check(SpirvInstructionOpcodeCount(spirv, spv::OpIAddCarry) == 0u &&
SpirvInstructionOpcodeCount(spirv, spv::OpUMulExtended) == 0u &&
SpirvInstructionOpcodeCount(spirv, spv::OpAny) == 0u &&
SpirvInstructionOpcodeCount(spirv, spv::OpAll) == 0u,
"native U64 arithmetic retained split-word operations");
const auto direct_metrics = MeasureSpirv(spirv);
Check(
direct_metrics.words <= 940u && direct_metrics.instructions <= 199u &&
direct_metrics.type_vectors == 2u && direct_metrics.phis == 0u &&
direct_metrics.function_variables == 0u,
"portable pair-U64 translation exceeded its declaration/code-size ratchet");
// The same fixture required 864 words and 182 instructions with split U64s.
Check(direct_metrics.words <= 285u && direct_metrics.instructions <= 67u &&
direct_metrics.phis == 0u &&
direct_metrics.function_variables == 0u,
"native U64 translation exceeded its declaration/code-size ratchet");
const uint32_t dispatcher_shader[] = {
EncodeSopp(0x05, 2), EncodeSopp(0x02, 0), EncodeSopp(0x05, 0xfffeu),
@@ -9976,7 +10037,39 @@ void TestNewShaderRecompilerU64PairTranslation() {
Check(after.function_variables == before.function_variables + 1u &&
after.loads == before.loads + 1u &&
after.stores == before.stores + 1u,
"dispatcher did not use one canonical U64 vector spill slot");
"dispatcher did not use one canonical U64 scalar spill slot");
}
void TestVertexMadU64UsesPortableProduct() {
using StageInputKind = ShaderRecompiler::IR::StageInputKind;
for (const bool dynamic_addend : {false, true}) {
const uint32_t shader[] = {
EncodeVop1(0x01, 6,
8 + 256), // Dynamic addend is [VertexIndex, InstanceIndex].
EncodeVop3Word0Sdst(0x176, 0, 20),
EncodeVop3Word1(5 + 256, 8 + 256, dynamic_addend ? 5 + 256 : 128),
EncodeExp0(0x0c, 0x3),
EncodeExp1(0, 1, 0, 0), // POS0.xy keeps both product words live.
EncodeSopp(0x01),
};
const auto result =
RecompileForTest(shader, MakeCompileOptions(ShaderType::Vertex));
Check(ProgramHasInput(result.program, StageInputKind::VertexIndex) &&
ProgramHasInput(result.program, StageInputKind::InstanceIndex),
"vertex MAD fixture lost its runtime multiplication operands");
CheckSpirvBinaryValidates(result.spirv);
const auto source = DisassembleSpirvBinary(result.spirv);
// Large vertex shaders on NVIDIA rejected native widening multiplication.
Check(SpirvInstructionOpcodeCount(result.spirv, spv::OpUMulExtended) ==
1u &&
SpirvSourceHasInstructionUsing(source, "OpIMul", "%uint") &&
!SpirvSourceHasInstructionUsing(source, "OpIMul", "%ulong"),
"vertex MAD lost its portable 32x32 product");
Check(!dynamic_addend ||
SpirvSourceHasInstructionUsing(source, "OpIAdd", "%ulong"),
"vertex MAD split its live 64-bit addition into word operations");
}
}
void TestComputeDispatchWaveSize() {
@@ -12073,7 +12166,8 @@ void TestU64ShiftConstantPropagation() {
std::vector<ShiftCase> shift_cases;
for (const uint64_t source :
{uint64_t{0x0123456789abcdefull}, uint64_t{0xf123456789abcdefull}}) {
for (const uint32_t count : {0u, 1u, 31u, 32u, 33u, 63u, 64u, 65u}) {
for (const uint32_t count :
{0u, 1u, 31u, 32u, 33u, 63u, 64u, 65u, 127u, UINT32_MAX}) {
const uint32_t amount = count & 63u;
shift_cases.push_back({shift_ir.Emit(ValueOpcode::ShiftLeftLogical64,
{Value(source), Value(count)}),
@@ -12092,10 +12186,41 @@ void TestU64ShiftConstantPropagation() {
const auto value = test.value.Resolve();
Check(value.IsImmediate() && value.GetType() == Type::U64 &&
value.U64() == test.expected,
"pair-U64 shift propagation violated the masked RDNA2 count");
"U64 shift propagation violated the masked RDNA2 count");
}
}
void TestU64RegisterPairRoundTrip() {
using namespace ShaderRecompiler::IR;
Program program;
program.block_storage.push_back(std::make_unique<Block>());
program.blocks.push_back(program.block_storage.back().get());
IREmitter ir(program.blocks.front());
const auto lane = ir.Emit(ValueOpcode::LaneId);
const auto pair = ir.Emit(ValueOpcode::CompositeConstructU64, {lane, lane});
const auto first = ir.Emit(ValueOpcode::IAdd64, {pair, Value(uint64_t{1})});
const auto second = ir.Emit(ValueOpcode::IAdd64, {pair, Value(uint64_t{2})});
const auto low =
ir.Emit(ValueOpcode::CompositeExtractU64, {first, Value(0u)});
const auto high =
ir.Emit(ValueOpcode::CompositeExtractU64, {first, Value(1u)});
const auto other_high =
ir.Emit(ValueOpcode::CompositeExtractU64, {second, Value(1u)});
const auto restored =
ir.Emit(ValueOpcode::CompositeConstructU64, {low, high});
const auto swapped = ir.Emit(ValueOpcode::CompositeConstructU64, {high, low});
const auto mixed =
ir.Emit(ValueOpcode::CompositeConstructU64, {low, other_high});
ConstantPropagationPass(program.blocks);
Check(restored.Resolve() == first.Resolve(),
"unchanged SGPR pair did not retain its native U64 value");
Check(swapped.Resolve().Instruction()->GetOpcode() ==
ValueOpcode::CompositeConstructU64 &&
mixed.Resolve().Instruction()->GetOpcode() ==
ValueOpcode::CompositeConstructU64,
"modified SGPR pair incorrectly reused an unrelated native U64 value");
}
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
void TestNativeWideValueValidation() {
using namespace ShaderRecompiler::IR;
@@ -14799,17 +14924,20 @@ int main() {
TestNewShaderRecompilerF64AluEncodings();
TestNewShaderRecompilerVop1SdwaBfrev();
TestNewShaderRecompilerVop1SdwaNotDestination();
TestVop2SdwaMulI24Destination();
TestNewShaderRecompilerScalarMemoryBindingDomains();
// Opcode semantics and optimized SPIR-V are exercised by
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks
// here.
TestScalarAshrI64Decoder();
TestImageAtomicWidthDecoder();
TestDsFloatMinMaxDecoder();
TestNewShaderDecoderArchitecture();
TestImageAddressOperands();
TestSopkCompareImmediateExtension();
TestNativeStoreCompletion();
TestNewShaderRecompilerCapturedVopcSdwaCmpxClass();
TestVopcCmpxClassF16Decoder();
TestNewShaderRecompilerCapturedVopcSdwaCmpxLtU16();
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
TestNewShaderRecompilerRejectsDppOn64BitCompares();
@@ -14870,7 +14998,8 @@ int main() {
TestNewShaderRecompilerCfgIrreducibleDispatcher();
TestNewShaderRecompilerDispatcherSpillsU32x3();
TestNewShaderRecompilerPlanningOnlyLoads();
TestNewShaderRecompilerU64PairTranslation();
TestNewShaderRecompilerNativeU64Translation();
TestVertexMadU64UsesPortableProduct();
TestComputeDispatchWaveSize();
TestNewShaderRecompilerBufferLoadsGuardedByExec();
TestNewShaderRecompilerBufferAtomicsGuardedByBounds();
@@ -14895,6 +15024,7 @@ int main() {
TestValuePhiValidation();
TestWave32MaskProjection();
TestU64ShiftConstantPropagation();
TestU64RegisterPairRoundTrip();
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
TestNativeWideValueValidation();
#endif