Compare commits

..

8 Commits

Author SHA1 Message Date
nmzik 77d7722173 shader: disable explicit MAD denormal checks for performance testing
Comment out the five explicit flush checks and record the substantial 3DMiniGolf menu FPS drop on NVIDIA RTX 5080 Laptop GPU. Preserve separate multiply/add rounding and both NoContraction decorations.

The matched runs used silent logging, no debugger, no Vulkan validation, and the cd9fc6ff revert on both revisions. Native denormal behavior remains GPU-dependent.
2026-10-05 18:39:31 +04:00
nmzik 266c12cec6 Revert "shader: keep scalar data native and preserve raw addresses"
This reverts commit cd9fc6ff2d.
2026-10-05 18:39:31 +04:00
nmzik 7a0e1dafd7 shader: classify native half values with shared operand decoding 2026-10-05 18:39:31 +04:00
nmzik 1d41ddb378 shader: allow RDNA2 SDWA partial destinations for i24 multiply 2026-10-05 18:39:31 +04:00
nmzik 5c83060481 shader: resolve masked image keys in strided descriptor records 2026-10-05 14:43:21 +04:00
nmzik 72e4989b1e Add trophy notification support
Co-authored-by: Felipe <54341095+Pipe530@users.noreply.github.com>
2026-10-05 10:24:12 +04:00
fxpw 1d55272467 shader: support V_CMPX_NE_U16 2026-10-05 09:25:19 +04:00
nmzik 2b1b7ed0e4 shader: FLAT opcode=0x24 2026-10-05 08:48:20 +04:00
45 changed files with 2585 additions and 1931 deletions
+10 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.18...4.3)
cmake_minimum_required(VERSION 3.19...4.3)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "Minimum policy version for bundled dependencies")
@@ -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.
+1
View File
@@ -27,6 +27,7 @@ target_link_libraries(common
kyty_git_version
spdlog::spdlog
ZArchive::zarchive
nlohmann_json::nlohmann_json
)
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
+4
View File
@@ -156,6 +156,10 @@ bool TessellationEnabled() {
return g_config->tessellation_enabled;
}
bool TrophyEnabled() {
return g_config->trophy_enabled;
}
bool PlayGoHackEnabled() {
return g_config->playgo_hack_enabled;
}
+2
View File
@@ -74,6 +74,7 @@ struct ConfigOptions {
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool playgo_hack_enabled = false;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool red_zone_protection_enabled = false;
@@ -123,6 +124,7 @@ bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool TessellationEnabled();
bool TrophyEnabled();
bool PlayGoHackEnabled();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool RedZoneProtectionEnabled();
+264
View File
@@ -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
+48
View File
@@ -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_
+227 -12
View File
@@ -3,14 +3,17 @@
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_vulkan.h"
#include "libs/controller.h"
#include "libs/dialog.h"
#include "libs/ime.h"
#include "libs/imeDialog.h"
#include "stb_image.h"
#include <algorithm>
#include <array>
@@ -18,12 +21,17 @@
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <deque>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace Libs::Graphics {
@@ -178,6 +186,14 @@ bool g_input_lifecycle_active = false;
bool g_controller_captured = false;
OverlaySession g_session;
SDL_Window* g_input_window = nullptr;
struct TrophyNotification {
std::string name;
int32_t grade;
std::vector<std::byte> icon_png;
};
std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
void ClearInputEvents() {
std::scoped_lock lock(g_input_mutex);
@@ -467,8 +483,9 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
const auto core = CoreIme::GetVisualState();
const auto dialog = DialogIme::GetVisualState();
const auto system = SystemDialog::GetVisualState();
return {core.active || dialog.active || system.active,
core.revision + dialog.revision + system.revision};
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@@ -599,12 +616,34 @@ bool ProcessSystemOverlayInput(const SDL_Event& event) {
}
}
void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
std::span<const std::byte> icon_png) {
std::scoped_lock lock(g_trophy_notification_mutex);
g_trophy_notifications.push_back(
{std::string(name), grade, std::vector<std::byte>(icon_png.begin(), icon_png.end())});
g_trophy_notification_active.store(true, std::memory_order_release);
}
struct SystemOverlay::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(GraphicContext& context): graphics(context) {}
~Impl() {
SDL_DestroyAudioStream(trophy_sound_stream);
SDL_free(trophy_sound_data);
if (trophy_sound_initialized) {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
ReleaseVulkan();
if (imgui_context != nullptr) {
ImGui::SetCurrentContext(imgui_context);
if (trophy_notification_image) {
ImGui::UnregisterUserTexture(trophy_notification_image.get());
}
for (const auto& texture: retired_trophy_images) {
ImGui::UnregisterUserTexture(texture.get());
}
ImGui::DestroyContext(imgui_context);
}
}
@@ -631,6 +670,75 @@ struct SystemOverlay::Impl {
style.ItemSpacing = {8.0f, 8.0f};
}
void PlayTrophySound() {
if (!trophy_sound_attempted) {
trophy_sound_attempted = true;
trophy_sound_initialized = SDL_InitSubSystem(SDL_INIT_AUDIO);
if (!trophy_sound_initialized) {
LOGF("[Trophy] could not initialize audio: %s\n", SDL_GetError());
return;
}
const char* base_path = SDL_GetBasePath();
if (base_path == nullptr) {
LOGF("[Trophy] could not locate sound: %s\n", SDL_GetError());
return;
}
const std::string path = std::string(base_path) + "assets/sounds/trophy-unlock.wav";
SDL_AudioSpec spec {};
if (!SDL_LoadWAV(path.c_str(), &spec, &trophy_sound_data, &trophy_sound_size)) {
LOGF("[Trophy] could not load %s: %s\n", path.c_str(), SDL_GetError());
return;
}
if (trophy_sound_size > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
LOGF("[Trophy] sound is too large: %s\n", path.c_str());
return;
}
trophy_sound_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec, nullptr, nullptr);
if (trophy_sound_stream == nullptr) {
LOGF("[Trophy] could not open audio stream: %s\n", SDL_GetError());
return;
}
}
if (trophy_sound_stream != nullptr &&
(!SDL_PutAudioStreamData(trophy_sound_stream, trophy_sound_data,
static_cast<int>(trophy_sound_size)) ||
!SDL_ResumeAudioStreamDevice(trophy_sound_stream))) {
LOGF("[Trophy] could not play sound: %s\n", SDL_GetError());
}
}
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
if (encoded.empty() ||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
const auto* data = reinterpret_cast<const stbi_uc*>(encoded.data());
const int size = static_cast<int>(encoded.size());
int width = 0;
int height = 0;
if (!stbi_info_from_memory(data, size, &width, &height, nullptr) || width <= 0 ||
height <= 0 || width > 512 || height > 512) {
LOGF("[Trophy] invalid icon or dimensions exceed 512x512\n");
return nullptr;
}
auto* pixels = stbi_load_from_memory(data, size, &width, &height, nullptr, 4);
if (pixels == nullptr) {
LOGF("[Trophy] could not decode trophy icon: %s\n", stbi_failure_reason());
return nullptr;
}
auto texture = std::make_unique<ImTextureData>();
texture->Create(ImTextureFormat_RGBA32, width, height);
std::memcpy(texture->Pixels, pixels,
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
texture->UseColors = true;
stbi_image_free(pixels);
ImGui::RegisterUserTexture(texture.get());
return texture;
}
void EnsureVulkan(vk::Format format, uint32_t image_count) {
EnsureContext();
if (vulkan_initialized) {
@@ -952,9 +1060,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);
+1
View File
@@ -96,6 +96,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
if(NOT KYTY_MACDEPLOYQT)
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>760</width>
<width>840</width>
<height>620</height>
</rect>
</property>
@@ -182,7 +182,22 @@
</layout>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
+4
View File
@@ -122,6 +122,7 @@ public:
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -154,6 +155,7 @@ public:
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
@@ -201,6 +203,7 @@ public:
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -244,6 +247,7 @@ public:
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s->value("trophy_enabled", trophy_enabled).toBool();
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
console_language = s->value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -28,6 +28,7 @@ public:
~ConfigurationListWidget() override;
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
void SetRuntimeDirectory(const QString& directory) { m_runtime_directory = directory; }
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
@@ -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;
-1
View File
@@ -12,7 +12,6 @@ namespace GameContent {
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
+3 -2
View File
@@ -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;
};
+3 -3
View File
@@ -123,6 +123,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
m_info(info) {
m_ui->setupUi(this);
setMinimumWidth(width());
InitGameDirectories();
m_ui->controller_group->setVisible(false);
@@ -164,9 +165,6 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
m_ui->lineEdit_printf_file->setEnabled(log == Configuration::LogDirection::File);
});
// Keep the controls at a usable minimum while allowing the settings window
// and its expanding fields to use any additional space the user gives them.
layout()->setSizeConstraint(QLayout::SetMinimumSize);
setSizeGripEnabled(true);
restoreGeometry(g_last_geometry);
@@ -276,6 +274,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
m_ui->comboBox_console_language->addItems(CONSOLE_LANGUAGE_NAMES);
@@ -429,6 +428,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
+2 -1
View File
@@ -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() {
+2
View File
@@ -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";
}
+70 -478
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -84,6 +84,8 @@ static void PrintUsage() {
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
::printf(
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
::printf(
" --trophy-notifications <true|false> Show trophy unlock toasts and play their sound.\n");
::printf(" --playgo-hack Use the supplied PlayGo stub fallback.\n");
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
::printf(" --redzone Protect the guest SysV red zone.\n");
@@ -421,6 +423,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--trophy-notifications") {
if (!ParseBool(value, options.config.trophy_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--keymap") {
const auto split = value.find('=');
if (split == std::string::npos || split == 0 || split + 1 == value.size()) {
+7 -41
View File
@@ -32,8 +32,7 @@ AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) {
return *result;
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address,
bool buffer = false) {
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@@ -42,22 +41,16 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan SrtPlan(uint64_t address,
auto &value_block = AddValueBlock(program);
MemoryInfo memory;
memory.kind = buffer ? ResourceKind::ScalarBuffer : ResourceKind::ScalarAddress;
memory.kind = ResourceKind::ScalarAddress;
memory.planning_only = true;
program.memory_info.push_back(memory);
const auto low = Value(static_cast<uint32_t>(address));
const auto high = Value(static_cast<uint32_t>(address >> 32u));
auto &handle = buffer
? value_block.AppendNewInst(ValueOpcode::GetBufferResource,
{low, high, Value(4u), Value(0u)})
: value_block.AppendNewInst(ValueOpcode::GetAddressResource,
{low, high});
auto &raw = buffer
? value_block.AppendNewInst(ValueOpcode::ReadConstBuffer,
{Value(&handle), Value(0u)})
: value_block.AppendNewInst(
ValueOpcode::LoadAddressU32,
{Value(&handle), Value(0u), Value(0u), Value(true)});
auto &handle =
value_block.AppendNewInst(ValueOpcode::GetAddressResource, {low, high});
auto &raw = value_block.AppendNewInst(
ValueOpcode::LoadAddressU32,
{Value(&handle), Value(0u), Value(0u), Value(true)});
raw.SetFlags(MemoryFlags{.index = 0, .pc = 0x40});
program.srt_reads.push_back({Value(&raw), 0});
@@ -175,32 +168,6 @@ void TestMappedSrtUsesDirectReaderByDefault() {
"cache rematerialization did not use the direct reader by default");
}
void TestRawScalarAddressPreservesHighBits() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
constexpr uint64_t address = 0xabcd000156780003ull;
for (const bool buffer : {false, true}) {
auto plan = SrtPlan(address, buffer);
uint64_t observed = 0;
const SrtRuntime runtime{
.read_memory = [](void *userdata, uint64_t requested,
std::span<uint32_t> words) {
if (words.size() != 1) return false;
*static_cast<uint64_t *>(userdata) = requested;
words[0] = 0x12345678;
return true;
},
.userdata = &observed};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"scalar address materialization failed");
Check(observed == (buffer ? 0x0000000156780000ull : address & ~uint64_t{3}),
"raw pointer high bits and buffer descriptor flags were conflated");
Check(snapshot.flattened_srt == std::vector<uint32_t>{0x12345678},
"scalar address read did not reach its flattened slot");
}
}
void TestIntegerRuntimeValueFollowsSrtReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = SrtPlan(0x10000);
@@ -599,7 +566,6 @@ void DbgExit(int) { std::abort(); }
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestRawScalarAddressPreservesHighBits();
TestIntegerRuntimeValueFollowsSrtReads();
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
+47 -78
View File
@@ -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);
+340 -58
View File
@@ -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());
+75
View File
@@ -3668,6 +3668,39 @@ void TestNewShaderRecompilerVop1SdwaBfrev() {
}
}
void TestVop2SdwaMulI24Destination() {
using namespace ShaderRecompiler::Decoder;
const uint32_t captured[][2] = {
{0x121818f9u, 0x04861488u}, {0x123434f9u, 0x0486146au}};
for (uint32_t index = 0; index < 2; ++index) {
Instruction decoded;
DecodeInstruction(captured[index], 0, decoded);
const uint32_t reg = index == 0 ? 12u : 26u;
Check(decoded.opcode == Opcode::V_MUL_I32_I24 && decoded.word_count == 2 &&
decoded.dst.kind == OperandKind::Vgpr && decoded.dst.reg == reg &&
decoded.dst.sdwa_sel == 4 && decoded.dst.sdwa_dst_unused == 2 &&
decoded.dst.explicit_sdwa_dst && !decoded.dst.clamp &&
decoded.dst.omod == 0 && decoded.src0.sdwa_sel == 6 &&
decoded.src1.kind == OperandKind::Vgpr && decoded.src1.reg == reg &&
decoded.src1.sdwa_sel == 4 && !decoded.src1.sdwa_sext,
"captured V_MUL_I32_I24 SDWA destination/source metadata is incorrect");
Check(index == 0 ? decoded.src0.kind == OperandKind::IntegerInlineConstant &&
decoded.src0.value == 8
: decoded.src0.kind == OperandKind::VccLo,
"captured V_MUL_I32_I24 SDWA scalar source is incorrect");
}
for (const uint32_t modifier : {
0x04861788u, 0x04861c88u, 0x04871488u, 0x07861488u,
0x04961488u, 0x04a61488u, 0x14861488u, 0x24861488u,
0x04863488u, 0x04865488u}) {
const uint32_t invalid[] = {captured[0][0], modifier};
Instruction decoded;
DecodeInstruction(invalid, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.word_count == 2,
"V_MUL_I32_I24 SDWA accepted reserved selectors or float modifiers");
}
}
void TestNewShaderRecompilerVop1SdwaNotDestination() {
auto options = MakeCompileOptions(ShaderType::Pixel);
@@ -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();