aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 16:21:45 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-01 16:21:45 -0500
commita41d11108b6478ad862fc7fcac36ab51c83b8602 (patch)
tree2c16c13764e6ef2b48c7327055fde64d7af2cd37 /src
parent01effa81ab0e151f8cfd674bf4da952d24645943 (diff)
parentac22c4087d9a6380c1937337530a5d4e54808ad1 (diff)
Merge remote-tracking branch 'origin/nx2' into merge/nx2-experimental-1101
# Conflicts: # src/core/loader/deconstructed_rom_directory.cpp
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt1
-rw-r--r--src/android/app/src/main/res/values/arrays.xml4
-rw-r--r--src/android/app/src/main/res/values/strings.xml1
-rw-r--r--src/common/CMakeLists.txt2
-rw-r--r--src/common/settings.cpp21
-rw-r--r--src/common/settings.h19
-rw-r--r--src/common/settings_enums.h3
-rw-r--r--src/core/CMakeLists.txt1
-rw-r--r--src/core/arm/nce/visitor_base.h26
-rw-r--r--src/core/core.cpp4
-rw-r--r--src/core/device_memory_manager.h5
-rw-r--r--src/core/device_memory_manager.inc53
-rw-r--r--src/core/hle/kernel/k_address_space_info.cpp6
-rw-r--r--src/core/hle/kernel/svc/svc_physical_memory.cpp7
-rw-r--r--src/core/hle/service/am/applet_manager.cpp10
-rw-r--r--src/core/hle/service/am/applet_manager.h1
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard.cpp9
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard_types.h1
-rw-r--r--src/core/hle/service/am/frontend/applets.cpp2
-rw-r--r--src/core/hle/service/am/lifecycle_manager.cpp8
-rw-r--r--src/core/hle/service/am/lifecycle_manager.h2
-rw-r--r--src/core/hle/service/am/service/application_functions.cpp15
-rw-r--r--src/core/hle/service/am/service/application_functions.h2
-rw-r--r--src/core/hle/service/am/service/library_applet_creator.cpp42
-rw-r--r--src/core/hle/service/audio/nx2_audio_user_client.h16
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp29
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp11
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h22
-rw-r--r--src/core/hle/service/nvdrv/nvdrv.cpp1
-rw-r--r--src/core/hle/service/nx2_gsv.h562
-rw-r--r--src/core/hle/service/nx2_streamplay.h65
-rw-r--r--src/core/hle/service/sm/sm.cpp9
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp89
-rw-r--r--src/core/memory.cpp99
-rw-r--r--src/core/memory.h6
-rw-r--r--src/qt_common/config/shared_translation.cpp15
-rw-r--r--src/qt_common/config/shared_translation.h6
-rw-r--r--src/qt_common/game_list/model.cpp23
-rw-r--r--src/qt_common/game_list/model.h1
-rw-r--r--src/qt_common/game_list/worker.cpp289
-rw-r--r--src/qt_common/game_list/worker.h3
-rw-r--r--src/shader_recompiler/CMakeLists.txt1
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_undefined.cpp22
-rw-r--r--src/shader_recompiler/frontend/ampere/lower_to_maxwell.h3
-rw-r--r--src/shader_recompiler/ir_opt/constant_propagation_pass.cpp36
-rw-r--r--src/shader_recompiler/ir_opt/predicate_mask.h111
-rw-r--r--src/shader_recompiler/object_pool.h7
-rw-r--r--src/tests/CMakeLists.txt1
-rw-r--r--src/tests/video_core/ampere_returns.cpp51
-rw-r--r--src/video_core/CMakeLists.txt4
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h10
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h31
-rw-r--r--src/video_core/host1x/codecs/codec.cpp2
-rw-r--r--src/video_core/host1x/ffmpeg.cpp2
-rw-r--r--src/video_core/host1x/gpu_device_memory_manager.cpp5
-rw-r--r--src/video_core/host1x/nvdec_common.h1
-rw-r--r--src/video_core/host1x/nx2_video_decoder.cpp4
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt2
-rw-r--r--src/video_core/host_shaders/fsr3_frame_interpolation.comp106
-rw-r--r--src/video_core/host_shaders/fsr3_optical_flow.comp112
-rw-r--r--src/video_core/nx2.h66
-rw-r--r--src/video_core/renderer_opengl/gl_blit_screen.cpp1
-rw-r--r--src/video_core/renderer_opengl/gl_texture_cache.cpp8
-rw-r--r--src/video_core/renderer_opengl/present/layer.cpp2
-rw-r--r--src/video_core/renderer_vulkan/maxwell_to_vk.cpp2
-rw-r--r--src/video_core/renderer_vulkan/present/frame_gen.cpp83
-rw-r--r--src/video_core/renderer_vulkan/present/frame_gen.h2
-rw-r--r--src/video_core/renderer_vulkan/present/fsr3.cpp232
-rw-r--r--src/video_core/renderer_vulkan/present/fsr3.h97
-rw-r--r--src/video_core/renderer_vulkan/present/fsr3_frame_gen.cpp408
-rw-r--r--src/video_core/renderer_vulkan/present/fsr3_frame_gen.h94
-rw-r--r--src/video_core/renderer_vulkan/present/layer.cpp7
-rw-r--r--src/video_core/renderer_vulkan/present/layer.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_blit_screen.cpp1
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp4
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp7
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp488
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h56
-rw-r--r--src/video_core/surface.cpp2
-rw-r--r--src/video_core/texture_cache/image_base.h7
-rw-r--r--src/video_core/texture_cache/texture_cache.h174
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h20
-rw-r--r--src/video_core/texture_cache/types.h9
-rw-r--r--src/video_core/texture_cache/util.cpp214
-rw-r--r--src/video_core/texture_cache/util.h21
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp105
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h29
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp5
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h3
-rw-r--r--src/yuzu/CMakeLists.txt2
-rw-r--r--src/yuzu/Info.plist2
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp3
-rw-r--r--src/yuzu/configuration/shared_widget.cpp2
-rw-r--r--src/yuzu/game/game_list.cpp1
-rw-r--r--src/yuzu/main_window.cpp20
-rw-r--r--src/yuzu/main_window.h3
-rw-r--r--src/yuzu/util/macos_sleep.cpp25
-rw-r--r--src/yuzu/util/macos_sleep.h12
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.cpp14
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.h6
-rw-r--r--src/yuzu_cmd/yuzu.cpp3
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) {