using Ryujinx.Memory; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Ryujinx.Graphics.Gpu.Memory { /// /// GPU memory manager. /// public class MemoryManager : IWritableBlock { private const int PtLvl0Bits = 14; private const int PtLvl1Bits = 14; public const int PtPageBits = 12; private const ulong PtLvl0Size = 1UL << PtLvl0Bits; private const ulong PtLvl1Size = 1UL << PtLvl1Bits; public const ulong PageSize = 1UL << PtPageBits; private const ulong PtLvl0Mask = PtLvl0Size - 1; private const ulong PtLvl1Mask = PtLvl1Size - 1; public const ulong PageMask = PageSize - 1; private const int PtLvl0Bit = PtPageBits + PtLvl1Bits; private const int PtLvl1Bit = PtPageBits; private const int AddressSpaceBits = PtPageBits + PtLvl1Bits + PtLvl0Bits; public const ulong PteUnmapped = 0xffffffff_ffffffff; private readonly ulong[][] _pageTable; public event EventHandler MemoryUnmapped; private GpuContext _context; /// /// Creates a new instance of the GPU memory manager. /// public MemoryManager(GpuContext context) { _context = context; _pageTable = new ulong[PtLvl0Size][]; } /// /// Reads data from GPU mapped memory. /// /// Type of the data /// GPU virtual address where the data is located /// The data at the specified memory location public T Read(ulong va) where T : unmanaged { return MemoryMarshal.Cast(GetSpan(va, Unsafe.SizeOf()))[0]; } /// /// Gets a read-only span of data from GPU mapped memory. /// /// GPU virtual address where the data is located /// Size of the data /// True if read tracking is triggered on the span /// The span of the data at the specified memory location public ReadOnlySpan GetSpan(ulong va, int size, bool tracked = false) { if (IsContiguous(va, size)) { return _context.PhysicalMemory.GetSpan(Translate(va), size, tracked); } else { Span data = new byte[size]; ReadImpl(va, data, tracked); return data; } } /// /// Reads data from a possibly non-contiguous region of GPU mapped memory. /// /// GPU virtual address of the data /// Span to write the read data into /// True to enable write tracking on read, false otherwise private void ReadImpl(ulong va, Span data, bool tracked) { if (data.Length == 0) { return; } int offset = 0, size; if ((va & PageMask) != 0) { ulong pa = Translate(va); size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask)); _context.PhysicalMemory.GetSpan(pa, size, tracked).CopyTo(data.Slice(0, size)); offset += size; } for (; offset < data.Length; offset += size) { ulong pa = Translate(va + (ulong)offset); size = Math.Min(data.Length - offset, (int)PageSize); _context.PhysicalMemory.GetSpan(pa, size, tracked).CopyTo(data.Slice(offset, size)); } } /// /// Gets a writable region from GPU mapped memory. /// /// Start address of the range /// Size in bytes to be range /// A writable region with the data at the specified memory location public WritableRegion GetWritableRegion(ulong va, int size) { if (IsContiguous(va, size)) { return _context.PhysicalMemory.GetWritableRegion(Translate(va), size); } else { Memory memory = new byte[size]; GetSpan(va, size).CopyTo(memory.Span); return new WritableRegion(this, va, memory); } } /// /// Writes data to GPU mapped memory. /// /// Type of the data /// GPU virtual address to write the value into /// The value to be written public void Write(ulong va, T value) where T : unmanaged { Write(va, MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref value, 1))); } /// /// Writes data to GPU mapped memory. /// /// GPU virtual address to write the data into /// The data to be written public void Write(ulong va, ReadOnlySpan data) { WriteImpl(va, data, _context.PhysicalMemory.Write); } /// /// Writes data to GPU mapped memory without write tracking. /// /// GPU virtual address to write the data into /// The data to be written public void WriteUntracked(ulong va, ReadOnlySpan data) { WriteImpl(va, data, _context.PhysicalMemory.WriteUntracked); } private delegate void WriteCallback(ulong address, ReadOnlySpan data); /// /// Writes data to possibly non-contiguous GPU mapped memory. /// /// GPU virtual address of the region to write into /// Data to be written /// Write callback private void WriteImpl(ulong va, ReadOnlySpan data, WriteCallback writeCallback) { if (IsContiguous(va, data.Length)) { writeCallback(Translate(va), data); } else { int offset = 0, size; if ((va & PageMask) != 0) { ulong pa = Translate(va); size = Math.Min(data.Length, (int)PageSize - (int)(va & PageMask)); writeCallback(pa, data.Slice(0, size)); offset += size; } for (; offset < data.Length; offset += size) { ulong pa = Translate(va + (ulong)offset); size = Math.Min(data.Length - offset, (int)PageSize); writeCallback(pa, data.Slice(offset, size)); } } } /// /// Maps a given range of pages to the specified CPU virtual address. /// /// /// All addresses and sizes must be page aligned. /// /// CPU virtual address to map into /// GPU virtual address to be mapped /// Size in bytes of the mapping public void Map(ulong pa, ulong va, ulong size) { lock (_pageTable) { MemoryUnmapped?.Invoke(this, new UnmapEventArgs(va, size)); for (ulong offset = 0; offset < size; offset += PageSize) { SetPte(va + offset, pa + offset); } } } /// /// Unmaps a given range of pages at the specified GPU virtual memory region. /// /// GPU virtual address to unmap /// Size in bytes of the region being unmapped public void Unmap(ulong va, ulong size) { lock (_pageTable) { // Event handlers are not expected to be thread safe. MemoryUnmapped?.Invoke(this, new UnmapEventArgs(va, size)); for (ulong offset = 0; offset < size; offset += PageSize) { SetPte(va + offset, PteUnmapped); } } } /// /// Checks if a region of GPU mapped memory is contiguous. /// /// GPU virtual address of the region /// Size of the region /// True if the region is contiguous, false otherwise [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsContiguous(ulong va, int size) { if (!ValidateAddress(va) || GetPte(va) == PteUnmapped) { return false; } ulong endVa = (va + (ulong)size + PageMask) & ~PageMask; va &= ~PageMask; int pages = (int)((endVa - va) / PageSize); for (int page = 0; page < pages - 1; page++) { if (!ValidateAddress(va + PageSize) || GetPte(va + PageSize) == PteUnmapped) { return false; } if (Translate(va) + PageSize != Translate(va + PageSize)) { return false; } va += PageSize; } return true; } /// /// Gets the physical regions that make up the given virtual address region. /// /// Virtual address of the range /// Size of the range /// Multi-range with the physical regions /// The memory region specified by and is not fully mapped public MultiRange GetPhysicalRegions(ulong va, ulong size) { if (IsContiguous(va, (int)size)) { return new MultiRange(Translate(va), size); } if (!IsMapped(va)) { throw new InvalidMemoryRegionException($"The specified GPU virtual address 0x{va:X} is not mapped."); } ulong regionStart = Translate(va); ulong regionSize = Math.Min(size, PageSize - (va & PageMask)); ulong endVa = va + size; ulong endVaRounded = (endVa + PageMask) & ~PageMask; va &= ~PageMask; int pages = (int)((endVaRounded - va) / PageSize); var regions = new List(); for (int page = 0; page < pages - 1; page++) { if (!IsMapped(va + PageSize)) { throw new InvalidMemoryRegionException($"The specified GPU virtual memory range 0x{va:X}..0x{(va + size):X} is not fully mapped."); } ulong newPa = Translate(va + PageSize); if (Translate(va) + PageSize != newPa) { regions.Add(new MemoryRange(regionStart, regionSize)); regionStart = newPa; regionSize = 0; } va += PageSize; regionSize += Math.Min(endVa - va, PageSize); } regions.Add(new MemoryRange(regionStart, regionSize)); return new MultiRange(regions.ToArray()); } /// /// Checks if a given GPU virtual memory range is mapped to the same physical regions /// as the specified physical memory multi-range. /// /// Physical memory multi-range /// GPU virtual memory address /// True if the virtual memory region is mapped into the specified physical one, false otherwise public bool CompareRange(MultiRange range, ulong va) { va &= ~PageMask; for (int i = 0; i < range.Count; i++) { MemoryRange currentRange = range.GetSubRange(i); ulong address = currentRange.Address & ~PageMask; ulong endAddress = (currentRange.EndAddress + PageMask) & ~PageMask; while (address < endAddress) { if (Translate(va) != address) { return false; } va += PageSize; address += PageSize; } } return true; } /// /// Validates a GPU virtual address. /// /// Address to validate /// True if the address is valid, false otherwise private static bool ValidateAddress(ulong va) { return va < (1UL << AddressSpaceBits); } /// /// Checks if a given page is mapped. /// /// GPU virtual address of the page to check /// True if the page is mapped, false otherwise public bool IsMapped(ulong va) { return Translate(va) != PteUnmapped; } /// /// Translates a GPU virtual address to a CPU virtual address. /// /// GPU virtual address to be translated /// CPU virtual address, or if unmapped public ulong Translate(ulong va) { if (!ValidateAddress(va)) { return PteUnmapped; } ulong baseAddress = GetPte(va); if (baseAddress == PteUnmapped) { return PteUnmapped; } return baseAddress + (va & PageMask); } /// /// Gets the Page Table entry for a given GPU virtual address. /// /// GPU virtual address /// Page table entry (CPU virtual address) private ulong GetPte(ulong va) { ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask; ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask; if (_pageTable[l0] == null) { return PteUnmapped; } return _pageTable[l0][l1]; } /// /// Sets a Page Table entry at a given GPU virtual address. /// /// GPU virtual address /// Page table entry (CPU virtual address) private void SetPte(ulong va, ulong pte) { ulong l0 = (va >> PtLvl0Bit) & PtLvl0Mask; ulong l1 = (va >> PtLvl1Bit) & PtLvl1Mask; if (_pageTable[l0] == null) { _pageTable[l0] = new ulong[PtLvl1Size]; for (ulong index = 0; index < PtLvl1Size; index++) { _pageTable[l0][index] = PteUnmapped; } } _pageTable[l0][l1] = pte; } } }