Files
ryujinx/src/ARMeilleure/Translation/Cache/JitCache.cs
T
6fb71b08ae instanced jit cache (#152)
- jit caches are no longer static.
- JitUnwindWindows now supports multiple jit caches.
- Jit caches should no longer cause crashes due to entry offset collisions.
- cpu tests now run concurrently.

Co-authored-by: LotP1 <68976644+LotP1@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/152
2026-06-30 13:40:18 +00:00

244 lines
7.9 KiB
C#

using ARMeilleure.CodeGen;
using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory;
using ARMeilleure.Native;
using Humanizer;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
namespace ARMeilleure.Translation.Cache
{
public partial class JitCache : IDisposable
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const uint CacheSize = 256 * 1024 * 1024;
private readonly JitCacheInvalidation _jitCacheInvalidator;
private readonly CacheMemoryAllocator _cacheAllocator;
private readonly List<CacheEntry> _cacheEntries = [];
private readonly Lock _lock = new();
private readonly List<ReservedRegion> _jitRegions = [];
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
public JitCache(IJitMemoryAllocator allocator)
{
lock (_lock)
{
_jitRegions.Add(new(allocator, CacheSize));
_cacheAllocator = new((int)CacheSize);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(
this, _jitRegions[0].Pointer, CacheSize, _jitRegions[0].Pointer + Allocate(_pageSize)
);
}
}
}
public nint Map(CompiledFunction func)
{
byte[] code = func.Code;
lock (_lock)
{
int funcOffset = Allocate(code.Length);
nint funcPtr = GetFunctionPtr(funcOffset);
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
unsafe
{
fixed (byte* codePtr = code)
{
JitSupportDarwin.Copy(funcPtr, (nint)codePtr, (ulong)code.Length);
}
}
}
else
{
ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
FlushInstructionCache(Process.GetCurrentProcess().Handle, funcPtr, (nuint)code.Length);
}
else
{
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
}
Add(funcOffset, code.Length, func.UnwindInfo);
return funcPtr;
}
}
public void Unmap(nint pointer)
{
lock (_lock)
{
for (int i = 0; i < _jitRegions.Count; i++)
{
ReservedRegion jitRegion = _jitRegions[i];
if (pointer.ToInt64() < jitRegion.Pointer.ToInt64() ||
pointer.ToInt64() >= (jitRegion.Pointer + (nint)CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - jitRegion.Pointer.ToInt64() + i * CacheSize);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
}
}
}
private void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
GetRegion(offset).Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
GetRegion(offset).Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
_cacheAllocator.AddNewBlocks(1);
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache of size {(_jitRegions.Count * CacheSize).Bytes()} exhausted, creating new Cache Region ({((_jitRegions.Count + 1) * CacheSize).Bytes()} Total Allocation).");
_jitRegions.Add(newRegion);
allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
private nint GetFunctionPtr(int offset)
{
return GetRegion(offset).Pointer + (offset % (int)CacheSize);
}
private ReservedRegion GetRegion(int offset)
{
int index = offset / (int)CacheSize;
return _jitRegions[index];
}
private static int AlignCodeSize(int codeSize)
{
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
}
private void Add(int offset, int size, UnwindInfo unwindInfo)
{
CacheEntry entry = new(offset, size, unwindInfo);
int index = _cacheEntries.BinarySearch(entry);
if (index < 0)
{
index = ~index;
}
_cacheEntries.Insert(index, entry);
}
public bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
if (index < 0)
{
index = ~index - 1;
}
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
}
entry = default;
entryIndex = 0;
return false;
}
public void Dispose()
{
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.RemoveFunctionTableHandler(_jitRegions[0].Pointer);
}
foreach (ReservedRegion jitRegion in _jitRegions)
{
jitRegion.Dispose();
}
}
}
}