mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-22 20:06:54 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77d7722173 | |||
| 266c12cec6 | |||
| 7a0e1dafd7 | |||
| 1d41ddb378 | |||
| 5c83060481 | |||
| 72e4989b1e | |||
| 1d55272467 | |||
| 2b1b7ed0e4 |
+10
-1
@@ -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")
|
||||
@@ -725,6 +725,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.
@@ -27,6 +27,7 @@ target_link_libraries(common
|
||||
kyty_git_version
|
||||
spdlog::spdlog
|
||||
ZArchive::zarchive
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
@@ -156,6 +156,10 @@ bool TessellationEnabled() {
|
||||
return g_config->tessellation_enabled;
|
||||
}
|
||||
|
||||
bool TrophyEnabled() {
|
||||
return g_config->trophy_enabled;
|
||||
}
|
||||
|
||||
bool PlayGoHackEnabled() {
|
||||
return g_config->playgo_hack_enabled;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ struct ConfigOptions {
|
||||
bool renderdoc_enabled = false;
|
||||
bool readback_linear_images = false;
|
||||
bool tessellation_enabled = false;
|
||||
bool trophy_enabled = true;
|
||||
bool playgo_hack_enabled = false;
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool red_zone_protection_enabled = false;
|
||||
@@ -123,6 +124,7 @@ bool GpuAssistedValidationEnabled();
|
||||
bool RenderDocEnabled();
|
||||
bool ReadbackLinearImagesEnabled();
|
||||
bool TessellationEnabled();
|
||||
bool TrophyEnabled();
|
||||
bool PlayGoHackEnabled();
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
bool RedZoneProtectionEnabled();
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
#include "common/trophies.h"
|
||||
|
||||
#include "common/file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <span>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Package ParsePackage(std::span<const std::byte> data, int console_language) {
|
||||
if (data.size() < 0x40 || data.size() > MaxPackageSize ||
|
||||
ReadBigEndian(data.data(), 4) != 0xb228c60a || ReadBigEndian(data.data() + 4, 4) != 1) {
|
||||
return {};
|
||||
}
|
||||
const auto 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 {};
|
||||
}
|
||||
Files files;
|
||||
for (uint64_t i = 0; i < count; ++i) {
|
||||
const auto* entry = data.data() + toc + 0x20 + i * 0x40;
|
||||
const auto* name = reinterpret_cast<const char*>(entry);
|
||||
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 {};
|
||||
}
|
||||
files.emplace(filename, data.subspan(offset, length));
|
||||
}
|
||||
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() && condition.contains("progressive") &&
|
||||
condition["progressive"] == true) {
|
||||
if (!condition.contains("targetValue") || !condition["targetValue"].is_string()) {
|
||||
return {};
|
||||
}
|
||||
const auto& value = condition["targetValue"].get_ref<const std::string&>();
|
||||
uint64_t target = 0;
|
||||
const auto [end, error] =
|
||||
std::from_chars(value.data(), value.data() + value.size(), target);
|
||||
if (error != std::errc {} || end != value.data() + value.size()) {
|
||||
return {};
|
||||
}
|
||||
trophy.target = target;
|
||||
}
|
||||
}
|
||||
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 / "_SaveData" / title_id /
|
||||
fmt::format("trophies_{}_{}.json", user_id, service_label);
|
||||
}
|
||||
|
||||
std::set<int> LoadUnlocks(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 {};
|
||||
}
|
||||
std::set<int> unlocked;
|
||||
for (const auto& value: json["unlockedTrophies"]) {
|
||||
const auto id = ReadId(value);
|
||||
if (id >= 0) {
|
||||
unlocked.insert(id);
|
||||
}
|
||||
}
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked) {
|
||||
if (path.empty() || !File::CreateDirectories(path.parent_path())) {
|
||||
return false;
|
||||
}
|
||||
const auto text = Json {{"unlockedTrophies", unlocked}}.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;
|
||||
}
|
||||
|
||||
} // namespace Common::Trophies
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef KYTY_COMMON_TROPHIES_H_
|
||||
#define KYTY_COMMON_TROPHIES_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Common::Trophies {
|
||||
|
||||
inline constexpr char PackageDirectory[] = "sce_sys/trophy2";
|
||||
|
||||
struct Trophy {
|
||||
int id = 0;
|
||||
int group_id = -1;
|
||||
int platinum_id = -1;
|
||||
int grade = 0;
|
||||
std::optional<uint64_t> target;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string reward;
|
||||
std::vector<std::byte> icon_png;
|
||||
bool hidden = false;
|
||||
bool has_reward = false;
|
||||
};
|
||||
|
||||
struct Package {
|
||||
std::string title;
|
||||
std::map<int, std::string> groups;
|
||||
std::map<int, Trophy> trophies;
|
||||
};
|
||||
|
||||
[[nodiscard]] Package LoadPackage(const std::filesystem::path& path, int console_language);
|
||||
[[nodiscard]] std::filesystem::path PackagePath(uint32_t service_label);
|
||||
[[nodiscard]] std::filesystem::path UnlocksPath(const std::filesystem::path& root,
|
||||
std::string_view title_id, int user_id,
|
||||
uint32_t service_label);
|
||||
[[nodiscard]] std::set<int> LoadUnlocks(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool SaveUnlocks(const std::filesystem::path& path, const std::set<int>& unlocked);
|
||||
|
||||
} // namespace Common::Trophies
|
||||
|
||||
#endif // KYTY_COMMON_TROPHIES_H_
|
||||
@@ -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,104 @@ 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(440.0f * screen_scale, static_cast<float>(frame_extent.width) - 32.0f));
|
||||
const float height = 104.0f * screen_scale;
|
||||
const float fade_in = std::clamp(age / 0.22f, 0.0f, 1.0f);
|
||||
const float fade_out = std::clamp((TOAST_LIFETIME - age) / 0.4f, 0.0f, 1.0f);
|
||||
const float alpha = std::min(fade_in, fade_out);
|
||||
const float slide = (1.0f - fade_in) * (width + 24.0f);
|
||||
const ImVec2 top_left {static_cast<float>(frame_extent.width) - 24.0f - width + slide,
|
||||
32.0f};
|
||||
const ImVec2 bottom_right {top_left.x + width, top_left.y + height};
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
draw->AddRectFilled({top_left.x + 5.0f, top_left.y + 6.0f},
|
||||
{bottom_right.x + 5.0f, bottom_right.y + 6.0f},
|
||||
IM_COL32(0, 0, 0, static_cast<int>(95.0f * alpha)), 10.0f);
|
||||
draw->AddRectFilled(top_left, bottom_right,
|
||||
IM_COL32(18, 21, 29, static_cast<int>(242.0f * alpha)), 10.0f);
|
||||
draw->AddRect(top_left, bottom_right,
|
||||
IM_COL32(190, 154, 78, static_cast<int>(200.0f * alpha)), 10.0f,
|
||||
ImDrawFlags_RoundCornersAll, 1.5f);
|
||||
|
||||
if (trophy_notification_image != nullptr) {
|
||||
constexpr float ICON_SIZE = 88.0f;
|
||||
const ImVec2 icon_min {top_left.x + 8.0f * screen_scale,
|
||||
top_left.y + (height - ICON_SIZE * screen_scale) * 0.5f};
|
||||
const ImVec2 icon_max {icon_min.x + ICON_SIZE * screen_scale,
|
||||
icon_min.y + ICON_SIZE * screen_scale};
|
||||
draw->AddImage(trophy_notification_image->GetTexRef(), icon_min, icon_max, {0, 0},
|
||||
{1, 1}, IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)));
|
||||
}
|
||||
|
||||
const ImVec2 text_position {top_left.x +
|
||||
(trophy_notification_image ? 112.0f : 18.0f) * screen_scale,
|
||||
top_left.y + 18.0f * screen_scale};
|
||||
draw->PushClipRect(top_left, {bottom_right.x - 12.0f * screen_scale, bottom_right.y}, true);
|
||||
draw->AddText(nullptr, 13.0f * screen_scale, text_position,
|
||||
IM_COL32(232, 203, 139, static_cast<int>(255.0f * alpha)), "Trophy earned");
|
||||
draw->AddText(nullptr, 21.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 22.0f * screen_scale},
|
||||
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha)),
|
||||
trophy_notification->name.c_str());
|
||||
constexpr std::array GRADE_LABELS {"Platinum", "Gold", "Silver", "Bronze"};
|
||||
constexpr std::array GRADE_COLORS {
|
||||
IM_COL32(161, 225, 245, 255), IM_COL32(255, 207, 82, 255), IM_COL32(211, 220, 232, 255),
|
||||
IM_COL32(211, 142, 91, 255)};
|
||||
const int grade = trophy_notification->grade;
|
||||
if (grade > 0 && grade <= static_cast<int>(GRADE_LABELS.size())) {
|
||||
draw->AddText(nullptr, 13.0f * screen_scale,
|
||||
{text_position.x, text_position.y + 50.0f * screen_scale},
|
||||
ImGui::GetColorU32(GRADE_COLORS[grade - 1], alpha),
|
||||
GRADE_LABELS[grade - 1]);
|
||||
}
|
||||
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 +1180,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 +1187,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 +1246,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:
|
||||
|
||||
@@ -231,15 +231,18 @@ uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
}
|
||||
|
||||
uint32_t EmitFPMad32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
// Legacy MAD/MAC round and flush between multiply and add, irrespective of SP_DENORM.
|
||||
a = EmitFlushF32DenormToSignedZero(state, a);
|
||||
b = EmitFlushF32DenormToSignedZero(state, b);
|
||||
c = EmitFlushF32DenormToSignedZero(state, 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, EmitFlushF32DenormToSignedZero(state, product), c);
|
||||
const auto sum = EmitFPAdd32(state, product, c);
|
||||
state.builder.AddAnnotation(spv::OpDecorate, sum, spv::DecorationNoContraction);
|
||||
return EmitFlushF32DenormToSignedZero(state, sum);
|
||||
// return EmitFlushF32DenormToSignedZero(state, sum);
|
||||
return sum;
|
||||
}
|
||||
|
||||
uint32_t EmitFPMedTri32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -152,6 +152,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)
|
||||
|
||||
@@ -510,6 +510,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);
|
||||
|
||||
|
||||
@@ -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[] = {
|
||||
@@ -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) {
|
||||
|
||||
@@ -663,6 +663,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:
|
||||
|
||||
@@ -397,6 +397,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,
|
||||
@@ -441,6 +442,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,
|
||||
@@ -532,6 +534,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},
|
||||
@@ -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},
|
||||
@@ -916,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;
|
||||
}
|
||||
@@ -1539,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:
|
||||
@@ -1557,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
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
|
||||
@@ -1077,6 +1077,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:
|
||||
|
||||
@@ -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);
|
||||
@@ -127,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;
|
||||
|
||||
@@ -493,6 +493,7 @@ struct DescriptorSource {
|
||||
uint32_t table_stride = 0;
|
||||
uint32_t workgroup_axis = UINT32_MAX;
|
||||
uint32_t selector_shift = 0;
|
||||
uint32_t selector_bits = UINT32_MAX;
|
||||
Value key_count;
|
||||
Value selector_first;
|
||||
Value selector_mask;
|
||||
|
||||
@@ -175,6 +175,7 @@ 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)
|
||||
|
||||
@@ -306,7 +306,8 @@ 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) ||
|
||||
((indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) &&
|
||||
(material.Base48() & 3u) != 0u) ||
|
||||
material.SwizzleEnabled() ||
|
||||
material.AddTid() || material.OutOfBounds() != 0u ||
|
||||
uint64_t {indirect.selector_offset} + 4u > material.Stride() ||
|
||||
@@ -362,8 +363,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
|
||||
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;
|
||||
for (auto& key: keys) key = (key >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.selector_shift != 0u || indirect.selector_bits != UINT32_MAX) {
|
||||
keys.push_back(0u); // An out-of-range material load returns zero.
|
||||
}
|
||||
std::ranges::sort(keys);
|
||||
@@ -663,7 +664,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 +687,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 +716,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 {};
|
||||
|
||||
@@ -742,6 +742,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];
|
||||
@@ -835,7 +844,7 @@ private:
|
||||
const auto& b = *descriptor.indirect_descriptor;
|
||||
if (a.material_source != b.material_source || a.table_source != b.table_source ||
|
||||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
|
||||
a.selector_shift != b.selector_shift ||
|
||||
a.selector_shift != b.selector_shift || a.selector_bits != b.selector_bits ||
|
||||
a.table_offset != b.table_offset || 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) ||
|
||||
@@ -968,11 +977,24 @@ private:
|
||||
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;
|
||||
Value MaterialKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
|
||||
indirect.selector_shift = 0u;
|
||||
indirect.selector_bits = UINT32_MAX;
|
||||
const auto* inst = key.Resolve().TryInstruction();
|
||||
uint32_t immediate;
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::BitwiseAnd32) {
|
||||
if (ImmediateU32(inst->Arg(1), immediate)) key = inst->Arg(0);
|
||||
else if (ImmediateU32(inst->Arg(0), immediate)) key = inst->Arg(1);
|
||||
else return key;
|
||||
indirect.selector_bits = immediate;
|
||||
inst = key.Resolve().TryInstruction();
|
||||
}
|
||||
if (inst != nullptr && inst->GetOpcode() == ValueOpcode::ShiftRightLogical32 &&
|
||||
ImmediateU32(inst->Arg(1), immediate) && immediate != 0u && immediate < 32u) {
|
||||
indirect.selector_shift = immediate;
|
||||
key = inst->Arg(0);
|
||||
}
|
||||
return key.Resolve();
|
||||
}
|
||||
|
||||
enum class LaneQuantifier { Any, All };
|
||||
@@ -1647,10 +1669,9 @@ private:
|
||||
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();
|
||||
const auto value = MaterialKey(selected->Arg(1), indirect);
|
||||
if (indirect.selector_shift == 0u && indirect.selector_bits == UINT32_MAX) return false;
|
||||
const auto* loaded = value.TryInstruction();
|
||||
if (loaded == nullptr || loaded->GetOpcode() != ValueOpcode::SelectU32 ||
|
||||
!EquivalentValue(m_program, selected->Arg(0), loaded->Arg(0))) return false;
|
||||
const auto* read = loaded->Arg(1).Resolve().TryInstruction();
|
||||
@@ -1668,7 +1689,6 @@ private:
|
||||
indirect.selector_first = Value(0u);
|
||||
indirect.key_count = material_source.dwords[2];
|
||||
indirect.selector_offset = memory.offset;
|
||||
indirect.selector_shift = amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1690,9 +1710,8 @@ private:
|
||||
// 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;
|
||||
} 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 &&
|
||||
@@ -1710,13 +1729,11 @@ 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();
|
||||
auto* material_read = MaterialKey(key, indirect).TryInstruction();
|
||||
uint32_t material_memory_index = 0;
|
||||
const auto* memory = material_read != nullptr
|
||||
? ScalarReadMemory(*material_read, material_memory_index) : nullptr;
|
||||
if (table_offset != 0u || memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
|
||||
if (memory == nullptr || memory->kind != ResourceKind::ScalarBuffer ||
|
||||
memory->offset > INT32_MAX || !MemoryIndexBelongsTo(material_memory_index, *material_read)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1735,6 +1752,12 @@ private:
|
||||
}
|
||||
indirect.material_source = InternSource(material_source);
|
||||
}
|
||||
// This plan combines shader byte-offset additions with the scalar immediate.
|
||||
// A nonzero combined offset is exact only when neither can cross U32 wrap.
|
||||
const auto maximum_key = (UINT32_MAX >> indirect.selector_shift) & indirect.selector_bits;
|
||||
if (indirect.workgroup_axis == UINT32_MAX && table_source.dword_count == 4u &&
|
||||
table_offset != 0u && uint64_t {maximum_key} * table_stride + table_offset + 28u > UINT32_MAX)
|
||||
return false;
|
||||
indirect.table_source = InternSource(table_source);
|
||||
DescriptorSource image_source;
|
||||
image_source.dword_count = 8u;
|
||||
@@ -2032,6 +2055,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);
|
||||
|
||||
@@ -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 && logs</string>
|
||||
|
||||
@@ -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; }
|
||||
@@ -78,6 +79,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,10 +12,11 @@ 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);
|
||||
|
||||
private:
|
||||
bool LoadGame(const Configuration& info, QString& error);
|
||||
bool LoadGame(const Configuration& info, const QString& runtime_directory, QString& error);
|
||||
|
||||
QTabWidget* m_tabs = nullptr;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -835,7 +835,8 @@ void ConfigurationListWidget::ViewTrophies() {
|
||||
return;
|
||||
}
|
||||
|
||||
TrophyViewerDialog::ShowForGame(&item->GetInfo(), this);
|
||||
const auto config = CreateConfiguration(*item);
|
||||
TrophyViewerDialog::ShowForGame(config.get(), m_runtime_directory, this);
|
||||
}
|
||||
|
||||
void ConfigurationListWidget::open_game_folder() {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -1,462 +1,62 @@
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/trophies.h"
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
#include <QIcon>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QLabel>
|
||||
#include <QMap>
|
||||
#include <QImage>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#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);
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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 {};
|
||||
}
|
||||
return trophy_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->setIconSize(QSize(96, 96));
|
||||
table->setShowGrid(false);
|
||||
table->setWordWrap(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
@@ -469,30 +69,16 @@ static void PrepareTable(QTableWidget* table) {
|
||||
table->setColumnWidth(3, 480);
|
||||
}
|
||||
|
||||
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 +102,8 @@ bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
|
||||
return !FindTrophyFiles(info).isEmpty();
|
||||
}
|
||||
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent) {
|
||||
void TrophyViewerDialog::ShowForGame(const Configuration* info, const QString& runtime_directory,
|
||||
QWidget* parent) {
|
||||
if (info == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -527,7 +114,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 +122,71 @@ 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;
|
||||
}
|
||||
|
||||
auto* table = new QTableWidget(set.trophies.size(), 4, m_tabs);
|
||||
const auto label = TrophyFilePattern.match(QFileInfo(file).fileName()).captured(1).toUInt();
|
||||
const auto unlocked = Common::Trophies::LoadUnlocks(
|
||||
Common::Trophies::UnlocksPath(GameContent::ToPath(runtime_directory),
|
||||
info.title_id.toStdString(), info.user_id, label));
|
||||
auto* table = new QTableWidget(static_cast<int>(package.trophies.size()), 4, m_tabs);
|
||||
PrepareTable(table);
|
||||
|
||||
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"));
|
||||
int row_index = 0;
|
||||
for (const auto& [id, trophy]: package.trophies) {
|
||||
const bool trophy_unlocked = unlocked.contains(id);
|
||||
auto* status = new QTableWidgetItem(trophy_unlocked ? tr("UNLOCKED") : tr("LOCKED"));
|
||||
status->setTextAlignment(Qt::AlignCenter);
|
||||
if (row.hidden) {
|
||||
status->setForeground(QBrush(Qt::gray));
|
||||
}
|
||||
auto status_font = status->font();
|
||||
status_font.setBold(true);
|
||||
status->setFont(status_font);
|
||||
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));
|
||||
auto* icon_item = new QTableWidgetItem;
|
||||
icon_item->setTextAlignment(Qt::AlignCenter);
|
||||
QPixmap icon;
|
||||
if (icon.loadFromData(reinterpret_cast<const uchar*>(trophy.icon_png.data()),
|
||||
static_cast<uint>(trophy.icon_png.size()))) {
|
||||
if (!trophy_unlocked) {
|
||||
icon = QPixmap::fromImage(
|
||||
icon.toImage().convertToFormat(QImage::Format_Grayscale8));
|
||||
}
|
||||
icon_item->setIcon(QIcon(icon));
|
||||
}
|
||||
table->setCellWidget(row_index, 1, icon_label);
|
||||
table->setItem(row_index, 1, icon_item);
|
||||
|
||||
auto* name = CreateItem(row.name);
|
||||
auto font = name->font();
|
||||
const auto name_text = trophy.name.empty() ? (trophy.hidden ? tr("Hidden Trophy")
|
||||
: tr("Trophy %1").arg(id))
|
||||
: QString::fromStdString(trophy.name);
|
||||
auto* name = new QTableWidgetItem(name_text);
|
||||
auto font = name->font();
|
||||
font.setBold(true);
|
||||
name->setFont(font);
|
||||
name->setToolTip(TrophyTooltip(row));
|
||||
name->setToolTip(TrophyTooltip(trophy));
|
||||
auto* detail = new QTableWidgetItem(QString::fromStdString(trophy.description));
|
||||
detail->setToolTip(name->toolTip());
|
||||
table->setItem(row_index, 2, name);
|
||||
|
||||
auto* detail = CreateItem(row.detail);
|
||||
detail->setToolTip(TrophyTooltip(row));
|
||||
table->setItem(row_index, 3, detail);
|
||||
++row_index;
|
||||
}
|
||||
|
||||
m_tabs->addTab(table, set.tab_title);
|
||||
m_tabs->addTab(table, tr("Trophy %1").arg(label));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
+7
-1118
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -208,7 +208,8 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
|
||||
std::unique_ptr<Fixture>
|
||||
MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
bool memory_backed_material = false, uint32_t member_offset = 0,
|
||||
uint32_t material_stride = 224) {
|
||||
uint32_t material_stride = 224, uint32_t selector_bits = UINT32_MAX,
|
||||
uint32_t table_stride = 32, uint32_t table_offset = 0) {
|
||||
auto fixture = std::make_unique<Fixture>();
|
||||
std::array<Value, 4> material_words;
|
||||
std::array<Value, 4> heap_words;
|
||||
@@ -257,17 +258,19 @@ MakeIndirectImageFixture(bool malformed, uint32_t material_immediate = 4,
|
||||
MemoryInfo material_scalar;
|
||||
material_scalar.kind = ResourceKind::ScalarBuffer;
|
||||
material_scalar.offset = material_immediate;
|
||||
const auto key =
|
||||
auto key =
|
||||
fixture->Emit(ValueOpcode::ReadConstBuffer, {material, member},
|
||||
fixture->AddMemory(material_scalar, 0x10d8));
|
||||
if (selector_bits != UINT32_MAX)
|
||||
key = fixture->Emit(ValueOpcode::BitwiseAnd32, {key, Value(selector_bits)});
|
||||
const auto heap_offset =
|
||||
fixture->Emit(ValueOpcode::ShiftLeftLogical32, {key, Value(5u)});
|
||||
fixture->Emit(ValueOpcode::IMul32, {key, Value(table_stride)});
|
||||
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);
|
||||
}
|
||||
@@ -579,6 +582,43 @@ void TestInvariantIndirectImageMaterialization() {
|
||||
split_memory.watched_reads == 1u,
|
||||
"indirect scalar offsets did not align and bound their components independently");
|
||||
}
|
||||
|
||||
// The shader packs flags above a 15-bit cube-image key in the final DWORD of
|
||||
// each 64-byte material record; the image occupies bytes 16..47 of its record.
|
||||
auto packed = MakeIndirectImageFixture(false, 60u, false, 0u, 64u, 0x7fffu, 48u, 16u);
|
||||
packed->PlanAndTrack();
|
||||
const auto packed_plan = ExtractResourcePlan(packed->program);
|
||||
LinearTestMemory packed_memory;
|
||||
std::array<uint32_t, 8> packed_data{0x1000u, 64u << 16u, 3u, 0u,
|
||||
0x2000u, 48u << 16u, 2u, 0u};
|
||||
for (uint32_t i = 0; i < 3u; ++i)
|
||||
packed_memory.words[(i * 64u + 60u) / 4u] = (0xffffu << 15u) | (i & 1u);
|
||||
for (uint32_t i = 0; i < 2u; ++i) {
|
||||
auto descriptor = image_descriptor;
|
||||
descriptor[0] += i;
|
||||
std::copy(descriptor.begin(), descriptor.end(),
|
||||
packed_memory.words.begin() + (0x1000u + i * 48u + 16u) / 4u);
|
||||
}
|
||||
SrtRuntime packed_runtime{.user_data = packed_data, .userdata = &packed_memory,
|
||||
.read_specialization_memory = ReadLinearTestMemory};
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 2u && snapshot.images[0].dwords == image_descriptor &&
|
||||
snapshot.images[1].dwords[0] == image_descriptor[0] + 1u &&
|
||||
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset] == 2u,
|
||||
"packed material flags changed the table key or 48-byte descriptor addressing");
|
||||
packed_memory.words[60u / 4u] = 0xffff8002u;
|
||||
packed_memory.fail_address = 0x2070u;
|
||||
Check(MaterializeResources(packed_plan, packed_runtime, snapshot, specialization) &&
|
||||
snapshot.images.size() == 3u &&
|
||||
std::ranges::all_of(snapshot.images[2].dwords, [](uint32_t word) { return word == 0u; }),
|
||||
"masked out-of-range descriptor did not return zero without reading past table bounds");
|
||||
packed_memory.fail_address = 0x205cu;
|
||||
Check(!MaterializeResources(packed_plan, packed_runtime, snapshot, specialization),
|
||||
"unreadable in-range DWORD in a 48-byte descriptor record was accepted");
|
||||
auto overflowing = MakeIndirectImageFixture(false, 60u, false, 0u, 64u,
|
||||
UINT32_MAX, 48u, 16u);
|
||||
CheckFatal([&] { overflowing->PlanAndTrack(); }, "not a valid runtime value",
|
||||
"unbounded table offset conflated shader U32 wrap with scalar immediate addition");
|
||||
}
|
||||
|
||||
void TestGuardedDirectImageTable() {
|
||||
@@ -1529,6 +1569,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 +3096,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.
|
||||
@@ -3685,7 +3655,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);
|
||||
|
||||
@@ -18598,6 +18598,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
|
||||
case Opcode::V_CMPX_NLT_F32:
|
||||
case Opcode::V_CMP_CLASS_F32:
|
||||
case Opcode::V_CMPX_CLASS_F32:
|
||||
case Opcode::V_CMPX_CLASS_F16:
|
||||
case Opcode::V_CMP_LT_F16:
|
||||
case Opcode::V_CMP_EQ_F16:
|
||||
case Opcode::V_CMP_LE_F16:
|
||||
@@ -18672,6 +18673,7 @@ CoverageClass ClassifyOpcode(ShaderOpcode opcode,
|
||||
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:
|
||||
@@ -21530,6 +21532,65 @@ TestCase Vop2SdwaMaxI32CapturedHighWord(u32 wave_size) {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Vop2SdwaMulI24Destinations(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr std::array<u32, 5> sources{
|
||||
0xdead1234u, 0xabcdffffu, 0x98768001u, 0x34560000u, 0x56789abcu};
|
||||
constexpr std::array<u32, 10> captured_expected{
|
||||
0xdead91a0u, 0xdead3400u, 0xabcdfff8u, 0xabcdff00u,
|
||||
0x98760008u, 0x98760100u, 0x34560000u, 0x34560000u,
|
||||
0x56789abcu, 0x56789abcu};
|
||||
constexpr std::array<u32, 18> partial_expected{
|
||||
0x00000080u, 0xffffff80u, 0xa1b2c380u,
|
||||
0x00008000u, 0xffff8000u, 0xa1b280d4u,
|
||||
0x00800000u, 0xff800000u, 0xa180c3d4u,
|
||||
0x80000000u, 0x80000000u, 0x80b2c3d4u,
|
||||
0x0000a180u, 0xffffa180u, 0xa1b2a180u,
|
||||
0xa1800000u, 0xa1800000u, 0xa180c3d4u};
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "Vop2SdwaMulI24DestinationsWave64"
|
||||
: "Vop2SdwaMulI24DestinationsWave32";
|
||||
test.initial.assign(sources.begin(), sources.end());
|
||||
test.initial.push_back(0xabcde080u);
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
for (u32 index = 0; index < sources.size(); ++index) {
|
||||
AppendVMovU32(&code, 30, index * sizeof(u32));
|
||||
AppendBufferLoadDword(&code, 12, 30);
|
||||
AppendBufferLoadDword(&code, 26, 30);
|
||||
AppendSMovLiteral(&code, 106, 256);
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(index + 1u == sources.size() ? 0 : 1)));
|
||||
// Captured WORD0/PRESERVE encodings alias their source and destination.
|
||||
code.insert(code.end(), {0x121818f9u, 0x04861488u,
|
||||
0x123434f9u, 0x0486146au});
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(1)));
|
||||
AppendStoreVgpr(&code, 12, static_cast<u32>(test.expected.size()));
|
||||
AppendStoreVgpr(&code, 26, static_cast<u32>(test.expected.size()) + 1u);
|
||||
test.expected.push_back(captured_expected[index * 2u]);
|
||||
test.expected.push_back(captured_expected[index * 2u + 1u]);
|
||||
}
|
||||
AppendVMovU32(&code, 30, sources.size() * sizeof(u32));
|
||||
AppendBufferLoadDword(&code, 12, 30);
|
||||
for (u32 selector = 0; selector < 6; ++selector) {
|
||||
for (u32 unused = 0; unused < 3; ++unused) {
|
||||
AppendVMovLiteral(&code, 26, 0xa1b2c3d4u);
|
||||
code.push_back(EncodeVop2(0x09, 26, 249, 12));
|
||||
code.push_back(EncodeVop2Sdwa(InlineU32(3), selector, unused, 6, 4,
|
||||
0, 0, 0, 0, 0, 0, 1));
|
||||
AppendStoreVgpr(&code, 26, static_cast<u32>(test.expected.size()));
|
||||
test.expected.push_back(partial_expected[selector * 3u + unused]);
|
||||
}
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.initial.resize(test.expected.size());
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_MUL_I32_I24, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.required_spirv = {"OpIMul", "OpBitFieldUExtract"};
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Vop2SdwaLshrrevCapturedByte1Source() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -23847,23 +23908,33 @@ TestCase FloatInlineConstantF16UsesNumericValue() {
|
||||
AppendVMovLiteral(&code, 2, 0x3c003c00u); // 1.0h, 1.0h
|
||||
AppendVMovLiteral(&code, 10, 0xaaaa0000u);
|
||||
AppendVMovLiteral(&code, 12, 0xbbbb0000u);
|
||||
AppendVMovLiteral(&code, 14, 0xcccc0000u);
|
||||
AppendVMovLiteral(&code, 15, 0xdddd0000u);
|
||||
|
||||
AppendVop3(&code, 0x34bu, 10, 246u, Vgpr(1), Vgpr(2));
|
||||
AppendVop3p(&code, 0x0eu, 11, 246u, Vgpr(1), Vgpr(2), 0x1u);
|
||||
AppendVop3(&code, 0x34bu, 12, 248u, 242u, 128u);
|
||||
AppendVop3(&code, 0x311u, 13, 242u, 248u);
|
||||
AppendVop3(&code, 0x34bu, 14, 246u, Vgpr(1), Vgpr(2), 0, 1);
|
||||
AppendVop3(&code, 0x303u, 15, 240u, InlineU32(1));
|
||||
|
||||
AppendStoreVgpr(&code, 10, 0);
|
||||
AppendStoreVgpr(&code, 11, 1);
|
||||
AppendStoreVgpr(&code, 12, 2);
|
||||
AppendStoreVgpr(&code, 13, 3);
|
||||
AppendStoreVgpr(&code, 14, 4);
|
||||
AppendStoreVgpr(&code, 15, 5);
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test;
|
||||
test.name = "FloatInlineConstantF16UsesNumericValue";
|
||||
test.code = std::move(code);
|
||||
test.expected = {0xaaaa4200u, 0x3c004200u, 0xbbbb3118u};
|
||||
test.expected = {0xaaaa4200u, 0x3c004200u, 0xbbbb3118u,
|
||||
0x31183c00u, 0xcccc3c00u, 0xdddd3801u};
|
||||
test.opcodes = {O::V_MOV_B32, O::V_FMA_F16, O::V_PK_FMA_F16,
|
||||
O::V_PACK_B32_F16, O::V_ADD_NC_U16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"4.000000", 2}, {"0.159155", 1}};
|
||||
test.decoded_counts = {{"4.000000", 3}, {"0.159155", 2}};
|
||||
return test;
|
||||
}
|
||||
|
||||
@@ -25616,6 +25687,77 @@ TestCase VectorVopcSdwaCmpxClassF32CapturedExecMask() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorCmpxClassF16(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
// One raw half value for each architectural class bit, in bit order.
|
||||
constexpr std::array<u32, 10> values{
|
||||
0x7c01u, 0x7e01u, 0xfc00u, 0xbc00u, 0x8001u,
|
||||
0x8000u, 0x0000u, 0x0001u, 0x3c00u, 0x7c00u};
|
||||
TestCase test;
|
||||
test.name = wave_size == 64 ? "VectorCmpxClassF16Wave64" : "VectorCmpxClassF16Wave32";
|
||||
for (const auto value : values) {
|
||||
test.initial.insert(test.initial.end(), {0xa55a0000u | value, (value << 16u) | 0x7e01u});
|
||||
}
|
||||
test.expected = test.initial;
|
||||
auto &code = test.code;
|
||||
AppendSMovLiteral(&code, 106, 0x12345678u);
|
||||
AppendSMovLiteral(&code, 107, 0x87654321u);
|
||||
code.push_back(EncodeSMovB32(127, InlineU32(0)));
|
||||
const auto compare = [&](std::initializer_list<u32> words, u32 expected) {
|
||||
code.insert(code.end(), words);
|
||||
code.push_back(EncodeSMovB32(20, 126));
|
||||
code.push_back(EncodeSMovB32(21, 127));
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(1)));
|
||||
AppendStoreSgpr(&code, 20, static_cast<u32>(test.expected.size()));
|
||||
AppendStoreSgpr(&code, 21, static_cast<u32>(test.expected.size()) + 1u);
|
||||
test.expected.insert(test.expected.end(), {expected, 0});
|
||||
};
|
||||
for (u32 index = 0; index < values.size(); ++index) {
|
||||
AppendVMovU32(&code, 30, index * 8u);
|
||||
AppendBufferLoadDword(&code, 0, 30);
|
||||
AppendVMovU32(&code, 30, index * 8u + 4u);
|
||||
AppendBufferLoadDword(&code, 2, 30);
|
||||
for (u32 bit = 0; bit < 12; ++bit) {
|
||||
AppendVMovU32(&code, 1, bit == 11 ? 0 : 1u << bit);
|
||||
const u32 expected = bit == index ? 1 : 0;
|
||||
if ((bit & 1u) == 0) {
|
||||
compare({EncodeVopc(0x9f, Vgpr(0), 1)}, expected);
|
||||
} else {
|
||||
compare({EncodeVop3Word0(0x9f, 126), EncodeVop3Word1(Vgpr(0), Vgpr(1))}, expected);
|
||||
}
|
||||
}
|
||||
const u32 finite = index >= 3 && index <= 8 ? 1 : 0;
|
||||
compare({0x7d3f70f9u, 0x86360000u}, finite); // Captured -abs(v0), inline56.
|
||||
compare({0x7d3f70f9u, 0x86350002u}, finite); // Captured -abs(v2.word1).
|
||||
}
|
||||
AppendVMovU32(&code, 1, 1u << 8u);
|
||||
compare({EncodeVopc(0x9f, 242, 1)}, 1); // Inline1.0 encodes half0x3c00.
|
||||
AppendVMovU32(&code, 1, 1u << 6u);
|
||||
compare({EncodeVopc(0x9f, 249, 1),
|
||||
EncodeVopcSdwa(242, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 1)}, 1);
|
||||
AppendVMovU32(&code, 1, 1u << 7u);
|
||||
compare({EncodeVopc(0x9f, 249, 1),
|
||||
EncodeVopcSdwa(242, 0, 0, 1, 6, 0, 0, 0, 0, 0, 0, 1)}, 1);
|
||||
AppendVMovU32(&code, 30, 8u * 8u);
|
||||
AppendBufferLoadDword(&code, 0, 30);
|
||||
compare({EncodeVopc(0x9f, 249, 248),
|
||||
EncodeVopcSdwa(0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 1)}, 1);
|
||||
// INV_2PI as a half class mask is0x3118, including positive normal bit8.
|
||||
AppendVMovU32(&code, 1, 1u << 8u);
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(0)));
|
||||
compare({EncodeVopc(0x9f, Vgpr(0), 1)}, 0);
|
||||
AppendStoreSgpr(&code, 106, static_cast<u32>(test.expected.size()));
|
||||
AppendStoreSgpr(&code, 107, static_cast<u32>(test.expected.size()) + 1u);
|
||||
test.expected.insert(test.expected.end(), {0x12345678u, 0x87654321u});
|
||||
AppendEnd(&code);
|
||||
test.initial.resize(test.expected.size());
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_CMPX_CLASS_F16, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase Wave32VccMasksPreserveOtherHalf() {
|
||||
using O = ShaderOpcode;
|
||||
TestCase test;
|
||||
@@ -26246,16 +26388,16 @@ TestCase VectorVopcCmpxLtI16WaveMasks(u32 wave_size) {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorVopcCmpxEqU16SdwaCompactVop3ExecMask() {
|
||||
TestCase VectorVopcCmpxU16SdwaCompactVop3ExecMask(bool not_equal) {
|
||||
using O = ShaderOpcode;
|
||||
struct CompareCase {
|
||||
u32 lhs;
|
||||
u32 rhs;
|
||||
u32 incoming_exec;
|
||||
u32 expected_exec;
|
||||
u32 encoding = 0; // SDWA, compact, VOP3, sign-extended words/bytes.
|
||||
u32 encoding = 0; // SDWA, compact, VOP3, sign-extended words/bytes, literal.
|
||||
};
|
||||
const std::array<CompareCase, 16> cases{{
|
||||
const std::array<CompareCase, 21> cases{{
|
||||
{0x12340000u, 0x56780000u, 1, 1}, // Equal low half; ignore high half.
|
||||
{0xffff0001u, 0xabcd0001u, 1, 1},
|
||||
{0x00000000u, 0x12340000u, 1, 1},
|
||||
@@ -26272,10 +26414,17 @@ TestCase VectorVopcCmpxEqU16SdwaCompactVop3ExecMask() {
|
||||
{0x123456ffu, 0x0000ffffu, 1, 1, 4},
|
||||
{0x0000ff80u, 0x12348000u, 1, 1, 5}, // SEXT BYTE_1 on src1.
|
||||
{0x1234ffffu, 0x5678ffffu, 1, 1, 6}, // SEXT WORD_0 versus DWORD low half.
|
||||
{0xabcd0001u, 1u, 1, 1, 7}, // Literal source, equal low halves.
|
||||
{0xabcd0000u, 1u, 1, 0, 7},
|
||||
{0x00000000u, 1u, 0, 0}, // Neither EQ nor NE may reactivate an inactive lane.
|
||||
{0x00010000u, 0xffff0001u, 1, 1, 8}, // Select the high word of src0.
|
||||
{0xabcd0002u, 0x0002ffffu, 1, 1, 9}, // Select the high word of src1.
|
||||
}};
|
||||
const u32 opcode = not_equal ? 0xbdu : 0xbau;
|
||||
constexpr u32 vcc_hi = 0x89abcdefu;
|
||||
TestCase test;
|
||||
test.name = "VectorVopcCmpxEqU16SdwaCompactVop3ExecMask";
|
||||
test.name = not_equal ? "VectorVopcCmpxNeU16SdwaCompactVop3ExecMask"
|
||||
: "VectorVopcCmpxEqU16SdwaCompactVop3ExecMask";
|
||||
for (const auto &entry : cases) {
|
||||
test.initial.push_back(entry.lhs);
|
||||
}
|
||||
@@ -26290,26 +26439,36 @@ TestCase VectorVopcCmpxEqU16SdwaCompactVop3ExecMask() {
|
||||
AppendSMovLiteral(&code, 107, vcc_hi);
|
||||
code.push_back(EncodeSMovB32(126, InlineU32(entry.incoming_exec)));
|
||||
switch (entry.encoding) {
|
||||
case 1: code.push_back(0x7d740300u); break; // compact v0, v1
|
||||
case 2: code.insert(code.end(), {0xd4ba007eu, 0x00020300u}); break;
|
||||
case 1: code.push_back(EncodeVopc(opcode, Vgpr(0), 1u)); break;
|
||||
case 2: AppendVop3(&code, opcode, 126u, Vgpr(0), Vgpr(1)); break;
|
||||
case 3:
|
||||
code.push_back(EncodeVopc(0xba, 249u, 1u));
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u, 0u, 0u, 6u, 6u, 1u, 1u));
|
||||
break;
|
||||
case 4:
|
||||
code.push_back(EncodeVopc(0xba, 249u, 1u));
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u, 0u, 0u, 0u, 6u, 1u, 0u));
|
||||
break;
|
||||
case 5:
|
||||
code.push_back(EncodeVopc(0xba, 249u, 1u));
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u, 0u, 0u, 6u, 1u, 0u, 1u));
|
||||
break;
|
||||
case 6:
|
||||
code.push_back(EncodeVopc(0xba, 249u, 1u));
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u, 0u, 0u, 4u, 6u, 1u, 0u));
|
||||
break;
|
||||
case 7:
|
||||
code.push_back(EncodeVopc(opcode, 255u, 0u));
|
||||
code.push_back(entry.rhs);
|
||||
break;
|
||||
case 8:
|
||||
case 9:
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u, 0u, 0u, entry.encoding == 8u ? 5u : 6u,
|
||||
entry.encoding == 9u ? 5u : 6u));
|
||||
break;
|
||||
default:
|
||||
code.push_back(EncodeVopc(0xba, 249u, 1u));
|
||||
code.push_back(EncodeVopc(opcode, 249u, 1u));
|
||||
code.push_back(EncodeVopcSdwa(0u));
|
||||
break;
|
||||
}
|
||||
@@ -26319,17 +26478,27 @@ TestCase VectorVopcCmpxEqU16SdwaCompactVop3ExecMask() {
|
||||
const u32 out = static_cast<u32>(cases.size()) + i * 4u;
|
||||
AppendStoreSgprPair(&code, 20, out);
|
||||
AppendStoreSgprPair(&code, 106, out + 2u); // CMPX must not overwrite either VCC half.
|
||||
test.expected.insert(test.expected.end(),
|
||||
{entry.expected_exec, 0u, entry.rhs, vcc_hi});
|
||||
const u32 expected_exec = not_equal ? entry.incoming_exec & (entry.expected_exec ^ 1u)
|
||||
: entry.expected_exec;
|
||||
test.expected.insert(test.expected.end(), {expected_exec, 0u, entry.rhs, vcc_hi});
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD,
|
||||
O::V_CMPX_EQ_U16, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"V_CMPX_EQ_U16", cases.size()}};
|
||||
test.required_spirv = {"OpIEqual"};
|
||||
not_equal ? O::V_CMPX_NE_U16 : O::V_CMPX_EQ_U16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{not_equal ? "V_CMPX_NE_U16" : "V_CMPX_EQ_U16", cases.size()}};
|
||||
test.required_spirv = {not_equal ? "OpINotEqual" : "OpIEqual"};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase VectorVopcCmpxEqU16SdwaCompactVop3ExecMask() {
|
||||
return VectorVopcCmpxU16SdwaCompactVop3ExecMask(false);
|
||||
}
|
||||
|
||||
TestCase VectorVopcCmpxNeU16SdwaCompactVop3ExecMask() {
|
||||
return VectorVopcCmpxU16SdwaCompactVop3ExecMask(true);
|
||||
}
|
||||
|
||||
TestCase VectorVopcCmpNgtF16CapturedSdwaAndEdges() {
|
||||
using O = ShaderOpcode;
|
||||
enum Encoding { Captured, Compact, Vop3NegAbs, HighWord, ScalarDst, SdwaNegAbs };
|
||||
@@ -26852,18 +27021,15 @@ TestCase ScalarMemoryLoadVariants() {
|
||||
std::vector<u32> expected = initial;
|
||||
expected.insert(expected.end(), initial.begin(), initial.end());
|
||||
|
||||
TestCase test{"ScalarMemoryLoadVariants",
|
||||
code,
|
||||
initial,
|
||||
expected,
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::S_LOAD_DWORDX2, O::S_LOAD_DWORDX4,
|
||||
O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX16, O::S_BUFFER_LOAD_DWORD,
|
||||
O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4,
|
||||
O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.bda_mappings = {{0, 0}};
|
||||
test.required_spirv = {"get_bda_pointer"};
|
||||
return test;
|
||||
return {"ScalarMemoryLoadVariants",
|
||||
code,
|
||||
initial,
|
||||
expected,
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::S_LOAD_DWORDX2, O::S_LOAD_DWORDX4,
|
||||
O::S_LOAD_DWORDX8, O::S_LOAD_DWORDX16, O::S_BUFFER_LOAD_DWORD,
|
||||
O::S_BUFFER_LOAD_DWORDX2, O::S_BUFFER_LOAD_DWORDX4,
|
||||
O::S_BUFFER_LOAD_DWORDX8, O::S_BUFFER_LOAD_DWORDX16, O::V_MOV_B32,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase ScalarBufferOffsetAlignmentAndCarry() {
|
||||
@@ -26907,15 +27073,11 @@ TestCase ScalarLoadSignedImmediateOffsetAddsSoffset() {
|
||||
AppendStoreSgpr(&code, 1, 0);
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test{"ScalarLoadSignedImmediateOffsetAddsSoffset",
|
||||
code,
|
||||
{0x11111111u, 0x22222222u},
|
||||
{0x22222222u, 0x22222222u},
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.bda_mappings = {{0, 0}};
|
||||
test.required_spirv = {"get_bda_pointer"};
|
||||
test.forbidden_spirv = {"flattened_srt"};
|
||||
return test;
|
||||
return {"ScalarLoadSignedImmediateOffsetAddsSoffset",
|
||||
code,
|
||||
{0x11111111u, 0x22222222u},
|
||||
{0x22222222u, 0x22222222u},
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase BufferLoadStore() {
|
||||
@@ -29171,6 +29333,89 @@ TestCase FlatSubdwordLoadsApplyByteOffset() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase GlobalLoadShortD16Captured(u32 wave_size) {
|
||||
using O = ShaderOpcode;
|
||||
constexpr uint64_t GuestBase = 0x0000000110000000ull;
|
||||
constexpr u32 Marker = 0xa5a50000u;
|
||||
constexpr uint64_t ScalarBase = GuestBase - Marker - 0x70u;
|
||||
constexpr uint64_t ExecMask = 0xaaaaaaaa55555555ull;
|
||||
constexpr u32 Values[] = {0u, 0x7fffu, 0x8000u, 0xffffu};
|
||||
TestCase test;
|
||||
test.name = wave_size == 32u ? "GlobalLoadShortD16CapturedWave32"
|
||||
: "GlobalLoadShortD16CapturedWave64";
|
||||
test.initial.resize(2u * wave_size + wave_size / 2u);
|
||||
test.expected.resize(2u * wave_size);
|
||||
for (u32 lane = 0; lane < wave_size; ++lane) {
|
||||
const u32 value = Values[lane % std::size(Values)];
|
||||
test.initial[2u * wave_size + lane / 2u] |= value << (16u * (lane % 2u));
|
||||
test.expected[lane] = Marker | value;
|
||||
test.expected[wave_size + lane] = Marker |
|
||||
((ExecMask & (uint64_t{1} << lane)) != 0u ? value : lane * 2u);
|
||||
}
|
||||
auto &code = test.code;
|
||||
AppendSMovLiteral(&code, 2, static_cast<u32>(ScalarBase));
|
||||
AppendSMovLiteral(&code, 3, static_cast<u32>(ScalarBase >> 32u));
|
||||
code.push_back(EncodeVop2(0x1a, 6, InlineU32(1), 0));
|
||||
AppendVMovLiteral(&code, 5, Marker);
|
||||
code.push_back(EncodeVop2(0x25, 6, Vgpr(5), 6));
|
||||
for (bool masked : {false, true}) {
|
||||
code.push_back(EncodeVop1(0x01, 5, Vgpr(6)));
|
||||
if (masked) {
|
||||
code.push_back(EncodeSop1(0x04, 8, 126));
|
||||
AppendSMovLiteral(&code, 126, static_cast<u32>(ExecMask));
|
||||
AppendSMovLiteral(&code, 127, static_cast<u32>(ExecMask >> 32u));
|
||||
}
|
||||
// Captured GLOBAL_LOAD_SHORT_D16: v5 supplies the byte offset and destination.
|
||||
code.insert(code.end(), {0xdc908070u, 0x05020005u});
|
||||
if (masked) code.push_back(EncodeSop1(0x04, 126, 8));
|
||||
AppendStoreVgprAtLaneDwordOffset(&code, 5, 0, masked ? wave_size : 0u);
|
||||
}
|
||||
AppendEnd(&code);
|
||||
test.bda_mappings = {{GuestBase, 8u * wave_size}};
|
||||
test.compute_info.threads_num[0] = wave_size;
|
||||
test.compute_info.threads_num[1] = 1;
|
||||
test.compute_info.threads_num[2] = 1;
|
||||
test.compute_info.thread_ids_num = 1;
|
||||
test.compute_info.wave_size = wave_size;
|
||||
test.has_compute_info = true;
|
||||
test.opcodes = {O::S_MOV_B32, O::S_MOV_B64, O::V_MOV_B32, O::V_LSHLREV_B32,
|
||||
O::V_ADD_NC_U32, O::FLAT_LOAD_SHORT_D16,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
|
||||
test.decoded_counts = {{"FLAT_LOAD_SHORT_D16", 2}};
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase FlatLoadShortD16AddressSegments() {
|
||||
using O = ShaderOpcode;
|
||||
constexpr uint64_t GuestBase = 0x0000000110000000ull;
|
||||
std::vector<u32> code;
|
||||
AppendVMovLiteral(&code, 20, static_cast<u32>(GuestBase) + 2u);
|
||||
AppendVMovLiteral(&code, 21, static_cast<u32>(GuestBase >> 32u));
|
||||
for (u32 segment : {0u, 2u}) {
|
||||
AppendVMovLiteral(&code, 3, 0x12345678u);
|
||||
code.push_back(EncodeFlat0(0x24, segment, segment == 0u ? 0u : 0xffeu));
|
||||
code.push_back(EncodeFlat1(3, 0x7d, 0, 20));
|
||||
AppendStoreVgpr(&code, 3, segment == 0u ? 0u : 1u);
|
||||
}
|
||||
AppendVMovU32(&code, 20, 0u);
|
||||
AppendVMovLiteral(&code, 3, 0xbeef5678u);
|
||||
code.push_back(EncodeFlat0(0x1c, 1, 0));
|
||||
code.push_back(EncodeFlat1(0, 0x7d, 3, 20));
|
||||
AppendVMovLiteral(&code, 3, 0x98761234u);
|
||||
code.push_back(EncodeFlat0(0x24, 1, 2));
|
||||
code.push_back(EncodeFlat1(3, 0x7d, 0, 20));
|
||||
AppendStoreVgpr(&code, 3, 2);
|
||||
AppendEnd(&code);
|
||||
TestCase test{"FlatLoadShortD16AddressSegments", code,
|
||||
{0, 0, 0, 0, 0x80017fffu},
|
||||
{0x12348001u, 0x12347fffu, 0x9876beefu},
|
||||
{O::V_MOV_B32, O::FLAT_LOAD_SHORT_D16, O::FLAT_STORE_DWORD,
|
||||
O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.bda_mappings = {{GuestBase, 16}};
|
||||
test.compute_info.scratch_size_dwords = 1;
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase BranchVccnzUsesCarryProducedWaveMask() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
@@ -29206,27 +29451,23 @@ TestCase BranchVccnzUsesCarryProducedWaveMask() {
|
||||
return test;
|
||||
}
|
||||
|
||||
TestCase ScalarLoadAlignsComponents() {
|
||||
TestCase ScalarLoadAlignsComponentsAndMasksAddress() {
|
||||
using O = ShaderOpcode;
|
||||
|
||||
std::vector<u32> code;
|
||||
AppendSMovLiteral(&code, 0, 1u);
|
||||
AppendSMovLiteral(&code, 2, 3u);
|
||||
AppendSMovLiteral(&code, 3, 0u);
|
||||
AppendSMovLiteral(&code, 3, 0xffff0000u);
|
||||
code.push_back(EncodeSmem0(0x00, 1, 1));
|
||||
code.push_back(EncodeSmem1(3, 0));
|
||||
AppendStoreSgpr(&code, 1, 0);
|
||||
AppendEnd(&code);
|
||||
|
||||
TestCase test{"ScalarLoadAlignsComponents",
|
||||
code,
|
||||
{0x11111111u, 0x22222222u},
|
||||
{0x11111111u, 0x22222222u},
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
test.bda_mappings = {{0, 0}};
|
||||
test.required_spirv = {"get_bda_pointer"};
|
||||
test.forbidden_spirv = {"flattened_srt"};
|
||||
return test;
|
||||
return {"ScalarLoadAlignsComponentsAndMasksAddress",
|
||||
code,
|
||||
{0x11111111u, 0x22222222u},
|
||||
{0x11111111u, 0x22222222u},
|
||||
{O::S_MOV_B32, O::S_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
|
||||
}
|
||||
|
||||
TestCase ScalarLoadAlignsDynamicBase() {
|
||||
@@ -34484,6 +34725,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(Vop2SdwaAshrrevCapturedWord0SignExtends);
|
||||
cases.push_back(Vop2SdwaMaxI32CapturedHighWord(32));
|
||||
cases.push_back(Vop2SdwaMaxI32CapturedHighWord(64));
|
||||
cases.push_back(Vop2SdwaMulI24Destinations(32));
|
||||
cases.push_back(Vop2SdwaMulI24Destinations(64));
|
||||
AddCase(Vop2SdwaLshrrevCapturedByte1Source);
|
||||
AddCase(Vop2SdwaSubNcPreservesByteAndWordDestinations);
|
||||
AddCase(Vop3CvtPkI16I32Captured);
|
||||
@@ -34591,6 +34834,8 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(VectorVop3CmpxNeI64CapturedExecMask);
|
||||
AddCase(VectorCompareClassF32);
|
||||
AddCase(VectorVopcSdwaCmpxClassF32CapturedExecMask);
|
||||
cases.push_back(VectorCmpxClassF16(32));
|
||||
cases.push_back(VectorCmpxClassF16(64));
|
||||
AddCase(VectorCompareF16Ops);
|
||||
AddCase(Wave32VccMasksPreserveOtherHalf);
|
||||
AddCase(Vop2SdwaCndmaskSourceModifier);
|
||||
@@ -34609,6 +34854,7 @@ std::vector<TestCase> MakeCases() {
|
||||
cases.push_back(VectorVopcCmpxLtI16WaveMasks(wave_size));
|
||||
}
|
||||
AddCase(VectorVopcCmpxEqU16SdwaCompactVop3ExecMask);
|
||||
AddCase(VectorVopcCmpxNeU16SdwaCompactVop3ExecMask);
|
||||
AddCase(VectorVopcCmpNgtF16CapturedSdwaAndEdges);
|
||||
AddCase(VectorVopcCmpNltF16CapturedSdwaAndEdges);
|
||||
AddCase(VectorVopcCmpxNgtF16CapturedSdwaExecMask);
|
||||
@@ -34624,7 +34870,7 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(ScalarMemoryLoadVariants);
|
||||
AddCase(ScalarBufferOffsetAlignmentAndCarry);
|
||||
AddCase(ScalarLoadSignedImmediateOffsetAddsSoffset);
|
||||
AddCase(ScalarLoadAlignsComponents);
|
||||
AddCase(ScalarLoadAlignsComponentsAndMasksAddress);
|
||||
AddCase(ScalarLoadAlignsDynamicBase);
|
||||
cases.push_back(ScalarBufferFromLoopReadlane(32));
|
||||
cases.push_back(ScalarBufferFromLoopReadlane(64));
|
||||
@@ -34720,6 +34966,9 @@ std::vector<TestCase> MakeCases() {
|
||||
AddCase(TBufferStoreVariants);
|
||||
AddCase(FlatLoadVariants);
|
||||
AddCase(FlatSubdwordLoadsApplyByteOffset);
|
||||
cases.push_back(GlobalLoadShortD16Captured(32));
|
||||
cases.push_back(GlobalLoadShortD16Captured(64));
|
||||
AddCase(FlatLoadShortD16AddressSegments);
|
||||
AddCase(FlatVirtualAddressRebasesGuestAllocation);
|
||||
AddCase(GlobalSignedImmediateRebasesBeforeSaddr);
|
||||
AddCase(FlatSegmentIgnoresSaddrAndMasksOffsetMsb);
|
||||
@@ -37532,19 +37781,17 @@ void CheckResourcePlanHandoff() {
|
||||
ValidateSpirv(name, compiled.spirv);
|
||||
const auto &bindings = compiled.program.bindings;
|
||||
Require(name, "GPU data requirements",
|
||||
bindings.user_data_registers == (numeric_read ? std::vector<u32>{0, 1}
|
||||
: std::vector<u32>{}) &&
|
||||
bindings.memory_offset_count == 1 &&
|
||||
FindBinding(bindings, DescriptorBindingKind::FlattenedSrt) == nullptr &&
|
||||
(FindBinding(bindings, DescriptorBindingKind::BdaPagetable) != nullptr) == numeric_read,
|
||||
"descriptor-only shader retained SRT uploads or native scalar data lost its address");
|
||||
bindings.user_data_registers.empty() && bindings.memory_offset_count == 1 &&
|
||||
(FindBinding(bindings, DescriptorBindingKind::FlattenedSrt) != nullptr) ==
|
||||
numeric_read,
|
||||
"descriptor-only shader retained SRT uploads or live scalar data was removed");
|
||||
|
||||
memory[0] = 0x2000u;
|
||||
memory[4] = 13u;
|
||||
Require(name, "independent host plan",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.buffers[0].dwords[0] == 0x2000u &&
|
||||
snapshot.flattened_srt.size() == 4,
|
||||
(!numeric_read || snapshot.flattened_srt.back() == 13u),
|
||||
"compiled shader cleanup invalidated or froze the host resource plan");
|
||||
}
|
||||
std::printf("[host] %-32s ok\n", name);
|
||||
@@ -39908,6 +40155,11 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, VectorVopcCmpxEqU16SdwaCompactVop3ExecMask());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cmpx-ne-u16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorVopcCmpxNeU16SdwaCompactVop3ExecMask());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cmpx-lt-i16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorVopcCmpxLtI16CapturedSdwaExecMask());
|
||||
@@ -39924,6 +40176,17 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, VectorVopcSdwaCmpxClassF32CapturedExecMask());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cmpx-class-f16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorCmpxClassF16(32));
|
||||
RunCase(&vulkan, VectorCmpxClassF16(64));
|
||||
RunCase(&vulkan, VectorCompareClassF32());
|
||||
RunCase(&vulkan, VectorVopcSdwaCmpxClassF32CapturedExecMask());
|
||||
RunCase(&vulkan, FloatInlineConstantF16UsesNumericValue());
|
||||
RunCase(&vulkan, VectorCompareF16Ops());
|
||||
RunCase(&vulkan, VectorMinMaxF16Ops());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cmp-eq-f64-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, VectorCompareF64Edges());
|
||||
@@ -40007,6 +40270,18 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--flat-d16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(32));
|
||||
RunCase(&vulkan, GlobalLoadShortD16Captured(64));
|
||||
RunCase(&vulkan, FlatLoadShortD16AddressSegments());
|
||||
RunCase(&vulkan, FlatSubdwordLoadsApplyByteOffset());
|
||||
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
|
||||
RunCase(&vulkan, GlobalSignedImmediateRebasesBeforeSaddr());
|
||||
RunCase(&vulkan, FlatSegmentIgnoresSaddrAndMasksOffsetMsb());
|
||||
RunCase(&vulkan, ScratchIsPrivatePerInvocation());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--indirect-buffer-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, ScalarBufferFromLoopReadlane(32));
|
||||
@@ -40283,6 +40558,13 @@ int main(int argc, char **argv) {
|
||||
RunCase(&vulkan, VectorAddcWritesPerLaneCarryOut());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--sdwa-mul-i24-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, Vop2SdwaMulI24Destinations(32));
|
||||
RunCase(&vulkan, Vop2SdwaMulI24Destinations(64));
|
||||
RunCase(&vulkan, VectorIntegerOps());
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::strcmp(argv[1], "--cvt-pk-i16-only") == 0) {
|
||||
VulkanHarness vulkan;
|
||||
RunCase(&vulkan, Vop3CvtPkI16I32Captured());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4861,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;
|
||||
|
||||
@@ -14784,6 +14857,7 @@ int main() {
|
||||
TestNewShaderRecompilerF64AluEncodings();
|
||||
TestNewShaderRecompilerVop1SdwaBfrev();
|
||||
TestNewShaderRecompilerVop1SdwaNotDestination();
|
||||
TestVop2SdwaMulI24Destination();
|
||||
TestNewShaderRecompilerScalarMemoryBindingDomains();
|
||||
// Opcode semantics and optimized SPIR-V are exercised by
|
||||
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks
|
||||
@@ -14796,6 +14870,7 @@ int main() {
|
||||
TestSopkCompareImmediateExtension();
|
||||
TestNativeStoreCompletion();
|
||||
TestNewShaderRecompilerCapturedVopcSdwaCmpxClass();
|
||||
TestVopcCmpxClassF16Decoder();
|
||||
TestNewShaderRecompilerCapturedVopcSdwaCmpxLtU16();
|
||||
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
|
||||
TestNewShaderRecompilerRejectsDppOn64BitCompares();
|
||||
|
||||
Reference in New Issue
Block a user