diff options
68 files changed, 904 insertions, 155 deletions
diff --git a/CMakeModules/GenerateSCMRev.cmake b/CMakeModules/GenerateSCMRev.cmake index 2cdb9189a..1d4aa979d 100644 --- a/CMakeModules/GenerateSCMRev.cmake +++ b/CMakeModules/GenerateSCMRev.cmake @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2019 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later -# Gets a UTC timstamp and sets the provided variable to it +# Gets a UTC timestamp and sets the provided variable to it function(get_timestamp _var) string(TIMESTAMP timestamp UTC) set(${_var} "${timestamp}" PARENT_SCOPE) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 9cf71680c..598f4e8bf 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -218,7 +218,6 @@ public: return; } m_window->OnSurfaceChanged(m_native_window); - m_system.Renderer().NotifySurfaceChanged(); } void ConfigureFilesystemProvider(const std::string& filepath) { diff --git a/src/audio_core/renderer/command/mix/depop_prepare.h b/src/audio_core/renderer/command/mix/depop_prepare.h index 161a94461..a0cc228f7 100644 --- a/src/audio_core/renderer/command/mix/depop_prepare.h +++ b/src/audio_core/renderer/command/mix/depop_prepare.h @@ -16,7 +16,7 @@ namespace AudioCore::Renderer { /** * AudioRenderer command for preparing depop. - * Adds the previusly output last samples to the depop buffer. + * Adds the previously output last samples to the depop buffer. */ struct DepopPrepareCommand : ICommand { /** diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 61bac9eba..441c8af97 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -18,10 +18,12 @@ #define LOAD_DIR "load" #define LOG_DIR "log" #define NAND_DIR "nand" +#define PLAY_TIME_DIR "play_time" #define SCREENSHOTS_DIR "screenshots" #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" #define TAS_DIR "tas" +#define ICONS_DIR "icons" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index dce219fcf..0abd81a45 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -124,10 +124,12 @@ public: GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR); + GenerateYuzuPath(YuzuPath::PlayTimeDir, yuzu_path / PLAY_TIME_DIR); GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); + GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); } private: diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index ba28964d0..63801c924 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -20,10 +20,12 @@ enum class YuzuPath { LoadDir, // Where cheat/mod files are stored. LogDir, // Where log files are stored. NANDDir, // Where the emulated NAND is stored. + PlayTimeDir, // Where play time data is stored. ScreenshotsDir, // Where yuzu screenshots are stored. SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. TASDir, // Where TAS scripts are stored. + IconsDir, // Where Icons for Windows shortcuts are stored. }; /** diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index feab1653d..4c7aba3f5 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -135,6 +135,11 @@ std::u16string UTF8ToUTF16(std::string_view input) { return convert.from_bytes(input.data(), input.data() + input.size()); } +std::u32string UTF8ToUTF32(std::string_view input) { + std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert; + return convert.from_bytes(input.data(), input.data() + input.size()); +} + #ifdef _WIN32 static std::wstring CPToUTF16(u32 code_page, std::string_view input) { const auto size = diff --git a/src/common/string_util.h b/src/common/string_util.h index c351f1a0c..9da1ca4e9 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -38,6 +38,7 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _ [[nodiscard]] std::string UTF16ToUTF8(std::u16string_view input); [[nodiscard]] std::u16string UTF8ToUTF16(std::string_view input); +[[nodiscard]] std::u32string UTF8ToUTF32(std::string_view input); #ifdef _WIN32 [[nodiscard]] std::string UTF16ToUTF8(std::wstring_view input); diff --git a/src/core/core.cpp b/src/core/core.cpp index 08cbb8978..0ab2e3b76 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1078,6 +1078,10 @@ void System::ApplySettings() { impl->RefreshTime(); if (IsPoweredOn()) { + if (Settings::values.custom_rtc_enabled) { + const s64 posix_time{Settings::values.custom_rtc.GetValue()}; + GetTimeManager().UpdateLocalSystemClockTime(posix_time); + } Renderer().RefreshBaseSettings(); } } diff --git a/src/core/debugger/gdbstub.cpp b/src/core/debugger/gdbstub.cpp index e55831f27..82964f0a1 100644 --- a/src/core/debugger/gdbstub.cpp +++ b/src/core/debugger/gdbstub.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <atomic> +#include <codecvt> +#include <locale> #include <numeric> #include <optional> #include <thread> @@ -12,6 +14,7 @@ #include "common/logging/log.h" #include "common/scope_exit.h" #include "common/settings.h" +#include "common/string_util.h" #include "core/arm/arm_interface.h" #include "core/core.h" #include "core/debugger/gdbstub.h" @@ -68,10 +71,16 @@ static std::string EscapeGDB(std::string_view data) { } static std::string EscapeXML(std::string_view data) { + std::u32string converted = U"[Encoding error]"; + try { + converted = Common::UTF8ToUTF32(data); + } catch (std::range_error&) { + } + std::string escaped; escaped.reserve(data.size()); - for (char c : data) { + for (char32_t c : converted) { switch (c) { case '&': escaped += "&"; @@ -86,7 +95,11 @@ static std::string EscapeXML(std::string_view data) { escaped += ">"; break; default: - escaped += c; + if (c > 0x7f) { + escaped += fmt::format("&#{};", static_cast<u32>(c)); + } else { + escaped += static_cast<char>(c); + } break; } } diff --git a/src/core/file_sys/program_metadata.cpp b/src/core/file_sys/program_metadata.cpp index f00479bd3..8e291ff67 100644 --- a/src/core/file_sys/program_metadata.cpp +++ b/src/core/file_sys/program_metadata.cpp @@ -5,6 +5,7 @@ #include <vector> #include "common/logging/log.h" +#include "common/scope_exit.h" #include "core/file_sys/program_metadata.h" #include "core/file_sys/vfs.h" #include "core/loader/loader.h" @@ -95,6 +96,13 @@ Loader::ResultStatus ProgramMetadata::Load(VirtualFile file) { return Loader::ResultStatus::Success; } +Loader::ResultStatus ProgramMetadata::Reload(VirtualFile file) { + const u64 original_program_id = aci_header.title_id; + SCOPE_EXIT({ aci_header.title_id = original_program_id; }); + + return this->Load(file); +} + /*static*/ ProgramMetadata ProgramMetadata::GetDefault() { // Allow use of cores 0~3 and thread priorities 1~63. constexpr u32 default_thread_info_capability = 0x30007F7; diff --git a/src/core/file_sys/program_metadata.h b/src/core/file_sys/program_metadata.h index 2e8960b07..9f8e74b13 100644 --- a/src/core/file_sys/program_metadata.h +++ b/src/core/file_sys/program_metadata.h @@ -56,6 +56,7 @@ public: static ProgramMetadata GetDefault(); Loader::ResultStatus Load(VirtualFile file); + Loader::ResultStatus Reload(VirtualFile file); /// Load from parameters instead of NPDM file, used for KIP void LoadManual(bool is_64_bit, ProgramAddressSpaceType address_space, s32 main_thread_prio, diff --git a/src/core/file_sys/vfs.h b/src/core/file_sys/vfs.h index a93e21f67..a7cd1cae3 100644 --- a/src/core/file_sys/vfs.h +++ b/src/core/file_sys/vfs.h @@ -175,7 +175,7 @@ public: return Write(reinterpret_cast<const u8*>(&data), sizeof(T), offset); } - // Renames the file to name. Returns whether or not the operation was successsful. + // Renames the file to name. Returns whether or not the operation was successful. virtual bool Rename(std::string_view name) = 0; // Returns the full path of this file as a string, recursively diff --git a/src/core/hle/kernel/k_memory_layout.cpp b/src/core/hle/kernel/k_memory_layout.cpp index af40092c0..bec714668 100644 --- a/src/core/hle/kernel/k_memory_layout.cpp +++ b/src/core/hle/kernel/k_memory_layout.cpp @@ -61,7 +61,7 @@ bool KMemoryRegionTree::Insert(u64 address, size_t size, u32 type_id, u32 new_at found->Reset(address, inserted_region_last, old_pair, new_attr, type_id); this->insert(*found); } else { - // If we can't re-use, adjust the old region. + // If we can't reuse, adjust the old region. found->Reset(old_address, address - 1, old_pair, old_attr, old_type); this->insert(*found); diff --git a/src/core/hle/kernel/k_page_table.cpp b/src/core/hle/kernel/k_page_table.cpp index 5b51edf30..1fbfbf31f 100644 --- a/src/core/hle/kernel/k_page_table.cpp +++ b/src/core/hle/kernel/k_page_table.cpp @@ -2949,6 +2949,23 @@ Result KPageTable::UnlockForIpcUserBuffer(KProcessAddress address, size_t size) KMemoryAttribute::Locked, nullptr)); } +Result KPageTable::LockForTransferMemory(KPageGroup* out, KProcessAddress address, size_t size, + KMemoryPermission perm) { + R_RETURN(this->LockMemoryAndOpen(out, nullptr, address, size, KMemoryState::FlagCanTransfer, + KMemoryState::FlagCanTransfer, KMemoryPermission::All, + KMemoryPermission::UserReadWrite, KMemoryAttribute::All, + KMemoryAttribute::None, perm, KMemoryAttribute::Locked)); +} + +Result KPageTable::UnlockForTransferMemory(KProcessAddress address, size_t size, + const KPageGroup& pg) { + R_RETURN(this->UnlockMemory(address, size, KMemoryState::FlagCanTransfer, + KMemoryState::FlagCanTransfer, KMemoryPermission::None, + KMemoryPermission::None, KMemoryAttribute::All, + KMemoryAttribute::Locked, KMemoryPermission::UserReadWrite, + KMemoryAttribute::Locked, std::addressof(pg))); +} + Result KPageTable::LockForCodeMemory(KPageGroup* out, KProcessAddress addr, size_t size) { R_RETURN(this->LockMemoryAndOpen( out, nullptr, addr, size, KMemoryState::FlagCanCodeMemory, KMemoryState::FlagCanCodeMemory, diff --git a/src/core/hle/kernel/k_page_table.h b/src/core/hle/kernel/k_page_table.h index b9e8c6042..7da675f27 100644 --- a/src/core/hle/kernel/k_page_table.h +++ b/src/core/hle/kernel/k_page_table.h @@ -104,6 +104,9 @@ public: Result CleanupForIpcServer(KProcessAddress address, size_t size, KMemoryState dst_state); Result CleanupForIpcClient(KProcessAddress address, size_t size, KMemoryState dst_state); + Result LockForTransferMemory(KPageGroup* out, KProcessAddress address, size_t size, + KMemoryPermission perm); + Result UnlockForTransferMemory(KProcessAddress address, size_t size, const KPageGroup& pg); Result LockForCodeMemory(KPageGroup* out, KProcessAddress addr, size_t size); Result UnlockForCodeMemory(KProcessAddress addr, size_t size, const KPageGroup& pg); Result MakeAndOpenPageGroup(KPageGroup* out, KProcessAddress address, size_t num_pages, diff --git a/src/core/hle/kernel/k_transfer_memory.cpp b/src/core/hle/kernel/k_transfer_memory.cpp index 13d34125c..0e2e11743 100644 --- a/src/core/hle/kernel/k_transfer_memory.cpp +++ b/src/core/hle/kernel/k_transfer_memory.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/scope_exit.h" #include "core/hle/kernel/k_process.h" #include "core/hle/kernel/k_resource_limit.h" #include "core/hle/kernel/k_transfer_memory.h" @@ -9,28 +10,50 @@ namespace Kernel { KTransferMemory::KTransferMemory(KernelCore& kernel) - : KAutoObjectWithSlabHeapAndContainer{kernel} {} + : KAutoObjectWithSlabHeapAndContainer{kernel}, m_lock{kernel} {} KTransferMemory::~KTransferMemory() = default; -Result KTransferMemory::Initialize(KProcessAddress address, std::size_t size, - Svc::MemoryPermission owner_perm) { +Result KTransferMemory::Initialize(KProcessAddress addr, std::size_t size, + Svc::MemoryPermission own_perm) { // Set members. m_owner = GetCurrentProcessPointer(m_kernel); - // TODO(bunnei): Lock for transfer memory + // Get the owner page table. + auto& page_table = m_owner->GetPageTable(); + + // Construct the page group, guarding to make sure our state is valid on exit. + m_page_group.emplace(m_kernel, page_table.GetBlockInfoManager()); + auto pg_guard = SCOPE_GUARD({ m_page_group.reset(); }); + + // Lock the memory. + R_TRY(page_table.LockForTransferMemory(std::addressof(*m_page_group), addr, size, + ConvertToKMemoryPermission(own_perm))); // Set remaining tracking members. m_owner->Open(); - m_owner_perm = owner_perm; - m_address = address; - m_size = size; + m_owner_perm = own_perm; + m_address = addr; m_is_initialized = true; + m_is_mapped = false; + // We succeeded. + pg_guard.Cancel(); R_SUCCEED(); } -void KTransferMemory::Finalize() {} +void KTransferMemory::Finalize() { + // Unlock. + if (!m_is_mapped) { + const size_t size = m_page_group->GetNumPages() * PageSize; + ASSERT(R_SUCCEEDED( + m_owner->GetPageTable().UnlockForTransferMemory(m_address, size, *m_page_group))); + } + + // Close the page group. + m_page_group->Close(); + m_page_group->Finalize(); +} void KTransferMemory::PostDestroy(uintptr_t arg) { KProcess* owner = reinterpret_cast<KProcess*>(arg); @@ -38,4 +61,54 @@ void KTransferMemory::PostDestroy(uintptr_t arg) { owner->Close(); } +Result KTransferMemory::Map(KProcessAddress address, size_t size, Svc::MemoryPermission map_perm) { + // Validate the size. + R_UNLESS(m_page_group->GetNumPages() == Common::DivideUp(size, PageSize), ResultInvalidSize); + + // Validate the permission. + R_UNLESS(m_owner_perm == map_perm, ResultInvalidState); + + // Lock ourselves. + KScopedLightLock lk(m_lock); + + // Ensure we're not already mapped. + R_UNLESS(!m_is_mapped, ResultInvalidState); + + // Map the memory. + const KMemoryState state = (m_owner_perm == Svc::MemoryPermission::None) + ? KMemoryState::Transfered + : KMemoryState::SharedTransfered; + R_TRY(GetCurrentProcess(m_kernel).GetPageTable().MapPageGroup( + address, *m_page_group, state, KMemoryPermission::UserReadWrite)); + + // Mark ourselves as mapped. + m_is_mapped = true; + + R_SUCCEED(); +} + +Result KTransferMemory::Unmap(KProcessAddress address, size_t size) { + // Validate the size. + R_UNLESS(m_page_group->GetNumPages() == Common::DivideUp(size, PageSize), ResultInvalidSize); + + // Lock ourselves. + KScopedLightLock lk(m_lock); + + // Unmap the memory. + const KMemoryState state = (m_owner_perm == Svc::MemoryPermission::None) + ? KMemoryState::Transfered + : KMemoryState::SharedTransfered; + R_TRY(GetCurrentProcess(m_kernel).GetPageTable().UnmapPageGroup(address, *m_page_group, state)); + + // Mark ourselves as unmapped. + ASSERT(m_is_mapped); + m_is_mapped = false; + + R_SUCCEED(); +} + +size_t KTransferMemory::GetSize() const { + return m_is_initialized ? m_page_group->GetNumPages() * PageSize : 0; +} + } // namespace Kernel diff --git a/src/core/hle/kernel/k_transfer_memory.h b/src/core/hle/kernel/k_transfer_memory.h index 54f97ccb4..8a0b08761 100644 --- a/src/core/hle/kernel/k_transfer_memory.h +++ b/src/core/hle/kernel/k_transfer_memory.h @@ -3,6 +3,9 @@ #pragma once +#include <optional> + +#include "core/hle/kernel/k_page_group.h" #include "core/hle/kernel/slab_helpers.h" #include "core/hle/kernel/svc_types.h" #include "core/hle/result.h" @@ -48,16 +51,19 @@ public: return m_address; } - size_t GetSize() const { - return m_is_initialized ? m_size : 0; - } + size_t GetSize() const; + + Result Map(KProcessAddress address, size_t size, Svc::MemoryPermission map_perm); + Result Unmap(KProcessAddress address, size_t size); private: + std::optional<KPageGroup> m_page_group{}; KProcess* m_owner{}; KProcessAddress m_address{}; + KLightLock m_lock; Svc::MemoryPermission m_owner_perm{}; - size_t m_size{}; bool m_is_initialized{}; + bool m_is_mapped{}; }; } // namespace Kernel diff --git a/src/core/hle/kernel/svc/svc_transfer_memory.cpp b/src/core/hle/kernel/svc/svc_transfer_memory.cpp index 7d94e7f09..1f97121b3 100644 --- a/src/core/hle/kernel/svc/svc_transfer_memory.cpp +++ b/src/core/hle/kernel/svc/svc_transfer_memory.cpp @@ -71,15 +71,59 @@ Result CreateTransferMemory(Core::System& system, Handle* out, u64 address, u64 } Result MapTransferMemory(Core::System& system, Handle trmem_handle, uint64_t address, uint64_t size, - MemoryPermission owner_perm) { - UNIMPLEMENTED(); - R_THROW(ResultNotImplemented); + MemoryPermission map_perm) { + // Validate the address/size. + R_UNLESS(Common::IsAligned(address, PageSize), ResultInvalidAddress); + R_UNLESS(Common::IsAligned(size, PageSize), ResultInvalidSize); + R_UNLESS(size > 0, ResultInvalidSize); + R_UNLESS((address < address + size), ResultInvalidCurrentMemory); + + // Validate the permission. + R_UNLESS(IsValidTransferMemoryPermission(map_perm), ResultInvalidState); + + // Get the transfer memory. + KScopedAutoObject trmem = GetCurrentProcess(system.Kernel()) + .GetHandleTable() + .GetObject<KTransferMemory>(trmem_handle); + R_UNLESS(trmem.IsNotNull(), ResultInvalidHandle); + + // Verify that the mapping is in range. + R_UNLESS(GetCurrentProcess(system.Kernel()) + .GetPageTable() + .CanContain(address, size, KMemoryState::Transfered), + ResultInvalidMemoryRegion); + + // Map the transfer memory. + R_TRY(trmem->Map(address, size, map_perm)); + + // We succeeded. + R_SUCCEED(); } Result UnmapTransferMemory(Core::System& system, Handle trmem_handle, uint64_t address, uint64_t size) { - UNIMPLEMENTED(); - R_THROW(ResultNotImplemented); + // Validate the address/size. + R_UNLESS(Common::IsAligned(address, PageSize), ResultInvalidAddress); + R_UNLESS(Common::IsAligned(size, PageSize), ResultInvalidSize); + R_UNLESS(size > 0, ResultInvalidSize); + R_UNLESS((address < address + size), ResultInvalidCurrentMemory); + + // Get the transfer memory. + KScopedAutoObject trmem = GetCurrentProcess(system.Kernel()) + .GetHandleTable() + .GetObject<KTransferMemory>(trmem_handle); + R_UNLESS(trmem.IsNotNull(), ResultInvalidHandle); + + // Verify that the mapping is in range. + R_UNLESS(GetCurrentProcess(system.Kernel()) + .GetPageTable() + .CanContain(address, size, KMemoryState::Transfered), + ResultInvalidMemoryRegion); + + // Unmap the transfer memory. + R_TRY(trmem->Unmap(address, size)); + + R_SUCCEED(); } Result MapTransferMemory64(Core::System& system, Handle trmem_handle, uint64_t address, diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 5a42dea48..5c36b71e5 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -118,7 +118,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect return {ResultStatus::ErrorMissingNPDM, {}}; } - const ResultStatus result2 = metadata.Load(npdm); + const ResultStatus result2 = metadata.Reload(npdm); if (result2 != ResultStatus::Success) { return {result2, {}}; } diff --git a/src/input_common/drivers/virtual_gamepad.h b/src/input_common/drivers/virtual_gamepad.h index dfbc45a28..3a40e3fd3 100644 --- a/src/input_common/drivers/virtual_gamepad.h +++ b/src/input_common/drivers/virtual_gamepad.h @@ -67,7 +67,7 @@ public: * @param player_index the player number that will take this action * @param delta_timestamp time passed since last reading * @param gyro_x,gyro_y,gyro_z the gyro sensor readings - * @param accel_x,accel_y,accel_z the acelerometer reading + * @param accel_x,accel_y,accel_z the accelerometer reading */ void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.h b/src/input_common/helpers/joycon_protocol/generic_functions.h index 90fcd17f6..b94567f82 100644 --- a/src/input_common/helpers/joycon_protocol/generic_functions.h +++ b/src/input_common/helpers/joycon_protocol/generic_functions.h @@ -55,7 +55,7 @@ public: /** * Configures the motion sensor with the specified parameters - * @param gsen gyroscope sensor sensitvity in degrees per second + * @param gsen gyroscope sensor sensitivity in degrees per second * @param gfrec gyroscope sensor frequency in hertz * @param asen accelerometer sensitivity in G force * @param afrec accelerometer frequency in hertz diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp index d508ee567..4e8ba4ae6 100644 --- a/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp +++ b/src/shader_recompiler/backend/glasm/emit_glasm_composite.cpp @@ -55,7 +55,7 @@ void CompositeInsert(EmitContext& ctx, IR::Inst& inst, Register composite, Objec "MOV.{} {}.{},{};", type, ret, composite, type, ret, swizzle, object); } else { - // The return value is alised so we can just insert the object, it doesn't matter if it's + // The return value is aliased so we can just insert the object, it doesn't matter if it's // aliased ctx.Add("MOV.{} {}.{},{};", type, ret, swizzle, object); } diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h index c9fab2d90..e46a8fa5c 100644 --- a/src/video_core/dma_pusher.h +++ b/src/video_core/dma_pusher.h @@ -161,7 +161,7 @@ private: u32 method_count; ///< Current method count u32 length_pending; ///< Large NI command length pending GPUVAddr dma_get; ///< Currently read segment - u64 dma_word_offset; ///< Current word ofset from address + u64 dma_word_offset; ///< Current word offset from address bool non_incrementing; ///< Current command's NI flag bool is_last_call; }; diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 6b912027f..8bb429578 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -19,6 +19,7 @@ set(SHADER_FILES block_linear_unswizzle_2d.comp block_linear_unswizzle_3d.comp convert_abgr8_to_d24s8.frag + convert_d32f_to_abgr8.frag convert_d24s8_to_abgr8.frag convert_depth_to_float.frag convert_float_to_depth.frag diff --git a/src/video_core/host_shaders/convert_d32f_to_abgr8.frag b/src/video_core/host_shaders/convert_d32f_to_abgr8.frag new file mode 100644 index 000000000..04cfef8b5 --- /dev/null +++ b/src/video_core/host_shaders/convert_d32f_to_abgr8.frag @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#version 450 + +layout(binding = 0) uniform sampler2D depth_tex; + +layout(location = 0) out vec4 color; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + float depth = textureLod(depth_tex, coord, 0).r; + color = vec4(depth, depth, depth, 1.0); +} diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 3e12a8813..78ea5208b 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -89,9 +89,6 @@ public: void RequestScreenshot(void* data, std::function<void(bool)> callback, const Layout::FramebufferLayout& layout); - /// This is called to notify the rendering backend of a surface change - virtual void NotifySurfaceChanged() {} - protected: Core::Frontend::EmuWindow& render_window; ///< Reference to the render window handle. std::unique_ptr<Core::Frontend::GraphicsContext> context; diff --git a/src/video_core/renderer_opengl/maxwell_to_gl.h b/src/video_core/renderer_opengl/maxwell_to_gl.h index c7dc7e0a1..5ea9e2378 100644 --- a/src/video_core/renderer_opengl/maxwell_to_gl.h +++ b/src/video_core/renderer_opengl/maxwell_to_gl.h @@ -116,6 +116,7 @@ constexpr std::array<FormatTuple, VideoCore::Surface::MaxPixelFormat> FORMAT_TAB {GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV}, // E5B9G9R9_FLOAT {GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT}, // D32_FLOAT {GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16_UNORM + {GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT_24_8}, // X8_D24_UNORM {GL_STENCIL_INDEX8, GL_STENCIL, GL_UNSIGNED_BYTE}, // S8_UINT {GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24_UNORM_S8_UINT {GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // S8_UINT_D24_UNORM diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index 1032c9d12..f01d2394e 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -9,6 +9,7 @@ #include "video_core/host_shaders/blit_color_float_frag_spv.h" #include "video_core/host_shaders/convert_abgr8_to_d24s8_frag_spv.h" #include "video_core/host_shaders/convert_d24s8_to_abgr8_frag_spv.h" +#include "video_core/host_shaders/convert_d32f_to_abgr8_frag_spv.h" #include "video_core/host_shaders/convert_depth_to_float_frag_spv.h" #include "video_core/host_shaders/convert_float_to_depth_frag_spv.h" #include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h" @@ -433,6 +434,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, convert_depth_to_float_frag(BuildShader(device, CONVERT_DEPTH_TO_FLOAT_FRAG_SPV)), convert_float_to_depth_frag(BuildShader(device, CONVERT_FLOAT_TO_DEPTH_FRAG_SPV)), convert_abgr8_to_d24s8_frag(BuildShader(device, CONVERT_ABGR8_TO_D24S8_FRAG_SPV)), + convert_d32f_to_abgr8_frag(BuildShader(device, CONVERT_D32F_TO_ABGR8_FRAG_SPV)), convert_d24s8_to_abgr8_frag(BuildShader(device, CONVERT_D24S8_TO_ABGR8_FRAG_SPV)), convert_s8d24_to_abgr8_frag(BuildShader(device, CONVERT_S8D24_TO_ABGR8_FRAG_SPV)), linear_sampler(device.GetLogical().CreateSampler(SAMPLER_CREATE_INFO<VK_FILTER_LINEAR>)), @@ -557,6 +559,13 @@ void BlitImageHelper::ConvertABGR8ToD24S8(const Framebuffer* dst_framebuffer, Convert(*convert_abgr8_to_d24s8_pipeline, dst_framebuffer, src_image_view); } +void BlitImageHelper::ConvertD32FToABGR8(const Framebuffer* dst_framebuffer, + ImageView& src_image_view) { + ConvertPipelineColorTargetEx(convert_d32f_to_abgr8_pipeline, dst_framebuffer->RenderPass(), + convert_d32f_to_abgr8_frag); + ConvertDepthStencil(*convert_d32f_to_abgr8_pipeline, dst_framebuffer, src_image_view); +} + void BlitImageHelper::ConvertD24S8ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view) { ConvertPipelineColorTargetEx(convert_d24s8_to_abgr8_pipeline, dst_framebuffer->RenderPass(), @@ -609,6 +618,8 @@ void BlitImageHelper::ClearDepthStencil(const Framebuffer* dst_framebuffer, bool const VkPipelineLayout layout = *clear_color_pipeline_layout; scheduler.RequestRenderpass(dst_framebuffer); scheduler.Record([pipeline, layout, clear_depth, dst_region](vk::CommandBuffer cmdbuf) { + constexpr std::array blend_constants{0.0f, 0.0f, 0.0f, 0.0f}; + cmdbuf.SetBlendConstants(blend_constants.data()); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); BindBlitState(cmdbuf, dst_region); cmdbuf.PushConstants(layout, VK_SHADER_STAGE_FRAGMENT_BIT, clear_depth); @@ -865,7 +876,7 @@ VkPipeline BlitImageHelper::FindOrEmplaceClearStencilPipeline( .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, .pNext = nullptr, .flags = 0, - .depthTestEnable = VK_FALSE, + .depthTestEnable = key.depth_clear, .depthWriteEnable = key.depth_clear, .depthCompareOp = VK_COMPARE_OP_ALWAYS, .depthBoundsTestEnable = VK_FALSE, diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h index dcfe217aa..a032c71fb 100644 --- a/src/video_core/renderer_vulkan/blit_image.h +++ b/src/video_core/renderer_vulkan/blit_image.h @@ -67,6 +67,8 @@ public: void ConvertABGR8ToD24S8(const Framebuffer* dst_framebuffer, const ImageView& src_image_view); + void ConvertD32FToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); + void ConvertD24S8ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); void ConvertS8D24ToABGR8(const Framebuffer* dst_framebuffer, ImageView& src_image_view); @@ -128,6 +130,7 @@ private: vk::ShaderModule convert_depth_to_float_frag; vk::ShaderModule convert_float_to_depth_frag; vk::ShaderModule convert_abgr8_to_d24s8_frag; + vk::ShaderModule convert_d32f_to_abgr8_frag; vk::ShaderModule convert_d24s8_to_abgr8_frag; vk::ShaderModule convert_s8d24_to_abgr8_frag; vk::Sampler linear_sampler; @@ -146,6 +149,7 @@ private: vk::Pipeline convert_d16_to_r16_pipeline; vk::Pipeline convert_r16_to_d16_pipeline; vk::Pipeline convert_abgr8_to_d24s8_pipeline; + vk::Pipeline convert_d32f_to_abgr8_pipeline; vk::Pipeline convert_d24s8_to_abgr8_pipeline; vk::Pipeline convert_s8d24_to_abgr8_pipeline; }; diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index 208e88533..a08f2f67f 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -214,8 +214,9 @@ struct FormatTuple { {VK_FORMAT_E5B9G9R9_UFLOAT_PACK32}, // E5B9G9R9_FLOAT // Depth formats - {VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT - {VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM + {VK_FORMAT_D32_SFLOAT, Attachable}, // D32_FLOAT + {VK_FORMAT_D16_UNORM, Attachable}, // D16_UNORM + {VK_FORMAT_X8_D24_UNORM_PACK32, Attachable}, // X8_D24_UNORM // Stencil formats {VK_FORMAT_S8_UINT, Attachable}, // S8_UINT diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 590bc1c64..14e257cf7 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -56,10 +56,6 @@ public: return device.GetDriverName(); } - void NotifySurfaceChanged() override { - present_manager.NotifySurfaceChanged(); - } - private: void Report() const; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 31928bb94..52fc142d1 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -96,6 +96,7 @@ std::size_t GetSizeInBytes(const Tegra::FramebufferConfig& framebuffer) { VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) { switch (framebuffer.pixel_format) { case Service::android::PixelFormat::Rgba8888: + case Service::android::PixelFormat::Rgbx8888: return VK_FORMAT_A8B8G8R8_UNORM_PACK32; case Service::android::PixelFormat::Rgb565: return VK_FORMAT_R5G6B5_UNORM_PACK16; diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index d681bd22a..2ef36583b 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -103,8 +103,7 @@ PresentManager::PresentManager(const vk::Instance& instance_, surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())}, use_present_thread{Settings::values.async_presentation.GetValue()}, - image_count{swapchain.GetImageCount()}, last_render_surface{ - render_window_.GetWindowInfo().render_surface} { + image_count{swapchain.GetImageCount()} { auto& dld = device.GetLogical(); cmdpool = dld.CreateCommandPool({ @@ -289,44 +288,36 @@ void PresentManager::PresentThread(std::stop_token token) { } } -void PresentManager::NotifySurfaceChanged() { -#ifdef ANDROID - std::scoped_lock lock{recreate_surface_mutex}; - recreate_surface_cv.notify_one(); -#endif +void PresentManager::RecreateSwapchain(Frame* frame) { + swapchain.Create(*surface, frame->width, frame->height, frame->is_srgb); + image_count = swapchain.GetImageCount(); } void PresentManager::CopyToSwapchain(Frame* frame) { - MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain); - - const auto recreate_swapchain = [&] { - swapchain.Create(*surface, frame->width, frame->height, frame->is_srgb); - image_count = swapchain.GetImageCount(); - }; - -#ifdef ANDROID - std::unique_lock lock{recreate_surface_mutex}; - - const auto needs_recreation = [&] { - if (last_render_surface != render_window.GetWindowInfo().render_surface) { - return true; - } - if (swapchain.NeedsRecreation(frame->is_srgb)) { - return true; + bool requires_recreation = false; + + while (true) { + try { + // Recreate surface and swapchain if needed. + if (requires_recreation) { + surface = CreateSurface(instance, render_window.GetWindowInfo()); + RecreateSwapchain(frame); + } + + // Draw to swapchain. + return CopyToSwapchainImpl(frame); + } catch (const vk::Exception& except) { + if (except.GetResult() != VK_ERROR_SURFACE_LOST_KHR) { + throw; + } + + requires_recreation = true; } - return false; - }; - - recreate_surface_cv.wait_for(lock, std::chrono::milliseconds(400), - [&]() { return !needs_recreation(); }); - - // If the frontend recreated the surface, recreate the renderer surface and swapchain. - if (last_render_surface != render_window.GetWindowInfo().render_surface) { - last_render_surface = render_window.GetWindowInfo().render_surface; - surface = CreateSurface(instance, render_window.GetWindowInfo()); - recreate_swapchain(); } -#endif +} + +void PresentManager::CopyToSwapchainImpl(Frame* frame) { + MICROPROFILE_SCOPE(Vulkan_CopyToSwapchain); // If the size or colorspace of the incoming frames has changed, recreate the swapchain // to account for that. @@ -334,11 +325,11 @@ void PresentManager::CopyToSwapchain(Frame* frame) { const bool size_changed = swapchain.GetWidth() != frame->width || swapchain.GetHeight() != frame->height; if (srgb_changed || size_changed) { - recreate_swapchain(); + RecreateSwapchain(frame); } while (swapchain.AcquireNextImage()) { - recreate_swapchain(); + RecreateSwapchain(frame); } const vk::CommandBuffer cmdbuf{frame->cmdbuf}; @@ -488,4 +479,4 @@ void PresentManager::CopyToSwapchain(Frame* frame) { swapchain.Present(render_semaphore); } -} // namespace Vulkan
\ No newline at end of file +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index 83e859416..a3d825fe6 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h @@ -54,14 +54,15 @@ public: /// Waits for the present thread to finish presenting all queued frames. void WaitPresent(); - /// This is called to notify the rendering backend of a surface change - void NotifySurfaceChanged(); - private: void PresentThread(std::stop_token token); void CopyToSwapchain(Frame* frame); + void CopyToSwapchainImpl(Frame* frame); + + void RecreateSwapchain(Frame* frame); + private: const vk::Instance& instance; Core::Frontend::EmuWindow& render_window; @@ -76,16 +77,13 @@ private: std::queue<Frame*> free_queue; std::condition_variable_any frame_cv; std::condition_variable free_cv; - std::condition_variable recreate_surface_cv; std::mutex swapchain_mutex; - std::mutex recreate_surface_mutex; std::mutex queue_mutex; std::mutex free_mutex; std::jthread present_thread; bool blit_supported; bool use_present_thread; std::size_t image_count{}; - void* last_render_surface{}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 1628d76d6..83f2b6045 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -422,7 +422,8 @@ void RasterizerVulkan::Clear(u32 layer_count) { return; } - if (use_stencil && regs.stencil_front_mask != 0xFF && regs.stencil_front_mask != 0) { + if (use_stencil && framebuffer->HasAspectStencilBit() && regs.stencil_front_mask != 0xFF && + regs.stencil_front_mask != 0) { Region2D dst_region = { Offset2D{.x = clear_rect.rect.offset.x, .y = clear_rect.rect.offset.y}, Offset2D{.x = clear_rect.rect.offset.x + static_cast<s32>(clear_rect.rect.extent.width), diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp index ce92f66ab..b278614e6 100644 --- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp @@ -24,25 +24,38 @@ using namespace Common::Literals; // Maximum potential alignment of a Vulkan buffer constexpr VkDeviceSize MAX_ALIGNMENT = 256; -// Maximum size to put elements in the stream buffer -constexpr VkDeviceSize MAX_STREAM_BUFFER_REQUEST_SIZE = 8_MiB; // Stream buffer size in bytes -constexpr VkDeviceSize STREAM_BUFFER_SIZE = 128_MiB; -constexpr VkDeviceSize REGION_SIZE = STREAM_BUFFER_SIZE / StagingBufferPool::NUM_SYNCS; +constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB; -size_t Region(size_t iterator) noexcept { - return iterator / REGION_SIZE; +size_t GetStreamBufferSize(const Device& device) { + VkDeviceSize size{0}; + if (device.HasDebuggingToolAttached()) { + ForEachDeviceLocalHostVisibleHeap(device, [&size](size_t index, VkMemoryHeap& heap) { + size = std::max(size, heap.size); + }); + // If rebar is not supported, cut the max heap size to 40%. This will allow 2 captures to be + // loaded at the same time in RenderDoc. If rebar is supported, this shouldn't be an issue + // as the heap will be much larger. + if (size <= 256_MiB) { + size = size * 40 / 100; + } + } else { + size = MAX_STREAM_BUFFER_SIZE; + } + return std::min(Common::AlignUp(size, MAX_ALIGNMENT), MAX_STREAM_BUFFER_SIZE); } } // Anonymous namespace StagingBufferPool::StagingBufferPool(const Device& device_, MemoryAllocator& memory_allocator_, Scheduler& scheduler_) - : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_} { + : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_}, + stream_buffer_size{GetStreamBufferSize(device)}, region_size{stream_buffer_size / + StagingBufferPool::NUM_SYNCS} { VkBufferCreateInfo stream_ci = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = nullptr, .flags = 0, - .size = STREAM_BUFFER_SIZE, + .size = stream_buffer_size, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, @@ -63,7 +76,7 @@ StagingBufferPool::StagingBufferPool(const Device& device_, MemoryAllocator& mem StagingBufferPool::~StagingBufferPool() = default; StagingBufferRef StagingBufferPool::Request(size_t size, MemoryUsage usage, bool deferred) { - if (!deferred && usage == MemoryUsage::Upload && size <= MAX_STREAM_BUFFER_REQUEST_SIZE) { + if (!deferred && usage == MemoryUsage::Upload && size <= region_size) { return GetStreamBuffer(size); } return GetStagingBuffer(size, usage, deferred); @@ -101,7 +114,7 @@ StagingBufferRef StagingBufferPool::GetStreamBuffer(size_t size) { used_iterator = iterator; free_iterator = std::max(free_iterator, iterator + size); - if (iterator + size >= STREAM_BUFFER_SIZE) { + if (iterator + size >= stream_buffer_size) { std::fill(sync_ticks.begin() + Region(used_iterator), sync_ticks.begin() + NUM_SYNCS, current_tick); used_iterator = 0; diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h index 5f69f08b1..d3deb9072 100644 --- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h +++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h @@ -90,6 +90,9 @@ private: void ReleaseCache(MemoryUsage usage); void ReleaseLevel(StagingBuffersCache& cache, size_t log2); + size_t Region(size_t iter) const noexcept { + return iter / region_size; + } const Device& device; MemoryAllocator& memory_allocator; @@ -97,6 +100,8 @@ private: vk::Buffer stream_buffer; std::span<u8> stream_pointer; + VkDeviceSize stream_buffer_size; + VkDeviceSize region_size; size_t iterator = 0; size_t used_iterator = 0; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 71fdec809..00ab47268 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -238,6 +238,7 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { return any_r ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; case PixelFormat::D16_UNORM: case PixelFormat::D32_FLOAT: + case PixelFormat::X8_D24_UNORM: return VK_IMAGE_ASPECT_DEPTH_BIT; case PixelFormat::S8_UINT: return VK_IMAGE_ASPECT_STENCIL_BIT; @@ -1200,6 +1201,9 @@ void TextureCacheRuntime::ConvertImage(Framebuffer* dst, ImageView& dst_view, Im if (src_view.format == PixelFormat::D24_UNORM_S8_UINT) { return blit_image_helper.ConvertS8D24ToABGR8(dst, src_view); } + if (src_view.format == PixelFormat::D32_FLOAT) { + return blit_image_helper.ConvertD32FToABGR8(dst, src_view); + } break; case PixelFormat::R32_FLOAT: if (src_view.format == PixelFormat::D32_FLOAT) { diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp index e16cd5e73..5b3c7aa5a 100644 --- a/src/video_core/surface.cpp +++ b/src/video_core/surface.cpp @@ -85,6 +85,8 @@ PixelFormat PixelFormatFromDepthFormat(Tegra::DepthFormat format) { return PixelFormat::S8_UINT; case Tegra::DepthFormat::Z32_FLOAT_X24S8_UINT: return PixelFormat::D32_FLOAT_S8_UINT; + case Tegra::DepthFormat::X8Z24_UNORM: + return PixelFormat::X8_D24_UNORM; default: UNIMPLEMENTED_MSG("Unimplemented format={}", format); return PixelFormat::S8_UINT_D24_UNORM; @@ -202,6 +204,7 @@ PixelFormat PixelFormatFromRenderTargetFormat(Tegra::RenderTargetFormat format) PixelFormat PixelFormatFromGPUPixelFormat(Service::android::PixelFormat format) { switch (format) { case Service::android::PixelFormat::Rgba8888: + case Service::android::PixelFormat::Rgbx8888: return PixelFormat::A8B8G8R8_UNORM; case Service::android::PixelFormat::Rgb565: return PixelFormat::R5G6B5_UNORM; diff --git a/src/video_core/surface.h b/src/video_core/surface.h index 9b9c4d9bc..a5e8e2f62 100644 --- a/src/video_core/surface.h +++ b/src/video_core/surface.h @@ -115,6 +115,7 @@ enum class PixelFormat { // Depth formats D32_FLOAT = MaxColorFormat, D16_UNORM, + X8_D24_UNORM, MaxDepthFormat, @@ -251,6 +252,7 @@ constexpr std::array<u8, MaxPixelFormat> BLOCK_WIDTH_TABLE = {{ 1, // E5B9G9R9_FLOAT 1, // D32_FLOAT 1, // D16_UNORM + 1, // X8_D24_UNORM 1, // S8_UINT 1, // D24_UNORM_S8_UINT 1, // S8_UINT_D24_UNORM @@ -360,6 +362,7 @@ constexpr std::array<u8, MaxPixelFormat> BLOCK_HEIGHT_TABLE = {{ 1, // E5B9G9R9_FLOAT 1, // D32_FLOAT 1, // D16_UNORM + 1, // X8_D24_UNORM 1, // S8_UINT 1, // D24_UNORM_S8_UINT 1, // S8_UINT_D24_UNORM @@ -469,6 +472,7 @@ constexpr std::array<u8, MaxPixelFormat> BITS_PER_BLOCK_TABLE = {{ 32, // E5B9G9R9_FLOAT 32, // D32_FLOAT 16, // D16_UNORM + 32, // X8_D24_UNORM 8, // S8_UINT 32, // D24_UNORM_S8_UINT 32, // S8_UINT_D24_UNORM diff --git a/src/video_core/texture_cache/format_lookup_table.cpp b/src/video_core/texture_cache/format_lookup_table.cpp index 56307d030..8c774f512 100644 --- a/src/video_core/texture_cache/format_lookup_table.cpp +++ b/src/video_core/texture_cache/format_lookup_table.cpp @@ -138,10 +138,16 @@ PixelFormat PixelFormatFromTextureInfo(TextureFormat format, ComponentType red, return PixelFormat::E5B9G9R9_FLOAT; case Hash(TextureFormat::Z32, FLOAT): return PixelFormat::D32_FLOAT; + case Hash(TextureFormat::Z32, FLOAT, UINT, UINT, UINT, LINEAR): + return PixelFormat::D32_FLOAT; case Hash(TextureFormat::Z16, UNORM): return PixelFormat::D16_UNORM; case Hash(TextureFormat::Z16, UNORM, UINT, UINT, UINT, LINEAR): return PixelFormat::D16_UNORM; + case Hash(TextureFormat::X8Z24, UNORM): + return PixelFormat::X8_D24_UNORM; + case Hash(TextureFormat::X8Z24, UNORM, UINT, UINT, UINT, LINEAR): + return PixelFormat::X8_D24_UNORM; case Hash(TextureFormat::Z24S8, UINT, UNORM, UNORM, UNORM, LINEAR): return PixelFormat::S8_UINT_D24_UNORM; case Hash(TextureFormat::Z24S8, UINT, UNORM, UINT, UINT, LINEAR): diff --git a/src/video_core/texture_cache/formatter.h b/src/video_core/texture_cache/formatter.h index 9ee57a076..cabbfcb2d 100644 --- a/src/video_core/texture_cache/formatter.h +++ b/src/video_core/texture_cache/formatter.h @@ -211,6 +211,8 @@ struct fmt::formatter<VideoCore::Surface::PixelFormat> : fmt::formatter<fmt::str return "D32_FLOAT"; case PixelFormat::D16_UNORM: return "D16_UNORM"; + case PixelFormat::X8_D24_UNORM: + return "X8_D24_UNORM"; case PixelFormat::S8_UINT: return "S8_UINT"; case PixelFormat::D24_UNORM_S8_UINT: diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 55d49d017..0587d7b72 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -41,7 +41,7 @@ enum class ImageFlagBits : u32 { IsRescalable = 1 << 15, AsynchronousDecode = 1 << 16, - IsDecoding = 1 << 17, ///< Is currently being decoded asynchornously. + IsDecoding = 1 << 17, ///< Is currently being decoded asynchronously. }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) diff --git a/src/video_core/texture_cache/image_view_base.cpp b/src/video_core/texture_cache/image_view_base.cpp index 0c5f4450d..18b9250f9 100644 --- a/src/video_core/texture_cache/image_view_base.cpp +++ b/src/video_core/texture_cache/image_view_base.cpp @@ -85,6 +85,7 @@ bool ImageViewBase::SupportsAnisotropy() const noexcept { // Depth formats case PixelFormat::D32_FLOAT: case PixelFormat::D16_UNORM: + case PixelFormat::X8_D24_UNORM: // Stencil formats case PixelFormat::S8_UINT: // DepthStencil formats diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index a83f5d41c..0a86ce139 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -1195,7 +1195,7 @@ std::optional<SubresourceBase> FindSubresource(const ImageInfo& candidate, const return std::nullopt; } } else { - // Format comaptibility is not relaxed, ensure we are creating a view on a compatible format + // Format compatibility is not relaxed, ensure we are creating a view on a compatible format if (!IsViewCompatible(existing.format, candidate.format, broken_views, native_bgr)) { return std::nullopt; } diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 3960b135a..876cec2e8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -84,9 +84,12 @@ constexpr std::array VK_FORMAT_A4B4G4R4_UNORM_PACK16{ } // namespace Alternatives enum class NvidiaArchitecture { - AmpereOrNewer, + KeplerOrOlder, + Maxwell, + Pascal, + Volta, Turing, - VoltaOrOlder, + AmpereOrNewer, }; template <typename T> @@ -200,6 +203,7 @@ std::unordered_map<VkFormat, VkFormatProperties> GetFormatProperties(vk::Physica VK_FORMAT_BC7_UNORM_BLOCK, VK_FORMAT_D16_UNORM, VK_FORMAT_D16_UNORM_S8_UINT, + VK_FORMAT_X8_D24_UNORM_PACK32, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, @@ -321,13 +325,38 @@ NvidiaArchitecture GetNvidiaArchitecture(vk::PhysicalDevice physical, physical.GetProperties2(physical_properties); if (shading_rate_props.primitiveFragmentShadingRateWithMultipleViewports) { // Only Ampere and newer support this feature + // TODO: Find a way to differentiate Ampere and Ada return NvidiaArchitecture::AmpereOrNewer; } - } - if (exts.contains(VK_NV_SHADING_RATE_IMAGE_EXTENSION_NAME)) { return NvidiaArchitecture::Turing; } - return NvidiaArchitecture::VoltaOrOlder; + + if (exts.contains(VK_EXT_BLEND_OPERATION_ADVANCED_EXTENSION_NAME)) { + VkPhysicalDeviceBlendOperationAdvancedPropertiesEXT advanced_blending_props{}; + advanced_blending_props.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_PROPERTIES_EXT; + VkPhysicalDeviceProperties2 physical_properties{}; + physical_properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + physical_properties.pNext = &advanced_blending_props; + physical.GetProperties2(physical_properties); + if (advanced_blending_props.advancedBlendMaxColorAttachments == 1) { + return NvidiaArchitecture::Maxwell; + } + + if (exts.contains(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME)) { + VkPhysicalDeviceConservativeRasterizationPropertiesEXT conservative_raster_props{}; + conservative_raster_props.sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONSERVATIVE_RASTERIZATION_PROPERTIES_EXT; + physical_properties.pNext = &conservative_raster_props; + physical.GetProperties2(physical_properties); + if (conservative_raster_props.degenerateLinesRasterized) { + return NvidiaArchitecture::Volta; + } + return NvidiaArchitecture::Pascal; + } + } + + return NvidiaArchitecture::KeplerOrOlder; } std::vector<const char*> ExtensionListForVulkan( @@ -504,19 +533,14 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (is_nvidia) { const u32 nv_major_version = (properties.properties.driverVersion >> 22) & 0x3ff; const auto arch = GetNvidiaArchitecture(physical, supported_extensions); - switch (arch) { - case NvidiaArchitecture::AmpereOrNewer: + if (arch >= NvidiaArchitecture::AmpereOrNewer) { LOG_WARNING(Render_Vulkan, "Ampere and newer have broken float16 math"); features.shader_float16_int8.shaderFloat16 = false; - break; - case NvidiaArchitecture::Turing: - break; - case NvidiaArchitecture::VoltaOrOlder: + } else if (arch <= NvidiaArchitecture::Volta) { if (nv_major_version < 527) { LOG_WARNING(Render_Vulkan, "Volta and older have broken VK_KHR_push_descriptor"); RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); } - break; } if (nv_major_version >= 510) { LOG_WARNING(Render_Vulkan, "NVIDIA Drivers >= 510 do not support MSAA image blits"); @@ -661,7 +685,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR "ANV drivers 22.3.0 to 23.1.0 have broken VK_KHR_push_descriptor"); RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); } + } else if (extensions.push_descriptor && is_nvidia) { + const auto arch = GetNvidiaArchitecture(physical, supported_extensions); + if (arch <= NvidiaArchitecture::Pascal) { + LOG_WARNING(Render_Vulkan, + "Pascal and older architectures have broken VK_KHR_push_descriptor"); + RemoveExtension(extensions.push_descriptor, VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME); + } } + if (is_mvk) { LOG_WARNING(Render_Vulkan, "MVK driver breaks when using more than 16 vertex attributes/bindings"); diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 9be612392..282a2925d 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -314,7 +314,7 @@ public: return GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY; } - /// Returns true if the device suppors float64 natively. + /// Returns true if the device supports float64 natively. bool IsFloat64Supported() const { return features.features.shaderFloat64; } diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 3ef381a38..82767fdf0 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -9,6 +9,7 @@ #include "common/alignment.h" #include "common/assert.h" #include "common/common_types.h" +#include "common/literals.h" #include "common/logging/log.h" #include "common/polyfill_ranges.h" #include "video_core/vulkan_common/vma.h" @@ -69,8 +70,7 @@ struct Range { case MemoryUsage::Download: return VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; case MemoryUsage::DeviceLocal: - return VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | - VMA_ALLOCATION_CREATE_HOST_ACCESS_ALLOW_TRANSFER_INSTEAD_BIT; + return {}; } return {}; } @@ -212,7 +212,20 @@ MemoryAllocator::MemoryAllocator(const Device& device_) : device{device_}, allocator{device.GetAllocator()}, properties{device_.GetPhysical().GetMemoryProperties().memoryProperties}, buffer_image_granularity{ - device_.GetPhysical().GetProperties().limits.bufferImageGranularity} {} + device_.GetPhysical().GetProperties().limits.bufferImageGranularity} { + // GPUs not supporting rebar may only have a region with less than 256MB host visible/device + // local memory. In that case, opening 2 RenderDoc captures side-by-side is not possible due to + // the heap running out of memory. With RenderDoc attached and only a small host/device region, + // only allow the stream buffer in this memory heap. + if (device.HasDebuggingToolAttached()) { + using namespace Common::Literals; + ForEachDeviceLocalHostVisibleHeap(device, [this](size_t index, VkMemoryHeap& heap) { + if (heap.size <= 256_MiB) { + valid_memory_types &= ~(1u << index); + } + }); + } +} MemoryAllocator::~MemoryAllocator() = default; @@ -244,7 +257,7 @@ vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo& ci, MemoryUsa .usage = MemoryUsageVma(usage), .requiredFlags = 0, .preferredFlags = MemoryUsagePreferedVmaFlags(usage), - .memoryTypeBits = 0, + .memoryTypeBits = usage == MemoryUsage::Stream ? 0u : valid_memory_types, .pool = VK_NULL_HANDLE, .pUserData = nullptr, .priority = 0.f, diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index f449bc8d0..38a182bcb 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -7,6 +7,7 @@ #include <span> #include <vector> #include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" VK_DEFINE_HANDLE(VmaAllocator) @@ -26,6 +27,18 @@ enum class MemoryUsage { Stream, ///< Requests device local host visible buffer, falling back host memory. }; +template <typename F> +void ForEachDeviceLocalHostVisibleHeap(const Device& device, F&& f) { + auto memory_props = device.GetPhysical().GetMemoryProperties().memoryProperties; + for (size_t i = 0; i < memory_props.memoryTypeCount; i++) { + auto& memory_type = memory_props.memoryTypes[i]; + if ((memory_type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) && + (memory_type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) { + f(memory_type.heapIndex, memory_props.memoryHeaps[memory_type.heapIndex]); + } + } +} + /// Ownership handle of a memory commitment. /// Points to a subregion of a memory allocation. class MemoryCommit { @@ -124,6 +137,7 @@ private: std::vector<std::unique_ptr<MemoryAllocation>> allocations; ///< Current allocations. VkDeviceSize buffer_image_granularity; // The granularity for adjacent offsets between buffers // and optimal images + u32 valid_memory_types{~0u}; }; } // namespace Vulkan diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 1e3c0fa64..0487cd3b6 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -117,6 +117,9 @@ public: virtual ~Exception() = default; const char* what() const noexcept override; + VkResult GetResult() const noexcept { + return result; + } private: VkResult result; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 8f86a1553..9ebece907 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -195,6 +195,8 @@ add_executable(yuzu multiplayer/state.cpp multiplayer/state.h multiplayer/validation.h + play_time_manager.cpp + play_time_manager.h precompiled_headers.h qt_common.cpp qt_common.h diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp index 9ccfb2435..81dd51ad3 100644 --- a/src/yuzu/configuration/configure_audio.cpp +++ b/src/yuzu/configuration/configure_audio.cpp @@ -42,6 +42,9 @@ void ConfigureAudio::Setup(const ConfigurationShared::Builder& builder) { for (auto* setting : Settings::values.linkage.by_category[category]) { settings.push_back(setting); } + for (auto* setting : UISettings::values.linkage.by_category[category]) { + settings.push_back(setting); + } }; push(Settings::Category::Audio); diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp index a9fde9f4f..82f3b6e78 100644 --- a/src/yuzu/configuration/configure_ui.cpp +++ b/src/yuzu/configuration/configure_ui.cpp @@ -123,6 +123,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent) connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); + connect(ui->show_play_time, &QCheckBox::stateChanged, this, + &ConfigureUi::RequestGameListUpdate); connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ConfigureUi::RequestGameListUpdate); connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), @@ -167,6 +169,7 @@ void ConfigureUi::ApplyConfiguration() { UISettings::values.show_compat = ui->show_compat->isChecked(); UISettings::values.show_size = ui->show_size->isChecked(); UISettings::values.show_types = ui->show_types->isChecked(); + UISettings::values.show_play_time = ui->show_play_time->isChecked(); UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt(); UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt(); UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); @@ -179,6 +182,7 @@ void ConfigureUi::ApplyConfiguration() { const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); UISettings::values.screenshot_height.SetValue(height); + RequestGameListUpdate(); system.ApplySettings(); } @@ -194,6 +198,7 @@ void ConfigureUi::SetConfiguration() { ui->show_compat->setChecked(UISettings::values.show_compat.GetValue()); ui->show_size->setChecked(UISettings::values.show_size.GetValue()); ui->show_types->setChecked(UISettings::values.show_types.GetValue()); + ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue()); ui->game_icon_size_combobox->setCurrentIndex( ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue())); ui->folder_icon_size_combobox->setCurrentIndex( diff --git a/src/yuzu/configuration/configure_ui.ui b/src/yuzu/configuration/configure_ui.ui index cb66ef104..b8e648381 100644 --- a/src/yuzu/configuration/configure_ui.ui +++ b/src/yuzu/configuration/configure_ui.ui @@ -105,6 +105,13 @@ </widget> </item> <item> + <widget class="QCheckBox" name="show_play_time"> + <property name="text"> + <string>Show Play Time Column</string> + </property> + </widget> + </item> + <item> <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2"> <item> <widget class="QLabel" name="game_icon_size_label"> diff --git a/src/yuzu/configuration/shared_translation.cpp b/src/yuzu/configuration/shared_translation.cpp index 276bdbaba..a4e8af1b4 100644 --- a/src/yuzu/configuration/shared_translation.cpp +++ b/src/yuzu/configuration/shared_translation.cpp @@ -29,9 +29,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) { INSERT(Settings, sink_id, "Output Engine:", ""); INSERT(Settings, audio_output_device_id, "Output Device:", ""); INSERT(Settings, audio_input_device_id, "Input Device:", ""); - INSERT(Settings, audio_muted, "Mute audio when in background", ""); + INSERT(Settings, audio_muted, "Mute audio", ""); INSERT(Settings, volume, "Volume:", ""); INSERT(Settings, dump_audio_commands, "", ""); + INSERT(UISettings, mute_when_in_background, "Mute audio when in background", ""); // Core INSERT(Settings, use_multi_core, "Multicore CPU Emulation", ""); diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index f254c1e1c..74f48031a 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -312,8 +312,10 @@ void GameList::OnFilterCloseClicked() { } GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, - Core::System& system_, GMainWindow* parent) - : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, system{system_} { + PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, + GMainWindow* parent) + : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, + play_time_manager{play_time_manager_}, system{system_} { watcher = new QFileSystemWatcher(this); connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory); @@ -340,6 +342,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); + tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); item_model->setSortRole(GameListItemPath::SortRole); connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); @@ -548,6 +551,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC")); QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration")); + QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data")); QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage")); QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache")); QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache")); @@ -560,9 +564,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); -#ifndef WIN32 QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); +#ifndef WIN32 QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu")); #endif @@ -622,6 +626,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path); }); + connect(remove_play_time_data, &QAction::triggered, + [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path); }); @@ -638,10 +644,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); -#ifndef WIN32 connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); }); +#ifndef WIN32 connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); }); @@ -790,6 +796,7 @@ void GameList::RetranslateUI() { item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons")); item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); + item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time")); } void GameListSearchField::changeEvent(QEvent* event) { @@ -817,6 +824,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); + tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); // Delete any rows that might already exist if we're repopulating item_model->removeRows(0, item_model->rowCount()); @@ -825,7 +833,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { emit ShouldCancelWorker(); GameListWorker* worker = - new GameListWorker(vfs, provider, game_dirs, compatibility_list, system); + new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system); connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 1fcbbf0ba..712570cea 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -18,6 +18,7 @@ #include "core/core.h" #include "uisettings.h" #include "yuzu/compatibility_list.h" +#include "yuzu/play_time_manager.h" namespace Core { class System; @@ -75,11 +76,13 @@ public: COLUMN_ADD_ONS, COLUMN_FILE_TYPE, COLUMN_SIZE, + COLUMN_PLAY_TIME, COLUMN_COUNT, // Number of columns }; explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_, - FileSys::ManualContentProvider* provider_, Core::System& system_, + FileSys::ManualContentProvider* provider_, + PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, GMainWindow* parent = nullptr); ~GameList() override; @@ -113,6 +116,7 @@ signals: void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type); void RemoveFileRequested(u64 program_id, GameListRemoveTarget target, const std::string& game_path); + void RemovePlayTimeRequested(u64 program_id); void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void VerifyIntegrityRequested(const std::string& game_path); void CopyTIDRequested(u64 program_id); @@ -168,6 +172,7 @@ private: friend class GameListSearchField; + const PlayTime::PlayTimeManager& play_time_manager; Core::System& system; }; diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 1800f090f..86a0c41d9 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -18,6 +18,7 @@ #include "common/common_types.h" #include "common/logging/log.h" #include "common/string_util.h" +#include "yuzu/play_time_manager.h" #include "yuzu/uisettings.h" #include "yuzu/util/util.h" @@ -221,6 +222,31 @@ public: } }; +/** + * GameListItem for Play Time values. + * This object stores the play time of a game in seconds, and its readable + * representation in minutes/hours + */ +class GameListItemPlayTime : public GameListItem { +public: + static constexpr int PlayTimeRole = SortRole; + + GameListItemPlayTime() = default; + explicit GameListItemPlayTime(const qulonglong time_seconds) { + setData(time_seconds, PlayTimeRole); + } + + void setData(const QVariant& value, int role) override { + qulonglong time_seconds = value.toULongLong(); + GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole); + GameListItem::setData(value, PlayTimeRole); + } + + bool operator<(const QStandardItem& other) const override { + return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong(); + } +}; + class GameListDir : public GameListItem { public: static constexpr int GameDirRole = Qt::UserRole + 2; diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index e7fb8a282..588f1dd6e 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -194,6 +194,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri const std::size_t size, const std::vector<u8>& icon, Loader::AppLoader& loader, u64 program_id, const CompatibilityList& compatibility_list, + const PlayTime::PlayTimeManager& play_time_manager, const FileSys::PatchManager& patch) { const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); @@ -212,6 +213,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri new GameListItemCompat(compatibility), new GameListItem(file_type_string), new GameListItemSize(size), + new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), }; const auto patch_versions = GetGameListCachedObject( @@ -227,9 +229,12 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, QVector<UISettings::GameDir>& game_dirs_, - const CompatibilityList& compatibility_list_, Core::System& system_) + const CompatibilityList& compatibility_list_, + const PlayTime::PlayTimeManager& play_time_manager_, + Core::System& system_) : vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_}, - compatibility_list{compatibility_list_}, system{system_} {} + compatibility_list{compatibility_list_}, + play_time_manager{play_time_manager_}, system{system_} {} GameListWorker::~GameListWorker() = default; @@ -280,7 +285,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { } emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, - program_id, compatibility_list, patch), + program_id, compatibility_list, play_time_manager, patch), parent_dir); } } @@ -357,7 +362,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa emit EntryReady(MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), icon, - *loader, id, compatibility_list, patch), + *loader, id, compatibility_list, + play_time_manager, patch), parent_dir); } } else { @@ -370,10 +376,11 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), system.GetContentProvider()}; - emit EntryReady( - MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), - icon, *loader, program_id, compatibility_list, patch), - parent_dir); + emit EntryReady(MakeGameListEntry(physical_name, name, + Common::FS::GetSize(physical_name), icon, + *loader, program_id, compatibility_list, + play_time_manager, patch), + parent_dir); } } } else if (is_dir) { diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 24a4e92c3..2bb0a0cb6 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -13,6 +13,7 @@ #include <QString> #include "yuzu/compatibility_list.h" +#include "yuzu/play_time_manager.h" namespace Core { class System; @@ -36,7 +37,9 @@ public: explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_, FileSys::ManualContentProvider* provider_, QVector<UISettings::GameDir>& game_dirs_, - const CompatibilityList& compatibility_list_, Core::System& system_); + const CompatibilityList& compatibility_list_, + const PlayTime::PlayTimeManager& play_time_manager_, + Core::System& system_); ~GameListWorker() override; /// Starts the processing of directory tree information. @@ -76,6 +79,7 @@ private: FileSys::ManualContentProvider* provider; QVector<UISettings::GameDir>& game_dirs; const CompatibilityList& compatibility_list; + const PlayTime::PlayTimeManager& play_time_manager; QStringList watch_list; std::atomic_bool stop_processing; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 55f61f98e..5427758c1 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -98,6 +98,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "common/scm_rev.h" #include "common/scope_exit.h" #ifdef _WIN32 +#include <shlobj.h> #include "common/windows/timer_resolution.h" #endif #ifdef ARCHITECTURE_x86_64 @@ -150,6 +151,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/install_dialog.h" #include "yuzu/loading_screen.h" #include "yuzu/main.h" +#include "yuzu/play_time_manager.h" #include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" #include "yuzu/util/clickable_label.h" @@ -338,6 +340,8 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + play_time_manager = std::make_unique<PlayTime::PlayTimeManager>(); + system->GetRoomNetwork().Init(); RegisterMetaTypes(); @@ -986,7 +990,7 @@ void GMainWindow::InitializeWidgets() { render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system); render_window->hide(); - game_list = new GameList(vfs, provider.get(), *system, this); + game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this); ui->horizontalLayout->addWidget(game_list); game_list_placeholder = new GameListPlaceholder(this); @@ -1447,6 +1451,7 @@ void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { Settings::values.audio_muted = false; auto_muted = false; } + UpdateVolumeUI(); } } @@ -1460,6 +1465,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::RemoveInstalledEntryRequested, this, &GMainWindow::OnGameListRemoveInstalledEntry); connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); + connect(game_list, &GameList::RemovePlayTimeRequested, this, + &GMainWindow::OnGameListRemovePlayTimeData); connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::VerifyIntegrityRequested, this, &GMainWindow::OnGameListVerifyIntegrity); @@ -2536,6 +2543,17 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ } } +void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { + if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) != QMessageBox::Yes) { + return; + } + + play_time_manager->ResetProgramPlayTime(program_id); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) { const auto target_file_name = [target] { switch (target) { @@ -2827,7 +2845,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga const QStringList args = QApplication::arguments(); std::filesystem::path yuzu_command = args[0].toStdString(); -#if defined(__linux__) || defined(__FreeBSD__) // If relative path, make it an absolute path if (yuzu_command.c_str()[0] == '.') { yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; @@ -2850,12 +2867,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga UISettings::values.shortcut_already_warned = true; } #endif // __linux__ -#endif // __linux__ || __FreeBSD__ std::filesystem::path target_directory{}; // Determine target directory for shortcut -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(WIN32) + const char* home = std::getenv("USERPROFILE"); +#else const char* home = std::getenv("HOME"); +#endif const std::filesystem::path home_path = (home == nullptr ? "~" : home); const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); @@ -2865,7 +2884,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga QMessageBox::critical( this, tr("Create Shortcut"), tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") - .arg(QString::fromStdString(target_directory)), + .arg(QString::fromStdString(target_directory.generic_string())), QMessageBox::StandardButton::Ok); return; } @@ -2873,15 +2892,15 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / "applications"; if (!Common::FS::CreateDirs(target_directory)) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Cannot create shortcut in applications menu. Path \"%1\" " - "does not exist and cannot be created.") - .arg(QString::fromStdString(target_directory)), - QMessageBox::StandardButton::Ok); + QMessageBox::critical( + this, tr("Create Shortcut"), + tr("Cannot create shortcut in applications menu. Path \"%1\" " + "does not exist and cannot be created.") + .arg(QString::fromStdString(target_directory.generic_string())), + QMessageBox::StandardButton::Ok); return; } } -#endif const std::string game_file_name = std::filesystem::path(game_path).filename().string(); // Determine full paths for icon and shortcut @@ -2903,9 +2922,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga const std::filesystem::path shortcut_path = target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) : fmt::format("yuzu-{:016X}.desktop", program_id)); +#elif defined(WIN32) + std::filesystem::path icons_path = + Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); + std::filesystem::path icon_path = + icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) + : fmt::format("yuzu-{:016X}.ico", program_id))); #else - const std::filesystem::path icon_path{}; - const std::filesystem::path shortcut_path{}; + std::string icon_extension; #endif // Get title from game file @@ -2930,29 +2954,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); } - QImage icon_jpeg = + QImage icon_data = QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); #if defined(__linux__) || defined(__FreeBSD__) // Convert and write the icon as a PNG - if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) { + if (!icon_data.save(QString::fromStdString(icon_path.string()))) { LOG_ERROR(Frontend, "Could not write icon as PNG to file"); } else { LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); } +#elif defined(WIN32) + if (!SaveIconToFile(icon_path.string(), icon_data)) { + LOG_ERROR(Frontend, "Could not write icon to file"); + return; + } #endif // __linux__ -#if defined(__linux__) || defined(__FreeBSD__) +#ifdef _WIN32 + // Replace characters that are illegal in Windows filenames by a dash + const std::string illegal_chars = "<>:\"/\\|?*"; + for (char c : illegal_chars) { + std::replace(title.begin(), title.end(), c, '_'); + } + const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); +#endif + const std::string comment = tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); const std::string arguments = fmt::format("-g \"{:s}\"", game_path); const std::string categories = "Game;Emulator;Qt;"; const std::string keywords = "Switch;Nintendo;"; -#else - const std::string comment{}; - const std::string arguments{}; - const std::string categories{}; - const std::string keywords{}; -#endif + if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), yuzu_command.string(), arguments, categories, keywords)) { QMessageBox::critical(this, tr("Create Shortcut"), @@ -3359,6 +3391,9 @@ void GMainWindow::OnStartGame() { UpdateMenuState(); OnTasStateChanged(); + play_time_manager->SetProgramId(system->GetApplicationProcessProgramID()); + play_time_manager->Start(); + discord_rpc->Update(); } @@ -3374,6 +3409,7 @@ void GMainWindow::OnRestartGame() { void GMainWindow::OnPauseGame() { emu_thread->SetRunning(false); + play_time_manager->Stop(); UpdateMenuState(); AllowOSSleep(); } @@ -3394,6 +3430,9 @@ void GMainWindow::OnStopGame() { return; } + play_time_manager->Stop(); + // Update game list to show new play time + game_list->PopulateAsync(UISettings::values.game_dirs); if (OnShutdownBegin()) { OnShutdownBeginDialog(); } else { @@ -3967,6 +4006,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st shortcut_stream.close(); return true; +#elif defined(WIN32) + IShellLinkW* shell_link; + auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + (void**)&shell_link); + if (FAILED(hres)) { + return false; + } + shell_link->SetPath( + Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to + shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); + shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); + shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); + + IPersistFile* persist_file; + hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); + if (FAILED(hres)) { + return false; + } + + hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); + if (FAILED(hres)) { + return false; + } + + persist_file->Release(); + shell_link->Release(); + + return true; #endif return false; } diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 6450beb65..2346eb3bd 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -81,6 +81,10 @@ namespace DiscordRPC { class DiscordInterface; } +namespace PlayTime { +class PlayTimeManager; +} + namespace FileSys { class ContentProvider; class ManualContentProvider; @@ -323,6 +327,7 @@ private slots: void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, const std::string& game_path); + void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListVerifyIntegrity(const std::string& game_path); void OnGameListCopyTID(u64 program_id); @@ -390,6 +395,7 @@ private: void RemoveVulkanDriverPipelineCache(u64 program_id); void RemoveAllTransferableShaderCaches(u64 program_id); void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); + void RemovePlayTimeData(u64 program_id); void RemoveCacheStorage(u64 program_id); bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, u64* selected_title_id, u8* selected_content_record_type); @@ -429,6 +435,7 @@ private: std::unique_ptr<Core::System> system; std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; + std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager; std::shared_ptr<InputCommon::InputSubsystem> input_subsystem; MultiplayerState* multiplayer_state = nullptr; diff --git a/src/yuzu/play_time_manager.cpp b/src/yuzu/play_time_manager.cpp new file mode 100644 index 000000000..155c36b7d --- /dev/null +++ b/src/yuzu/play_time_manager.cpp @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/alignment.h" +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "common/thread.h" +#include "core/hle/service/acc/profile_manager.h" +#include "yuzu/play_time_manager.h" + +namespace PlayTime { + +namespace { + +struct PlayTimeElement { + ProgramId program_id; + PlayTime play_time; +}; + +std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() { + const Service::Account::ProfileManager manager; + const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user)); + if (!uuid.has_value()) { + return std::nullopt; + } + return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) / + uuid->RawString().append(".bin"); +} + +[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); + + if (!filename.has_value()) { + LOG_ERROR(Frontend, "Failed to get current user path"); + return false; + } + + out_play_time_db.clear(); + + if (Common::FS::Exists(filename.value())) { + Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + LOG_ERROR(Frontend, "Failed to open play time file: {}", + Common::FS::PathToUTF8String(filename.value())); + return false; + } + + const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement); + std::vector<PlayTimeElement> elements(num_elements); + + if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) { + return false; + } + + for (const auto& [program_id, play_time] : elements) { + if (program_id != 0) { + out_play_time_db[program_id] = play_time; + } + } + } + + return true; +} + +[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) { + const auto filename = GetCurrentUserPlayTimePath(); + + if (!filename.has_value()) { + LOG_ERROR(Frontend, "Failed to get current user path"); + return false; + } + + Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + LOG_ERROR(Frontend, "Failed to open play time file: {}", + Common::FS::PathToUTF8String(filename.value())); + return false; + } + + std::vector<PlayTimeElement> elements; + elements.reserve(play_time_db.size()); + + for (auto& [program_id, play_time] : play_time_db) { + if (program_id != 0) { + elements.push_back(PlayTimeElement{program_id, play_time}); + } + } + + return file.WriteSpan<PlayTimeElement>(elements) == elements.size(); +} + +} // namespace + +PlayTimeManager::PlayTimeManager() { + if (!ReadPlayTimeFile(database)) { + LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default."); + } +} + +PlayTimeManager::~PlayTimeManager() { + Save(); +} + +void PlayTimeManager::SetProgramId(u64 program_id) { + running_program_id = program_id; +} + +void PlayTimeManager::Start() { + play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); }); +} + +void PlayTimeManager::Stop() { + play_time_thread = {}; +} + +void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) { + Common::SetCurrentThreadName("PlayTimeReport"); + + using namespace std::literals::chrono_literals; + using std::chrono::seconds; + using std::chrono::steady_clock; + + auto timestamp = steady_clock::now(); + + const auto GetDuration = [&]() -> u64 { + const auto last_timestamp = std::exchange(timestamp, steady_clock::now()); + const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp); + return static_cast<u64>(duration.count()); + }; + + while (!stop_token.stop_requested()) { + Common::StoppableTimedWait(stop_token, 30s); + + database[running_program_id] += GetDuration(); + Save(); + } +} + +void PlayTimeManager::Save() { + if (!WritePlayTimeFile(database)) { + LOG_ERROR(Frontend, "Failed to update play time database!"); + } +} + +u64 PlayTimeManager::GetPlayTime(u64 program_id) const { + auto it = database.find(program_id); + if (it != database.end()) { + return it->second; + } else { + return 0; + } +} + +void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { + database.erase(program_id); + Save(); +} + +QString ReadablePlayTime(qulonglong time_seconds) { + if (time_seconds == 0) { + return {}; + } + const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0); + const auto time_hours = static_cast<double>(time_seconds) / 3600; + const bool is_minutes = time_minutes < 60; + const char* unit = is_minutes ? "m" : "h"; + const auto value = is_minutes ? time_minutes : time_hours; + + return QStringLiteral("%L1 %2") + .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) + .arg(QString::fromUtf8(unit)); +} + +} // namespace PlayTime diff --git a/src/yuzu/play_time_manager.h b/src/yuzu/play_time_manager.h new file mode 100644 index 000000000..5f96f3447 --- /dev/null +++ b/src/yuzu/play_time_manager.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QString> + +#include <map> + +#include "common/common_funcs.h" +#include "common/common_types.h" +#include "common/polyfill_thread.h" + +namespace PlayTime { + +using ProgramId = u64; +using PlayTime = u64; +using PlayTimeDatabase = std::map<ProgramId, PlayTime>; + +class PlayTimeManager { +public: + explicit PlayTimeManager(); + ~PlayTimeManager(); + + YUZU_NON_COPYABLE(PlayTimeManager); + YUZU_NON_MOVEABLE(PlayTimeManager); + + u64 GetPlayTime(u64 program_id) const; + void ResetProgramPlayTime(u64 program_id); + void SetProgramId(u64 program_id); + void Start(); + void Stop(); + +private: + PlayTimeDatabase database; + u64 running_program_id; + std::jthread play_time_thread; + void AutoTimestamp(std::stop_token stop_token); + void Save(); +}; + +QString ReadablePlayTime(qulonglong time_seconds); + +} // namespace PlayTime diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 8efd63f31..975008159 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -103,7 +103,7 @@ struct Values { true, true}; Setting<bool> mute_when_in_background{ - linkage, false, "muteWhenInBackground", Category::Ui, Settings::Specialization::Default, + linkage, false, "muteWhenInBackground", Category::Audio, Settings::Specialization::Default, true, true}; Setting<bool> hide_mouse{ linkage, true, "hideInactiveMouse", Category::UiGeneral, Settings::Specialization::Default, @@ -183,6 +183,9 @@ struct Values { Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList}; Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList}; + // Play time + Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList}; + bool configuration_applied; bool reset_to_defaults; bool shortcut_already_warned{false}; diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 5c3e4589e..61cf00176 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -5,6 +5,10 @@ #include <cmath> #include <QPainter> #include "yuzu/util/util.h" +#ifdef _WIN32 +#include <windows.h> +#include "common/fs/file.h" +#endif QFont GetMonospaceFont() { QFont font(QStringLiteral("monospace")); @@ -37,3 +41,76 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); return circle_pixmap; } + +bool SaveIconToFile(const std::string_view path, const QImage& image) { +#if defined(WIN32) +#pragma pack(push, 2) + struct IconDir { + WORD id_reserved; + WORD id_type; + WORD id_count; + }; + + struct IconDirEntry { + BYTE width; + BYTE height; + BYTE color_count; + BYTE reserved; + WORD planes; + WORD bit_count; + DWORD bytes_in_res; + DWORD image_offset; + }; +#pragma pack(pop) + + QImage source_image = image.convertToFormat(QImage::Format_RGB32); + constexpr int bytes_per_pixel = 4; + const int image_size = source_image.width() * source_image.height() * bytes_per_pixel; + + BITMAPINFOHEADER info_header{}; + info_header.biSize = sizeof(BITMAPINFOHEADER), info_header.biWidth = source_image.width(), + info_header.biHeight = source_image.height() * 2, info_header.biPlanes = 1, + info_header.biBitCount = bytes_per_pixel * 8, info_header.biCompression = BI_RGB; + + const IconDir icon_dir{.id_reserved = 0, .id_type = 1, .id_count = 1}; + const IconDirEntry icon_entry{.width = static_cast<BYTE>(source_image.width()), + .height = static_cast<BYTE>(source_image.height() * 2), + .color_count = 0, + .reserved = 0, + .planes = 1, + .bit_count = bytes_per_pixel * 8, + .bytes_in_res = + static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size), + .image_offset = sizeof(IconDir) + sizeof(IconDirEntry)}; + + Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile); + if (!icon_file.IsOpen()) { + return false; + } + + if (!icon_file.Write(icon_dir)) { + return false; + } + if (!icon_file.Write(icon_entry)) { + return false; + } + if (!icon_file.Write(info_header)) { + return false; + } + + for (int y = 0; y < image.height(); y++) { + const auto* line = source_image.scanLine(source_image.height() - 1 - y); + std::vector<u8> line_data(source_image.width() * bytes_per_pixel); + std::memcpy(line_data.data(), line, line_data.size()); + if (!icon_file.Write(line_data)) { + return false; + } + } + icon_file.Close(); + + return true; +#else + return false; +#endif +} diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 39dd2d895..09c14ce3f 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -7,14 +7,22 @@ #include <QString> /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. -QFont GetMonospaceFont(); +[[nodiscard]] QFont GetMonospaceFont(); /// Convert a size in bytes into a readable format (KiB, MiB, etc.) -QString ReadableByteSize(qulonglong size); +[[nodiscard]] QString ReadableByteSize(qulonglong size); /** * Creates a circle pixmap from a specified color * @param color The color the pixmap shall have * @return QPixmap circle pixmap */ -QPixmap CreateCirclePixmapFromColor(const QColor& color); +[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color); + +/** + * Saves a windows icon to a file + * @param path The icons path + * @param image The image to save + * @return bool If the operation succeeded + */ +[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); |