Merge pull request 'fix/vram-leak-prevention' (#111) from fix/vram-leak-prevention into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/111
This commit is contained in:
Zephyron
2026-01-25 06:44:29 +01:00
22 changed files with 949 additions and 219 deletions

View File

@@ -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)

View File

@@ -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"),

View File

@@ -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(

View File

@@ -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)
}
}

View File

@@ -429,15 +429,28 @@
<string-array name="vramUsageModeNames">
<item>Conservative</item>
<item>Aggressive</item>
<item>High-End GPU (4090/4080+)</item>
<item>Insane (RTX 4090 24GB)</item>
</string-array>
<integer-array name="vramUsageModeValues">
<item>0</item>
<item>1</item>
</integer-array>
<!-- VRAM Management setting arrays (FIXED: VRAM leak prevention) -->
<string-array name="gcAggressivenessNames">
<item>@string/gc_aggressiveness_off</item>
<item>@string/gc_aggressiveness_light</item>
<item>@string/gc_aggressiveness_moderate</item>
<item>@string/gc_aggressiveness_heavy</item>
<item>@string/gc_aggressiveness_extreme</item>
</string-array>
<integer-array name="gcAggressivenessValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<!-- Applet Mode setting arrays -->

View File

@@ -448,6 +448,7 @@
<string name="memory_layout_header">Memory Layout</string>
<string name="astc_settings_header">ASTC Settings</string>
<string name="advanced_graphics_header">Advanced Graphics</string>
<string name="vram_management_header">VRAM Management</string>
<string name="applet_settings_header">Applet Settings</string>
<!-- Applet Mode Settings -->
@@ -1273,6 +1274,25 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<string name="frame_skipping_enabled">Enabled</string>
<string name="frame_skipping_mode_adaptive">Adaptive</string>
<string name="frame_skipping_mode_fixed">Fixed</string>
<!-- VRAM Management settings (FIXED: VRAM leak prevention) -->
<string name="vram_limit_mb">VRAM Limit (MB)</string>
<string name="vram_limit_mb_description">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.</string>
<string name="gc_aggressiveness">GC Aggressiveness</string>
<string name="gc_aggressiveness_description">Controls how aggressively the emulator evicts unused textures and buffers from VRAM. Higher levels free memory faster but may cause more texture reloading.</string>
<string name="texture_eviction_frames">Texture Eviction Frames</string>
<string name="texture_eviction_frames_description">Number of frames a texture must be unused before it can be evicted. Lower values free VRAM faster but may cause more texture reloading.</string>
<string name="buffer_eviction_frames">Buffer Eviction Frames</string>
<string name="buffer_eviction_frames_description">Number of frames a buffer must be unused before it can be evicted. Lower values free VRAM faster but may cause more buffer reloading.</string>
<string name="sparse_texture_priority_eviction">Sparse Texture Priority Eviction</string>
<string name="sparse_texture_priority_eviction_description">Prioritize evicting large sparse textures when VRAM pressure is high. Helps prevent VRAM exhaustion in games with large texture atlases.</string>
<string name="log_vram_usage">Log VRAM Usage</string>
<string name="log_vram_usage_description">Enable logging of VRAM usage statistics for debugging purposes.</string>
<string name="gc_aggressiveness_off">Off (Not Recommended)</string>
<string name="gc_aggressiveness_light">Light</string>
<string name="gc_aggressiveness_moderate">Moderate (Recommended)</string>
<string name="gc_aggressiveness_heavy">Heavy (Low VRAM)</string>
<string name="gc_aggressiveness_extreme">Extreme (4GB VRAM)</string>
<string name="frame_skipping_header">Frame Skipping</string>
</resources>

View File

@@ -192,6 +192,31 @@ std::unique_ptr<TranslationMap> 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 "
@@ -357,8 +382,6 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
{
PAIR(VramUsageMode, Conservative, tr("Conservative")),
PAIR(VramUsageMode, Aggressive, tr("Aggressive")),
PAIR(VramUsageMode, HighEnd, tr("High-End GPU (4090/4080+)")),
PAIR(VramUsageMode, Insane, tr("Insane (RTX 4090 24GB)")),
}});
translations->insert({Settings::EnumMetadata<Settings::ExtendedDynamicState>::Index(),
{
@@ -367,6 +390,17 @@ std::unique_ptr<ComboboxTranslationMap> 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<Settings::GCAggressiveness>::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<Settings::RendererBackend>::Index(),
{
#ifdef HAS_OPENGL

View File

@@ -162,8 +162,6 @@ void VramOverlay::DrawVramInfo(QPainter& painter) {
switch (Settings::values.vram_usage_mode.GetValue()) {
case Settings::VramUsageMode::Conservative: mode_text = QString::fromUtf8("Mode: Conservative"); break;
case Settings::VramUsageMode::Aggressive: mode_text = QString::fromUtf8("Mode: Aggressive"); break;
case Settings::VramUsageMode::HighEnd: mode_text = QString::fromUtf8("Mode: High-End GPU"); break;
case Settings::VramUsageMode::Insane: mode_text = QString::fromUtf8("Mode: Insane"); painter.setPen(leak_warning_color); break;
default: mode_text = QString::fromUtf8("Mode: Unknown"); break;
}
painter.drawText(section_padding, y_offset, mode_text);

View File

@@ -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);
@@ -498,9 +499,64 @@ struct Values {
SwitchableSetting<VramUsageMode, true> vram_usage_mode{linkage,
VramUsageMode::Conservative,
VramUsageMode::Conservative,
VramUsageMode::Insane,
VramUsageMode::Aggressive,
"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<u32, true> 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<GCAggressiveness, true> 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<u32, true> 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<u32, true> 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<bool> sparse_texture_priority_eviction{linkage, true,
"sparse_texture_priority_eviction",
Category::RendererAdvanced};
// Enable VRAM usage logging for debugging
SwitchableSetting<bool> log_vram_usage{linkage, false, "log_vram_usage",
Category::RendererAdvanced};
SwitchableSetting<bool> async_presentation{linkage,
#ifdef ANDROID
true,

View File

@@ -406,8 +406,6 @@ inline u32 EnumMetadata<VSyncMode>::Index() {
enum class VramUsageMode : u32 {
Conservative = 0,
Aggressive = 1,
HighEnd = 2,
Insane = 3,
};
template <>
@@ -416,8 +414,6 @@ EnumMetadata<VramUsageMode>::Canonicalizations() {
return {
{"Conservative", VramUsageMode::Conservative},
{"Aggressive", VramUsageMode::Aggressive},
{"HighEnd", VramUsageMode::HighEnd},
{"Insane", VramUsageMode::Insane},
};
}
@@ -880,6 +876,32 @@ inline u32 EnumMetadata<ExtendedDynamicState>::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<std::pair<std::string, GCAggressiveness>>
EnumMetadata<GCAggressiveness>::Canonicalizations() {
return {
{"Off", GCAggressiveness::Off},
{"Light", GCAggressiveness::Light},
{"Moderate", GCAggressiveness::Moderate},
{"Heavy", GCAggressiveness::Heavy},
{"Extreme", GCAggressiveness::Extreme},
};
}
template <>
inline u32 EnumMetadata<GCAggressiveness>::Index() {
return 27;
}
template <typename Type>
inline std::string CanonicalizeEnum(Type id) {

View File

@@ -26,24 +26,60 @@ BufferCache<P>::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<u64>(configured_limit_mb) * 1_MiB
: 6_GiB;
return;
}
const s64 device_local_memory = static_cast<s64>(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<u64>(
std::max(std::min(device_local_memory - min_vacancy_expected, min_spacing_expected),
DEFAULT_EXPECTED_MEMORY));
critical_memory = static_cast<u64>(
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<u64>(configured_limit_mb) * 1_MiB;
} else {
vram_limit_bytes = static_cast<u64>(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<u64>(vram_limit_bytes * expected_ratio);
critical_memory = static_cast<u64>(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 <class P>
@@ -51,20 +87,90 @@ BufferCache<P>::~BufferCache() = default;
template <class P>
void BufferCache<P>::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<u64>(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 <class P>
@@ -96,9 +202,22 @@ void BufferCache<P>::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<u64>(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 <bool insert>
void BufferCache<P>::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;

View File

@@ -175,6 +175,12 @@ class BufferCache : public VideoCommon::ChannelSetupCaches<BufferCacheChannelInf
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB;
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
// FIXED: VRAM leak prevention - Enhanced buffer eviction constants
static constexpr u64 DEFAULT_BUFFER_EVICTION_FRAMES = 5;
static constexpr size_t LARGE_BUFFER_THRESHOLD = 8_MiB;
static constexpr f32 BUFFER_VRAM_WARNING_THRESHOLD = 0.70f;
static constexpr f32 BUFFER_VRAM_CRITICAL_THRESHOLD = 0.85f;
// Debug Flags.
static constexpr bool DISABLE_DOWNLOADS = true;
@@ -350,6 +356,31 @@ public:
RunGarbageCollector();
}
// FIXED: VRAM leak prevention - Enhanced public interface for buffer VRAM management
/// Get buffer VRAM usage statistics
struct BufferVRAMStats {
u64 total_used_bytes;
u64 large_buffer_bytes;
u64 evicted_total;
u32 buffer_count;
u32 large_buffer_count;
};
[[nodiscard]] BufferVRAMStats GetBufferVRAMStats() const noexcept {
return BufferVRAMStats{
.total_used_bytes = total_used_memory,
.large_buffer_bytes = large_buffer_memory,
.evicted_total = evicted_buffer_bytes,
.buffer_count = buffer_count,
.large_buffer_count = large_buffer_count,
};
}
/// Check if buffer VRAM pressure is high
[[nodiscard]] bool IsBufferVRAMPressureHigh() const noexcept {
return total_used_memory >= 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<BufferId, ((1ULL << 34) >> CACHING_PAGEBITS)> page_table;
Common::ScratchBuffer<u8> tmp_buffer;
};

View File

@@ -159,6 +159,25 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> 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<u64>(configured_limit) * 1024ULL * 1024ULL
: static_cast<u64>(total_vram * 0.80);
// If VRAM usage is above 90% of limit, trigger emergency GC on texture/buffer caches
if (current_usage >= static_cast<u64>(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<f32>(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<u32>(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<f64>(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);

View File

@@ -70,40 +70,11 @@ vk::Buffer CreateBuffer(const Device& device, const MemoryAllocator& memory_allo
flags |= VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT;
}
// Optimize buffer size based on VRAM usage mode
u64 optimized_size = size;
const auto vram_mode = Settings::values.vram_usage_mode.GetValue();
if (vram_mode == Settings::VramUsageMode::HighEnd) {
// High-End GPU mode: Use larger buffer chunks for high-end GPUs to reduce allocation overhead
// but still keep them reasonable to avoid excessive VRAM usage
if (size > 64_MiB && size < 512_MiB) {
// Round up to next 64MB boundary for large buffers
optimized_size = Common::AlignUp(size, 64_MiB);
} else if (size > 4_MiB && size <= 64_MiB) {
// Round up to next 8MB boundary for medium buffers
optimized_size = Common::AlignUp(size, 8_MiB);
}
} else if (vram_mode == Settings::VramUsageMode::Insane) {
// Insane mode: Use massive buffer chunks for RTX 4090 to minimize allocation overhead
// and maximize performance for shader compilation and caching
if (size > 128_MiB && size < 1024_MiB) {
// Round up to next 128MB boundary for very large buffers
optimized_size = Common::AlignUp(size, 128_MiB);
} else if (size > 16_MiB && size <= 128_MiB) {
// Round up to next 32MB boundary for large buffers
optimized_size = Common::AlignUp(size, 32_MiB);
} else if (size > 1_MiB && size <= 16_MiB) {
// Round up to next 4MB boundary for medium buffers
optimized_size = Common::AlignUp(size, 4_MiB);
}
}
const VkBufferCreateInfo buffer_ci = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.size = optimized_size,
.size = size,
.usage = flags,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
@@ -115,31 +86,8 @@ vk::Buffer CreateBuffer(const Device& device, const MemoryAllocator& memory_allo
} // Anonymous namespace
void BufferCacheRuntime::CleanupUnusedBuffers() {
// Aggressive cleanup for Insane mode to prevent VRAM leaks
const auto vram_mode = Settings::values.vram_usage_mode.GetValue();
if (vram_mode == Settings::VramUsageMode::Insane) {
// For Insane mode, periodically clean up unused large buffers to prevent memory leaks
static u32 cleanup_counter = 0;
static u64 last_buffer_memory = 0;
cleanup_counter++;
// Monitor buffer memory usage to detect potential leaks
if (cleanup_counter % 120 == 0) {
const u64 current_buffer_memory = GetDeviceMemoryUsage();
// Check for buffer memory leak (usage increasing without corresponding game activity)
if (current_buffer_memory > last_buffer_memory + 50_MiB) {
LOG_WARNING(Render_Vulkan, "Potential buffer memory leak detected! Usage increased by {} MB",
(current_buffer_memory - last_buffer_memory) / (1024 * 1024));
// Force cleanup of any cached buffers that might be accumulating
LOG_INFO(Render_Vulkan, "Performed aggressive buffer cleanup (Insane mode)");
}
last_buffer_memory = current_buffer_memory;
LOG_DEBUG(Render_Vulkan, "Buffer memory usage: {} MB (Insane mode)", current_buffer_memory / (1024 * 1024));
}
}
// Cleanup is now handled by the VRAM management system (gc_aggressiveness setting)
// This function is kept for compatibility but no longer performs mode-specific cleanup
}
Buffer::Buffer(BufferCacheRuntime& runtime, VideoCommon::NullBufferParams null_params)

View File

@@ -748,11 +748,8 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
const auto runtime_info{MakeRuntimeInfo(programs, key, program, previous_stage)};
ConvertLegacyToGeneric(program, runtime_info);
std::vector<u32> code = EmitSPIRV(profile, runtime_info, program, binding);
// Reserve more space for Insane mode to reduce allocations during shader compilation
const size_t reserve_size = Settings::values.vram_usage_mode.GetValue() == Settings::VramUsageMode::Insane
? std::max<size_t>(code.size(), 64 * 1024 / sizeof(u32)) // 64KB for Insane mode
: std::max<size_t>(code.size(), 16 * 1024 / sizeof(u32)); // 16KB for other modes
code.reserve(reserve_size);
// Reserve space to reduce allocations during shader compilation
code.reserve(std::max<size_t>(code.size(), 16 * 1024 / sizeof(u32)));
device.SaveShader(code);
modules[stage_index] = BuildShader(device, code);
if (device.HasDebuggingToolAttached()) {
@@ -854,11 +851,8 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
auto program{TranslateProgram(pools.inst, pools.block, env, cfg, host_info)};
std::vector<u32> code = EmitSPIRV(profile, program);
// Reserve more space for Insane mode to reduce allocations during shader compilation
const size_t reserve_size = Settings::values.vram_usage_mode.GetValue() == Settings::VramUsageMode::Insane
? std::max<size_t>(code.size(), 64 * 1024 / sizeof(u32)) // 64KB for Insane mode
: std::max<size_t>(code.size(), 16 * 1024 / sizeof(u32)); // 16KB for other modes
code.reserve(reserve_size);
// Reserve space to reduce allocations during shader compilation
code.reserve(std::max<size_t>(code.size(), 16 * 1024 / sizeof(u32)));
device.SaveShader(code);
vk::ShaderModule spv_module{BuildShader(device, code)};
if (device.HasDebuggingToolAttached()) {

View File

@@ -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();

View File

@@ -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,

View File

@@ -99,24 +99,7 @@ void StagingBufferPool::FreeDeferred(StagingBufferRef& ref) {
void StagingBufferPool::TickFrame() {
current_delete_level = (current_delete_level + 1) % NUM_LEVELS;
// Enhanced cleanup for Insane mode to prevent VRAM leaks
const auto vram_mode = Settings::values.vram_usage_mode.GetValue();
if (vram_mode == Settings::VramUsageMode::Insane) {
static u32 cleanup_counter = 0;
cleanup_counter++;
// More aggressive cleanup for Insane mode every 30 frames
if (cleanup_counter % 30 == 0) {
// Force release of all caches to prevent memory accumulation
ReleaseCache(MemoryUsage::DeviceLocal);
ReleaseCache(MemoryUsage::Upload);
ReleaseCache(MemoryUsage::Download);
// Additional cleanup for large staging buffers
LOG_DEBUG(Render_Vulkan, "Performed aggressive staging buffer cleanup (Insane mode)");
}
}
// Cleanup is now handled by the VRAM management system (gc_aggressiveness setting)
ReleaseCache(MemoryUsage::DeviceLocal);
ReleaseCache(MemoryUsage::Upload);
ReleaseCache(MemoryUsage::Download);

View File

@@ -939,29 +939,8 @@ VkBuffer TextureCacheRuntime::GetTemporaryBuffer(size_t needed_size) {
return *buffers[level];
}
// Optimize buffer size based on VRAM usage mode
size_t new_size = Common::NextPow2(needed_size);
const auto vram_mode = Settings::values.vram_usage_mode.GetValue();
if (vram_mode == Settings::VramUsageMode::HighEnd) {
// For high-end GPUs, use larger temporary buffers to reduce allocation overhead
// but cap them to prevent excessive VRAM usage
if (needed_size > 32_MiB && needed_size < 256_MiB) {
new_size = Common::AlignUp(needed_size, 32_MiB);
} else if (needed_size > 2_MiB && needed_size <= 32_MiB) {
new_size = Common::AlignUp(needed_size, 4_MiB);
}
} else if (vram_mode == Settings::VramUsageMode::Insane) {
// Insane mode: Use massive temporary buffers for RTX 4090 to maximize texture caching
// and shader compilation performance
if (needed_size > 64_MiB && needed_size < 512_MiB) {
new_size = Common::AlignUp(needed_size, 64_MiB);
} else if (needed_size > 8_MiB && needed_size <= 64_MiB) {
new_size = Common::AlignUp(needed_size, 16_MiB);
} else if (needed_size > 1_MiB && needed_size <= 8_MiB) {
new_size = Common::AlignUp(needed_size, 2_MiB);
}
}
// Use power-of-2 buffer sizes for efficient allocation
const size_t new_size = Common::NextPow2(needed_size);
static constexpr VkBufferUsageFlags flags =
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
@@ -981,46 +960,8 @@ VkBuffer TextureCacheRuntime::GetTemporaryBuffer(size_t needed_size) {
}
void TextureCacheRuntime::CleanupUnusedBuffers() {
// Aggressive cleanup for Insane mode to prevent VRAM leaks
const auto vram_mode = Settings::values.vram_usage_mode.GetValue();
if (vram_mode == Settings::VramUsageMode::Insane) {
// For Insane mode, periodically clean up unused large buffers to prevent memory leaks
static u32 cleanup_counter = 0;
static u64 last_vram_usage = 0;
cleanup_counter++;
// Monitor VRAM usage to detect potential leaks
if (cleanup_counter % 60 == 0) {
const u64 current_vram_usage = GetDeviceMemoryUsage();
// Check for VRAM leak (usage increasing without corresponding game activity)
if (current_vram_usage > last_vram_usage + 100_MiB) {
LOG_WARNING(Render_Vulkan, "Potential VRAM leak detected! Usage increased by {} MB",
(current_vram_usage - last_vram_usage) / (1024 * 1024));
// Force aggressive cleanup
for (auto& buffer : buffers) {
if (buffer) {
buffer.reset();
}
}
LOG_INFO(Render_Vulkan, "Performed aggressive VRAM cleanup (Insane mode)");
}
last_vram_usage = current_vram_usage;
LOG_DEBUG(Render_Vulkan, "VRAM usage: {} MB (Insane mode)", current_vram_usage / (1024 * 1024));
}
// Regular cleanup every 120 frames
if (cleanup_counter % 120 == 0) {
for (auto& buffer : buffers) {
if (buffer) {
buffer.reset();
}
}
LOG_DEBUG(Render_Vulkan, "Cleaned up unused temporary buffers (Insane mode)");
}
}
// Cleanup is now handled by the VRAM management system (gc_aggressiveness setting)
// This function is kept for compatibility but no longer performs mode-specific cleanup
}
void TextureCacheRuntime::BarrierFeedbackLoop() {

View File

@@ -50,21 +50,59 @@ TextureCache<P>::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<s64>(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<u64>(
std::max(std::min(device_local_memory - min_vacancy_expected, min_spacing_expected),
DEFAULT_EXPECTED_MEMORY));
critical_memory = static_cast<u64>(
std::max(std::min(device_local_memory - min_vacancy_critical, min_spacing_critical),
DEFAULT_CRITICAL_MEMORY));
minimum_memory = static_cast<u64>((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<u64>(configured_limit_mb) * 1_MiB;
} else {
// Auto-detect: use 80% of available VRAM as limit
vram_limit_bytes = static_cast<u64>(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<u64>(vram_limit_bytes * expected_ratio);
critical_memory = static_cast<u64>(vram_limit_bytes * critical_ratio);
minimum_memory = static_cast<u64>(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<u32>(gc_level));
} else {
vram_limit_bytes = configured_limit_mb > 0 ? static_cast<u64>(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<P>::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag
template <class P>
void TextureCache<P>::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<u64>(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<size_t>(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<P>::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<P>::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<u64>(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<f32>(total_used_memory) / vram_limit_bytes) * 100.0f);
}
}
template <class P>
void TextureCache<P>::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<u64>(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<P>::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<f32>(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 <class P>
void TextureCache<P>::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 <class P>
typename TextureCache<P>::VRAMStats TextureCache<P>::GetVRAMStats() const noexcept {
const f32 usage_ratio = vram_limit_bytes > 0
? static_cast<f32>(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 <class P>
void TextureCache<P>::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<u64>(vram_limit_bytes * expected_ratio);
critical_memory = static_cast<u64>(vram_limit_bytes * critical_ratio);
minimum_memory = static_cast<u64>(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 <class P>
bool TextureCache<P>::IsVRAMPressureHigh() const noexcept {
return total_used_memory >= expected_memory;
}
template <class P>
bool TextureCache<P>::IsVRAMPressureCritical() const noexcept {
return total_used_memory >= static_cast<u64>(vram_limit_bytes * VRAM_USAGE_EMERGENCY_THRESHOLD);
}
template <class P>
u64 TextureCache<P>::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 <class P>
u64 TextureCache<P>::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<std::pair<ImageId, u64>> 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 <class P>
@@ -2018,7 +2374,22 @@ void TextureCache<P>::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) {

View File

@@ -113,6 +113,14 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
// FIXED: VRAM leak prevention - Enhanced eviction constants
static constexpr size_t SPARSE_EVICTION_PRIORITY_THRESHOLD = 4_MiB; // Prioritize sparse textures > 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 <bool has_blacklists>
void FillImageViews(DescriptorTable<TICEntry>& 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;

View File

@@ -1354,20 +1354,6 @@ void Device::CollectPhysicalMemoryInfo() {
const size_t scaler_memory = 1_GiB * Settings::values.resolution_info.ScaleUp(1);
device_access_memory =
std::min<u64>(device_access_memory, normal_memory + scaler_memory);
} else if (vram_mode == Settings::VramUsageMode::HighEnd) {
// High-End GPU mode: Use more VRAM but with smart buffer management
// Allow up to 12GB for RTX 4090/4080+ users, but optimize buffer allocation
const size_t high_end_memory = 12_GiB;
const size_t scaler_memory = 1_GiB * Settings::values.resolution_info.ScaleUp(1);
device_access_memory =
std::min<u64>(device_access_memory, high_end_memory + scaler_memory);
} else if (vram_mode == Settings::VramUsageMode::Insane) {
// Insane mode: Use most of RTX 4090's 24GB VRAM for maximum performance
// Reserve only 2GB for system and other applications
const size_t insane_memory = 22_GiB;
const size_t scaler_memory = 2_GiB * Settings::values.resolution_info.ScaleUp(1);
device_access_memory =
std::min<u64>(device_access_memory, insane_memory + scaler_memory);
}
// Aggressive mode uses full available VRAM (no limits)