diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 16:21:45 -0500 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-11-01 16:21:45 -0500 |
| commit | a41d11108b6478ad862fc7fcac36ab51c83b8602 (patch) | |
| tree | 2c16c13764e6ef2b48c7327055fde64d7af2cd37 /src | |
| parent | 01effa81ab0e151f8cfd674bf4da952d24645943 (diff) | |
| parent | ac22c4087d9a6380c1937337530a5d4e54808ad1 (diff) | |
Merge remote-tracking branch 'origin/nx2' into merge/nx2-experimental-1101
# Conflicts:
# src/core/loader/deconstructed_rom_directory.cpp
Diffstat (limited to 'src')
105 files changed, 3938 insertions, 322 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2abc7d1b3..c49e252ccb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -178,6 +178,13 @@ else() $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter> $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-field-initializers>) + if (ENABLE_WERROR AND (NOT MSVC)) + add_compile_options( + $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused> + $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused-function> + ) + endif() + if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++ if (NOT MSVC) add_compile_options( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt index c2ef683cfd..62c9b5e753 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt @@ -15,6 +15,7 @@ import android.widget.TextView import androidx.drawerlayout.widget.DrawerLayout import com.google.android.material.color.MaterialColors import com.google.android.material.materialswitch.MaterialSwitch +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting @@ -150,6 +151,10 @@ class QuickSettings(val emulationFragment: EmulationFragment) { switchView.setOnCheckedChangeListener { _, isChecked -> setting.setBoolean(isChecked) saveSettings() + // Lets the running game know it was docked or undocked. + if (setting == BooleanSetting.USE_DOCKED_MODE) { + NativeLibrary.applySettings() + } } switchContainer.setOnClickListener { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 824a54181e..99b45cf3f3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -140,6 +140,7 @@ class SettingsFragmentPresenter( context.getString(R.string.scaling_filter_fsr), context.getString(R.string.scaling_filter_sgsr), context.getString(R.string.scaling_filter_sgsr_edge), + context.getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 3323b3cf9a..9db255d70c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1250,6 +1250,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { getString(R.string.scaling_filter_fsr), getString(R.string.scaling_filter_sgsr), getString(R.string.scaling_filter_sgsr_edge), + getString(R.string.scaling_filter_fsr3), ) return names.asSequence() .mapIndexedNotNull { index, name -> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index be367a2fcf..a9cf000e43 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -81,6 +81,7 @@ <item>0</item> <item>7</item> <item>14</item> + <item>15</item> <item>9</item> <item>10</item> <item>15</item> @@ -279,6 +280,7 @@ <item>@string/scaling_filter_spline1</item> <item>@string/scaling_filter_sgsr</item> <item>@string/scaling_filter_sgsr_edge</item> + <item>@string/scaling_filter_fsr3</item> </string-array> <integer-array name="rendererScalingFilterValues"> @@ -297,6 +299,7 @@ <item>12</item> <item>13</item> <item>14</item> + <item>15</item> </integer-array> <string-array name="rendererAntiAliasingNames"> @@ -454,6 +457,7 @@ <item>13</item> <item>14</item> <item>15</item> + <item>15</item> <item>16</item> <item>17</item> <item>18</item> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 5dd20af9a7..6304ef256b 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1170,6 +1170,7 @@ <string name="scaling_filter_lanczos" translatable="false">Lanczos</string> <string name="scaling_filter_scale_force" translatable="false">ScaleForce</string> <string name="scaling_filter_fsr" translatable="false">AMD FidelityFX Super Resolution</string> + <string name="scaling_filter_fsr3" translatable="false">AMD FidelityFX Super Resolution 3</string> <string name="scaling_filter_area" translatable="false">Area</string> <string name="scaling_filter_zero_tangent" translatable="false">Zero-Tangent</string> <string name="scaling_filter_bspline" translatable="false">B-Spline</string> diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d3e7b03cd2..76c5019788 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -152,7 +152,7 @@ add_library( set_source_files_properties(zbic_compression.cpp PROPERTIES INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}" - COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>") + COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:AppleClang,Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>") if(WIN32) target_sources(common PRIVATE windows/timer_resolution.cpp diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 3f5154026e..c63943a9d3 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -60,6 +60,7 @@ SWITCHABLE(NvdecEmulation, false); SWITCHABLE(Region, true); SWITCHABLE(RendererBackend, true); SWITCHABLE(ScalingFilter, false); +SWITCHABLE(FrameGenType, false); SWITCHABLE(SpirvOptimizeMode, true); SWITCHABLE(TimeZone, true); SETTING(VSyncMode, true); @@ -224,6 +225,26 @@ u64 GetCurrentProgramID() { return current_program_id; } +static bool current_program_is_switch2 = false; + +void SetCurrentProgramIsSwitch2(bool is_switch2) { + current_program_is_switch2 = is_switch2; +} + +bool CurrentProgramIsSwitch2() { + return current_program_is_switch2; +} + +static std::optional<AstcRecompression> astc_recompression_override; + +AstcRecompression GetAstcRecompression() { + return astc_recompression_override.value_or(values.astc_recompression.GetValue()); +} + +void SetAstcRecompressionOverride(std::optional<AstcRecompression> recompression) { + astc_recompression_override = recompression; +} + bool IsDockedMode() { return values.use_docked_mode.GetValue() == Settings::ConsoleMode::Docked; } diff --git a/src/common/settings.h b/src/common/settings.h index b57bec5c12..97103f457d 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -10,6 +10,7 @@ #include <array> #include <map> #include <memory> +#include <optional> #include <stdexcept> #include <string> #include <utility> @@ -420,6 +421,15 @@ struct Values { SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer, Specialization::Default, true, false}; + SwitchableSetting<FrameGenType> frame_gen_type{linkage, + FrameGenType::Fsr3, + "frame_gen_type", + Category::Renderer, + Specialization::Default, + true, + false, + &frame_gen}; + SwitchableSetting<u32, true> frame_gen_multiplier{linkage, 2, 2, @@ -977,6 +987,15 @@ bool IsNceEnabled(); void SetCurrentProgramID(u64 program_id); u64 GetCurrentProgramID(); +// Whether the program being loaded is a Switch 2 one (set by the core before the GPU starts). +void SetCurrentProgramIsSwitch2(bool is_switch2); +bool CurrentProgramIsSwitch2(); + +// The ASTC recompression in effect: astc_recompression, unless the renderer picked another one +// for the running program (the configuration is left as is). The core clears it on load. +AstcRecompression GetAstcRecompression(); +void SetAstcRecompressionOverride(std::optional<AstcRecompression> recompression); + bool IsOpenGL(); bool IsDockedMode(); diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index eaf3ec541d..133b503f72 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -146,7 +146,7 @@ ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never); ENUM(FullscreenMode, Borderless, Exclusive); ENUM(NvdecEmulation, Off, Cpu, Gpu); ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res5_4X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge, Fsr3); ENUM(AntiAliasing, None, Fxaa, Smaa); ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch); ENUM(ConsoleMode, Handheld, Docked); @@ -161,6 +161,7 @@ ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3); ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All) ENUM(GameListMode, TreeView, GridView, CarouselView); ENUM(SpeedMode, Standard, Turbo, Slow); +ENUM(FrameGenType, Lsfg, Fsr3); template <typename Type> inline std::string_view CanonicalizeEnum(Type id) { diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 56fa653738..0075838d49 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1036,6 +1036,7 @@ add_library(core STATIC hle/service/server_manager.h hle/service/service.cpp hle/service/service.h + hle/service/nx2_streamplay.h hle/service/nx2_stub_interface.h hle/service/nx2_gsv.h hle/service/services.cpp diff --git a/src/core/arm/nce/visitor_base.h b/src/core/arm/nce/visitor_base.h index 6a2be3d9bc..a92307997e 100644 --- a/src/core/arm/nce/visitor_base.h +++ b/src/core/arm/nce/visitor_base.h @@ -717,6 +717,32 @@ public: return false; } + // ARMv8.1+ forms decoded for Switch 2 code (must match TranslatorVisitor in dynarmic's impl.h) + virtual bool AtomicMemOp(Imm<2> size, bool A, bool R, Reg Rs, Imm<4> op, Reg Rn, Reg Rt) { + return false; + } + virtual bool LDAPR_sz(Imm<2> size, Reg Rn, Reg Rt) { + return false; + } + virtual bool CompareAndSwap(Imm<2> size, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) { + return false; + } + virtual bool CompareAndSwapPair(bool sz, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) { + return false; + } + virtual bool PACAUT_reg(bool Z, Imm<3> op, Reg Rn, Reg Rd) { + return false; + } + virtual bool BRA_BLRA(bool Z, bool L, bool M, Reg Rn, Reg Rm) { + return false; + } + virtual bool STG_family(Imm<2> opc, Imm<9> imm9, Imm<2> mode, Reg Rn, Reg Rt) { + return false; + } + virtual bool STGP(Imm<2> mode, Imm<7> imm7, Reg Rt2, Reg Rn, Reg Rt) { + return false; + } + // Loads and stores - Load/Store register (register offset) virtual bool STRx_reg(Imm<2> size, Imm<1> opc_1, Reg Rm, Imm<3> option, bool S, Reg Rn, Reg Rt) { diff --git a/src/core/core.cpp b/src/core/core.cpp index 64e1d8de06..41871df870 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -318,6 +318,8 @@ struct System::Impl { const bool switch2_program = virtual_filesystem != nullptr && IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath)); + Settings::SetCurrentProgramIsSwitch2(switch2_program); + Settings::SetAstcRecompressionOverride(std::nullopt); // Switch 2 titles size their heap from the total memory and need the 12 GiB layout. Set it // as a per-game value, so RestoreGlobalState brings back the configured layout afterwards. @@ -1059,6 +1061,8 @@ void System::ApplySettings() { if (IsPoweredOn()) { Renderer().RefreshBaseSettings(); + // Frontends switch docked/handheld through the settings; let the running game know. + GetAppletManager().OperationModeChanged(); } } diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h index 46cda14e78..27ff342213 100644 --- a/src/core/device_memory_manager.h +++ b/src/core/device_memory_manager.h @@ -126,6 +126,9 @@ public: // New batch API to update multiple ranges with a single lock acquisition. void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta); + /// Traps the next guest CPU read of these pages, to learn which GPU writes the CPU reads. + void ArmReadTraps(DAddr addr, size_t size); + private: struct TranslationEntry { DAddr guest_page{}; @@ -187,6 +190,8 @@ private: void WalkBlock(const DAddr addr, const std::size_t size, auto on_unmapped, auto on_memory, auto increment); + void CutContinuityAt(size_t page); + void InnerGatherDeviceAddresses(Common::ScratchBuffer<u32>& buffer, PAddr address); std::unique_ptr<DeviceMemoryManagerAllocator<Traits>> impl; diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc index d78a229e1f..83faa1b3f3 100644 --- a/src/core/device_memory_manager.inc +++ b/src/core/device_memory_manager.inc @@ -211,9 +211,13 @@ void DeviceMemoryManager<Traits>::Map(DAddr address, VAddr virtual_address, size std::scoped_lock lk(mapping_guard); tracked_entries.CommitRegion(start_page_d, start_page_d + num_pages); + CutContinuityAt(start_page_d); for (size_t i = 0; i < num_pages; i++) { const VAddr new_vaddress = virtual_address + i * Memory::YUZU_PAGESIZE; auto* ptr = process_memory->GetPointerSilent(Common::ProcessAddress(new_vaddress)); + // One page per run unless tracked below: a run left from an earlier mapping of these + // device pages would make WalkBlock copy the host memory that followed that mapping. + tracked_entries.GetUnchecked(start_page_d + i).continuity_tracker = 0; if (ptr == nullptr) [[unlikely]] { tracked_entries.GetUnchecked(start_page_d + i).compressed_physical_ptr = 0; continue; @@ -248,11 +252,13 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) { std::scoped_lock lk(mapping_guard); tracked_entries.CommitRegion(start_page_d, start_page_d + num_pages); // should already be committed, but just in case + CutContinuityAt(start_page_d); for (size_t i = 0; i < num_pages; i++) { auto& entry = tracked_entries.GetUnchecked(start_page_d + i); auto phys_addr = entry.compressed_physical_ptr; entry.compressed_physical_ptr = 0; entry.cpu_backing_address = 0; + entry.continuity_tracker = 0; if (phys_addr != 0) [[likely]] { u32& base_dev = compressed_device_addr.GetAndFault(phys_addr - 1U); if ((base_dev >> MULTI_FLAG_BITS) == 0) [[likely]] { @@ -270,6 +276,21 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) { } translation_generation.fetch_add(1, std::memory_order_release); } + +template <typename Traits> +void DeviceMemoryManager<Traits>::CutContinuityAt(size_t page) { + // Runs counted from the pages before `page` end there: the pages from `page` on are being + // mapped again or unmapped. A run is counted down page by page (TrackContinuityImpl), so the + // first page whose run already ends before `page` ends the walk. + for (size_t index = page; index-- > 0;) { + const size_t last_count = page - 1 - index; + if (tracked_entries[index].continuity_tracker <= last_count) { + break; + } + tracked_entries.GetUnchecked(index).continuity_tracker = static_cast<u32>(last_count); + } +} + template <typename Traits> void DeviceMemoryManager<Traits>::TrackContinuityImpl(DAddr address, VAddr virtual_address, size_t size, Asid asid) { @@ -671,4 +692,36 @@ void DeviceMemoryManager<Traits>::UpdatePagesCachedBatch(std::span<const std::pa } } +template <typename Traits> +void DeviceMemoryManager<Traits>::ArmReadTraps(DAddr addr, size_t size) { + const auto MarkRegionReadTrap = &DeviceMemoryManager<Traits>::DeviceMethods::MarkRegionReadTrap; + Memory::Memory* run_inter = nullptr; + u64 run_begin = 0; + u64 run_pages = 0; + const auto release_run = [&] { + if (run_pages > 0 && run_inter != nullptr) { + MarkRegionReadTrap(run_inter, run_begin << Memory::YUZU_PAGEBITS, + run_pages << Memory::YUZU_PAGEBITS); + } + run_pages = 0; + }; + const size_t page_end = Common::DivCeil(addr + size, Memory::YUZU_PAGESIZE); + for (size_t page = addr >> Memory::YUZU_PAGEBITS; page != page_end; ++page) { + auto [asid, vaddr] = ExtractCPUBacking(page); + const u64 vpage = vaddr >> Memory::YUZU_PAGEBITS; + if (vpage == 0) { + release_run(); + continue; + } + auto* const inter = registered_processes[asid.id]; + if (inter != run_inter || vpage != run_begin + run_pages) { + release_run(); + run_inter = inter; + run_begin = vpage; + } + ++run_pages; + } + release_run(); +} + } // namespace Core diff --git a/src/core/hle/kernel/k_address_space_info.cpp b/src/core/hle/kernel/k_address_space_info.cpp index 23258071e1..13a669cff5 100644 --- a/src/core/hle/kernel/k_address_space_info.cpp +++ b/src/core/hle/kernel/k_address_space_info.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -32,7 +35,8 @@ constexpr std::array<KAddressSpaceInfo, 13> AddressSpaceInfos{{ { .bit_width = 39, .address = 128_MiB , .size = 512_GiB - 128_MiB, .type = KAddressSpaceInfo::Type::Map39Bit, }, #endif { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::MapSmall }, - { .bit_width = 39, .address = Size_Invalid, .size = 8_GiB , .type = KAddressSpaceInfo::Type::Heap, }, + // Switch 2 titles size their heap from up to 12 GiB of memory, which doesn't fit Switch 1's 8 GiB. + { .bit_width = 39, .address = Size_Invalid, .size = 16_GiB , .type = KAddressSpaceInfo::Type::Heap, }, { .bit_width = 39, .address = Size_Invalid, .size = 64_GiB , .type = KAddressSpaceInfo::Type::Alias, }, { .bit_width = 39, .address = Size_Invalid, .size = 2_GiB , .type = KAddressSpaceInfo::Type::Stack, }, }}; diff --git a/src/core/hle/kernel/svc/svc_physical_memory.cpp b/src/core/hle/kernel/svc/svc_physical_memory.cpp index 4fde85fdaa..280731e488 100644 --- a/src/core/hle/kernel/svc/svc_physical_memory.cpp +++ b/src/core/hle/kernel/svc/svc_physical_memory.cpp @@ -4,10 +4,12 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <algorithm> #include <cstdlib> #include "core/core.h" #include "core/hle/kernel/k_process.h" +#include "core/hle/kernel/k_system_control.h" #include "core/hle/kernel/svc.h" namespace Kernel::Svc { @@ -18,7 +20,10 @@ Result SetHeapSize(Core::System& system, u64* out_address, u64 size) { // Validate size. R_UNLESS(Common::IsAligned(size, HeapSizeAlignment), ResultInvalidSize); - R_UNLESS(size < MainMemorySizeMax, ResultInvalidSize); + // Switch 2 titles size their heap from the total memory, which exceeds Switch 1's 8 GiB. + const size_t max_heap_size = + (std::max)(MainMemorySizeMax, KSystemControl::Init::GetIntendedMemorySize()); + R_UNLESS(size < max_heap_size, ResultInvalidSize); // Set the heap size. KProcessAddress address{}; diff --git a/src/core/hle/service/am/applet_manager.cpp b/src/core/hle/service/am/applet_manager.cpp index 05bcf6cc05..887c4521fe 100644 --- a/src/core/hle/service/am/applet_manager.cpp +++ b/src/core/hle/service/am/applet_manager.cpp @@ -248,11 +248,16 @@ void AppletManager::RequestExit() { } } +// Frontends call this after any settings change, so only an actual docked/handheld switch is +// passed on. void AppletManager::OperationModeChanged() { std::scoped_lock lk{m_lock}; - if (m_window_system) { - m_window_system->OnOperationModeChanged(); + const bool docked = Settings::IsDockedMode(); + if (!m_window_system || docked == m_docked) { + return; } + m_docked = docked; + m_window_system->OnOperationModeChanged(); } void AppletManager::SetWindowSystem(WindowSystem* window_system) { @@ -262,6 +267,7 @@ void AppletManager::SetWindowSystem(WindowSystem* window_system) { if (!m_window_system) { return; } + m_docked = Settings::IsDockedMode(); m_cv.wait(lk, [&] { return m_pending_process != nullptr; }); diff --git a/src/core/hle/service/am/applet_manager.h b/src/core/hle/service/am/applet_manager.h index cd3035de31..bae3ab4269 100644 --- a/src/core/hle/service/am/applet_manager.h +++ b/src/core/hle/service/am/applet_manager.h @@ -58,6 +58,7 @@ private: std::condition_variable m_cv; WindowSystem* m_window_system{}; + bool m_docked{}; // console mode the applets were last told about FrontendAppletParameters m_pending_parameters{}; std::unique_ptr<Process> m_pending_process{}; diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp index fe07d72912..db42781e18 100644 --- a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp +++ b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp @@ -213,6 +213,15 @@ void SoftwareKeyboard::InitializeForeground() { std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), sizeof(SwkbdConfigNew)); break; + case SwkbdAppletVersion::Version1310736: + // The common and 8.0.0 parts keep their layout; the 0x68 bytes after them are unknown. + // This revision exchanges UTF-8 text: the game's text check replies are UTF-8, and a + // UTF-16 result reached it as its first character only. + ASSERT(swkbd_config_data.size() >= sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigNew)); + std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon), + sizeof(SwkbdConfigNew)); + swkbd_config_common.use_utf8 = true; + break; default: UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version, swkbd_config_data.size()); diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h index 6bb8f437d3..f263a704cd 100644 --- a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h +++ b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h @@ -30,6 +30,7 @@ enum class SwkbdAppletVersion : u32_le { Version327689 = 0x50009, // 5.0.0 - 5.1.0 Version393227 = 0x6000B, // 6.0.0 - 7.0.1 Version524301 = 0x8000D, // 8.0.0+ + Version1310736 = 0x140010, // Switch 2 (Pokemon Pokopia): 0x530-byte config, UTF-8 text }; enum class SwkbdType : u32 { diff --git a/src/core/hle/service/am/frontend/applets.cpp b/src/core/hle/service/am/frontend/applets.cpp index c59e4b9c6b..c1a0a1dd29 100644 --- a/src/core/hle/service/am/frontend/applets.cpp +++ b/src/core/hle/service/am/frontend/applets.cpp @@ -253,7 +253,7 @@ std::shared_ptr<FrontendApplet> FrontendAppletHolder::GetApplet(std::shared_ptr< case AppletId::None: return nullptr; default: - UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:02X} program_id={:016X}! Using stub applet.", static_cast<u8>(id), applet->program_id); + UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:#x} program_id={:016X}! Using stub applet.", static_cast<u32>(id), applet->program_id); return std::make_shared<StubApplet>(system, applet, id, mode); } } diff --git a/src/core/hle/service/am/lifecycle_manager.cpp b/src/core/hle/service/am/lifecycle_manager.cpp index 4b003c4696..14c6179949 100644 --- a/src/core/hle/service/am/lifecycle_manager.cpp +++ b/src/core/hle/service/am/lifecycle_manager.cpp @@ -12,7 +12,8 @@ namespace Service::AM { LifecycleManager::LifecycleManager(Core::System& system, KernelHelpers::ServiceContext& context, bool is_application) : m_system_event(context), m_operation_mode_changed_system_event(context), - m_hdcp_state_changed_event(context), m_is_application(is_application) {} + m_operation_mode_refresh_event(context), m_hdcp_state_changed_event(context), + m_is_application(is_application) {} LifecycleManager::~LifecycleManager() = default; @@ -24,6 +25,10 @@ Event& LifecycleManager::GetOperationModeChangedSystemEvent() { return m_operation_mode_changed_system_event; } +Event& LifecycleManager::GetOperationModeRefreshEvent() { + return m_operation_mode_refresh_event; +} + Event& LifecycleManager::GetHDCPStateChangedEvent() { return m_hdcp_state_changed_event; } @@ -152,6 +157,7 @@ void LifecycleManager::OnOperationAndPerformanceModeChanged(Kernel::KernelCore& m_has_performance_mode_changed = true; } m_operation_mode_changed_system_event.Signal(kernel); + m_operation_mode_refresh_event.Signal(kernel); this->SignalSystemEventIfNeeded(kernel); } diff --git a/src/core/hle/service/am/lifecycle_manager.h b/src/core/hle/service/am/lifecycle_manager.h index 15657a36d8..097ad00776 100644 --- a/src/core/hle/service/am/lifecycle_manager.h +++ b/src/core/hle/service/am/lifecycle_manager.h @@ -45,6 +45,7 @@ public: public: Event& GetSystemEvent(); Event& GetOperationModeChangedSystemEvent(); + Event& GetOperationModeRefreshEvent(); Event& GetHDCPStateChangedEvent(); public: @@ -158,6 +159,7 @@ private: private: Event m_system_event; Event m_operation_mode_changed_system_event; + Event m_operation_mode_refresh_event; Event m_hdcp_state_changed_event; std::list<AppletMessage> m_unordered_messages{}; diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp index e495537130..37dfbfa344 100644 --- a/src/core/hle/service/am/service/application_functions.cpp +++ b/src/core/hle/service/am/service/application_functions.cpp @@ -15,6 +15,7 @@ #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/savedata_factory.h" +#include "core/hle/kernel/k_process.h" #include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/result.h" #include "core/hle/service/am/am_results.h" @@ -116,7 +117,7 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_ {181, nullptr, "UpgradeLaunchRequiredVersion"}, {190, nullptr, "SendServerMaintenanceOverlayNotification"}, {200, nullptr, "GetLastApplicationExitReason"}, - {210, D<&IApplicationFunctions::GetUnknownEvent210>, "Unknown210"}, + {210, D<&IApplicationFunctions::GetOperationModeRefreshEvent>, "GetOperationModeRefreshEvent (guessed)"}, {220, nullptr, "Unknown220"}, // [20.0.0+] {300, nullptr, "CreateMovieWriter"}, // [19.0.0+] {310, nullptr, "Unknown310"}, // [20.0.0+] @@ -555,10 +556,18 @@ Result IApplicationFunctions::GetHealthWarningDisappearedSystemEvent( R_SUCCEED(); } -Result IApplicationFunctions::GetUnknownEvent210( +// The Switch 2 nn::oe::GetOperationMode caches the mode and only asks ICommonStateGetter again +// once this event is signaled, so it has to fire on every docked/handheld change. Switch 1 titles +// keep the event that never fires, as its use there is unknown. +Result IApplicationFunctions::GetOperationModeRefreshEvent( OutCopyHandle<Kernel::KReadableEvent> out_event) { LOG_DEBUG(Service_AM, "called"); - *out_event = m_applet->unknown_event.GetHandle(); + const auto* process = m_applet->process ? m_applet->process->GetHandle() : nullptr; + if (process != nullptr && process->IsSwitch2()) { + *out_event = m_applet->lifecycle_manager.GetOperationModeRefreshEvent().GetHandle(); + } else { + *out_event = m_applet->unknown_event.GetHandle(); + } R_SUCCEED(); } diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h index 101ee0c970..44b5fadff0 100644 --- a/src/core/hle/service/am/service/application_functions.h +++ b/src/core/hle/service/am/service/application_functions.h @@ -82,7 +82,7 @@ private: Result GetNotificationStorageChannelEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result CreateApplicationAndRequestToStart(u64 application_id); - Result GetUnknownEvent210(OutCopyHandle<Kernel::KReadableEvent> out_event); + Result GetOperationModeRefreshEvent(OutCopyHandle<Kernel::KReadableEvent> out_event); Result Unknown320(); Result Unknown321(); Result Unknown330(Out<u8> out); diff --git a/src/core/hle/service/am/service/library_applet_creator.cpp b/src/core/hle/service/am/service/library_applet_creator.cpp index 4ee92908b5..afdb609c00 100644 --- a/src/core/hle/service/am/service/library_applet_creator.cpp +++ b/src/core/hle/service/am/service/library_applet_creator.cpp @@ -4,6 +4,9 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <mutex> +#include <unordered_set> + #include "common/settings.h" #include "core/hle/kernel/k_transfer_memory.h" #include "core/hle/service/am/applet_data_broker.h" @@ -99,6 +102,41 @@ AppletProgramId AppletIdToProgramId(AppletId applet_id) { } } +AppletId NormalizeAppletId(AppletId id) { + const u32 raw_id = static_cast<u32>(id); + static std::mutex mutex; + static std::unordered_set<u32> logged_raw_ids; + static std::unordered_set<u32> warned_raw_ids; + + bool log_info = false; + { + std::scoped_lock lk{mutex}; + log_info = logged_raw_ids.insert(raw_id).second; + } + if (log_info) { + LOG_INFO(Service_AM, "CreateLibraryApplet called with raw applet_id={:#x}", raw_id); + } + + if (AppletIdToProgramId(id) == static_cast<AppletProgramId>(0)) { + const auto low_byte = static_cast<AppletId>(raw_id & 0xFF); + if (AppletIdToProgramId(low_byte) != static_cast<AppletProgramId>(0)) { + bool log_warning = false; + { + std::scoped_lock lk{mutex}; + log_warning = warned_raw_ids.insert(raw_id).second; + } + if (log_warning) { + LOG_WARNING(Service_AM, + "Normalizing unknown raw applet_id={:#x} to low byte {:#x}", + raw_id, static_cast<u32>(low_byte)); + } + return low_byte; + } + } + + return id; +} + std::shared_ptr<ILibraryAppletAccessor> CreateGuestApplet(Core::System& system, WindowSystem& window_system, std::shared_ptr<Applet> caller_applet, @@ -184,6 +222,8 @@ Result ILibraryAppletCreator::CreateLibraryApplet( LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={}", applet_id, library_applet_mode); + applet_id = NormalizeAppletId(applet_id); + std::shared_ptr<ILibraryAppletAccessor> library_applet; if (ShouldCreateGuestApplet(applet_id)) { library_applet = @@ -210,6 +250,8 @@ Result ILibraryAppletCreator::CreateLibraryAppletEx( LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={} thread_id={}", applet_id, library_applet_mode, thread_id); + applet_id = NormalizeAppletId(applet_id); + std::shared_ptr<ILibraryAppletAccessor> library_applet; if (ShouldCreateGuestApplet(applet_id)) { library_applet = diff --git a/src/core/hle/service/audio/nx2_audio_user_client.h b/src/core/hle/service/audio/nx2_audio_user_client.h index 2d1a158c84..f053ad62af 100644 --- a/src/core/hle/service/audio/nx2_audio_user_client.h +++ b/src/core/hle/service/audio/nx2_audio_user_client.h @@ -14,14 +14,16 @@ namespace Service::Audio { // Switch 2 "aud:u" (nn::audio::CreateUserClientSessionByHipc). The SDK reaches the Switch 1 // audio interfaces through it: command 2 returns an object used exactly like // IAudioRendererManager (vtable slot 1 = GetWorkBufferSize with a Switch 1 parameter block), so -// it is backed by Eden's audren:u implementation. The nx2: name keeps unknown commands -// auto-stubbed and logged. +// it is backed by Eden's audren:u implementation. Command 1 is nn::audio::OpenAudioOut's +// manager, called like IAudioOutManager (slot 1 = OpenAudioOut), backed by audout:u. The nx2: name +// keeps unknown commands auto-stubbed and logged. class INx2AudioUserClient final : public ServiceFramework<INx2AudioUserClient> { public: explicit INx2AudioUserClient(Core::System& system_) : ServiceFramework{system_, "nx2:aud:u"} { // clang-format off static const FunctionInfo functions[] = { + {1, &INx2AudioUserClient::GetAudioOutManager, "GetAudioOutManager (guessed)"}, {2, &INx2AudioUserClient::GetAudioRendererManager, "GetAudioRendererManager (guessed)"}, }; // clang-format on @@ -29,6 +31,16 @@ public: } private: + void GetAudioOutManager(HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const u32 arg = rp.Pop<u32>(); + LOG_WARNING(Service_Audio, "(STUBBED) Switch 2 aud:u 1 arg={}, returning IAudioOutManager", + arg); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<IAudioOutManager>(ctx, system); + } + void GetAudioRendererManager(HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const u32 arg = rp.Pop<u32>(); diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index c4119b2ab1..e66c72cd26 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -69,6 +69,9 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i }, [this](u64 gpu_address, u64 cpu_address, u64 size) { Nx2BackSparse(gpu_address, cpu_address, size); + }, + [this](u64 gpu_address, u64 cpu_address, u64 size) { + Nx2SparseTile(gpu_address, cpu_address, size); }); } return result; @@ -243,6 +246,32 @@ void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) { session->asid, true); } +void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lock(mutex); + auto it = nx2_sparse_ranges.upper_bound(gpu_address); + auto* session = container.GetSession(nx2_session); + if (it == nx2_sparse_ranges.begin() || session == nullptr) { + return; + } + --it; + const auto& [start, range] = *it; + const auto& [region_size, device_address] = range; + if (gpu_address + size > start + region_size) { + return; + } + // In GPU-thread order: command lists submitted before the request still see the old tile. + // Unmapping first keeps the SMMU's reverse map right and invalidates the caches over the tile, + // whose contents are now what the game's memory holds there. + const DAddr device = device_address + (gpu_address - start); + auto& smmu = system.Host1x().MemoryManager(); + system.GPU().RunInOrder([&smmu, device, cpu_address, size, asid = session->asid] { + smmu.Unmap(device, size); + if (cpu_address != 0) { + smmu.Map(device, cpu_address, size, asid, true); + } + }); +} + void nvhost_as_gpu::OnClose(DeviceFD fd) {} // Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h index 69050bb677..39b63ea369 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h @@ -246,6 +246,8 @@ private: // blocks get memory behind their device pages as the GPU writes them. void Nx2Sparse(u64 gpu_address, u64 size, bool create); void Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size); + // A tile of a sparse region (nx2:gpu 216): its device pages show process memory, or nothing. + void Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size); std::map<u64, std::pair<u64, DAddr>> nx2_sparse_ranges; ///< GPU address -> (size, device) }; diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp index 5db984b53d..5998940afa 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp @@ -6,6 +6,7 @@ #include <array> #include <algorithm> +#include <cstdlib> #include <cstring> #include "common/assert.h" #include "common/logging.h" @@ -207,6 +208,16 @@ void ApplyNx2Characteristics(GC& gc) { // Without the flag NvRm fails that call and the texture stays at GPU address 0 (title 1's // movie and the passes rendered into compressible pools never showed). gc.flags |= 1ULL << 34; + // NVGPU_GPU_FLAGS_SUPPORT_REMAP: without it NvRm fails every remap (nvnMemoryPoolMapVirtual) + // before asking the server, so title 1 never maps or unmaps its sparse texture tiles (nx2:gpu + // 216) and streams its terrain into unmapped sparse memory. EDEN_NX2_SPARSE_REMAP=0 leaves it out. + static const bool remap = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_REMAP"); + return value == nullptr || value[0] != '0'; + }(); + if (remap) { + gc.flags |= 1ULL << 35; + } // The Switch 2 layout keeps nine s16 interface versions at 0x70..0x80 (like nvgpu's // *_ioctl_nr_last); its NvRm requires e.g. [0x78] >= 6 and enables features above 0xa, 0x1f diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h index 989bf61682..869e17caf6 100644 --- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h +++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h @@ -44,6 +44,9 @@ public: using SparseListener = std::function<void(u64 gpu_address, u64 size, bool create)>; /// A block of a sparse region gets memory [cpu_address, +size). using BackListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; + /// A tile of a sparse region shows process memory [cpu_address, +size), or reads zero again + /// (cpu_address 0). + using TileListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>; static Nx2GpuMemoryRegistry& Get() { static Nx2GpuMemoryRegistry registry; @@ -120,9 +123,22 @@ public: } } + // nx2:gpu 216 (sparse map requests): the tile [gpu_address, +size) of a sparse region now + // shows the process memory at cpu_address, the game's own memory (sub 101), or with + // cpu_address 0 it is unmapped and reads zero. The GPU mapping stays as it is; only the + // region's device pages change, in GPU-thread order. + void MapSparseTile(u64 gpu_address, u64 cpu_address, u64 size) { + std::scoped_lock lk{mutex}; + for (auto& [owner, listener] : listeners) { + if (listener.tile) { + listener.tile(gpu_address, cpu_address, size); + } + } + } + void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener, ReleaseListener release_listener = {}, SparseListener sparse_listener = {}, - BackListener back_listener = {}) { + BackListener back_listener = {}, TileListener tile_listener = {}) { std::scoped_lock lk{mutex}; for (const auto& m : mappings) { listener(m.gpu_address, m.cpu_address, m.size); @@ -135,7 +151,8 @@ public: listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener), std::move(release_listener), std::move(sparse_listener), - std::move(back_listener)}); + std::move(back_listener), + std::move(tile_listener)}); } void RemoveListener(const void* owner) { @@ -192,6 +209,7 @@ private: ReleaseListener release; SparseListener sparse; BackListener back; + TileListener tile; }; // Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it. diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp index 2be24e3f57..12923496fb 100644 --- a/src/core/hle/service/nvdrv/nvdrv.cpp +++ b/src/core/hle/service/nvdrv/nvdrv.cpp @@ -132,6 +132,7 @@ Module::Module(Core::System& system) // (their table filled up, so the new game's GPU waits never completed) and the Switch 2 flags. Devices::Nx2GpuMemoryRegistry::Get().Reset(); Tegra::Nx2ClearSemaphoreRegions(); + Tegra::Nx2ClearSparseRegions(); Devices::g_nx2_driver = false; Tegra::g_nx2_ampere = false; Core::g_nx2_present_count = 0; diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h index 9d00b81368..628bdeda5b 100644 --- a/src/core/hle/service/nx2_gsv.h +++ b/src/core/hle/service/nx2_gsv.h @@ -315,6 +315,7 @@ public: {213, &INx2GpuSession::SubUnmapJob, "NormalUnmapGpuMemory4K"}, {214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"}, {215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"}, + {216, &INx2GpuSession::SubSparseMap, "ExecuteSparseMapRequests"}, }); if (LazySparseRegions()) { Tegra::Nx2SetSparseBacking( @@ -542,6 +543,9 @@ private: const u64 size = last + 1 - start; std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); + if ((w[7] & SparseRegionFlag) == 0) { + m_gpu_only_ranges[start] = size; // kept out of 216, however it is backed + } if ((w[7] & SparseRegionFlag) != 0 && LazySparseRegions()) { // Title 1's terrain texture arrays and layer pools (170 MB-5.8 GB, mostly empty): // backed whole, they ran the pool dry and the later ones got no memory at all. @@ -579,8 +583,17 @@ private: first, last, w[1], w[2], w[7], w[8]); std::scoped_lock lk{m_region_mutex}; FreeReleasedRegions(); + if (m_gpu_only_ranges.erase(first) != 0 && last > first) { + DropGpuOnlyTiles(first, last - first + 1, false); + m_shared_regions.erase(first); + std::erase_if(m_shared_memory, [first](const auto& entry) { return entry.second == first; }); + } if (last > first) { - if (m_sparse_regions.erase(first) != 0) { + if (const auto region = m_sparse_regions.find(first); region != m_sparse_regions.end()) { + m_tile_bytes -= Core::Memory::YUZU_PAGESIZE * + static_cast<u64>(std::count(region->second.tile_pages.begin(), + region->second.tile_pages.end(), true)); + m_sparse_regions.erase(region); Tegra::Nx2SparseRegionDestroyed(first); Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1); } else { @@ -609,6 +622,49 @@ private: rb.Push(ResultSuccess); } + // Sub 216 (nn::gpu::sdk::ExecuteSparseMapRequests, behind nvnMemoryPoolMapVirtual and NvRm's + // remap): an A buffer of SparseMapRequests and a B buffer that gets the kind applied to each; + // word 8-9 is 1, 10 the address space, 11-12 the request size (40), 13 a sync flag, 14 the job + // slot. The reply is the number of requests done. This is how the game commits and releases + // its sparse texture tiles: a map points a tile at its own memory (sub 101), an unmap makes it + // read zero again. A region the game maps tiles into needs no memory of its own. + void SubSparseMap(HLERequestContext& ctx) { + const auto* w = ctx.CommandBuffer(); + const u32 slot = w[14]; + const auto data = ctx.BufferDescriptorA().empty() ? std::span<const u8>{} : ctx.ReadBufferA(0); + const size_t count = data.size() / sizeof(SparseMapRequest); + std::vector<u8> kinds(count); + { + std::scoped_lock lk{m_region_mutex}; + FreeReleasedRegions(); + ++m_tile_calls; + for (size_t i = 0; i < count; ++i) { + SparseMapRequest request; + std::memcpy(&request, data.data() + i * sizeof(SparseMapRequest), sizeof(request)); + kinds[i] = request.kind; + ApplySparseMapRequest(request); + } + // The first request, then at most every 30 s. + const auto now = std::chrono::steady_clock::now(); + if (count != 0 && (m_tile_requests == count || + now - m_tile_log_time >= std::chrono::seconds{30})) { + m_tile_log_time = now; + LOG_INFO(Service, "nx2:gpu 216: {} calls, {} maps, {} unmaps; {} MiB of sparse tiles " + "and {} MiB in GPU-only regions mapped, {} maps into regions that " + "keep their own memory left out; {} MiB of sparse blocks backed, {} " + "MiB released", + m_tile_calls, m_tile_maps, m_tile_unmaps, m_tile_bytes >> 20, + m_gpu_only_tile_bytes >> 20, m_tile_kept, m_sparse_backed_bytes >> 20, + m_sparse_released_bytes >> 20); + } + } + if (!ctx.BufferDescriptorB().empty() && count != 0) { + ctx.WriteBufferB(kinds.data(), kinds.size(), 0); + } + SignalAfterGpu(slot); + TipcReplyWords(ctx, {static_cast<u32>(count), static_cast<u32>(u64{count} >> 32)}); + } + // Returns the pages of destroyed regions the GPU thread is done with to the kernel. void FreeReleasedRegions() { auto* const process = system.ApplicationProcess(); @@ -630,8 +686,17 @@ private: }); } - std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size, bool log_failure = true, - bool clear = false) { + struct MappedBacking { + u64 address; + u64 size; + std::unique_ptr<Kernel::KPageGroup> pages; + }; + + // Allocates kernel memory and maps it in the alias code region. Touches no region state, so + // it needs no lock: the GPU back thread calls it without m_region_mutex, because mapping can + // wait on the guest page table lock and a guest thread holding that lock must not be stalled + // behind a host mutex the service thread is waiting for. + std::optional<MappedBacking> AllocateGpuOnlyBacking(u64 size, bool log_failure, bool clear) { using Core::Memory::YUZU_PAGESIZE; auto& kernel_core = system.Kernel(); auto* const process = system.ApplicationProcess(); @@ -667,18 +732,16 @@ private: // From where the last backing went (sparse regions back hundreds of blocks), then wrapping. const u64 slots = region_size / Alignment; for (u64 i = 0; i < slots; ++i) { - const u64 offset = ((m_alias_hint + i) % slots) * Alignment; + const u64 offset = ((m_alias_hint.load(std::memory_order_relaxed) + i) % slots) * Alignment; if (offset + size > region_size) { continue; } const Common::ProcessAddress address{region_start + offset}; if (R_SUCCEEDED(page_table.MapPageGroup(address, *pg, Kernel::KMemoryState::IoMemory, Kernel::KMemoryPermission::UserReadWrite))) { - m_alias_hint = (offset + Common::AlignUp(size, Alignment)) / Alignment; - m_gpu_only_regions[gpu_address] = {GetInteger(address), size, std::move(pg)}; - m_gpu_only_bytes += size; - m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); - return GetInteger(address); + m_alias_hint.store((offset + Common::AlignUp(size, Alignment)) / Alignment, + std::memory_order_relaxed); + return MappedBacking{GetInteger(address), size, std::move(pg)}; } } LOG_ERROR(Service, "nx2:gpu 214 no room for {:#x} bytes in the alias code region " @@ -687,32 +750,93 @@ private: return std::nullopt; } + // Records a mapped backing for the region at gpu_address. Needs m_region_mutex. + void RegisterGpuOnlyBacking(u64 gpu_address, MappedBacking&& backing) { + m_gpu_only_regions[gpu_address] = {backing.address, backing.size, std::move(backing.pages)}; + m_gpu_only_bytes += backing.size; + m_gpu_only_peak = (std::max)(m_gpu_only_peak, m_gpu_only_bytes); + } + + // Needs m_region_mutex. + std::optional<u64> BackGpuOnlyRegion(u64 gpu_address, u64 size) { + auto backing = AllocateGpuOnlyBacking(size, true, false); + if (!backing) { + return std::nullopt; + } + const u64 address = backing->address; + RegisterGpuOnlyBacking(gpu_address, std::move(*backing)); + return address; + } + // Runs before a GPU-side write that may land in a sparse region (Tegra::Nx2BackSparseWrite, // usually on the GPU thread): the blocks the write touches get memory, cleared, behind the // region's device pages. The GPU mapping stays as it is, so the images over the region keep // their storage and only see the write. void BackSparseWrite(u64 gpu_address, u64 size) { - std::scoped_lock lk{m_region_mutex}; + struct Pending { + u64 region; + u64 block; + }; + std::vector<Pending> todo; const u64 end = gpu_address + size; - auto it = m_sparse_regions.upper_bound(gpu_address); - if (it != m_sparse_regions.begin()) { - --it; - } - for (; it != m_sparse_regions.end() && it->first < end; ++it) { - auto& [start, region] = *it; - if (start + region.size <= gpu_address) { - continue; + // Calls fn(region_start, region, first_block, last_block) for each region the write touches. + auto for_regions = [&](auto&& fn) { + auto it = m_sparse_regions.upper_bound(gpu_address); + if (it != m_sparse_regions.begin()) { + --it; } - const u64 first_block = ((std::max)(gpu_address, start) - start) / SparseBlockSize; - const u64 last_block = ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize; - for (u64 block = first_block; block <= last_block; ++block) { - if (region.blocks[block] != BlockEmpty) { + for (; it != m_sparse_regions.end() && it->first < end; ++it) { + auto& [start, region] = *it; + if (start + region.size <= gpu_address) { continue; } - const u64 block_address = start + block * SparseBlockSize; - const u64 block_size = (std::min)(SparseBlockSize, region.size - block * SparseBlockSize); - const auto address = BackGpuOnlyRegion(block_address, block_size, false, true); - if (!address) { + fn(start, region, ((std::max)(gpu_address, start) - start) / SparseBlockSize, + ((std::min)(end, start + region.size) - 1 - start) / SparseBlockSize); + } + }; + bool waits_for_other = false; + { + std::unique_lock lk{m_region_mutex}; + for_regions([&](u64 start, SparseRegion& region, u64 first, u64 last) { + for (u64 block = first; block <= last; ++block) { + if (region.blocks[block] == BlockEmpty && region.tiles) { + // The write lands in the game's own memory (a tile it mapped) or nowhere; + // the texture cache only learns that the block has contents now. + region.blocks[block] = BlockWritten; + Tegra::Nx2SparseBlockWritten(start, block); + } else if (region.blocks[block] == BlockEmpty) { + region.blocks[block] = BlockPending; + todo.push_back({start, block}); + } else if (region.blocks[block] == BlockPending) { + waits_for_other = true; + } + } + }); + } + // Allocating and mapping happens outside the lock (see AllocateGpuOnlyBacking). + for (const auto& [start, block] : todo) { + u64 block_size = SparseBlockSize; + { + std::scoped_lock lk{m_region_mutex}; + const auto it = m_sparse_regions.find(start); + if (it != m_sparse_regions.end()) { + block_size = (std::min)(SparseBlockSize, it->second.size - block * SparseBlockSize); + } + } + auto backing = AllocateGpuOnlyBacking(block_size, false, true); + bool stale = false; + { + std::scoped_lock lk{m_region_mutex}; + const auto it = m_sparse_regions.find(start); + if (it == m_sparse_regions.end() || block >= it->second.blocks.size()) { + stale = true; // the region was destroyed while its block was being backed + } else if (it->second.tiles) { + // The game started mapping its tiles there meanwhile (216): no block. + stale = true; + it->second.blocks[block] = BlockWritten; + Tegra::Nx2SparseBlockWritten(start, block); + } else if (!backing) { + auto& region = it->second; region.blocks[block] = BlockFailed; // out of memory: not again for every write if (!region.warned) { region.warned = true; @@ -720,21 +844,47 @@ private: "to its unbacked blocks are dropped", start); } - continue; + } else { + const u64 block_address = start + block * SparseBlockSize; + const u64 address = backing->address; + it->second.blocks[block] = BlockBacked; + RegisterGpuOnlyBacking(block_address, std::move(*backing)); + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, address, + block_size); + Tegra::Nx2SparseBlockWritten(start, block); + m_sparse_backed_bytes += block_size; + if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { + LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " + "memory in use, peak {} MiB", + m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, + m_gpu_only_peak >> 20); + } } - region.blocks[block] = BlockBacked; - Nvidia::Devices::Nx2GpuMemoryRegistry::Get().BackSparse(block_address, *address, - block_size); - Tegra::Nx2SparseBlockWritten(start, block); - m_sparse_backed_bytes += block_size; - if (++m_sparse_blocks == 1 || (m_sparse_blocks % 64) == 0) { - LOG_INFO(Service, "nx2:gpu {} sparse blocks backed ({} MiB); {} MiB of GPU-only " - "memory in use, peak {} MiB", - m_sparse_blocks, m_sparse_backed_bytes >> 20, m_gpu_only_bytes >> 20, - m_gpu_only_peak >> 20); + } + m_region_cv.notify_all(); + if (stale && backing) { + // Never handed to the GPU: unmap and free right away. + if (auto* const process = system.ApplicationProcess(); process != nullptr) { + process->GetPageTable().UnmapPageGroup(Common::ProcessAddress{backing->address}, + *backing->pages, + Kernel::KMemoryState::IoMemory); } + backing->pages->Close(system.Kernel()); } } + if (waits_for_other) { + // Another thread is backing a block this write needs: wait until it is done. + std::unique_lock lk{m_region_mutex}; + m_region_cv.wait(lk, [&] { + bool pending = false; + for_regions([&](u64, SparseRegion& region, u64 first, u64 last) { + for (u64 block = first; block <= last; ++block) { + pending |= region.blocks[block] == BlockPending; + } + }); + return !pending; + }); + } } // EDEN_NX2_SPARSE_LAZY=0 backs sparse regions whole, as before. @@ -751,19 +901,314 @@ private: static constexpr u8 BlockEmpty = 0; static constexpr u8 BlockBacked = 1; static constexpr u8 BlockFailed = 2; + static constexpr u8 BlockPending = 3; ///< Being allocated and mapped by BackSparseWrite + static constexpr u8 BlockWritten = 4; ///< Written in a region the game maps tiles into struct SparseRegion { u64 size; - std::vector<u8> blocks; ///< BlockEmpty / BlockBacked / BlockFailed per SparseBlockSize + std::vector<u8> blocks; ///< BlockEmpty / Backed / Failed / Pending / Written per block bool warned{}; ///< Logged that the pool ran out for its blocks + bool tiles{}; ///< The game maps its own memory into it (216): no blocks + std::vector<bool> tile_pages; ///< Pages a tile maps, once `tiles` }; std::map<u64, SparseRegion> m_sparse_regions; ///< By GPU address u64 m_sparse_blocks{}; ///< Sparse blocks backed so far u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far + u64 m_sparse_released_bytes{}; ///< Blocks given back once the game mapped tiles there + + // nn::gpu::sdk::SparseMapRequest (216), filled from NvRm's nvgpu_as_remap_op. + struct SparseMapRequest { + u64 gpu_address; + u64 size; + u8 no_write; + u8 cacheable; + u8 unknown_12; + u8 kind; ///< Wanted: the compressible kind if there is one + u8 fallback_kind; ///< The incompressible kind + u8 unknown_15; ///< Remap op flag bit 15 + u16 padding_16; + u32 memory_handle; ///< Sub 101's handle; 0 unmaps the range + u32 padding_1c; + u64 offset; ///< Into that memory + }; + static_assert(sizeof(SparseMapRequest) == 40); + u64 m_tile_calls{}; + u64 m_tile_requests{}; + std::chrono::steady_clock::time_point m_tile_log_time{}; + u64 m_tile_maps{}; + u64 m_tile_unmaps{}; + u64 m_tile_kept{}; ///< Maps into GPU-only regions that keep their own memory, left out + u64 m_tile_bytes{}; ///< Sparse region pages a tile maps right now + + void ApplySparseMapRequest(const SparseMapRequest& request) { + ++m_tile_requests; + if (request.size == 0) { + return; + } + const u64 gpu_address = request.gpu_address; + const u64 size = request.size; + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + auto it = m_sparse_regions.upper_bound(gpu_address); + SparseRegion* region = nullptr; + u64 start = 0; + if (it != m_sparse_regions.begin()) { + --it; + if (gpu_address + size <= it->first + it->second.size) { + start = it->first; + region = &it->second; + } + } + if (const auto gpu_only = GpuOnlyRegionOf(gpu_address, size)) { + ApplyGpuOnlyRequest(*gpu_only, request); + return; + } + if (request.memory_handle == 0) { + ++m_tile_unmaps; + LOG_DEBUG(Service, "nx2:gpu 216 unmap [{:#x}, +{:#x}]", gpu_address, size); + if (region != nullptr) { + Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, false); + registry.MapSparseTile(gpu_address, 0, size); + MarkTiles(*region, gpu_address - start, size, false); + } else { + registry.Unmap(gpu_address, size); + } + return; + } + ++m_tile_maps; + const auto memory = m_memory.find(request.memory_handle); + if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) { + LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}", + gpu_address, size, request.memory_handle, request.offset); + return; + } + const u64 cpu_address = memory->second.address + request.offset; + LOG_DEBUG(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] -> memory {} offset {:#x} kind {:#x}", + gpu_address, size, request.memory_handle, request.offset, request.kind); + if (region == nullptr) { + registry.MapAt(gpu_address, cpu_address, size); + return; + } + if (!region->tiles) { + StartTiles(start, *region); + } + // The game's memory already holds what the tile shows (title 1 fills nine world-sized + // masks, R8/RG16/R16 at 8192-16384 texels, in its own memory before mapping them): the + // blocks count as written, so the texture cache gives the images over them memory and + // loads them. Left unwritten, those images read zero and the static world was unlit. + const u64 first_block = (gpu_address - start) / SparseBlockSize; + const u64 last_block = (gpu_address + size - 1 - start) / SparseBlockSize; + for (u64 block = first_block; block <= last_block; ++block) { + if (region->blocks[block] == BlockEmpty) { + region->blocks[block] = BlockWritten; + } + Tegra::Nx2SparseBlockWritten(start, block); + } + Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, true); + registry.MapSparseTile(gpu_address, cpu_address, size); + MarkTiles(*region, gpu_address - start, size, true); + } + + // Start of the region 214 made without the sparse flag that holds the range (by its flags, + // not by how it is backed). + std::optional<u64> GpuOnlyRegionOf(u64 gpu_address, u64 size) const { + auto it = m_gpu_only_ranges.upper_bound(gpu_address); + if (it == m_gpu_only_ranges.begin()) { + return std::nullopt; + } + --it; + if (gpu_address + size > it->first + it->second) { + return std::nullopt; + } + return it->first; + } + + // 216 into a GPU-only region: render targets, and with remap support also some texture arrays + // (title 1: 5.8 GB and 1.4 GB). The game backs those with its own memory too, which it fills + // through its own address, so a map points the range at that memory the way 211 does: both + // addresses reach the same device pages and the texture cache sees the writes. (Re-pointing the + // region's own device pages instead, as for sparse tiles, left the arrays stale: black terrain.) + // Left alone: + // - Regions whose memory the game shares with another GPU-only region (render targets): the + // texture cache would flush and re-upload such overlapping render targets every frame. All of + // a group keep the memory 214 gave them. + // - Regions 214 backs as they are written: the texture cache reads an image's memory in full + // when it uploads it, which makes the game's pages behind it resident on the host, while + // unwritten blocks read as zero for free (title 1: ~1 GB less resident at 150 s). + void ApplyGpuOnlyRequest(u64 region, const SparseMapRequest& request) { + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + const u64 gpu_address = request.gpu_address; + const u64 size = request.size; + if (request.memory_handle == 0) { + ++m_tile_unmaps; + DropGpuOnlyTiles(gpu_address, size, true); + return; + } + ++m_tile_maps; + if (m_sparse_regions.contains(region)) { + ++m_tile_kept; + return; + } + if (m_shared_regions.contains(region)) { + m_shared_memory[{request.memory_handle, request.offset}] = region; + ++m_tile_kept; + return; + } + const auto memory = m_memory.find(request.memory_handle); + if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) { + LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}", + gpu_address, size, request.memory_handle, request.offset); + return; + } + if (const auto other = RegionSharing(request.memory_handle, request.offset, size, region)) { + ++m_tile_kept; + for (const u64 shared : {region, *other}) { + if (m_shared_regions.insert(shared).second) { + const auto range = m_gpu_only_ranges.find(shared); + if (range != m_gpu_only_ranges.end()) { + DropGpuOnlyTiles(shared, range->second, true); + } + LOG_INFO(Service, "nx2:gpu GPU-only region at {:#x} shares memory with another: " + "it keeps its own", + shared); + } + } + return; + } + DropGpuOnlyTiles(gpu_address, size, false); + registry.MapAt(gpu_address, memory->second.address + request.offset, size); + m_gpu_only_tiles[gpu_address] = {size, region, request.memory_handle, request.offset}; + m_gpu_only_memory[{request.memory_handle, request.offset}] = {request.offset + size, region}; + m_gpu_only_tile_max = (std::max)(m_gpu_only_tile_max, size); + m_gpu_only_tile_bytes += size; + } + + // Another GPU-only region whose tiles hold any of [offset, +size) of the memory, if one does. + std::optional<u64> RegionSharing(u32 handle, u64 offset, u64 size, u64 region) const { + auto it = m_gpu_only_memory.lower_bound({handle, offset + size}); + while (it != m_gpu_only_memory.begin()) { + --it; + const auto& [key, value] = *it; + const auto& [end, owner] = value; + if (key.first != handle || key.second + m_gpu_only_tile_max <= offset) { + break; + } + if (end > offset && owner != region) { + return owner; + } + } + // Regions that already keep their own memory record no tiles: compare with their requests. + if (const auto kept = m_shared_memory.find({handle, offset}); kept != m_shared_memory.end() && + kept->second != region) { + return kept->second; + } + return std::nullopt; + } + + // The memory 214 gave a GPU-only region at [gpu_address, +size), when it was backed whole. + std::optional<u64> OwnBackingOf(u64 gpu_address, u64 size) const { + auto it = m_gpu_only_regions.upper_bound(gpu_address); + if (it == m_gpu_only_regions.begin()) { + return std::nullopt; + } + --it; + if (gpu_address + size > it->first + it->second.size) { + return std::nullopt; + } + return it->second.cpu_address + (gpu_address - it->first); + } + + // Forgets the game's tiles in [gpu_address, +size) of GPU-only regions; with `restore` (an + // unmap) those ranges show the region's own memory again, or nothing where it has none. + void DropGpuOnlyTiles(u64 gpu_address, u64 size, bool restore) { + auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get(); + auto it = m_gpu_only_tiles.upper_bound(gpu_address); + if (it != m_gpu_only_tiles.begin()) { + const auto previous = std::prev(it); + if (previous->first + previous->second.size > gpu_address) { + it = previous; + } + } + while (it != m_gpu_only_tiles.end() && it->first < gpu_address + size) { + const auto& [tile_address, tile] = *it; + if (restore) { + if (const auto backing = OwnBackingOf(tile_address, tile.size)) { + registry.MapAt(tile_address, *backing, tile.size); + } else { + registry.Unmap(tile_address, tile.size); + } + } + if (const auto owner = m_gpu_only_memory.find({tile.memory_handle, tile.offset}); + owner != m_gpu_only_memory.end() && owner->second.second == tile.region) { + if (m_shared_regions.contains(tile.region)) { + m_shared_memory[owner->first] = tile.region; + } + m_gpu_only_memory.erase(owner); + } + m_gpu_only_tile_bytes -= tile.size; + it = m_gpu_only_tiles.erase(it); + } + } + + // The game maps tiles into this region itself (216): writes outside its tiles are dropped, as + // on the console, so it gets no more blocks, and the blocks it got before (written while no + // tile was mapped, which the console would have dropped) go back. + void StartTiles(u64 start, SparseRegion& region) { + using Core::Memory::YUZU_PAGESIZE; + region.tiles = true; + region.tile_pages.assign(Common::DivCeil(region.size, YUZU_PAGESIZE), false); + std::shared_ptr<std::atomic<bool>> done; + u64 released = 0; + for (u64 block = 0; block < region.blocks.size(); ++block) { + if (region.blocks[block] != BlockBacked) { + continue; + } + region.blocks[block] = BlockWritten; + const u64 block_address = start + block * SparseBlockSize; + const auto backing = m_gpu_only_regions.find(block_address); + if (backing == m_gpu_only_regions.end()) { + continue; + } + Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapSparseTile(block_address, 0, + backing->second.size); + if (!done) { + done = std::make_shared<std::atomic<bool>>(false); + system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); }); + } + released += backing->second.size; + m_released_regions.push_back({std::move(backing->second), done}); + m_gpu_only_regions.erase(backing); + } + m_sparse_released_bytes += released; + LOG_INFO(Service, "nx2:gpu sparse region at {:#x}: the game maps its tiles, {} MiB of blocks " + "released", + start, released >> 20); + } + + // Counts the bytes of tiles mapped, page by page. + void MarkTiles(SparseRegion& region, u64 offset, u64 size, bool mapped) { + using Core::Memory::YUZU_PAGESIZE; + if (!region.tiles) { + return; + } + const u64 last = (std::min)(Common::DivCeil(offset + size, YUZU_PAGESIZE), + static_cast<u64>(region.tile_pages.size())); + for (u64 page = offset / YUZU_PAGESIZE; page < last; ++page) { + if (region.tile_pages[page] != mapped) { + region.tile_pages[page] = mapped; + if (mapped) { + m_tile_bytes += YUZU_PAGESIZE; + } else { + m_tile_bytes -= YUZU_PAGESIZE; + } + } + } + } + u64 m_gpu_only_bytes{}; ///< Backing of GPU-only regions and sparse blocks right now u64 m_gpu_only_peak{}; - u64 m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region + std::atomic<u64> m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region /// Regions are created and destroyed on the service thread and backed on the GPU thread. std::recursive_mutex m_region_mutex; + std::condition_variable_any m_region_cv; ///< Signalled when a pending sparse block resolves struct GpuOnlyRegion { u64 cpu_address; @@ -775,6 +1220,21 @@ private: std::shared_ptr<std::atomic<bool>> done; ///< Set by the GPU thread past the region's last use }; std::map<u64, GpuOnlyRegion> m_gpu_only_regions; ///< By GPU address + std::map<u64, u64> m_gpu_only_ranges; ///< 214 regions without the sparse flag: GPU address -> size + struct GpuOnlyTile { + u64 size; + u64 region; ///< Start of its GPU-only region + u32 memory_handle; + u64 offset; + }; + std::map<u64, GpuOnlyTile> m_gpu_only_tiles; ///< The game's memory 216 put in GPU-only regions + /// (memory handle, offset) -> (end offset, region) of those tiles, to find shared memory + std::map<std::pair<u32, u64>, std::pair<u64, u64>> m_gpu_only_memory; + /// Memory mapped into regions that keep their own (handle, offset) -> region + std::map<std::pair<u32, u64>, u64> m_shared_memory; + std::set<u64> m_shared_regions; ///< GPU-only regions sharing memory: they keep their own + u64 m_gpu_only_tile_max{}; ///< Largest such tile, bounds the search + u64 m_gpu_only_tile_bytes{}; ///< GPU-only region memory the game maps right now std::vector<ReleasedRegion> m_released_regions; struct GpuMemory { @@ -1150,18 +1610,18 @@ private: } u32 crop_width = layout.width; u32 crop_height = layout.height; - // Mario Kart World draws into the top-left corner of its present images: 2560x1440 docked - // (in 3840x2160 images), 1920x1080 handheld. It keeps its docked images after a switch to - // handheld, and showing all of them put the frame in a corner with stale docked frames - // around it. - if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL) { - if (!Settings::IsDockedMode()) { - crop_width = (std::min)(crop_width, 1920U); - crop_height = (std::min)(crop_height, 1080U); - } else if (layout.width == 3840 && layout.height == 2160) { - crop_width = 2560; - crop_height = 1440; - } + // Handheld titles keep their docked present images (up to 3840x2160) and draw the frame + // into the top-left 1920x1080, the handheld panel's resolution; the console's display + // scans out only that region. Showing whole images put the frame in a corner inside black + // margins that scaled with the window (seen on Mario Kart World and Splatoon Raiders). + if (!Settings::IsDockedMode()) { + crop_width = (std::min)(crop_width, 1920U); + crop_height = (std::min)(crop_height, 1080U); + } else if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL && + layout.width == 3840 && layout.height == 2160) { + // Mario Kart World docked draws 2560x1440 into the top-left of its 4K images. + crop_width = 2560; + crop_height = 1440; } static const u32 env_crop_w = [] { const char* v = std::getenv("EDEN_NX2_CROP_WIDTH"); diff --git a/src/core/hle/service/nx2_streamplay.h b/src/core/hle/service/nx2_streamplay.h new file mode 100644 index 0000000000..4dc839d04b --- /dev/null +++ b/src/core/hle/service/nx2_streamplay.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "core/hle/kernel/k_event.h" +#include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/kernel_helpers.h" +#include "core/hle/service/service.h" + +namespace Service { + +// User service returned by stpl:u 0 (nn::streamplay::detail::GetUserService). The SDK attaches a +// system event to the handle command 300 returns right after opening it, so that one needs a real +// event; everything else falls through to the nx2 auto stub (zero sizes, nothing streaming). +class INx2StreamPlayUserService final : public ServiceFramework<INx2StreamPlayUserService> { +public: + explicit INx2StreamPlayUserService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u::0"}, service_context{system_, "stpl:u"} { + static const FunctionInfo functions[] = { + {300, &INx2StreamPlayUserService::GetEvent, "GetEvent (guessed)"}, + }; + RegisterHandlers(functions); + event = service_context.CreateEvent("INx2StreamPlayUserService:Event"); + } + + ~INx2StreamPlayUserService() override { + service_context.CloseEvent(event); + } + +private: + void GetEvent(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u::0 {}, returning an event", ctx.GetCommand()); + IPC::ResponseBuilder rb{ctx, 2, 1}; + rb.Push(ResultSuccess); + rb.PushCopyObjects(ctx, event->GetReadableEvent()); + } + + KernelHelpers::ServiceContext service_context; + Kernel::KEvent* event; +}; + +// Switch 2 stream play (GameShare), stpl:u. Command 0 (u32 0x12) opens the user service the SDK +// keeps for the process; without an object the first nn::streamplay call jumps through null. +class INx2StreamPlayService final : public ServiceFramework<INx2StreamPlayService> { +public: + explicit INx2StreamPlayService(Core::System& system_) + : ServiceFramework{system_, "nx2:stpl:u"} { + static const FunctionInfo functions[] = { + {0, &INx2StreamPlayService::GetUserService, "GetUserService (guessed)"}, + }; + RegisterHandlers(functions); + } + +private: + void GetUserService(HLERequestContext& ctx) { + LOG_WARNING(Service, "(STUBBED) stpl:u 0 [8]={:#x}, returning a user service", + ctx.CommandBuffer()[8]); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(ResultSuccess); + rb.PushIpcInterface<INx2StreamPlayUserService>(ctx, system); + } +}; + +} // namespace Service diff --git a/src/core/hle/service/sm/sm.cpp b/src/core/hle/service/sm/sm.cpp index 60f0dd89cb..406d6c9028 100644 --- a/src/core/hle/service/sm/sm.cpp +++ b/src/core/hle/service/sm/sm.cpp @@ -23,6 +23,7 @@ #include "core/hle/service/cmif_types.h" #include "core/hle/service/hle_ipc.h" #include "core/hle/service/ipc_helpers.h" +#include "core/hle/service/nx2_streamplay.h" #include "core/hle/service/nx2_stub_interface.h" #include "core/hle/service/server_manager.h" #include "core/hle/service/sm/sm.h" @@ -194,8 +195,12 @@ Kernel::KClientSession* CreateStubSession(Core::System& system, HLERequestContex auto manager = std::make_shared<SessionRequestManager>(kernel, ctx.GetManager()->GetServerManager()); - manager->SetSessionHandler(std::make_shared<INx2StubInterface>( - system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + if (name == "stpl:u") { + manager->SetSessionHandler(std::make_shared<INx2StreamPlayService>(system)); + } else { + manager->SetSessionHandler(std::make_shared<INx2StubInterface>( + system, INx2StubInterface::InternName(fmt::format("{}{}", Nx2StubPrefix, name)))); + } ctx.GetManager()->GetServerManager().RegisterSession(&session->GetServerSession(), manager); return &session->GetClientSession(); } diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index bff7e76efd..cda1645548 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> +#include <cctype> #include <cstring> #include <string_view> #include "common/logging.h" @@ -130,6 +131,38 @@ std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) { return fmt::format("{}.{}.{}", best[0], best[1], best[2]); } +// Loose ExeFS dumps carry no control data, but icon/nacp sidecars can be dropped next to the +// ExeFS or in the game folder above it (<game>/exefs/main, <game>/base/exefs/main and +// <game>/updates/<title id>/exefs/main layouts). +std::vector<FileSys::VirtualDir> FindSidecarDirs(const FileSys::VirtualDir& exefs_dir) { + const auto equals_ignore_case = [](const std::string& a, std::string_view b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { + return std::tolower(static_cast<unsigned char>(x)) == + std::tolower(static_cast<unsigned char>(y)); + }); + }; + + std::vector<FileSys::VirtualDir> dirs{exefs_dir}; + for (auto dir = exefs_dir; dirs.size() < 4;) { + const auto parent = dir->GetParentDirectory(); + if (parent == nullptr) { + break; + } + const auto& name = parent->GetName(); + dirs.push_back(parent); + const bool storage_level = + equals_ignore_case(name, "base") || equals_ignore_case(name, "update") || + equals_ignore_case(name, "updates") || + (!name.empty() && name.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos); + if (!storage_level) { + break; + } + dir = parent; + } + return dirs; +} + } // namespace AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_, bool override_update_) @@ -137,6 +170,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys , override_update(override_update_) { const auto file_dir = file->GetContainingDirectory(); + const auto sidecar_dirs = FindSidecarDirs(file_dir); // Title ID const auto npdm = file_dir->GetFile("main.npdm"); @@ -148,8 +182,13 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys // Icon FileSys::VirtualFile icon_file = nullptr; - for (const auto& language : FileSys::LANGUAGE_NAMES) { - icon_file = file_dir->GetFile("icon_" + std::string(language) + ".dat"); + for (const auto& sidecar_dir : sidecar_dirs) { + for (const auto& language : FileSys::LANGUAGE_NAMES) { + icon_file = sidecar_dir->GetFile("icon_" + std::string(language) + ".dat"); + if (icon_file != nullptr) { + break; + } + } if (icon_file != nullptr) { icon_data = icon_file->ReadAllBytes(); break; @@ -158,31 +197,35 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys if (icon_data.empty()) { // Any png, jpeg, or bmp file - const auto& files = file_dir->GetFiles(); - const auto icon_iter = - std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { - return f->GetExtension() == "png" || f->GetExtension() == "jpg" || - f->GetExtension() == "bmp" || f->GetExtension() == "jpeg"; - }); - if (icon_iter != files.end()) - icon_data = (*icon_iter)->ReadAllBytes(); - } - - // Metadata: control.nacp (or any .nacp) next to main, else a control.nacp in the folders above - // it (dumps keep the title's or the update's control data at their root). - FileSys::VirtualFile nacp_file = file_dir->GetFile("control.nacp"); - if (nacp_file == nullptr) { - const auto& files = file_dir->GetFiles(); + for (const auto& sidecar_dir : sidecar_dirs) { + const auto& files = sidecar_dir->GetFiles(); + const auto icon_iter = + std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { + return f->GetExtension() == "png" || f->GetExtension() == "jpg" || + f->GetExtension() == "bmp" || f->GetExtension() == "jpeg"; + }); + if (icon_iter != files.end()) { + icon_data = (*icon_iter)->ReadAllBytes(); + break; + } + } + } + + // Metadata + FileSys::VirtualFile nacp_file = nullptr; + for (const auto& sidecar_dir : sidecar_dirs) { + nacp_file = sidecar_dir->GetFile("control.nacp"); + if (nacp_file != nullptr) { + break; + } + const auto& files = sidecar_dir->GetFiles(); const auto nacp_iter = std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& f) { return f->GetExtension() == "nacp"; }); - if (nacp_iter != files.end()) + if (nacp_iter != files.end()) { nacp_file = *nacp_iter; - } - auto parent = file_dir->GetParentDirectory(); - for (int level = 0; level < 3 && nacp_file == nullptr && parent != nullptr; ++level) { - nacp_file = parent->GetFile("control.nacp"); - parent = parent->GetParentDirectory(); + break; + } } if (nacp_file != nullptr) { diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 6e0ab0ee6d..cfc39916db 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -8,10 +8,13 @@ #include <atomic> #include <algorithm> #include <cstring> +#include <cstdlib> #include <mutex> +#include <set> #include <span> #include <thread> #include <type_traits> +#include <utility> #include <vector> #include "common/assert.h" @@ -468,6 +471,9 @@ struct Memory::Impl { perm |= Common::MemoryPermission::Write; } host_buffer->Protect(vaddr, size, perm); + if (cached && perm != Common::MemoryPermission{}) { + ProtectArmedPages(vaddr, size); + } } // Iterate over a contiguous CPU address space, which corresponds to the specified GPU @@ -528,6 +534,84 @@ struct Memory::Impl { } } + /// Read traps replace reactive flushing's blanket read protection of every cached page: only + /// pages the GPU writes are protected, and each one only until its first read + /// (EDEN_NX2_READ_TRAPS=0 turns them off). Under NCE every fault is taken as a write, so they + /// are not used there. + bool ReadTrapsEnabled() const { + static const bool disabled = [] { + const char* value = std::getenv("EDEN_NX2_READ_TRAPS"); + return value != nullptr && value[0] == '0'; + }(); + return !disabled && current_page_table->fastmem_arena && + !Settings::values.use_reactive_flushing.GetValue() && + Settings::values.cpu_backend.GetValue() != Settings::CpuBackend::Nce; + } + + void RasterizerArmReadTraps(u64 vaddr, u64 size) { + if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) || + !ReadTrapsEnabled()) { + return; + } + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + std::scoped_lock lk{read_trap_mutex}; + u64 run_begin = 0; + u64 run_pages = 0; + const auto protect_run = [&] { + if (run_pages > 0) { + // Reads that hit a cached read area must not skip the flush of a new trap. + read_trap_generation.fetch_add(1, std::memory_order_release); + host_buffer->Protect(run_begin << YUZU_PAGEBITS, run_pages << YUZU_PAGEBITS, + Common::MemoryPermission{}); + run_pages = 0; + } + }; + for (u64 page = vaddr >> YUZU_PAGEBITS; page != page_end; ++page) { + const bool cached = std::as_const(current_page_table->entries)[page].Type() == + Common::PageType::RasterizerCachedMemory; + if (!cached || !armed_read_traps.insert(page).second) { + protect_run(); + continue; + } + armed_read_trap_count.fetch_add(1, std::memory_order_relaxed); + if (run_pages == 0) { + run_begin = page; + } + ++run_pages; + } + protect_run(); + } + + /// Takes read permission away again from armed pages that were just cached again. + void ProtectArmedPages(u64 vaddr, u64 size) { + if (armed_read_trap_count.load(std::memory_order_relaxed) == 0) { + return; + } + std::scoped_lock lk{read_trap_mutex}; + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + for (auto it = armed_read_traps.lower_bound(vaddr >> YUZU_PAGEBITS); + it != armed_read_traps.end() && *it < page_end; ++it) { + host_buffer->Protect(*it << YUZU_PAGEBITS, YUZU_PAGESIZE, Common::MemoryPermission{}); + } + } + + /// A slow-path read of a cached page: its trap, if any, has done its job. + void DisarmReadTraps(u64 vaddr, size_t size) { + if (armed_read_trap_count.load(std::memory_order_relaxed) == 0) { + return; + } + std::scoped_lock lk{read_trap_mutex}; + const u64 page_end = ((vaddr + size - 1) >> YUZU_PAGEBITS) + 1; + auto it = armed_read_traps.lower_bound(vaddr >> YUZU_PAGEBITS); + while (it != armed_read_traps.end() && *it < page_end) { + // Still cached (this read came through the rasterizer path): writes stay trapped. + host_buffer->Protect(*it << YUZU_PAGEBITS, YUZU_PAGESIZE, + Common::MemoryPermission::Read); + it = armed_read_traps.erase(it); + armed_read_trap_count.fetch_sub(1, std::memory_order_relaxed); + } + } + /** * Maps a region of pages as a specific type. * @@ -698,12 +782,18 @@ struct Memory::Impl { } const size_t core = system.GetCurrentHostThreadID(); auto& current_area = rasterizer_read_areas[core]; + if (const u64 generation = read_trap_generation.load(std::memory_order_acquire); + read_area_generations[core] != generation) { + current_area = {}; + read_area_generations[core] = generation; + } gpu_device_memory->ApplyOpOnPointer(p, scratch_buffers[core], [&](DAddr address) { const DAddr end_address = address + size; if (current_area.start_address <= address && end_address <= current_area.end_address) [[likely]] { return; } + DisarmReadTraps(v_address, size); current_area = system.GPU().OnCPURead(address, size); }); } @@ -777,6 +867,11 @@ struct Memory::Impl { Common::PageTable* current_page_table = nullptr; std::array<VideoCore::RasterizerDownloadArea, Core::Hardware::NUM_CPU_CORES> rasterizer_read_areas{}; + std::array<u64, Core::Hardware::NUM_CPU_CORES> read_area_generations{}; + std::atomic<u64> read_trap_generation{}; + std::set<u64> armed_read_traps; + std::atomic<size_t> armed_read_trap_count{}; + std::mutex read_trap_mutex; std::array<GPUDirtyState, Core::Hardware::NUM_CPU_CORES> rasterizer_write_areas{}; std::array<Common::ScratchBuffer<u32>, Core::Hardware::NUM_CPU_CORES> scratch_buffers{}; std::span<Core::GPUDirtyMemoryManager> gpu_dirty_managers; @@ -969,6 +1064,10 @@ void Memory::RasterizerMarkRegionCached(Common::ProcessAddress vaddr, u64 size, impl->RasterizerMarkRegionCached(GetInteger(vaddr), size, cached); } +void Memory::RasterizerArmReadTraps(Common::ProcessAddress vaddr, u64 size) { + impl->RasterizerArmReadTraps(GetInteger(vaddr), size); +} + void Memory::MarkRegionDebug(Common::ProcessAddress vaddr, u64 size, bool debug) { impl->MarkRegionDebug(GetInteger(vaddr), size, debug); } diff --git a/src/core/memory.h b/src/core/memory.h index 204b7514f6..2b418b8864 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -484,6 +484,12 @@ public: void RasterizerMarkRegionCached(Common::ProcessAddress vaddr, u64 size, bool cached); /** + * Makes the next guest read of each rasterizer cached page in the range take the slow path + * (fastmem only), so that the read reaches the GPU's reactive flush once. + */ + void RasterizerArmReadTraps(Common::ProcessAddress vaddr, u64 size); + + /** * Marks each page within the specified address range as debug or non-debug. * Debug addresses are not accessible from fastmem pointers. * diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index bb92d896f8..37f5a89f26 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -204,6 +204,15 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) { INSERT(Settings, bg_green, QString(), QString()); INSERT(Settings, bg_blue, QString(), QString()); + INSERT(Settings, frame_gen, tr("Enable Frame Generation"), QString()); + INSERT(Settings, frame_gen_type, tr("Frame Generation Type"), QString()); + INSERT(Settings, frame_gen_multiplier, tr("Frame Generation Multiplier"), QString()); + INSERT(Settings, frame_gen_target_rate, tr("Target Frame Rate"), QString()); + INSERT(Settings, frame_gen_flow_scale_auto, tr("Auto Flow Scale"), QString()); + INSERT(Settings, frame_gen_flow_scale, tr("Flow Scale"), QString()); + INSERT(Settings, frame_gen_queue_target, tr("Queue Target"), QString()); + INSERT(Settings, frame_gen_dump_flow, tr("Dump Optical Flow"), QString()); + // Renderer (Advanced Graphics) INSERT(Settings, use_asynchronous_gpu_emulation, QString(), QString()); @@ -475,6 +484,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ResolutionSetup, Res7X, tr("7X (5040p/7560p)")), PAIR(ResolutionSetup, Res8X, tr("8X (5760p/8640p)")), }}); + translations->insert({Settings::EnumMetadata<Settings::FrameGenType>::Index(), + { + PAIR(FrameGenType, Lsfg, tr("Lossless Scaling (LSFG)")), + PAIR(FrameGenType, Fsr3, tr("AMD FidelityFX Super Resolution 3 (FSR 3)")), + }}); translations->insert({Settings::EnumMetadata<Settings::ScalingFilter>::Index(), { PAIR(ScalingFilter, NearestNeighbor, tr("Nearest Neighbor")), @@ -492,6 +506,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, Spline1, tr("Spline-1")), PAIR(ScalingFilter, Sgsr, tr("Snapdragon Game Super Resolution")), PAIR(ScalingFilter, SgsrEdge, tr("Snapdragon Game Super Resolution EdgeDir")), + PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 3")), }}); translations->insert({Settings::EnumMetadata<Settings::AntiAliasing>::Index(), { diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index dbe472caa1..15d427d4aa 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -33,6 +33,11 @@ static const std::map<Settings::AntiAliasing, QString> anti_aliasing_texts_map = {Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SMAA"))}, }; +static const std::map<Settings::FrameGenType, QString> frame_gen_type_texts_map = { + {Settings::FrameGenType::Lsfg, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "LSFG"))}, + {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, +}; + static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map = { {Settings::ScalingFilter::NearestNeighbor, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Nearest"))}, @@ -55,6 +60,7 @@ static const std::map<Settings::ScalingFilter, QString> scaling_filter_texts_map {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, {Settings::ScalingFilter::Sgsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR"))}, {Settings::ScalingFilter::SgsrEdge, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR EdgeDir"))}, + {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, }; static const std::map<Settings::ConsoleMode, QString> use_docked_mode_texts_map = { diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp index 708ef576c6..4f3916b04d 100644 --- a/src/qt_common/game_list/model.cpp +++ b/src/qt_common/game_list/model.cpp @@ -49,7 +49,7 @@ GameListModel::~GameListModel() = default; void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { emit PopulatingStarted(); - current_worker.reset(); + StopWorker(); removeRows(0, rowCount()); current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, @@ -157,8 +157,27 @@ void GameListModel::RemoveFavorite(u64 program_id) { } } +void GameListModel::StopWorker() { + if (!current_worker) { + return; + } + + // The worker may still be inside ProcessEvents further up the stack (a nested event loop + // can trigger a repopulate), so stop it now but defer freeing it to the event loop. + current_worker->disconnect(); + current_worker->Stop(); + current_worker.release()->deleteLater(); +} + void GameListModel::Repopulate() { - current_worker.reset(); + // CreateFactories replaces the content providers a running game reads from. Stopping + // emulation repopulates the list anyway, so skip watcher-driven reloads until then. + if (QtCommon::system->IsPoweredOn()) { + LOG_INFO(Frontend, "Emulation is running, deferring game list reload."); + return; + } + + StopWorker(); QtCommon::system->GetFileSystemController().CreateFactories(*QtCommon::vfs); PopulateAsync(UISettings::values.game_dirs); } diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h index 98614ae33a..b850895d2f 100644 --- a/src/qt_common/game_list/model.h +++ b/src/qt_common/game_list/model.h @@ -78,6 +78,7 @@ private: void AddFavorite(u64 program_id); void RemoveFavorite(u64 program_id); void Repopulate(); + void StopWorker(); bool m_flat = false; diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index 70577cb270..d18f84089a 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <algorithm> #include <filesystem> #include <memory> #include <string> @@ -167,17 +168,78 @@ QString FormatGameName(const std::string& physical_name) { return physical_name_as_qstring; } -// Loose ExeFS dumps (<game>/exefs/main, updates in <game>/updates/exefs/main) have no control -// data, so they are named after their game folder. +// Loose ExeFS dumps (<game>/exefs/main, <game>/base/exefs/main, updates in +// <game>/updates/<title id>/exefs/main) have no control data, so they are named after their +// game folder. QString LooseExeFSName(const std::string& physical_name) { QDir dir = QFileInfo(QString::fromStdString(physical_name)).dir(); if (dir.dirName().compare(QStringLiteral("exefs"), Qt::CaseInsensitive) != 0 || !dir.cdUp()) { return {}; } - if (dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0 && dir.cdUp()) { - return QObject::tr("%1 (update)").arg(dir.dirName()); + + // Storage levels sit between the game folder and its content directories. + const auto is_storage_level = [](const QString& name) { + if (name.compare(QStringLiteral("base"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + name.compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + return true; + } + return !name.isEmpty() && + std::all_of(name.cbegin(), name.cend(), [](QChar c) { + return c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f')) || + (c >= QLatin1Char('A') && c <= QLatin1Char('F')); + }); + }; + + bool is_update = false; + while (is_storage_level(dir.dirName())) { + if (dir.dirName().compare(QStringLiteral("update"), Qt::CaseInsensitive) == 0 || + dir.dirName().compare(QStringLiteral("updates"), Qt::CaseInsensitive) == 0) { + is_update = true; + } + if (!dir.cdUp()) { + return {}; + } + } + + const QString name = dir.dirName(); + if (name.isEmpty()) { + return {}; + } + if (is_update && !name.contains(QStringLiteral("update"), Qt::CaseInsensitive)) { + return QObject::tr("%1 (update)").arg(name); + } + return name; +} + +// A loose dump's game list entry is its small main executable, so the size column would only +// show a few dozen MB. Sum the dump's real contents instead: the ExeFS directory plus the +// RomFS directory next to it, matching the loader's loose RomFS discovery. +u64 LooseDumpSize(const std::filesystem::path& main_path) { + const auto exefs_dir = main_path.parent_path(); + + u64 total{}; + std::error_code ec; + const auto accumulate = [&total, &ec](const std::filesystem::path& dir) { + for (std::filesystem::recursive_directory_iterator it{ + dir, std::filesystem::directory_options::skip_permission_denied, ec}; + !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) { + std::error_code entry_ec; + if (it->is_regular_file(entry_ec)) { + const auto size = it->file_size(entry_ec); + if (!entry_ec) { + total += size; + } + } + } + }; + + accumulate(exefs_dir); + const auto romfs_dir = exefs_dir.parent_path() / "romfs"; + if (std::filesystem::is_directory(romfs_dir, ec)) { + accumulate(romfs_dir); } - return dir.dirName(); + return total; } QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, @@ -251,12 +313,18 @@ GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, GameListWorker::~GameListWorker() { this->disconnect(); - stop_requested.store(true); - processing_completed.Wait(); + Stop(); +} + +void GameListWorker::Stop() { + if (!stop_requested.exchange(true)) { + processing_completed.Wait(); + } } void GameListWorker::ProcessEvents(GameListModel* model) { - while (true) { + // An event may spin a nested event loop that replaces this worker, in which case stop. + while (model->current_worker.get() == this) { std::function<void(GameListModel*)> func; { // Lock queue to protect concurrent modification. @@ -346,105 +414,142 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan, GameListDir* parent_dir) { - const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool { - if (stop_requested) { - // Breaks the callback loop. - return false; + const auto process_candidate = [this, target, + parent_dir](const std::filesystem::path& path) { + const auto physical_name = Common::FS::PathToUTF8String(path); + const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); + if (!file) { + return; } - const auto physical_name = Common::FS::PathToUTF8String(path); - const auto is_dir = Common::FS::IsDir(path); + auto loader = Loader::GetLoader(system, file); + if (!loader) { + return; + } - if (!is_dir && - (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { - const auto file = vfs->OpenFile(physical_name, FileSys::OpenMode::Read); - if (!file) { - return true; - } + const auto file_type = loader->GetFileType(); + if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { + return; + } - auto loader = Loader::GetLoader(system, file); - if (!loader) { - return true; - } + if (target == ScanTarget::PopulateGameList && + (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) && + !Loader::IsBootableGameContainer(file, file_type)) { + return; + } - const auto file_type = loader->GetFileType(); - if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { - return true; + u64 program_id = 0; + const auto res2 = loader->ReadProgramId(program_id); + + if (target == ScanTarget::FillManualContentProvider) { + if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { + provider->AddEntry(FileSys::TitleType::Application, + FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), + program_id, file); + } else if (Settings::values.ext_content_from_game_dirs.GetValue() && + (file_type == Loader::FileType::XCI || + file_type == Loader::FileType::XCZ || + file_type == Loader::FileType::NSP || + file_type == Loader::FileType::NSZ)) { + void(provider->AddEntriesFromContainer(file)); } + } else { + std::vector<u64> program_ids; + loader->ReadProgramIds(program_ids); + + const auto addEntry = [this, &path, physical_name, + parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader, + const u64 id) { + std::vector<u8> icon; + [[maybe_unused]] const auto res1 = app_loader->ReadIcon(icon); + + std::string name = " "; + const auto res3 = app_loader->ReadTitle(name); + if ((res3 != Loader::ResultStatus::Success || name.empty() || name == " ") && + IsExtractedNCAMain(physical_name)) { + if (const QString loose = LooseExeFSName(physical_name); !loose.isEmpty()) { + name = loose.toStdString(); + } + } + + const FileSys::PatchManager patch{id, system.GetFileSystemController(), + system.GetContentProvider()}; + + const u64 size = IsExtractedNCAMain(physical_name) + ? LooseDumpSize(path) + : Common::FS::GetSize(physical_name); + + auto entry = MakeGameListEntry(physical_name, name, size, icon, *app_loader, id, + play_time_manager, patch); - if (target == ScanTarget::PopulateGameList && + RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); + }; + + if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) && - !Loader::IsBootableGameContainer(file, file_type)) { - return true; - } + file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) { + for (const auto id : program_ids) { + // dravee suggested this, only viable way to + // not show sub-games in qlaunch for now. + if ((id & 0xFFF) != 0) { + continue; + } + loader = Loader::GetLoader(system, file, id); + if (!loader) { + continue; + } - u64 program_id = 0; - const auto res2 = loader->ReadProgramId(program_id); - - if (target == ScanTarget::FillManualContentProvider) { - if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) { - provider->AddEntry(FileSys::TitleType::Application, - FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), - program_id, file); - } else if (Settings::values.ext_content_from_game_dirs.GetValue() && - (file_type == Loader::FileType::XCI || - file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || - file_type == Loader::FileType::NSZ)) { - void(provider->AddEntriesFromContainer(file)); + addEntry(loader, id); } } else { - std::vector<u64> program_ids; - loader->ReadProgramIds(program_ids); - - const auto addEntry = [this, physical_name, - parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader, - const u64 id) { - std::vector<u8> icon; - [[maybe_unused]] const auto res1 = app_loader->ReadIcon(icon); - - std::string name = " "; - const auto res3 = app_loader->ReadTitle(name); - if ((res3 != Loader::ResultStatus::Success || name.empty() || name == " ") && - IsExtractedNCAMain(physical_name)) { - if (const QString loose = LooseExeFSName(physical_name); !loose.isEmpty()) { - name = loose.toStdString(); - } + addEntry(loader, program_id); + } + } + }; + + const auto callback = [this, target, deep_scan, + &process_candidate](const std::filesystem::path& path) -> bool { + if (stop_requested) { + // Breaks the callback loop. + return false; + } + + if (Common::FS::IsDir(path)) { + watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(path))); + + // Shallow scans stop at the first level, so probe the standard layouts of loose + // ExeFS dumps: <game>/exefs/main, <game>/base/exefs/main and their updates in + // <root>/updates/<title id>/exefs/main. + if (!deep_scan && target == ScanTarget::PopulateGameList) { + for (const auto& content_root : {path, path / "base"}) { + const auto exefs_main = content_root / "exefs" / "main"; + if (Common::FS::IsFile(exefs_main)) { + process_candidate(exefs_main); } - const FileSys::PatchManager patch{id, system.GetFileSystemController(), - system.GetContentProvider()}; - - auto entry = MakeGameListEntry( - physical_name, name, Common::FS::GetSize(physical_name), icon, *app_loader, - id, play_time_manager, patch); - - RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); }); - }; - - if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 && - (file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ || - file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ)) { - for (const auto id : program_ids) { - // dravee suggested this, only viable way to - // not show sub-games in qlaunch for now. - if ((id & 0xFFF) != 0) { - continue; - } - loader = Loader::GetLoader(system, file, id); - if (!loader) { - continue; - } - - addEntry(loader, id); + const auto updates = content_root / "updates"; + if (Common::FS::IsDir(updates)) { + Common::FS::IterateDirEntries( + updates, + [&process_candidate](const std::filesystem::path& entry) { + const auto update_main = entry / "exefs" / "main"; + if (Common::FS::IsFile(update_main)) { + process_candidate(update_main); + } + return true; + }, + Common::FS::DirEntryFilter::Directory); } - } else { - addEntry(loader, program_id); } } - } else if (is_dir) { - watch_list.append(QString::fromStdString(physical_name)); + + return true; + } + + const auto physical_name = Common::FS::PathToUTF8String(path); + if (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)) { + process_candidate(path); } return true; @@ -454,7 +559,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa Common::FS::IterateDirEntriesRecursively(dir_path, callback, Common::FS::DirEntryFilter::All); } else { - Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File); + Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::All); } } diff --git a/src/qt_common/game_list/worker.h b/src/qt_common/game_list/worker.h index c007498d4b..cbc1fa7021 100644 --- a/src/qt_common/game_list/worker.h +++ b/src/qt_common/game_list/worker.h @@ -52,6 +52,9 @@ public: /// Starts the processing of directory tree information. void run() override; + /// Requests the worker to stop and blocks until processing has finished. + void Stop(); + public: /** * Synchronously processes any events queued by the worker. diff --git a/src/shader_recompiler/CMakeLists.txt b/src/shader_recompiler/CMakeLists.txt index 39484a1bd5..e60f26d47f 100644 --- a/src/shader_recompiler/CMakeLists.txt +++ b/src/shader_recompiler/CMakeLists.txt @@ -183,6 +183,7 @@ add_library(shader_recompiler STATIC ir_opt/lower_fp64_to_fp32.cpp ir_opt/lower_int64_to_int32.cpp ir_opt/passes.h + ir_opt/predicate_mask.h ir_opt/position_pass.cpp ir_opt/rescaling_pass.cpp ir_opt/ssa_rewrite_pass.cpp diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp index 0649290b0e..471c0f3795 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp @@ -1,13 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include <cstdlib> + #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" namespace Shader::Backend::SPIRV { +namespace { +// Registers read before any write are zero rather than OpUndef, as NVIDIA hardware and drivers +// give them; AMD's OpUndef is whatever the register held. Title 1's glow pass exits before +// writing its colour on some pixels, which AMD blended in as green patches on the road. +// EDEN_NX2_UNDEF_ZERO=0 emits OpUndef again. +bool UndefIsZero() { + static const bool zero = [] { + const char* const value = std::getenv("EDEN_NX2_UNDEF_ZERO"); + return value == nullptr || value[0] != '0'; + }(); + return zero; +} +} // Anonymous namespace Id EmitUndefU1(EmitContext& ctx) { - return ctx.OpUndef(ctx.U1); + return UndefIsZero() ? ctx.false_value : ctx.OpUndef(ctx.U1); } Id EmitUndefU8(EmitContext&) { @@ -19,7 +37,7 @@ Id EmitUndefU16(EmitContext&) { } Id EmitUndefU32(EmitContext& ctx) { - return ctx.OpUndef(ctx.U32[1]); + return UndefIsZero() ? ctx.u32_zero_value : ctx.OpUndef(ctx.U32[1]); } Id EmitUndefU64(EmitContext&) { diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h index 0bed72eaec..031f7fac75 100644 --- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h +++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h @@ -92,7 +92,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) { active_link = call_links[call]; } } - if (active_link && insn.Bits(16, 8) == *active_link) + // RET has no destination GPR; bits 16..23 are reserved, not a write to R0. + if (active_link && opcode != 0x950 && insn.Bits(16, 8) == *active_link) return fail(index, "native callee overwrites link register"); } // Scratch values and emulated uniform state occupy reserved high GPRs. diff --git a/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp b/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp index 0c0d4fb1a2..a5b249f504 100644 --- a/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp +++ b/src/shader_recompiler/ir_opt/constant_propagation_pass.cpp @@ -16,6 +16,7 @@ #include "shader_recompiler/frontend/ir/modifiers.h" #include "shader_recompiler/frontend/ir/value.h" #include "shader_recompiler/ir_opt/passes.h" +#include "shader_recompiler/ir_opt/predicate_mask.h" namespace Shader::Optimization { namespace { @@ -954,6 +955,8 @@ void FoldDriverConstBuffer(Environment& env, IR::Block& block, IR::Inst& inst, u } void FoldSMax32(IR::Block& block, IR::Inst& inst) { + // guest shaders normalize predicate masks with max(x, -x): it is 1 for -1 and 1, and 0 for + // 0. NVIDIA's compiler miscompiles the max form which crashes with a vkDeviceLost for (size_t index = 0; index < 2; ++index) { const IR::Value value{inst.Arg(index).Resolve()}; const IR::Inst* const negation{inst.Arg(1 - index).TryInstRecursive()}; @@ -961,26 +964,25 @@ void FoldSMax32(IR::Block& block, IR::Inst& inst) { negation->Arg(0).Resolve() != value) { continue; } + IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; const IR::Inst* const select{value.TryInstRecursive()}; - if (!select || select->GetOpcode() != IR::Opcode::SelectU32) { - continue; + if (select && select->GetOpcode() == IR::Opcode::SelectU32 && + IsMaskConstant(select->Arg(1).Resolve()) && + IsMaskConstant(select->Arg(2).Resolve())) { + // absolute value on both arms + const IR::Value true_value{select->Arg(1).Resolve()}; + const IR::Value false_value{select->Arg(2).Resolve()}; + inst.ReplaceUsesWith(ir.Select(IR::U1{select->Arg(0)}, + ir.Imm32(true_value.U32() != 0 ? 1U : 0U), + ir.Imm32(false_value.U32() != 0 ? 1U : 0U))); + return; } - const IR::Value true_value{select->Arg(1).Resolve()}; - const IR::Value false_value{select->Arg(2).Resolve()}; - const auto is_mask = [](const IR::Value& candidate) { - return candidate.IsImmediate() && - (candidate.U32() == 0 || candidate.U32() == 1 || - candidate.U32() == 0xffffffffU); - }; - if (!is_mask(true_value) || !is_mask(false_value)) { - continue; + // mask merged by predicated moves (phi of -s and 1 - s) is still -1, 0 or 1 when the + // it can be proven it, and max(x, -x) of such a mask is exactly x & 1. + if (MaskValues(value) != MaskUnknown) { + inst.ReplaceUsesWith(ir.BitwiseAnd(IR::U32{value}, ir.Imm32(1))); + return; } - // max(x, -x) is 0 or 1 for boolean and predicate-mask constants. - IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; - inst.ReplaceUsesWith(ir.Select(IR::U1{select->Arg(0)}, - ir.Imm32(true_value.U32() != 0 ? 1U : 0U), - ir.Imm32(false_value.U32() != 0 ? 1U : 0U))); - return; } } void ConstantPropagation(Environment& env, IR::Block& block, IR::Inst& inst) { diff --git a/src/shader_recompiler/ir_opt/predicate_mask.h b/src/shader_recompiler/ir_opt/predicate_mask.h new file mode 100644 index 0000000000..cc1cd6a8fc --- /dev/null +++ b/src/shader_recompiler/ir_opt/predicate_mask.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <cstddef> + +#include "common/common_types.h" +#include "shader_recompiler/frontend/ir/value.h" + +namespace Shader::Optimization { + +constexpr u32 MaskCanBeMinusOne{1u << 0}; +constexpr u32 MaskCanBeZero{1u << 1}; +constexpr u32 MaskCanBeOne{1u << 2}; +constexpr u32 MaskUnknown{0}; + +constexpr u32 MaxMaskAnalysisDepth{8}; + +// negation maps -1 -> 1, 0 -> 0 and 1 -> -1: swap the -1 and 1 bits, keep 0 +inline u32 NegateMask(u32 values) { + return (values & MaskCanBeZero) | ((values & MaskCanBeMinusOne) << 2) | + ((values & MaskCanBeOne) >> 2); +} + +// x + 1 with x != 1: -1 becomes 0, 0 becomes 1 +inline u32 AddOneToMask(u32 values) { + return values << 1; +} + +// x - 1 with x != -1: 0 becomes -1, 1 becomes 0 +inline u32 SubtractOneFromMask(u32 values) { + return values >> 1; +} + +// mask constant: guest code encodes booleans as 0, 1 and -1 (all bits set) +inline bool IsMaskConstant(const IR::Value& candidate) { + return candidate.IsImmediate() && + (candidate.U32() == 0 || candidate.U32() == 1 || candidate.U32() == 0xffffffffU); +} + +/// "predicate mask" can take: -1, 0 and 1. +/// guest compilers build masks from selects, negations, +/-1 adjustments and phis. +/// everything unproven comes back as MaskUnknown, so callers only fold +/// with a proven mask and never fold on a wrong one. +inline u32 MaskValues(const IR::Value& value, u32 depth = 0) { + const IR::Value resolved{value.Resolve()}; + if (resolved.IsImmediate()) { + if (resolved.Type() != IR::Type::U32) { + return MaskUnknown; + } + switch (resolved.U32()) { + case 0xffffffffU: + return MaskCanBeMinusOne; + case 0: + return MaskCanBeZero; + case 1: + return MaskCanBeOne; + default: + return MaskUnknown; + } + } + if (depth >= MaxMaskAnalysisDepth) { + return MaskUnknown; + } + const IR::Inst* const inst{resolved.InstRecursive()}; + switch (inst->GetOpcode()) { + case IR::Opcode::SelectU32: { + const u32 true_values{MaskValues(inst->Arg(1), depth + 1)}; + const u32 false_values{MaskValues(inst->Arg(2), depth + 1)}; + return true_values != MaskUnknown && false_values != MaskUnknown + ? true_values | false_values + : MaskUnknown; + } + case IR::Opcode::INeg32: + return NegateMask(MaskValues(inst->Arg(0), depth + 1)); + case IR::Opcode::IAdd32: + for (size_t index = 0; index < 2; ++index) { + const IR::Value step{inst->Arg(1 - index).Resolve()}; + if (!step.IsImmediate()) { + continue; + } + const u32 values{MaskValues(inst->Arg(index), depth + 1)}; + // +/- 1 stays in the domain only if the sum cannot escape it + // 1 + 1 = 2 and -1 - 1 = -2 are not mask values + if (step.U32() == 1 && (values & MaskCanBeOne) == 0) { + return AddOneToMask(values); + } + if (step.U32() == 0xffffffffU && (values & MaskCanBeMinusOne) == 0) { + return SubtractOneFromMask(values); + } + return MaskUnknown; + } + return MaskUnknown; + case IR::Opcode::Phi: { + u32 values{}; + for (size_t index = 0; index < inst->NumArgs(); ++index) { + const u32 arg_values{MaskValues(inst->Arg(index), depth + 1)}; + if (arg_values == MaskUnknown) { + return MaskUnknown; + } + values |= arg_values; + } + return values; + } + default: + return MaskUnknown; + } +} + +} // namespace Shader::Optimization diff --git a/src/shader_recompiler/object_pool.h b/src/shader_recompiler/object_pool.h index 5d648b1592..c39d3527ea 100644 --- a/src/shader_recompiler/object_pool.h +++ b/src/shader_recompiler/object_pool.h @@ -75,8 +75,13 @@ private: Release(); } + // Destroys the objects themselves: Storage's destructor is empty on purpose (the union + // also holds unused slots), so destroying the Storage elements would skip T's + // destructor and leak what the objects own (e.g. an IR::Inst's phi operand list). void Release() { - std::destroy_n(storage.get(), used_objects); + for (size_t index = 0; index < used_objects; ++index) { + std::destroy_at(&storage[index].object); + } used_objects = 0; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 875c40604c..dd6f5d97b5 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(tests core/core_timing.cpp core/internal_network/network.cpp video_core/memory_tracker.cpp + video_core/ampere_returns.cpp input_common/calibration_configuration_job.cpp ) diff --git a/src/tests/video_core/ampere_returns.cpp b/src/tests/video_core/ampere_returns.cpp new file mode 100644 index 0000000000..56b8b374da --- /dev/null +++ b/src/tests/video_core/ampere_returns.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <catch2/catch_test_macros.hpp> + +#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" + +namespace { +using Shader::Ampere::Instruction; +using Shader::Ampere::LowerToMaxwell; + +TEST_CASE("Ampere returns do not write the link register", "[video_core][ampere]") { + for (const u32 link : {0U, 55U}) { + const std::array<Instruction, 4> code{{ + {{0x00007802U | (link << 16), 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00007950U | (link << 24), 0xffffffc0, 0x03c3ffff, 0}}, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(static_cast<bool>(lowered)); + REQUIRE(lowered.source_size == sizeof(code)); + REQUIRE(lowered.instructions.back() == 0xe32000000007000fULL); + } +} + +TEST_CASE("Ampere returns can read a copy of the R0 link", "[video_core][ampere]") { + const std::array<Instruction, 5> code{{ + {{0x00007802, 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00017202, 0, 0x00000f00, 0}}, + {{0x01007950, 0xffffffb0, 0x03c3ffff, 0}}, + }}; + REQUIRE(static_cast<bool>(LowerToMaxwell(code))); +} + +TEST_CASE("Ampere callees still reject writes to the link", "[video_core][ampere]") { + const std::array<Instruction, 5> code{{ + {{0x00007802, 0x20, 0x00000f00, 0}}, + {{0x00007944, 0x10, 0x03c00000, 0}}, + {{0x0000794d, 0, 0x03800000, 0}}, + {{0x00007802, 0, 0x00000f00, 0}}, + {{0x00007950, 0xffffffb0, 0x03c3ffff, 0}}, + }}; + const auto lowered = LowerToMaxwell(code); + REQUIRE(!static_cast<bool>(lowered)); + REQUIRE(lowered.error == "native callee overwrites link register"); + REQUIRE(lowered.error_offset == 0x30); +} +} // namespace diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 7f83e0d3c2..5ed43fc903 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -134,6 +134,8 @@ add_library(video_core STATIC renderer_vulkan/present/filters.h renderer_vulkan/present/fsr.cpp renderer_vulkan/present/fsr.h + renderer_vulkan/present/fsr3.cpp + renderer_vulkan/present/fsr3.h renderer_vulkan/present/fxaa.cpp renderer_vulkan/present/fxaa.h renderer_vulkan/present/layer.cpp @@ -288,6 +290,8 @@ if (ENABLE_LSFG) frame_gen/lsfg_translate.h renderer_vulkan/present/frame_gen.cpp renderer_vulkan/present/frame_gen.h + renderer_vulkan/present/fsr3_frame_gen.cpp + renderer_vulkan/present/fsr3_frame_gen.h renderer_vulkan/present/frame_gen_pacer.cpp renderer_vulkan/present/frame_gen_pacer.h renderer_vulkan/present/lsfg_alpha.cpp diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 42d7ba066d..e90af3214d 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -279,8 +279,10 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am const DAddr diff = base_address - *cpu_src_address; const DAddr new_base_address = *cpu_dest_address + diff; tmp_intervals.push_back({new_base_address, size}); - if (AsyncBufferDownloads()) { + if (DownloadsAtFence(new_base_address, size)) { uncommitted_gpu_modified_ranges.Add(new_base_address, size); + } else { + ArmReadTraps(new_base_address, size); } }; gpu_modified_ranges.ForEachInRange(*cpu_src_address, amount, mirror); @@ -1603,8 +1605,10 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3 } memory_tracker.MarkRegionAsGpuModified(device_addr, size); gpu_modified_ranges.Add(device_addr, size); - if (AsyncBufferDownloads()) { + if (DownloadsAtFence(device_addr, size)) { uncommitted_gpu_modified_ranges.Add(device_addr, size); + } else { + ArmReadTraps(device_addr, size); } } @@ -2090,6 +2094,8 @@ void BufferCache<P>::DeleteBuffer(BufferId buffer_id, bool do_not_mark) { if constexpr (requires { runtime.OnBufferDeleted(slot_buffers[buffer_id]); }) { runtime.OnBufferDeleted(slot_buffers[buffer_id]); } + read_trapped_ranges.Subtract(slot_buffers[buffer_id].CpuAddr(), + slot_buffers[buffer_id].SizeBytes()); bool dirty_index{false}; boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> dirty_vertex_buffers; const auto scalar_replace = [buffer_id](Binding& binding) { diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 45a1836341..d8a61b7913 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -245,6 +245,12 @@ public: std::optional<VideoCore::RasterizerDownloadArea> GetFlushArea(DAddr device_addr, u64 size); + /// Without downloads at every fence, a guest CPU read is the only way GPU-written buffer + /// data reaches guest memory, so reads must ask the buffer cache (GetFlushArea). + bool FlushesOnCpuRead() const { + return !AsyncBufferDownloads(); + } + bool InlineMemory(DAddr dest_address, size_t copy_size, std::span<const u8> inlined_buffer); void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size); @@ -536,6 +542,31 @@ private: } return gpu_memory == nullptr || !gpu_memory->UsesNativeTiling(); } + + /// Whether a GPU write to this range is copied back at the next fence. Ranges the guest CPU + /// has read (Preflushable, set by the first reactive flush) always are: later reads skip the + /// flush and rely on these downloads, e.g. collision meshes skinned by compute shaders. + bool DownloadsAtFence(DAddr device_addr, u64 size) noexcept { + return AsyncBufferDownloads() || memory_tracker.IsRegionPreflushable(device_addr, size); + } + + /// GPU writes that are not downloaded at fences: trap the guest CPU's next read of their + /// pages, which flushes them once and marks them Preflushable (then DownloadsAtFence). + void ArmReadTraps(DAddr device_addr, u64 size) { + const DAddr begin = Common::AlignDown(device_addr, Core::DEVICE_PAGESIZE); + const DAddr end = Common::AlignUp(device_addr + size, Core::DEVICE_PAGESIZE); + u64 armed_bytes = 0; + read_trapped_ranges.ForEachInRange(begin, end - begin, [&](DAddr lower, DAddr upper) { + armed_bytes += upper - lower; + }); + if (armed_bytes == end - begin) { + return; + } + read_trapped_ranges.Add(begin, end - begin); + device_memory.ArmReadTraps(begin, end - begin); + } + + Common::RangeSet<DAddr> read_trapped_ranges; Common::RangeSet<DAddr> gpu_modified_ranges; std::deque<Common::RangeSet<DAddr>> committed_gpu_modified_ranges; diff --git a/src/video_core/host1x/codecs/codec.cpp b/src/video_core/host1x/codecs/codec.cpp index 56332866e5..3cbef1d891 100644 --- a/src/video_core/host1x/codecs/codec.cpp +++ b/src/video_core/host1x/codecs/codec.cpp @@ -104,6 +104,8 @@ std::string_view Codec::GetCurrentCodecName() const { return "H265"; case Host1x::NvdecCommon::VideoCodec::VP9: return "VP9"; + case Host1x::NvdecCommon::VideoCodec::AV1: + return "AV1"; default: return "Unknown"; } diff --git a/src/video_core/host1x/ffmpeg.cpp b/src/video_core/host1x/ffmpeg.cpp index 676e5820a5..1af8307adc 100644 --- a/src/video_core/host1x/ffmpeg.cpp +++ b/src/video_core/host1x/ffmpeg.cpp @@ -163,6 +163,8 @@ Decoder::Decoder(Tegra::Host1x::NvdecCommon::VideoCodec codec) { return AV_CODEC_ID_VP8; case Tegra::Host1x::NvdecCommon::VideoCodec::VP9: return AV_CODEC_ID_VP9; + case Tegra::Host1x::NvdecCommon::VideoCodec::AV1: + return AV_CODEC_ID_AV1; default: UNIMPLEMENTED_MSG("Unknown codec {}", codec); return AV_CODEC_ID_NONE; diff --git a/src/video_core/host1x/gpu_device_memory_manager.cpp b/src/video_core/host1x/gpu_device_memory_manager.cpp index 668c2f08b8..3d7d88b92d 100644 --- a/src/video_core/host1x/gpu_device_memory_manager.cpp +++ b/src/video_core/host1x/gpu_device_memory_manager.cpp @@ -12,6 +12,11 @@ struct MaxwellDeviceMethods { size_t size, bool caching) { interface->RasterizerMarkRegionCached(address, size, caching); } + + static inline void MarkRegionReadTrap(Core::Memory::Memory* interface, VAddr address, + size_t size) { + interface->RasterizerArmReadTraps(address, size); + } }; } // namespace Tegra diff --git a/src/video_core/host1x/nvdec_common.h b/src/video_core/host1x/nvdec_common.h index 0857a9f95c..26d7c86c98 100644 --- a/src/video_core/host1x/nvdec_common.h +++ b/src/video_core/host1x/nvdec_common.h @@ -22,6 +22,7 @@ enum class VideoCodec : u64 { VP8 = 0x5, H265 = 0x7, VP9 = 0x9, + AV1 = 0xA, }; struct Offset { diff --git a/src/video_core/host1x/nx2_video_decoder.cpp b/src/video_core/host1x/nx2_video_decoder.cpp index 7d185ca573..9b34bd84af 100644 --- a/src/video_core/host1x/nx2_video_decoder.cpp +++ b/src/video_core/host1x/nx2_video_decoder.cpp @@ -35,6 +35,10 @@ std::optional<NvdecCommon::VideoCodec> CodecFromMime(std::string_view mime) { if (mime.find("vp8") != std::string_view::npos) { return NvdecCommon::VideoCodec::VP8; } + // "video/av01" or "video/av1". FFmpeg decodes AV1 with libdav1d. + if (mime.find("av01") != std::string_view::npos || mime.find("av1") != std::string_view::npos) { + return NvdecCommon::VideoCodec::AV1; + } if (mime.find("avc") != std::string_view::npos || mime.find("264") != std::string_view::npos) { return NvdecCommon::VideoCodec::H264; } diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 8082e830d6..35b350c2cd 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -92,6 +92,8 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader.vert ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile.frag ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_optical_flow.comp + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_frame_interpolation.comp ) if (HAIKUOS) diff --git a/src/video_core/host_shaders/fsr3_frame_interpolation.comp b/src/video_core/host_shaders/fsr3_frame_interpolation.comp new file mode 100644 index 0000000000..e33691e801 --- /dev/null +++ b/src/video_core/host_shaders/fsr3_frame_interpolation.comp @@ -0,0 +1,106 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2) uniform sampler2D MotionVectors; +layout(binding = 3, rgba8) uniform writeonly image2D OutGeneratedFrame; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float blend_factor; // timestamp between 0.0 and 1.0 + float flow_scale; +}; + +// ── Optimized 3×3 motion vector median using textureGather ────────── +// textureGather fetches a 2×2 quad per call, so we only need 4 calls +// to cover a 3×3 neighbourhood (vs 9 individual texture() calls). +// We extract the median of X and Y components independently. +void insertionSort5(inout float a[5]) { + for (int i = 1; i < 5; i++) { + float key = a[i]; + int j = i - 1; + while (j >= 0 && a[j] > key) { + a[j + 1] = a[j]; + j--; + } + a[j + 1] = key; + } +} + +vec2 sampleMotionMedian(vec2 uv) { + vec2 texel = inv_render_size; + + // textureGather returns 2×2 quads: (x-0.5, y-0.5) to (x+0.5, y+0.5) + // We use 4 gathers offset to cover the 3×3 region + // Gather component 0 = R (motion X), component 1 = G (motion Y) + vec4 gx0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 0); // TL quad + vec4 gx1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 0); // TR quad + vec4 gx2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 0); // BL quad + vec4 gx3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 0); // BR quad + + vec4 gy0 = textureGather(MotionVectors, uv + vec2(-0.5, -0.5) * texel, 1); + vec4 gy1 = textureGather(MotionVectors, uv + vec2( 0.5, -0.5) * texel, 1); + vec4 gy2 = textureGather(MotionVectors, uv + vec2(-0.5, 0.5) * texel, 1); + vec4 gy3 = textureGather(MotionVectors, uv + vec2( 0.5, 0.5) * texel, 1); + + // Extract 9 unique samples from overlapping 2×2 quads + // gatherN.w=TL, .z=TR, .x=BL, .y=BR of each quad + // 3×3 grid: row0=[gx0.w, gx0.z/gx1.w, gx1.z] + // row1=[gx0.x/gx2.w, center, gx1.y/gx3.z] + // row2=[gx2.x, gx2.y/gx3.x, gx3.y] + float xs[9] = float[](gx0.w, gx1.w, gx1.z, gx0.x, texture(MotionVectors, uv).x, gx1.y, gx2.x, gx3.x, gx3.y); + float ys[9] = float[](gy0.w, gy1.w, gy1.z, gy0.x, texture(MotionVectors, uv).y, gy1.y, gy2.x, gy3.x, gy3.y); + + // Partial sort: only need to find element [4] (median) + // Use two passes of 5-element insertion sort on the lower half + float xlo[5] = float[](xs[0], xs[1], xs[2], xs[3], xs[4]); + float ylo[5] = float[](ys[0], ys[1], ys[2], ys[3], ys[4]); + insertionSort5(xlo); + insertionSort5(ylo); + + // Insert remaining 4 elements, keeping only the 5 smallest + for (int i = 5; i < 9; i++) { + if (xs[i] < xlo[4]) { xlo[4] = xs[i]; insertionSort5(xlo); } + if (ys[i] < ylo[4]) { ylo[4] = ys[i]; insertionSort5(ylo); } + } + + return vec2(xlo[4], ylo[4]); +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + + // Median-filtered motion vector (reduces outlier jitter) + vec2 motion = sampleMotionMedian(uv) * (flow_scale * inv_render_size); + + // Forward and backward warped coordinates + vec2 forward_uv = clamp(uv - motion * blend_factor, 0.0, 1.0); + vec2 backward_uv = clamp(uv + motion * (1.0 - blend_factor), 0.0, 1.0); + + vec4 forward_color = texture(PreviousFrame, forward_uv); + vec4 backward_color = texture(CurrentFrame, backward_uv); + + // Colour-difference occlusion + vec3 color_diff = abs(forward_color.rgb - backward_color.rgb); + float occlusion = clamp(dot(color_diff, vec3(0.333)) * 2.0, 0.0, 1.0); + + // Smooth bilateral interpolation — bias toward centre on occlusion + float alpha = mix(blend_factor, 0.5, occlusion * 0.4); + vec4 interpolated = mix(forward_color, backward_color, alpha); + + // ── Anti-flicker: blend with un-warped centre pixel ───────────── + float motion_magnitude = length(motion * render_size); + float direct_weight = exp(-motion_magnitude * 0.5); + vec4 center_color = mix(texture(CurrentFrame, uv), texture(PreviousFrame, uv), blend_factor); + interpolated = mix(interpolated, center_color, direct_weight * 0.3); + + imageStore(OutGeneratedFrame, pixel_coord, interpolated); +} diff --git a/src/video_core/host_shaders/fsr3_optical_flow.comp b/src/video_core/host_shaders/fsr3_optical_flow.comp new file mode 100644 index 0000000000..b0e04600cb --- /dev/null +++ b/src/video_core/host_shaders/fsr3_optical_flow.comp @@ -0,0 +1,112 @@ +#version 460 core + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(binding = 0) uniform sampler2D CurrentFrame; +layout(binding = 1) uniform sampler2D PreviousFrame; +layout(binding = 2, rg16f) uniform writeonly image2D OutMotionVectors; + +layout(push_constant) uniform PushConstants { + vec2 inv_render_size; + vec2 render_size; + float search_radius; + float block_size; +}; + +// ── Shared memory tile cache ──────────────────────────────────────────── +const uint TILE = 16u; +const uint BORDER = 2u; +const uint PADDED = TILE + 2u * BORDER; // 20 +shared float s_curr_luma[PADDED * PADDED]; + +float rgb2luma(vec3 c) { + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void loadTile(ivec2 tile_origin) { + uint thread_id = gl_LocalInvocationID.y * TILE + gl_LocalInvocationID.x; + for (uint i = thread_id; i < PADDED * PADDED; i += TILE * TILE) { + uint lx = i % PADDED; + uint ly = i / PADDED; + ivec2 coord = tile_origin + ivec2(lx, ly) - ivec2(BORDER); + vec2 uv = (vec2(coord) + 0.5) * inv_render_size; + s_curr_luma[i] = rgb2luma(texture(CurrentFrame, uv).rgb); + } + barrier(); +} + +float getCachedLuma(ivec2 local_coord) { + ivec2 padded = local_coord + ivec2(BORDER); + return s_curr_luma[padded.y * PADDED + padded.x]; +} + +void main() { + ivec2 pixel_coord = ivec2(gl_GlobalInvocationID.xy); + + ivec2 tile_origin = ivec2(gl_WorkGroupID.xy) * ivec2(TILE); + loadTile(tile_origin); + + if (pixel_coord.x >= int(render_size.x) || pixel_coord.y >= int(render_size.y)) { + return; + } + + vec2 uv = (vec2(pixel_coord) + 0.5) * inv_render_size; + ivec2 local = ivec2(gl_LocalInvocationID.xy); + float curr_luma = getCachedLuma(local); + + float center_diff = abs(curr_luma - rgb2luma(texture(PreviousFrame, uv).rgb)); + + // Early-out only for truly identical pixels (very tight threshold) + if (center_diff < 0.002) { + imageStore(OutMotionVectors, pixel_coord, vec4(0.0, 0.0, 0.0, 0.0)); + return; + } + + vec2 best_motion = vec2(0.0); + float min_diff = center_diff; + + // Large diamond (radius 2) + const vec2 diamond_large[4] = vec2[]( + vec2(0.0, -2.0), + vec2(-2.0, 0.0), vec2(2.0, 0.0), + vec2(0.0, 2.0) + ); + + for (int i = 0; i < 4; ++i) { + vec2 offset_uv = uv + diamond_large[i] * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(diamond_large[i]); + if (d < min_diff) { + min_diff = d; + best_motion = diamond_large[i]; + } + } + + // Small refinement diamond (radius 1) + const vec2 diamond_small[4] = vec2[]( + vec2(0.0, -1.0), + vec2(-1.0, 0.0), vec2(1.0, 0.0), + vec2(0.0, 1.0) + ); + + vec2 center = best_motion; + for (int i = 0; i < 4; ++i) { + vec2 cand = center + diamond_small[i]; + vec2 offset_uv = uv + cand * inv_render_size; + float luma = rgb2luma(texture(PreviousFrame, offset_uv).rgb); + float d = abs(curr_luma - luma) + 0.04 * length(cand); + if (d < min_diff) { + min_diff = d; + best_motion = cand; + } + } + + // ── Dampen very small motions to prevent micro-jitter ── + // Motions below ~0.5 pixels are likely noise, not real movement + float motion_len = length(best_motion); + if (motion_len < 0.5) { + best_motion *= motion_len * 2.0; // smooth fade to zero + } + + imageStore(OutMotionVectors, pixel_coord, vec4(best_motion, 0.0, 0.0)); +} diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h index 4b0d3d5f62..318607384b 100644 --- a/src/video_core/nx2.h +++ b/src/video_core/nx2.h @@ -9,6 +9,7 @@ #include <functional> #include <map> #include <mutex> +#include <utility> #include <vector> #include "common/common_types.h" @@ -101,7 +102,13 @@ struct Nx2SparseRegionState { u64 size; u64 block_size; std::vector<bool> written; + /// Once the game maps its own memory into the region (nx2:gpu 216): which tiles it maps. + /// Only those hold data then (writes elsewhere are dropped, as on the console). + std::vector<bool> mapped; }; + +/// The granularity of the game's sparse tiles (nx2:gpu 216 maps 64 KiB pages). +inline constexpr u64 Nx2SparseTileSize = 64ULL << 10; inline std::mutex g_nx2_sparse_state_mutex; inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state; @@ -116,6 +123,12 @@ inline void Nx2SparseRegionDestroyed(u64 gpu_address) { g_nx2_sparse_state.erase(gpu_address); } +/// A new emulation session: the previous game's regions are gone (nvdrv's Module clears them). +inline void Nx2ClearSparseRegions() { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + g_nx2_sparse_state.clear(); +} + inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { std::scoped_lock lk{g_nx2_sparse_state_mutex}; if (const auto it = g_nx2_sparse_state.find(region_address); it != g_nx2_sparse_state.end()) { @@ -123,6 +136,59 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) { } } +/// The game maps its own memory over [offset, +size) of the region at region_address (mapped), or +/// unmaps it there. +inline void Nx2SparseTilesMapped(u64 region_address, u64 offset, u64 size, bool mapped) { + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + const auto it = g_nx2_sparse_state.find(region_address); + if (it == g_nx2_sparse_state.end() || size == 0) { + return; + } + auto& region = it->second; + if (region.mapped.empty()) { + region.mapped.resize((region.size + Nx2SparseTileSize - 1) / Nx2SparseTileSize); + } + const u64 last = (std::min)((offset + size - 1) / Nx2SparseTileSize, u64{region.mapped.size() - 1}); + for (u64 tile = offset / Nx2SparseTileSize; tile <= last; ++tile) { + region.mapped[tile] = mapped; + } +} + +/// The parts of [gpu_address, +size), which lies in one sparse region, that can hold data: the +/// tiles the game maps there if it maps any, otherwise the blocks written. As (offset from +/// gpu_address, size) runs. +inline std::vector<std::pair<u64, u64>> Nx2SparseDataRanges(u64 gpu_address, u64 size) { + std::vector<std::pair<u64, u64>> ranges; + std::scoped_lock lk{g_nx2_sparse_state_mutex}; + auto it = g_nx2_sparse_state.upper_bound(gpu_address); + if (it == g_nx2_sparse_state.begin()) { + return ranges; + } + --it; + const auto& [start, region] = *it; + if (size == 0 || gpu_address + size > start + region.size) { + return ranges; + } + const bool by_tile = !region.mapped.empty(); + const u64 unit = by_tile ? Nx2SparseTileSize : region.block_size; + const std::vector<bool>& present = by_tile ? region.mapped : region.written; + const u64 begin = gpu_address - start; + const u64 end = begin + size; + for (u64 index = begin / unit; index * unit < end && index < present.size(); ++index) { + if (!present[index]) { + continue; + } + const u64 run_begin = (std::max)(index * unit, begin) - begin; + const u64 run_end = (std::min)((index + 1) * unit, end) - begin; + if (!ranges.empty() && ranges.back().first + ranges.back().second == run_begin) { + ranges.back().second += run_end - run_begin; + } else { + ranges.emplace_back(run_begin, run_end - run_begin); + } + } + return ranges; +} + /// True when [gpu_address, +size) lies in one sparse region. inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) { if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) || diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 23e5eb7481..5bad51112b 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -115,6 +115,7 @@ void BlitScreen::CreateWindowAdapt() { window_adapt = MakeMmpx(device); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_opengl/gl_texture_cache.cpp b/src/video_core/renderer_opengl/gl_texture_cache.cpp index 680cb2179f..8cca26aee9 100644 --- a/src/video_core/renderer_opengl/gl_texture_cache.cpp +++ b/src/video_core/renderer_opengl/gl_texture_cache.cpp @@ -236,8 +236,7 @@ void ApplySwizzle(GLuint handle, PixelFormat format, std::array<SwizzleSource, 4 const VideoCommon::ImageInfo& info) { if (IsPixelFormatASTC(info.format) && info.size.depth == 1 && !runtime.HasNativeASTC()) { return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && - Settings::values.astc_recompression.GetValue() == - Settings::AstcRecompression::Uncompressed; + Settings::GetAstcRecompression() == Settings::AstcRecompression::Uncompressed; } // Disable other accelerated uploads for now as they don't implement swizzled uploads return false; @@ -443,12 +442,11 @@ OGLTexture MakeImage(const VideoCommon::ImageInfo& info, GLenum gl_internal_form } [[nodiscard]] bool IsAstcRecompressionEnabled() { - return Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed; + return Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed; } [[nodiscard]] GLenum SelectAstcFormat(PixelFormat format, bool is_srgb) { - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Bc1: return is_srgb ? GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT : GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; break; diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp index d0a8e1186e..9e4a0f854a 100644 --- a/src/video_core/renderer_opengl/present/layer.cpp +++ b/src/video_core/renderer_opengl/present/layer.cpp @@ -69,7 +69,7 @@ GLuint Layer::ConfigureDraw(std::array<GLfloat, 3 * 2>& out_matrix, glDisablei(GL_SCISSOR_TEST, 0); - if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr) { + if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr || filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { if (!fsr || fsr->NeedsRecreation(layout.screen)) { fsr.emplace(layout.screen.GetWidth(), layout.screen.GetHeight()); } diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index 6c18ade926..c8e43d0650 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -246,7 +246,7 @@ FormatInfo SurfaceFormat(const Device& device, FormatType format_type, bool with bool const is_srgb = with_srgb && VideoCore::Surface::IsPixelFormatSRGB(pixel_format); // Transcode on hardware that doesn't support ASTC natively if (!device.IsOptimalAstcSupported() && VideoCore::Surface::IsPixelFormatASTC(pixel_format)) { - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Uncompressed: if (is_srgb) { tuple.format = VK_FORMAT_A8B8G8R8_SRGB_PACK32; diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp index e6d857323b..a54598e9fc 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -203,15 +203,16 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, } if (unavailable || !Settings::values.frame_gen.GetValue()) { - if (chain) { + if (chain || fsr3_chain) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); } warm_streak = 0; return; } - if (!shaders->IsValid()) { + if (!shaders->IsValid() && Settings::values.frame_gen_type.GetValue() != Settings::FrameGenType::Fsr3) { unavailable = true; return; } @@ -226,7 +227,7 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, const VkExtent2D extent{.width = frame->width, .height = frame->height}; const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); - if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + if ((!chain && !fsr3_chain) || built_extent.width != extent.width || built_extent.height != extent.height || built_format != format || built_flow_scale != flow_scale) { Rebuild(device, extent, format, flow_scale); } @@ -239,21 +240,28 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, warm_streak = warm ? warm_streak + 1 : 0; generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, source = *frame->image, extent, count, - dispatch = warm](vk::CommandBuffer cmdbuf) { - CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); - if (dispatch) { - chain->DispatchShared(cmdbuf, count); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Process(device, scheduler, *frame->image, *frame->storage_view, count); + } else if (chain) { + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; } - }); - - const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); - if (!dump_requested) { - dumped = false; - } else if (!dumped) { - DumpDebugImages(count); - dumped = true; } } @@ -271,27 +279,42 @@ size_t FrameGen::GeneratedFrameCount() const { } void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { - chain->SetTarget(device, last_generations, generation, destination->index, - *destination->storage_view); - - const VkExtent2D extent{.width = destination->width, .height = destination->height}; - - scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, count = last_count, generation_count = last_generations, generation, - target = destination->index, image = *destination->image, - extent](vk::CommandBuffer cmdbuf) { - chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, - extent); - }); + const bool use_fsr3 = Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3; + + if (use_fsr3 && fsr3_chain) { + const f32 blend = static_cast<f32>(generation + 1) / static_cast<f32>(last_generations + 1); + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, built_extent); + + scheduler.RequestOutsideRenderPassOperationContext(); + fsr3_chain->Generate(device, scheduler, *destination->storage_view, blend, flow_scale); + } else if (chain) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); + } } void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { scheduler.Finish(); chain.reset(); + fsr3_chain.reset(); built_flow_scale = flow_scale; - chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + if (Settings::values.frame_gen_type.GetValue() == Settings::FrameGenType::Fsr3) { + fsr3_chain.emplace(device, memory_allocator, extent, format); + } else { + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + } built_extent = extent; built_format = format; frame_count = 0; diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h index 19c6d04289..1eb49cd7b1 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.h +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -9,6 +9,7 @@ #include "video_core/renderer_vulkan/present/frame_gen_pacer.h" #include "video_core/renderer_vulkan/present/lsfg_chain.h" #include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" namespace Vulkan { @@ -39,6 +40,7 @@ private: std::optional<LsfgShaders> shaders; std::optional<LsfgChain> chain; + std::optional<Fsr3FrameGen> fsr3_chain; FrameGenPacer pacer; FrameGenPlan plan{}; VkExtent2D peak_guest_extent{}; diff --git a/src/video_core/renderer_vulkan/present/fsr3.cpp b/src/video_core/renderer_vulkan/present/fsr3.cpp new file mode 100644 index 0000000000..ad1cd6c8b1 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.cpp @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/common_types.h" +#include "common/div_ceil.h" +#include "common/settings.h" + +#include "video_core/fsr.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_vert_spv.h" +#include "video_core/renderer_vulkan/present/fsr3.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { +using namespace FSR; + +using PushConstants = std::array<u32, 4 * 4>; + +FSR3::FSR3(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, + VkExtent2D extent) + : m_memory_allocator{memory_allocator}, m_image_count{image_count}, m_extent{extent} { + CreateImages(device); + CreateRenderPasses(device); + CreateSampler(device); + CreateShaders(device); + CreateDescriptorPool(device); + CreateDescriptorSetLayout(device); + CreateDescriptorSets(device); + CreatePipelineLayouts(device); + CreatePipelines(device); +} + +FSR3::~FSR3() = default; + +void FSR3::CreateImages(const Device& device) { + m_dynamic_images.resize(m_image_count); + for (auto& images : m_dynamic_images) { + images.images[Easu] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.images[Rcas] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Easu] = + CreateWrappedImageView(device, images.images[Easu], VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Rcas] = + CreateWrappedImageView(device, images.images[Rcas], VK_FORMAT_R16G16B16A16_SFLOAT); + } + + // Allocate a history image for temporal accumulation feedback. + m_prev_frame_image = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + m_prev_frame_view = + CreateWrappedImageView(device, m_prev_frame_image, VK_FORMAT_R16G16B16A16_SFLOAT); +} + +void FSR3::CreateRenderPasses(const Device& device) { + m_renderpass = CreateWrappedRenderPass(device, VK_FORMAT_R16G16B16A16_SFLOAT); + for (auto& images : m_dynamic_images) { + images.framebuffers[Easu] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Easu], m_extent); + images.framebuffers[Rcas] = CreateWrappedFramebuffer(device, m_renderpass, + images.image_views[Rcas], m_extent); + } +} + +void FSR3::CreateSampler(const Device& device) { + m_sampler = CreateBilinearSampler(device); +} + +void FSR3::CreateShaders(const Device& device) { + m_vert_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_VERT_SPV); + + if (device.IsFloat16Supported()) { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP16_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP16_FRAG_SPV); + } else { + m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP32_FRAG_SPV); + m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP32_FRAG_SPV); + } +} + +void FSR3::CreateDescriptorPool(const Device& device) { + // EASU: 1 descriptor, RCAS: 1 descriptor → 2 per invocation + m_descriptor_pool = CreateWrappedDescriptorPool(device, 2 * m_image_count, 2 * m_image_count); +} + +void FSR3::CreateDescriptorSetLayout(const Device& device) { + m_descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); +} + +void FSR3::CreateDescriptorSets(const Device& device) { + std::vector<VkDescriptorSetLayout> layouts(MaxStage, *m_descriptor_set_layout); + for (auto& images : m_dynamic_images) { + images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layouts); + } +} + +void FSR3::CreatePipelineLayouts(const Device& device) { + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + VkPipelineLayoutCreateInfo ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = m_descriptor_set_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + + m_pipeline_layout = device.GetLogical().CreatePipelineLayout(ci); +} + +void FSR3::CreatePipelines(const Device& device) { + m_easu_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_easu_shader)); + m_rcas_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_rcas_shader)); +} + +void FSR3::UpdateDescriptorSets(const Device& device, VkImageView image_view, + size_t image_index) { + Images& images = m_dynamic_images[image_index]; + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(2); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, + images.descriptor_sets[Easu], 0), + CreateWriteDescriptorSet(image_infos, *m_sampler, *images.image_views[Easu], + images.descriptor_sets[Rcas], 0), + }; + device.GetLogical().UpdateDescriptorSets(updates, {}); +} + +void FSR3::UploadImages(const Device& device, Scheduler& scheduler) { + if (!m_images_ready) { + m_images_ready = true; + scheduler.Record([&](vk::CommandBuffer cmdbuf) { + for (auto& image : m_dynamic_images) { + ClearColorImage(cmdbuf, *image.images[Easu]); + ClearColorImage(cmdbuf, *image.images[Rcas]); + } + ClearColorImage(cmdbuf, *m_prev_frame_image); + }); + scheduler.Finish(); + } +} + +VkImageView FSR3::Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect) { + Images& images = m_dynamic_images[image_index]; + + VkImage easu_image = *images.images[Easu]; + VkImage rcas_image = *images.images[Rcas]; + VkDescriptorSet easu_descriptor_set = images.descriptor_sets[Easu]; + VkDescriptorSet rcas_descriptor_set = images.descriptor_sets[Rcas]; + VkFramebuffer easu_framebuffer = *images.framebuffers[Easu]; + VkFramebuffer rcas_framebuffer = *images.framebuffers[Rcas]; + VkPipeline easu_pipeline = *m_easu_pipeline; + VkPipeline rcas_pipeline = *m_rcas_pipeline; + VkPipelineLayout pipeline_layout = *m_pipeline_layout; + VkRenderPass renderpass = *m_renderpass; + VkExtent2D extent = m_extent; + + const f32 input_image_width = static_cast<f32>(input_image_extent.width); + const f32 input_image_height = static_cast<f32>(input_image_extent.height); + const f32 output_image_width = static_cast<f32>(extent.width); + const f32 output_image_height = static_cast<f32>(extent.height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; + const f32 viewport_x = crop_rect.left * input_image_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; + const f32 viewport_y = crop_rect.top * input_image_height; + + PushConstants easu_con{}; + PushConstants rcas_con{}; + FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, + easu_con.data() + 12, viewport_width, viewport_height, input_image_width, + input_image_height, output_image_width, output_image_height, viewport_x, + viewport_y); + + // FSR 3 uses a slightly reduced default sharpening compared to FSR 1 + // to avoid over-sharpening on temporally accumulated detail. + const float sharpening = + static_cast<float>(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f * 0.8f; + FsrRcasCon(rcas_con.data(), sharpening); + + UploadImages(device, scheduler); + UpdateDescriptorSets(device, source_image_view, image_index); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + // ── Pass 1: EASU (edge-adaptive spatial upsampling) ────────── + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, easu_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, easu_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + easu_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, easu_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + // ── Pass 2: RCAS (robust contrast-adaptive sharpening) ─────── + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, rcas_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, rcas_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + rcas_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, rcas_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + }); + + ++m_frame_counter; + return *images.image_views[Rcas]; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3.h b/src/video_core/renderer_vulkan/present/fsr3.h new file mode 100644 index 0000000000..033440eeaf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/math_util.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 upscaler implementation. +/// +/// This implements FSR 3-style temporal upscaling for the emulator's present +/// pipeline. Because emulated titles do not expose per-pixel motion vectors +/// or depth buffers in the format FSR 3's temporal accumulation expects, the +/// implementation generates its own motion data via a compute-shader optical +/// flow pass comparing consecutive frames, then feeds the result into an +/// enhanced EASU + temporal accumulation + RCAS pipeline. +/// +/// The pipeline runs entirely in compute shaders: +/// 1. **Optical Flow** – produces screen-space motion vectors from the +/// previous and current colour inputs. +/// 2. **Temporal Accumulation + EASU** – accumulates sub-pixel detail +/// across frames using the motion vectors, then applies edge-adaptive +/// spatial upsampling. +/// 3. **RCAS** – robust contrast-adaptive sharpening on the final output. +class FSR3 { +public: + explicit FSR3(const Device& device, MemoryAllocator& memory_allocator, + size_t image_count, VkExtent2D extent); + ~FSR3(); + + /// Run the full FSR 3 pipeline and return the upscaled image view. + VkImageView Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage source_image, VkImageView source_image_view, + VkExtent2D input_image_extent, + const Common::Rectangle<f32>& crop_rect); + +private: + // ── resource creation helpers ────────────────────────────────────── + void CreateImages(const Device& device); + void CreateRenderPasses(const Device& device); + void CreateSampler(const Device& device); + void CreateShaders(const Device& device); + void CreateDescriptorPool(const Device& device); + void CreateDescriptorSetLayout(const Device& device); + void CreateDescriptorSets(const Device& device); + void CreatePipelineLayouts(const Device& device); + void CreatePipelines(const Device& device); + + // ── per-frame helpers ───────────────────────────────────────────── + void UploadImages(const Device& device, Scheduler& scheduler); + void UpdateDescriptorSets(const Device& device, VkImageView image_view, size_t image_index); + + // ── members ─────────────────────────────────────────────────────── + MemoryAllocator& m_memory_allocator; + const size_t m_image_count; + const VkExtent2D m_extent; + + enum Stage { + Easu, // Edge-adaptive spatial upsampling + temporal accumulation + Rcas, // Robust contrast-adaptive sharpening + MaxStage, + }; + + vk::DescriptorPool m_descriptor_pool; + vk::DescriptorSetLayout m_descriptor_set_layout; + vk::PipelineLayout m_pipeline_layout; + vk::ShaderModule m_vert_shader; + vk::ShaderModule m_easu_shader; + vk::ShaderModule m_rcas_shader; + vk::Pipeline m_easu_pipeline; + vk::Pipeline m_rcas_pipeline; + vk::RenderPass m_renderpass; + vk::Sampler m_sampler; + + struct Images { + vk::DescriptorSets descriptor_sets; + std::array<vk::Image, MaxStage> images; + std::array<vk::ImageView, MaxStage> image_views; + std::array<vk::Framebuffer, MaxStage> framebuffers; + }; + std::vector<Images> m_dynamic_images; + + // Previous-frame history for temporal accumulation + vk::Image m_prev_frame_image; + vk::ImageView m_prev_frame_view; + bool m_images_ready{}; + bool m_has_prev_frame{}; + u32 m_frame_counter{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp new file mode 100644 index 0000000000..1dfca4b37c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "video_core/renderer_vulkan/present/fsr3_frame_gen.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +#include "video_core/host_shaders/fsr3_frame_interpolation_comp_spv.h" +#include "video_core/host_shaders/fsr3_optical_flow_comp_spv.h" + +namespace Vulkan { + +namespace { + +// ── Precise pipeline barriers ────────────────────────────────────────── + +VkImageMemoryBarrier MakeBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; +} + +void BarrierComputeWriteToRead(vk::CommandBuffer cmdbuf, VkImage image) { + const auto barrier = MakeBarrier(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barrier); +} + +// ── Batched dual-copy ────────────────────────────────────────────────── +// Copy one source image into TWO destinations with only 3 barrier calls +// instead of 6 (avoiding redundant source transitions). +void CopyImageToTwo(vk::CommandBuffer cmdbuf, VkImage source, + VkImage dest_a, VkImage dest_b, VkExtent2D extent) { + const VkImageCopy region{ + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {0, 0, 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {0, 0, 0}, + .extent = {extent.width, extent.height, 1}, + }; + + // 1. Transition source → TRANSFER_SRC, both dests → TRANSFER_DST (batched) + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(dest_a, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + MakeBarrier(dest_b, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + // 2. Copy source → dest_a, then source → dest_b (source stays TRANSFER_SRC) + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_a, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dest_b, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + // 3. Transition all three back to GENERAL (batched) + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_a, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(dest_b, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +// Single-image copy (for first-frame init) +[[maybe_unused]] void CopyImageToImage(vk::CommandBuffer cmdbuf, VkImage source, VkImage destination, + VkExtent2D extent) { + const VkImageCopy region{ + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {0, 0, 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {0, 0, 0}, + .extent = {extent.width, extent.height, 1}, + }; + + const std::array before{ + MakeBarrier(source, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + MakeBarrier(destination, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region); + + const std::array after{ + MakeBarrier(source, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + MakeBarrier(destination, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, {}, {}, after); +} + +} // anonymous namespace + +struct FlowPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float search_radius; + float block_size; +}; + +struct InterpPushConstants { + float inv_render_w; + float inv_render_h; + float render_w; + float render_h; + float blend_factor; + float flow_scale; +}; + +Fsr3FrameGen::Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format) + : m_allocator{memory_allocator}, m_extent{extent}, + m_flow_extent{std::max(1u, extent.width / 2), std::max(1u, extent.height / 2)}, + m_format{format} { + CreateResources(device); + CreateShaders(device); + CreatePipelines(device); + m_initialized = true; +} + +Fsr3FrameGen::~Fsr3FrameGen() = default; + +void Fsr3FrameGen::CreateResources(const Device& device) { + m_linear_sampler = CreateBilinearSampler(device); + + m_history_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_history_view = CreateWrappedImageView(device, m_history_image, m_format); + + // Current-frame copy for interpolation shader + m_current_image = CreateWrappedImage(m_allocator, m_extent, m_format); + m_current_view = CreateWrappedImageView(device, m_current_image, m_format); + + // Half-resolution motion vector image + m_motion_image = CreateWrappedImage(m_allocator, m_flow_extent, VK_FORMAT_R16G16_SFLOAT); + m_motion_view = CreateWrappedImageView(device, m_motion_image, VK_FORMAT_R16G16_SFLOAT); + + // Descriptor pool — sized for flow (3 bindings) + interp (4 bindings) × 2 sets + const std::array pool_sizes{ + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 12}, + VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 6}, + }; + m_desc_pool = device.GetLogical().CreateDescriptorPool(VkDescriptorPoolCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = 4, + .poolSizeCount = static_cast<u32>(pool_sizes.size()), + .pPoolSizes = pool_sizes.data(), + }); +} + +void Fsr3FrameGen::CreateShaders(const Device& device) { + m_flow_shader = BuildShader(device, FSR3_OPTICAL_FLOW_COMP_SPV); + m_interp_shader = BuildShader(device, FSR3_FRAME_INTERPOLATION_COMP_SPV); +} + +void Fsr3FrameGen::CreatePipelines(const Device& device) { + // ── Optical Flow Pipeline ────────────────────────────────────────── + const std::array flow_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_flow_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(flow_bindings.size()), + .pBindings = flow_bindings.data(), + }); + + const VkPushConstantRange flow_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(FlowPushConstants), + }; + + m_flow_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_flow_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &flow_push_range, + }); + + m_flow_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_flow_shader, + .pName = "main", + }, + .layout = *m_flow_pipe_layout, + }); + + // ── Frame Interpolation Pipeline ─────────────────────────────────── + const std::array interp_bindings{ + VkDescriptorSetLayoutBinding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + VkDescriptorSetLayoutBinding{3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, + }; + + m_interp_desc_layout = device.GetLogical().CreateDescriptorSetLayout(VkDescriptorSetLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .bindingCount = static_cast<u32>(interp_bindings.size()), + .pBindings = interp_bindings.data(), + }); + + const VkPushConstantRange interp_push_range{ + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .offset = 0, + .size = sizeof(InterpPushConstants), + }; + + m_interp_pipe_layout = device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .setLayoutCount = 1, + .pSetLayouts = m_interp_desc_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &interp_push_range, + }); + + m_interp_pipeline = device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = *m_interp_shader, + .pName = "main", + }, + .layout = *m_interp_pipe_layout, + }); + + // Allocate descriptor sets + m_flow_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_flow_desc_layout}); + m_interp_desc_sets = CreateWrappedDescriptorSets(m_desc_pool, {*m_interp_desc_layout}); +} + +void Fsr3FrameGen::Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count) { + if (!m_has_history) { + // First frame: populate history and current-frame copy via batched dual-copy + scheduler.Record([this, current_frame](vk::CommandBuffer cmdbuf) { + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + m_has_history = true; + m_last_frame_count = frame_count; + return; + } + + // ── Duplicate-frame detection ────────────────────────────────────── + if (frame_count == m_last_frame_count && m_flow_valid) { + return; + } + m_last_frame_count = frame_count; + + // ── Descriptor caching ───────────────────────────────────────────── + if (current_view != m_cached_flow_current_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, current_view, m_flow_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_flow_desc_sets[0], 1), + }; + + const VkDescriptorImageInfo motion_store_info{ + .imageView = *m_motion_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_flow_desc_sets[0], + .dstBinding = 2, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &motion_store_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_flow_current_view = current_view; + } + + // Dispatch Optical Flow at half resolution + const u32 flow_w = m_flow_extent.width; + const u32 flow_h = m_flow_extent.height; + const FlowPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(flow_w), + .inv_render_h = 1.0f / static_cast<float>(flow_h), + .render_w = static_cast<float>(flow_w), + .render_h = static_cast<float>(flow_h), + .search_radius = 2.0f, + .block_size = 2.0f, + }; + + scheduler.Record([this, constants, current_frame, flow_w, flow_h](vk::CommandBuffer cmdbuf) { + // Transition motion image to GENERAL for storage write + TransitionImageLayout(cmdbuf, *m_motion_image, VK_IMAGE_LAYOUT_GENERAL); + + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_flow_pipe_layout, 0, + m_flow_desc_sets[0], {}); + cmdbuf.PushConstants(*m_flow_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((flow_w + 15) / 16, (flow_h + 15) / 16, 1); + + // Motion vectors: GENERAL → SHADER_READ_ONLY + BarrierComputeWriteToRead(cmdbuf, *m_motion_image); + + // Batched dual-copy: source → history + current (3 barriers instead of 6) + CopyImageToTwo(cmdbuf, current_frame, *m_history_image, *m_current_image, m_extent); + }); + + m_flow_valid = true; +} + +void Fsr3FrameGen::Generate(const Device& device, Scheduler& scheduler, + VkImageView destination_storage_view, float blend_factor, + float flow_scale) { + if (!m_has_history || !m_flow_valid) { + return; + } + + // ── Descriptor caching for interpolation ─────────────────────────── + if (destination_storage_view != m_cached_interp_dest_view) { + std::vector<VkDescriptorImageInfo> image_infos; + image_infos.reserve(4); + std::vector<VkWriteDescriptorSet> updates{ + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_current_view, m_interp_desc_sets[0], 0), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_history_view, m_interp_desc_sets[0], 1), + CreateWriteDescriptorSet(image_infos, *m_linear_sampler, *m_motion_view, m_interp_desc_sets[0], 2), + }; + + const VkDescriptorImageInfo dest_info{ + .imageView = destination_storage_view, + .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + updates.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = m_interp_desc_sets[0], + .dstBinding = 3, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .pImageInfo = &dest_info, + }); + device.GetLogical().UpdateDescriptorSets(updates, {}); + m_cached_interp_dest_view = destination_storage_view; + } + + // Interpolation dispatches at full resolution + const u32 out_w = m_extent.width; + const u32 out_h = m_extent.height; + const InterpPushConstants constants{ + .inv_render_w = 1.0f / static_cast<float>(out_w), + .inv_render_h = 1.0f / static_cast<float>(out_h), + .render_w = static_cast<float>(out_w), + .render_h = static_cast<float>(out_h), + .blend_factor = blend_factor, + .flow_scale = flow_scale, + }; + + scheduler.Record([this, constants, out_w, out_h](vk::CommandBuffer cmdbuf) { + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *m_interp_pipe_layout, 0, + m_interp_desc_sets[0], {}); + cmdbuf.PushConstants(*m_interp_pipe_layout, VK_SHADER_STAGE_COMPUTE_BIT, constants); + cmdbuf.Dispatch((out_w + 15) / 16, (out_h + 15) / 16, 1); + }); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h new file mode 100644 index 0000000000..241204399c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr3_frame_gen.h @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +/// AMD FidelityFX Super Resolution 3 (FSR 3) Frame Generation pipeline. +/// Uses compute-based optical flow and bilateral frame interpolation +/// to synthesize intermediate frames without requiring engine motion vectors. +/// +/// Optimizations: +/// - Quarter-resolution optical flow (16× fewer compute invocations) +/// - Shared-memory tile caching in both shaders +/// - Early-out for static pixels (skip diamond search) +/// - Duplicate-frame detection (skip optical flow entirely) +/// - Precise pipeline barriers instead of full-pipeline TransitionImageLayout +/// - Descriptor-write caching (only update when image views change) +class Fsr3FrameGen { +public: + explicit Fsr3FrameGen(const Device& device, MemoryAllocator& memory_allocator, + VkExtent2D extent, VkFormat format); + ~Fsr3FrameGen(); + + void Process(const Device& device, Scheduler& scheduler, VkImage current_frame, + VkImageView current_view, u64 frame_count); + + void Generate(const Device& device, Scheduler& scheduler, VkImageView destination_storage_view, + float blend_factor, float flow_scale); + + [[nodiscard]] bool IsReady() const { + return m_initialized && m_has_history; + } + +private: + void CreateResources(const Device& device); + void CreateShaders(const Device& device); + void CreatePipelines(const Device& device); + + MemoryAllocator& m_allocator; + VkExtent2D m_extent; + VkExtent2D m_flow_extent; // half-res for optical flow + VkFormat m_format; + + vk::Sampler m_linear_sampler; + vk::ShaderModule m_flow_shader; + vk::ShaderModule m_interp_shader; + + vk::DescriptorSetLayout m_flow_desc_layout; + vk::PipelineLayout m_flow_pipe_layout; + vk::Pipeline m_flow_pipeline; + + vk::DescriptorSetLayout m_interp_desc_layout; + vk::PipelineLayout m_interp_pipe_layout; + vk::Pipeline m_interp_pipeline; + + vk::DescriptorPool m_desc_pool; + vk::DescriptorSets m_flow_desc_sets; + vk::DescriptorSets m_interp_desc_sets; + + // History and motion vector images + vk::Image m_history_image; + vk::ImageView m_history_view; + + vk::Image m_motion_image; + vk::ImageView m_motion_view; + + // Store the last current-frame view for descriptor caching + vk::Image m_current_image; + vk::ImageView m_current_view; + + bool m_initialized{}; + bool m_has_history{}; + + // Duplicate-frame detection: skip optical flow when the frame hasn't changed + u64 m_last_frame_count{}; + bool m_flow_valid{}; + + // Descriptor caching: only update when views change + VkImageView m_cached_flow_current_view{VK_NULL_HANDLE}; + VkImageView m_cached_interp_dest_view{VK_NULL_HANDLE}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index 8a11c356ff..fa24c02f6d 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -15,6 +15,7 @@ #include "common/settings.h" #include "video_core/framebuffer_config.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" @@ -75,6 +76,8 @@ Layer::Layer(const Device& device, MemoryAllocator& memory_allocator_, Scheduler sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, false); } else if (filters.get_scaling_filter() == Settings::ScalingFilter::SgsrEdge) { sr_filter.emplace<SGSR>(device, memory_allocator, image_count, output_size, true); + } else if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr3) { + sr_filter.emplace<FSR3>(device, memory_allocator, image_count, output_size); } } @@ -146,6 +149,10 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c source_image_view, render_extent, crop_rect); crop_rect = {0, 0, 1, 1}; } + } else if (auto* fsr3 = std::get_if<FSR3>(&sr_filter)) { + source_image_view = fsr3->Draw(device, scheduler, image_index, source_image, + source_image_view, render_extent, crop_rect); + crop_rect = {0, 0, 1, 1}; } SetMatrixData(device, *out_push_constants, layout); diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index b965ef7c49..88fa0d6f9c 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -13,6 +13,7 @@ #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #ifdef HAS_RESHADE @@ -100,7 +101,7 @@ private: Settings::AntiAliasing anti_alias_setting{}; std::variant<std::monostate, FXAA, SMAA> anti_alias{}; - std::variant<std::monostate, SGSR, FSR> sr_filter{}; + std::variant<std::monostate, SGSR, FSR, FSR3> sr_filter{}; #ifdef HAS_RESHADE std::optional<PostProcessChain> post_process{}; u64 post_process_generation{}; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 205382f81a..d4a3c8972b 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -77,6 +77,7 @@ void BlitScreen::SetWindowAdaptPass(const Device& device) { window_adapt = MakeMmpx(device, swapchain_view_format); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr3: case Settings::ScalingFilter::Sgsr: case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 2c64f3375b..05511c10e1 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -256,6 +256,10 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program if (device.IsMoltenVK()) { for (size_t i = 0; i < 8; ++i) { const auto format = static_cast<Tegra::RenderTargetFormat>(key.state.color_formats[i]); + if (format == Tegra::RenderTargetFormat::NONE) { + info.color_output_types[i] = Shader::AttributeType::Float; + continue; + } const auto pixel_format = VideoCore::Surface::PixelFormatFromRenderTargetFormat(format); if (VideoCore::Surface::IsPixelFormatInteger(pixel_format)) { if (VideoCore::Surface::IsPixelFormatSignedInteger(pixel_format)) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 0826820714..cb31a0e7e4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1006,6 +1006,13 @@ VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 return *area; } } + if (buffer_cache.FlushesOnCpuRead()) { + std::scoped_lock lock{buffer_cache.mutex}; + auto area = buffer_cache.GetFlushArea(addr, size); + if (area) { + return *area; + } + } VideoCore::RasterizerDownloadArea new_area{ .start_address = Common::AlignDown(addr, Core::DEVICE_PAGESIZE), .end_address = Common::AlignUp(addr + size, Core::DEVICE_PAGESIZE), diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index d5136a2abe..b5bbabb3b0 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -19,6 +19,7 @@ #include "common/alignment.h" #include "common/bit_util.h" #include "common/div_ceil.h" +#include "common/literals.h" #include "common/settings.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" @@ -171,7 +172,10 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { // A large image over Switch 2 sparse blocks nobody has written (title 1's terrain layer pools, 2.9 // GB of R8/RG16/R16 that stay empty in the attract demo) is all zeros: it gets no device memory // until a write makes the texture cache replace it. -[[nodiscard]] bool IsUnwrittenSparse(const ImageInfo& info, GPUVAddr gpu_addr) { +[[nodiscard]] bool IsUnwrittenSparse(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { + if (!device.CanUseUnbackedSparseImages()) { + return false; + } constexpr u32 MinSize = 32U << 20; const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); return size >= MinSize && info.type != ImageType::e1D && info.type != ImageType::Buffer && @@ -183,7 +187,8 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { // Image::BindLayers. Only formats uploaded layer by layer: huge ASTC arrays (GPU decode) and // formats the host samples as they are. [[nodiscard]] bool UsesSparseLayers(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { - if (!device.IsSparseResidencyImage2DSupported() || info.type != ImageType::e2D || + if (!device.CanUseUnbackedSparseImages() || !device.IsSparseResidencyImage2DSupported() || + info.type != ImageType::e2D || info.resources.layers <= 1 || info.num_samples != 1 || VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) { return false; @@ -196,6 +201,49 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size); } +// How SparseTiles images leave the blocks without data: unbound (reads give zero), or all bound to +// one zeroed block where the driver can't sample unbound memory (AMD) but lets blocks share +// memory. EDEN_NX2_SPARSE_ALIASED=1 takes the shared block wherever it is supported, =0 never. +enum class SparseTilesMode { + None, + Unbound, + Aliased, +}; + +[[nodiscard]] SparseTilesMode SparseTilesModeOf(const Device& device) { + static const char force_aliased = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_ALIASED"); + return value == nullptr ? '\0' : value[0]; + }(); + if (!device.IsSparseResidencyImage2DSupported()) { + return SparseTilesMode::None; + } + const bool aliased = device.IsSparseResidencyAliasedSupported() && force_aliased != '0'; + if (aliased && force_aliased == '1') { + return SparseTilesMode::Aliased; + } + if (device.CanUseUnbackedSparseImages()) { + return SparseTilesMode::Unbound; + } + return aliased ? SparseTilesMode::Aliased : SparseTilesMode::None; +} + +// A large single 2D image in a Switch 2 sparse region (title 1's world-sized R8/RG16/R16 masks, +// 170-341 MB, of which the game maps a few MiB) gets memory by sparse block where the guest memory +// under it can hold data: unbacked at first, then Image::BindTiles. Formats of one texel per +// block that the host samples as they are. +[[nodiscard]] bool UsesSparseTiles(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) { + if (SparseTilesModeOf(device) == SparseTilesMode::None || info.type != ImageType::e2D || info.resources.layers != 1 || info.num_samples != 1 || + info.size.depth != 1 || info.block.width != 0 || + VideoCore::Surface::DefaultBlockWidth(info.format) != 1 || + VideoCore::Surface::DefaultBlockHeight(info.format) != 1 || + VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) { + return false; + } + const u32 size = VideoCommon::CalculateGuestSizeInBytes(info); + return size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size); +} + // The most device memory one SparseLayers image binds: 768 MiB, 384 MiB on GPUs with less than // 7 GiB (title 1's BC6H arrays are 85 MB a layer). [[nodiscard]] VkDeviceSize SparseLayersCap(const Device& device) { @@ -239,7 +287,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM return false; } return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && - Settings::values.astc_recompression.GetValue() != Settings::AstcRecompression::Uncompressed; + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed; } [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { @@ -252,9 +300,8 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM if (Settings::values.accelerate_astc.GetValue() != Settings::AstcDecodeMode::Gpu) { return false; } - return Settings::values.astc_recompression.GetValue() == - Settings::AstcRecompression::Uncompressed && - info.size.depth == 1; + return Settings::GetAstcRecompression() == Settings::AstcRecompression::Uncompressed && + info.size.depth == 1; } // The most a trimmed ASTC array keeps on the host: 512 MiB, 256 MiB on GPUs with less than 7 GiB @@ -364,7 +411,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM [[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator, const ImageInfo& info, std::span<const VkFormat> view_formats, std::optional<VkFormat> format_override = {}, - bool unbacked = false) { + bool unbacked = false, bool aliased = false) { if (info.type == ImageType::Buffer) { return vk::Image{}; } @@ -393,7 +440,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM } } if (unbacked) { - if (vk::Image image = allocator.CreateUnbackedImage(image_ci)) { + if (vk::Image image = allocator.CreateUnbackedImage(image_ci, aliased)) { return image; } } @@ -1097,6 +1144,29 @@ void BlitScale(Scheduler& scheduler, VkImage src_image, VkImage dst_image, const 0, nullptr, nullptr, write_barriers); }); } + +// Uncompressed ASTC needs more video memory than a Switch 2 title leaves under a 12 GiB budget +// (Mario Kart World filled a 16 GB card's 13.5 GB budget; a 12 GB card has ~11.2 GB): there the +// textures went unbacked (garbage) and the game crashed. Use BC3 for this session. +void UseBc3ForSwitch2OnSmallCards(const Device& device) { + using namespace Common::Literals; + if (!Settings::CurrentProgramIsSwitch2() || device.IsOptimalAstcSupported() || + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { + return; + } + const u64 budget = device.GetDeviceMemoryBudget(); + const u64 memory = budget != 0 ? budget : device.GetDeviceLocalMemory(); + const char* keep = std::getenv("EDEN_NX2_KEEP_ASTC"); + if (memory >= 12_GiB || (keep != nullptr && keep[0] == '1')) { + return; + } + LOG_WARNING(Render_Vulkan, + "Switch 2 program with a {} MiB video memory budget: ASTC textures are " + "re-compressed to BC3 for this session (uncompressed ASTC needs a 12 GiB budget). " + "Set EDEN_NX2_KEEP_ASTC=1 to keep them uncompressed", + memory >> 20); + Settings::SetAstcRecompressionOverride(Settings::AstcRecompression::Bc3); +} } // Anonymous namespace TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& scheduler_, @@ -1109,13 +1179,14 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_}, staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_}, render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} { + // Before anything below reads astc_recompression (passes, view formats). + UseBc3ForSwitch2OnSmallCards(device); // Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting. if (Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu || !device.IsOptimalAstcSupported()) { astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); - if (Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed) { + if (Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { bc_encoder_pass.emplace(device, scheduler, descriptor_pool, compute_pass_descriptor_queue); } @@ -2003,7 +2074,7 @@ bool TextureCacheRuntime::UseOpaqueAstcFormat() const { return value == nullptr || value[0] != '0'; }(); return enabled && !device.IsOptimalAstcSupported() && - Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3; + Settings::GetAstcRecompression() == Settings::AstcRecompression::Bc3; } void TextureCacheRuntime::FlushDeferredClear() { @@ -2080,7 +2151,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu : runtime->ViewFormats(info.format), AstcHostFormat(runtime_.device, info), UsesSparseLayers(runtime_.device, info, gpu_addr_) || - IsUnwrittenSparse(info, gpu_addr_))), + UsesSparseTiles(runtime_.device, info, gpu_addr_) || + IsUnwrittenSparse(runtime_.device, info, gpu_addr_), + UsesSparseTiles(runtime_.device, info, gpu_addr_) && + SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased)), aspect_mask(ImageAspectMask(info.format)) { if (original_image.IsUnbacked()) { flags |= VideoCommon::ImageFlagBits::Unbacked; @@ -2089,6 +2163,8 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu host_info.size.depth * static_cast<u64>(host_info.resources.layers); if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) { flags |= VideoCommon::ImageFlagBits::SparseLayers; + } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) { + flags |= VideoCommon::ImageFlagBits::SparseTiles; } else if (texels <= (u64{1} << 28)) { // Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing // was written there yet, so the first write gets it replaced. @@ -2620,6 +2696,381 @@ bool Image::InitSparseLayers() { return true; } +bool Image::InitSparseTiles() { + const Device& device = runtime->device; + const VkImage handle = *original_image; + const auto requirements = device.GetLogical().GetImageSparseMemoryRequirements(handle); + const auto it = std::ranges::find_if(requirements, [](const auto& r) { + return (r.formatProperties.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0; + }); + if (it == requirements.end()) { + return false; + } + SparseLayerMemory& s = sparse_layers; + s.requirements = device.GetLogical().GetImageMemoryRequirements(handle); + const VkExtent3D granularity = it->formatProperties.imageGranularity; + const u32 bytes = VideoCore::Surface::BytesPerBlock(info.format); + // Rectangles are uploaded by whole GOB columns (64 bytes across). + if (granularity.width == 0 || granularity.height == 0 || (granularity.width * bytes) % 64 != 0 || + u64{granularity.width} * granularity.height * bytes != s.requirements.alignment) { + return false; + } + s.allocator = &runtime->memory_allocator; + s.sparse = *it; + s.width = info.size.width; + s.height = info.size.height; + s.levels = static_cast<u32>(info.resources.levels); + s.tile_width = granularity.width; + s.tile_height = granularity.height; + s.tail_level = (std::min)(it->imageMipTailFirstLod, s.levels); + s.level_tiles.resize(s.tail_level); + for (u32 level = 0; level < s.tail_level; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + s.level_tiles[level].assign( + size_t{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height), + VK_NULL_HANDLE); + } + if (SparseTilesModeOf(device) == SparseTilesMode::Aliased && !BindZeroBlock()) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: no zeroed block to share", gpu_addr); + s.Release(); + s.allocator = nullptr; + s.bound_bytes = 0; + return false; + } + LOG_INFO(Render_Vulkan, "Sparse tiled image {:#x}: {}x{} {} levels, block {}x{}, mip tail from {}{}", + gpu_addr, s.width, s.height, s.levels, s.tile_width, s.tile_height, s.tail_level, + s.zero_block != VK_NULL_HANDLE ? ", empty blocks share a zeroed one" : ""); + return true; +} + +bool Image::BindZeroBlock() { + SparseLayerMemory& s = sparse_layers; + const Device& device = runtime->device; + const u64 tile = s.requirements.alignment; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + s.zero_block = s.allocator->AllocateSparseMemory( + VkMemoryRequirements{ + .size = tile, + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }, + memory, offset); + if (s.zero_block == VK_NULL_HANDLE) { + return false; + } + s.bound_bytes += tile; + std::vector<VkSparseImageMemoryBind> image_binds; + for (u32 level = 0; level < s.tail_level; ++level) { + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + for (u32 y = 0; y < height; y += s.tile_height) { + for (u32 x = 0; x < width; x += s.tile_width) { + image_binds.push_back(VkSparseImageMemoryBind{ + .subresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = level, + .arrayLayer = 0, + }, + .offset{static_cast<s32>(x), static_cast<s32>(y), 0}, + .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y), + 1}, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + } + } + // The mip tail gets memory of its own: it is small, and the clear below zeroes it too. + std::vector<VkSparseMemoryBind> opaque_binds; + if (s.tail_level < s.levels) { + const VkMemoryRequirements requirements{ + .size = Common::AlignUp(s.sparse.imageMipTailSize, tile), + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory tail_memory{}; + VkDeviceSize tail_offset{}; + s.tail_memory = s.allocator->AllocateSparseMemory(requirements, tail_memory, tail_offset); + if (s.tail_memory == VK_NULL_HANDLE) { + return false; + } + s.bound_bytes += requirements.size; + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset, + .size = s.sparse.imageMipTailSize, + .memory = tail_memory, + .memoryOffset = tail_offset, + .flags = 0, + }); + } + const VkSparseImageMemoryBindInfo image_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(image_binds.size()), + .pBinds = image_binds.data(), + }; + const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(opaque_binds.size()), + .pBinds = opaque_binds.data(), + }; + const VkBindSparseInfo bind_info{ + .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .bufferBindCount = 0, + .pBufferBinds = nullptr, + .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U, + .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info, + .imageBindCount = image_binds.empty() ? 0U : 1U, + .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }); + VkResult result; + { + std::scoped_lock lock{scheduler->submit_mutex}; + result = device.GetGraphicsQueue().BindSparse(bind_info, *fence); + } + if (result != VK_SUCCESS) { + LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding the zeroed block failed ({})", + gpu_addr, static_cast<int>(result)); + return false; + } + fence.Wait(); + // Every block now reads the shared block (and the tail its own memory): zero them all. + ClearTiles(); + return true; +} + +void Image::ClearTiles() { + scheduler->RequestOutsideRenderPassOperationContext(); + const VkImage vk_image = *original_image; + const bool was_initialized = std::exchange(initialized, true); + scheduler->Record([vk_image, was_initialized](vk::CommandBuffer cmdbuf) { + static constexpr VkImageSubresourceRange range{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; + const VkImageMemoryBarrier to_transfer{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = was_initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = vk_image, + .subresourceRange = range, + }; + const VkImageMemoryBarrier to_general{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = vk_image, + .subresourceRange = range, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + to_transfer); + cmdbuf.ClearColorImage(vk_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VkClearColorValue{}, + range); + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + to_general); + }); +} + +bool Image::BackTilesWhole() { + SparseLayerMemory& s = sparse_layers; + if (s.zero_block == VK_NULL_HANDLE) { + return true; + } + std::vector<VideoCommon::LevelRect> rects; + for (u32 level = 0; level < s.levels; ++level) { + rects.push_back({static_cast<s32>(level), 0, 0, (std::max)(s.width >> level, 1U), + (std::max)(s.height >> level, 1U)}); + } + if (!BindTiles(rects)) { + return false; + } + // No block reads the shared one any more. + s.allocator->FreeSparseMemory(s.zero_block); + s.zero_block = VK_NULL_HANDLE; + s.bound_bytes -= s.requirements.alignment; + ClearTiles(); + return true; +} + +bool Image::BindTiles(std::span<const VideoCommon::LevelRect> rects) { + SparseLayerMemory& s = sparse_layers; + if (s.allocator == nullptr) { + return false; + } + const Device& device = runtime->device; + const u64 tile = s.requirements.alignment; + const VkMemoryRequirements tile_requirements{ + .size = tile, + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + std::vector<VkSparseImageMemoryBind> image_binds; + std::vector<VkSparseMemoryBind> opaque_binds; + std::vector<VmaAllocation*> fresh; // Blocks given memory by this call + bool fresh_tail = false; + // A failed bind leaves the blocks as they were, so the next call tries them again. + const auto roll_back = [&] { + for (VmaAllocation* const allocation : fresh) { + s.allocator->FreeSparseMemory(*allocation); + *allocation = VK_NULL_HANDLE; + s.bound_bytes -= tile; + } + if (fresh_tail) { + s.allocator->FreeSparseMemory(s.tail_memory); + s.tail_memory = VK_NULL_HANDLE; + s.bound_bytes -= Common::AlignUp(s.sparse.imageMipTailSize, tile); + } + }; + bool needs_tail = false; + for (const VideoCommon::LevelRect& rect : rects) { + const u32 level = static_cast<u32>(rect.level); + if (level >= s.tail_level) { + needs_tail = true; + continue; + } + const u32 width = (std::max)(s.width >> level, 1U); + const u32 height = (std::max)(s.height >> level, 1U); + const u32 tiles_x = Common::DivCeil(width, s.tile_width); + for (u32 ty = rect.y / s.tile_height; ty <= (rect.y + rect.height - 1) / s.tile_height; + ++ty) { + for (u32 tx = rect.x / s.tile_width; tx <= (rect.x + rect.width - 1) / s.tile_width; + ++tx) { + VmaAllocation& allocation = s.level_tiles[level][size_t{ty} * tiles_x + tx]; + if (allocation != VK_NULL_HANDLE) { + continue; + } + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + allocation = s.allocator->AllocateSparseMemory(tile_requirements, memory, offset); + if (allocation == VK_NULL_HANDLE) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory", + gpu_addr); + roll_back(); + return false; + } + fresh.push_back(&allocation); + s.bound_bytes += tile; + const u32 x = tx * s.tile_width; + const u32 y = ty * s.tile_height; + image_binds.push_back(VkSparseImageMemoryBind{ + .subresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = level, + .arrayLayer = 0, + }, + .offset{static_cast<s32>(x), static_cast<s32>(y), 0}, + .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y), + 1}, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + } + } + if (needs_tail && s.tail_level < s.levels && s.tail_memory == VK_NULL_HANDLE) { + const VkMemoryRequirements requirements{ + .size = Common::AlignUp(s.sparse.imageMipTailSize, tile), + .alignment = tile, + .memoryTypeBits = s.requirements.memoryTypeBits, + }; + VkDeviceMemory memory{}; + VkDeviceSize offset{}; + s.tail_memory = s.allocator->AllocateSparseMemory(requirements, memory, offset); + if (s.tail_memory == VK_NULL_HANDLE) { + LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory for the mip " + "tail", + gpu_addr); + roll_back(); + return false; + } + fresh_tail = true; + s.bound_bytes += requirements.size; + opaque_binds.push_back(VkSparseMemoryBind{ + .resourceOffset = s.sparse.imageMipTailOffset, + .size = s.sparse.imageMipTailSize, + .memory = memory, + .memoryOffset = offset, + .flags = 0, + }); + } + if (image_binds.empty() && opaque_binds.empty()) { + return true; + } + const VkSparseImageMemoryBindInfo image_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(image_binds.size()), + .pBinds = image_binds.data(), + }; + const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{ + .image = *original_image, + .bindCount = static_cast<u32>(opaque_binds.size()), + .pBinds = opaque_binds.data(), + }; + const VkBindSparseInfo bind_info{ + .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, + .pNext = nullptr, + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .bufferBindCount = 0, + .pBufferBinds = nullptr, + .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U, + .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info, + .imageBindCount = image_binds.empty() ? 0U : 1U, + .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + // Bound before the command buffer that uploads the blocks is submitted (as BindLayers). + vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{ + .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + }); + VkResult result; + { + std::scoped_lock lock{scheduler->submit_mutex}; + result = device.GetGraphicsQueue().BindSparse(bind_info, *fence); + } + if (result != VK_SUCCESS) { + LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding failed ({})", gpu_addr, + static_cast<int>(result)); + roll_back(); + return false; + } + fence.Wait(); + return true; +} + std::vector<u32> Image::BindLayers(std::span<const u32> layers) { SparseLayerMemory& s = sparse_layers; std::vector<u32> bound; @@ -3173,6 +3624,16 @@ vk::ImageView ImageView::MakeView(VkFormat vk_format, VkImageAspectFlags aspect_ std::optional<Shader::TextureType> texture_type) { VkImageViewType view_type = ImageViewType(type); VkImageSubresourceRange subresource_range = MakeSubresourceRange(aspect_mask, range); + const auto is_3d = [](VkImageViewType t) { return t == VK_IMAGE_VIEW_TYPE_3D; }; + if (texture_type && is_3d(ImageViewType(*texture_type)) != is_3d(view_type)) { + // A 3D view of a 2D image (or the reverse) is invalid in Vulkan and aborts on Metal; keep + // the image's own type when the shader's declared dimension disagrees with it. + LOG_WARNING(Render_Vulkan, + "Shader image type {} does not match view type {} at 0x{:x} (format={} " + "size={}x{}x{}), using the view type", + *texture_type, type, gpu_addr, format, size.width, size.height, size.depth); + texture_type.reset(); + } if (texture_type) { view_type = ImageViewType(*texture_type); switch (view_type) { @@ -3679,8 +4140,7 @@ void TextureCacheRuntime::TranscodeAstcToBcn( // Encode every level into one buffer, then copy it into the image. const bool bc3 = !IsOpaqueBc1Astc(device, info) && - Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Bc1; + Settings::GetAstcRecompression() != Settings::AstcRecompression::Bc1; const u64 block_bytes = bc3 ? 16 : 8; boost::container::small_vector<VideoCommon::BufferImageCopy, 16> copies; u64 total = 0; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 13919f1270..43218d6ad3 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -389,6 +389,35 @@ public: /// layers that have memory afterwards (a cap per image can leave some out). std::vector<u32> BindLayers(std::span<const u32> layers); + /// SparseTiles images: gives the host sparse blocks under `rects` (and the mip tail, for + /// levels in it) device memory where they have none yet. False if that failed. + bool BindTiles(std::span<const VideoCommon::LevelRect> rects); + + /// SparseTiles images: the host sparse block, in texels, and the first level of the mip tail. + u32 SparseTileWidth() const noexcept { + return sparse_layers.tile_width; + } + u32 SparseTileHeight() const noexcept { + return sparse_layers.tile_height; + } + s32 SparseTailLevel() const noexcept { + return static_cast<s32>(sparse_layers.tail_level); + } + + /// SparseTiles images whose blocks without memory of their own share one zeroed block. + bool SparseTilesShareZeroBlock() const noexcept { + return sparse_layers.zero_block != VK_NULL_HANDLE; + } + + /// SparseTiles images sharing a zeroed block, before the GPU renders into them: every block + /// gets memory of its own, all cleared (the caller loads the data again). False on failure. + bool BackTilesWhole(); + + /// Device memory BindLayers has bound to the image's layers (an unbacked image has no other). + u64 SparseBoundBytes() const noexcept { + return sparse_layers.bound_bytes; + } + bool IsRescaled() const noexcept; bool ScaleUp(bool ignore = false); @@ -429,6 +458,10 @@ private: layer_bytes = other.layer_bytes; layer_memory = std::move(other.layer_memory); other.layer_memory.clear(); + level_tiles = std::move(other.level_tiles); + other.level_tiles.clear(); + tail_level = other.tail_level; + zero_block = std::exchange(other.zero_block, VK_NULL_HANDLE); tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE); bound_bytes = other.bound_bytes; warned = other.warned; @@ -444,8 +477,16 @@ private: for (const VmaAllocation allocation : layer_memory) { allocator->FreeSparseMemory(allocation); } + for (const auto& tiles : level_tiles) { + for (const VmaAllocation allocation : tiles) { + allocator->FreeSparseMemory(allocation); + } + } allocator->FreeSparseMemory(tail_memory); + allocator->FreeSparseMemory(zero_block); + zero_block = VK_NULL_HANDLE; layer_memory.clear(); + level_tiles.clear(); tail_memory = VK_NULL_HANDLE; } @@ -459,6 +500,12 @@ private: u32 tile_height{}; VkDeviceSize layer_bytes{}; ///< One layer's levels before the mip tail, and its own tail std::vector<VmaAllocation> layer_memory; ///< Per layer, null until bound + /// SparseTiles: per level before the mip tail, per sparse block (rows), null until bound + std::vector<std::vector<VmaAllocation>> level_tiles; + u32 tail_level{}; ///< SparseTiles: first level of the mip tail + /// SparseTiles: the zeroed block every block without memory of its own is bound to, where + /// the driver can't sample unbound memory + VmaAllocation zero_block{}; VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one VkDeviceSize bound_bytes{}; bool warned{}; @@ -468,6 +515,15 @@ private: /// Reads the image's sparse requirements; false when it can't be bound by layer. bool InitSparseLayers(); + /// Reads the image's sparse requirements; false when it can't be bound by sparse block. + bool InitSparseTiles(); + + /// Binds one zeroed block to every block of a SparseTiles image (and memory to its mip tail). + bool BindZeroBlock(); + + /// Records a clear of the whole image to zero. + void ClearTiles(); + vk::Image original_image; vk::Image scaled_image; diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp index 4603870bfa..66b9577cb5 100644 --- a/src/video_core/surface.cpp +++ b/src/video_core/surface.cpp @@ -469,7 +469,7 @@ u64 TranscodedAstcSize(u64 base_size, PixelFormat format) { static_cast<u64>(DefaultBlockHeight(format)) * RGBA8_PIXEL_SIZE; const u64 uncompressed_size = (base_size * base_block_size) / BytesPerBlock(format); - switch (Settings::values.astc_recompression.GetValue()) { + switch (Settings::GetAstcRecompression()) { case Settings::AstcRecompression::Bc1: return uncompressed_size / 8; case Settings::AstcRecompression::Bc3: diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index 04254fb417..878c229953 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -56,6 +56,10 @@ enum class ImageFlagBits : u32 { /// A large array over Switch 2 sparse blocks: unbacked, each layer gets device memory when /// something is written to it (Image::BindLayers) and only those layers are uploaded. SparseLayers = 1 << 22, + /// A large single image over Switch 2 sparse blocks: unbacked, each host sparse block gets + /// device memory when the guest memory under it can hold data (Image::BindTiles), and only + /// the parts written are uploaded. + SparseTiles = 1 << 23, }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) @@ -108,6 +112,9 @@ struct ImageBase { /// Large layered images uploaded by layer: one bit per layer written since the last upload /// while CpuModified. Empty while CpuModified means the whole image. std::vector<u64> dirty_layers; + /// SparseTiles images: the guest byte ranges (offset, size) written since the last upload while + /// CpuModified. Empty while CpuModified means all of it that can hold data. + std::vector<std::pair<u64, u64>> dirty_ranges; u64 scale_tick = 0; bool has_scaled = false; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 63abffed0f..33ca051482 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -225,19 +225,24 @@ void TextureCache<P>::TickFrame() { // VRAM (another game: 7.0 -> 6.4 GB), and collecting only past it means the driver pages // textures out. It is also the room to use: Eden's device memory figure is capped near // 7 GB, which on a 12 GB card (11.2 GB budget) kept title 1 evicting textures with 3 GB - // unused. EDEN_NX2_TC_*_MIB overrides still win. + // unused. Both thresholds stay under it with a margin (1/8 of it, at most 1 GiB): past it + // allocations fail and images end up unbacked (garbage textures), and Windows shrinks the + // budget as the card fills (16 GB card: 13.5 GB left mid-race), hence the re-read every 30 + // frames. EDEN_NX2_TC_*_MIB overrides still win. if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) { - if (native_thresholds_applied && budget_check_frames++ % 60 == 0) { + if (native_thresholds_applied && budget_check_frames++ % 30 == 0) { const u64 budget = runtime.GetDeviceMemoryBudget(); if (budget > 2_GiB) { const u64 old_critical = critical_memory; - critical_memory = budget - 768_MiB; - expected_memory = budget - 1536_MiB; - // Log only real changes (the budget moves by a few MiB all the time). + const u64 margin = (std::min)(budget / 8, 1_GiB); + critical_memory = budget - margin; + expected_memory = budget - 2 * margin; + // Log the first values and real changes (the budget moves by a few MiB all + // the time). const u64 change = critical_memory > old_critical ? critical_memory - old_critical : old_critical - critical_memory; - ApplyThresholdOverrides(change >= 256_MiB); + ApplyThresholdOverrides(budget_check_frames == 1 || change >= 256_MiB); } } } @@ -724,6 +729,10 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) { image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped; return; } + if (True(image.flags & ImageFlagBits::SparseTiles)) { + MarkRangeWritten(image, cpu_addr, size); + return; + } if (UploadsByLayer(image)) { MarkLayersWritten(image, cpu_addr, size); return; @@ -1311,6 +1320,10 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) { RefreshSparseLayers(image); return; } + if (True(image.flags & ImageFlagBits::SparseTiles)) { + RefreshSparseTiles(image); + return; + } if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) { runtime.InsertUploadMemoryBarrier(); return; @@ -1396,6 +1409,77 @@ void TextureCache<P>::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t siz } template <class P> +void TextureCache<P>::MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size) { + if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_ranges.empty()) { + return; // all of it is dirty already + } + const DAddr begin = (std::max<DAddr>)(cpu_addr, image.cpu_addr); + const DAddr end = (std::min<DAddr>)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes); + if (end <= begin) { + return; + } + image.flags |= ImageFlagBits::CpuModified; + image.dirty_ranges.emplace_back(begin - image.cpu_addr, end - begin); +} + +template <class P> +void TextureCache<P>::RefreshSparseTiles(Image& image) { + if constexpr (requires { &Image::BindTiles; }) { + // The ranges written since the last upload, or, for a new image, all of it that can hold + // data (the tiles the game maps there, or the blocks written). + std::vector<std::pair<u64, u64>> ranges = std::move(image.dirty_ranges); + image.dirty_ranges.clear(); + if (ranges.empty()) { + ranges = Tegra::Nx2SparseDataRanges(image.gpu_addr, image.guest_size_bytes); + } + const std::vector<LevelRect> rects = + SparseTileRects(image.info, ranges, image.SparseTileWidth(), image.SparseTileHeight(), + image.SparseTailLevel()); + if (rects.empty() || !BindSparseTiles(image, rects)) { + runtime.TransitionImageLayout(image); + return; + } + const size_t bytes = UnswizzledRectsBytes(image.info, rects); + auto staging = runtime.UploadStagingBuffer(bytes); + const std::vector<BufferImageCopy> copies = + UnswizzleImageRects(*gpu_memory, image.gpu_addr, image.info, rects, staging.mapped_span); + image.UploadMemory(staging, copies); + runtime.InsertUploadMemoryBarrier(); + ++images_uploaded; + uploaded_bytes += bytes; + } +} + +template <class P> +bool TextureCache<P>::BindSparseTiles(Image& image, std::span<const LevelRect> rects) { + if constexpr (requires { &Image::BindTiles; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + const bool bound = image.BindTiles(rects); + // RegisterImage counts what an image has bound by then; later binds are added here. + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return bound; + } else { + return false; + } +} + +template <class P> +bool TextureCache<P>::BackSparseTilesWhole(Image& image) { + if constexpr (requires { &Image::BackTilesWhole; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + const bool backed = image.BackTilesWhole(); + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return backed; + } else { + return false; + } +} + +template <class P> bool TextureCache<P>::UploadWrittenLayers(Image& image) { if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { const u32 num_layers = static_cast<u32>(image.info.resources.layers); @@ -1434,7 +1518,8 @@ void TextureCache<P>::RefreshSparseLayers(Image& image) { } } image.dirty_layers.clear(); - const std::vector<u32> bound = layers.empty() ? std::vector<u32>{} : image.BindLayers(layers); + const std::vector<u32> bound = + layers.empty() ? std::vector<u32>{} : BindSparseLayers(image, layers); if (bound.empty()) { runtime.TransitionImageLayout(image); return; @@ -1445,6 +1530,21 @@ void TextureCache<P>::RefreshSparseLayers(Image& image) { } template <class P> +std::vector<u32> TextureCache<P>::BindSparseLayers(Image& image, std::span<const u32> layers) { + if constexpr (requires { &Image::BindLayers; }) { + const u64 bound_bytes = ImageMemoryBytes(image); + std::vector<u32> bound = image.BindLayers(layers); + // RegisterImage counts what an image has bound by then; later binds are added here. + if (True(image.flags & ImageFlagBits::Registered)) { + total_used_memory += ImageMemoryBytes(image) - bound_bytes; + } + return bound; + } else { + return {}; + } +} + +template <class P> void TextureCache<P>::UploadLayers(Image& image, std::span<const u32> layers) { if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) { if (False(image.flags & ImageFlagBits::AcceleratedUpload)) { @@ -1714,6 +1814,26 @@ u64 TextureCache<P>::GetScaledImageSizeBytes(const ImageBase& image) { } template <class P> +u64 TextureCache<P>::ImageMemoryBytes(const Image& image) const { + if (True(image.flags & ImageFlagBits::Unbacked)) { + // No device memory but what BindLayers gave a SparseLayers image: at its full size (GBs + // for title 1's sparse terrain images), deleting it would look to the collector like + // freeing that much. + if constexpr (requires { image.SparseBoundBytes(); }) { + return image.SparseBoundBytes(); + } + return 0; + } + u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); + if ((IsPixelFormatASTC(image.info.format) && + True(image.flags & ImageFlagBits::AcceleratedUpload)) || + True(image.flags & ImageFlagBits::Converted)) { + tentative_size = TranscodedAstcSize(tentative_size, image.info.format); + } + return Common::AlignUp(tentative_size, 1024); +} + +template <class P> void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) { UNIMPLEMENTED_IF(False(image.flags & ImageFlagBits::Converted)); LOG_DEBUG(HW_GPU, "Queuing async texture decode {:#x}", image.gpu_addr); @@ -2613,13 +2733,7 @@ void TextureCache<P>::RegisterImage(ImageId image_id) { ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Trying to register an already registered image"); image.flags |= ImageFlagBits::Registered; - u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); - if ((IsPixelFormatASTC(image.info.format) && - True(image.flags & ImageFlagBits::AcceleratedUpload)) || - True(image.flags & ImageFlagBits::Converted)) { - tentative_size = TranscodedAstcSize(tentative_size, image.info.format); - } - total_used_memory += Common::AlignUp(tentative_size, 1024); + total_used_memory += ImageMemoryBytes(slot_images[image_id]); image.lru_index = lru_cache.Insert(image_id, frame_tick); ForEachGPUPage(image.gpu_addr, image.guest_size_bytes, [this, image_id](u64 page) { @@ -2795,13 +2909,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) { if (image.HasScaled()) { total_used_memory -= GetScaledImageSizeBytes(image); } - u64 tentative_size = (std::max)(image.guest_size_bytes, image.unswizzled_size_bytes); - if ((IsPixelFormatASTC(image.info.format) && - True(image.flags & ImageFlagBits::AcceleratedUpload)) || - True(image.flags & ImageFlagBits::Converted)) { - tentative_size = TranscodedAstcSize(tentative_size, image.info.format); - } - total_used_memory -= Common::AlignUp(tentative_size, 1024); + // TickFrame sets the total from the driver, which can be under the sum of these estimates. + total_used_memory -= (std::min)(total_used_memory, ImageMemoryBytes(slot_images[image_id])); const GPUVAddr gpu_addr = image.gpu_addr; const auto alloc_it = image_allocs_table.find(gpu_addr); if (alloc_it == image_allocs_table.end()) { @@ -2995,12 +3104,29 @@ void TextureCache<P>::PrepareImage(ImageId image_id, bool is_modification, bool } } if (is_modification) { + if constexpr (requires { &Image::BackTilesWhole; }) { + if (True(image.flags & ImageFlagBits::SparseTiles) && image.SparseTilesShareZeroBlock()) { + // The render would write the zeroed block all empty blocks share: each gets memory + // of its own first, and the parts that hold data are loaded again. + LOG_INFO(HW_GPU, "GPU write into sparse tiled image {:#x}; backing it whole", + image.gpu_addr); + if (BackSparseTilesWhole(image)) { + image.dirty_ranges.clear(); + RefreshSparseTiles(image); + } else { + LOG_ERROR(HW_GPU, + "Sparse tiled image {:#x}: out of memory backing it whole, the GPU " + "write lands in the shared zeroed block", + image.gpu_addr); + } + } + } if constexpr (requires { &Image::BindLayers; }) { if (True(image.flags & ImageFlagBits::SparseLayers)) { std::vector<u32> layers(static_cast<size_t>(image.info.resources.layers)); std::iota(layers.begin(), layers.end(), 0U); // Layers over the image's memory cap stay unbacked (logged once). - image.BindLayers(layers); + BindSparseLayers(image, layers); } } if (True(image.flags & ImageFlagBits::UnwrittenSparse) && @@ -3033,6 +3159,10 @@ template <class P> void TextureCache<P>::CopyImage(ImageId dst_id, ImageId src_id, std::vector<ImageCopy> copies) { Image& dst = slot_images[dst_id]; Image& src = slot_images[src_id]; + if (True(dst.flags & ImageFlagBits::UnwrittenSparse) || + True(src.flags & ImageFlagBits::UnwrittenSparse)) { + return; + } if (dst.info.astc_opaque != src.info.astc_opaque) { // BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the // same guest data, which is uploaded from guest memory instead. diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 11e95f98b3..141e5b76d5 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -164,12 +164,27 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI /// Records the layers a write of [cpu_addr, +size) touches. void MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size); + /// SparseTiles images: notes a written range, uploaded with the next refresh. + void MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size); + + /// SparseTiles images: binds and uploads the parts written since the last upload. + void RefreshSparseTiles(Image& image); + + /// Image::BindTiles, counting what it binds in total_used_memory. + bool BindSparseTiles(Image& image, std::span<const LevelRect> rects); + + /// Image::BackTilesWhole, counting what it binds in total_used_memory. + bool BackSparseTilesWhole(Image& image); + /// Uploads the image's dirty layers. False when it needs a full upload instead. bool UploadWrittenLayers(Image& image); /// Gives the written layers of a SparseLayers image memory and uploads them. void RefreshSparseLayers(Image& image); + /// Image::BindLayers, with the memory it binds counted in total_used_memory. + std::vector<u32> BindSparseLayers(Image& image, std::span<const u32> layers); + /// Uploads `layers` of the image from guest memory. void UploadLayers(Image& image, std::span<const u32> layers); @@ -464,6 +479,11 @@ private: bool ScaleDown(Image& image); u64 GetScaledImageSizeBytes(const ImageBase& image); + /// The device memory an image counts for in total_used_memory between two driver readings: + /// none for an unbacked image (too large to back, or over unwritten sparse blocks), what is + /// bound to its layers for a SparseLayers one. + u64 ImageMemoryBytes(const Image& image) const; + void QueueAsyncDecode(Image& image, ImageId image_id); void TickAsyncDecode(); diff --git a/src/video_core/texture_cache/types.h b/src/video_core/texture_cache/types.h index ecacd8c6bd..9e541d6afc 100644 --- a/src/video_core/texture_cache/types.h +++ b/src/video_core/texture_cache/types.h @@ -132,6 +132,15 @@ struct ImageCopy { Extent3D extent; }; +/// A rectangle of one mip level, in texels. +struct LevelRect { + s32 level; + u32 x; + u32 y; + u32 width; + u32 height; +}; + struct BufferImageCopy { size_t buffer_offset; size_t buffer_size; diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index ffc1d59ec3..0e1acba813 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -613,10 +613,10 @@ u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept { return info.size.width * BytesPerBlock(info.format); } static constexpr Extent2D TILE_SIZE{1, 1}; - if (IsPixelFormatASTC(info.format) && Settings::values.astc_recompression.GetValue() != - Settings::AstcRecompression::Uncompressed) { + if (IsPixelFormatASTC(info.format) && + Settings::GetAstcRecompression() != Settings::AstcRecompression::Uncompressed) { const u32 bpp_div = - Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 || + Settings::GetAstcRecompression() == Settings::AstcRecompression::Bc1 || info.astc_opaque ? 2 : 1; @@ -929,9 +929,213 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory return copies; } +namespace { +// One level of a single-layer 2D block-linear image of 1x1-texel blocks, laid out as +// UnswizzleImage reads it: rows of blocks, each block one GOB wide and 2^block_height GOBs tall. +struct BlockLinearLevel { + u32 offset; ///< Into the image + u32 width; ///< Texels + u32 height; + u32 block_height; ///< Log2 of the GOBs a block is tall + u32 block_texels; ///< Texels across a block + u32 block_rows; ///< Texel rows in a block + u32 block_bytes; + u32 blocks_per_row; +}; + +std::vector<BlockLinearLevel> BlockLinearLevels(const ImageInfo& info) { + const LevelInfo level_info = MakeLevelInfo(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + const Extent2D gob = GobSize(bpp_log2, info.block.height, info.tile_width_spacing); + std::vector<BlockLinearLevel> levels; + u32 offset = 0; + for (s32 level = 0; level < info.resources.levels; ++level) { + const Extent3D level_size = AdjustMipSize(info.size, level); + const Extent3D num_tiles = AdjustTileSize(level_size, level_info.tile_size); + const Extent3D block = + AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels); + const u32 stride_alignment = StrideAlignment(num_tiles, info.block, gob, bpp_log2); + const u32 stride = Common::AlignUpLog2(num_tiles.width, stride_alignment) << bpp_log2; + levels.push_back({ + .offset = offset, + .width = level_size.width, + .height = level_size.height, + .block_height = block.height, + .block_texels = GOB_SIZE_X >> bpp_log2, + .block_rows = GOB_SIZE_Y << block.height, + .block_bytes = GOB_SIZE << block.height, + .blocks_per_row = Common::DivCeilLog2(stride, GOB_SIZE_X_SHIFT), + }); + offset += CalculateLevelSize(level_info, level); + } + return levels; +} + +// Rows of blocks a rectangle covers, and its blocks across. +struct RectBlocks { + u32 first_row; + u32 last_row; + u32 first_block; + u32 num_blocks; +}; + +RectBlocks BlocksOf(const BlockLinearLevel& level, const LevelRect& rect) { + const u32 first_block = rect.x / level.block_texels; + return { + .first_row = rect.y / level.block_rows, + .last_row = (rect.y + rect.height - 1) / level.block_rows, + .first_block = first_block, + .num_blocks = (std::min)(Common::DivCeil(rect.width, level.block_texels), + level.blocks_per_row - first_block), + }; +} + +u32 BlockRowTexelRows(const BlockLinearLevel& level, u32 row) { + return (std::min)(level.block_rows, level.height - row * level.block_rows); +} +} // Anonymous namespace + +std::vector<LevelRect> SparseTileRects(const ImageInfo& info, + std::span<const std::pair<u64, u64>> ranges, u32 tile_width, + u32 tile_height, s32 tail_level) { + std::vector<LevelRect> rects; + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u64 image_end = CalculateLevelBytes( + CalculateLevelSizes(MakeLevelInfo(info), info.resources.levels), info.resources.levels); + for (s32 index = 0; index < static_cast<s32>(levels.size()); ++index) { + const BlockLinearLevel& level = levels[index]; + const u64 level_end = + index + 1 < static_cast<s32>(levels.size()) ? levels[index + 1].offset : image_end; + const auto overlaps = [&](const std::pair<u64, u64>& range) { + return range.first < level_end && range.first + range.second > level.offset; + }; + if (index >= tail_level) { + if (std::ranges::any_of(ranges, overlaps)) { + rects.push_back({index, 0, 0, level.width, level.height}); + } + continue; + } + const u32 tiles_x = Common::DivCeil(level.width, tile_width); + const u32 tiles_y = Common::DivCeil(level.height, tile_height); + std::vector<bool> touched(size_t{tiles_x} * tiles_y); + for (const auto& range : ranges) { + if (!overlaps(range)) { + continue; + } + const u64 begin = (std::max<u64>)(range.first, level.offset) - level.offset; + const u64 end = (std::min<u64>)(range.first + range.second, level_end) - level.offset; + const u64 first = begin / level.block_bytes; + const u64 last = (end - 1) / level.block_bytes; + for (u64 row = first / level.blocks_per_row; row <= last / level.blocks_per_row; ++row) { + const u64 row_start = row * level.blocks_per_row; + const u64 low = (std::max)(first, row_start) - row_start; + const u64 high = (std::min)(last, row_start + level.blocks_per_row - 1) - row_start; + const u64 x0 = low * level.block_texels; + const u64 y0 = row * level.block_rows; + if (x0 >= level.width || y0 >= level.height) { + continue; + } + const u64 x1 = (std::min<u64>)((high + 1) * level.block_texels, level.width); + const u64 y1 = (std::min<u64>)(y0 + level.block_rows, level.height); + for (u64 ty = y0 / tile_height; ty <= (y1 - 1) / tile_height; ++ty) { + for (u64 tx = x0 / tile_width; tx <= (x1 - 1) / tile_width; ++tx) { + touched[ty * tiles_x + tx] = true; + } + } + } + } + for (u32 ty = 0; ty < tiles_y; ++ty) { + for (u32 tx = 0; tx < tiles_x;) { + if (!touched[size_t{ty} * tiles_x + tx]) { + ++tx; + continue; + } + const u32 first_tile = tx; + while (tx < tiles_x && touched[size_t{ty} * tiles_x + tx]) { + ++tx; + } + const u32 x = first_tile * tile_width; + const u32 y = ty * tile_height; + rects.push_back({ + .level = index, + .x = x, + .y = y, + .width = (std::min)(tx * tile_width, level.width) - x, + .height = (std::min)(y + tile_height, level.height) - y, + }); + } + } + } + return rects; +} + +size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects) { + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + size_t bytes = 0; + for (const LevelRect& rect : rects) { + const BlockLinearLevel& level = levels[rect.level]; + const RectBlocks blocks = BlocksOf(level, rect); + for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) { + bytes += (size_t{blocks.num_blocks * level.block_texels} << bpp_log2) * + BlockRowTexelRows(level, row); + } + } + return bytes; +} + +std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory, + GPUVAddr gpu_addr, const ImageInfo& info, + std::span<const LevelRect> rects, + std::span<u8> output) { + const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info); + const u32 bpp_log2 = BytesPerBlockLog2(info.format); + std::vector<BufferImageCopy> copies; + std::vector<u8> swizzled; + size_t output_offset = 0; + for (const LevelRect& rect : rects) { + const BlockLinearLevel& level = levels[rect.level]; + const RectBlocks blocks = BlocksOf(level, rect); + const u32 width = blocks.num_blocks * level.block_texels; + const size_t row_bytes = size_t{blocks.num_blocks} * level.block_bytes; + for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) { + // A row of whole blocks is laid out like an image of that width on its own. + const u32 texel_rows = BlockRowTexelRows(level, row); + const size_t bytes = (size_t{width} << bpp_log2) * texel_rows; + const GPUVAddr row_addr = gpu_addr + level.offset + + u64{row} * level.blocks_per_row * level.block_bytes + + u64{blocks.first_block} * level.block_bytes; + swizzled.assign(row_bytes, 0); + gpu_memory.ReadBlockUnsafe(row_addr, swizzled.data(), row_bytes); + UnswizzleTexture(output.subspan(output_offset, bytes), swizzled, 1U << bpp_log2, width, + texel_rows, 1, level.block_height, 0, 0, + gpu_memory.UsesNativeTiling()); + const u32 row_y = row * level.block_rows; + const u32 y0 = (std::max)(rect.y, row_y); + const u32 y1 = (std::min)(rect.y + rect.height, row_y + texel_rows); + copies.push_back(BufferImageCopy{ + .buffer_offset = output_offset + ((size_t{y0 - row_y} * width) << bpp_log2), + .buffer_size = bytes, + .buffer_row_length = width, + .buffer_image_height = texel_rows, + .image_subresource = + { + .base_level = rect.level, + .base_layer = 0, + .num_layers = 1, + }, + .image_offset = {static_cast<s32>(rect.x), static_cast<s32>(y0), 0}, + .image_extent = {rect.width, y1 - y0, 1}, + }); + output_offset += bytes; + } + } + return copies; +} + void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies) { // Same offsets and sizes as ConvertImage below. - const auto recompression = Settings::values.astc_recompression.GetValue(); + const auto recompression = Settings::GetAstcRecompression(); const bool astc = IsPixelFormatASTC(info.format); u64 output_offset = 0; for (BufferImageCopy& copy : copies) { @@ -976,7 +1180,7 @@ void ConvertImage(std::span<const u8> input, const ImageInfo& info, std::span<u8 const auto input_offset = input.subspan(copy.buffer_offset); copy.buffer_offset = output_offset; - const auto recompression_setting = Settings::values.astc_recompression.GetValue(); + const auto recompression_setting = Settings::GetAstcRecompression(); const bool astc = IsPixelFormatASTC(info.format); if (astc && recompression_setting == Settings::AstcRecompression::Uncompressed) { diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h index 0381d2d24b..74da0f90f9 100644 --- a/src/video_core/texture_cache/util.h +++ b/src/video_core/texture_cache/util.h @@ -8,6 +8,8 @@ #include <optional> #include <span> +#include <utility> +#include <vector> #include <boost/container/small_vector.hpp> #include "common/common_types.h" @@ -72,6 +74,25 @@ struct OverlapResult { Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info, std::span<const u8> input, std::span<u8> output); +/// For a single-layer 2D block-linear image of 1x1-texel blocks: the host sparse blocks +/// (tile_width x tile_height texels) that the guest byte ranges (offset, size) touch, merged into +/// runs along x. Levels from tail_level on (the host's mip tail) come whole. +[[nodiscard]] std::vector<LevelRect> SparseTileRects(const ImageInfo& info, + std::span<const std::pair<u64, u64>> ranges, + u32 tile_width, u32 tile_height, + s32 tail_level); + +/// Bytes UnswizzleImageRects writes for `rects`. +[[nodiscard]] size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects); + +/// Unswizzles `rects` (from SparseTileRects: on GOB columns) of a single-layer 2D block-linear +/// image into output, reading only the guest blocks under them. +[[nodiscard]] std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory, + GPUVAddr gpu_addr, + const ImageInfo& info, + std::span<const LevelRect> rects, + std::span<u8> output); + /// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting. void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies); diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 69c3ba14fa..b65c721bb8 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -8,6 +8,7 @@ #include <array> #include <bitset> #include <chrono> +#include <cstdlib> #include <filesystem> #include <fstream> #include <optional> @@ -410,6 +411,16 @@ std::filesystem::path StaticPipelineCacheFilename() { return shader_dir / "vulkan_static_pipelines.bin"; } +// What a heap's allocations use. The driver also counts the space in VMA's blocks no allocation +// uses, but freeing an image gives its share back only once its whole block is empty: the caches, +// collecting until the usage dropped, evicted thousands of textures in a row without lowering it +// (title 1). +u64 AllocatedUsage(VkDeviceSize driver_usage, const VmaBudget& vma_budget) { + const VmaStatistics& statistics = vma_budget.statistics; + const u64 unused = statistics.blockBytes - statistics.allocationBytes; + return driver_usage - (std::min)(driver_usage, unused); +} + } // Anonymous namespace void Device::RemoveExtension(bool& extension, const std::string& extension_name) { @@ -609,12 +620,16 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR } sets_per_pool = 64; + // AMD GCN4 and older are told apart by their lack of float16, read before float16 is turned + // off for newer AMD drivers below: checked after that, every AMD GPU looked like GCN4 and lost + // VK_EXT_sampler_filter_minmax (an RDNA3 Radeon 780M logged it). + const bool amd_gcn4_or_older = is_amd && !features.shader_float16_int8.shaderFloat16; if (is_amd_driver) { // AMD drivers need a higher amount of Sets per Pool in certain circumstances like in XC2. sets_per_pool = 96; // Disable VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT"); has_broken_cube_compatibility = true; @@ -630,7 +645,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (extensions.sampler_filter_minmax && is_amd) { // Disable ext_sampler_filter_minmax on AMD GCN4 and lower as it is broken. - if (!features.shader_float16_int8.shaderFloat16) { + if (amd_gcn4_or_older) { LOG_WARNING(Render_Vulkan, "AMD GCN4 and earlier have broken VK_EXT_sampler_filter_minmax"); RemoveExtension(extensions.sampler_filter_minmax, @@ -749,6 +764,17 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR if (extensions.buffer_device_address) { flags |= VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; } + // EDEN_NX2_VRAM_LIMIT_MIB: VMA fails allocations past the limit, like a smaller card. + std::array<VkDeviceSize, VK_MAX_MEMORY_HEAPS> heap_size_limits; + heap_size_limits.fill(VK_WHOLE_SIZE); + if (heap_size_limit != 0) { + const auto memory_properties = physical.GetMemoryProperties().memoryProperties; + for (u32 heap = 0; heap < memory_properties.memoryHeapCount; ++heap) { + if (memory_properties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) { + heap_size_limits[heap] = heap_size_limit; + } + } + } const VmaAllocatorCreateInfo allocator_info{ .flags = flags, .physicalDevice = physical, @@ -759,7 +785,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR .preferredLargeHeapBlockSize = 64u * 1024u * 1024u, .pAllocationCallbacks = nullptr, .pDeviceMemoryCallbacks = nullptr, - .pHeapSizeLimit = nullptr, + .pHeapSizeLimit = heap_size_limit != 0 ? heap_size_limits.data() : nullptr, .pVulkanFunctions = &functions, .instance = instance, .vulkanApiVersion = ApiVersion(), @@ -1603,26 +1629,16 @@ void Device::ReleaseCustomBorderColorSamplers(size_t count) const { } u64 Device::GetDeviceMemoryUsage() const { - VkPhysicalDeviceMemoryBudgetPropertiesEXT budget; + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; - budget.pNext = nullptr; physical.GetMemoryProperties(&budget); - u64 result{}; - for (const size_t heap : valid_heap_memory) { - result += budget.heapUsage[heap]; - } - // Space in VMA's blocks that no allocation uses counts as used, but freeing an image gives - // its share back only once its whole block is empty: the caches, collecting until this drops, - // evicted thousands of textures in a row without lowering it (title 1). Count only what - // allocations use. + std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; if (allocator != nullptr) { - std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; vmaGetHeapBudgets(allocator, heap_budgets.data()); - for (const size_t heap : valid_heap_memory) { - const VmaStatistics& statistics = heap_budgets[heap].statistics; - const u64 unused = statistics.blockBytes - statistics.allocationBytes; - result -= (std::min)(result, unused); - } + } + u64 result{}; + for (const size_t heap : valid_heap_memory) { + result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]); } return result; } @@ -1634,9 +1650,29 @@ u64 Device::GetDeviceMemoryBudget() const { VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; physical.GetMemoryProperties(&budget); - u64 result{}; + std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{}; + if (allocator != nullptr) { + vmaGetHeapBudgets(allocator, heap_budgets.data()); + } + // Only the main heap's budget: summing every heap's counted VRAM twice on AMD (an 8 GB card + // reported 8.5 GB) and kept the collector's thresholds above the heap the images fill. What + // the other heaps use is added the way GetDeviceMemoryUsage() counts it, so the usage reaches + // this when the main heap's does its budget. + u64 result = budget.heapBudget[main_heap]; + if (heap_size_limit != 0) { + result = (std::min)(result, heap_size_limit); + } for (const size_t heap : valid_heap_memory) { - result += budget.heapBudget[heap]; + if (heap != main_heap) { + result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]); + } + } + if (is_integrated) { + // An integrated GPU's device-local heap is system memory (a Radeon 780M reported a 21 GB + // budget): following it let the texture cache fill that much RAM before collecting, and + // kept ASTC uncompressed. Its budget is at most the share CollectPhysicalMemoryInfo + // gives it (4 GiB, 6 GiB with aggressive VRAM use). + result = (std::min)(result, device_access_memory); } return result; } @@ -1652,6 +1688,20 @@ void Device::CollectPhysicalMemoryInfo() { device_access_memory = 0; u64 device_initial_usage = 0; u64 local_memory = 0; + // EDEN_NX2_VRAM_LIMIT_MIB=<n>: treat every device-local heap as n MiB (small card tests). + if (const char* value = std::getenv("EDEN_NX2_VRAM_LIMIT_MIB")) { + heap_size_limit = std::strtoull(value, nullptr, 10) << 20; + if (heap_size_limit != 0) { + LOG_INFO(Render_Vulkan, "EDEN_NX2_VRAM_LIMIT_MIB: device-local heaps limited to {} MiB", + heap_size_limit >> 20); + } else { + LOG_WARNING(Render_Vulkan, "EDEN_NX2_VRAM_LIMIT_MIB={} is not a size in MiB, ignored", + value); + } + } + const auto limit = [this](u64 size) { + return heap_size_limit != 0 ? (std::min)(size, heap_size_limit) : size; + }; for (size_t element = 0; element < num_properties; ++element) { const bool is_heap_local = (mem_properties.memoryHeaps[element].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; @@ -1659,15 +1709,22 @@ void Device::CollectPhysicalMemoryInfo() { continue; } valid_heap_memory.push_back(element); + const VkMemoryHeap& main = mem_properties.memoryHeaps[main_heap]; + u64 heap_size = mem_properties.memoryHeaps[element].size; if (is_heap_local) { - local_memory += mem_properties.memoryHeaps[element].size; + if ((main.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || heap_size > main.size) { + main_heap = element; + } + heap_size = limit(heap_size); + local_memory += heap_size; } if (extensions.memory_budget) { device_initial_usage += budget.heapUsage[element]; - device_access_memory += budget.heapBudget[element]; + device_access_memory += is_heap_local ? limit(budget.heapBudget[element]) + : budget.heapBudget[element]; continue; } - device_access_memory += mem_properties.memoryHeaps[element].size; + device_access_memory += heap_size; } if (is_integrated) { const s64 available_memory = static_cast<s64>(device_access_memory - device_initial_usage); diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 39ae25f43a..8237868c6d 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -7,6 +7,7 @@ #pragma once #include <atomic> +#include <cstdlib> #include <optional> #include <set> #include <span> @@ -326,6 +327,28 @@ public: return IsSparseBindingSupported() && features.features.sparseResidencyImage2D; } + /// Whether a sparse image may bind the same memory to several of its blocks. + bool IsSparseResidencyAliasedSupported() const { + return IsSparseResidencyImage2DSupported() && features.features.sparseResidencyAliased; + } + + /// Whether images with no memory bound may be sampled and used as attachments. AMD's drivers + /// fault on it (the GPU hangs shortly after the first such draw), so large images there are + /// backed normally. RADV is counted in until someone shows it doesn't need it. + /// EDEN_NX2_SPARSE_UNBACKED=1 allows unbound images on any driver, =0 on none. + bool CanUseUnbackedSparseImages() const { + static const char forced = [] { + const char* value = std::getenv("EDEN_NX2_SPARSE_UNBACKED"); + return value == nullptr ? '\0' : value[0]; + }(); + if (forced == '0' || forced == '1') { + return forced == '1'; + } + const VkDriverIdKHR id = GetDriverID(); + return id != VK_DRIVER_ID_AMD_PROPRIETARY && id != VK_DRIVER_ID_AMD_OPEN_SOURCE && + id != VK_DRIVER_ID_MESA_RADV; + } + /// Returns true for tile-based deferred renderers. bool IsTiler() const { switch (GetDriverID()) { @@ -968,7 +991,9 @@ FN_MAX_LIMIT_LIST u64 GetDeviceMemoryUsage() const; - /// The driver's current memory budget for this process (VK_EXT_memory_budget), 0 without it. + /// The driver's current memory budget for this process (VK_EXT_memory_budget), in + /// GetDeviceMemoryUsage() terms: the usage at which the main device-local heap reaches its + /// budget. 0 without the extension. u64 GetDeviceMemoryBudget() const; u32 GetSetsPerPool() const { @@ -1257,6 +1282,8 @@ private: std::set<std::string, std::less<>> supported_extensions; ///< Reported Vulkan extensions. std::set<std::string, std::less<>> loaded_extensions; ///< Loaded Vulkan extensions. std::vector<size_t> valid_heap_memory; ///< Heaps used. + size_t main_heap{}; ///< Largest device-local heap, where images live. + u64 heap_size_limit{}; ///< EDEN_NX2_VRAM_LIMIT_MIB for device-local heaps, 0 if unset. /// Format properties dictionary. ::Common::unordered_map<VkFormat, VkFormatProperties> format_properties; diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index a0c96d1b45..1798611e26 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -280,12 +280,15 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat device.GetDispatchLoader()); } -vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) const { +vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased) const { if (ci.imageType == VK_IMAGE_TYPE_1D) { return vk::Image{}; } VkImageCreateInfo sparse_ci = ci; sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT; + if (aliased) { + sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_ALIASED_BIT; + } VkImage sparse{}; if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr, &sparse) != VK_SUCCESS) { diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index f3c6c177a2..e1c267ab79 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -109,7 +109,8 @@ namespace Vulkan { /// A sparse resident image with nothing bound (reads return zero, writes are dropped), or /// a null image when the device can't make one. - vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const; + /// aliased: its sparse blocks may share memory (VK_IMAGE_CREATE_SPARSE_ALIASED_BIT). + vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased = false) const; /// Device memory to bind to part of a sparse resource. Null when there is none left. VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements, diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index e6906a4ef0..40ff1d45f9 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -358,6 +358,8 @@ target_sources(yuzu # TODO(crueter): Move Assets.car/icon/icns handling to its own Module. if (APPLE) + target_sources(yuzu PRIVATE util/macos_sleep.cpp util/macos_sleep.h) + # Xcode will automatically generate the Assets.car and icns file for us. set(_dist "${CMAKE_SOURCE_DIR}/dist") if (CMAKE_GENERATOR MATCHES "Xcode") diff --git a/src/yuzu/Info.plist b/src/yuzu/Info.plist index cf6f597e99..9b6d49b87a 100644 --- a/src/yuzu/Info.plist +++ b/src/yuzu/Info.plist @@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleExecutable</key> - <string>${EXECUTABLE_NAME}</string> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> <key>CFBundleGetInfoString</key> <string></string> <key>CFBundleIconFile</key> diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 641f65d82a..e4a140fff5 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -225,6 +225,9 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { continue; } #endif + if (setting->Id() == Settings::values.frame_gen.Id()) { + continue; + } ConfigurationShared::Widget* widget = [&]() { if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) { diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp index 5e8e04be58..0e829e9100 100644 --- a/src/yuzu/configuration/shared_widget.cpp +++ b/src/yuzu/configuration/shared_widget.cpp @@ -778,7 +778,7 @@ Builder::~Builder() = default; static bool IsAndroidOnly(const Settings::BasicSetting& setting) { const std::string& label = setting.GetLabel(); - return label.starts_with("frame_gen") || label == "emulate_bgr565"; + return label == "emulate_bgr565"; } Widget* Builder::BuildWidget(Settings::BasicSetting* setting, diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp index 6b89e59253..cd7fc19de4 100644 --- a/src/yuzu/game/game_list.cpp +++ b/src/yuzu/game/game_list.cpp @@ -295,7 +295,6 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) { if (!chunk.isEmpty()) { callback(chunk); } - QCoreApplication::processEvents(); } }; diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index d59ac16c07..ab6d243046 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -85,6 +85,10 @@ #include <QStatusBar> #include <QtConcurrentRun> +#ifdef __APPLE__ +#include "util/macos_sleep.h" +#endif + // Qt Common // #include "qt_common/config/shared_translation.h" #include "qt_common/config/uisettings.h" @@ -493,7 +497,8 @@ MainWindow::MainWindow(bool has_broken_vulkan) VkDeviceInfo::PopulateRecords(vk_device_records, this->window()->windowHandle()); } -#if !defined(_WIN32) + // On macOS, SDL video would pump AppKit events alongside Qt and disrupt native menus. +#if !defined(_WIN32) && !defined(__APPLE__) SDL_InitSubSystem(SDL_INIT_VIDEO); // Set a screensaver inhibition reason string. Currently passed to DBus by SDL and visible to @@ -549,6 +554,10 @@ MainWindow::MainWindow(bool has_broken_vulkan) } MainWindow::~MainWindow() { +#ifdef __APPLE__ + AllowOSSleep(); +#endif + // will get automatically deleted otherwise if (render_window->parent() == nullptr) { delete render_window; @@ -1758,6 +1767,10 @@ void MainWindow::OnSigInterruptNotifierActivated() { void MainWindow::PreventOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(__APPLE__) + if (sleep_assertion_id == 0) { + sleep_assertion_id = MacOS::PreventSleep(); + } #else SDL_DisableScreenSaver(); #endif @@ -1766,6 +1779,11 @@ void MainWindow::PreventOSSleep() { void MainWindow::AllowOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(__APPLE__) + if (sleep_assertion_id != 0) { + MacOS::AllowSleep(sleep_assertion_id); + sleep_assertion_id = 0; + } #else SDL_EnableScreenSaver(); #endif diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index 627ade4f6b..5181b3fc68 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -547,6 +547,9 @@ private: bool auto_paused = false; bool auto_muted = false; +#ifdef __APPLE__ + u32 sleep_assertion_id = 0; +#endif QTimer mouse_hide_timer; QTimer update_input_timer; diff --git a/src/yuzu/util/macos_sleep.cpp b/src/yuzu/util/macos_sleep.cpp new file mode 100644 index 0000000000..7525c37dca --- /dev/null +++ b/src/yuzu/util/macos_sleep.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <IOKit/pwr_mgt/IOPMLib.h> +#include "common/logging.h" +#include "yuzu/util/macos_sleep.h" + +namespace MacOS { + +u32 PreventSleep() { + IOPMAssertionID assertion_id = kIOPMNullAssertionID; + const auto result = + IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, + kIOPMAssertionLevelOn, CFSTR("Running a game"), &assertion_id); + if (result != kIOReturnSuccess) { + LOG_WARNING(Frontend, "Failed to prevent display sleep: {}", result); + return kIOPMNullAssertionID; + } + return assertion_id; +} + +void AllowSleep(u32 assertion_id) { + IOPMAssertionRelease(assertion_id); +} + +} // namespace MacOS diff --git a/src/yuzu/util/macos_sleep.h b/src/yuzu/util/macos_sleep.h new file mode 100644 index 0000000000..600dd7c553 --- /dev/null +++ b/src/yuzu/util/macos_sleep.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" + +namespace MacOS { + +u32 PreventSleep(); +void AllowSleep(u32 assertion_id); + +} // namespace MacOS diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp index ecb8171ac7..93bbbfb8fc 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp @@ -38,8 +38,10 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co titlebar_timer = SDL_AddTimer(2000, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 { auto* this_ = (EmuWindow_SDL3*)userdata; auto const results = this_->system.GetAndResetPerfStats(); - auto const title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f); - SDL_SetWindowTitle(this_->render_window, title.c_str()); + auto* const title = new std::string(fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f)); + // Timers run on their own thread and AppKit throws when a title is set off the main + // thread: ApplyPendingTitle sets it from the main loop. + delete this_->pending_title.exchange(title); return 2000; }, this); mouse_timer = SDL_AddTimer(100, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 { @@ -55,6 +57,7 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co EmuWindow_SDL3::~EmuWindow_SDL3() { SDL_RemoveTimer(titlebar_timer); SDL_RemoveTimer(mouse_timer); + delete pending_title.exchange(nullptr); system.HIDCore().UnloadInputDevices(); input_subsystem->Shutdown(); SDL_Quit(); @@ -154,6 +157,13 @@ bool EmuWindow_SDL3::IsOpen() const { return is_open; } +void EmuWindow_SDL3::ApplyPendingTitle() { + if (std::string* const title = pending_title.exchange(nullptr)) { + SDL_SetWindowTitle(render_window, title->c_str()); + delete title; + } +} + bool EmuWindow_SDL3::IsShown() const { return is_shown; } diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h index 0dc2a4983a..a76a2bfd85 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h @@ -6,7 +6,9 @@ #pragma once +#include <atomic> #include <cstdlib> +#include <string> #include <tuple> #include <utility> @@ -41,6 +43,9 @@ public: /// Whether the window is still open, and a close request hasn't yet been sent bool IsOpen() const; + /// Sets the title the titlebar timer prepared; call from the main thread. + void ApplyPendingTitle(); + /// Returns if window is shown (not minimized) bool IsShown() const override; @@ -92,6 +97,7 @@ protected: /// Periodic changer of titlebar (independent of event loop) SDL_TimerID titlebar_timer; + std::atomic<std::string*> pending_title{}; // Mouse resetter once it SDL_TimerID mouse_timer; diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 22abe6a759..4d71a29993 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -481,6 +481,9 @@ static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) { extern "C" SDL_AppResult SDL_AppIterate(void *appstate) { SdlState *state = (SdlState *)appstate; RunKeyScript(state->input_subsystem); + if (state->emu_window) { + state->emu_window->ApplyPendingTitle(); + } return state->emu_window && state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS; } extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { |
