diff options
Diffstat (limited to 'src')
35 files changed, 1989 insertions, 743 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5462decee..877a9e353 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -70,6 +70,8 @@ add_library(core STATIC file_sys/sdmc_factory.h file_sys/submission_package.cpp file_sys/submission_package.h + file_sys/system_archive/mii_model.cpp + file_sys/system_archive/mii_model.h file_sys/system_archive/ng_word.cpp file_sys/system_archive/ng_word.h file_sys/system_archive/system_archive.cpp diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp index 6dd633363..46aceec3d 100644 --- a/src/core/crypto/key_manager.cpp +++ b/src/core/crypto/key_manager.cpp @@ -37,6 +37,7 @@ namespace Core::Crypto { constexpr u64 CURRENT_CRYPTO_REVISION = 0x5; +constexpr u64 FULL_TICKET_SIZE = 0x400; using namespace Common; @@ -55,6 +56,99 @@ const std::map<std::pair<S128KeyType, u64>, std::string> KEYS_VARIABLE_LENGTH{ {{S128KeyType::KeyblobMAC, 0}, "keyblob_mac_key_"}, }; +namespace { +template <std::size_t Size> +bool IsAllZeroArray(const std::array<u8, Size>& array) { + return std::all_of(array.begin(), array.end(), [](const auto& elem) { return elem == 0; }); +} +} // namespace + +u64 GetSignatureTypeDataSize(SignatureType type) { + switch (type) { + case SignatureType::RSA_4096_SHA1: + case SignatureType::RSA_4096_SHA256: + return 0x200; + case SignatureType::RSA_2048_SHA1: + case SignatureType::RSA_2048_SHA256: + return 0x100; + case SignatureType::ECDSA_SHA1: + case SignatureType::ECDSA_SHA256: + return 0x3C; + } + UNREACHABLE(); +} + +u64 GetSignatureTypePaddingSize(SignatureType type) { + switch (type) { + case SignatureType::RSA_4096_SHA1: + case SignatureType::RSA_4096_SHA256: + case SignatureType::RSA_2048_SHA1: + case SignatureType::RSA_2048_SHA256: + return 0x3C; + case SignatureType::ECDSA_SHA1: + case SignatureType::ECDSA_SHA256: + return 0x40; + } + UNREACHABLE(); +} + +SignatureType Ticket::GetSignatureType() const { + if (auto ticket = std::get_if<RSA4096Ticket>(&data)) { + return ticket->sig_type; + } + if (auto ticket = std::get_if<RSA2048Ticket>(&data)) { + return ticket->sig_type; + } + if (auto ticket = std::get_if<ECDSATicket>(&data)) { + return ticket->sig_type; + } + + UNREACHABLE(); +} + +TicketData& Ticket::GetData() { + if (auto ticket = std::get_if<RSA4096Ticket>(&data)) { + return ticket->data; + } + if (auto ticket = std::get_if<RSA2048Ticket>(&data)) { + return ticket->data; + } + if (auto ticket = std::get_if<ECDSATicket>(&data)) { + return ticket->data; + } + + UNREACHABLE(); +} + +const TicketData& Ticket::GetData() const { + if (auto ticket = std::get_if<RSA4096Ticket>(&data)) { + return ticket->data; + } + if (auto ticket = std::get_if<RSA2048Ticket>(&data)) { + return ticket->data; + } + if (auto ticket = std::get_if<ECDSATicket>(&data)) { + return ticket->data; + } + + UNREACHABLE(); +} + +u64 Ticket::GetSize() const { + const auto sig_type = GetSignatureType(); + + return sizeof(SignatureType) + GetSignatureTypeDataSize(sig_type) + + GetSignatureTypePaddingSize(sig_type) + sizeof(TicketData); +} + +Ticket Ticket::SynthesizeCommon(Key128 title_key, const std::array<u8, 16>& rights_id) { + RSA2048Ticket out{}; + out.sig_type = SignatureType::RSA_2048_SHA256; + out.data.rights_id = rights_id; + out.data.title_key_common = title_key; + return Ticket{out}; +} + Key128 GenerateKeyEncryptionKey(Key128 source, Key128 master, Key128 kek_seed, Key128 key_seed) { Key128 out{}; @@ -135,6 +229,27 @@ void KeyManager::DeriveGeneralPurposeKeys(std::size_t crypto_revision) { } } +RSAKeyPair<2048> KeyManager::GetETicketRSAKey() const { + if (IsAllZeroArray(eticket_extended_kek) || !HasKey(S128KeyType::ETicketRSAKek)) + return {}; + + const auto eticket_final = GetKey(S128KeyType::ETicketRSAKek); + + std::vector<u8> extended_iv(eticket_extended_kek.begin(), eticket_extended_kek.begin() + 0x10); + std::array<u8, 0x230> extended_dec{}; + AESCipher<Key128> rsa_1(eticket_final, Mode::CTR); + rsa_1.SetIV(extended_iv); + rsa_1.Transcode(eticket_extended_kek.data() + 0x10, eticket_extended_kek.size() - 0x10, + extended_dec.data(), Op::Decrypt); + + RSAKeyPair<2048> rsa_key{}; + std::memcpy(rsa_key.decryption_key.data(), extended_dec.data(), rsa_key.decryption_key.size()); + std::memcpy(rsa_key.modulus.data(), extended_dec.data() + 0x100, rsa_key.modulus.size()); + std::memcpy(rsa_key.exponent.data(), extended_dec.data() + 0x200, rsa_key.exponent.size()); + + return rsa_key; +} + Key128 DeriveKeyblobMACKey(const Key128& keyblob_key, const Key128& mac_source) { AESCipher<Key128> mac_cipher(keyblob_key, Mode::ECB); Key128 mac_key{}; @@ -237,7 +352,7 @@ Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, KeyManager& ke return Loader::ResultStatus::Success; } -std::vector<TicketRaw> GetTicketblob(const FileUtil::IOFile& ticket_save) { +std::vector<Ticket> GetTicketblob(const FileUtil::IOFile& ticket_save) { if (!ticket_save.IsOpen()) return {}; @@ -246,14 +361,14 @@ std::vector<TicketRaw> GetTicketblob(const FileUtil::IOFile& ticket_save) { return {}; } - std::vector<TicketRaw> out; + std::vector<Ticket> out; for (std::size_t offset = 0; offset + 0x4 < buffer.size(); ++offset) { if (buffer[offset] == 0x4 && buffer[offset + 1] == 0x0 && buffer[offset + 2] == 0x1 && buffer[offset + 3] == 0x0) { out.emplace_back(); auto& next = out.back(); - std::memcpy(&next, buffer.data() + offset, sizeof(TicketRaw)); - offset += next.size(); + std::memcpy(&next, buffer.data() + offset, sizeof(Ticket)); + offset += FULL_TICKET_SIZE; } } @@ -305,29 +420,23 @@ static std::optional<u64> FindTicketOffset(const std::array<u8, size>& data) { return offset; } -std::optional<std::pair<Key128, Key128>> ParseTicket(const TicketRaw& ticket, +std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket, const RSAKeyPair<2048>& key) { - u32 cert_authority; - std::memcpy(&cert_authority, ticket.data() + 0x140, sizeof(cert_authority)); - if (cert_authority == 0) + const auto issuer = ticket.GetData().issuer; + if (issuer == std::array<u8, 0x40>{}) return {}; - if (cert_authority != Common::MakeMagic('R', 'o', 'o', 't')) { - LOG_INFO(Crypto, - "Attempting to parse ticket with non-standard certificate authority {:08X}.", - cert_authority); + if (issuer[0] != 'R' || issuer[1] != 'o' || issuer[2] != 'o' || issuer[3] != 't') { + LOG_INFO(Crypto, "Attempting to parse ticket with non-standard certificate authority."); } - Key128 rights_id; - std::memcpy(rights_id.data(), ticket.data() + 0x2A0, sizeof(Key128)); + Key128 rights_id = ticket.GetData().rights_id; if (rights_id == Key128{}) return {}; - Key128 key_temp{}; - - if (!std::any_of(ticket.begin() + 0x190, ticket.begin() + 0x280, [](u8 b) { return b != 0; })) { - std::memcpy(key_temp.data(), ticket.data() + 0x180, key_temp.size()); - return std::make_pair(rights_id, key_temp); + if (!std::any_of(ticket.GetData().title_key_common_pad.begin(), + ticket.GetData().title_key_common_pad.end(), [](u8 b) { return b != 0; })) { + return std::make_pair(rights_id, ticket.GetData().title_key_common); } mbedtls_mpi D; // RSA Private Exponent @@ -342,7 +451,7 @@ std::optional<std::pair<Key128, Key128>> ParseTicket(const TicketRaw& ticket, mbedtls_mpi_read_binary(&D, key.decryption_key.data(), key.decryption_key.size()); mbedtls_mpi_read_binary(&N, key.modulus.data(), key.modulus.size()); - mbedtls_mpi_read_binary(&S, ticket.data() + 0x180, 0x100); + mbedtls_mpi_read_binary(&S, ticket.GetData().title_key_block.data(), 0x100); mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr); @@ -366,6 +475,7 @@ std::optional<std::pair<Key128, Key128>> ParseTicket(const TicketRaw& ticket, return {}; ASSERT(*offset > 0); + Key128 key_temp{}; std::memcpy(key_temp.data(), m_2.data() + *offset, key_temp.size()); return std::make_pair(rights_id, key_temp); @@ -450,6 +560,8 @@ void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) { const auto index = std::stoul(out[0].substr(18, 2), nullptr, 16); encrypted_keyblobs[index] = Common::HexStringToArray<0xB0>(out[1]); + } else if (out[0].compare(0, 20, "eticket_extended_kek") == 0) { + eticket_extended_kek = Common::HexStringToArray<576>(out[1]); } else { for (const auto& kv : KEYS_VARIABLE_LENGTH) { if (!ValidCryptoRevisionString(out[0], kv.second.size(), 2)) @@ -862,20 +974,19 @@ void KeyManager::DeriveETicket(PartitionDataManager& data) { // Titlekeys data.DecryptProdInfo(GetBISKey(0)); - const auto eticket_extended_kek = data.GetETicketExtendedKek(); + eticket_extended_kek = data.GetETicketExtendedKek(); + WriteKeyToFile(KeyCategory::Console, "eticket_extended_kek", eticket_extended_kek); + PopulateTickets(); +} - std::vector<u8> extended_iv(0x10); - std::memcpy(extended_iv.data(), eticket_extended_kek.data(), extended_iv.size()); - std::array<u8, 0x230> extended_dec{}; - AESCipher<Key128> rsa_1(eticket_final, Mode::CTR); - rsa_1.SetIV(extended_iv); - rsa_1.Transcode(eticket_extended_kek.data() + 0x10, eticket_extended_kek.size() - 0x10, - extended_dec.data(), Op::Decrypt); +void KeyManager::PopulateTickets() { + const auto rsa_key = GetETicketRSAKey(); - RSAKeyPair<2048> rsa_key{}; - std::memcpy(rsa_key.decryption_key.data(), extended_dec.data(), rsa_key.decryption_key.size()); - std::memcpy(rsa_key.modulus.data(), extended_dec.data() + 0x100, rsa_key.modulus.size()); - std::memcpy(rsa_key.exponent.data(), extended_dec.data() + 0x200, rsa_key.exponent.size()); + if (rsa_key == RSAKeyPair<2048>{}) + return; + + if (!common_tickets.empty() && !personal_tickets.empty()) + return; const FileUtil::IOFile save1(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/80000000000000e1", @@ -886,19 +997,41 @@ void KeyManager::DeriveETicket(PartitionDataManager& data) { const auto blob2 = GetTicketblob(save2); auto res = GetTicketblob(save1); + const auto idx = res.size(); res.insert(res.end(), blob2.begin(), blob2.end()); - for (const auto& raw : res) { - const auto pair = ParseTicket(raw, rsa_key); + for (std::size_t i = 0; i < res.size(); ++i) { + const auto common = i < idx; + const auto pair = ParseTicket(res[i], rsa_key); if (!pair) continue; const auto& [rid, key] = *pair; u128 rights_id; std::memcpy(rights_id.data(), rid.data(), rid.size()); + + if (common) { + common_tickets[rights_id] = res[i]; + } else { + personal_tickets[rights_id] = res[i]; + } + SetKey(S128KeyType::Titlekey, key, rights_id[1], rights_id[0]); } } +void KeyManager::SynthesizeTickets() { + for (const auto& key : s128_keys) { + if (key.first.type != S128KeyType::Titlekey) { + continue; + } + u128 rights_id{key.first.field1, key.first.field2}; + Key128 rights_id_2; + std::memcpy(rights_id_2.data(), rights_id.data(), rights_id_2.size()); + const auto ticket = Ticket::SynthesizeCommon(key.second, rights_id_2); + common_tickets.insert_or_assign(rights_id, ticket); + } +} + void KeyManager::SetKeyWrapped(S128KeyType id, Key128 key, u64 field1, u64 field2) { if (key == Key128{}) return; @@ -997,6 +1130,46 @@ void KeyManager::PopulateFromPartitionData(PartitionDataManager& data) { DeriveBase(); } +const std::map<u128, Ticket>& KeyManager::GetCommonTickets() const { + return common_tickets; +} + +const std::map<u128, Ticket>& KeyManager::GetPersonalizedTickets() const { + return personal_tickets; +} + +bool KeyManager::AddTicketCommon(Ticket raw) { + const auto rsa_key = GetETicketRSAKey(); + if (rsa_key == RSAKeyPair<2048>{}) + return false; + + const auto pair = ParseTicket(raw, rsa_key); + if (!pair) + return false; + const auto& [rid, key] = *pair; + u128 rights_id; + std::memcpy(rights_id.data(), rid.data(), rid.size()); + common_tickets[rights_id] = raw; + SetKey(S128KeyType::Titlekey, key, rights_id[1], rights_id[0]); + return true; +} + +bool KeyManager::AddTicketPersonalized(Ticket raw) { + const auto rsa_key = GetETicketRSAKey(); + if (rsa_key == RSAKeyPair<2048>{}) + return false; + + const auto pair = ParseTicket(raw, rsa_key); + if (!pair) + return false; + const auto& [rid, key] = *pair; + u128 rights_id; + std::memcpy(rights_id.data(), rid.data(), rid.size()); + common_tickets[rights_id] = raw; + SetKey(S128KeyType::Titlekey, key, rights_id[1], rights_id[0]); + return true; +} + const boost::container::flat_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = { {"eticket_rsa_kek", {S128KeyType::ETicketRSAKek, 0, 0}}, {"eticket_rsa_kek_source", diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h index 22f268c65..7265c4171 100644 --- a/src/core/crypto/key_manager.h +++ b/src/core/crypto/key_manager.h @@ -9,8 +9,10 @@ #include <optional> #include <string> +#include <variant> #include <boost/container/flat_map.hpp> #include <fmt/format.h> +#include "common/common_funcs.h" #include "common/common_types.h" #include "core/crypto/partition_data_manager.h" #include "core/file_sys/vfs_types.h" @@ -30,7 +32,79 @@ constexpr u64 TICKET_FILE_TITLEKEY_OFFSET = 0x180; using Key128 = std::array<u8, 0x10>; using Key256 = std::array<u8, 0x20>; using SHA256Hash = std::array<u8, 0x20>; -using TicketRaw = std::array<u8, 0x400>; + +enum class SignatureType { + RSA_4096_SHA1 = 0x10000, + RSA_2048_SHA1 = 0x10001, + ECDSA_SHA1 = 0x10002, + RSA_4096_SHA256 = 0x10003, + RSA_2048_SHA256 = 0x10004, + ECDSA_SHA256 = 0x10005, +}; + +u64 GetSignatureTypeDataSize(SignatureType type); +u64 GetSignatureTypePaddingSize(SignatureType type); + +enum class TitleKeyType : u8 { + Common = 0, + Personalized = 1, +}; + +struct TicketData { + std::array<u8, 0x40> issuer; + union { + std::array<u8, 0x100> title_key_block; + + struct { + Key128 title_key_common; + std::array<u8, 0xF0> title_key_common_pad; + }; + }; + + INSERT_PADDING_BYTES(0x1); + TitleKeyType type; + INSERT_PADDING_BYTES(0x3); + u8 revision; + INSERT_PADDING_BYTES(0xA); + u64 ticket_id; + u64 device_id; + std::array<u8, 0x10> rights_id; + u32 account_id; + INSERT_PADDING_BYTES(0x14C); +}; +static_assert(sizeof(TicketData) == 0x2C0, "TicketData has incorrect size."); + +struct RSA4096Ticket { + SignatureType sig_type; + std::array<u8, 0x200> sig_data; + INSERT_PADDING_BYTES(0x3C); + TicketData data; +}; + +struct RSA2048Ticket { + SignatureType sig_type; + std::array<u8, 0x100> sig_data; + INSERT_PADDING_BYTES(0x3C); + TicketData data; +}; + +struct ECDSATicket { + SignatureType sig_type; + std::array<u8, 0x3C> sig_data; + INSERT_PADDING_BYTES(0x40); + TicketData data; +}; + +struct Ticket { + std::variant<RSA4096Ticket, RSA2048Ticket, ECDSATicket> data; + + SignatureType GetSignatureType() const; + TicketData& GetData(); + const TicketData& GetData() const; + u64 GetSize() const; + + static Ticket SynthesizeCommon(Key128 title_key, const std::array<u8, 0x10>& rights_id); +}; static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big."); static_assert(sizeof(Key256) == 32, "Key256 must be 256 bytes big."); @@ -43,6 +117,19 @@ struct RSAKeyPair { std::array<u8, 4> exponent; }; +template <size_t bit_size, size_t byte_size> +bool operator==(const RSAKeyPair<bit_size, byte_size>& lhs, + const RSAKeyPair<bit_size, byte_size>& rhs) { + return std::tie(lhs.encryption_key, lhs.decryption_key, lhs.modulus, lhs.exponent) == + std::tie(rhs.encryption_key, rhs.decryption_key, rhs.modulus, rhs.exponent); +} + +template <size_t bit_size, size_t byte_size> +bool operator!=(const RSAKeyPair<bit_size, byte_size>& lhs, + const RSAKeyPair<bit_size, byte_size>& rhs) { + return !(lhs == rhs); +} + enum class KeyCategory : u8 { Standard, Title, @@ -151,22 +238,35 @@ public: static bool KeyFileExists(bool title); - // Call before using the sd seed to attempt to derive it if it dosen't exist. Needs system save - // 8*43 and the private file to exist. + // Call before using the sd seed to attempt to derive it if it dosen't exist. Needs system + // save 8*43 and the private file to exist. void DeriveSDSeedLazy(); bool BaseDeriveNecessary() const; void DeriveBase(); void DeriveETicket(PartitionDataManager& data); + void PopulateTickets(); + void SynthesizeTickets(); void PopulateFromPartitionData(PartitionDataManager& data); + const std::map<u128, Ticket>& GetCommonTickets() const; + const std::map<u128, Ticket>& GetPersonalizedTickets() const; + + bool AddTicketCommon(Ticket raw); + bool AddTicketPersonalized(Ticket raw); + private: std::map<KeyIndex<S128KeyType>, Key128> s128_keys; std::map<KeyIndex<S256KeyType>, Key256> s256_keys; + // Map from rights ID to ticket + std::map<u128, Ticket> common_tickets; + std::map<u128, Ticket> personal_tickets; + std::array<std::array<u8, 0xB0>, 0x20> encrypted_keyblobs{}; std::array<std::array<u8, 0x90>, 0x20> keyblobs{}; + std::array<u8, 576> eticket_extended_kek{}; bool dev_mode; void LoadFromFile(const std::string& filename, bool is_title_keys); @@ -178,6 +278,8 @@ private: void DeriveGeneralPurposeKeys(std::size_t crypto_revision); + RSAKeyPair<2048> GetETicketRSAKey() const; + void SetKeyWrapped(S128KeyType id, Key128 key, u64 field1 = 0, u64 field2 = 0); void SetKeyWrapped(S256KeyType id, Key256 key, u64 field1 = 0, u64 field2 = 0); @@ -195,11 +297,11 @@ std::array<u8, 0x90> DecryptKeyblob(const std::array<u8, 0xB0>& encrypted_keyblo std::optional<Key128> DeriveSDSeed(); Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, KeyManager& keys); -std::vector<TicketRaw> GetTicketblob(const FileUtil::IOFile& ticket_save); +std::vector<Ticket> GetTicketblob(const FileUtil::IOFile& ticket_save); -// Returns a pair of {rights_id, titlekey}. Fails if the ticket has no certificate authority (offset -// 0x140-0x144 is zero) -std::optional<std::pair<Key128, Key128>> ParseTicket(const TicketRaw& ticket, +// Returns a pair of {rights_id, titlekey}. Fails if the ticket has no certificate authority +// (offset 0x140-0x144 is zero) +std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket, const RSAKeyPair<2048>& eticket_extended_key); } // namespace Core::Crypto diff --git a/src/core/file_sys/system_archive/mii_model.cpp b/src/core/file_sys/system_archive/mii_model.cpp new file mode 100644 index 000000000..6a9add87c --- /dev/null +++ b/src/core/file_sys/system_archive/mii_model.cpp @@ -0,0 +1,46 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/file_sys/system_archive/mii_model.h" +#include "core/file_sys/vfs_vector.h" + +namespace FileSys::SystemArchive { + +namespace MiiModelData { + +constexpr std::array<u8, 0x10> NFTR_STANDARD{'N', 'F', 'T', 'R', 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +constexpr std::array<u8, 0x10> NFSR_STANDARD{'N', 'F', 'S', 'R', 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +constexpr auto TEXTURE_LOW_LINEAR = NFTR_STANDARD; +constexpr auto TEXTURE_LOW_SRGB = NFTR_STANDARD; +constexpr auto TEXTURE_MID_LINEAR = NFTR_STANDARD; +constexpr auto TEXTURE_MID_SRGB = NFTR_STANDARD; +constexpr auto SHAPE_HIGH = NFSR_STANDARD; +constexpr auto SHAPE_MID = NFSR_STANDARD; + +} // namespace MiiModelData + +VirtualDir MiiModel() { + auto out = std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{}, + std::vector<VirtualDir>{}, "data"); + + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::TEXTURE_LOW_LINEAR.size()>>( + MiiModelData::TEXTURE_LOW_LINEAR, "NXTextureLowLinear.dat")); + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::TEXTURE_LOW_SRGB.size()>>( + MiiModelData::TEXTURE_LOW_SRGB, "NXTextureLowSRGB.dat")); + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::TEXTURE_MID_LINEAR.size()>>( + MiiModelData::TEXTURE_MID_LINEAR, "NXTextureMidLinear.dat")); + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::TEXTURE_MID_SRGB.size()>>( + MiiModelData::TEXTURE_MID_SRGB, "NXTextureMidSRGB.dat")); + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::SHAPE_HIGH.size()>>( + MiiModelData::SHAPE_HIGH, "ShapeHigh.dat")); + out->AddFile(std::make_shared<ArrayVfsFile<MiiModelData::SHAPE_MID.size()>>( + MiiModelData::SHAPE_MID, "ShapeMid.dat")); + + return std::move(out); +} + +} // namespace FileSys::SystemArchive diff --git a/src/core/file_sys/system_archive/mii_model.h b/src/core/file_sys/system_archive/mii_model.h new file mode 100644 index 000000000..6c2d9398b --- /dev/null +++ b/src/core/file_sys/system_archive/mii_model.h @@ -0,0 +1,13 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/file_sys/vfs_types.h" + +namespace FileSys::SystemArchive { + +VirtualDir MiiModel(); + +} // namespace FileSys::SystemArchive diff --git a/src/core/file_sys/system_archive/system_archive.cpp b/src/core/file_sys/system_archive/system_archive.cpp index c9722ed77..6d8445383 100644 --- a/src/core/file_sys/system_archive/system_archive.cpp +++ b/src/core/file_sys/system_archive/system_archive.cpp @@ -4,6 +4,7 @@ #include "common/logging/log.h" #include "core/file_sys/romfs.h" +#include "core/file_sys/system_archive/mii_model.h" #include "core/file_sys/system_archive/ng_word.h" #include "core/file_sys/system_archive/system_archive.h" #include "core/file_sys/system_archive/system_version.h" @@ -24,7 +25,7 @@ struct SystemArchiveDescriptor { constexpr std::array<SystemArchiveDescriptor, SYSTEM_ARCHIVE_COUNT> SYSTEM_ARCHIVES{{ {0x0100000000000800, "CertStore", nullptr}, {0x0100000000000801, "ErrorMessage", nullptr}, - {0x0100000000000802, "MiiModel", nullptr}, + {0x0100000000000802, "MiiModel", &MiiModel}, {0x0100000000000803, "BrowserDll", nullptr}, {0x0100000000000804, "Help", nullptr}, {0x0100000000000805, "SharedFont", nullptr}, diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index a192a1f5f..111633ba3 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -1057,6 +1057,7 @@ IApplicationFunctions::IApplicationFunctions() : ServiceFramework("IApplicationF {120, nullptr, "ExecuteProgram"}, {121, nullptr, "ClearUserChannel"}, {122, nullptr, "UnpopToUserChannel"}, + {130, &IApplicationFunctions::GetGpuErrorDetectedSystemEvent, "GetGpuErrorDetectedSystemEvent"}, {500, nullptr, "StartContinuousRecordingFlushForDebug"}, {1000, nullptr, "CreateMovieMaker"}, {1001, nullptr, "PrepareForJit"}, @@ -1064,6 +1065,10 @@ IApplicationFunctions::IApplicationFunctions() : ServiceFramework("IApplicationF // clang-format on RegisterHandlers(functions); + + auto& kernel = Core::System::GetInstance().Kernel(); + gpu_error_detected_event = Kernel::WritableEvent::CreateEventPair( + kernel, Kernel::ResetType::Manual, "IApplicationFunctions:GpuErrorDetectedSystemEvent"); } IApplicationFunctions::~IApplicationFunctions() = default; @@ -1285,6 +1290,14 @@ void IApplicationFunctions::GetSaveDataSize(Kernel::HLERequestContext& ctx) { rb.Push(size.journal); } +void IApplicationFunctions::GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx) { + LOG_WARNING(Service_AM, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushCopyObjects(gpu_error_detected_event.readable); +} + void InstallInterfaces(SM::ServiceManager& service_manager, std::shared_ptr<NVFlinger::NVFlinger> nvflinger, Core::System& system) { auto message_queue = std::make_shared<AppletMessageQueue>(); diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 6cb582483..cbc9da7b6 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -242,6 +242,9 @@ private: void BeginBlockingHomeButton(Kernel::HLERequestContext& ctx); void EndBlockingHomeButton(Kernel::HLERequestContext& ctx); void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx); + void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx); + + Kernel::EventPair gpu_error_detected_event; }; class IHomeMenuFunctions final : public ServiceFramework<IHomeMenuFunctions> { diff --git a/src/core/hle/service/audio/audren_u.cpp b/src/core/hle/service/audio/audren_u.cpp index 5b0b7f17e..f162249ed 100644 --- a/src/core/hle/service/audio/audren_u.cpp +++ b/src/core/hle/service/audio/audren_u.cpp @@ -165,15 +165,15 @@ public: static const FunctionInfo functions[] = { {0, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceName"}, {1, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolume"}, - {2, nullptr, "GetAudioDeviceOutputVolume"}, + {2, &IAudioDevice::GetAudioDeviceOutputVolume, "GetAudioDeviceOutputVolume"}, {3, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceName"}, {4, &IAudioDevice::QueryAudioDeviceSystemEvent, "QueryAudioDeviceSystemEvent"}, {5, &IAudioDevice::GetActiveChannelCount, "GetActiveChannelCount"}, {6, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceNameAuto"}, {7, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolumeAuto"}, - {8, nullptr, "GetAudioDeviceOutputVolumeAuto"}, + {8, &IAudioDevice::GetAudioDeviceOutputVolume, "GetAudioDeviceOutputVolumeAuto"}, {10, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceNameAuto"}, - {11, nullptr, "QueryAudioDeviceInputEvent"}, + {11, &IAudioDevice::QueryAudioDeviceInputEvent, "QueryAudioDeviceInputEvent"}, {12, &IAudioDevice::QueryAudioDeviceOutputEvent, "QueryAudioDeviceOutputEvent"}, {13, nullptr, "GetAudioSystemMasterVolumeSetting"}, }; @@ -183,6 +183,10 @@ public: buffer_event = Kernel::WritableEvent::CreateEventPair(kernel, Kernel::ResetType::Automatic, "IAudioOutBufferReleasedEvent"); + // Should be similar to audio_output_device_switch_event + audio_input_device_switch_event = Kernel::WritableEvent::CreateEventPair( + kernel, Kernel::ResetType::Automatic, "IAudioDevice:AudioInputDeviceSwitchedEvent"); + // Should only be signalled when an audio output device has been changed, example: speaker // to headset audio_output_device_switch_event = Kernel::WritableEvent::CreateEventPair( @@ -246,6 +250,19 @@ private: rb.Push(RESULT_SUCCESS); } + void GetAudioDeviceOutputVolume(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + + const auto device_name_buffer = ctx.ReadBuffer(); + const std::string name = Common::StringFromBuffer(device_name_buffer); + + LOG_WARNING(Service_Audio, "(STUBBED) called. name={}", name); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push(1.0f); + } + void GetActiveAudioDeviceName(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_Audio, "(STUBBED) called"); @@ -279,6 +296,15 @@ private: rb.Push<u32>(1); } + // Should be similar to QueryAudioDeviceOutputEvent + void QueryAudioDeviceInputEvent(Kernel::HLERequestContext& ctx) { + LOG_WARNING(Service_Audio, "(STUBBED) called"); + + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushCopyObjects(audio_input_device_switch_event.readable); + } + void QueryAudioDeviceOutputEvent(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_Audio, "called"); @@ -289,6 +315,7 @@ private: u32_le revision = 0; Kernel::EventPair buffer_event; + Kernel::EventPair audio_input_device_switch_event; Kernel::EventPair audio_output_device_switch_event; }; // namespace Audio diff --git a/src/core/hle/service/es/es.cpp b/src/core/hle/service/es/es.cpp index 6701cb913..af70d174d 100644 --- a/src/core/hle/service/es/es.cpp +++ b/src/core/hle/service/es/es.cpp @@ -2,32 +2,37 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "core/crypto/key_manager.h" +#include "core/hle/ipc_helpers.h" #include "core/hle/service/service.h" namespace Service::ES { +constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::ETicket, 2}; +constexpr ResultCode ERROR_INVALID_RIGHTS_ID{ErrorModule::ETicket, 3}; + class ETicket final : public ServiceFramework<ETicket> { public: explicit ETicket() : ServiceFramework{"es"} { // clang-format off static const FunctionInfo functions[] = { - {1, nullptr, "ImportTicket"}, + {1, &ETicket::ImportTicket, "ImportTicket"}, {2, nullptr, "ImportTicketCertificateSet"}, {3, nullptr, "DeleteTicket"}, {4, nullptr, "DeletePersonalizedTicket"}, {5, nullptr, "DeleteAllCommonTicket"}, {6, nullptr, "DeleteAllPersonalizedTicket"}, {7, nullptr, "DeleteAllPersonalizedTicketEx"}, - {8, nullptr, "GetTitleKey"}, - {9, nullptr, "CountCommonTicket"}, - {10, nullptr, "CountPersonalizedTicket"}, - {11, nullptr, "ListCommonTicket"}, - {12, nullptr, "ListPersonalizedTicket"}, + {8, &ETicket::GetTitleKey, "GetTitleKey"}, + {9, &ETicket::CountCommonTicket, "CountCommonTicket"}, + {10, &ETicket::CountPersonalizedTicket, "CountPersonalizedTicket"}, + {11, &ETicket::ListCommonTicket, "ListCommonTicket"}, + {12, &ETicket::ListPersonalizedTicket, "ListPersonalizedTicket"}, {13, nullptr, "ListMissingPersonalizedTicket"}, - {14, nullptr, "GetCommonTicketSize"}, - {15, nullptr, "GetPersonalizedTicketSize"}, - {16, nullptr, "GetCommonTicketData"}, - {17, nullptr, "GetPersonalizedTicketData"}, + {14, &ETicket::GetCommonTicketSize, "GetCommonTicketSize"}, + {15, &ETicket::GetPersonalizedTicketSize, "GetPersonalizedTicketSize"}, + {16, &ETicket::GetCommonTicketData, "GetCommonTicketData"}, + {17, &ETicket::GetPersonalizedTicketData, "GetPersonalizedTicketData"}, {18, nullptr, "OwnTicket"}, {19, nullptr, "GetTicketInfo"}, {20, nullptr, "ListLightTicketInfo"}, @@ -51,7 +56,212 @@ public: }; // clang-format on RegisterHandlers(functions); + + keys.PopulateTickets(); + keys.SynthesizeTickets(); + } + +private: + bool CheckRightsId(Kernel::HLERequestContext& ctx, const u128& rights_id) { + if (rights_id == u128{}) { + LOG_ERROR(Service_ETicket, "The rights ID was invalid!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_RIGHTS_ID); + return false; + } + + return true; + } + + void ImportTicket(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto ticket = ctx.ReadBuffer(); + const auto cert = ctx.ReadBuffer(1); + + if (ticket.size() < sizeof(Core::Crypto::Ticket)) { + LOG_ERROR(Service_ETicket, "The input buffer is not large enough!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } + + Core::Crypto::Ticket raw{}; + std::memcpy(&raw, ticket.data(), sizeof(Core::Crypto::Ticket)); + + if (!keys.AddTicketPersonalized(raw)) { + LOG_ERROR(Service_ETicket, "The ticket could not be imported!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_ARGUMENT); + return; + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + void GetTitleKey(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto rights_id = rp.PopRaw<u128>(); + + LOG_DEBUG(Service_ETicket, "called, rights_id={:016X}{:016X}", rights_id[1], rights_id[0]); + + if (!CheckRightsId(ctx, rights_id)) + return; + + const auto key = + keys.GetKey(Core::Crypto::S128KeyType::Titlekey, rights_id[1], rights_id[0]); + + if (key == Core::Crypto::Key128{}) { + LOG_ERROR(Service_ETicket, + "The titlekey doesn't exist in the KeyManager or the rights ID was invalid!"); + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ERROR_INVALID_RIGHTS_ID); + return; + } + + ctx.WriteBuffer(key.data(), key.size()); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); + } + + void CountCommonTicket(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_ETicket, "called"); + + const auto count = keys.GetCommonTickets().size(); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push<u32>(count); + } + + void CountPersonalizedTicket(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_ETicket, "called"); + + const auto count = keys.GetPersonalizedTickets().size(); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push<u32>(count); + } + + void ListCommonTicket(Kernel::HLERequestContext& ctx) { + u32 out_entries; + if (keys.GetCommonTickets().empty()) + out_entries = 0; + else + out_entries = ctx.GetWriteBufferSize() / sizeof(u128); + + LOG_DEBUG(Service_ETicket, "called, entries={:016X}", out_entries); + + keys.PopulateTickets(); + const auto tickets = keys.GetCommonTickets(); + std::vector<u128> ids; + std::transform(tickets.begin(), tickets.end(), std::back_inserter(ids), + [](const auto& pair) { return pair.first; }); + + out_entries = std::min<u32>(ids.size(), out_entries); + ctx.WriteBuffer(ids.data(), out_entries * sizeof(u128)); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push<u32>(out_entries); } + + void ListPersonalizedTicket(Kernel::HLERequestContext& ctx) { + u32 out_entries; + if (keys.GetPersonalizedTickets().empty()) + out_entries = 0; + else + out_entries = ctx.GetWriteBufferSize() / sizeof(u128); + + LOG_DEBUG(Service_ETicket, "called, entries={:016X}", out_entries); + + keys.PopulateTickets(); + const auto tickets = keys.GetPersonalizedTickets(); + std::vector<u128> ids; + std::transform(tickets.begin(), tickets.end(), std::back_inserter(ids), + [](const auto& pair) { return pair.first; }); + + out_entries = std::min<u32>(ids.size(), out_entries); + ctx.WriteBuffer(ids.data(), out_entries * sizeof(u128)); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push<u32>(out_entries); + } + + void GetCommonTicketSize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto rights_id = rp.PopRaw<u128>(); + + LOG_DEBUG(Service_ETicket, "called, rights_id={:016X}{:016X}", rights_id[1], rights_id[0]); + + if (!CheckRightsId(ctx, rights_id)) + return; + + const auto ticket = keys.GetCommonTickets().at(rights_id); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(ticket.GetSize()); + } + + void GetPersonalizedTicketSize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto rights_id = rp.PopRaw<u128>(); + + LOG_DEBUG(Service_ETicket, "called, rights_id={:016X}{:016X}", rights_id[1], rights_id[0]); + + if (!CheckRightsId(ctx, rights_id)) + return; + + const auto ticket = keys.GetPersonalizedTickets().at(rights_id); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(ticket.GetSize()); + } + + void GetCommonTicketData(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto rights_id = rp.PopRaw<u128>(); + + LOG_DEBUG(Service_ETicket, "called, rights_id={:016X}{:016X}", rights_id[1], rights_id[0]); + + if (!CheckRightsId(ctx, rights_id)) + return; + + const auto ticket = keys.GetCommonTickets().at(rights_id); + + const auto write_size = std::min<u64>(ticket.GetSize(), ctx.GetWriteBufferSize()); + ctx.WriteBuffer(&ticket, write_size); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(write_size); + } + + void GetPersonalizedTicketData(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto rights_id = rp.PopRaw<u128>(); + + LOG_DEBUG(Service_ETicket, "called, rights_id={:016X}{:016X}", rights_id[1], rights_id[0]); + + if (!CheckRightsId(ctx, rights_id)) + return; + + const auto ticket = keys.GetPersonalizedTickets().at(rights_id); + + const auto write_size = std::min<u64>(ticket.GetSize(), ctx.GetWriteBufferSize()); + ctx.WriteBuffer(&ticket, write_size); + + IPC::ResponseBuilder rb{ctx, 4}; + rb.Push(RESULT_SUCCESS); + rb.Push<u64>(write_size); + } + + Core::Crypto::KeyManager keys; }; void InstallInterfaces(SM::ServiceManager& service_manager) { diff --git a/src/core/hle/service/fatal/fatal.cpp b/src/core/hle/service/fatal/fatal.cpp index fe49c2161..01fa06ad3 100644 --- a/src/core/hle/service/fatal/fatal.cpp +++ b/src/core/hle/service/fatal/fatal.cpp @@ -5,7 +5,7 @@ #include <array> #include <cstring> #include <ctime> -#include <fmt/time.h> +#include <fmt/chrono.h> #include "common/file_util.h" #include "common/logging/log.h" #include "common/scm_rev.h" diff --git a/src/core/hle/service/hid/controllers/npad.cpp b/src/core/hle/service/hid/controllers/npad.cpp index 1e81f776f..e47fe8188 100644 --- a/src/core/hle/service/hid/controllers/npad.cpp +++ b/src/core/hle/service/hid/controllers/npad.cpp @@ -636,10 +636,15 @@ Controller_NPad::LedPattern Controller_NPad::GetLedPattern(u32 npad_id) { return LedPattern{0, 0, 0, 0}; }; } + void Controller_NPad::SetVibrationEnabled(bool can_vibrate) { can_controllers_vibrate = can_vibrate; } +bool Controller_NPad::IsVibrationEnabled() const { + return can_controllers_vibrate; +} + void Controller_NPad::ClearAllConnectedControllers() { for (auto& controller : connected_controllers) { if (controller.is_connected && controller.type != NPadControllerType::None) { @@ -648,6 +653,7 @@ void Controller_NPad::ClearAllConnectedControllers() { } } } + void Controller_NPad::DisconnectAllConnectedControllers() { std::for_each(connected_controllers.begin(), connected_controllers.end(), [](ControllerHolder& controller) { controller.is_connected = false; }); diff --git a/src/core/hle/service/hid/controllers/npad.h b/src/core/hle/service/hid/controllers/npad.h index 4b6c1083f..f28b36806 100644 --- a/src/core/hle/service/hid/controllers/npad.h +++ b/src/core/hle/service/hid/controllers/npad.h @@ -119,6 +119,7 @@ public: void DisconnectNPad(u32 npad_id); LedPattern GetLedPattern(u32 npad_id); void SetVibrationEnabled(bool can_vibrate); + bool IsVibrationEnabled() const; void ClearAllConnectedControllers(); void DisconnectAllConnectedControllers(); void ConnectAllDisconnectedControllers(); diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp index 0bd24b8eb..f8b1ca816 100644 --- a/src/core/hle/service/hid/hid.cpp +++ b/src/core/hle/service/hid/hid.cpp @@ -216,8 +216,8 @@ Hid::Hid() : ServiceFramework("hid") { {201, &Hid::SendVibrationValue, "SendVibrationValue"}, {202, &Hid::GetActualVibrationValue, "GetActualVibrationValue"}, {203, &Hid::CreateActiveVibrationDeviceList, "CreateActiveVibrationDeviceList"}, - {204, nullptr, "PermitVibration"}, - {205, nullptr, "IsVibrationPermitted"}, + {204, &Hid::PermitVibration, "PermitVibration"}, + {205, &Hid::IsVibrationPermitted, "IsVibrationPermitted"}, {206, &Hid::SendVibrationValues, "SendVibrationValues"}, {207, nullptr, "SendVibrationGcErmCommand"}, {208, nullptr, "GetActualVibrationGcErmCommand"}, @@ -679,6 +679,27 @@ void Hid::CreateActiveVibrationDeviceList(Kernel::HLERequestContext& ctx) { rb.PushIpcInterface<IActiveVibrationDeviceList>(); } +void Hid::PermitVibration(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto can_vibrate{rp.Pop<bool>()}; + applet_resource->GetController<Controller_NPad>(HidController::NPad) + .SetVibrationEnabled(can_vibrate); + + LOG_DEBUG(Service_HID, "called, can_vibrate={}", can_vibrate); + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(RESULT_SUCCESS); +} + +void Hid::IsVibrationPermitted(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_HID, "called"); + + IPC::ResponseBuilder rb{ctx, 3}; + rb.Push(RESULT_SUCCESS); + rb.Push( + applet_resource->GetController<Controller_NPad>(HidController::NPad).IsVibrationEnabled()); +} + void Hid::ActivateConsoleSixAxisSensor(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto applet_resource_user_id{rp.Pop<u64>()}; diff --git a/src/core/hle/service/hid/hid.h b/src/core/hle/service/hid/hid.h index 28260ef1b..2fd6d9fc7 100644 --- a/src/core/hle/service/hid/hid.h +++ b/src/core/hle/service/hid/hid.h @@ -114,6 +114,8 @@ private: void SetNpadHandheldActivationMode(Kernel::HLERequestContext& ctx); void GetVibrationDeviceInfo(Kernel::HLERequestContext& ctx); void CreateActiveVibrationDeviceList(Kernel::HLERequestContext& ctx); + void PermitVibration(Kernel::HLERequestContext& ctx); + void IsVibrationPermitted(Kernel::HLERequestContext& ctx); void ActivateConsoleSixAxisSensor(Kernel::HLERequestContext& ctx); void StartConsoleSixAxisSensor(Kernel::HLERequestContext& ctx); void StopSixAxisSensor(Kernel::HLERequestContext& ctx); diff --git a/src/core/reporter.cpp b/src/core/reporter.cpp index 5d4c3e6ea..cfe0771e2 100644 --- a/src/core/reporter.cpp +++ b/src/core/reporter.cpp @@ -5,8 +5,8 @@ #include <ctime> #include <fstream> +#include <fmt/chrono.h> #include <fmt/format.h> -#include <fmt/time.h> #include <json.hpp> #include "common/file_util.h" diff --git a/src/video_core/renderer_opengl/gl_device.cpp b/src/video_core/renderer_opengl/gl_device.cpp index 03d434b28..4f59a87b4 100644 --- a/src/video_core/renderer_opengl/gl_device.cpp +++ b/src/video_core/renderer_opengl/gl_device.cpp @@ -14,12 +14,22 @@ namespace OpenGL { namespace { + template <typename T> T GetInteger(GLenum pname) { GLint temporary; glGetIntegerv(pname, &temporary); return static_cast<T>(temporary); } + +bool TestProgram(const GLchar* glsl) { + const GLuint shader{glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &glsl)}; + GLint link_status; + glGetProgramiv(shader, GL_LINK_STATUS, &link_status); + glDeleteProgram(shader); + return link_status == GL_TRUE; +} + } // Anonymous namespace Device::Device() { @@ -32,6 +42,11 @@ Device::Device() { has_vertex_viewport_layer = GLAD_GL_ARB_shader_viewport_layer_array; has_variable_aoffi = TestVariableAoffi(); has_component_indexing_bug = TestComponentIndexingBug(); + has_precise_bug = TestPreciseBug(); + + LOG_INFO(Render_OpenGL, "Renderer_VariableAOFFI: {}", has_variable_aoffi); + LOG_INFO(Render_OpenGL, "Renderer_ComponentIndexingBug: {}", has_component_indexing_bug); + LOG_INFO(Render_OpenGL, "Renderer_PreciseBug: {}", has_precise_bug); } Device::Device(std::nullptr_t) { @@ -42,30 +57,21 @@ Device::Device(std::nullptr_t) { has_vertex_viewport_layer = true; has_variable_aoffi = true; has_component_indexing_bug = false; + has_precise_bug = false; } bool Device::TestVariableAoffi() { - const GLchar* AOFFI_TEST = R"(#version 430 core + return TestProgram(R"(#version 430 core // This is a unit test, please ignore me on apitrace bug reports. uniform sampler2D tex; uniform ivec2 variable_offset; out vec4 output_attribute; void main() { output_attribute = textureOffset(tex, vec2(0), variable_offset); -} -)"; - const GLuint shader{glCreateShaderProgramv(GL_VERTEX_SHADER, 1, &AOFFI_TEST)}; - GLint link_status{}; - glGetProgramiv(shader, GL_LINK_STATUS, &link_status); - glDeleteProgram(shader); - - const bool supported{link_status == GL_TRUE}; - LOG_INFO(Render_OpenGL, "Renderer_VariableAOFFI: {}", supported); - return supported; +})"); } bool Device::TestComponentIndexingBug() { - constexpr char log_message[] = "Renderer_ComponentIndexingBug: {}"; const GLchar* COMPONENT_TEST = R"(#version 430 core layout (std430, binding = 0) buffer OutputBuffer { uint output_value; @@ -105,12 +111,21 @@ void main() { GLuint result; glGetNamedBufferSubData(ssbo.handle, 0, sizeof(result), &result); if (result != values.at(index)) { - LOG_INFO(Render_OpenGL, log_message, true); return true; } } - LOG_INFO(Render_OpenGL, log_message, false); return false; } +bool Device::TestPreciseBug() { + return !TestProgram(R"(#version 430 core +in vec3 coords; +out float out_value; +uniform sampler2DShadow tex; +void main() { + precise float tmp_value = vec4(texture(tex, coords)).x; + out_value = tmp_value; +})"); +} + } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_device.h b/src/video_core/renderer_opengl/gl_device.h index 3ef7c6dd8..ba6dcd3be 100644 --- a/src/video_core/renderer_opengl/gl_device.h +++ b/src/video_core/renderer_opengl/gl_device.h @@ -46,9 +46,14 @@ public: return has_component_indexing_bug; } + bool HasPreciseBug() const { + return has_precise_bug; + } + private: static bool TestVariableAoffi(); static bool TestComponentIndexingBug(); + static bool TestPreciseBug(); std::size_t uniform_buffer_alignment{}; std::size_t shader_storage_alignment{}; @@ -58,6 +63,7 @@ private: bool has_vertex_viewport_layer{}; bool has_variable_aoffi{}; bool has_component_indexing_bug{}; + bool has_precise_bug{}; }; } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp index 359d58cbe..a5cc1a86f 100644 --- a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp +++ b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp @@ -39,7 +39,7 @@ using namespace VideoCommon::Shader; using Maxwell = Tegra::Engines::Maxwell3D::Regs; using Operation = const OperationNode&; -enum class Type { Bool, Bool2, Float, Int, Uint, HalfFloat }; +enum class Type { Void, Bool, Bool2, Float, Int, Uint, HalfFloat }; struct TextureAoffi {}; using TextureArgument = std::pair<Type, Node>; @@ -48,7 +48,7 @@ using TextureIR = std::variant<TextureAoffi, TextureArgument>; constexpr u32 MAX_CONSTBUFFER_ELEMENTS = static_cast<u32>(Maxwell::MaxConstBufferSize) / (4 * sizeof(float)); -class ShaderWriter { +class ShaderWriter final { public: void AddExpression(std::string_view text) { DEBUG_ASSERT(scope >= 0); @@ -93,9 +93,157 @@ private: u32 temporary_index = 1; }; +class Expression final { +public: + Expression(std::string code, Type type) : code{std::move(code)}, type{type} { + ASSERT(type != Type::Void); + } + Expression() : type{Type::Void} {} + + Type GetType() const { + return type; + } + + std::string GetCode() const { + return code; + } + + void CheckVoid() const { + ASSERT(type == Type::Void); + } + + std::string As(Type type) const { + switch (type) { + case Type::Bool: + return AsBool(); + case Type::Bool2: + return AsBool2(); + case Type::Float: + return AsFloat(); + case Type::Int: + return AsInt(); + case Type::Uint: + return AsUint(); + case Type::HalfFloat: + return AsHalfFloat(); + default: + UNREACHABLE_MSG("Invalid type"); + return code; + } + } + + std::string AsBool() const { + switch (type) { + case Type::Bool: + return code; + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + + std::string AsBool2() const { + switch (type) { + case Type::Bool2: + return code; + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + + std::string AsFloat() const { + switch (type) { + case Type::Float: + return code; + case Type::Uint: + return fmt::format("utof({})", code); + case Type::Int: + return fmt::format("itof({})", code); + case Type::HalfFloat: + return fmt::format("utof(packHalf2x16({}))", code); + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + + std::string AsInt() const { + switch (type) { + case Type::Float: + return fmt::format("ftoi({})", code); + case Type::Uint: + return fmt::format("int({})", code); + case Type::Int: + return code; + case Type::HalfFloat: + return fmt::format("int(packHalf2x16({}))", code); + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + + std::string AsUint() const { + switch (type) { + case Type::Float: + return fmt::format("ftou({})", code); + case Type::Uint: + return code; + case Type::Int: + return fmt::format("uint({})", code); + case Type::HalfFloat: + return fmt::format("packHalf2x16({})", code); + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + + std::string AsHalfFloat() const { + switch (type) { + case Type::Float: + return fmt::format("unpackHalf2x16(ftou({}))", code); + case Type::Uint: + return fmt::format("unpackHalf2x16({})", code); + case Type::Int: + return fmt::format("unpackHalf2x16(int({}))", code); + case Type::HalfFloat: + return code; + default: + UNREACHABLE_MSG("Incompatible types"); + return code; + } + } + +private: + std::string code; + Type type{}; +}; + +constexpr const char* GetTypeString(Type type) { + switch (type) { + case Type::Bool: + return "bool"; + case Type::Bool2: + return "bvec2"; + case Type::Float: + return "float"; + case Type::Int: + return "int"; + case Type::Uint: + return "uint"; + case Type::HalfFloat: + return "vec2"; + default: + UNREACHABLE_MSG("Invalid type"); + return "<invalid type>"; + } +} + /// Generates code to use for a swizzle operation. constexpr const char* GetSwizzle(u32 element) { - constexpr std::array<const char*, 4> swizzle = {".x", ".y", ".z", ".w"}; + constexpr std::array swizzle = {".x", ".y", ".z", ".w"}; return swizzle.at(element); } @@ -134,8 +282,8 @@ constexpr bool IsGenericAttribute(Attribute::Index index) { return index >= Attribute::Index::Attribute_0 && index <= Attribute::Index::Attribute_31; } -constexpr Attribute::Index ToGenericAttribute(u32 value) { - return static_cast<Attribute::Index>(value + static_cast<u32>(Attribute::Index::Attribute_0)); +constexpr Attribute::Index ToGenericAttribute(u64 value) { + return static_cast<Attribute::Index>(value + static_cast<u64>(Attribute::Index::Attribute_0)); } u32 GetGenericAttributeIndex(Attribute::Index index) { @@ -191,7 +339,7 @@ public: // VM's program counter const auto first_address = ir.GetBasicBlocks().begin()->first; - code.AddLine("uint jmp_to = {}u;", first_address); + code.AddLine("uint jmp_to = {}U;", first_address); // TODO(Subv): Figure out the actual depth of the flow stack, for now it seems // unlikely that shaders will use 20 nested SSYs and PBKs. @@ -199,7 +347,7 @@ public: constexpr u32 FLOW_STACK_SIZE = 20; for (const auto stack : std::array{MetaStackClass::Ssy, MetaStackClass::Pbk}) { code.AddLine("uint {}[{}];", FlowStackName(stack), FLOW_STACK_SIZE); - code.AddLine("uint {} = 0u;", FlowStackTopName(stack)); + code.AddLine("uint {} = 0U;", FlowStackTopName(stack)); } } @@ -210,7 +358,7 @@ public: for (const auto& pair : ir.GetBasicBlocks()) { const auto [address, bb] = pair; - code.AddLine("case 0x{:x}u: {{", address); + code.AddLine("case 0x{:X}U: {{", address); ++code.scope; VisitBlock(bb); @@ -322,7 +470,7 @@ private: void DeclareRegisters() { const auto& registers = ir.GetRegisters(); for (const u32 gpr : registers) { - code.AddLine("float {} = 0;", GetRegister(gpr)); + code.AddLine("float {} = 0.0f;", GetRegister(gpr)); } if (!registers.empty()) { code.AddNewLine(); @@ -348,7 +496,7 @@ private: return; } const auto element_count = Common::AlignUp(local_memory_size, 4) / 4; - code.AddLine("float {}[{}];", GetLocalMemory(), element_count); + code.AddLine("uint {}[{}];", GetLocalMemory(), element_count); code.AddNewLine(); } @@ -371,8 +519,6 @@ private: return "noperspective "; default: case AttributeUse::Unused: - UNREACHABLE_MSG("Unused attribute being fetched"); - return {}; UNIMPLEMENTED_MSG("Unknown attribute usage index={}", static_cast<u32>(attribute)); return {}; } @@ -449,7 +595,7 @@ private: const auto [index, size] = entry; code.AddLine("layout (std140, binding = CBUF_BINDING_{}) uniform {} {{", index, GetConstBufferBlock(index)); - code.AddLine(" vec4 {}[MAX_CONSTBUFFER_ELEMENTS];", GetConstBuffer(index)); + code.AddLine(" uvec4 {}[{}];", GetConstBuffer(index), MAX_CONSTBUFFER_ELEMENTS); code.AddLine("}};"); code.AddNewLine(); } @@ -470,7 +616,7 @@ private: code.AddLine("layout (std430, binding = GMEM_BINDING_{}_{}) {} buffer {} {{", base.cbuf_index, base.cbuf_offset, qualifier, GetGlobalMemoryBlock(base)); - code.AddLine(" float {}[];", GetGlobalMemory(base)); + code.AddLine(" uint {}[];", GetGlobalMemory(base)); code.AddLine("}};"); code.AddNewLine(); } @@ -528,7 +674,7 @@ private: if (!ir.HasPhysicalAttributes()) { return; } - code.AddLine("float readPhysicalAttribute(uint physical_address) {{"); + code.AddLine("float ReadPhysicalAttribute(uint physical_address) {{"); ++code.scope; code.AddLine("switch (physical_address) {{"); @@ -537,15 +683,16 @@ private: for (u32 index = 0; index < num_attributes; ++index) { const auto attribute{ToGenericAttribute(index)}; for (u32 element = 0; element < 4; ++element) { - constexpr u32 generic_base{0x80}; - constexpr u32 generic_stride{16}; - constexpr u32 element_stride{4}; + constexpr u32 generic_base = 0x80; + constexpr u32 generic_stride = 16; + constexpr u32 element_stride = 4; const u32 address{generic_base + index * generic_stride + element * element_stride}; - const bool declared{stage != ProgramType::Fragment || - header.ps.GetAttributeUse(index) != AttributeUse::Unused}; - const std::string value{declared ? ReadAttribute(attribute, element) : "0"}; - code.AddLine("case 0x{:x}: return {};", address, value); + const bool declared = stage != ProgramType::Fragment || + header.ps.GetAttributeUse(index) != AttributeUse::Unused; + const std::string value = + declared ? ReadAttribute(attribute, element).AsFloat() : "0.0f"; + code.AddLine("case 0x{:X}U: return {};", address, value); } } @@ -590,13 +737,11 @@ private: void VisitBlock(const NodeBlock& bb) { for (const auto& node : bb) { - if (const std::string expr = Visit(node); !expr.empty()) { - code.AddLine(expr); - } + Visit(node).CheckVoid(); } } - std::string Visit(const Node& node) { + Expression Visit(const Node& node) { if (const auto operation = std::get_if<OperationNode>(&*node)) { const auto operation_index = static_cast<std::size_t>(operation->GetCode()); if (operation_index >= operation_decompilers.size()) { @@ -614,18 +759,18 @@ private: if (const auto gpr = std::get_if<GprNode>(&*node)) { const u32 index = gpr->GetIndex(); if (index == Register::ZeroIndex) { - return "0"; + return {"0U", Type::Uint}; } - return GetRegister(index); + return {GetRegister(index), Type::Float}; } if (const auto immediate = std::get_if<ImmediateNode>(&*node)) { const u32 value = immediate->GetValue(); if (value < 10) { // For eyecandy avoid using hex numbers on single digits - return fmt::format("utof({}u)", immediate->GetValue()); + return {fmt::format("{}U", immediate->GetValue()), Type::Uint}; } - return fmt::format("utof(0x{:x}u)", immediate->GetValue()); + return {fmt::format("0x{:X}U", immediate->GetValue()), Type::Uint}; } if (const auto predicate = std::get_if<PredicateNode>(&*node)) { @@ -640,17 +785,18 @@ private: } }(); if (predicate->IsNegated()) { - return fmt::format("!({})", value); + return {fmt::format("!({})", value), Type::Bool}; } - return value; + return {value, Type::Bool}; } if (const auto abuf = std::get_if<AbufNode>(&*node)) { UNIMPLEMENTED_IF_MSG(abuf->IsPhysicalBuffer() && stage == ProgramType::Geometry, "Physical attributes in geometry shaders are not implemented"); if (abuf->IsPhysicalBuffer()) { - return fmt::format("readPhysicalAttribute(ftou({}))", - Visit(abuf->GetPhysicalAddress())); + return {fmt::format("ReadPhysicalAttribute({})", + Visit(abuf->GetPhysicalAddress()).AsUint()), + Type::Float}; } return ReadAttribute(abuf->GetIndex(), abuf->GetElement(), abuf->GetBuffer()); } @@ -661,59 +807,64 @@ private: // Direct access const u32 offset_imm = immediate->GetValue(); ASSERT_MSG(offset_imm % 4 == 0, "Unaligned cbuf direct access"); - return fmt::format("{}[{}][{}]", GetConstBuffer(cbuf->GetIndex()), - offset_imm / (4 * 4), (offset_imm / 4) % 4); + return {fmt::format("{}[{}][{}]", GetConstBuffer(cbuf->GetIndex()), + offset_imm / (4 * 4), (offset_imm / 4) % 4), + Type::Uint}; } if (std::holds_alternative<OperationNode>(*offset)) { // Indirect access const std::string final_offset = code.GenerateTemporary(); - code.AddLine("uint {} = ftou({}) >> 2;", final_offset, Visit(offset)); + code.AddLine("uint {} = {} >> 2;", final_offset, Visit(offset).AsUint()); if (!device.HasComponentIndexingBug()) { - return fmt::format("{}[{} >> 2][{} & 3]", GetConstBuffer(cbuf->GetIndex()), - final_offset, final_offset); + return {fmt::format("{}[{} >> 2][{} & 3]", GetConstBuffer(cbuf->GetIndex()), + final_offset, final_offset), + Type::Uint}; } // AMD's proprietary GLSL compiler emits ill code for variable component access. // To bypass this driver bug generate 4 ifs, one per each component. const std::string pack = code.GenerateTemporary(); - code.AddLine("vec4 {} = {}[{} >> 2];", pack, GetConstBuffer(cbuf->GetIndex()), + code.AddLine("uvec4 {} = {}[{} >> 2];", pack, GetConstBuffer(cbuf->GetIndex()), final_offset); const std::string result = code.GenerateTemporary(); - code.AddLine("float {};", result); + code.AddLine("uint {};", result); for (u32 swizzle = 0; swizzle < 4; ++swizzle) { code.AddLine("if (({} & 3) == {}) {} = {}{};", final_offset, swizzle, result, pack, GetSwizzle(swizzle)); } - return result; + return {result, Type::Uint}; } UNREACHABLE_MSG("Unmanaged offset node type"); } if (const auto gmem = std::get_if<GmemNode>(&*node)) { - const std::string real = Visit(gmem->GetRealAddress()); - const std::string base = Visit(gmem->GetBaseAddress()); - const std::string final_offset = fmt::format("(ftou({}) - ftou({})) / 4", real, base); - return fmt::format("{}[{}]", GetGlobalMemory(gmem->GetDescriptor()), final_offset); + const std::string real = Visit(gmem->GetRealAddress()).AsUint(); + const std::string base = Visit(gmem->GetBaseAddress()).AsUint(); + const std::string final_offset = fmt::format("({} - {}) >> 2", real, base); + return {fmt::format("{}[{}]", GetGlobalMemory(gmem->GetDescriptor()), final_offset), + Type::Uint}; } if (const auto lmem = std::get_if<LmemNode>(&*node)) { if (stage == ProgramType::Compute) { LOG_WARNING(Render_OpenGL, "Local memory is stubbed on compute shaders"); } - return fmt::format("{}[ftou({}) / 4]", GetLocalMemory(), Visit(lmem->GetAddress())); + return { + fmt::format("{}[{} >> 2]", GetLocalMemory(), Visit(lmem->GetAddress()).AsUint()), + Type::Uint}; } if (const auto internal_flag = std::get_if<InternalFlagNode>(&*node)) { - return GetInternalFlag(internal_flag->GetFlag()); + return {GetInternalFlag(internal_flag->GetFlag()), Type::Bool}; } if (const auto conditional = std::get_if<ConditionalNode>(&*node)) { // It's invalid to call conditional on nested nodes, use an operation instead - code.AddLine("if ({}) {{", Visit(conditional->GetCondition())); + code.AddLine("if ({}) {{", Visit(conditional->GetCondition()).AsBool()); ++code.scope; VisitBlock(conditional->GetCode()); @@ -724,20 +875,21 @@ private: } if (const auto comment = std::get_if<CommentNode>(&*node)) { - return "// " + comment->GetText(); + code.AddLine("// " + comment->GetText()); + return {}; } UNREACHABLE(); return {}; } - std::string ReadAttribute(Attribute::Index attribute, u32 element, const Node& buffer = {}) { + Expression ReadAttribute(Attribute::Index attribute, u32 element, const Node& buffer = {}) { const auto GeometryPass = [&](std::string_view name) { if (stage == ProgramType::Geometry && buffer) { // TODO(Rodrigo): Guard geometry inputs against out of bound reads. Some games // set an 0x80000000 index for those and the shader fails to build. Find out why // this happens and what's its intent. - return fmt::format("gs_{}[ftou({}) % MAX_VERTEX_INPUT]", name, Visit(buffer)); + return fmt::format("gs_{}[{} % MAX_VERTEX_INPUT]", name, Visit(buffer).AsUint()); } return std::string(name); }; @@ -746,25 +898,27 @@ private: case Attribute::Index::Position: switch (stage) { case ProgramType::Geometry: - return fmt::format("gl_in[ftou({})].gl_Position{}", Visit(buffer), - GetSwizzle(element)); + return {fmt::format("gl_in[{}].gl_Position{}", Visit(buffer).AsUint(), + GetSwizzle(element)), + Type::Float}; case ProgramType::Fragment: - return element == 3 ? "1.0f" : ("gl_FragCoord"s + GetSwizzle(element)); + return {element == 3 ? "1.0f" : ("gl_FragCoord"s + GetSwizzle(element)), + Type::Float}; default: UNREACHABLE(); } case Attribute::Index::PointCoord: switch (element) { case 0: - return "gl_PointCoord.x"; + return {"gl_PointCoord.x", Type::Float}; case 1: - return "gl_PointCoord.y"; + return {"gl_PointCoord.y", Type::Float}; case 2: case 3: - return "0"; + return {"0.0f", Type::Float}; } UNREACHABLE(); - return "0"; + return {"0", Type::Int}; case Attribute::Index::TessCoordInstanceIDVertexID: // TODO(Subv): Find out what the values are for the first two elements when inside a // vertex shader, and what's the value of the fourth element when inside a Tess Eval @@ -773,44 +927,49 @@ private: switch (element) { case 2: // Config pack's first value is instance_id. - return "uintBitsToFloat(config_pack[0])"; + return {"config_pack[0]", Type::Uint}; case 3: - return "uintBitsToFloat(gl_VertexID)"; + return {"gl_VertexID", Type::Int}; } UNIMPLEMENTED_MSG("Unmanaged TessCoordInstanceIDVertexID element={}", element); - return "0"; + return {"0", Type::Int}; case Attribute::Index::FrontFacing: // TODO(Subv): Find out what the values are for the other elements. ASSERT(stage == ProgramType::Fragment); switch (element) { case 3: - return "itof(gl_FrontFacing ? -1 : 0)"; + return {"(gl_FrontFacing ? -1 : 0)", Type::Int}; } UNIMPLEMENTED_MSG("Unmanaged FrontFacing element={}", element); - return "0"; + return {"0", Type::Int}; default: if (IsGenericAttribute(attribute)) { - return GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element); + return {GeometryPass(GetInputAttribute(attribute)) + GetSwizzle(element), + Type::Float}; } break; } UNIMPLEMENTED_MSG("Unhandled input attribute: {}", static_cast<u32>(attribute)); - return "0"; + return {"0", Type::Int}; } - std::string ApplyPrecise(Operation operation, const std::string& value) { + Expression ApplyPrecise(Operation operation, std::string value, Type type) { if (!IsPrecise(operation)) { - return value; + return {std::move(value), type}; } - // There's a bug in NVidia's proprietary drivers that makes precise fail on fragment shaders - const std::string precise = stage != ProgramType::Fragment ? "precise " : ""; + // Old Nvidia drivers have a bug with precise and texture sampling. These are more likely to + // be found in fragment shaders, so we disable precise there. There are vertex shaders that + // also fail to build but nobody seems to care about those. + // Note: Only bugged drivers will skip precise. + const bool disable_precise = device.HasPreciseBug() && stage == ProgramType::Fragment; - const std::string temporary = code.GenerateTemporary(); - code.AddLine("{}float {} = {};", precise, temporary, value); - return temporary; + std::string temporary = code.GenerateTemporary(); + code.AddLine("{}{} {} = {};", disable_precise ? "" : "precise ", GetTypeString(type), + temporary, value); + return {std::move(temporary), type}; } - std::string VisitOperand(Operation operation, std::size_t operand_index) { + Expression VisitOperand(Operation operation, std::size_t operand_index) { const auto& operand = operation[operand_index]; const bool parent_precise = IsPrecise(operation); const bool child_precise = IsPrecise(operand); @@ -819,19 +978,16 @@ private: return Visit(operand); } - const std::string temporary = code.GenerateTemporary(); - code.AddLine("float {} = {};", temporary, Visit(operand)); - return temporary; - } - - std::string VisitOperand(Operation operation, std::size_t operand_index, Type type) { - return CastOperand(VisitOperand(operation, operand_index), type); + Expression value = Visit(operand); + std::string temporary = code.GenerateTemporary(); + code.AddLine("{} {} = {};", GetTypeString(value.GetType()), temporary, value.GetCode()); + return {std::move(temporary), value.GetType()}; } - std::optional<std::pair<std::string, bool>> GetOutputAttribute(const AbufNode* abuf) { + Expression GetOutputAttribute(const AbufNode* abuf) { switch (const auto attribute = abuf->GetIndex()) { case Attribute::Index::Position: - return std::make_pair("gl_Position"s + GetSwizzle(abuf->GetElement()), false); + return {"gl_Position"s + GetSwizzle(abuf->GetElement()), Type::Float}; case Attribute::Index::LayerViewportPointSize: switch (abuf->GetElement()) { case 0: @@ -841,119 +997,79 @@ private: if (IsVertexShader(stage) && !device.HasVertexViewportLayer()) { return {}; } - return std::make_pair("gl_Layer", true); + return {"gl_Layer", Type::Int}; case 2: if (IsVertexShader(stage) && !device.HasVertexViewportLayer()) { return {}; } - return std::make_pair("gl_ViewportIndex", true); + return {"gl_ViewportIndex", Type::Int}; case 3: UNIMPLEMENTED_MSG("Requires some state changes for gl_PointSize to work in shader"); - return std::make_pair("gl_PointSize", false); + return {"gl_PointSize", Type::Float}; } return {}; case Attribute::Index::ClipDistances0123: - return std::make_pair(fmt::format("gl_ClipDistance[{}]", abuf->GetElement()), false); + return {fmt::format("gl_ClipDistance[{}]", abuf->GetElement()), Type::Float}; case Attribute::Index::ClipDistances4567: - return std::make_pair(fmt::format("gl_ClipDistance[{}]", abuf->GetElement() + 4), - false); + return {fmt::format("gl_ClipDistance[{}]", abuf->GetElement() + 4), Type::Float}; default: if (IsGenericAttribute(attribute)) { - return std::make_pair( - GetOutputAttribute(attribute) + GetSwizzle(abuf->GetElement()), false); + return {GetOutputAttribute(attribute) + GetSwizzle(abuf->GetElement()), + Type::Float}; } UNIMPLEMENTED_MSG("Unhandled output attribute: {}", static_cast<u32>(attribute)); return {}; } } - std::string CastOperand(const std::string& value, Type type) const { - switch (type) { - case Type::Bool: - case Type::Bool2: - case Type::Float: - return value; - case Type::Int: - return fmt::format("ftoi({})", value); - case Type::Uint: - return fmt::format("ftou({})", value); - case Type::HalfFloat: - return fmt::format("toHalf2({})", value); - } - UNREACHABLE(); - return value; + Expression GenerateUnary(Operation operation, std::string_view func, Type result_type, + Type type_a) { + std::string op_str = fmt::format("{}({})", func, VisitOperand(operation, 0).As(type_a)); + return ApplyPrecise(operation, std::move(op_str), result_type); } - std::string BitwiseCastResult(const std::string& value, Type type, - bool needs_parenthesis = false) { - switch (type) { - case Type::Bool: - case Type::Bool2: - case Type::Float: - if (needs_parenthesis) { - return fmt::format("({})", value); - } - return value; - case Type::Int: - return fmt::format("itof({})", value); - case Type::Uint: - return fmt::format("utof({})", value); - case Type::HalfFloat: - return fmt::format("fromHalf2({})", value); - } - UNREACHABLE(); - return value; - } - - std::string GenerateUnary(Operation operation, const std::string& func, Type result_type, - Type type_a, bool needs_parenthesis = true) { - const std::string op_str = fmt::format("{}({})", func, VisitOperand(operation, 0, type_a)); - - return ApplyPrecise(operation, BitwiseCastResult(op_str, result_type, needs_parenthesis)); - } - - std::string GenerateBinaryInfix(Operation operation, const std::string& func, Type result_type, - Type type_a, Type type_b) { - const std::string op_a = VisitOperand(operation, 0, type_a); - const std::string op_b = VisitOperand(operation, 1, type_b); - const std::string op_str = fmt::format("({} {} {})", op_a, func, op_b); + Expression GenerateBinaryInfix(Operation operation, std::string_view func, Type result_type, + Type type_a, Type type_b) { + const std::string op_a = VisitOperand(operation, 0).As(type_a); + const std::string op_b = VisitOperand(operation, 1).As(type_b); + std::string op_str = fmt::format("({} {} {})", op_a, func, op_b); - return ApplyPrecise(operation, BitwiseCastResult(op_str, result_type)); + return ApplyPrecise(operation, std::move(op_str), result_type); } - std::string GenerateBinaryCall(Operation operation, const std::string& func, Type result_type, - Type type_a, Type type_b) { - const std::string op_a = VisitOperand(operation, 0, type_a); - const std::string op_b = VisitOperand(operation, 1, type_b); - const std::string op_str = fmt::format("{}({}, {})", func, op_a, op_b); + Expression GenerateBinaryCall(Operation operation, std::string_view func, Type result_type, + Type type_a, Type type_b) { + const std::string op_a = VisitOperand(operation, 0).As(type_a); + const std::string op_b = VisitOperand(operation, 1).As(type_b); + std::string op_str = fmt::format("{}({}, {})", func, op_a, op_b); - return ApplyPrecise(operation, BitwiseCastResult(op_str, result_type)); + return ApplyPrecise(operation, std::move(op_str), result_type); } - std::string GenerateTernary(Operation operation, const std::string& func, Type result_type, - Type type_a, Type type_b, Type type_c) { - const std::string op_a = VisitOperand(operation, 0, type_a); - const std::string op_b = VisitOperand(operation, 1, type_b); - const std::string op_c = VisitOperand(operation, 2, type_c); - const std::string op_str = fmt::format("{}({}, {}, {})", func, op_a, op_b, op_c); + Expression GenerateTernary(Operation operation, std::string_view func, Type result_type, + Type type_a, Type type_b, Type type_c) { + const std::string op_a = VisitOperand(operation, 0).As(type_a); + const std::string op_b = VisitOperand(operation, 1).As(type_b); + const std::string op_c = VisitOperand(operation, 2).As(type_c); + std::string op_str = fmt::format("{}({}, {}, {})", func, op_a, op_b, op_c); - return ApplyPrecise(operation, BitwiseCastResult(op_str, result_type)); + return ApplyPrecise(operation, std::move(op_str), result_type); } - std::string GenerateQuaternary(Operation operation, const std::string& func, Type result_type, - Type type_a, Type type_b, Type type_c, Type type_d) { - const std::string op_a = VisitOperand(operation, 0, type_a); - const std::string op_b = VisitOperand(operation, 1, type_b); - const std::string op_c = VisitOperand(operation, 2, type_c); - const std::string op_d = VisitOperand(operation, 3, type_d); - const std::string op_str = fmt::format("{}({}, {}, {}, {})", func, op_a, op_b, op_c, op_d); + Expression GenerateQuaternary(Operation operation, const std::string& func, Type result_type, + Type type_a, Type type_b, Type type_c, Type type_d) { + const std::string op_a = VisitOperand(operation, 0).As(type_a); + const std::string op_b = VisitOperand(operation, 1).As(type_b); + const std::string op_c = VisitOperand(operation, 2).As(type_c); + const std::string op_d = VisitOperand(operation, 3).As(type_d); + std::string op_str = fmt::format("{}({}, {}, {}, {})", func, op_a, op_b, op_c, op_d); - return ApplyPrecise(operation, BitwiseCastResult(op_str, result_type)); + return ApplyPrecise(operation, std::move(op_str), result_type); } std::string GenerateTexture(Operation operation, const std::string& function_suffix, const std::vector<TextureIR>& extras) { - constexpr std::array<const char*, 4> coord_constructors = {"float", "vec2", "vec3", "vec4"}; + constexpr std::array coord_constructors = {"float", "vec2", "vec3", "vec4"}; const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); @@ -970,17 +1086,17 @@ private: expr += coord_constructors.at(count + (has_array ? 1 : 0) + (has_shadow ? 1 : 0) - 1); expr += '('; for (std::size_t i = 0; i < count; ++i) { - expr += Visit(operation[i]); + expr += Visit(operation[i]).AsFloat(); const std::size_t next = i + 1; if (next < count) expr += ", "; } if (has_array) { - expr += ", float(ftoi(" + Visit(meta->array) + "))"; + expr += ", float(" + Visit(meta->array).AsInt() + ')'; } if (has_shadow) { - expr += ", " + Visit(meta->depth_compare); + expr += ", " + Visit(meta->depth_compare).AsFloat(); } expr += ')'; @@ -1011,11 +1127,11 @@ private: // required to be constant) expr += std::to_string(static_cast<s32>(immediate->GetValue())); } else { - expr += fmt::format("ftoi({})", Visit(operand)); + expr += Visit(operand).AsInt(); } break; case Type::Float: - expr += Visit(operand); + expr += Visit(operand).AsFloat(); break; default: { const auto type_int = static_cast<u32>(type); @@ -1031,7 +1147,7 @@ private: if (aoffi.empty()) { return {}; } - constexpr std::array<const char*, 3> coord_constructors = {"int", "ivec2", "ivec3"}; + constexpr std::array coord_constructors = {"int", "ivec2", "ivec3"}; std::string expr = ", "; expr += coord_constructors.at(aoffi.size() - 1); expr += '('; @@ -1044,7 +1160,7 @@ private: expr += std::to_string(static_cast<s32>(immediate->GetValue())); } else if (device.HasVariableAoffi()) { // Avoid using variable AOFFI on unsupported devices. - expr += fmt::format("ftoi({})", Visit(operand)); + expr += Visit(operand).AsInt(); } else { // Insert 0 on devices not supporting variable AOFFI. expr += '0'; @@ -1058,328 +1174,314 @@ private: return expr; } - std::string Assign(Operation operation) { + Expression Assign(Operation operation) { const Node& dest = operation[0]; const Node& src = operation[1]; - std::string target; - bool is_integer = false; - + Expression target; if (const auto gpr = std::get_if<GprNode>(&*dest)) { if (gpr->GetIndex() == Register::ZeroIndex) { // Writing to Register::ZeroIndex is a no op return {}; } - target = GetRegister(gpr->GetIndex()); + target = {GetRegister(gpr->GetIndex()), Type::Float}; } else if (const auto abuf = std::get_if<AbufNode>(&*dest)) { UNIMPLEMENTED_IF(abuf->IsPhysicalBuffer()); - const auto result = GetOutputAttribute(abuf); - if (!result) { - return {}; - } - target = result->first; - is_integer = result->second; + target = GetOutputAttribute(abuf); } else if (const auto lmem = std::get_if<LmemNode>(&*dest)) { if (stage == ProgramType::Compute) { LOG_WARNING(Render_OpenGL, "Local memory is stubbed on compute shaders"); } - target = fmt::format("{}[ftou({}) / 4]", GetLocalMemory(), Visit(lmem->GetAddress())); + target = { + fmt::format("{}[{} >> 2]", GetLocalMemory(), Visit(lmem->GetAddress()).AsUint()), + Type::Uint}; } else if (const auto gmem = std::get_if<GmemNode>(&*dest)) { - const std::string real = Visit(gmem->GetRealAddress()); - const std::string base = Visit(gmem->GetBaseAddress()); - const std::string final_offset = fmt::format("(ftou({}) - ftou({})) / 4", real, base); - target = fmt::format("{}[{}]", GetGlobalMemory(gmem->GetDescriptor()), final_offset); + const std::string real = Visit(gmem->GetRealAddress()).AsUint(); + const std::string base = Visit(gmem->GetBaseAddress()).AsUint(); + const std::string final_offset = fmt::format("({} - {}) >> 2", real, base); + target = {fmt::format("{}[{}]", GetGlobalMemory(gmem->GetDescriptor()), final_offset), + Type::Uint}; } else { UNREACHABLE_MSG("Assign called without a proper target"); } - if (is_integer) { - code.AddLine("{} = ftoi({});", target, Visit(src)); - } else { - code.AddLine("{} = {};", target, Visit(src)); - } + code.AddLine("{} = {};", target.GetCode(), Visit(src).As(target.GetType())); return {}; } template <Type type> - std::string Add(Operation operation) { + Expression Add(Operation operation) { return GenerateBinaryInfix(operation, "+", type, type, type); } template <Type type> - std::string Mul(Operation operation) { + Expression Mul(Operation operation) { return GenerateBinaryInfix(operation, "*", type, type, type); } template <Type type> - std::string Div(Operation operation) { + Expression Div(Operation operation) { return GenerateBinaryInfix(operation, "/", type, type, type); } template <Type type> - std::string Fma(Operation operation) { + Expression Fma(Operation operation) { return GenerateTernary(operation, "fma", type, type, type, type); } template <Type type> - std::string Negate(Operation operation) { - return GenerateUnary(operation, "-", type, type, true); + Expression Negate(Operation operation) { + return GenerateUnary(operation, "-", type, type); } template <Type type> - std::string Absolute(Operation operation) { - return GenerateUnary(operation, "abs", type, type, false); + Expression Absolute(Operation operation) { + return GenerateUnary(operation, "abs", type, type); } - std::string FClamp(Operation operation) { + Expression FClamp(Operation operation) { return GenerateTernary(operation, "clamp", Type::Float, Type::Float, Type::Float, Type::Float); } - std::string FCastHalf0(Operation operation) { - const std::string op_a = VisitOperand(operation, 0, Type::HalfFloat); - return fmt::format("({})[0]", op_a); + Expression FCastHalf0(Operation operation) { + return {fmt::format("({})[0]", VisitOperand(operation, 0).AsHalfFloat()), Type::Float}; } - std::string FCastHalf1(Operation operation) { - const std::string op_a = VisitOperand(operation, 0, Type::HalfFloat); - return fmt::format("({})[1]", op_a); + Expression FCastHalf1(Operation operation) { + return {fmt::format("({})[1]", VisitOperand(operation, 0).AsHalfFloat()), Type::Float}; } template <Type type> - std::string Min(Operation operation) { + Expression Min(Operation operation) { return GenerateBinaryCall(operation, "min", type, type, type); } template <Type type> - std::string Max(Operation operation) { + Expression Max(Operation operation) { return GenerateBinaryCall(operation, "max", type, type, type); } - std::string Select(Operation operation) { - const std::string condition = Visit(operation[0]); - const std::string true_case = Visit(operation[1]); - const std::string false_case = Visit(operation[2]); - const std::string op_str = fmt::format("({} ? {} : {})", condition, true_case, false_case); + Expression Select(Operation operation) { + const std::string condition = Visit(operation[0]).AsBool(); + const std::string true_case = Visit(operation[1]).AsUint(); + const std::string false_case = Visit(operation[2]).AsUint(); + std::string op_str = fmt::format("({} ? {} : {})", condition, true_case, false_case); - return ApplyPrecise(operation, op_str); + return ApplyPrecise(operation, std::move(op_str), Type::Uint); } - std::string FCos(Operation operation) { - return GenerateUnary(operation, "cos", Type::Float, Type::Float, false); + Expression FCos(Operation operation) { + return GenerateUnary(operation, "cos", Type::Float, Type::Float); } - std::string FSin(Operation operation) { - return GenerateUnary(operation, "sin", Type::Float, Type::Float, false); + Expression FSin(Operation operation) { + return GenerateUnary(operation, "sin", Type::Float, Type::Float); } - std::string FExp2(Operation operation) { - return GenerateUnary(operation, "exp2", Type::Float, Type::Float, false); + Expression FExp2(Operation operation) { + return GenerateUnary(operation, "exp2", Type::Float, Type::Float); } - std::string FLog2(Operation operation) { - return GenerateUnary(operation, "log2", Type::Float, Type::Float, false); + Expression FLog2(Operation operation) { + return GenerateUnary(operation, "log2", Type::Float, Type::Float); } - std::string FInverseSqrt(Operation operation) { - return GenerateUnary(operation, "inversesqrt", Type::Float, Type::Float, false); + Expression FInverseSqrt(Operation operation) { + return GenerateUnary(operation, "inversesqrt", Type::Float, Type::Float); } - std::string FSqrt(Operation operation) { - return GenerateUnary(operation, "sqrt", Type::Float, Type::Float, false); + Expression FSqrt(Operation operation) { + return GenerateUnary(operation, "sqrt", Type::Float, Type::Float); } - std::string FRoundEven(Operation operation) { - return GenerateUnary(operation, "roundEven", Type::Float, Type::Float, false); + Expression FRoundEven(Operation operation) { + return GenerateUnary(operation, "roundEven", Type::Float, Type::Float); } - std::string FFloor(Operation operation) { - return GenerateUnary(operation, "floor", Type::Float, Type::Float, false); + Expression FFloor(Operation operation) { + return GenerateUnary(operation, "floor", Type::Float, Type::Float); } - std::string FCeil(Operation operation) { - return GenerateUnary(operation, "ceil", Type::Float, Type::Float, false); + Expression FCeil(Operation operation) { + return GenerateUnary(operation, "ceil", Type::Float, Type::Float); } - std::string FTrunc(Operation operation) { - return GenerateUnary(operation, "trunc", Type::Float, Type::Float, false); + Expression FTrunc(Operation operation) { + return GenerateUnary(operation, "trunc", Type::Float, Type::Float); } template <Type type> - std::string FCastInteger(Operation operation) { - return GenerateUnary(operation, "float", Type::Float, type, false); + Expression FCastInteger(Operation operation) { + return GenerateUnary(operation, "float", Type::Float, type); } - std::string ICastFloat(Operation operation) { - return GenerateUnary(operation, "int", Type::Int, Type::Float, false); + Expression ICastFloat(Operation operation) { + return GenerateUnary(operation, "int", Type::Int, Type::Float); } - std::string ICastUnsigned(Operation operation) { - return GenerateUnary(operation, "int", Type::Int, Type::Uint, false); + Expression ICastUnsigned(Operation operation) { + return GenerateUnary(operation, "int", Type::Int, Type::Uint); } template <Type type> - std::string LogicalShiftLeft(Operation operation) { + Expression LogicalShiftLeft(Operation operation) { return GenerateBinaryInfix(operation, "<<", type, type, Type::Uint); } - std::string ILogicalShiftRight(Operation operation) { - const std::string op_a = VisitOperand(operation, 0, Type::Uint); - const std::string op_b = VisitOperand(operation, 1, Type::Uint); - const std::string op_str = fmt::format("int({} >> {})", op_a, op_b); + Expression ILogicalShiftRight(Operation operation) { + const std::string op_a = VisitOperand(operation, 0).AsUint(); + const std::string op_b = VisitOperand(operation, 1).AsUint(); + std::string op_str = fmt::format("int({} >> {})", op_a, op_b); - return ApplyPrecise(operation, BitwiseCastResult(op_str, Type::Int)); + return ApplyPrecise(operation, std::move(op_str), Type::Int); } - std::string IArithmeticShiftRight(Operation operation) { + Expression IArithmeticShiftRight(Operation operation) { return GenerateBinaryInfix(operation, ">>", Type::Int, Type::Int, Type::Uint); } template <Type type> - std::string BitwiseAnd(Operation operation) { + Expression BitwiseAnd(Operation operation) { return GenerateBinaryInfix(operation, "&", type, type, type); } template <Type type> - std::string BitwiseOr(Operation operation) { + Expression BitwiseOr(Operation operation) { return GenerateBinaryInfix(operation, "|", type, type, type); } template <Type type> - std::string BitwiseXor(Operation operation) { + Expression BitwiseXor(Operation operation) { return GenerateBinaryInfix(operation, "^", type, type, type); } template <Type type> - std::string BitwiseNot(Operation operation) { - return GenerateUnary(operation, "~", type, type, false); + Expression BitwiseNot(Operation operation) { + return GenerateUnary(operation, "~", type, type); } - std::string UCastFloat(Operation operation) { - return GenerateUnary(operation, "uint", Type::Uint, Type::Float, false); + Expression UCastFloat(Operation operation) { + return GenerateUnary(operation, "uint", Type::Uint, Type::Float); } - std::string UCastSigned(Operation operation) { - return GenerateUnary(operation, "uint", Type::Uint, Type::Int, false); + Expression UCastSigned(Operation operation) { + return GenerateUnary(operation, "uint", Type::Uint, Type::Int); } - std::string UShiftRight(Operation operation) { + Expression UShiftRight(Operation operation) { return GenerateBinaryInfix(operation, ">>", Type::Uint, Type::Uint, Type::Uint); } template <Type type> - std::string BitfieldInsert(Operation operation) { + Expression BitfieldInsert(Operation operation) { return GenerateQuaternary(operation, "bitfieldInsert", type, type, type, Type::Int, Type::Int); } template <Type type> - std::string BitfieldExtract(Operation operation) { + Expression BitfieldExtract(Operation operation) { return GenerateTernary(operation, "bitfieldExtract", type, type, Type::Int, Type::Int); } template <Type type> - std::string BitCount(Operation operation) { - return GenerateUnary(operation, "bitCount", type, type, false); + Expression BitCount(Operation operation) { + return GenerateUnary(operation, "bitCount", type, type); } - std::string HNegate(Operation operation) { + Expression HNegate(Operation operation) { const auto GetNegate = [&](std::size_t index) { - return VisitOperand(operation, index, Type::Bool) + " ? -1 : 1"; + return VisitOperand(operation, index).AsBool() + " ? -1 : 1"; }; - const std::string value = - fmt::format("({} * vec2({}, {}))", VisitOperand(operation, 0, Type::HalfFloat), - GetNegate(1), GetNegate(2)); - return BitwiseCastResult(value, Type::HalfFloat); - } - - std::string HClamp(Operation operation) { - const std::string value = VisitOperand(operation, 0, Type::HalfFloat); - const std::string min = VisitOperand(operation, 1, Type::Float); - const std::string max = VisitOperand(operation, 2, Type::Float); - const std::string clamped = fmt::format("clamp({}, vec2({}), vec2({}))", value, min, max); - - return ApplyPrecise(operation, BitwiseCastResult(clamped, Type::HalfFloat)); - } - - std::string HCastFloat(Operation operation) { - const std::string op_a = VisitOperand(operation, 0, Type::Float); - return fmt::format("fromHalf2(vec2({}, 0.0f))", op_a); - } - - std::string HUnpack(Operation operation) { - const std::string operand{VisitOperand(operation, 0, Type::HalfFloat)}; - const auto value = [&]() -> std::string { - switch (std::get<Tegra::Shader::HalfType>(operation.GetMeta())) { - case Tegra::Shader::HalfType::H0_H1: - return operand; - case Tegra::Shader::HalfType::F32: - return fmt::format("vec2(fromHalf2({}))", operand); - case Tegra::Shader::HalfType::H0_H0: - return fmt::format("vec2({}[0])", operand); - case Tegra::Shader::HalfType::H1_H1: - return fmt::format("vec2({}[1])", operand); - } - UNREACHABLE(); - return "0"; - }(); - return fmt::format("fromHalf2({})", value); + return {fmt::format("({} * vec2({}, {}))", VisitOperand(operation, 0).AsHalfFloat(), + GetNegate(1), GetNegate(2)), + Type::HalfFloat}; + } + + Expression HClamp(Operation operation) { + const std::string value = VisitOperand(operation, 0).AsHalfFloat(); + const std::string min = VisitOperand(operation, 1).AsFloat(); + const std::string max = VisitOperand(operation, 2).AsFloat(); + std::string clamped = fmt::format("clamp({}, vec2({}), vec2({}))", value, min, max); + + return ApplyPrecise(operation, std::move(clamped), Type::HalfFloat); + } + + Expression HCastFloat(Operation operation) { + return {fmt::format("vec2({})", VisitOperand(operation, 0).AsFloat()), Type::HalfFloat}; + } + + Expression HUnpack(Operation operation) { + Expression operand = VisitOperand(operation, 0); + switch (std::get<Tegra::Shader::HalfType>(operation.GetMeta())) { + case Tegra::Shader::HalfType::H0_H1: + return operand; + case Tegra::Shader::HalfType::F32: + return {fmt::format("vec2({})", operand.AsFloat()), Type::HalfFloat}; + case Tegra::Shader::HalfType::H0_H0: + return {fmt::format("vec2({}[0])", operand.AsHalfFloat()), Type::HalfFloat}; + case Tegra::Shader::HalfType::H1_H1: + return {fmt::format("vec2({}[1])", operand.AsHalfFloat()), Type::HalfFloat}; + } } - std::string HMergeF32(Operation operation) { - return fmt::format("float(toHalf2({})[0])", Visit(operation[0])); + Expression HMergeF32(Operation operation) { + return {fmt::format("float({}[0])", VisitOperand(operation, 0).AsHalfFloat()), Type::Float}; } - std::string HMergeH0(Operation operation) { - return fmt::format("fromHalf2(vec2(toHalf2({})[0], toHalf2({})[1]))", Visit(operation[1]), - Visit(operation[0])); + Expression HMergeH0(Operation operation) { + std::string dest = VisitOperand(operation, 0).AsUint(); + std::string src = VisitOperand(operation, 1).AsUint(); + return {fmt::format("(({} & 0x0000FFFFU) | ({} & 0xFFFF0000U))", src, dest), Type::Uint}; } - std::string HMergeH1(Operation operation) { - return fmt::format("fromHalf2(vec2(toHalf2({})[0], toHalf2({})[1]))", Visit(operation[0]), - Visit(operation[1])); + Expression HMergeH1(Operation operation) { + std::string dest = VisitOperand(operation, 0).AsUint(); + std::string src = VisitOperand(operation, 1).AsUint(); + return {fmt::format("(({} & 0x0000FFFFU) | ({} & 0xFFFF0000U))", dest, src), Type::Uint}; } - std::string HPack2(Operation operation) { - return fmt::format("utof(packHalf2x16(vec2({}, {})))", Visit(operation[0]), - Visit(operation[1])); + Expression HPack2(Operation operation) { + return {fmt::format("vec2({}, {})", VisitOperand(operation, 0).AsFloat(), + VisitOperand(operation, 1).AsFloat()), + Type::HalfFloat}; } template <Type type> - std::string LogicalLessThan(Operation operation) { + Expression LogicalLessThan(Operation operation) { return GenerateBinaryInfix(operation, "<", Type::Bool, type, type); } template <Type type> - std::string LogicalEqual(Operation operation) { + Expression LogicalEqual(Operation operation) { return GenerateBinaryInfix(operation, "==", Type::Bool, type, type); } template <Type type> - std::string LogicalLessEqual(Operation operation) { + Expression LogicalLessEqual(Operation operation) { return GenerateBinaryInfix(operation, "<=", Type::Bool, type, type); } template <Type type> - std::string LogicalGreaterThan(Operation operation) { + Expression LogicalGreaterThan(Operation operation) { return GenerateBinaryInfix(operation, ">", Type::Bool, type, type); } template <Type type> - std::string LogicalNotEqual(Operation operation) { + Expression LogicalNotEqual(Operation operation) { return GenerateBinaryInfix(operation, "!=", Type::Bool, type, type); } template <Type type> - std::string LogicalGreaterEqual(Operation operation) { + Expression LogicalGreaterEqual(Operation operation) { return GenerateBinaryInfix(operation, ">=", Type::Bool, type, type); } - std::string LogicalFIsNan(Operation operation) { - return GenerateUnary(operation, "isnan", Type::Bool, Type::Float, false); + Expression LogicalFIsNan(Operation operation) { + return GenerateUnary(operation, "isnan", Type::Bool, Type::Float); } - std::string LogicalAssign(Operation operation) { + Expression LogicalAssign(Operation operation) { const Node& dest = operation[0]; const Node& src = operation[1]; @@ -1400,78 +1502,80 @@ private: target = GetInternalFlag(flag->GetFlag()); } - code.AddLine("{} = {};", target, Visit(src)); + code.AddLine("{} = {};", target, Visit(src).AsBool()); return {}; } - std::string LogicalAnd(Operation operation) { + Expression LogicalAnd(Operation operation) { return GenerateBinaryInfix(operation, "&&", Type::Bool, Type::Bool, Type::Bool); } - std::string LogicalOr(Operation operation) { + Expression LogicalOr(Operation operation) { return GenerateBinaryInfix(operation, "||", Type::Bool, Type::Bool, Type::Bool); } - std::string LogicalXor(Operation operation) { + Expression LogicalXor(Operation operation) { return GenerateBinaryInfix(operation, "^^", Type::Bool, Type::Bool, Type::Bool); } - std::string LogicalNegate(Operation operation) { - return GenerateUnary(operation, "!", Type::Bool, Type::Bool, false); + Expression LogicalNegate(Operation operation) { + return GenerateUnary(operation, "!", Type::Bool, Type::Bool); } - std::string LogicalPick2(Operation operation) { - const std::string pair = VisitOperand(operation, 0, Type::Bool2); - return fmt::format("{}[{}]", pair, VisitOperand(operation, 1, Type::Uint)); + Expression LogicalPick2(Operation operation) { + return {fmt::format("{}[{}]", VisitOperand(operation, 0).AsBool2(), + VisitOperand(operation, 1).AsUint()), + Type::Bool}; } - std::string LogicalAnd2(Operation operation) { + Expression LogicalAnd2(Operation operation) { return GenerateUnary(operation, "all", Type::Bool, Type::Bool2); } template <bool with_nan> - std::string GenerateHalfComparison(Operation operation, const std::string& compare_op) { - const std::string comparison{GenerateBinaryCall(operation, compare_op, Type::Bool2, - Type::HalfFloat, Type::HalfFloat)}; + Expression GenerateHalfComparison(Operation operation, std::string_view compare_op) { + Expression comparison = GenerateBinaryCall(operation, compare_op, Type::Bool2, + Type::HalfFloat, Type::HalfFloat); if constexpr (!with_nan) { return comparison; } - return fmt::format("halfFloatNanComparison({}, {}, {})", comparison, - VisitOperand(operation, 0, Type::HalfFloat), - VisitOperand(operation, 1, Type::HalfFloat)); + return {fmt::format("HalfFloatNanComparison({}, {}, {})", comparison.AsBool2(), + VisitOperand(operation, 0).AsHalfFloat(), + VisitOperand(operation, 1).AsHalfFloat()), + Type::Bool2}; } template <bool with_nan> - std::string Logical2HLessThan(Operation operation) { + Expression Logical2HLessThan(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "lessThan"); } template <bool with_nan> - std::string Logical2HEqual(Operation operation) { + Expression Logical2HEqual(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "equal"); } template <bool with_nan> - std::string Logical2HLessEqual(Operation operation) { + Expression Logical2HLessEqual(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "lessThanEqual"); } template <bool with_nan> - std::string Logical2HGreaterThan(Operation operation) { + Expression Logical2HGreaterThan(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "greaterThan"); } template <bool with_nan> - std::string Logical2HNotEqual(Operation operation) { + Expression Logical2HNotEqual(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "notEqual"); } template <bool with_nan> - std::string Logical2HGreaterEqual(Operation operation) { + Expression Logical2HGreaterEqual(Operation operation) { return GenerateHalfComparison<with_nan>(operation, "greaterThanEqual"); } - std::string Texture(Operation operation) { + Expression Texture(Operation operation) { const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); @@ -1480,10 +1584,10 @@ private: if (meta->sampler.IsShadow()) { expr = "vec4(" + expr + ')'; } - return expr + GetSwizzle(meta->element); + return {expr + GetSwizzle(meta->element), Type::Float}; } - std::string TextureLod(Operation operation) { + Expression TextureLod(Operation operation) { const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); @@ -1492,54 +1596,54 @@ private: if (meta->sampler.IsShadow()) { expr = "vec4(" + expr + ')'; } - return expr + GetSwizzle(meta->element); + return {expr + GetSwizzle(meta->element), Type::Float}; } - std::string TextureGather(Operation operation) { + Expression TextureGather(Operation operation) { const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); const auto type = meta->sampler.IsShadow() ? Type::Float : Type::Int; - return GenerateTexture(operation, "Gather", - {TextureArgument{type, meta->component}, TextureAoffi{}}) + - GetSwizzle(meta->element); + return {GenerateTexture(operation, "Gather", + {TextureArgument{type, meta->component}, TextureAoffi{}}) + + GetSwizzle(meta->element), + Type::Float}; } - std::string TextureQueryDimensions(Operation operation) { + Expression TextureQueryDimensions(Operation operation) { const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); const std::string sampler = GetSampler(meta->sampler); - const std::string lod = VisitOperand(operation, 0, Type::Int); + const std::string lod = VisitOperand(operation, 0).AsInt(); switch (meta->element) { case 0: case 1: - return fmt::format("itof(int(textureSize({}, {}){}))", sampler, lod, - GetSwizzle(meta->element)); - case 2: - return "0"; + return {fmt::format("textureSize({}, {}){}", sampler, lod, GetSwizzle(meta->element)), + Type::Int}; case 3: - return fmt::format("itof(textureQueryLevels({}))", sampler); + return {fmt::format("textureQueryLevels({})", sampler), Type::Int}; } UNREACHABLE(); - return "0"; + return {"0", Type::Int}; } - std::string TextureQueryLod(Operation operation) { + Expression TextureQueryLod(Operation operation) { const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); if (meta->element < 2) { - return fmt::format("itof(int(({} * vec2(256)){}))", - GenerateTexture(operation, "QueryLod", {}), - GetSwizzle(meta->element)); + return {fmt::format("int(({} * vec2(256)){})", + GenerateTexture(operation, "QueryLod", {}), + GetSwizzle(meta->element)), + Type::Int}; } - return "0"; + return {"0", Type::Int}; } - std::string TexelFetch(Operation operation) { - constexpr std::array<const char*, 4> constructors = {"int", "ivec2", "ivec3", "ivec4"}; + Expression TexelFetch(Operation operation) { + constexpr std::array constructors = {"int", "ivec2", "ivec3", "ivec4"}; const auto meta = std::get_if<MetaTexture>(&operation.GetMeta()); ASSERT(meta); UNIMPLEMENTED_IF(meta->sampler.IsArray()); @@ -1552,7 +1656,7 @@ private: expr += constructors.at(operation.GetOperandsCount() - 1); expr += '('; for (std::size_t i = 0; i < count; ++i) { - expr += VisitOperand(operation, i, Type::Int); + expr += VisitOperand(operation, i).AsInt(); const std::size_t next = i + 1; if (next == count) expr += ')'; @@ -1565,7 +1669,7 @@ private: if (meta->lod) { expr += ", "; - expr += CastOperand(Visit(meta->lod), Type::Int); + expr += Visit(meta->lod).AsInt(); } expr += ')'; expr += GetSwizzle(meta->element); @@ -1580,11 +1684,11 @@ private: code.AddLine("float {} = {};", tmp, expr); code.AddLine("#endif"); - return tmp; + return {tmp, Type::Float}; } - std::string ImageStore(Operation operation) { - constexpr std::array<const char*, 4> constructors{"int(", "ivec2(", "ivec3(", "ivec4("}; + Expression ImageStore(Operation operation) { + constexpr std::array constructors{"int(", "ivec2(", "ivec3(", "ivec4("}; const auto meta{std::get<MetaImage>(operation.GetMeta())}; std::string expr = "imageStore("; @@ -1594,7 +1698,7 @@ private: const std::size_t coords_count{operation.GetOperandsCount()}; expr += constructors.at(coords_count - 1); for (std::size_t i = 0; i < coords_count; ++i) { - expr += VisitOperand(operation, i, Type::Int); + expr += VisitOperand(operation, i).AsInt(); if (i + 1 < coords_count) { expr += ", "; } @@ -1605,7 +1709,7 @@ private: UNIMPLEMENTED_IF(values_count != 4); expr += "vec4("; for (std::size_t i = 0; i < values_count; ++i) { - expr += Visit(meta.values.at(i)); + expr += Visit(meta.values.at(i)).AsFloat(); if (i + 1 < values_count) { expr += ", "; } @@ -1616,52 +1720,52 @@ private: return {}; } - std::string Branch(Operation operation) { + Expression Branch(Operation operation) { const auto target = std::get_if<ImmediateNode>(&*operation[0]); UNIMPLEMENTED_IF(!target); - code.AddLine("jmp_to = 0x{:x}u;", target->GetValue()); + code.AddLine("jmp_to = 0x{:X}U;", target->GetValue()); code.AddLine("break;"); return {}; } - std::string BranchIndirect(Operation operation) { - const std::string op_a = VisitOperand(operation, 0, Type::Uint); + Expression BranchIndirect(Operation operation) { + const std::string op_a = VisitOperand(operation, 0).AsUint(); code.AddLine("jmp_to = {};", op_a); code.AddLine("break;"); return {}; } - std::string PushFlowStack(Operation operation) { + Expression PushFlowStack(Operation operation) { const auto stack = std::get<MetaStackClass>(operation.GetMeta()); const auto target = std::get_if<ImmediateNode>(&*operation[0]); UNIMPLEMENTED_IF(!target); - code.AddLine("{}[{}++] = 0x{:x}u;", FlowStackName(stack), FlowStackTopName(stack), + code.AddLine("{}[{}++] = 0x{:X}U;", FlowStackName(stack), FlowStackTopName(stack), target->GetValue()); return {}; } - std::string PopFlowStack(Operation operation) { + Expression PopFlowStack(Operation operation) { const auto stack = std::get<MetaStackClass>(operation.GetMeta()); code.AddLine("jmp_to = {}[--{}];", FlowStackName(stack), FlowStackTopName(stack)); code.AddLine("break;"); return {}; } - std::string Exit(Operation operation) { + Expression Exit(Operation operation) { if (stage != ProgramType::Fragment) { code.AddLine("return;"); return {}; } const auto& used_registers = ir.GetRegisters(); - const auto SafeGetRegister = [&](u32 reg) -> std::string { + const auto SafeGetRegister = [&](u32 reg) -> Expression { // TODO(Rodrigo): Replace with contains once C++20 releases if (used_registers.find(reg) != used_registers.end()) { - return GetRegister(reg); + return {GetRegister(reg), Type::Float}; } - return "0.0f"; + return {"0.0f", Type::Float}; }; UNIMPLEMENTED_IF_MSG(header.ps.omap.sample_mask != 0, "Sample mask write is unimplemented"); @@ -1674,7 +1778,7 @@ private: for (u32 component = 0; component < 4; ++component) { if (header.ps.IsColorComponentOutputEnabled(render_target, component)) { code.AddLine("FragColor{}[{}] = {};", render_target, component, - SafeGetRegister(current_reg)); + SafeGetRegister(current_reg).AsFloat()); ++current_reg; } } @@ -1683,14 +1787,14 @@ private: if (header.ps.omap.depth) { // The depth output is always 2 registers after the last color output, and current_reg // already contains one past the last color register. - code.AddLine("gl_FragDepth = {};", SafeGetRegister(current_reg + 1)); + code.AddLine("gl_FragDepth = {};", SafeGetRegister(current_reg + 1).AsFloat()); } code.AddLine("return;"); return {}; } - std::string Discard(Operation operation) { + Expression Discard(Operation operation) { // Enclose "discard" in a conditional, so that GLSL compilation does not complain // about unexecuted instructions that may follow this. code.AddLine("if (true) {{"); @@ -1701,7 +1805,7 @@ private: return {}; } - std::string EmitVertex(Operation operation) { + Expression EmitVertex(Operation operation) { ASSERT_MSG(stage == ProgramType::Geometry, "EmitVertex is expected to be used in a geometry shader."); @@ -1712,7 +1816,7 @@ private: return {}; } - std::string EndPrimitive(Operation operation) { + Expression EndPrimitive(Operation operation) { ASSERT_MSG(stage == ProgramType::Geometry, "EndPrimitive is expected to be used in a geometry shader."); @@ -1720,59 +1824,59 @@ private: return {}; } - std::string YNegate(Operation operation) { + Expression YNegate(Operation operation) { // Config pack's third value is Y_NEGATE's state. - return "uintBitsToFloat(config_pack[2])"; + return {"config_pack[2]", Type::Uint}; } template <u32 element> - std::string LocalInvocationId(Operation) { - return "utof(gl_LocalInvocationID"s + GetSwizzle(element) + ')'; + Expression LocalInvocationId(Operation) { + return {"gl_LocalInvocationID"s + GetSwizzle(element), Type::Uint}; } template <u32 element> - std::string WorkGroupId(Operation) { - return "utof(gl_WorkGroupID"s + GetSwizzle(element) + ')'; + Expression WorkGroupId(Operation) { + return {"gl_WorkGroupID"s + GetSwizzle(element), Type::Uint}; } - std::string BallotThread(Operation operation) { - const std::string value = VisitOperand(operation, 0, Type::Bool); + Expression BallotThread(Operation operation) { + const std::string value = VisitOperand(operation, 0).AsBool(); if (!device.HasWarpIntrinsics()) { LOG_ERROR(Render_OpenGL, "Nvidia warp intrinsics are not available and its required by a shader"); // Stub on non-Nvidia devices by simulating all threads voting the same as the active // one. - return fmt::format("utof({} ? 0xFFFFFFFFU : 0U)", value); + return {fmt::format("({} ? 0xFFFFFFFFU : 0U)", value), Type::Uint}; } - return fmt::format("utof(ballotThreadNV({}))", value); + return {fmt::format("ballotThreadNV({})", value), Type::Uint}; } - std::string Vote(Operation operation, const char* func) { - const std::string value = VisitOperand(operation, 0, Type::Bool); + Expression Vote(Operation operation, const char* func) { + const std::string value = VisitOperand(operation, 0).AsBool(); if (!device.HasWarpIntrinsics()) { LOG_ERROR(Render_OpenGL, "Nvidia vote intrinsics are not available and its required by a shader"); // Stub with a warp size of one. - return value; + return {value, Type::Bool}; } - return fmt::format("{}({})", func, value); + return {fmt::format("{}({})", func, value), Type::Bool}; } - std::string VoteAll(Operation operation) { + Expression VoteAll(Operation operation) { return Vote(operation, "allThreadsNV"); } - std::string VoteAny(Operation operation) { + Expression VoteAny(Operation operation) { return Vote(operation, "anyThreadNV"); } - std::string VoteEqual(Operation operation) { + Expression VoteEqual(Operation operation) { if (!device.HasWarpIntrinsics()) { LOG_ERROR(Render_OpenGL, "Nvidia vote intrinsics are not available and its required by a shader"); // We must return true here since a stub for a theoretical warp size of 1 will always // return an equal result for all its votes. - return "true"; + return {"true", Type::Bool}; } return Vote(operation, "allThreadsEqualNV"); } @@ -1973,8 +2077,8 @@ private: } std::string GetInternalFlag(InternalFlag flag) const { - constexpr std::array<const char*, 4> InternalFlagNames = {"zero_flag", "sign_flag", - "carry_flag", "overflow_flag"}; + constexpr std::array InternalFlagNames = {"zero_flag", "sign_flag", "carry_flag", + "overflow_flag"}; const auto index = static_cast<u32>(flag); ASSERT(index < static_cast<u32>(InternalFlag::Amount)); @@ -2022,24 +2126,16 @@ private: std::string GetCommonDeclarations() { return fmt::format( - "#define MAX_CONSTBUFFER_ELEMENTS {}\n" "#define ftoi floatBitsToInt\n" "#define ftou floatBitsToUint\n" "#define itof intBitsToFloat\n" "#define utof uintBitsToFloat\n\n" - "float fromHalf2(vec2 pair) {{\n" - " return utof(packHalf2x16(pair));\n" - "}}\n\n" - "vec2 toHalf2(float value) {{\n" - " return unpackHalf2x16(ftou(value));\n" - "}}\n\n" - "bvec2 halfFloatNanComparison(bvec2 comparison, vec2 pair1, vec2 pair2) {{\n" + "bvec2 HalfFloatNanComparison(bvec2 comparison, vec2 pair1, vec2 pair2) {{\n" " bvec2 is_nan1 = isnan(pair1);\n" " bvec2 is_nan2 = isnan(pair2);\n" " return bvec2(comparison.x || is_nan1.x || is_nan2.x, comparison.y || is_nan1.y || " "is_nan2.y);\n" - "}}\n", - MAX_CONSTBUFFER_ELEMENTS); + "}}\n\n"); } ProgramResult Decompile(const Device& device, const ShaderIR& ir, ProgramType stage, diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 0456248ac..f594106bf 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -517,10 +517,37 @@ void Config::ReadPathValues() { UISettings::values.roms_path = ReadSetting(QStringLiteral("romsPath")).toString(); UISettings::values.symbols_path = ReadSetting(QStringLiteral("symbolsPath")).toString(); UISettings::values.screenshot_path = ReadSetting(QStringLiteral("screenshotPath")).toString(); - UISettings::values.game_directory_path = + UISettings::values.game_dir_deprecated = ReadSetting(QStringLiteral("gameListRootDir"), QStringLiteral(".")).toString(); - UISettings::values.game_directory_deepscan = + UISettings::values.game_dir_deprecated_deepscan = ReadSetting(QStringLiteral("gameListDeepScan"), false).toBool(); + const int gamedirs_size = qt_config->beginReadArray(QStringLiteral("gamedirs")); + for (int i = 0; i < gamedirs_size; ++i) { + qt_config->setArrayIndex(i); + UISettings::GameDir game_dir; + game_dir.path = ReadSetting(QStringLiteral("path")).toString(); + game_dir.deep_scan = ReadSetting(QStringLiteral("deep_scan"), false).toBool(); + game_dir.expanded = ReadSetting(QStringLiteral("expanded"), true).toBool(); + UISettings::values.game_dirs.append(game_dir); + } + qt_config->endArray(); + // create NAND and SD card directories if empty, these are not removable through the UI, + // also carries over old game list settings if present + if (UISettings::values.game_dirs.isEmpty()) { + UISettings::GameDir game_dir; + game_dir.path = QStringLiteral("SDMC"); + game_dir.expanded = true; + UISettings::values.game_dirs.append(game_dir); + game_dir.path = QStringLiteral("UserNAND"); + UISettings::values.game_dirs.append(game_dir); + game_dir.path = QStringLiteral("SysNAND"); + UISettings::values.game_dirs.append(game_dir); + if (UISettings::values.game_dir_deprecated != QStringLiteral(".")) { + game_dir.path = UISettings::values.game_dir_deprecated; + game_dir.deep_scan = UISettings::values.game_dir_deprecated_deepscan; + UISettings::values.game_dirs.append(game_dir); + } + } UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); qt_config->endGroup(); @@ -899,10 +926,15 @@ void Config::SavePathValues() { WriteSetting(QStringLiteral("romsPath"), UISettings::values.roms_path); WriteSetting(QStringLiteral("symbolsPath"), UISettings::values.symbols_path); WriteSetting(QStringLiteral("screenshotPath"), UISettings::values.screenshot_path); - WriteSetting(QStringLiteral("gameListRootDir"), UISettings::values.game_directory_path, - QStringLiteral(".")); - WriteSetting(QStringLiteral("gameListDeepScan"), UISettings::values.game_directory_deepscan, - false); + qt_config->beginWriteArray(QStringLiteral("gamedirs")); + for (int i = 0; i < UISettings::values.game_dirs.size(); ++i) { + qt_config->setArrayIndex(i); + const auto& game_dir = UISettings::values.game_dirs[i]; + WriteSetting(QStringLiteral("path"), game_dir.path); + WriteSetting(QStringLiteral("deep_scan"), game_dir.deep_scan, false); + WriteSetting(QStringLiteral("expanded"), game_dir.expanded, true); + } + qt_config->endArray(); WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); qt_config->endGroup(); diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index e636964e3..775e3f2ea 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -68,12 +68,14 @@ void ConfigureDialog::RetranslateUI() { ui->tabWidget->setCurrentIndex(old_index); } +Q_DECLARE_METATYPE(QList<QWidget*>); + void ConfigureDialog::PopulateSelectionList() { - const std::array<std::pair<QString, QStringList>, 4> items{ - {{tr("General"), {tr("General"), tr("Web"), tr("Debug"), tr("Game List")}}, - {tr("System"), {tr("System"), tr("Profiles"), tr("Audio")}}, - {tr("Graphics"), {tr("Graphics")}}, - {tr("Controls"), {tr("Input"), tr("Hotkeys")}}}, + const std::array<std::pair<QString, QList<QWidget*>>, 4> items{ + {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}}, + {tr("System"), {ui->systemTab, ui->profileManagerTab, ui->audioTab}}, + {tr("Graphics"), {ui->graphicsTab}}, + {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}, }; [[maybe_unused]] const QSignalBlocker blocker(ui->selectorList); @@ -81,7 +83,7 @@ void ConfigureDialog::PopulateSelectionList() { ui->selectorList->clear(); for (const auto& entry : items) { auto* const item = new QListWidgetItem(entry.first); - item->setData(Qt::UserRole, entry.second); + item->setData(Qt::UserRole, QVariant::fromValue(entry.second)); ui->selectorList->addItem(item); } @@ -93,24 +95,26 @@ void ConfigureDialog::UpdateVisibleTabs() { return; } - const std::map<QString, QWidget*> widgets = { - {tr("General"), ui->generalTab}, - {tr("System"), ui->systemTab}, - {tr("Profiles"), ui->profileManagerTab}, - {tr("Input"), ui->inputTab}, - {tr("Hotkeys"), ui->hotkeysTab}, - {tr("Graphics"), ui->graphicsTab}, - {tr("Audio"), ui->audioTab}, - {tr("Debug"), ui->debugTab}, - {tr("Web"), ui->webTab}, - {tr("Game List"), ui->gameListTab}, + const std::map<QWidget*, QString> widgets = { + {ui->generalTab, tr("General")}, + {ui->systemTab, tr("System")}, + {ui->profileManagerTab, tr("Profiles")}, + {ui->inputTab, tr("Input")}, + {ui->hotkeysTab, tr("Hotkeys")}, + {ui->graphicsTab, tr("Graphics")}, + {ui->audioTab, tr("Audio")}, + {ui->debugTab, tr("Debug")}, + {ui->webTab, tr("Web")}, + {ui->gameListTab, tr("Game List")}, }; [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget); ui->tabWidget->clear(); - const QStringList tabs = items[0]->data(Qt::UserRole).toStringList(); - for (const auto& tab : tabs) { - ui->tabWidget->addTab(widgets.find(tab)->second, tab); + + const QList<QWidget*> tabs = qvariant_cast<QList<QWidget*>>(items[0]->data(Qt::UserRole)); + + for (const auto tab : tabs) { + ui->tabWidget->addTab(tab, widgets.at(tab)); } } diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index 75fcbfea3..10bcd650e 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp @@ -20,25 +20,29 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) SetConfiguration(); - connect(ui->toggle_deepscan, &QCheckBox::stateChanged, this, - [] { UISettings::values.is_game_list_reload_pending.exchange(true); }); + connect(ui->toggle_frame_limit, &QCheckBox::toggled, ui->frame_limit, &QSpinBox::setEnabled); } ConfigureGeneral::~ConfigureGeneral() = default; void ConfigureGeneral::SetConfiguration() { - ui->toggle_deepscan->setChecked(UISettings::values.game_directory_deepscan); ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_user_on_boot->setChecked(UISettings::values.select_user_on_boot); ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); + + ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit); + ui->frame_limit->setEnabled(ui->toggle_frame_limit->isChecked()); + ui->frame_limit->setValue(Settings::values.frame_limit); } void ConfigureGeneral::ApplyConfiguration() { - UISettings::values.game_directory_deepscan = ui->toggle_deepscan->isChecked(); UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.select_user_on_boot = ui->toggle_user_on_boot->isChecked(); UISettings::values.theme = ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString(); + + Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked(); + Settings::values.frame_limit = ui->frame_limit->value(); } void ConfigureGeneral::changeEvent(QEvent* event) { diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui index 184fdd329..0bb91d64b 100644 --- a/src/yuzu/configuration/configure_general.ui +++ b/src/yuzu/configuration/configure_general.ui @@ -25,11 +25,31 @@ <item> <layout class="QVBoxLayout" name="GeneralVerticalLayout"> <item> - <widget class="QCheckBox" name="toggle_deepscan"> - <property name="text"> - <string>Search sub-directories for games</string> - </property> - </widget> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QCheckBox" name="toggle_frame_limit"> + <property name="text"> + <string>Limit Speed Percent</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="frame_limit"> + <property name="suffix"> + <string>%</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>9999</number> + </property> + <property name="value"> + <number>100</number> + </property> + </widget> + </item> + </layout> </item> <item> <widget class="QCheckBox" name="toggle_check_exit"> diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 2b17b250c..2c9e322c9 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -55,7 +55,6 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent) SetConfiguration(); - connect(ui->toggle_frame_limit, &QCheckBox::toggled, ui->frame_limit, &QSpinBox::setEnabled); connect(ui->bg_button, &QPushButton::clicked, this, [this] { const QColor new_bg_color = QColorDialog::getColor(bg_color); if (!new_bg_color.isValid()) { @@ -72,9 +71,6 @@ void ConfigureGraphics::SetConfiguration() { ui->resolution_factor_combobox->setCurrentIndex( static_cast<int>(FromResolutionFactor(Settings::values.resolution_factor))); - ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit); - ui->frame_limit->setEnabled(ui->toggle_frame_limit->isChecked()); - ui->frame_limit->setValue(Settings::values.frame_limit); ui->use_disk_shader_cache->setEnabled(runtime_lock); ui->use_disk_shader_cache->setChecked(Settings::values.use_disk_shader_cache); ui->use_accurate_gpu_emulation->setChecked(Settings::values.use_accurate_gpu_emulation); @@ -89,8 +85,6 @@ void ConfigureGraphics::SetConfiguration() { void ConfigureGraphics::ApplyConfiguration() { Settings::values.resolution_factor = ToResolutionFactor(static_cast<Resolution>(ui->resolution_factor_combobox->currentIndex())); - Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked(); - Settings::values.frame_limit = ui->frame_limit->value(); Settings::values.use_disk_shader_cache = ui->use_disk_shader_cache->isChecked(); Settings::values.use_accurate_gpu_emulation = ui->use_accurate_gpu_emulation->isChecked(); Settings::values.use_asynchronous_gpu_emulation = diff --git a/src/yuzu/configuration/configure_graphics.ui b/src/yuzu/configuration/configure_graphics.ui index 15ab18ecd..0309ee300 100644 --- a/src/yuzu/configuration/configure_graphics.ui +++ b/src/yuzu/configuration/configure_graphics.ui @@ -23,33 +23,6 @@ </property> <layout class="QVBoxLayout" name="verticalLayout_2"> <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QCheckBox" name="toggle_frame_limit"> - <property name="text"> - <string>Limit Speed Percent</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="frame_limit"> - <property name="suffix"> - <string>%</string> - </property> - <property name="minimum"> - <number>1</number> - </property> - <property name="maximum"> - <number>9999</number> - </property> - <property name="value"> - <number>100</number> - </property> - </widget> - </item> - </layout> - </item> - <item> <widget class="QCheckBox" name="use_disk_shader_cache"> <property name="text"> <string>Use disk shader cache</string> diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index 7b70f307c..a968cfb5d 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -301,13 +301,16 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i }); } connect(analog_map_stick[analog_id], &QPushButton::clicked, [=] { - QMessageBox::information(this, tr("Information"), - tr("After pressing OK, first move your joystick horizontally, " - "and then vertically.")); - HandleClick( - analog_map_stick[analog_id], - [=](const Common::ParamPackage& params) { analogs_param[analog_id] = params; }, - InputCommon::Polling::DeviceType::Analog); + if (QMessageBox::information( + this, tr("Information"), + tr("After pressing OK, first move your joystick horizontally, " + "and then vertically."), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { + HandleClick( + analog_map_stick[analog_id], + [=](const Common::ParamPackage& params) { analogs_param[analog_id] = params; }, + InputCommon::Polling::DeviceType::Analog); + } }); } diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index d18b96519..d5fab2f1f 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -34,7 +34,6 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve return QObject::eventFilter(obj, event); QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); - int rowCount = gamelist->tree_view->model()->rowCount(); QString edit_filter_text = gamelist->search_field->edit_filter->text().toLower(); // If the searchfield's text hasn't changed special function keys get checked @@ -56,19 +55,9 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve // If there is only one result launch this game case Qt::Key_Return: case Qt::Key_Enter: { - QStandardItemModel* item_model = new QStandardItemModel(gamelist->tree_view); - QModelIndex root_index = item_model->invisibleRootItem()->index(); - QStandardItem* child_file; - QString file_path; - int resultCount = 0; - for (int i = 0; i < rowCount; ++i) { - if (!gamelist->tree_view->isRowHidden(i, root_index)) { - ++resultCount; - child_file = gamelist->item_model->item(i, 0); - file_path = child_file->data(GameListItemPath::FullPathRole).toString(); - } - } - if (resultCount == 1) { + if (gamelist->search_field->visible == 1) { + QString file_path = gamelist->getLastFilterResultItem(); + // To avoid loading error dialog loops while confirming them using enter // Also users usually want to run a different game after closing one gamelist->search_field->edit_filter->clear(); @@ -88,9 +77,31 @@ bool GameListSearchField::KeyReleaseEater::eventFilter(QObject* obj, QEvent* eve } void GameListSearchField::setFilterResult(int visible, int total) { + this->visible = visible; + this->total = total; + label_filter_result->setText(tr("%1 of %n result(s)", "", total).arg(visible)); } +QString GameList::getLastFilterResultItem() const { + QStandardItem* folder; + QStandardItem* child; + QString file_path; + const int folder_count = item_model->rowCount(); + for (int i = 0; i < folder_count; ++i) { + folder = item_model->item(i, 0); + const QModelIndex folder_index = folder->index(); + const int children_count = folder->rowCount(); + for (int j = 0; j < children_count; ++j) { + if (!tree_view->isRowHidden(j, folder_index)) { + child = folder->child(j, 0); + file_path = child->data(GameListItemPath::FullPathRole).toString(); + } + } + } + return file_path; +} + void GameListSearchField::clear() { edit_filter->clear(); } @@ -147,45 +158,120 @@ static bool ContainsAllWords(const QString& haystack, const QString& userinput) [&haystack](const QString& s) { return haystack.contains(s); }); } +// Syncs the expanded state of Game Directories with settings to persist across sessions +void GameList::onItemExpanded(const QModelIndex& item) { + const auto type = item.data(GameListItem::TypeRole).value<GameListItemType>(); + if (type == GameListItemType::CustomDir || type == GameListItemType::SdmcDir || + type == GameListItemType::UserNandDir || type == GameListItemType::SysNandDir) + item.data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded = + tree_view->isExpanded(item); +} + // Event in order to filter the gamelist after editing the searchfield void GameList::onTextChanged(const QString& new_text) { - const int row_count = tree_view->model()->rowCount(); - const QString edit_filter_text = new_text.toLower(); - const QModelIndex root_index = item_model->invisibleRootItem()->index(); + const int folder_count = tree_view->model()->rowCount(); + QString edit_filter_text = new_text.toLower(); + QStandardItem* folder; + QStandardItem* child; + int children_total = 0; + QModelIndex root_index = item_model->invisibleRootItem()->index(); // If the searchfield is empty every item is visible // Otherwise the filter gets applied if (edit_filter_text.isEmpty()) { - for (int i = 0; i < row_count; ++i) { - tree_view->setRowHidden(i, root_index, false); + for (int i = 0; i < folder_count; ++i) { + folder = item_model->item(i, 0); + const QModelIndex folder_index = folder->index(); + const int children_count = folder->rowCount(); + for (int j = 0; j < children_count; ++j) { + ++children_total; + tree_view->setRowHidden(j, folder_index, false); + } } - search_field->setFilterResult(row_count, row_count); + search_field->setFilterResult(children_total, children_total); } else { int result_count = 0; - for (int i = 0; i < row_count; ++i) { - const QStandardItem* child_file = item_model->item(i, 0); - const QString file_path = - child_file->data(GameListItemPath::FullPathRole).toString().toLower(); - const QString file_title = - child_file->data(GameListItemPath::TitleRole).toString().toLower(); - const QString file_program_id = - child_file->data(GameListItemPath::ProgramIdRole).toString().toLower(); - - // Only items which filename in combination with its title contains all words - // that are in the searchfield will be visible in the gamelist - // The search is case insensitive because of toLower() - // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent - // multiple conversions of edit_filter_text for each game in the gamelist - const QString file_name = file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + - QLatin1Char{' '} + file_title; - if (ContainsAllWords(file_name, edit_filter_text) || - (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { - tree_view->setRowHidden(i, root_index, false); - ++result_count; - } else { - tree_view->setRowHidden(i, root_index, true); + for (int i = 0; i < folder_count; ++i) { + folder = item_model->item(i, 0); + const QModelIndex folder_index = folder->index(); + const int children_count = folder->rowCount(); + for (int j = 0; j < children_count; ++j) { + ++children_total; + const QStandardItem* child = folder->child(j, 0); + const QString file_path = + child->data(GameListItemPath::FullPathRole).toString().toLower(); + const QString file_title = + child->data(GameListItemPath::TitleRole).toString().toLower(); + const QString file_program_id = + child->data(GameListItemPath::ProgramIdRole).toString().toLower(); + + // Only items which filename in combination with its title contains all words + // that are in the searchfield will be visible in the gamelist + // The search is case insensitive because of toLower() + // I decided not to use Qt::CaseInsensitive in containsAllWords to prevent + // multiple conversions of edit_filter_text for each game in the gamelist + const QString file_name = + file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + + file_title; + if (ContainsAllWords(file_name, edit_filter_text) || + (file_program_id.count() == 16 && edit_filter_text.contains(file_program_id))) { + tree_view->setRowHidden(j, folder_index, false); + ++result_count; + } else { + tree_view->setRowHidden(j, folder_index, true); + } + search_field->setFilterResult(result_count, children_total); } - search_field->setFilterResult(result_count, row_count); + } + } +} + +void GameList::onUpdateThemedIcons() { + for (int i = 0; i < item_model->invisibleRootItem()->rowCount(); i++) { + QStandardItem* child = item_model->invisibleRootItem()->child(i); + + const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); + switch (child->data(GameListItem::TypeRole).value<GameListItemType>()) { + case GameListItemType::SdmcDir: + child->setData( + QIcon::fromTheme(QStringLiteral("sd_card")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::UserNandDir: + child->setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::SysNandDir: + child->setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + case GameListItemType::CustomDir: { + const UISettings::GameDir* game_dir = + child->data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); + const QString icon_name = QFileInfo::exists(game_dir->path) + ? QStringLiteral("folder") + : QStringLiteral("bad_folder"); + child->setData( + QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( + icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; + } + case GameListItemType::AddDir: + child->setData( + QIcon::fromTheme(QStringLiteral("plus")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + break; } } } @@ -214,7 +300,6 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); tree_view->setSortingEnabled(true); tree_view->setEditTriggers(QHeaderView::NoEditTriggers); - tree_view->setUniformRowHeights(true); tree_view->setContextMenuPolicy(Qt::CustomContextMenu); tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); @@ -230,12 +315,16 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide item_model->setHeaderData(COLUMN_FILE_TYPE - 1, Qt::Horizontal, tr("File type")); item_model->setHeaderData(COLUMN_SIZE - 1, Qt::Horizontal, tr("Size")); } + item_model->setSortRole(GameListItemPath::TitleRole); + connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::onUpdateThemedIcons); connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); + connect(tree_view, &QTreeView::expanded, this, &GameList::onItemExpanded); + connect(tree_view, &QTreeView::collapsed, this, &GameList::onItemExpanded); - // We must register all custom types with the Qt Automoc system so that we are able to use it - // with signals/slots. In this case, QList falls under the umbrells of custom types. + // We must register all custom types with the Qt Automoc system so that we are able to use + // it with signals/slots. In this case, QList falls under the umbrells of custom types. qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); layout->setContentsMargins(0, 0, 0, 0); @@ -263,38 +352,68 @@ void GameList::clearFilter() { search_field->clear(); } -void GameList::AddEntry(const QList<QStandardItem*>& entry_items) { +void GameList::AddDirEntry(GameListDir* entry_items) { item_model->invisibleRootItem()->appendRow(entry_items); + tree_view->setExpanded( + entry_items->index(), + entry_items->data(GameListDir::GameDirRole).value<UISettings::GameDir*>()->expanded); } -void GameList::ValidateEntry(const QModelIndex& item) { - // We don't care about the individual QStandardItem that was selected, but its row. - const int row = item_model->itemFromIndex(item)->row(); - const QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); - const QString file_path = child_file->data(GameListItemPath::FullPathRole).toString(); - - if (file_path.isEmpty()) - return; - - if (!QFileInfo::exists(file_path)) - return; +void GameList::AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent) { + parent->appendRow(entry_items); +} - const QFileInfo file_info{file_path}; - if (file_info.isDir()) { - const QDir dir{file_path}; - const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); - if (matching_main.size() == 1) { - emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); +void GameList::ValidateEntry(const QModelIndex& item) { + const auto selected = item.sibling(item.row(), 0); + + switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { + case GameListItemType::Game: { + const QString file_path = selected.data(GameListItemPath::FullPathRole).toString(); + if (file_path.isEmpty()) + return; + const QFileInfo file_info(file_path); + if (!file_info.exists()) + return; + + if (file_info.isDir()) { + const QDir dir{file_path}; + const QStringList matching_main = dir.entryList({QStringLiteral("main")}, QDir::Files); + if (matching_main.size() == 1) { + emit GameChosen(dir.path() + QDir::separator() + matching_main[0]); + } + return; } - return; + + // Users usually want to run a different game after closing one + search_field->clear(); + emit GameChosen(file_path); + break; } + case GameListItemType::AddDir: + emit AddDirectory(); + break; + } +} - // Users usually want to run a diffrent game after closing one - search_field->clear(); - emit GameChosen(file_path); +bool GameList::isEmpty() const { + for (int i = 0; i < item_model->rowCount(); i++) { + const QStandardItem* child = item_model->invisibleRootItem()->child(i); + const auto type = static_cast<GameListItemType>(child->type()); + if (!child->hasChildren() && + (type == GameListItemType::SdmcDir || type == GameListItemType::UserNandDir || + type == GameListItemType::SysNandDir)) { + item_model->invisibleRootItem()->removeRow(child->row()); + i--; + }; + } + return !item_model->invisibleRootItem()->hasChildren(); } void GameList::DonePopulating(QStringList watch_list) { + emit ShowList(!isEmpty()); + + item_model->invisibleRootItem()->appendRow(new GameListAddDir()); + // Clear out the old directories to watch for changes and add the new ones auto watch_dirs = watcher->directories(); if (!watch_dirs.isEmpty()) { @@ -311,9 +430,13 @@ void GameList::DonePopulating(QStringList watch_list) { QCoreApplication::processEvents(); } tree_view->setEnabled(true); - int rowCount = tree_view->model()->rowCount(); - search_field->setFilterResult(rowCount, rowCount); - if (rowCount > 0) { + const int folder_count = tree_view->model()->rowCount(); + int children_total = 0; + for (int i = 0; i < folder_count; ++i) { + children_total += item_model->item(i, 0)->rowCount(); + } + search_field->setFilterResult(children_total, children_total); + if (children_total > 0) { search_field->setFocus(); } } @@ -323,12 +446,27 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { if (!item.isValid()) return; - int row = item_model->itemFromIndex(item)->row(); - QStandardItem* child_file = item_model->invisibleRootItem()->child(row, COLUMN_NAME); - u64 program_id = child_file->data(GameListItemPath::ProgramIdRole).toULongLong(); - std::string path = child_file->data(GameListItemPath::FullPathRole).toString().toStdString(); - + const auto selected = item.sibling(item.row(), 0); QMenu context_menu; + switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { + case GameListItemType::Game: + AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(), + selected.data(GameListItemPath::FullPathRole).toString().toStdString()); + break; + case GameListItemType::CustomDir: + AddPermDirPopup(context_menu, selected); + AddCustomDirPopup(context_menu, selected); + break; + case GameListItemType::SdmcDir: + case GameListItemType::UserNandDir: + case GameListItemType::SysNandDir: + AddPermDirPopup(context_menu, selected); + break; + } + context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); +} + +void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, std::string path) { QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); QAction* open_lfs_location = context_menu.addAction(tr("Open Mod Data Location")); QAction* open_transferable_shader_cache = @@ -344,19 +482,86 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); - connect(open_save_location, &QAction::triggered, - [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); }); - connect(open_lfs_location, &QAction::triggered, - [&]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); }); + connect(open_save_location, &QAction::triggered, [this, program_id]() { + emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData); + }); + connect(open_lfs_location, &QAction::triggered, [this, program_id]() { + emit OpenFolderRequested(program_id, GameListOpenTarget::ModData); + }); connect(open_transferable_shader_cache, &QAction::triggered, - [&]() { emit OpenTransferableShaderCacheRequested(program_id); }); - connect(dump_romfs, &QAction::triggered, [&]() { emit DumpRomFSRequested(program_id, path); }); - connect(copy_tid, &QAction::triggered, [&]() { emit CopyTIDRequested(program_id); }); - connect(navigate_to_gamedb_entry, &QAction::triggered, - [&]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); - connect(properties, &QAction::triggered, [&]() { emit OpenPerGameGeneralRequested(path); }); + [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); + connect(dump_romfs, &QAction::triggered, + [this, program_id, path]() { emit DumpRomFSRequested(program_id, path); }); + connect(copy_tid, &QAction::triggered, + [this, program_id]() { emit CopyTIDRequested(program_id); }); + connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { + emit NavigateToGamedbEntryRequested(program_id, compatibility_list); + }); + connect(properties, &QAction::triggered, + [this, path]() { emit OpenPerGameGeneralRequested(path); }); +}; + +void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { + UISettings::GameDir& game_dir = + *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); + + QAction* deep_scan = context_menu.addAction(tr("Scan Subfolders")); + QAction* delete_dir = context_menu.addAction(tr("Remove Game Directory")); + + deep_scan->setCheckable(true); + deep_scan->setChecked(game_dir.deep_scan); + + connect(deep_scan, &QAction::triggered, [this, &game_dir] { + game_dir.deep_scan = !game_dir.deep_scan; + PopulateAsync(UISettings::values.game_dirs); + }); + connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { + UISettings::values.game_dirs.removeOne(game_dir); + item_model->invisibleRootItem()->removeRow(selected.row()); + }); +} - context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); +void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { + UISettings::GameDir& game_dir = + *selected.data(GameListDir::GameDirRole).value<UISettings::GameDir*>(); + + QAction* move_up = context_menu.addAction(tr(u8"\U000025b2 Move Up")); + QAction* move_down = context_menu.addAction(tr(u8"\U000025bc Move Down ")); + QAction* open_directory_location = context_menu.addAction(tr("Open Directory Location")); + + const int row = selected.row(); + + move_up->setEnabled(row > 0); + move_down->setEnabled(row < item_model->rowCount() - 2); + + connect(move_up, &QAction::triggered, [this, selected, row, &game_dir] { + // find the indices of the items in settings and swap them + std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], + UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( + *selected.sibling(row - 1, 0) + .data(GameListDir::GameDirRole) + .value<UISettings::GameDir*>())]); + // move the treeview items + QList<QStandardItem*> item = item_model->takeRow(row); + item_model->invisibleRootItem()->insertRow(row - 1, item); + tree_view->setExpanded(selected, game_dir.expanded); + }); + + connect(move_down, &QAction::triggered, [this, selected, row, &game_dir] { + // find the indices of the items in settings and swap them + std::swap(UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf(game_dir)], + UISettings::values.game_dirs[UISettings::values.game_dirs.indexOf( + *selected.sibling(row + 1, 0) + .data(GameListDir::GameDirRole) + .value<UISettings::GameDir*>())]); + // move the treeview items + const QList<QStandardItem*> item = item_model->takeRow(row); + item_model->invisibleRootItem()->insertRow(row + 1, item); + tree_view->setExpanded(selected, game_dir.expanded); + }); + + connect(open_directory_location, &QAction::triggered, + [this, game_dir] { emit OpenDirectory(game_dir.path); }); } void GameList::LoadCompatibilityList() { @@ -403,14 +608,7 @@ void GameList::LoadCompatibilityList() { } } -void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { - const QFileInfo dir_info{dir_path}; - if (!dir_info.exists() || !dir_info.isDir()) { - LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString()); - search_field->setFilterResult(0, 0); - return; - } - +void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { tree_view->setEnabled(false); // Update the columns in case UISettings has changed @@ -433,17 +631,19 @@ void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { // Delete any rows that might already exist if we're repopulating item_model->removeRows(0, item_model->rowCount()); + search_field->clear(); emit ShouldCancelWorker(); - GameListWorker* worker = - new GameListWorker(vfs, provider, dir_path, deep_scan, compatibility_list); + GameListWorker* worker = new GameListWorker(vfs, provider, game_dirs, compatibility_list); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); + connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, + Qt::QueuedConnection); connect(worker, &GameListWorker::Finished, this, &GameList::DonePopulating, Qt::QueuedConnection); - // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to cancel - // without delay. + // Use DirectConnection here because worker->Cancel() is thread-safe and we want it to + // cancel without delay. connect(this, &GameList::ShouldCancelWorker, worker, &GameListWorker::Cancel, Qt::DirectConnection); @@ -471,10 +671,40 @@ const QStringList GameList::supported_file_extensions = { QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")}; void GameList::RefreshGameDirectory() { - if (!UISettings::values.game_directory_path.isEmpty() && current_worker != nullptr) { + if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list."); - search_field->clear(); - PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + PopulateAsync(UISettings::values.game_dirs); } } + +GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent} { + connect(parent, &GMainWindow::UpdateThemedIcons, this, + &GameListPlaceholder::onUpdateThemedIcons); + + layout = new QVBoxLayout; + image = new QLabel; + text = new QLabel; + layout->setAlignment(Qt::AlignCenter); + image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); + + text->setText(tr("Double-click to add a new folder to the game list")); + QFont font = text->font(); + font.setPointSize(20); + text->setFont(font); + text->setAlignment(Qt::AlignHCenter); + image->setAlignment(Qt::AlignHCenter); + + layout->addWidget(image); + layout->addWidget(text); + setLayout(layout); +} + +GameListPlaceholder::~GameListPlaceholder() = default; + +void GameListPlaceholder::onUpdateThemedIcons() { + image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200)); +} + +void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { + emit GameListPlaceholder::AddDirectory(); +} diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index f8f8bd6c5..878d94413 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -8,6 +8,7 @@ #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> +#include <QList> #include <QModelIndex> #include <QSettings> #include <QStandardItem> @@ -16,13 +17,16 @@ #include <QToolButton> #include <QTreeView> #include <QVBoxLayout> +#include <QVector> #include <QWidget> #include "common/common_types.h" +#include "uisettings.h" #include "yuzu/compatibility_list.h" class GameListWorker; class GameListSearchField; +class GameListDir; class GMainWindow; namespace FileSys { @@ -52,12 +56,14 @@ public: FileSys::ManualContentProvider* provider, GMainWindow* parent = nullptr); ~GameList() override; + QString getLastFilterResultItem() const; void clearFilter(); void setFilterFocus(); void setFilterVisible(bool visibility); + bool isEmpty() const; void LoadCompatibilityList(); - void PopulateAsync(const QString& dir_path, bool deep_scan); + void PopulateAsync(QVector<UISettings::GameDir>& game_dirs); void SaveInterfaceLayout(); void LoadInterfaceLayout(); @@ -74,19 +80,29 @@ signals: void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const std::string& file); + void OpenDirectory(const QString& directory); + void AddDirectory(); + void ShowList(bool show); private slots: + void onItemExpanded(const QModelIndex& item); void onTextChanged(const QString& new_text); void onFilterCloseClicked(); + void onUpdateThemedIcons(); private: - void AddEntry(const QList<QStandardItem*>& entry_items); + void AddDirEntry(GameListDir* entry_items); + void AddEntry(const QList<QStandardItem*>& entry_items, GameListDir* parent); void ValidateEntry(const QModelIndex& item); void DonePopulating(QStringList watch_list); - void PopupContextMenu(const QPoint& menu_location); void RefreshGameDirectory(); + void PopupContextMenu(const QPoint& menu_location); + void AddGamePopup(QMenu& context_menu, u64 program_id, std::string path); + void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); + void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); + std::shared_ptr<FileSys::VfsFilesystem> vfs; FileSys::ManualContentProvider* provider; GameListSearchField* search_field; @@ -102,3 +118,24 @@ private: }; Q_DECLARE_METATYPE(GameListOpenTarget); + +class GameListPlaceholder : public QWidget { + Q_OBJECT +public: + explicit GameListPlaceholder(GMainWindow* parent = nullptr); + ~GameListPlaceholder(); + +signals: + void AddDirectory(); + +private slots: + void onUpdateThemedIcons(); + +protected: + void mouseDoubleClickEvent(QMouseEvent* event) override; + +private: + QVBoxLayout* layout = nullptr; + QLabel* image = nullptr; + QLabel* text = nullptr; +}; diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index ece534dd6..a8d888fee 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -10,6 +10,7 @@ #include <utility> #include <QCoreApplication> +#include <QFileInfo> #include <QImage> #include <QObject> #include <QStandardItem> @@ -22,6 +23,17 @@ #include "yuzu/uisettings.h" #include "yuzu/util/util.h" +enum class GameListItemType { + Game = QStandardItem::UserType + 1, + CustomDir = QStandardItem::UserType + 2, + SdmcDir = QStandardItem::UserType + 3, + UserNandDir = QStandardItem::UserType + 4, + SysNandDir = QStandardItem::UserType + 5, + AddDir = QStandardItem::UserType + 6 +}; + +Q_DECLARE_METATYPE(GameListItemType); + /** * Gets the default icon (for games without valid title metadata) * @param size The desired width and height of the default icon. @@ -36,8 +48,13 @@ static QPixmap GetDefaultIcon(u32 size) { class GameListItem : public QStandardItem { public: + // used to access type from item index + static const int TypeRole = Qt::UserRole + 1; + static const int SortRole = Qt::UserRole + 2; GameListItem() = default; - explicit GameListItem(const QString& string) : QStandardItem(string) {} + GameListItem(const QString& string) : QStandardItem(string) { + setData(string, SortRole); + } }; /** @@ -48,14 +65,15 @@ public: */ class GameListItemPath : public GameListItem { public: - static const int FullPathRole = Qt::UserRole + 1; - static const int TitleRole = Qt::UserRole + 2; - static const int ProgramIdRole = Qt::UserRole + 3; - static const int FileTypeRole = Qt::UserRole + 4; + static const int TitleRole = SortRole; + static const int FullPathRole = SortRole + 1; + static const int ProgramIdRole = SortRole + 2; + static const int FileTypeRole = SortRole + 3; GameListItemPath() = default; GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, const QString& game_name, const QString& game_type, u64 program_id) { + setData(type(), TypeRole); setData(game_path, FullPathRole); setData(game_name, TitleRole); setData(qulonglong(program_id), ProgramIdRole); @@ -72,6 +90,10 @@ public: setData(picture, Qt::DecorationRole); } + int type() const override { + return static_cast<int>(GameListItemType::Game); + } + QVariant data(int role) const override { if (role == Qt::DisplayRole) { std::string filename; @@ -103,9 +125,11 @@ public: class GameListItemCompat : public GameListItem { Q_DECLARE_TR_FUNCTIONS(GameListItemCompat) public: - static const int CompatNumberRole = Qt::UserRole + 1; + static const int CompatNumberRole = SortRole; GameListItemCompat() = default; explicit GameListItemCompat(const QString& compatibility) { + setData(type(), TypeRole); + struct CompatStatus { QString color; const char* text; @@ -135,6 +159,10 @@ public: setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole); } + int type() const override { + return static_cast<int>(GameListItemType::Game); + } + bool operator<(const QStandardItem& other) const override { return data(CompatNumberRole) < other.data(CompatNumberRole); } @@ -146,12 +174,12 @@ public: * human-readable string representation will be displayed to the user. */ class GameListItemSize : public GameListItem { - public: - static const int SizeRole = Qt::UserRole + 1; + static const int SizeRole = SortRole; GameListItemSize() = default; explicit GameListItemSize(const qulonglong size_bytes) { + setData(type(), TypeRole); setData(size_bytes, SizeRole); } @@ -167,6 +195,10 @@ public: } } + int type() const override { + return static_cast<int>(GameListItemType::Game); + } + /** * This operator is, in practice, only used by the TreeView sorting systems. * Override it so that it will correctly sort by numerical value instead of by string @@ -177,6 +209,82 @@ public: } }; +class GameListDir : public GameListItem { +public: + static const int GameDirRole = Qt::UserRole + 2; + + explicit GameListDir(UISettings::GameDir& directory, + GameListItemType dir_type = GameListItemType::CustomDir) + : dir_type{dir_type} { + setData(type(), TypeRole); + + UISettings::GameDir* game_dir = &directory; + setData(QVariant::fromValue(game_dir), GameDirRole); + + const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); + switch (dir_type) { + case GameListItemType::SdmcDir: + setData( + QIcon::fromTheme(QStringLiteral("sd_card")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole); + break; + case GameListItemType::UserNandDir: + setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole); + break; + case GameListItemType::SysNandDir: + setData( + QIcon::fromTheme(QStringLiteral("chip")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(QObject::tr("System Titles"), Qt::DisplayRole); + break; + case GameListItemType::CustomDir: + const QString icon_name = QFileInfo::exists(game_dir->path) + ? QStringLiteral("folder") + : QStringLiteral("bad_folder"); + setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled( + icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(game_dir->path, Qt::DisplayRole); + break; + }; + }; + + int type() const override { + return static_cast<int>(dir_type); + } + +private: + GameListItemType dir_type; +}; + +class GameListAddDir : public GameListItem { +public: + explicit GameListAddDir() { + setData(type(), TypeRole); + + const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64); + setData(QIcon::fromTheme(QStringLiteral("plus")) + .pixmap(icon_size) + .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), + Qt::DecorationRole); + setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole); + } + + int type() const override { + return static_cast<int>(GameListItemType::AddDir); + } +}; + class GameList; class QHBoxLayout; class QTreeView; @@ -208,6 +316,9 @@ private: // EventFilter in order to process systemkeys while editing the searchfield bool eventFilter(QObject* obj, QEvent* event) override; }; + int visible; + int total; + QHBoxLayout* layout_filter = nullptr; QTreeView* tree_view = nullptr; QLabel* label_filter = nullptr; diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 77f358630..fd21a9761 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -223,21 +223,37 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri } // Anonymous namespace GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs, - FileSys::ManualContentProvider* provider, QString dir_path, - bool deep_scan, const CompatibilityList& compatibility_list) - : vfs(std::move(vfs)), provider(provider), dir_path(std::move(dir_path)), deep_scan(deep_scan), + FileSys::ManualContentProvider* provider, + QVector<UISettings::GameDir>& game_dirs, + const CompatibilityList& compatibility_list) + : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs), compatibility_list(compatibility_list) {} GameListWorker::~GameListWorker() = default; -void GameListWorker::AddTitlesToGameList() { - const auto& cache = dynamic_cast<FileSys::ContentProviderUnion&>( - Core::System::GetInstance().GetContentProvider()); - const auto installed_games = cache.ListEntriesFilterOrigin( - std::nullopt, FileSys::TitleType::Application, FileSys::ContentRecordType::Program); +void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { + using namespace FileSys; + + const auto& cache = + dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider()); + + std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games; + installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application, + ContentRecordType::Program); + + if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) { + installed_games = cache.ListEntriesFilterOrigin( + ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program); + } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) { + installed_games = cache.ListEntriesFilterOrigin( + ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program); + } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) { + installed_games = cache.ListEntriesFilterOrigin( + ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program); + } for (const auto& [slot, game] : installed_games) { - if (slot == FileSys::ContentProviderUnionSlot::FrontendManual) + if (slot == ContentProviderUnionSlot::FrontendManual) continue; const auto file = cache.GetEntryUnparsed(game.title_id, game.type); @@ -250,21 +266,22 @@ void GameListWorker::AddTitlesToGameList() { u64 program_id = 0; loader->ReadProgramId(program_id); - const FileSys::PatchManager patch{program_id}; - const auto control = cache.GetEntry(game.title_id, FileSys::ContentRecordType::Control); + const PatchManager patch{program_id}; + const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control); if (control != nullptr) GetMetadataFromControlNCA(patch, *control, icon, name); emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id, - compatibility_list, patch)); + compatibility_list, patch), + parent_dir); } } void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, - unsigned int recursion) { - const auto callback = [this, target, recursion](u64* num_entries_out, - const std::string& directory, - const std::string& virtual_name) -> bool { + unsigned int recursion, GameListDir* parent_dir) { + const auto callback = [this, target, recursion, + parent_dir](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { if (stop_processing) { // Breaks the callback loop. return false; @@ -317,11 +334,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const FileSys::PatchManager patch{program_id}; emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id, - compatibility_list, patch)); + compatibility_list, patch), + parent_dir); } } else if (is_dir && recursion > 0) { watch_list.append(QString::fromStdString(physical_name)); - ScanFileSystem(target, physical_name, recursion - 1); + ScanFileSystem(target, physical_name, recursion - 1, parent_dir); } return true; @@ -332,12 +350,32 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa void GameListWorker::run() { stop_processing = false; - watch_list.append(dir_path); - provider->ClearAllEntries(); - ScanFileSystem(ScanTarget::FillManualContentProvider, dir_path.toStdString(), - deep_scan ? 256 : 0); - AddTitlesToGameList(); - ScanFileSystem(ScanTarget::PopulateGameList, dir_path.toStdString(), deep_scan ? 256 : 0); + + for (UISettings::GameDir& game_dir : game_dirs) { + if (game_dir.path == QStringLiteral("SDMC")) { + auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir); + emit DirEntryReady({game_list_dir}); + AddTitlesToGameList(game_list_dir); + } else if (game_dir.path == QStringLiteral("UserNAND")) { + auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir); + emit DirEntryReady({game_list_dir}); + AddTitlesToGameList(game_list_dir); + } else if (game_dir.path == QStringLiteral("SysNAND")) { + auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir); + emit DirEntryReady({game_list_dir}); + AddTitlesToGameList(game_list_dir); + } else { + watch_list.append(game_dir.path); + auto* const game_list_dir = new GameListDir(game_dir); + emit DirEntryReady({game_list_dir}); + provider->ClearAllEntries(); + ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2, + game_list_dir); + ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(), + game_dir.deep_scan ? 256 : 0, game_list_dir); + } + }; + emit Finished(watch_list); } diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 7c3074af9..6e52fca89 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -14,6 +14,7 @@ #include <QObject> #include <QRunnable> #include <QString> +#include <QVector> #include "common/common_types.h" #include "yuzu/compatibility_list.h" @@ -33,9 +34,10 @@ class GameListWorker : public QObject, public QRunnable { Q_OBJECT public: - GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, - FileSys::ManualContentProvider* provider, QString dir_path, bool deep_scan, - const CompatibilityList& compatibility_list); + explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs, + FileSys::ManualContentProvider* provider, + QVector<UISettings::GameDir>& game_dirs, + const CompatibilityList& compatibility_list); ~GameListWorker() override; /// Starts the processing of directory tree information. @@ -48,31 +50,33 @@ signals: /** * The `EntryReady` signal is emitted once an entry has been prepared and is ready * to be added to the game list. - * @param entry_items a list with `QStandardItem`s that make up the columns of the new entry. + * @param entry_items a list with `QStandardItem`s that make up the columns of the new + * entry. */ - void EntryReady(QList<QStandardItem*> entry_items); + void DirEntryReady(GameListDir* entry_items); + void EntryReady(QList<QStandardItem*> entry_items, GameListDir* parent_dir); /** - * After the worker has traversed the game directory looking for entries, this signal is emitted - * with a list of folders that should be watched for changes as well. + * After the worker has traversed the game directory looking for entries, this signal is + * emitted with a list of folders that should be watched for changes as well. */ void Finished(QStringList watch_list); private: - void AddTitlesToGameList(); + void AddTitlesToGameList(GameListDir* parent_dir); enum class ScanTarget { FillManualContentProvider, PopulateGameList, }; - void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion = 0); + void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion, + GameListDir* parent_dir); std::shared_ptr<FileSys::VfsFilesystem> vfs; FileSys::ManualContentProvider* provider; QStringList watch_list; - QString dir_path; - bool deep_scan; const CompatibilityList& compatibility_list; + QVector<UISettings::GameDir>& game_dirs; std::atomic_bool stop_processing; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index ac57229d5..6d249cb3e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -216,8 +216,7 @@ GMainWindow::GMainWindow() OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); game_list->LoadCompatibilityList(); - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); // Show one-time "callout" messages to the user ShowTelemetryCallout(); @@ -427,6 +426,10 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(vfs, provider.get(), this); ui.horizontalLayout->addWidget(game_list); + game_list_placeholder = new GameListPlaceholder(this); + ui.horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + loading_screen = new LoadingScreen(this); loading_screen->hide(); ui.horizontalLayout->addWidget(loading_screen); @@ -660,6 +663,7 @@ void GMainWindow::RestoreUIState() { void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); connect(game_list, &GameList::OpenTransferableShaderCacheRequested, this, &GMainWindow::OnTransferableShaderCacheOpenFile); @@ -667,6 +671,11 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); @@ -684,8 +693,6 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); connect(ui.action_Install_File_NAND, &QAction::triggered, this, &GMainWindow::OnMenuInstallToNAND); - connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, - &GMainWindow::OnMenuSelectGameListRoot); connect(ui.action_Select_NAND_Directory, &QAction::triggered, this, [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::NAND); }); connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, @@ -950,6 +957,7 @@ void GMainWindow::BootGame(const QString& filename) { // Update the GUI if (ui.action_Single_Window_Mode->isChecked()) { game_list->hide(); + game_list_placeholder->hide(); } status_bar_update_timer.start(2000); @@ -1007,7 +1015,10 @@ void GMainWindow::ShutdownGame() { render_window->hide(); loading_screen->hide(); loading_screen->Clear(); - game_list->show(); + if (game_list->isEmpty()) + game_list_placeholder->show(); + else + game_list->show(); game_list->setFilterFocus(); UpdateWindowTitle(); @@ -1298,6 +1309,47 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); } +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("SDMC")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo/Contents/registered"); + } else if (directory == QStringLiteral("UserNAND")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "user/Contents/registered"); + } else if (directory == QStringLiteral("SysNAND")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "system/Contents/registered"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui.action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { u64 title_id{}; const auto v_file = Core::GetGameFileFromPath(vfs, file); @@ -1316,8 +1368,7 @@ void GMainWindow::OnGameListOpenPerGameProperties(const std::string& file) { const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); if (reload) { - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); } config->Save(); @@ -1407,8 +1458,7 @@ void GMainWindow::OnMenuInstallToNAND() { const auto success = [this]() { QMessageBox::information(this, tr("Successfully Installed"), tr("The file was successfully installed.")); - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + DIR_SEP + "game_list"); }; @@ -1533,14 +1583,6 @@ void GMainWindow::OnMenuInstallToNAND() { } } -void GMainWindow::OnMenuSelectGameListRoot() { - QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); - if (!dir_path.isEmpty()) { - UISettings::values.game_directory_path = dir_path; - game_list->PopulateAsync(dir_path, UISettings::values.game_directory_deepscan); - } -} - void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) { const auto res = QMessageBox::information( this, tr("Changing Emulated Directory"), @@ -1559,8 +1601,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) : FileUtil::UserPath::NANDDir, dir_path.toStdString()); Service::FileSystem::CreateFactories(*vfs); - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); } } @@ -1724,11 +1765,11 @@ void GMainWindow::OnConfigure() { if (UISettings::values.enable_discord_presence != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence); } + emit UpdateThemedIcons(); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); if (reload) { - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); } config->Save(); @@ -1992,8 +2033,7 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { Service::FileSystem::CreateFactories(*vfs); if (behavior == ReinitializeKeyBehavior::Warning) { - game_list->PopulateAsync(UISettings::values.game_directory_path, - UISettings::values.game_directory_deepscan); + game_list->PopulateAsync(UISettings::values.game_dirs); } } @@ -2158,7 +2198,6 @@ void GMainWindow::UpdateUITheme() { } QIcon::setThemeSearchPaths(theme_paths); - emit UpdateThemedIcons(); } void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 501608ddc..7d16188cb 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -30,6 +30,7 @@ class ProfilerWidget; class QLabel; class WaitTreeWidget; enum class GameListOpenTarget; +class GameListPlaceholder; namespace Core::Frontend { struct SoftwareKeyboardParameters; @@ -186,12 +187,13 @@ private slots: void OnGameListCopyTID(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); + void OnGameListOpenDirectory(const QString& directory); + void OnGameListAddDirectory(); + void OnGameListShowList(bool show); void OnGameListOpenPerGameProperties(const std::string& file); void OnMenuLoadFile(); void OnMenuLoadFolder(); void OnMenuInstallToNAND(); - /// Called whenever a user selects the "File->Select Game List Root" menu item - void OnMenuSelectGameListRoot(); /// Called whenever a user select the "File->Select -- Directory" where -- is NAND or SD Card void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); void OnMenuRecentFile(); @@ -223,6 +225,8 @@ private: GameList* game_list; LoadingScreen* loading_screen; + GameListPlaceholder* game_list_placeholder; + // Status bar elements QLabel* message_label = nullptr; QLabel* emu_speed_label = nullptr; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index ffcabb495..a1ce3c0c3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -62,7 +62,6 @@ <addaction name="action_Load_File"/> <addaction name="action_Load_Folder"/> <addaction name="separator"/> - <addaction name="action_Select_Game_List_Root"/> <addaction name="menu_recent_files"/> <addaction name="separator"/> <addaction name="action_Select_NAND_Directory"/> diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index a62cd6911..c57290006 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -8,8 +8,10 @@ #include <atomic> #include <vector> #include <QByteArray> +#include <QMetaType> #include <QString> #include <QStringList> +#include <QVector> #include "common/common_types.h" namespace UISettings { @@ -25,6 +27,18 @@ struct Shortcut { using Themes = std::array<std::pair<const char*, const char*>, 2>; extern const Themes themes; +struct GameDir { + QString path; + bool deep_scan; + bool expanded; + bool operator==(const GameDir& rhs) const { + return path == rhs.path; + }; + bool operator!=(const GameDir& rhs) const { + return !operator==(rhs); + }; +}; + struct Values { QByteArray geometry; QByteArray state; @@ -55,8 +69,9 @@ struct Values { QString roms_path; QString symbols_path; QString screenshot_path; - QString game_directory_path; - bool game_directory_deepscan; + QString game_dir_deprecated; + bool game_dir_deprecated_deepscan; + QVector<UISettings::GameDir> game_dirs; QStringList recent_files; QString theme; @@ -84,3 +99,5 @@ struct Values { extern Values values; } // namespace UISettings + +Q_DECLARE_METATYPE(UISettings::GameDir*); |