From 3e2137a470bf2891bc0ea6cf0c7779405cab4975 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Sun, 25 Jan 2026 15:21:02 +1000 Subject: [PATCH] feat(video_core): implement comprehensive VRAM management system Add automatic VRAM leak prevention with configurable garbage collection to prevent device loss crashes during extended play sessions. New Settings: - vram_limit_mb: Configurable VRAM limit (0 = auto-detect 80%) - gc_aggressiveness: Off/Light/Moderate/Heavy/Extreme levels - texture_eviction_frames: Frames before texture eviction (default 2) - buffer_eviction_frames: Frames before buffer eviction (default 5) - sparse_texture_priority_eviction: Prioritize large sparse textures - log_vram_usage: Enable VRAM statistics logging Core Changes: - Enhanced texture cache with LRU eviction and sparse texture priority - Enhanced buffer cache with configurable eviction thresholds - Added VRAM pressure monitoring using VK_EXT_memory_budget - Emergency GC triggers at 90%/95% VRAM usage thresholds Platform Support: - Desktop: Settings in Graphics > Advanced tab - Android: Settings in Zep Zone category Fixes VRAM climbing steadily during gameplay until device loss. Target: Stable VRAM usage below configured limit for 2+ hours. Signed-off-by: Zephyron --- .../features/settings/model/BooleanSetting.kt | 6 +- .../features/settings/model/IntSetting.kt | 6 + .../settings/model/view/SettingsItem.kt | 55 +++ .../settings/ui/SettingsFragmentPresenter.kt | 9 + .../app/src/main/res/values/arrays.xml | 17 + .../app/src/main/res/values/strings.xml | 20 + .../configuration/shared_translation.cpp | 36 ++ src/common/settings.h | 56 +++ src/common/settings_enums.h | 26 ++ src/video_core/buffer_cache/buffer_cache.h | 172 ++++++- .../buffer_cache/buffer_cache_base.h | 38 ++ .../renderer_vulkan/renderer_vulkan.cpp | 43 ++ .../renderer_vulkan/vk_rasterizer.cpp | 11 + .../renderer_vulkan/vk_rasterizer.h | 4 + src/video_core/texture_cache/texture_cache.h | 431 ++++++++++++++++-- .../texture_cache/texture_cache_base.h | 56 +++ 16 files changed, 938 insertions(+), 48 deletions(-) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt index f0b07bd80..f6e273cb2 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/BooleanSetting.kt @@ -32,7 +32,11 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { SHOW_SHADER_BUILDING_OVERLAY("show_shader_building_overlay"), SHOW_PERFORMANCE_GRAPH("show_performance_graph"), USE_CONDITIONAL_RENDERING("use_conditional_rendering"), - AIRPLANE_MODE("airplane_mode"); + AIRPLANE_MODE("airplane_mode"), + + // VRAM Management settings (FIXED: VRAM leak prevention) + SPARSE_TEXTURE_PRIORITY_EVICTION("sparse_texture_priority_eviction"), + LOG_VRAM_USAGE("log_vram_usage"); override fun getBoolean(needsGlobal: Boolean): Boolean = NativeConfig.getBoolean(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt index 92298f804..f540dacb8 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt @@ -40,6 +40,12 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { VRAM_USAGE_MODE("vram_usage_mode"), EXTENDED_DYNAMIC_STATE("extended_dynamic_state"), + // VRAM Management settings (FIXED: VRAM leak prevention) + VRAM_LIMIT_MB("vram_limit_mb"), + GC_AGGRESSIVENESS("gc_aggressiveness"), + TEXTURE_EVICTION_FRAMES("texture_eviction_frames"), + BUFFER_EVICTION_FRAMES("buffer_eviction_frames"), + // Applet Mode settings CABINET_APPLET_MODE("cabinet_applet_mode"), CONTROLLER_APPLET_MODE("controller_applet_mode"), diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt index 81ca3bc55..87f685395 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt @@ -479,6 +479,61 @@ abstract class SettingsItem( ) ) + // VRAM Management Settings (FIXED: VRAM leak prevention) + put( + SliderSetting( + IntSetting.VRAM_LIMIT_MB, + titleId = R.string.vram_limit_mb, + descriptionId = R.string.vram_limit_mb_description, + min = 0, + max = 16384, + units = " MB" + ) + ) + put( + SingleChoiceSetting( + IntSetting.GC_AGGRESSIVENESS, + titleId = R.string.gc_aggressiveness, + descriptionId = R.string.gc_aggressiveness_description, + choicesId = R.array.gcAggressivenessNames, + valuesId = R.array.gcAggressivenessValues + ) + ) + put( + SliderSetting( + IntSetting.TEXTURE_EVICTION_FRAMES, + titleId = R.string.texture_eviction_frames, + descriptionId = R.string.texture_eviction_frames_description, + min = 1, + max = 60, + units = " frames" + ) + ) + put( + SliderSetting( + IntSetting.BUFFER_EVICTION_FRAMES, + titleId = R.string.buffer_eviction_frames, + descriptionId = R.string.buffer_eviction_frames_description, + min = 1, + max = 120, + units = " frames" + ) + ) + put( + SwitchSetting( + BooleanSetting.SPARSE_TEXTURE_PRIORITY_EVICTION, + titleId = R.string.sparse_texture_priority_eviction, + descriptionId = R.string.sparse_texture_priority_eviction_description + ) + ) + put( + SwitchSetting( + BooleanSetting.LOG_VRAM_USAGE, + titleId = R.string.log_vram_usage, + descriptionId = R.string.log_vram_usage_description + ) + ) + // Applet Mode Settings put( SingleChoiceSetting( diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt index f7d7ab2e0..f11a41d73 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -1036,6 +1036,15 @@ class SettingsFragmentPresenter( add(HeaderSetting(R.string.frame_skipping_header)) add(IntSetting.FRAME_SKIPPING.key) add(IntSetting.FRAME_SKIPPING_MODE.key) + + // VRAM Management settings (FIXED: VRAM leak prevention) + add(HeaderSetting(R.string.vram_management_header)) + add(IntSetting.VRAM_LIMIT_MB.key) + add(IntSetting.GC_AGGRESSIVENESS.key) + add(IntSetting.TEXTURE_EVICTION_FRAMES.key) + add(IntSetting.BUFFER_EVICTION_FRAMES.key) + add(BooleanSetting.SPARSE_TEXTURE_PRIORITY_EVICTION.key) + add(BooleanSetting.LOG_VRAM_USAGE.key) } } diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index e00d54ea5..e0e32ca11 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -440,6 +440,23 @@ 3 + + + @string/gc_aggressiveness_off + @string/gc_aggressiveness_light + @string/gc_aggressiveness_moderate + @string/gc_aggressiveness_heavy + @string/gc_aggressiveness_extreme + + + + 0 + 1 + 2 + 3 + 4 + + HLE (High-Level Emulation) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 58dfbc2a1..7770bcb0e 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -448,6 +448,7 @@ Memory Layout ASTC Settings Advanced Graphics + VRAM Management Applet Settings @@ -1273,6 +1274,25 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Enabled Adaptive Fixed + + + VRAM Limit (MB) + Maximum VRAM usage limit in megabytes. Set to 0 for auto-detection (80%% of available VRAM). Recommended: 6144 for 8GB GPUs, 4096 for 6GB GPUs. + GC Aggressiveness + Controls how aggressively the emulator evicts unused textures and buffers from VRAM. Higher levels free memory faster but may cause more texture reloading. + Texture Eviction Frames + Number of frames a texture must be unused before it can be evicted. Lower values free VRAM faster but may cause more texture reloading. + Buffer Eviction Frames + Number of frames a buffer must be unused before it can be evicted. Lower values free VRAM faster but may cause more buffer reloading. + Sparse Texture Priority Eviction + Prioritize evicting large sparse textures when VRAM pressure is high. Helps prevent VRAM exhaustion in games with large texture atlases. + Log VRAM Usage + Enable logging of VRAM usage statistics for debugging purposes. + Off (Not Recommended) + Light + Moderate (Recommended) + Heavy (Low VRAM) + Extreme (4GB VRAM) Frame Skipping diff --git a/src/citron/configuration/shared_translation.cpp b/src/citron/configuration/shared_translation.cpp index 46fa6cfcf..a1e9d854f 100644 --- a/src/citron/configuration/shared_translation.cpp +++ b/src/citron/configuration/shared_translation.cpp @@ -192,6 +192,31 @@ std::unique_ptr InitializeTranslations(QWidget* parent) { "of available video memory for performance. Has no effect on integrated graphics. " "Aggressive mode may severely impact the performance of other applications such as " "recording software.")); + + // FIXED: VRAM leak prevention - New VRAM management settings + INSERT(Settings, vram_limit_mb, tr("VRAM Limit (MB):"), + tr("Sets the maximum VRAM usage limit in megabytes. Set to 0 for auto-detection " + "(80% of available VRAM). Recommended: 6144 for 8GB GPUs, 4096 for 6GB GPUs.")); + INSERT(Settings, gc_aggressiveness, tr("GC Aggressiveness:"), + tr("Controls how aggressively the emulator evicts unused textures and buffers from VRAM.\n" + "Off: Disable automatic cleanup (not recommended, may cause crashes).\n" + "Light: Gentle cleanup, keeps more textures cached.\n" + "Moderate: Balanced cleanup (recommended for most users).\n" + "Heavy: Aggressive cleanup for low VRAM systems (6GB or less).\n" + "Extreme: Maximum cleanup for very low VRAM systems (4GB).")); + INSERT(Settings, texture_eviction_frames, tr("Texture Eviction Frames:"), + tr("Number of frames a texture must be unused before it can be evicted. " + "Lower values free VRAM faster but may cause more texture reloading.")); + INSERT(Settings, buffer_eviction_frames, tr("Buffer Eviction Frames:"), + tr("Number of frames a buffer must be unused before it can be evicted. " + "Lower values free VRAM faster but may cause more buffer reloading.")); + INSERT(Settings, sparse_texture_priority_eviction, tr("Sparse Texture Priority Eviction"), + tr("Prioritize evicting large sparse textures when VRAM pressure is high. " + "This helps prevent VRAM exhaustion in games with large texture atlases.")); + INSERT(Settings, log_vram_usage, tr("Log VRAM Usage"), + tr("Enable logging of VRAM usage statistics for debugging purposes. " + "Check the log for 'VRAM GC' and 'VRAM Status' messages.")); + INSERT( Settings, vsync_mode, tr("VSync Mode:"), tr("FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen " @@ -367,6 +392,17 @@ std::unique_ptr ComboboxEnumeration(QWidget* parent) { PAIR(ExtendedDynamicState, EDS2, tr("EDS2")), PAIR(ExtendedDynamicState, EDS3, tr("EDS3")), }}); + + // FIXED: VRAM leak prevention - GC Aggressiveness dropdown options + translations->insert({Settings::EnumMetadata::Index(), + { + PAIR(GCAggressiveness, Off, tr("Off (Not Recommended)")), + PAIR(GCAggressiveness, Light, tr("Light")), + PAIR(GCAggressiveness, Moderate, tr("Moderate (Recommended)")), + PAIR(GCAggressiveness, Heavy, tr("Heavy (Low VRAM)")), + PAIR(GCAggressiveness, Extreme, tr("Extreme (4GB VRAM)")), + }}); + translations->insert({Settings::EnumMetadata::Index(), { #ifdef HAS_OPENGL diff --git a/src/common/settings.h b/src/common/settings.h index 7dd29b402..76fffa163 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -66,6 +66,7 @@ SWITCHABLE(AstcDecodeMode, true); SWITCHABLE(AstcRecompression, true); SWITCHABLE(AudioMode, true); SWITCHABLE(ExtendedDynamicState, true); +SWITCHABLE(GCAggressiveness, true); SWITCHABLE(CpuBackend, true); SWITCHABLE(CpuAccuracy, true); SWITCHABLE(FullscreenMode, true); @@ -501,6 +502,61 @@ struct Values { VramUsageMode::Insane, "vram_usage_mode", Category::RendererAdvanced}; + + // FIXED: VRAM leak prevention - New memory management settings + // VRAM limit in MB (0 = auto-detect based on GPU, default 6144 for 6GB limit) + SwitchableSetting vram_limit_mb{linkage, + 0, // 0 = auto-detect (80% of available VRAM) + 0, // min: 0 (auto) + 32768, // max: 32GB + "vram_limit_mb", + Category::RendererAdvanced, + Specialization::Default, + true, + true}; + + // GC aggressiveness level for texture/buffer cache eviction + SwitchableSetting gc_aggressiveness{linkage, + GCAggressiveness::Moderate, + GCAggressiveness::Off, + GCAggressiveness::Extreme, + "gc_aggressiveness", + Category::RendererAdvanced, + Specialization::Default, + true, + true}; + + // Number of frames before unused textures are evicted (default 2) + SwitchableSetting texture_eviction_frames{linkage, + 2, // default: 2 frames + 1, // min: 1 frame + 60, // max: 60 frames (1 second at 60fps) + "texture_eviction_frames", + Category::RendererAdvanced, + Specialization::Default, + true, + true}; + + // Number of frames before unused buffers are evicted (default 5) + SwitchableSetting buffer_eviction_frames{linkage, + 5, // default: 5 frames + 1, // min: 1 frame + 120, // max: 120 frames (2 seconds at 60fps) + "buffer_eviction_frames", + Category::RendererAdvanced, + Specialization::Default, + true, + true}; + + // Enable sparse texture priority eviction (evict large unmapped pages first) + SwitchableSetting sparse_texture_priority_eviction{linkage, true, + "sparse_texture_priority_eviction", + Category::RendererAdvanced}; + + // Enable VRAM usage logging for debugging + SwitchableSetting log_vram_usage{linkage, false, "log_vram_usage", + Category::RendererAdvanced}; + SwitchableSetting async_presentation{linkage, #ifdef ANDROID true, diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 1b654bfb0..49ae303e9 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -880,6 +880,32 @@ inline u32 EnumMetadata::Index() { return 26; } +// FIXED: VRAM leak prevention - GC aggressiveness levels +enum class GCAggressiveness : u32 { + Off = 0, // Disable automatic GC (not recommended) + Light = 1, // Light GC - only evict very old textures + Moderate = 2, // Moderate GC - balanced eviction (default) + Heavy = 3, // Heavy GC - aggressive eviction for low VRAM systems + Extreme = 4, // Extreme GC - maximum eviction for 4GB VRAM systems +}; + +template <> +inline std::vector> +EnumMetadata::Canonicalizations() { + return { + {"Off", GCAggressiveness::Off}, + {"Light", GCAggressiveness::Light}, + {"Moderate", GCAggressiveness::Moderate}, + {"Heavy", GCAggressiveness::Heavy}, + {"Extreme", GCAggressiveness::Extreme}, + }; +} + +template <> +inline u32 EnumMetadata::Index() { + return 27; +} + template inline std::string CanonicalizeEnum(Type id) { diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 9302d6ace..c8d14c78c 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -26,24 +26,60 @@ BufferCache

::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, R gpu_modified_ranges.Clear(); inline_buffer_id = NULL_BUFFER_ID; + // FIXED: VRAM leak prevention - Initialize buffer VRAM management from settings + const u32 configured_limit_mb = Settings::values.vram_limit_mb.GetValue(); + if (!runtime.CanReportMemoryUsage()) { minimum_memory = DEFAULT_EXPECTED_MEMORY; critical_memory = DEFAULT_CRITICAL_MEMORY; + vram_limit_bytes = configured_limit_mb > 0 ? static_cast(configured_limit_mb) * 1_MiB + : 6_GiB; return; } const s64 device_local_memory = static_cast(runtime.GetDeviceLocalMemory()); - const s64 min_spacing_expected = device_local_memory - 1_GiB; - const s64 min_spacing_critical = device_local_memory - 512_MiB; - const s64 mem_threshold = std::min(device_local_memory, TARGET_THRESHOLD); - const s64 min_vacancy_expected = (6 * mem_threshold) / 10; - const s64 min_vacancy_critical = (2 * mem_threshold) / 10; - minimum_memory = static_cast( - std::max(std::min(device_local_memory - min_vacancy_expected, min_spacing_expected), - DEFAULT_EXPECTED_MEMORY)); - critical_memory = static_cast( - std::max(std::min(device_local_memory - min_vacancy_critical, min_spacing_critical), - DEFAULT_CRITICAL_MEMORY)); + + // FIXED: VRAM leak prevention - Use configured limit or auto-detect + if (configured_limit_mb > 0) { + vram_limit_bytes = static_cast(configured_limit_mb) * 1_MiB; + } else { + vram_limit_bytes = static_cast(device_local_memory * 0.80); + } + + // Adjust thresholds based on GC aggressiveness setting + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + f32 expected_ratio = 0.5f; + f32 critical_ratio = 0.7f; + + switch (gc_level) { + case Settings::GCAggressiveness::Off: + expected_ratio = 0.90f; + critical_ratio = 0.95f; + break; + case Settings::GCAggressiveness::Light: + expected_ratio = 0.70f; + critical_ratio = 0.85f; + break; + case Settings::GCAggressiveness::Moderate: + expected_ratio = 0.50f; + critical_ratio = 0.70f; + break; + case Settings::GCAggressiveness::Heavy: + expected_ratio = 0.40f; + critical_ratio = 0.60f; + break; + case Settings::GCAggressiveness::Extreme: + expected_ratio = 0.30f; + critical_ratio = 0.50f; + break; + } + + minimum_memory = static_cast(vram_limit_bytes * expected_ratio); + critical_memory = static_cast(vram_limit_bytes * critical_ratio); + + LOG_INFO(Render_Vulkan, + "Buffer cache VRAM initialized: limit={}MB, minimum={}MB, critical={}MB", + vram_limit_bytes / 1_MiB, minimum_memory / 1_MiB, critical_memory / 1_MiB); } template @@ -51,20 +87,90 @@ BufferCache

::~BufferCache() = default; template void BufferCache

::RunGarbageCollector() { + // FIXED: VRAM leak prevention - Enhanced buffer GC with settings integration + + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + if (gc_level == Settings::GCAggressiveness::Off) { + return; // GC disabled by user + } + const bool aggressive_gc = total_used_memory >= critical_memory; - const u64 ticks_to_destroy = aggressive_gc ? 60 : 120; - int num_iterations = aggressive_gc ? 64 : 32; - const auto clean_up = [this, &num_iterations](BufferId buffer_id) { + const bool emergency_gc = total_used_memory >= static_cast(vram_limit_bytes * BUFFER_VRAM_CRITICAL_THRESHOLD); + + // FIXED: VRAM leak prevention - Get eviction frames from settings + const u64 eviction_frames = Settings::values.buffer_eviction_frames.GetValue(); + + // Adjust based on GC level + u64 base_ticks = eviction_frames; + int base_iterations = 32; + + switch (gc_level) { + case Settings::GCAggressiveness::Light: + base_ticks = eviction_frames * 2; + base_iterations = 16; + break; + case Settings::GCAggressiveness::Moderate: + base_ticks = eviction_frames; + base_iterations = 32; + break; + case Settings::GCAggressiveness::Heavy: + base_ticks = std::max(1ULL, eviction_frames / 2); + base_iterations = 64; + break; + case Settings::GCAggressiveness::Extreme: + base_ticks = 1; + base_iterations = 128; + break; + default: + break; + } + + u64 ticks_to_destroy; + int num_iterations; + + if (emergency_gc) { + ticks_to_destroy = 1; + num_iterations = base_iterations * 4; + LOG_WARNING(Render_Vulkan, "Buffer cache emergency GC: usage={}MB, limit={}MB", + total_used_memory / 1_MiB, vram_limit_bytes / 1_MiB); + } else if (aggressive_gc) { + ticks_to_destroy = std::max(1ULL, base_ticks / 2); + num_iterations = base_iterations * 2; + } else { + ticks_to_destroy = base_ticks; + num_iterations = base_iterations; + } + + u64 bytes_freed = 0; + const auto clean_up = [this, &num_iterations, &bytes_freed](BufferId buffer_id) { if (num_iterations == 0) { return true; } --num_iterations; auto& buffer = slot_buffers[buffer_id]; + const u64 buffer_size = buffer.SizeBytes(); + DownloadBufferMemory(buffer); DeleteBuffer(buffer_id); + + bytes_freed += buffer_size; + --buffer_count; + if (buffer_size >= LARGE_BUFFER_THRESHOLD) { + large_buffer_memory -= buffer_size; + --large_buffer_count; + } return false; }; lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, clean_up); + + evicted_buffer_bytes += bytes_freed; + + // FIXED: VRAM leak prevention - Log buffer eviction if enabled + if (Settings::values.log_vram_usage.GetValue() && bytes_freed > 0) { + LOG_INFO(Render_Vulkan, "Buffer GC: evicted {}MB, total={}MB, usage={}MB/{}MB", + bytes_freed / 1_MiB, evicted_buffer_bytes / 1_MiB, total_used_memory / 1_MiB, + vram_limit_bytes / 1_MiB); + } } template @@ -96,9 +202,22 @@ void BufferCache

::TickFrame() { if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); } - if (total_used_memory >= minimum_memory) { + + // FIXED: VRAM leak prevention - Enhanced buffer GC triggering + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + const bool should_gc = gc_level != Settings::GCAggressiveness::Off && + (total_used_memory >= minimum_memory || + total_used_memory >= static_cast(vram_limit_bytes * BUFFER_VRAM_WARNING_THRESHOLD)); + + if (should_gc) { RunGarbageCollector(); } + + // FIXED: VRAM leak prevention - Force additional GC if still above critical + if (total_used_memory >= critical_memory && gc_level != Settings::GCAggressiveness::Off) { + RunGarbageCollector(); + } + ++frame_tick; delayed_destruction_ring.Tick(); @@ -1420,12 +1539,31 @@ template void BufferCache

::ChangeRegister(BufferId buffer_id) { Buffer& buffer = slot_buffers[buffer_id]; const auto size = buffer.SizeBytes(); + const u64 aligned_size = Common::AlignUp(size, 1024); + const bool is_large = aligned_size >= LARGE_BUFFER_THRESHOLD; + if (insert) { - total_used_memory += Common::AlignUp(size, 1024); + total_used_memory += aligned_size; buffer.setLRUID(lru_cache.Insert(buffer_id, frame_tick)); + + // FIXED: VRAM leak prevention - Track buffer statistics + ++buffer_count; + if (is_large) { + large_buffer_memory += aligned_size; + ++large_buffer_count; + } } else { - total_used_memory -= Common::AlignUp(size, 1024); + total_used_memory -= aligned_size; lru_cache.Free(buffer.getLRUID()); + + // FIXED: VRAM leak prevention - Update buffer statistics on removal + if (buffer_count > 0) { + --buffer_count; + } + if (is_large && large_buffer_count > 0) { + large_buffer_memory -= aligned_size; + --large_buffer_count; + } } const DAddr device_addr_begin = buffer.CpuAddr(); const DAddr device_addr_end = device_addr_begin + size; diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h index 2e6501419..1d885136c 100644 --- a/src/video_core/buffer_cache/buffer_cache_base.h +++ b/src/video_core/buffer_cache/buffer_cache_base.h @@ -175,6 +175,12 @@ class BufferCache : public VideoCommon::ChannelSetupCaches= minimum_memory; + } + void BindHostIndexBuffer(); void BindHostVertexBuffers(); @@ -488,6 +519,13 @@ public: u64 critical_memory = 0; BufferId inline_buffer_id; + // FIXED: VRAM leak prevention - Enhanced buffer memory tracking + u64 vram_limit_bytes = 0; // Configured VRAM limit for buffers + u64 large_buffer_memory = 0; // Memory used by large buffers (>8MB) + u64 evicted_buffer_bytes = 0; // Total bytes evicted since start + u32 buffer_count = 0; // Total buffer count + u32 large_buffer_count = 0; // Large buffer count + std::array> CACHING_PAGEBITS)> page_table; Common::ScratchBuffer tmp_buffer; }; diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index ae70dc479..3e24cd84d 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -159,6 +159,25 @@ void RendererVulkan::Composite(std::span framebu render_window.OnFrameDisplayed(); }; + // FIXED: VRAM leak prevention - Check VRAM pressure before rendering + if (device.CanReportMemoryUsage()) { + const u64 current_usage = device.GetDeviceMemoryUsage(); + const u64 total_vram = device.GetDeviceLocalMemory(); + const u32 configured_limit = Settings::values.vram_limit_mb.GetValue(); + const u64 vram_limit = configured_limit > 0 + ? static_cast(configured_limit) * 1024ULL * 1024ULL + : static_cast(total_vram * 0.80); + + // If VRAM usage is above 90% of limit, trigger emergency GC on texture/buffer caches + if (current_usage >= static_cast(vram_limit * 0.90)) { + LOG_WARNING(Render_Vulkan, + "VRAM pressure critical: {}MB/{}MB ({:.1f}%), triggering emergency GC", + current_usage / (1024ULL * 1024ULL), vram_limit / (1024ULL * 1024ULL), + (static_cast(current_usage) / vram_limit) * 100.0f); + rasterizer.TriggerMemoryGC(); + } + } + RenderAppletCaptureLayer(framebuffers); if (!render_window.IsShown()) { @@ -201,6 +220,30 @@ void RendererVulkan::Report() const { LOG_INFO(Render_Vulkan, "Vulkan: {}", api_version); LOG_INFO(Render_Vulkan, "Available VRAM: {:.2f} GiB", available_vram); + // FIXED: VRAM leak prevention - Report VRAM management settings + const u32 vram_limit_mb = Settings::values.vram_limit_mb.GetValue(); + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + const u32 texture_eviction = Settings::values.texture_eviction_frames.GetValue(); + const u32 buffer_eviction = Settings::values.buffer_eviction_frames.GetValue(); + + if (vram_limit_mb > 0) { + LOG_INFO(Render_Vulkan, "VRAM Limit: {} MB (configured)", vram_limit_mb); + } else { + LOG_INFO(Render_Vulkan, "VRAM Limit: Auto ({:.0f} MB, 80% of available)", + available_vram * 0.8 * 1024.0); + } + LOG_INFO(Render_Vulkan, "GC Aggressiveness: {}, Texture eviction: {} frames, Buffer eviction: {} frames", + static_cast(gc_level), texture_eviction, buffer_eviction); + + // FIXED: VRAM leak prevention - Report VK_EXT_memory_budget support + if (device.CanReportMemoryUsage()) { + const auto current_usage = device.GetDeviceMemoryUsage(); + LOG_INFO(Render_Vulkan, "VK_EXT_memory_budget: Supported, Current usage: {:.2f} GiB", + static_cast(current_usage) / f64{1_GiB}); + } else { + LOG_INFO(Render_Vulkan, "VK_EXT_memory_budget: Not supported (using estimates)"); + } + static constexpr auto field = Common::Telemetry::FieldType::UserSystem; telemetry_session.AddField(field, "GPU_Vendor", vendor_name); telemetry_session.AddField(field, "GPU_Model", model_name); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index e9e4bc52a..e0f90c402 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -861,6 +861,17 @@ u64 RasterizerVulkan::GetStagingMemoryUsage() const { } } +// FIXED: VRAM leak prevention - Trigger garbage collection on texture/buffer caches +void RasterizerVulkan::TriggerMemoryGC() { + std::scoped_lock lock{texture_cache.mutex, buffer_cache.mutex}; + + // Trigger GC on both caches + texture_cache.TriggerGarbageCollection(); + buffer_cache.TriggerGarbageCollection(); + + LOG_DEBUG(Render_Vulkan, "Manual memory GC triggered"); +} + bool RasterizerVulkan::AccelerateConditionalRendering() { gpu_memory->FlushCaching(); return query_cache.AccelerateHostConditionalRendering(); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 9107efa61..e79b86c6e 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -125,6 +125,10 @@ public: u64 GetBufferMemoryUsage() const; u64 GetTextureMemoryUsage() const; u64 GetStagingMemoryUsage() const; + + // FIXED: VRAM leak prevention - Trigger garbage collection on texture/buffer caches + void TriggerMemoryGC(); + bool AccelerateConditionalRendering() override; bool AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst, diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index be4c3c948..f16b0c6fe 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -50,21 +50,59 @@ TextureCache

::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag void(slot_image_views.insert(runtime, NullImageViewParams{})); void(slot_samplers.insert(runtime, sampler_descriptor)); + // FIXED: VRAM leak prevention - Initialize VRAM limit from settings + const u32 configured_limit_mb = Settings::values.vram_limit_mb.GetValue(); + if constexpr (HAS_DEVICE_MEMORY_INFO) { const s64 device_local_memory = static_cast(runtime.GetDeviceLocalMemory()); - const s64 min_spacing_expected = device_local_memory - 1_GiB; - const s64 min_spacing_critical = device_local_memory - 512_MiB; - const s64 mem_threshold = std::min(device_local_memory, TARGET_THRESHOLD); - const s64 min_vacancy_expected = (6 * mem_threshold) / 10; - const s64 min_vacancy_critical = (2 * mem_threshold) / 10; - expected_memory = static_cast( - std::max(std::min(device_local_memory - min_vacancy_expected, min_spacing_expected), - DEFAULT_EXPECTED_MEMORY)); - critical_memory = static_cast( - std::max(std::min(device_local_memory - min_vacancy_critical, min_spacing_critical), - DEFAULT_CRITICAL_MEMORY)); - minimum_memory = static_cast((device_local_memory - mem_threshold) / 2); + + // FIXED: VRAM leak prevention - Use configured limit or auto-detect (80% of VRAM) + if (configured_limit_mb > 0) { + vram_limit_bytes = static_cast(configured_limit_mb) * 1_MiB; + } else { + // Auto-detect: use 80% of available VRAM as limit + vram_limit_bytes = static_cast(device_local_memory * 0.80); + } + + // Adjust thresholds based on VRAM limit and GC aggressiveness setting + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + f32 expected_ratio = 0.6f; + f32 critical_ratio = 0.8f; + + switch (gc_level) { + case Settings::GCAggressiveness::Off: + expected_ratio = 0.95f; + critical_ratio = 0.99f; + break; + case Settings::GCAggressiveness::Light: + expected_ratio = 0.75f; + critical_ratio = 0.90f; + break; + case Settings::GCAggressiveness::Moderate: + expected_ratio = 0.60f; + critical_ratio = 0.80f; + break; + case Settings::GCAggressiveness::Heavy: + expected_ratio = 0.50f; + critical_ratio = 0.70f; + break; + case Settings::GCAggressiveness::Extreme: + expected_ratio = 0.40f; + critical_ratio = 0.60f; + break; + } + + expected_memory = static_cast(vram_limit_bytes * expected_ratio); + critical_memory = static_cast(vram_limit_bytes * critical_ratio); + minimum_memory = static_cast(vram_limit_bytes * 0.25f); + + LOG_INFO(Render_Vulkan, + "VRAM Management initialized: limit={}MB, expected={}MB, critical={}MB, gc_level={}", + vram_limit_bytes / 1_MiB, expected_memory / 1_MiB, critical_memory / 1_MiB, + static_cast(gc_level)); } else { + vram_limit_bytes = configured_limit_mb > 0 ? static_cast(configured_limit_mb) * 1_MiB + : 6_GiB; // Default 6GB if no info expected_memory = DEFAULT_EXPECTED_MEMORY + 512_MiB; critical_memory = DEFAULT_CRITICAL_MEMORY + 1_GiB; minimum_memory = 0; @@ -73,37 +111,111 @@ TextureCache

::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag template void TextureCache

::RunGarbageCollector() { + // FIXED: VRAM leak prevention - Enhanced garbage collector with settings integration + + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + if (gc_level == Settings::GCAggressiveness::Off) { + return; // GC disabled by user + } + + // Reset per-frame stats + if (last_gc_frame != frame_tick) { + evicted_this_frame = 0; + gc_runs_this_frame = 0; + last_gc_frame = frame_tick; + } + ++gc_runs_this_frame; + bool high_priority_mode = false; bool aggressive_mode = false; + bool emergency_mode = false; u64 ticks_to_destroy = 0; size_t num_iterations = 0; + u64 bytes_freed = 0; - const auto Configure = [&](bool allow_aggressive) { + // FIXED: VRAM leak prevention - Get eviction frames from settings + const u64 eviction_frames = Settings::values.texture_eviction_frames.GetValue(); + const bool sparse_priority = Settings::values.sparse_texture_priority_eviction.GetValue(); + + const auto Configure = [&](bool allow_aggressive, bool allow_emergency) { high_priority_mode = total_used_memory >= expected_memory; aggressive_mode = allow_aggressive && total_used_memory >= critical_memory; - ticks_to_destroy = aggressive_mode ? 10ULL : high_priority_mode ? 25ULL : 50ULL; - num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10); + emergency_mode = allow_emergency && total_used_memory >= static_cast(vram_limit_bytes * VRAM_USAGE_EMERGENCY_THRESHOLD); + + // FIXED: VRAM leak prevention - Adjust iterations based on GC level + u64 base_ticks = eviction_frames; + size_t base_iterations = 10; + + switch (gc_level) { + case Settings::GCAggressiveness::Light: + base_ticks = eviction_frames * 2; + base_iterations = 5; + break; + case Settings::GCAggressiveness::Moderate: + base_ticks = eviction_frames; + base_iterations = 10; + break; + case Settings::GCAggressiveness::Heavy: + base_ticks = std::max(1ULL, eviction_frames / 2); + base_iterations = 20; + break; + case Settings::GCAggressiveness::Extreme: + base_ticks = 1; + base_iterations = 40; + break; + default: + break; + } + + if (emergency_mode) { + ticks_to_destroy = 1; + num_iterations = base_iterations * 4; + } else if (aggressive_mode) { + ticks_to_destroy = std::max(1ULL, base_ticks / 2); + num_iterations = base_iterations * 2; + } else if (high_priority_mode) { + ticks_to_destroy = base_ticks; + num_iterations = static_cast(base_iterations * 1.5); + } else { + ticks_to_destroy = base_ticks * 2; + num_iterations = base_iterations; + } }; - const auto Cleanup = [this, &num_iterations, &high_priority_mode, - &aggressive_mode](ImageId image_id) { + + const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode, + &emergency_mode, &bytes_freed, sparse_priority](ImageId image_id) { if (num_iterations == 0) { return true; } --num_iterations; auto& image = slot_images[image_id]; + + // Skip images being decoded if (True(image.flags & ImageFlagBits::IsDecoding)) { - // This image is still being decoded, deleting it will invalidate the slot - // used by the async decoder thread. return false; } - if (!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) { - return false; + + // FIXED: VRAM leak prevention - Prioritize sparse textures if enabled + const bool is_sparse = True(image.flags & ImageFlagBits::Sparse); + const u64 image_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes); + const bool is_large = image_size >= LARGE_TEXTURE_THRESHOLD; + + // Skip costly loads unless aggressive/emergency mode, unless it's a large sparse texture + if (!aggressive_mode && !emergency_mode && True(image.flags & ImageFlagBits::CostlyLoad)) { + if (!(sparse_priority && is_sparse && image_size >= SPARSE_EVICTION_PRIORITY_THRESHOLD)) { + return false; + } } + const bool must_download = image.IsSafeDownload() && False(image.flags & ImageFlagBits::BadOverlap); - if (!high_priority_mode && must_download) { + + // Skip downloads unless high priority or emergency + if (!high_priority_mode && !emergency_mode && must_download) { return false; } + + // Perform download if needed if (must_download) { auto map = runtime.DownloadStagingBuffer(image.unswizzled_size_bytes); const auto copies = FullDownloadCopies(image.info); @@ -112,16 +224,29 @@ void TextureCache

::RunGarbageCollector() { SwizzleImage(*gpu_memory, image.gpu_addr, image.info, copies, map.mapped_span, swizzle_data_buffer); } + + // Track eviction statistics + bytes_freed += Common::AlignUp(image_size, 1024); + if (is_sparse) { + sparse_texture_memory -= Common::AlignUp(image_size, 1024); + --sparse_texture_count; + } + if (is_large) { + large_texture_memory -= Common::AlignUp(image_size, 1024); + } + if (True(image.flags & ImageFlagBits::Tracked)) { UntrackImage(image, image_id); } UnregisterImage(image_id); DeleteImage(image_id, image.scale_tick > frame_tick + 5); + + // Adjust mode based on remaining memory pressure if (total_used_memory < critical_memory) { - if (aggressive_mode) { - // Sink the aggresiveness. + if (aggressive_mode || emergency_mode) { num_iterations >>= 2; aggressive_mode = false; + emergency_mode = false; return false; } if (high_priority_mode && total_used_memory < expected_memory) { @@ -132,26 +257,80 @@ void TextureCache

::RunGarbageCollector() { return false; }; - // Try to remove anything old enough and not high priority. - Configure(false); + // FIXED: VRAM leak prevention - First pass: evict sparse textures if priority enabled + if (sparse_priority && sparse_texture_memory > 0 && total_used_memory >= expected_memory) { + Configure(false, false); + // Target sparse textures specifically + lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, [this, &Cleanup](ImageId image_id) { + auto& image = slot_images[image_id]; + if (True(image.flags & ImageFlagBits::Sparse)) { + return Cleanup(image_id); + } + return false; + }); + } + + // Normal pass: remove anything old enough + Configure(false, false); lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup); - // If pressure is still too high, prune aggressively. + // Aggressive pass if still above critical if (total_used_memory >= critical_memory) { - Configure(true); + Configure(true, false); lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup); } + + // FIXED: VRAM leak prevention - Emergency pass if still above emergency threshold + if (total_used_memory >= static_cast(vram_limit_bytes * VRAM_USAGE_EMERGENCY_THRESHOLD)) { + Configure(true, true); + emergency_gc_triggered = true; + LOG_WARNING(Render_Vulkan, "VRAM Emergency GC triggered: usage={}MB, limit={}MB", + total_used_memory / 1_MiB, vram_limit_bytes / 1_MiB); + lru_cache.ForEachItemBelow(frame_tick, Cleanup); // Evict everything below current frame + } + + // Update statistics + evicted_this_frame += bytes_freed; + evicted_total += bytes_freed; + + // FIXED: VRAM leak prevention - Log VRAM usage if enabled + if (Settings::values.log_vram_usage.GetValue() && bytes_freed > 0) { + LOG_INFO(Render_Vulkan, + "VRAM GC: evicted {}MB this frame, total={}MB, usage={}MB/{}MB ({:.1f}%)", + bytes_freed / 1_MiB, evicted_total / 1_MiB, total_used_memory / 1_MiB, + vram_limit_bytes / 1_MiB, + (static_cast(total_used_memory) / vram_limit_bytes) * 100.0f); + } } template void TextureCache

::TickFrame() { + // FIXED: VRAM leak prevention - Enhanced frame tick with VRAM monitoring + + // Reset emergency flag at start of frame + emergency_gc_triggered = false; + // If we can obtain the memory info, use it instead of the estimate. if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); } - if (total_used_memory > minimum_memory) { + + // FIXED: VRAM leak prevention - Check if GC should run based on settings + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + const bool should_gc = gc_level != Settings::GCAggressiveness::Off && + (total_used_memory > minimum_memory || + total_used_memory >= static_cast(vram_limit_bytes * VRAM_USAGE_WARNING_THRESHOLD)); + + if (should_gc) { RunGarbageCollector(); } + + // FIXED: VRAM leak prevention - Force additional GC if still above critical after normal GC + if (total_used_memory >= critical_memory && gc_level != Settings::GCAggressiveness::Off) { + // Run GC again if we're still above critical + RunGarbageCollector(); + } + sentenced_images.Tick(); sentenced_framebuffers.Tick(); sentenced_image_view.Tick(); @@ -166,6 +345,183 @@ void TextureCache

::TickFrame() { } async_buffers_death_ring.clear(); } + + // FIXED: VRAM leak prevention - Periodic VRAM usage logging + if (Settings::values.log_vram_usage.GetValue() && (frame_tick % 300 == 0)) { + const f32 usage_ratio = vram_limit_bytes > 0 + ? static_cast(total_used_memory) / vram_limit_bytes + : 0.0f; + LOG_INFO(Render_Vulkan, + "VRAM Status: {}MB/{}MB ({:.1f}%), textures={}, sparse={}, evicted_total={}MB", + total_used_memory / 1_MiB, vram_limit_bytes / 1_MiB, usage_ratio * 100.0f, + texture_count, sparse_texture_count, evicted_total / 1_MiB); + } +} + +// FIXED: VRAM leak prevention - Implementation of new VRAM management methods + +template +void TextureCache

::ForceEmergencyGC() { + LOG_WARNING(Render_Vulkan, "Force emergency GC triggered: usage={}MB, limit={}MB", + total_used_memory / 1_MiB, vram_limit_bytes / 1_MiB); + + emergency_gc_triggered = true; + u64 bytes_freed = 0; + + // Evict 10% of textures immediately, prioritizing sparse and large textures + const u64 target_bytes = total_used_memory / 10; + bytes_freed += EvictSparseTexturesPriority(target_bytes / 2); + bytes_freed += EvictToFreeMemory(target_bytes - bytes_freed); + + evicted_this_frame += bytes_freed; + evicted_total += bytes_freed; + + LOG_INFO(Render_Vulkan, "Emergency GC freed {}MB", bytes_freed / 1_MiB); +} + +template +typename TextureCache

::VRAMStats TextureCache

::GetVRAMStats() const noexcept { + const f32 usage_ratio = vram_limit_bytes > 0 + ? static_cast(total_used_memory) / vram_limit_bytes + : 0.0f; + return VRAMStats{ + .total_used_bytes = total_used_memory, + .texture_bytes = total_used_memory - sparse_texture_memory, + .sparse_texture_bytes = sparse_texture_memory, + .evicted_this_frame = evicted_this_frame, + .evicted_total = evicted_total, + .texture_count = texture_count, + .sparse_texture_count = sparse_texture_count, + .usage_ratio = usage_ratio, + }; +} + +template +void TextureCache

::SetVRAMLimit(u64 limit_bytes) { + vram_limit_bytes = limit_bytes; + + // Recalculate thresholds + const auto gc_level = Settings::values.gc_aggressiveness.GetValue(); + f32 expected_ratio = 0.6f; + f32 critical_ratio = 0.8f; + + switch (gc_level) { + case Settings::GCAggressiveness::Off: + expected_ratio = 0.95f; + critical_ratio = 0.99f; + break; + case Settings::GCAggressiveness::Light: + expected_ratio = 0.75f; + critical_ratio = 0.90f; + break; + case Settings::GCAggressiveness::Moderate: + expected_ratio = 0.60f; + critical_ratio = 0.80f; + break; + case Settings::GCAggressiveness::Heavy: + expected_ratio = 0.50f; + critical_ratio = 0.70f; + break; + case Settings::GCAggressiveness::Extreme: + expected_ratio = 0.40f; + critical_ratio = 0.60f; + break; + } + + expected_memory = static_cast(vram_limit_bytes * expected_ratio); + critical_memory = static_cast(vram_limit_bytes * critical_ratio); + minimum_memory = static_cast(vram_limit_bytes * 0.25f); + + LOG_INFO(Render_Vulkan, "VRAM limit updated: {}MB, expected={}MB, critical={}MB", + vram_limit_bytes / 1_MiB, expected_memory / 1_MiB, critical_memory / 1_MiB); +} + +template +bool TextureCache

::IsVRAMPressureHigh() const noexcept { + return total_used_memory >= expected_memory; +} + +template +bool TextureCache

::IsVRAMPressureCritical() const noexcept { + return total_used_memory >= static_cast(vram_limit_bytes * VRAM_USAGE_EMERGENCY_THRESHOLD); +} + +template +u64 TextureCache

::EvictToFreeMemory(u64 target_bytes) { + u64 bytes_freed = 0; + const u64 start_memory = total_used_memory; + + lru_cache.ForEachItemBelow(frame_tick, [this, &bytes_freed, target_bytes](ImageId image_id) { + if (bytes_freed >= target_bytes) { + return true; + } + + auto& image = slot_images[image_id]; + if (True(image.flags & ImageFlagBits::IsDecoding)) { + return false; + } + + const u64 image_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes); + + if (True(image.flags & ImageFlagBits::Tracked)) { + UntrackImage(image, image_id); + } + UnregisterImage(image_id); + DeleteImage(image_id, false); + + bytes_freed += Common::AlignUp(image_size, 1024); + return false; + }); + + return start_memory - total_used_memory; +} + +template +u64 TextureCache

::EvictSparseTexturesPriority(u64 target_bytes) { + if (!Settings::values.sparse_texture_priority_eviction.GetValue()) { + return 0; + } + + u64 bytes_freed = 0; + + // Collect sparse textures and sort by size (largest first) + std::vector> sparse_textures; + lru_cache.ForEachItemBelow(frame_tick, [this, &sparse_textures](ImageId image_id) { + auto& image = slot_images[image_id]; + if (True(image.flags & ImageFlagBits::Sparse) && + False(image.flags & ImageFlagBits::IsDecoding)) { + const u64 size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes); + sparse_textures.emplace_back(image_id, size); + } + return false; + }); + + // Sort by size descending (largest first for priority eviction) + std::sort(sparse_textures.begin(), sparse_textures.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + + for (const auto& [image_id, size] : sparse_textures) { + if (bytes_freed >= target_bytes) { + break; + } + + auto& image = slot_images[image_id]; + if (True(image.flags & ImageFlagBits::Tracked)) { + UntrackImage(image, image_id); + } + UnregisterImage(image_id); + DeleteImage(image_id, false); + + bytes_freed += Common::AlignUp(size, 1024); + --sparse_texture_count; + sparse_texture_memory -= Common::AlignUp(size, 1024); + } + + if (bytes_freed > 0) { + LOG_DEBUG(Render_Vulkan, "Sparse texture priority eviction freed {}MB", bytes_freed / 1_MiB); + } + + return bytes_freed; } template @@ -2018,7 +2374,22 @@ void TextureCache

::RegisterImage(ImageId image_id) { True(image.flags & ImageFlagBits::Converted)) { tentative_size = TranscodedAstcSize(tentative_size, image.info.format); } - total_used_memory += Common::AlignUp(tentative_size, 1024); + const u64 aligned_size = Common::AlignUp(tentative_size, 1024); + total_used_memory += aligned_size; + + // FIXED: VRAM leak prevention - Track texture statistics + ++texture_count; + const bool is_sparse = True(image.flags & ImageFlagBits::Sparse); + const bool is_large = aligned_size >= LARGE_TEXTURE_THRESHOLD; + + if (is_sparse) { + sparse_texture_memory += aligned_size; + ++sparse_texture_count; + } + if (is_large) { + large_texture_memory += aligned_size; + } + image.lru_index = lru_cache.Insert(image_id, frame_tick); ForEachGPUPage(image.gpu_addr, image.guest_size_bytes, [this, image_id](u64 page) { diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index ee3ce8be1..70a91e5d4 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -113,6 +113,14 @@ class TextureCache : public VideoCommon::ChannelSetupCaches 4MB + static constexpr size_t LARGE_TEXTURE_THRESHOLD = 16_MiB; // Large texture threshold + static constexpr u64 DEFAULT_EVICTION_FRAMES = 2; // Default frames before eviction + static constexpr f32 VRAM_USAGE_WARNING_THRESHOLD = 0.75f; // 75% - start warning + static constexpr f32 VRAM_USAGE_CRITICAL_THRESHOLD = 0.85f; // 85% - aggressive GC + static constexpr f32 VRAM_USAGE_EMERGENCY_THRESHOLD = 0.95f; // 95% - emergency eviction + using Runtime = typename P::Runtime; using Image = typename P::Image; using ImageAlloc = typename P::ImageAlloc; @@ -296,6 +304,42 @@ public: RunGarbageCollector(); } + // FIXED: VRAM leak prevention - Enhanced public interface for VRAM management + + /// Force emergency garbage collection when VRAM pressure is critical + void ForceEmergencyGC(); + + /// Get current VRAM usage statistics + struct VRAMStats { + u64 total_used_bytes; + u64 texture_bytes; + u64 sparse_texture_bytes; + u64 evicted_this_frame; + u64 evicted_total; + u32 texture_count; + u32 sparse_texture_count; + f32 usage_ratio; // Current usage / limit + }; + [[nodiscard]] VRAMStats GetVRAMStats() const noexcept; + + /// Get configured VRAM limit in bytes + [[nodiscard]] u64 GetVRAMLimit() const noexcept { return vram_limit_bytes; } + + /// Set VRAM limit (0 = auto-detect) + void SetVRAMLimit(u64 limit_bytes); + + /// Check if VRAM pressure is high + [[nodiscard]] bool IsVRAMPressureHigh() const noexcept; + + /// Check if VRAM pressure is critical (emergency) + [[nodiscard]] bool IsVRAMPressureCritical() const noexcept; + + /// Evict oldest textures to free target_bytes of VRAM + u64 EvictToFreeMemory(u64 target_bytes); + + /// Evict sparse textures with priority (large unmapped pages first) + u64 EvictSparseTexturesPriority(u64 target_bytes); + /// Fills image_view_ids in the image views in indices template void FillImageViews(DescriptorTable& table, @@ -450,6 +494,18 @@ public: u64 expected_memory; u64 critical_memory; + // FIXED: VRAM leak prevention - Enhanced memory tracking + u64 vram_limit_bytes = 0; // Configured VRAM limit (0 = auto) + u64 sparse_texture_memory = 0; // Memory used by sparse textures + u64 large_texture_memory = 0; // Memory used by large textures (>16MB) + u64 evicted_this_frame = 0; // Bytes evicted in current frame + u64 evicted_total = 0; // Total bytes evicted since start + u32 gc_runs_this_frame = 0; // Number of GC runs this frame + u32 texture_count = 0; // Total texture count + u32 sparse_texture_count = 0; // Sparse texture count + u64 last_gc_frame = 0; // Last frame GC was run + bool emergency_gc_triggered = false; // Emergency GC flag + struct BufferDownload { GPUVAddr address; size_t size;