Android fixes and features

* Jit cache eviction (fixes out of memory errors in some games)

* Low power PPTC

* Fix 'unknown' games displayed when using game folder with subfolders

* Turn off NCE and PPTC by default
This commit is contained in:
KeatonTheBot 2025-05-31 03:00:29 -05:00
parent 3af88ad2e6
commit fda90239bc
58 changed files with 914 additions and 190 deletions

View file

@ -2,10 +2,12 @@ using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory;
using ARMeilleure.Native;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
@ -17,8 +19,15 @@ namespace ARMeilleure.Translation.Cache
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 2047 * 1024 * 1024;
private const int CodeAlignment = 4;
private const int FullCacheSize = 2047 * 1024 * 1024;
private const int ReducedCacheSize = FullCacheSize / 8;
private const float EvictionTargetPercentage = 0.20f;
private const int MaxEntriesToEvictAtOnce = 100;
// Simple logging configuration
private const int LogInterval = 5000; // Log every 5000 allocations
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator;
@ -26,9 +35,33 @@ namespace ARMeilleure.Translation.Cache
private static CacheMemoryAllocator _cacheAllocator;
private static readonly List<CacheEntry> _cacheEntries = [];
private static readonly Dictionary<int, EntryUsageStats> _entryUsageStats = [];
private static readonly Lock _lock = new();
private static bool _initialized;
private static int _cacheSize;
// Basic statistics
private static int _totalAllocations = 0;
private static int _totalEvictions = 0;
private class EntryUsageStats
{
public long LastAccessTime { get; private set; }
public int UsageCount { get; private set; }
public EntryUsageStats()
{
LastAccessTime = DateTime.UtcNow.Ticks;
UsageCount = 1;
}
public void UpdateUsage()
{
LastAccessTime = DateTime.UtcNow.Ticks;
UsageCount++;
}
}
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
@ -48,20 +81,22 @@ namespace ARMeilleure.Translation.Cache
return;
}
_jitRegion = new ReservedRegion(allocator, CacheSize);
_cacheSize = Optimizations.CacheEviction ? ReducedCacheSize : FullCacheSize;
_jitRegion = new ReservedRegion(allocator, (ulong)_cacheSize);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
_cacheAllocator = new CacheMemoryAllocator(_cacheSize);
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, (uint)_cacheSize, _jitRegion.Pointer + Allocate(_pageSize));
}
Logger.Info?.Print(LogClass.Cpu, $"JIT Cache initialized: Size={_cacheSize / (1024 * 1024)} MB, Eviction={Optimizations.CacheEviction}");
_initialized = true;
}
}
@ -73,8 +108,32 @@ namespace ARMeilleure.Translation.Cache
lock (_lock)
{
Debug.Assert(_initialized);
_totalAllocations++;
int funcOffset = Allocate(code.Length);
int funcOffset;
if (Optimizations.CacheEviction)
{
int codeSize = AlignCodeSize(code.Length);
funcOffset = _cacheAllocator.Allocate(codeSize);
if (funcOffset < 0)
{
EvictEntries(codeSize);
funcOffset = _cacheAllocator.Allocate(codeSize);
if (funcOffset < 0)
{
throw new OutOfMemoryException("JIT Cache exhausted even after eviction.");
}
}
_jitRegion.ExpandIfNeeded((ulong)funcOffset + (ulong)codeSize);
}
else
{
funcOffset = Allocate(code.Length);
}
IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
@ -106,6 +165,12 @@ namespace ARMeilleure.Translation.Cache
Add(funcOffset, code.Length, func.UnwindInfo);
// Simple periodic logging
if (_totalAllocations % LogInterval == 0)
{
LogCacheStatus();
}
return funcPtr;
}
}
@ -122,6 +187,11 @@ namespace ARMeilleure.Translation.Cache
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
if (Optimizations.CacheEviction)
{
_entryUsageStats.Remove(funcOffset);
}
}
}
}
@ -179,6 +249,11 @@ namespace ARMeilleure.Translation.Cache
}
_cacheEntries.Insert(index, entry);
if (Optimizations.CacheEviction)
{
_entryUsageStats[offset] = new EntryUsageStats();
}
}
public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
@ -195,6 +270,12 @@ namespace ARMeilleure.Translation.Cache
if (index >= 0)
{
entry = _cacheEntries[index];
if (Optimizations.CacheEviction && _entryUsageStats.TryGetValue(offset, out var stats))
{
stats.UpdateUsage();
}
entryIndex = index;
return true;
}
@ -204,5 +285,83 @@ namespace ARMeilleure.Translation.Cache
entryIndex = 0;
return false;
}
private static void EvictEntries(int requiredSize)
{
if (!Optimizations.CacheEviction)
{
return;
}
lock (_lock)
{
int targetSpace = Math.Max(requiredSize, (int)(_cacheSize * EvictionTargetPercentage));
int freedSpace = 0;
int evictedCount = 0;
var entriesWithStats = _cacheEntries
.Where(e => _entryUsageStats.ContainsKey(e.Offset))
.Select(e => new {
Entry = e,
Stats = _entryUsageStats[e.Offset],
Score = CalculateEvictionScore(_entryUsageStats[e.Offset])
})
.OrderBy(x => x.Score)
.Take(MaxEntriesToEvictAtOnce)
.ToList();
foreach (var item in entriesWithStats)
{
int entrySize = AlignCodeSize(item.Entry.Size);
int entryIndex = _cacheEntries.BinarySearch(item.Entry);
if (entryIndex >= 0)
{
_cacheAllocator.Free(item.Entry.Offset, entrySize);
_cacheEntries.RemoveAt(entryIndex);
_entryUsageStats.Remove(item.Entry.Offset);
freedSpace += entrySize;
evictedCount++;
if (freedSpace >= targetSpace)
{
break;
}
}
}
_totalEvictions += evictedCount;
Logger.Info?.Print(LogClass.Cpu, $"JIT Cache: Evicted {evictedCount} entries, freed {freedSpace / (1024 * 1024.0):F2} MB");
}
}
private static double CalculateEvictionScore(EntryUsageStats stats)
{
long currentTime = DateTime.UtcNow.Ticks;
long ageInTicks = currentTime - stats.LastAccessTime;
double ageInSeconds = ageInTicks / 10_000_000.0;
const double usageWeight = 1.0;
const double ageWeight = 2.0;
double usageScore = Math.Log10(stats.UsageCount + 1) * usageWeight;
double ageScore = (10.0 / (ageInSeconds + 1.0)) * ageWeight;
return usageScore + ageScore;
}
private static void LogCacheStatus()
{
int estimatedUsedSize = _cacheEntries.Sum(e => AlignCodeSize(e.Size));
double usagePercentage = 100.0 * estimatedUsedSize / _cacheSize;
Logger.Info?.Print(LogClass.Cpu,
$"JIT Cache status: entries={_cacheEntries.Count}, " +
$"est. used={estimatedUsedSize / (1024 * 1024.0):F2} MB ({usagePercentage:F1}%), " +
$"evictions={_totalEvictions}, allocations={_totalAllocations}");
}
}
}